SLOPSHOPPER

brain-dump

脑洞倾倒结构化转换器:拦住你语音/随手打的长篇凌乱 Prompt,先问 2-3 个关键问题,再重写成 The Task / The Why / Guardrails / What Done Looks Like 四段式放行

newbandcommandtoaststatusprompt
v0.2.1no licenseupdated 2026-10-23weizhongl/claude-code-mods/brain-dump
A shopper browsing a rack in a slop shop
Preview · a replayed session in a sandbox
claude · ~/work/app · brain-dump
› fix the failing auth test and add an audit log call ⏺ Read(src/auth.ts) ⎿ Read 6 lines ⏺ Update(src/auth.ts) ⎿ 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 › /dump ⎿ brain-dump: 现在没有等你回答的 Brain Dump。 ────────────────────────────────────────────────────────────────────────────────────────────────────────────────────── › ? for shortcuts
README

Claude Code Mods

十个给 Claude Code 用的 Mod。只能装在 Claude Code(终端和桌面 App 的 Code 页)。Codex、Anti-Gravity 没有 Mod 接口,装不了。

先装 switchboard(总控台),其余随意挑。

安装

终端里:

/plugin install switchboard --marketplace weizhongl/claude-code-mods

第一次会问 Add marketplace?,回 y,再选 user scope。其余 Mod 把名字换掉即可:usage-gauge、brain-dump、helper-desk、cache-clock、handoff、reflect、chat-inventory、screen-share、replay-theater。

在 shell 里:

claude plugin install switchboard --marketplace weizhongl/claude-code-mods

桌面 App(/plugin 在那里不能用):点输入框旁的 +,选 Plugins → Add marketplace,填 weizhongl/claude-code-mods,再逐个 Add plugin。

每个 Mod 怎么用

Mod怎么用注意
switchboard输入 /mods 打开总控台。每个 Mod 一行:On/Off、Hide UI Panel。输入框上方那一行条(额度、缓存倒计时、任务进度、👁 按钮)也是它画的:平时只占一行,别的 Mod 有事才在它旁边多出一行。条的右端有按钮:「收起」变成一小行(只剩几个百分比和倒计时),再点「隐藏」只留一个「📊 展开」按钮;也可以输入 /bar(或 /bar full、/bar mini、/bar off)切换,你的选择会记住。进度条在桌面版是细线,在终端里是小方块。也可以 /mods off 名字、/mods on 名字、/mods hide 名字、/mods show 名字。新放进同一个文件夹的 Mod 会自动出现。Off = 该 Mod 的钩子立刻停手;Hide = 后台照跑,只收起面板。
usage-gauge自动。显示在总控台那一行条里:对话上下文用了多少、5 小时(会话)额度、本周额度,各有短进度条、颜色(绿 / 黄 / 红)和重置时间,最后是本次对话花费。/gauge 看文字版。读的是引擎自己的数字(和状态栏同一份),零 Token。额度数字要等发过一条消息、引擎读到后才出现。
brain-dump直接打字或语音说一大段(默认 180 字以上、没有排版)。它会拦住,先问你 2–3 个问题;你在输入框直接回答(回「跳过」= 不补充),然后重写成 The Task / The Why / Guardrails / What Done Looks Like 再发给 Claude。输入框上方有 跳过提问 / 原话直接发 / 放弃 三个按钮,也可用 /dump skip、/dump raw、/dump cancel。消息开头加 >> 可以跳过拦截。会用到两次小的模型调用(提问用 haiku,重写用 sonnet)。
helper-desk自动。Claude 有待办清单时,一行条里显示「任务 n/m」和短进度条;Claude 派出子代理时,侧边栏出现 Helper Desk,每个子代理一张卡(进度、正在做什么、读入/输出 token、花费)。/helpers 手动打开。子代理花费是按公开标价估算;整段对话总花费取自引擎账本。零额外 Token。
cache-clock自动。第一条回复之后,一行条里显示「⏱ mm:ss」。点旁边的 Keep Warm 或输入 /keep warm,快冷之前会自动发一条极小的心跳续命。你发下一条真实消息,或连续保温满 2 小时,会自动关闭。拿不到服务器的缓存时间,所以倒计时是推算(订阅登入 60 分钟,API 5 分钟,并按真实缓存命中自动校正)。设置里的「缓存有效分钟数」可固定。
handoff对话累计到 250,000 token、或缓存变冷时,输入框上方提醒你。按「生成交接」或输入 /handoff,会写出一份详细的交接提示词(存在 ~/.claude/handoffs/,可直接发给同事),再按 Start Fresh Chat with Handoff 带去新对话。生成会花 Token,默认只提醒、不自动生成(设置里「触发后做什么」选 generate 才会自动生成)。桌面 App 不允许自动开新对话时,会改成复制到剪贴板。
reflect你在对话里说「以后永远不要…」「每次都要…」「记住…」这类话,输入框上方弹出小卡片,一键存进:内置 Memory / 项目 CLAUDE.md(没有就 AGENTS.md)/ 你指定的文件 / 仅本次忽略。只在句子里有规则关键词时才调用一次小模型判断。设置里可填默认的第二大脑文件路径。
chat-inventory自动。侧边栏「In this chat」:目标与待办、调用过的连接器(含次数)、用过的技能、读过和新建/修改过的文件(点文件在文件管理器里定位)。/inventory 手动打开。纯追踪,零 Token。
screen-share输入 /screen share 开或关,或点一行条右端的 👁 按钮。开启后,画面上的邮箱、电话、金额、API key、公司名、人名遮成灰条。只改画面,不改真实数据;不遮输入框里正在打的字。人名只遮设置里填的名单和「客户:xxx」「陈小姐」这类写法,没有真正的人名识别。
replay-theater自动录下 Claude 用 Edit / Write / NotebookEdit 对文件做的每次修改。输入 /replay 打开:选文件、播放/暂停、上一步/下一步、点步骤号跳转,看每一步的 diff。Revert 按钮按一次变成「再按一次确认」,第二次才会写回文件。/replay clear 清空。用 Bash 改文件(如 sed)录不到;单个文件超过 20 万字不录。记录只在当前会话。

目录

每个 Mod 一个文件夹:.claude-plugin/plugin.json、hooks/hooks.json、hooks/register.tsx,有状态的还有 types/index.d.ts。switchboard、screen-share、replay-theater 带自动测试(claude plugin test <文件夹>)。

各 Mod 里的 hooks/flag.ts 是同一小段代码(读总控台的开关),Mod 之间不能共享文件,所以每个放一份。

Source 4 files
hooks/register.tsx 247 lines
1import { atom, read, update } from 'claude-code'
2import type { EngineInterface, Register } from 'claude-code'
3
4import {
5  QUESTIONS_SYSTEM,
6  REWRITE_SYSTEM,
7  isBrainDump,
8  isSkip,
9  parseQuestions,
10} from './dump'
11import { flagOf } from './flag'
12
13const MOD = 'brain-dump'
14
15const stage = atom({ plugin: 'brain-dump', key: 'stage' } as const, 'idle')
16const original = atom({ plugin: 'brain-dump', key: 'original' } as const, '')
17const questions = atom({ plugin: 'brain-dump', key: 'questions' } as const, [])
18const since = atom({ plugin: 'brain-dump', key: 'since' } as const, 0)
19
20// A step that does not answer in time must never keep the person's messages out.
21const ASK_MS = 25_000
22const REWRITE_MS = 60_000
23const STALE_MS = 90_000
24
25let minChars = 180
26
27async function reset($: EngineInterface) {
28  await update($, stage, () => 'idle')
29  await update($, original, () => '')
30  await update($, questions, () => [])
31  $.ui.status(undefined)
32}
33
34async function within<T>($: EngineInterface, work: Promise<T>, ms: number): Promise<T> {
35  return Promise.race([
36    work,
37    $.clock.sleep(ms).then((): never => {
38      throw new Error(`超过 ${Math.round(ms / 1000)} 秒没有回应`)
39    }),
40  ])
41}
42
43async function enter($: EngineInterface, next: 'thinking' | 'rewriting') {
44  await update($, since, () => 0)
45  await update($, stage, () => next)
46  const at = await $.clock.now()
47  await update($, since, () => at)
48}
49
50// Nothing the person said is ever lost: any failure sends their own words on.
51async function sendRaw($: EngineInterface, text: string, why: string) {
52  await reset($)
53  $.ui.toast(why)
54  await $.prompt.submit({ text })
55}
56
57async function ask($: EngineInterface, text: string) {
58  try {
59    const reply = await within(
60      $,
61      $.model.complete({
62        model: 'haiku',
63        system: QUESTIONS_SYSTEM,
64        prompt: text,
65        maxTokens: 400,
66        timeoutMs: ASK_MS,
67      }),
68      ASK_MS + 5000,
69    )
70    const list = reply.isAnswered ? parseQuestions(reply.text) : []
71    if (list.length === 0) {
72      await sendRaw(
73        $,
74        text,
75        `Brain Dump 问不出问题${reply.isAnswered ? '' : `(${reply.reason})`},原样发给 Claude。`,
76      )
77
78      return
79    }
80    await update($, questions, () => list)
81    await update($, stage, () => 'asking')
82    $.ui.status('🧠 Brain Dump:等你回答问题')
83    $.ui.log(
84      [
85        '🧠 Brain Dump 先问你几个问题(你的原话已存好,还没发给 Claude):',
86        ...list.map((q, at) => `  ${at + 1}. ${q}`),
87        '',
88        '直接在下面回答就行,一口气讲完也可以。回「跳过」= 不补充,直接重写。',
89      ].join('\n'),
90    )
91  } catch (failure) {
92    await sendRaw($, text, `Brain Dump 出错(${failure instanceof Error ? failure.message : '未知'}),原样发给 Claude。`)
93  }
94}
95
96async function rewrite($: EngineInterface, dump: string, answers: string) {
97  await enter($, 'rewriting')
98  $.ui.status('🧠 Brain Dump:重写中…')
99  const asked = await read($, questions)
100  try {
101    const reply = await within(
102      $,
103      $.model.complete({
104        model: 'sonnet',
105        system: REWRITE_SYSTEM,
106        prompt: [
107          `<brain_dump>\n${dump}\n</brain_dump>`,
108          `<clarifications>\n${asked.map((q, at) => `Q${at + 1}: ${q}`).join('\n')}\nAnswers: ${answers === '' ? '(none given)' : answers}\n</clarifications>`,
109        ].join('\n\n'),
110        maxTokens: 3000,
111        timeoutMs: REWRITE_MS,
112      }),
113      REWRITE_MS + 5000,
114    )
115    if (!reply.isAnswered || reply.text.trim() === '') {
116      await sendRaw($, answers === '' ? dump : `${dump}\n\n补充:${answers}`, 'Brain Dump 重写失败,把你的原话和补充一起发给 Claude。')
117
118      return
119    }
120    const text = reply.text.trim()
121    $.ui.log(`🧠 Brain Dump 重写后的指令(已发给 Claude):\n\n${text}`)
122    await reset($)
123    await $.prompt.submit({ text })
124  } catch (failure) {
125    await sendRaw($, `${dump}\n\n补充:${answers}`, `Brain Dump 出错(${failure instanceof Error ? failure.message : '未知'}),原话已发出。`)
126  }
127}
128
129async function skipQuestions($: EngineInterface) {
130  const dump = await read($, original)
131  if (dump !== '') void rewrite($, dump, '')
132}
133
134async function sendOriginal($: EngineInterface) {
135  const dump = await read($, original)
136  if (dump !== '') await sendRaw($, dump, '已原样发给 Claude。')
137}
138
139export const register: Register = (on, options) => {
140  minChars = Math.max(40, Number(options.minChars ?? 180))
141
142  on('session.start', async ($, e, next) => {
143    await $.command.register({
144      name: 'dump',
145      description: 'Brain Dump 控制:skip 跳过提问 · raw 原样发送 · cancel 放弃',
146      argumentHint: 'skip | raw | cancel',
147    })
148    // A reload ends any step that was running: hand the person's words on.
149    const state = await read($, stage)
150    const dump = await read($, original)
151    if ((state === 'thinking' || state === 'rewriting') && dump !== '') {
152      void sendRaw($, dump, 'Brain Dump 重新加载了,已把你上一段话原样发出。')
153    } else if (state === 'thinking' || state === 'rewriting') {
154      await reset($)
155    }
156
157    return next(e)
158  })
159
160  on('command.run', { command: 'dump' }, async ($, e) => {
161    const verb = e.args.trim()
162    const state = await read($, stage)
163    if (state === 'idle') return { text: '现在没有等你回答的 Brain Dump。' }
164    if (verb === 'skip') {
165      void skipQuestions($)
166
167      return { text: '好,跳过提问,直接重写。' }
168    }
169    if (verb === 'raw') {
170      void sendOriginal($)
171
172      return { text: '好,原话直接发。' }
173    }
174    if (verb === 'cancel') {
175      await reset($)
176
177      return { text: '已放弃这次 Brain Dump。' }
178    }
179
180    return { text: '用法:/dump skip(跳过提问直接重写)· /dump raw(原话直接发)· /dump cancel(放弃)。输入框上方的按钮更快。' }
181  })
182
183  on('prompt.submit', async ($, e, next) => {
184    if (!(await flagOf($, MOD)).isOn) return next(e)
185    if (e.isMeta === true || e.origin.kind !== 'composer') return next(e)
186
187    const text = e.text.trim()
188    const state = await read($, stage)
189
190    if (e.command !== undefined) return next(e)
191    if (text.startsWith('>>')) return next({ ...e, text: text.slice(2).trim() })
192
193    if (state === 'asking') {
194      const dump = await read($, original)
195      void rewrite($, dump, isSkip(text) ? '' : text)
196
197      return { drop: '🧠 收到你的回答,正在整理成结构化指令…' }
198    }
199    if (state === 'thinking' || state === 'rewriting') {
200      const age = (await $.clock.now()) - (await read($, since))
201      if (age > STALE_MS) {
202        const dump = await read($, original)
203        void sendRaw($, dump === '' ? text : dump, 'Brain Dump 卡住了,已把你上一段话原样发出。')
204
205        return { drop: '🧠 Brain Dump 之前卡住了,已把你上一段话原样发出;你刚打的这句请再发一次。' }
206      }
207
208      return { drop: '🧠 Brain Dump 还在处理上一条,最多再等一会儿(/dump cancel 可放弃)。' }
209    }
210    if (e.attachments !== undefined && e.attachments.length > 0) return next(e)
211    if (!isBrainDump(text, minChars)) return next(e)
212
213    await update($, original, () => text)
214    await enter($, 'thinking')
215    $.ui.status('🧠 Brain Dump:分析中…')
216    void ask($, text)
217
218    return { drop: '🧠 Brain Dump 接住了这段长话(还没发给 Claude),先整理几个问题…' }
219  }).catch(($, e, next) => next(e))
220
221  on('ui.render', { component: 'AbovePrompt' }, async ($, e, next) => {
222    const below = await next(e)
223    const flag = await flagOf($, MOD)
224    const state = await read($, stage)
225    if (!flag.isOn || flag.isHidden || state === 'idle') return below
226
227    const { Box, Text, Button } = $.ui.resolve(e)
228
229    return (
230      <Box flexDirection="column">
231        {below}
232        <Box columnGap={1} flexWrap="wrap">
233          <Text color="#6fa8e0">●</Text>
234          {state === 'thinking' && <Text>Brain Dump 正在想要问你什么…</Text>}
235          {state === 'rewriting' && <Text>Brain Dump 正在重写成四段式指令…</Text>}
236          {state === 'asking' && <Text>Brain Dump 在等你回答上面的问题</Text>}
237          {state === 'asking' && (
238            <Button key="skip" label="跳过提问" variant="primary" onPress={() => skipQuestions($)} />
239          )}
240          {state === 'asking' && <Button key="raw" label="原话直接发" onPress={() => sendOriginal($)} />}
241          <Button key="cancel" label="放弃" onPress={() => reset($)} />
242        </Box>
243      </Box>
244    )
245  })
246}
247
hooks/dump.ts 49 lines
1// Pure: what counts as a brain dump, and the two prompts that tame it.
2
3const SKIPS = new Set(['跳过', '不用', '没有', '没了', '直接做', '直接重写', 'skip', 'no', 'none', 'nothing'])
4
5export const isSkip = (text: string) => SKIPS.has(text.trim().toLowerCase())
6
7// Long, and without the structure of something already written with care.
8export const isBrainDump = (text: string, minChars: number): boolean => {
9  if (text.length < minChars) return false
10  if (text.startsWith('/') || text.startsWith('!')) return false
11  if (text.includes('```')) return false
12  if (/The Task/i.test(text) && /Guardrails/i.test(text)) return false
13  const lines = text.split('\n').filter(line => line.trim() !== '')
14  const listed = lines.filter(line => /^\s*([-*•]|\d+[.)、]|#{1,4}\s)/.test(line)).length
15
16  return listed < 3
17}
18
19export const QUESTIONS_SYSTEM =
20  "The user dictated a messy, rambling prompt for an AI assistant. You are the editor who must get it ready. " +
21  "Ask the 2 or 3 clarifying questions whose answers would change the result most (a missing goal, audience, hard constraint, deliverable or format). " +
22  "Never ask what the text already answers. Keep each question short and concrete, answerable in one sentence. " +
23  "Write in the same language as the text. Output a JSON array of strings only, no prose."
24
25export const REWRITE_SYSTEM =
26  'You turn a messy brain-dump (plus the user\'s answers to clarifying questions) into one precise prompt for an AI assistant. ' +
27  'Write in the same language as the brain-dump. Use exactly these four markdown sections, in this order:\n' +
28  '## The Task(核心任务): what to do, in plain imperative sentences.\n' +
29  '## The Why(背景与目的): the context and the reason, so the assistant can judge trade-offs.\n' +
30  '## Guardrails(限制与护栏): hard limits, formats, things that must not happen, what not to touch.\n' +
31  '## What Done Looks Like(完成标准): the deliverable and how to tell it is good enough.\n' +
32  'Rules: keep every concrete detail (names, numbers, paths, URLs, quotes) exactly as given; fix typos and speech disfluencies; ' +
33  'merge the answers in; do not invent requirements; where something is still unknown, write it as an explicit assumption beginning with "假设:" (or "Assumption:"). ' +
34  'Output the prompt only, with no preface and no closing remarks.'
35
36export const parseQuestions = (text: string): string[] => {
37  const match = text.match(/\[[\s\S]*\]/)
38  if (match === null) return []
39  try {
40    const list = JSON.parse(match[0]) as unknown
41
42    return Array.isArray(list)
43      ? list.filter((one): one is string => typeof one === 'string' && one.trim() !== '').slice(0, 3)
44      : []
45  } catch {
46    return []
47  }
48}
49
hooks/flag.ts 14 lines
1import type { EngineInterface } from 'claude-code'
2
3const FLAGS = { plugin: 'switchboard', key: 'flags' } as const
4
5export type Flag = { isOn: boolean; isHidden: boolean }
6
7// Switchboard owns the flags. A mod it has never heard of counts as on, shown.
8export async function flagOf($: EngineInterface, mod: string): Promise<Flag> {
9  const { value } = await $.state.get(FLAGS)
10  const own = value?.[mod]
11
12  return { isOn: own?.isOn ?? true, isHidden: own?.isHidden ?? false }
13}
14
types/index.d.ts 14 lines
1declare module 'claude-code' {
2  interface PluginState {
3    'brain-dump': {
4      stage: 'idle' | 'thinking' | 'asking' | 'rewriting'
5      original: string
6      questions: string[]
7      // when the current step began (ms); a step older than a minute and a half is stale
8      since: number
9    }
10    // Read-only view of Switchboard's value (it alone writes it).
11    switchboard: { flags: Record<string, { isOn: boolean; isHidden: boolean }> }
12  }
13}
14