SLOPSHOPPER

fortune

Programmer jokes after the working spinner, and a daily fortune (/fortune) — 今日宜重构,忌周五上线

newspinnercommandtoastprompttimer
v0.3.0no licenseupdated 2026-10-09YuehengHan/my-claude-mods/plugins/fortune
A shopper browsing a rack in a slop shop
Preview · a replayed session in a sandbox
claude · ~/work/app · fortune
› fix the failing auth test and add an audit log call ╭────────────────────────────────────────────╮ │ fortune │ ⏺ Read(src/auth.ts) │ 🔮 今日运势 ★☆☆☆☆ 宜:code review、提 PR 忌:改 │ ⎿ Read 6 lines │ shared-proto、force push 幸运语言:TypeScript · │ ⏺ Update(src/auth.ts) │ 幸运 commit 时间:19:00 │ ⎿ Added 2 lines, removed 1 line ╰────────────────────────────────────────────╯ ⏺ Bash(bun test) ⎿ 3 pass, 1 fail ● Done. refresh now rejects expired claims and logs an audit event. ✻ Worked for 42s · done 4:20 PM › /fortune ⎿ fortune: 🔮 今日运势 ★☆☆☆☆ ⎿ fortune: 宜:code review、提 PR ⎿ fortune: 忌:改 shared-proto、force push ⎿ fortune: 幸运语言:TypeScript · 幸运 commit 时间:19:00 ────────────────────────────────────────────────────────────────────────────────────────────────────────────────────── › ? for shortcuts
README

my-claude-mods

Claude Code mods (function-hook plugins) for working across many repos and many sessions at once — plus a pet.

ModWhat it does
where-am-iA band above the prompt: 📁 repo 🌿 branch ↑2 ± 3 changed 🎯 task. /task <text> names what this session is doing.
done-pingA macOS notification (repo, duration, your prompt) with a sound per kind: 🔔 Glass when a turn over 30 s is done, Basso when it failed, Submarine when Claude is waiting on you. Change them in SOUNDS.
repo-guardBlocks push --force, pushes to main/master, reset --hard, clean -f, checkout ., branch -D, stash clear, rm -rf ~. Asks before Claude first edits a repo other than the session's own.
claude-petA pet that earns xp from your work, grows through five stages and unlocks achievements. Pick a species (/pet species: chick, cat, dog, dragon, dino, ocean, bug, plant, robot, moon) or your own emojis (/pet emoji 🦊, /pet emoji 🥚 🐣 🦊 🐺 🐉). Lives in the status line (🦮 xixi (・_・)📖 Lv7 ▰▱▱▱▱▱) with 18 moods that follow Claude: 💭 thinking, 📖 reading, ✍️ writing, ⚡ running, 🧪 testing, 🌐 browsing, 📣 delegating, ✋ waiting for you, 🔥 on a 25-call streak, 🌙 past 1am, 🎉 tests pass, 📦 commit/push, (×_×) error, (╬ಠ益ಠ) three in a row, bored after 5 idle minutes, asleep after 15. /pet for stats, /pet name <name>.
claude-moodClaude's mood in the status line: 🤔 reading, ✍️ writing, 🧪 testing, 😤 after a few errors, 😌 when done — with 📖 ✏️ ⚡ 💥 counters.
control-towerA row under where-am-i's, shown only while another session waits for you (a permission or a question): ✋ backend-ng 在等你 2m [tower]. /tower (or the button) opens a pane listing every Claude session on this machine, named repo · branch for worktrees.
tldrAfter every long answer, a dim 💡 TL;DR line (Haiku) in the transcript, so a session you switch back to reads at a glance.
sdk-syncRelease audit for the SpatialReal SDKs. Never acts while you work: when a new v* tag appears it reminds you once; /release-check <repo> [tag] gathers the facts (public API files changed, CHANGELOG entry, commits in each sibling SDK / docs / examples since the release, version pins) and asks Claude for a read-only audit table. /release-check status lists unchecked releases; --facts skips the audit. Graph: plugins/sdk-sync/hooks/sdk-map.ts.
fortuneProgrammer jokes after the working spinner (Sauteing… 🎲 删库跑路前,记得先 git push。); the day's first prompt shows 今日运势 (/fortune). Fridays never deploy.

Install

At a Claude Code terminal prompt:

/plugin marketplace add YuehengHan/my-claude-mods
/plugin install where-am-i@my-claude-mods
/plugin install done-ping@my-claude-mods
/plugin install repo-guard@my-claude-mods
/plugin install claude-pet@my-claude-mods
/plugin install claude-mood@my-claude-mods

Pick the user scope so they load in every session. Install only the ones you want.

Developing

Add a local clone as the marketplace instead, and Claude Code reads the plugins straight from the folder:

claude plugin marketplace add ~/Desktop/my-claude-mods
claude plugin install where-am-i@my-claude-mods --scope user

Claude Code runs an installed copy, so after editing a mod bump its version in plugin.json, then:

claude plugin marketplace update my-claude-mods
claude plugin update <mod>@my-claude-mods

and run /reload-plugins (or restart) in each open session.

Check and test a mod:

claude plugin validate plugins/<mod>
claude plugin test plugins/<mod>
Source 2 files
hooks/register.ts 146 lines
1import type { EngineInterface, Register } from 'claude-code'
2
3export const JOKES: readonly string[] = [
4  '程序员最讨厌的两件事:写文档,和别人不写文档。',
5  '代码能跑就别动它——跑不起来也别动,可能是它在思考。',
6  '“这个 bug 很简单” —— 三天前的我',
7  '世界上只有 10 种人:懂二进制的和不懂的。',
8  '我的代码没有 bug,只有未被文档化的特性。',
9  '删库跑路前,记得先 git push。',
10  'It works on my machine. 那就把你的机器发给用户。',
11  '两个难题:缓存失效、命名,以及差一错误。',
12  '注释写着“临时方案”,git blame 显示 2019 年。',
13  '产品经理:这个需求很简单,就加个按钮。',
14  '递归的定义:见“递归”。',
15  '周五下午的 deploy,周六早上的 incident。',
16  '我不是在摸鱼,我在等 Claude 编译我的想法。',
17  'TODO: 写完这个 TODO。',
18  '测试全绿的那一刻,人生达到了巅峰。',
19  'Claude 正在读你三年前写的代码,请保持冷静。',
20  '最好的代码是没写的代码,其次是 Claude 写的代码。',
21  'merge conflict 是两个平行宇宙在争夺现实。',
22  'console.log 是最朴实无华的调试器。',
23  '“我就改一行” —— 改动 47 个文件',
24  '有些 bug 只在演示的时候出现,这叫观察者效应。',
25  '先让它跑起来,再让它跑对,最后让它跑快。然后重写。',
26  'Ctrl+C Ctrl+V 是生产力,Ctrl+Z 是后悔药。',
27  '真正的全栈:从 CSS 居中到 GPU 显存爆炸。',
28  '没有什么问题是重启解决不了的,如果有,就重启两次。',
29  'README 写着“开箱即用”,箱子是焊死的。',
30  'AI 不会取代程序员,但会取代不用 AI 的程序员(和他们的周末加班)。',
31  '数字人会眨眼了,我的眼睛还在 debug。',
32]
33
34const GOOD: readonly string[] = [
35  '重构', '写测试', '提 PR', '删死代码', '更新文档', '升级依赖', '和产品经理喝咖啡',
36  'code review', '早点下班', '写 CHANGELOG', '对齐四端 SDK', '清理 TODO', '跑 benchmark', '补类型',
37]
38const BAD: readonly string[] = [
39  '上线', 'force push', '改 shared-proto', '周会发言', '相信“很简单”', '直接改 main',
40  '跳过测试', '半夜部署', '改别人的代码不说', '“顺手”改一下', '升级大版本', '动生产数据库',
41]
42const LANGS: readonly string[] = ['TypeScript', 'Kotlin', 'Swift', 'Python', 'Go', 'C++', 'Rust', 'SQL', 'Bash', 'Protobuf']
43
44/** A small stable hash so a day's fortune is the same all day, in every session. */
45export function hash(text: string): number {
46  let h = 2166136261
47  for (let i = 0; i < text.length; i += 1) {
48    h ^= text.charCodeAt(i)
49    h = Math.imul(h, 16777619)
50  }
51  return h >>> 0
52}
53
54function pick<T>(list: readonly T[], seed: number, n: number): T[] {
55  const out: T[] = []
56  for (let i = 0; out.length < n && i < list.length * 4; i += 1) {
57    const item = list[hash(`${seed}:${i}`) % list.length] as T
58    if (!out.includes(item)) out.push(item)
59  }
60  return out
61}
62
63export type Fortune = { stars: number; good: string[]; bad: string[]; lang: string; hour: number }
64
65export function fortuneOf(day: string, weekday: number): Fortune {
66  const seed = hash(day)
67  const good = pick(GOOD, seed, 2)
68  const bad = pick(BAD, seed + 1, 2)
69  // Friday never deploys.
70  if (weekday === 5 && !bad.includes('上线')) bad[0] = '上线'
71  return {
72    stars: 1 + (seed % 5),
73    good,
74    bad,
75    lang: LANGS[seed % LANGS.length] as string,
76    hour: 9 + (seed % 12),
77  }
78}
79
80export function formatFortune(f: Fortune): string {
81  return [
82    `🔮 今日运势 ${'★'.repeat(f.stars)}${'☆'.repeat(5 - f.stars)}`,
83    `   宜:${f.good.join('、')}`,
84    `   忌:${f.bad.join('、')}`,
85    `   幸运语言:${f.lang} · 幸运 commit 时间:${f.hour}:00`,
86  ].join('\n')
87}
88
89let rotation: { cancel: () => void } | null = null
90let lastFortuneDay = ''
91
92async function today($: EngineInterface): Promise<{ day: string; weekday: number }> {
93  const d = new Date(await $.clock.now())
94  return { day: `${d.getFullYear()}-${d.getMonth() + 1}-${d.getDate()}`, weekday: d.getDay() }
95}
96
97const JOKE = { plugin: 'fortune', key: 'joke' } as const
98
99async function joke($: EngineInterface) {
100  const n = Math.floor((await $.clock.now()) / 10_000)
101  await $.state.set(JOKE, `🎲 ${JOKES[hash(String(n)) % JOKES.length]}`)
102}
103
104export const register: Register = on => {
105  on('session.start', async ($, e, next) => {
106    await $.command.register({ name: 'fortune', description: '🔮 Today\'s coding fortune', immediate: true })
107    lastFortuneDay = String((await $.store.get('lastFortuneDay')) ?? '')
108    return next(e)
109  })
110
111  on('command.run', { command: 'fortune' }, async $ => {
112    const { day, weekday } = await today($)
113    return { text: formatFortune(fortuneOf(day, weekday)) }
114  })
115
116  on('prompt.submit', async ($, e, next) => {
117    // The day's first prompt, across all sessions, gets the fortune.
118    const { day, weekday } = await today($)
119    if (day !== lastFortuneDay) {
120      lastFortuneDay = day
121      await $.store.set('lastFortuneDay', day)
122      $.ui.toast(formatFortune(fortuneOf(day, weekday)), { timeoutMs: 10_000 })
123    }
124    rotation?.cancel()
125    void joke($)
126    rotation = $.clock.every(10_000, () => void joke($))
127    return next(e)
128  })
129
130  on('turn.complete', async ($, e, next) => {
131    if (e.agentId === undefined) {
132      rotation?.cancel()
133      rotation = null
134      await $.state.set(JOKE, '')
135    }
136    return next(e)
137  })
138
139  on('ui.render', { component: 'Spinner' }, async ($, e, next) => {
140    const { value: line = '' } = await $.state.get(JOKE)
141    if (line === '') return next(e)
142    // After the spinner's own words, never in their place.
143    return next({ ...e, props: { ...e.props, suffix: `…  ${line}` } })
144  })
145}
146
types/index.d.ts 8 lines
1export type Joke = string
2
3declare module 'claude-code' {
4  interface PluginState {
5    fortune: { joke: Joke }
6  }
7}
8