An amber pill above the prompt while Claude is waiting on you: open it for each question or command, with a copy button.

Hooks, tools, one skill, and a set of mods for Claude Code. The hooks and tools cover the four things that quietly cost the most: a memory store that rots, subagent fan-out that eats a usage window, bulk work sent to an expensive model, and an agent pushing to a repo it should not.
Zero dependencies, plain Node (18+), ESM. The mods run inside Claude Code itself.
On macOS, install the two prerequisites first. agentcall is the one bash script here and it needs bash 4+; macOS ships 3.2, where expanding an empty array under set -u is a fatal error. Two adapters parse JSON output with jq. Everything else is Node and works as-is.
brew install bash jq
git clone https://github.com/VibeCodyH/claude-craft
cd claude-craft
node install.mjs --dry-run # show the plan
node install.mjs # copy the tools, merge the hook wiring
The installer reads your existing ~/.claude/settings.json, adds only the hook entries that are missing, and writes one timestamped backup before its first change. Re-running it is a no-op. If the file is not valid JSON it stops and tells you, rather than replacing config it cannot read.
Install a subset by name, and remove the wiring the same way:
node install.mjs --list
node install.mjs memory delegation
node install.mjs --uninstall rotate
Restart Claude Code afterward to pick up the hooks.
| Component | Runs as | Does |
|---|---|---|
memory | hooks + CLI | Lint, recall, and staleness detection for the auto-memory store. |
delegation | hooks | Caps subagent fan-out; nudges mechanical sweeps toward a cheaper executor. |
agentcall | CLI | One calling convention for every agent CLI you have installed. |
crunch | CLI + library | Bulk LLM lane: strongest free cloud model first, your own box as the floor. |
protected-repos | hook | Blocks pushes and PRs on repos where a human runs every push. |
rotate | hooks | Swaps logins at a usage threshold, via swappy. |
skills | skill | delegate: when to hand work to an executor, and how. |
mods | /plugin install, see Mods | Bands and panes in Claude Code's own UI: blast radius, prod tripwire, PR tracker, usage limits. |
Claude Code's auto-memory is a directory of markdown files plus a MEMORY.md index. It's great — until it rots. Links dangle after renames, the index drifts from the files, near-duplicate memories pile up, and a memory that pins "how deploy.sh works" goes silently stale the day deploy.sh changes. And even a healthy store only helps if the agent actually recalls the right memory before acting on a fresh (worse) idea.
| Tool | Runs as | Does |
|---|---|---|
memory-lint.mjs | manual / CI | Reports dangling [[wikilinks]], index drift (broken + orphaned entries), and likely duplicates. |
memory-preflight.mjs | UserPromptSubmit hook | Injects strongly-relevant prior memories into context when the user's prompt is on-topic. |
memcheck.mjs | invoked by the agent | Scores a proposed approach against the store, so a recorded ★decision outranks a fresh idea. |
memory-staleness.mjs | SessionStart hook | Recomputes hashes for file-pinned memories and reports only drift. |
memory-lib.mjs | library | Shared parser + scorer the rest import. |
The preflight/memcheck pair matters: a prompt hook only sees the user's words, so it can't catch a mistake born from the agent's own plan. memcheck is the other half — tell your agent (in CLAUDE.md) to run it against its own approach before committing to one.
# lint the current project's store (or any dir)
node ~/.claude/tools/memory-lint.mjs
node ~/.claude/tools/memory-lint.mjs --json --dir path/to/memory
# self-check an approach before pitching it
node ~/.claude/tools/memcheck.mjs "rewrite the dark mode cards to use a new surface color"
# pin a memory to file contents, then let SessionStart flag drift
node ~/.claude/tools/memory-staleness.mjs --bind src/deploy.sh
# - /abs/path/src/deploy.sh@1a2b3c4d5e6f <- paste into the memory under `binds:`
Both hooks are fail-safe: on any error they print nothing and exit 0. They will never block a prompt or a session.
Memory-repo benchmarks (LoCoMo, LongMemEval) measure QA accuracy over long chat histories — the wrong question for a hook that injects context into a coding agent. What actually matters there is: does it surface the right memory when the prompt is on-topic, and stay silent when it isn't? So this repo ships a labeled eval instead: a synthetic 18-memory store and 36 developer-realistic queries (24 on-topic incl. hard paraphrases, 12 off-topic), run through the exact shipped gating code:
| Tier | Recall | False-fire |
|---|---|---|
preflight (hook — every prompt) | 14/24 (58%) | 0/12 (0%) |
memcheck (agent-invoked) | 20/24 (83%) | 1/12 (8%) |
The asymmetry is the design: the hook fires on every prompt, so it is tuned to never inject noise; memcheck is asked for deliberately, so it trades a little noise for recall. The misses are paraphrase-heavy queries with few shared tokens — the known ceiling of a lexical scorer, and the price of zero dependencies. Reproduce with npm run bench (--verbose lists every miss); CI floors in the test suite pin false-fire at 0 and recall above 50%/75%.
One memory per file, frontmatter with name / description / metadata.type (user | feedback | project | reference), [[wikilinks]] between memories, and a MEMORY.md index of - Title — hook lines. A MEMORY-cold-index.md is treated as a second, deliberately-not-resident index tier. feedback/project memories get a scoring boost — they carry decisions.
Hash-pinned binds — a memory that describes a specific file can pin it:
binds:
- /abs/path/to/file@1a2b3c4d5e6f # first 12 hex of sha256(contents)
Shared team store — point memcheck at a git repo of memories your team maintains (--team <dir> or CLAUDE_TEAM_MEMORY_DIR). Results are tagged [local] / [team] / [both]; mirrored entries collapse with the team copy winning. Two engineers' Claude instances sharing one curated store of platform facts works better than it has any right to.
Domain acronyms — the scorer drops tokens under 4 chars except a keep-list (llm ssh api db …). Extend it for your domain: CLAUDE_MEM_KEEP_SHORT="k8s rds sqs".
Two failures cost real money, and both are reflexes rather than decisions. The first: a task looks parallel, so the agent spawns five subagents, each starting cold and re-deriving context the parent already had. The second: the agent hand-edits forty files itself, one at a time, while an executor CLI sits idle on its own quota.
delegation-guard.mjs (PreToolUse on Agent and Workflow) puts two hard caps on the first, in a rolling window:
The premium cap is the one that earns its keep. A spawn with no explicit model on the default agent type inherits the parent's model, which is normally the most expensive one you run, so an unqualified spawn counts as premium. Named specialist types with no model are exempt, because they carry their own default. A refused spawn is not recorded, so being stopped never extends the window.
delegation-nudge.mjs (PostToolUse on edits) handles the second. When the main loop touches 3 distinct files in 5 minutes, it prints a ready-to-run executor command for the rest of the sweep. It never blocks. It stays silent inside any repo listed in protected-repos.json, on the theory that a repo you must push by hand is a repo whose edits you want to have read yourself.
Every threshold is an environment variable. Defaults in parentheses:
CLAUDE_DELEGATION_CAP (4) CLAUDE_NUDGE_FILES (3)
CLAUDE_DELEGATION_PREMIUM_CAP (2) CLAUDE_NUDGE_WINDOW_MIN (5)
CLAUDE_DELEGATION_WINDOW_MIN (10) CLAUDE_NUDGE_COOLDOWN_MIN (8)
CLAUDE_PREMIUM_MODELS (opus,fable) CLAUDE_NUDGE_SKIP (colon-separated paths)
CLAUDE_WORKFLOW_POLICY (ask) CLAUDE_NUDGE_CMD (codex)
CLAUDE_EXECUTORS (named in the refusal message)
CLAUDE_WORKFLOW_POLICY=ask is the default because a workflow's own agent() calls fan out subagents, and a stock research workflow can spawn dozens of them before anyone notices. Set it to deny or allow once you know which workflows you run.
Every agent CLI has a different flag for "yes, you may use your tools" and a different way of emitting its final answer. Getting one of those wrong does not produce an error. It produces a confident review of a diff the model never opened. bin/agentcall normalizes them:
agentcall codex -d ~/src/app -m ro < review-prompt.md # same shape for every agent
agentcall --probe # which CLIs are installed and authenticated
agentcall --list # what each is good for, and its known traps
agentcall --print-command grok -d . "hi" # inspect the invocation, run nothing
agentcall --new mytool # scaffold an adapter for a new CLI
It ships adapters for codex, claude, grok, kimi, vibe, glm, agy, opencode, copilot, and coderabbit. Each one encodes a trap found by hitting it:
-o FILE. One 4-file PR produced 152KB.-p is --single and consumes the next argument, so the prompt has to go last. Without --always-approve its tool calls are cancelled silently, and the partial narration reads exactly like a short bad review.--auto, with the same failure mode.type:"complete" record.--print-command exists because every one of those bugs was invisible until something downstream looked wrong. A new CLI is a drop-in file at ~/.config/agentcall/agents.d/<name>.sh, not a patch to the script. A drop-in whose name matches a builtin overrides it, which is how you fix a recipe that upstream changed.
The delegate skill in skills/ is the policy layer over all of this: what to hand off, what to keep, how to write the spec, and how to review what comes back.
A lot of what gets pushed through a coding agent is not reasoning. It is summarize this transcript, classify these 400 rows, extract fields from this dump. That work wants a cheap model, and several vendors give away a good one. crunch walks them in order and falls through to a local OpenAI-compatible server (Ollama, LM Studio, llama.cpp, vLLM) when every cloud tier is missing, capped, or down.
crunch "write a one-line commit message for: added retry backoff"
git log --oneline -50 | crunch --task summarize
crunch --task classify --json "<text>"
crunch --local "<sensitive text>" # your box only, nothing leaves the machine
crunch --models cerebras # list the model ids a provider really serves
Chain: openrouter → cerebras → groq → zai → mistral → cloudflare → local. A provider with no key is skipped, so one key is enough to start. A provider that returns 429 is skipped for the rest of the run rather than costing a round trip on every remaining call of a bulk job. Every attempt logs to stderr, so a chain that is quietly landing on the local floor looks different from one that is working. Copy crunch.env.example to ~/.config/claude-craft/crunch.env and fill in what you have.
Free model slugs rotate. A slug that stops being free returns 404, which cascades to the next provider instead of failing loudly, so a chain can quietly get slower without breaking. Run crunch --models openrouter | grep ':free' and update CRUNCH_OPENROUTER_MODEL when that happens. GitHub Models was in this chain until 2026-08-07 and is not any more: its endpoint now answers 410 github_models_retirement_brownout. It was the best entry while it lasted, because it reused your gh auth login token and needed no key.
On the redactor. Input is scrubbed for credential shapes before any cloud request: API keys, tokens, JWTs, private key blocks, connection strings, and NAME=value pairs whose name says credential. It is not a PII scrubber and will not catch names, emails, addresses, or account numbers. There is a test asserting that, so the limit stays honest. For real PII use --local, which skips both the cloud chain and the redactor, because scrubbing would gut the data you kept local in order to work on.
Point CRUNCH_SCRUB_MODULE at your own ESM module exporting scrub(text) -> { text, redactions } to substitute a better one. If it fails to load or throws, crunch falls through to the local floor rather than sending text the intended guard never inspected.
Importable as a library from the checkout: import { crunch } from "<checkout>/tools/crunch.mjs". The installer puts the CLI on your PATH and deliberately does not keep a second copy under ~/.claude/tools, because two copies of the same file drift apart.
tools/protected-repo-guard.mjs enforces "the agent may work on this clone locally but NEVER pushes it" — useful when Claude works on an employer's or teammate's repo where a human must run every push and open every PR. A prose rule in CLAUDE.md can be argued with; a PreToolUse deny can't. It catches git push (by cwd, cd chains, -C, slug, or URL), gh pr create|merge, gh repo sync, and gh api writes — while leaving local commits, branches, and the issue/comment lane open.
Configure ~/.claude/protected-repos.json:
[{ "slug": "some-org/their-repo", "dir": "/abs/path/to/local/clone" }]
Fail-open by design: no config or a broken config protects nothing rather than bricking every Bash call. The guard is belt-and-suspenders over your prose rule, not a replacement for it.
tools/swappy-auto-rotate.mjs closes the loop on multi-account usage limits: when either subscription window (5-hour or 7-day) crosses a threshold (default 95%), a Stop hook rotates you to your next saved login automatically and sends a desktop notification. The swap itself is done by swappy — PiXeL16's excellent minimal Claude Code login rotator. Install and set that up first (swappy save <name> per account); this hook is just the trigger finger.
Three things make the design work:
rate_limits percentages every turn. Persist them from your statusline command script: // statusline receives the payload as JSON on stdin — add:
if (payload.rate_limits) {
fs.writeFileSync(path.join(os.homedir(), ".claude", "usage-snapshot.json"),
JSON.stringify({ ts: Date.now(), rate_limits: payload.rate_limits }));
}
If you rotate logins, also stamp which account the reading came from — the payload has no account field, so read oauthAccount.accountUuid out of ~/.claude.json and add it as acct. A session that has not made an API call since a swap keeps re-rendering the account it was on, and that snapshot is fresh by ts while being wrong about whose usage it is. With acct present the hook skips those runs instead of filing them under the wrong profile. It stays optional: no stamp means the old trust-the-snapshot behavior.
swappy next can move you from an account with hours of 5-hour headroom onto one that is 100% capped and kill the turn. (Observed: work at 7d 96% but 5h 9%, rotated onto a 5h-exhausted personal.) The hook keeps a per-profile ledger of the last usage seen for each login — it is running every turn anyway — and swaps with swappy use <name> to the account it knows is healthy.The rule that falls out of that is worth stating on its own: an unmeasured account is only worth gambling on when you are already stuck. If your current 5-hour window is still live, the hook only swaps to a profile the ledger knows is healthy; being 7-day-hot is not a reason to trade a working session for a coin flip. When nothing beats where you are it stays put, and says so only if you are genuinely capped — a correct "everything still works" is not worth a notification every cooldown period.
The installer wires it as both a Stop hook and an async PostToolUse hook. Why both: a Stop hook only fires between turns, and one marathon turn can ride from 90% to a hard limit without ever reaching Stop (field-tested: 98% before a human noticed). The statusline snapshot updates during turns, so the PostToolUse wiring catches the crossing after the next tool call; "async": true keeps it off the critical path, and the cooldown stamp already dedups the two wirings.
Guard rails: windows whose resets_at already passed are ignored (the limit reset while you were idle), malformed or >6h-old snapshots are ignored, and when every account is over the threshold a cooldown stamp (default 15 min) stops exhausted accounts from ping-ponging — the stamp is claimed before swapping so concurrent sessions can't double-rotate, and a failed swap backs off only 2 minutes instead of consuming the full cooldown. All subprocess calls are timeboxed so the hook can never stall a turn.
The ledger has its own rails, because a wrong entry there sends you to a dead account: a reading is only filed against the active profile if the snapshot is under 5 minutes old, recording pauses for 2 minutes after a swap (Claude Code has not re-read credentials yet, so the statusline is still reporting the account you just left), entries older than 8 days stop counting as evidence, and a recorded cap whose resets_at has since passed stops counting against that profile without needing to have been active to observe it. If swappy ls cannot be read at all the hook refuses to swap rather than guess. Tune via SWAPPY_GUARD_THRESHOLD, SWAPPY_GUARD_COOLDOWN_MIN, SWAPPY_GUARD_LEDGER, SWAPPY_BIN; test decisions safely with SWAPPY_GUARD_DRYRUN=1.
Mods hook into Claude Code's own interface: a band above the prompt, a pane beside the approval dialog, a tool the model can call. They need Claude Code 2.1.286 or later (mods are early access). Install each one by name:
/plugin install usage-band --marketplace VibeCodyH/claude-craft
Answer y to add the marketplace, then pick a scope. The options below are userConfig fields in each mod's plugin.json.
| Mod | Does | Options |
|---|---|---|
blast-radius | For rm -r, git reset --hard, git clean, force pushes, branch -D, discards and db:migrate:prod, runs the tools' own dry runs and shows what would be touched beside the approval prompt. It never approves or blocks. | wrappers: extra command prefixes to see through, like a wrapper script. |
prod-tripwire | A red PROD band while a command touches production, then a dim "last prod touch" line until your next prompt. Built-in patterns: db:migrate:prod, neonctl, Vercel --prod, gh pr merge. | patterns: your own label=regex entries, separated by ;;. |
pr-tracker | Pins the PR the session is babysitting: review verdict, checks, merge state. A PR joins when the session runs gh pr create, or gh pr edit --add-reviewer, or a watch-review.sh owner/repo N watcher line. Needs gh. | |
usage-band | One row above the prompt: Codex weekly limit (from ~/.codex/sessions), Claude 5-hour and 7-day limits, context left, and a clock. Shows % left with reset countdowns. | nextFile: a JSON file with next_date and next_text to show your next event. Empty hides it. |
needs-you | Gives Claude ask and resolve tools. Each question or command it needs from you sits in an amber pill above the prompt, with a copy button, until it's answered. | |
clipboard-step | When Claude puts a command on your clipboard, shows it above the prompt until you paste it, clear it, or send your next prompt. Wayland only (wl-copy / wl-paste). | |
yellow-titles | Draws bold spans in replies in yellow so titles stand out. Display only. |
For keeping secrets, emails and IPs out of the transcript, use Ray Amjad's secret-redactor. I run it alongside these.
If you would rather not run the installer, copy what you want and wire it yourself:
cp claude-craft/tools/*.mjs ~/.claude/tools/
cp -r claude-craft/skills/delegate ~/.claude/skills/
cp claude-craft/bin/agentcall claude-craft/tools/crunch.mjs ~/.local/bin/
{
"hooks": {
"UserPromptSubmit": [
{ "hooks": [{ "type": "command", "command": "node /abs/path/.claude/tools/memory-preflight.mjs" }] }
],
"SessionStart": [
{ "hooks": [{ "type": "command", "command": "node /abs/path/.claude/tools/memory-staleness.mjs" }] }
],
"PreToolUse": [
{ "matcher": "Bash", "hooks": [{ "type": "command", "command": "node /abs/path/.claude/tools/protected-repo-guard.mjs" }] },
{ "matcher": "Agent|Workflow", "hooks": [{ "type": "command", "command": "node /abs/path/.claude/tools/delegation-guard.mjs" }] }
],
"PostToolUse": [
{ "matcher": "Edit|Write|MultiEdit", "hooks": [{ "type": "command", "command": "node /abs/path/.claude/tools/delegation-nudge.mjs" }] },
{ "matcher": "*", "hooks": [{ "type": "command", "command": "node /abs/path/.claude/tools/swappy-auto-rotate.mjs", "timeout": 15, "async": true }] }
],
"Stop": [
{ "matcher": "", "hooks": [{ "type": "command", "command": "node /abs/path/.claude/tools/swappy-auto-rotate.mjs", "timeout": 15 }] }
]
}
}
Use absolute paths. ~ in a hook command is not expanded on every platform, and a hook that fails to run fails silently.
npm test
npm run bench
Each mod has its own suite:
claude plugin validate mods/usage-band
claude plugin test mods/usage-band
MIT
hooks/register.tsx 162 lines1import { atom, read, update } from 'claude-code'
2import type { EngineInterface, Register } from 'claude-code'
3
4import type { Ask, AskKind } from '../types'
5
6// Needed from you: while Claude waits on you (a question, a command or a SQL
7// query to run), an amber pill sits above the prompt. Opening it lists each ask
8// with a copy button, so nothing said mid-turn gets lost in the scrollback.
9// Claude adds asks with the `ask` tool and clears them with `resolve`; you can
10// clear one with Done.
11
12const asks = atom({ plugin: 'needs-you', key: 'asks' } as const, [] as Ask[])
13const isOpen = atom({ plugin: 'needs-you', key: 'isOpen' } as const, false)
14
15const AMBER = '#ffb000'
16const KINDS: readonly AskKind[] = ['question', 'run', 'sql', 'shell']
17const LABEL: Record<AskKind, string> = {
18 question: 'QUESTION', run: 'RUN IN SESSION', sql: 'SQL', shell: 'RUN IN A TERMINAL',
19}
20
21const ASK_TOOL = 'mcp__needs-you__ask'
22const RESOLVE_TOOL = 'mcp__needs-you__resolve'
23
24// Parallel asks in one turn can land in the same millisecond.
25let seq = 0
26
27const GUIDE = [
28 'Needed-from-you queue: the user often misses text written mid-turn, so every time you need',
29 'something from them (a question to answer, a command to run with `!` in this session, a SQL',
30 'query, a command for another terminal) call `mcp__needs-you__ask` with it, in addition to',
31 'writing it in your reply. Put the exact text to paste in `command` (without the `!`).',
32 'Call `mcp__needs-you__resolve` with its id once they have answered or run it.',
33].join(' ')
34
35// What the copy button puts on the clipboard: in-session runs get the `!` prefix.
36export function copyText(ask: Ask): string {
37 const command = ask.command ?? ''
38 return ask.kind === 'run' && !command.startsWith('!') ? `! ${command}` : command
39}
40
41export function ago(at: number, now: number): string {
42 const minutes = Math.max(0, Math.floor((now - at) / 60000))
43 if (minutes < 1) return 'just now'
44 if (minutes < 60) return `${minutes}m ago`
45 return `${Math.floor(minutes / 60)}h${minutes % 60 ? `${minutes % 60}m` : ''} ago`
46}
47
48async function remove($: EngineInterface, id: string) {
49 await update($, asks, list => list.filter(a => a.id !== id))
50}
51
52async function copy($: EngineInterface, ask: Ask, surface: Parameters<EngineInterface['ui']['copy']>[0]['surface']) {
53 const done = await $.ui.copy({ text: copyText(ask), surface })
54 $.ui.toast(done.isCopied ? 'Copied. Paste it at the prompt.' : 'Could not copy on this surface.')
55}
56
57export const register: Register = on => {
58 on('session.start', async ($, e, next) => {
59 await $.tool.register({
60 name: 'ask',
61 description: 'Queue something Claude needs from the user (a question, or a command / SQL for them to run) in the amber "needed from you" pill above their prompt, so they see it even if it scrolled past mid-turn. Returns the ask id.',
62 isDeferred: false,
63 inputSchema: {
64 type: 'object',
65 properties: {
66 kind: { type: 'string', enum: KINDS, description: 'question: they answer in chat. run: a command they run in THIS session with `!`. sql: a query they run themselves. shell: a command for another terminal.' },
67 text: { type: 'string', description: 'One or two plain sentences: what you need and why.' },
68 command: { type: 'string', description: 'The exact text to paste, for run/sql/shell (no leading `!`).' },
69 },
70 required: ['kind', 'text'],
71 },
72 })
73 await $.tool.register({
74 name: 'resolve',
75 description: 'Clear an ask from the "needed from you" pill once the user answered or ran it.',
76 isDeferred: false,
77 inputSchema: { type: 'object', properties: { id: { type: 'string' } }, required: ['id'] },
78 })
79 return next(e)
80 })
81
82 on('prompt.compose', async ($, e, next) => {
83 const composed = await next(e)
84 return { sections: [...composed.sections, { id: 'needs-you:guide', text: GUIDE, scope: 'session' as const }] }
85 })
86
87 on('tool.call', { tool: ASK_TOOL }, async ($, e) => {
88 try {
89 // The model's arguments sit on the event itself, as Read's `file_path` does.
90 const input = e as unknown as { kind?: string; text?: string; command?: string }
91 const kind = KINDS.find(k => k === input.kind) ?? 'question'
92 const text = String(input.text ?? '').trim()
93 if (!text) return { deny: 'text is required' }
94 const now = await $.clock.now()
95 const ask: Ask = { id: `${now.toString(36)}${(seq++ % 36).toString(36)}`, kind, text, at: now, ...(input.command ? { command: String(input.command) } : {}) }
96 await update($, asks, list => [...list, ask])
97 // A plugin tool's result is the text the model reads (string or content blocks).
98 return { result: `Queued for the user as ${ask.id}.` }
99 } catch (err) {
100 return { deny: `needs-you: ${String(err)}` }
101 }
102 })
103
104 on('tool.call', { tool: RESOLVE_TOOL }, async ($, e) => {
105 try {
106 const id = String((e as unknown as { id?: string }).id ?? '')
107 const had = (await read($, asks)).some(a => a.id === id)
108 await remove($, id)
109 return { result: had ? `Cleared ${id}.` : `No open ask ${id}.` }
110 } catch (err) {
111 return { deny: `needs-you: ${String(err)}` }
112 }
113 })
114
115 on('ui.render', { component: 'AbovePrompt' }, async ($, e, next) => {
116 const list = await read($, asks)
117 if (list.length === 0 || e.props.hasSurvey) return next(e)
118 const { Box, Text, Button, Code } = $.ui.resolve(e)
119 const open = await read($, isOpen)
120 const now = await $.clock.now()
121 const toggle = () => update($, isOpen, v => !v)
122 const pill = (
123 <Box gap={1}>
124 <Text backgroundColor={AMBER} color="#000000" bold> ● {list.length} needed from you </Text>
125 <Button key="toggle" plain label={open ? '▾ hide' : '▸ show'} onPress={toggle} />
126 {open && <Button key="clear-all" plain dimColor label="clear all" onPress={() => update($, asks, () => [])} />}
127 </Box>
128 )
129 if (!open) return pill
130
131 // Newest first, as many as the band's rows allow.
132 let rows = Math.max(1, e.props.maxRows) - 1
133 const shown: Ask[] = []
134 for (const ask of [...list].reverse()) {
135 const need = 4 + (ask.command ? ask.command.split('\n').length : 0)
136 if (shown.length > 0 && need > rows) break
137 shown.push(ask)
138 rows -= need
139 }
140 return (
141 <Box flexDirection="column">
142 {pill}
143 {shown.map(ask => (
144 <Box key={`ask-${ask.id}`} flexDirection="column" borderStyle="round" borderColor={AMBER} paddingX={1}>
145 <Box justifyContent="space-between">
146 <Text color={AMBER} bold>{LABEL[ask.kind]}</Text>
147 <Text dimColor>{ago(ask.at, now)}</Text>
148 </Box>
149 <Text>{ask.text}</Text>
150 {ask.command && <Code source={copyText(ask)} language={ask.kind === 'sql' ? 'sql' : 'bash'} wrap="wrap" />}
151 <Box gap={2}>
152 {ask.command && <Button key={`copy-${ask.id}`} variant="primary" label="copy" onPress={press => copy($, ask, press.surface)} />}
153 <Button key={`done-${ask.id}`} label="done" onPress={() => remove($, ask.id)} />
154 </Box>
155 </Box>
156 ))}
157 {shown.length < list.length && <Text dimColor>+{list.length - shown.length} older</Text>}
158 </Box>
159 )
160 })
161}
162types/index.d.ts 16 lines1export type AskKind = 'question' | 'run' | 'sql' | 'shell'
2
3export type Ask = {
4 id: string
5 kind: AskKind
6 text: string
7 command?: string
8 at: number
9}
10
11declare module 'claude-code' {
12 interface PluginState {
13 'needs-you': { asks: Ask[]; isOpen: boolean }
14 }
15}
16