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

Claude Code mods (function-hook plugins) for working across many repos and many sessions at once — plus a pet.
| Mod | What it does |
|---|---|
| where-am-i | A band above the prompt: 📁 repo 🌿 branch ↑2 ± 3 changed 🎯 task. /task <text> names what this session is doing. |
| done-ping | A 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-guard | Blocks 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-pet | A 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-mood | Claude's mood in the status line: 🤔 reading, ✍️ writing, 🧪 testing, 😤 after a few errors, 😌 when done — with 📖 ✏️ ⚡ 💥 counters. |
| control-tower | A 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. |
| tldr | After every long answer, a dim 💡 TL;DR line (Haiku) in the transcript, so a session you switch back to reads at a glance. |
| sdk-sync | Release 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. |
| fortune | Programmer jokes after the working spinner (Sauteing… 🎲 删库跑路前,记得先 git push。); the day's first prompt shows 今日运势 (/fortune). Fridays never deploy. |
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.
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>hooks/register.ts 146 lines1import 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}
146types/index.d.ts 8 lines1export type Joke = string
2
3declare module 'claude-code' {
4 interface PluginState {
5 fortune: { joke: Joke }
6 }
7}
8