Band line showing whether Claude's prompt cache is still warm, with countdown, reuse, and estimated cold-rewrite cost

Lawrence Chase's Claude Code marketplace: code review, diagram, and test-evidence skills, plus a set of small Claude Code mods that add band lines and panes to the terminal UI.
/plugin marketplace add lchase/skills
/plugin install smart-review@lchase
/plugin install tldraw@lchase
/plugin install test-drive@lchase
/plugin install cache-clock@lchase
/plugin install usage-gauge@lchase
/plugin install auto-handoff@lchase
/plugin install agent-watch@lchase
/plugin install recording-mode@lchase
Each plugin is independent; install only what you want. @lchase is the marketplace name (from .claude-plugin/marketplace.json), not the GitHub slug.
New versions ship whenever the version string in a plugin's manifest changes. Claude Code refreshes marketplaces in the background by default, so you normally pick up updates without doing anything. To pull them right now:
/plugin marketplace update lchase # refresh this marketplace's manifest
/plugin update smart-review@lchase # apply a pending update to one plugin
/plugin update tldraw@lchase
/plugin marketplace update (no name) refreshes every marketplace. From a shell: claude plugin marketplace update lchase and claude plugin update <plugin>@lchase.
If a plugin gets into a bad state (stale cached script, half-applied update), do a clean reinstall:
/plugin uninstall tldraw
/plugin marketplace update lchase
/plugin install tldraw@lchase
Nuclear option, re-register the whole marketplace:
/plugin marketplace remove lchase
/plugin marketplace add lchase/skills
/plugin install smart-review@lchase
/plugin install tldraw@lchase
Before this repo became a multi-plugin marketplace it published one plugin from the repo root under a marketplace named chase. If you installed it back then, clear the old registration first:
/plugin uninstall smart-review
/plugin uninstall tldraw # only if you installed the short-lived tldraw@chase
/plugin marketplace remove chase
/plugin marketplace add lchase/skills
/plugin install smart-review@lchase
/plugin install tldraw@lchase
The tldraw skill's command also changed: /tldraw:tldraw → /tldraw:diagram.
| Plugin | What it does | Skills / commands | Harnesses |
|---|---|---|---|
| smart-review | Ensemble code review: six specialized lenses over a diff, merged into one deduplicated, severity-ranked verdict | /smart-review:{review,min,max,add-pr-review,review-pr-comments} + auto-trigger | Claude Code, Cursor, Codex, Gemini CLI, any AGENTS.md agent |
| tldraw | Natural-language description → editable tldraw document (.tldr) + rendered PNG/SVG | /tldraw:diagram + auto-trigger | Claude Code only (needs Node + npx) |
| test-drive | Before/after evidence of a UI or API change: record the app before you edit, replay the same scripted scenario after, with a deterministic verdict | /test-drive:{baseline,compare} + auto-trigger | Claude Code only (needs Node; UI also needs Playwright and ffmpeg) |
| cache-clock | Mod: band line showing whether the prompt cache is warm, with countdown, reuse %, cached tokens, and estimated cold-rewrite cost | Compact button, /cache-clock | Claude Code only |
| usage-gauge | Mod: band line with plan usage windows (5-hour, weekly, spend limit), reset times, pace warning, session cost on API billing, threshold toasts | /usage-gauge (detail pane) | Claude Code only |
| auto-handoff | Mod: band line showing how full the chat is; saves a HANDOFF.md at a threshold (auto or on request), then clear and continue or resume in a new session | /auto-handoff | Claude Code only |
| agent-watch | Mod: band line showing who is doing the work this turn (main chat, helper agents, models, tasks, status, skills), plus a pane with per-agent blocks, per-model totals, timeline | /agent-watch | Claude Code only |
| recording-mode | Mod: screen-record without leaking secrets; tells the model to keep keys, emails, private words off screen and masks them in the transcript display only, red REC chip | /rec | Claude Code only |
An ensemble code review: several specialized lenses, one merged verdict, not another pile of comments.
Most code review, human or AI, is one reviewer making a single pass and leaving a list of comments. This runs six specialized lenses instead (spec-conformance, correctness, security, performance, design, and tests), then merges the results into one verdict: duplicates collapsed, findings that several lenses independently raise weighted up, conflicting advice reconciled, severity rolled up, structural problems first, nits capped. The merge is the point. An ensemble without it is just louder, and a longer checklist handed to one reviewer only dilutes its attention and repeats its own misses.
Two modes: a fast single pass (min) for tight loops, and an orchestrated ensemble (max) for pre-merge review. Auto-routing picks between them by change size and sensitivity.
The skill lives in plugins/smart-review/skills/smart-review/ as harness-neutral markdown. Each harness gets a thin manifest pointing at that same directory, so no skill content is copied or forked.
| Harness | Install | min | max | Slash commands / auto-trigger |
|---|---|---|---|---|
| Claude Code | /plugin marketplace add lchase/skills then /plugin install smart-review@lchase | ✅ | ✅ isolated parallel subagents | ✅ |
| Cursor | point Cursor at plugins/smart-review/.cursor-plugin/ | ✅ | ✅ sequential ensemble | via session-start hook |
| Codex | plugins/smart-review/.codex-plugin/ manifest | ✅ | ✅ sequential ensemble | no |
| Gemini CLI | gemini extensions install https://github.com/lchase/skills (reads root gemini-extension.json) | ✅ | ✅ sequential ensemble | via GEMINI.md |
| Any AGENTS.md agent | reads root AGENTS.md | ✅ | ✅ sequential ensemble | no |
max's isolated parallel subagents are a Claude Code capability. A model panel (plugins/smart-review/skills/smart-review/references/ensemble.md, off by default) rides on top when the harness can route more than one model. Otherwise max is single-model. Elsewhere max runs the same spec-gate, six lenses, and merge as a disciplined sequential walk. Design and internals: plugins/smart-review/SMART-REVIEW.md.
Claude Code slash commands:
/smart-review:review: auto (routes to min or max by size + sensitivity)/smart-review:min: fast single-pass review/smart-review:max: orchestrated ensemble review/smart-review:add-pr-review <PR number or URL>: reviews a GitHub PR and, after you confirm what to publish, posts the findings as PR comments/smart-review:review-pr-comments <PR number or URL>: triages a PR's existing unresolved review comments, fixes or explains each after your approval, replies, and resolves the threadThe skill also triggers automatically when you ask Claude to review a diff, PR, or branch.
Turn a described diagram (flowchart, architecture / system diagram, process map, decision tree, state machine) into two artifacts: an editable .tldr document (opens at tldraw.com or in the tldraw editor) and a rendered PNG/SVG for docs and PRs.
Claude writes a {nodes, edges} spec from your description; a bundled Node script does a simple layered auto-layout, emits the .tldr, and renders images by loading it into a headless-Chromium tldraw instance via @kitschpatrol/tldraw-cli (first run downloads Chromium, ~1 min, cached).
/plugin install tldraw@lchase
Claude Code only. Skill: /tldraw:diagram (also auto-triggers on "draw this diagram", "diagram this flow", "make a tldraw of…"). Details: plugins/tldraw/skills/diagram/SKILL.md.
A test drive is an e2e test that happens to produce video, and the "before" is part of the process rather than something reconstructed later. Run /test-drive:baseline before you edit: it writes a short browser scenario and records the app as it stands. Make the change, then run /test-drive:compare: it replays the identical scenario with Playwright and composes a labeled side-by-side MP4. The pass/warn/fail verdict comes from the scenario's assertions, not from reading the video.
Backend changes work the same way with "kind": "api": the scenario is a list of HTTP steps (method, path, body, expected status, JSON fields, and server-log lines to expect or avoid). There is nothing to screen-record, so each step captures the request, the response, and the server log lines it produced, and compare writes a before/after report.md instead of a video. Secrets are redacted from the stored transcript (best effort, extendable per scenario with redact), and the finished report is scanned and flagged if anything still looks secret.
/plugin install test-drive@lchase
Claude Code only. UI scenarios need ffmpeg and Playwright in the repo under test; API scenarios need only Node 18+. Only the working tree is ever run, so there is no second checkout to boot. Details: baseline and compare.
Five small plugins that change Claude Code's own interface instead of adding a task skill. Each draws a band line above the prompt (some add a side pane or slash command), is independent of the others, and works on Claude Code only.
cacheMinutes (5 or 60, default 60) to match your cache TTL./usage-gauge opens a detail pane.HANDOFF.md at a threshold (automatically or on request), then clears and continues, or resumes in a new session. /auto-handoff./agent-watch opens a pane with per-agent blocks, per-model totals, and a timeline./rec to toggle./plugin install cache-clock@lchase # or usage-gauge, auto-handoff, agent-watch, recording-mode
.claude-plugin/marketplace.json # the "lchase" marketplace: 8 plugin entries
AGENTS.md # repo-level agent pointer (→ smart-review)
gemini-extension.json + GEMINI.md # Gemini CLI installs the repo as an extension (→ smart-review)
plugins/smart-review/ # plugin 1, multi-harness code review
.claude-plugin/plugin.json
skills/smart-review/ # the harness-neutral core (SKILL.md + references/)
agents/ commands/ hooks/ # Claude Code packaging
.cursor-plugin/ .codex-plugin/ # thin per-harness manifests
scripts/ # validate-adapters.sh, bump-version.sh
evals/ # dev-only eval kit (not shipped to installers)
SMART-REVIEW.md # design writeup
plugins/tldraw/ # plugin 2, Claude Code only
.claude-plugin/plugin.json
skills/diagram/ # SKILL.md + scripts/build-tldr.mjs + references/
plugins/test-drive/ # plugin 3, Claude Code only
.claude-plugin/plugin.json
skills/{baseline,compare}/ # SKILL.md each
scripts/{lib.mjs,record.mjs,record-api.mjs,redact.mjs,verdict.mjs,compose.sh,report.mjs} # shared by both skills
references/ # UI and API scenario schemas + examples
plugins/{cache-clock,usage-gauge,auto-handoff,agent-watch,recording-mode}/ # plugins 4-8, Claude Code mods
.claude-plugin/plugin.json # each with its own version
hooks/ # register.tsx (the mod) + hooks.json, tests where present
types/ # mod API typings
claude plugin validate . # marketplace + all plugins
./plugins/smart-review/scripts/validate-adapters.sh # smart-review's per-harness manifests
Releasing: users only get an update when a plugin's version string changes, so bump it on every user-facing change and push to main.
./plugins/smart-review/scripts/bump-version.sh <version>..claude-plugin/plugin.json.Once pushed, installed clients pick it up on their next background marketplace refresh, or immediately with /plugin marketplace update lchase.
hooks/register.tsx 168 lines1import { atom, read, update } from 'claude-code'
2import type { Register } from 'claude-code'
3
4import type { Last, Totals } from '../types'
5import { MIN, bar, coldCost, mins, reusedPct, savedBy, tok, usd } from './cache'
6
7// all in $.state so a hot reload keeps them; lastCache is the clock's whole input
8const lastCache = atom({ plugin: 'cache-clock', key: 'lastCache' } as const, 0)
9const toastedFor = atom({ plugin: 'cache-clock', key: 'toastedFor' } as const, 0)
10const lastStep = atom({ plugin: 'cache-clock', key: 'last' } as const, null as Last | null)
11const totals = atom({ plugin: 'cache-clock', key: 'totals' } as const, { reads: 0, writes: 0, fresh: 0, saved: 0, unpricedReads: 0 } as Totals)
12const isOn = atom({ plugin: 'cache-clock', key: 'isOn' } as const, true)
13
14const CHIP_BG = '#3b2160'
15const CHIP_FG = '#b8aece'
16const GREEN = '#22c55e'
17const AMBER = '#f59e0b'
18const RED = '#ef4444'
19const GREY = '#c0c0c0'
20const AMBER_AT = 15 * MIN
21
22const cost = (c: number | null) => (c === null ? '≈ ?' : `≈ ${usd(c)}`)
23
24const coldRewrite = (last: Last, cached: number, ttlMinutes: number) =>
25 coldCost(last.model, cached, last.reads + last.writes + last.fresh, ttlMinutes)
26
27async function tick($: any, ttlMinutes: number) {
28 $.ui.invalidate('ui.render')
29 if (!(await read($, isOn))) return
30 const at = await read($, lastCache)
31 const last = await read($, lastStep)
32 if (!at || !last) return
33 const left = ttlMinutes * MIN - ((await $.clock.now()) - at)
34 // a 5-minute cache is at 5 minutes left the moment it is written, so warn at 1 minute there
35 const warnAt = ttlMinutes > 5 ? 5 * MIN : MIN
36 if (left <= 0 || left > warnAt || (await read($, toastedFor)) === at) return
37 await update($, toastedFor, () => at)
38 const cached = last.reads + last.writes
39 $.ui.toast(
40 `Cache goes cold in ${mins(left)}: ${tok(cached)} tokens cached, rewrite ${cost(coldRewrite(last, cached, ttlMinutes))} (estimate). Send anything to keep it warm.`,
41 { timeoutMs: 10000 },
42 )
43}
44
45export const register: Register = (on, options) => {
46 const ttlMinutes = options?.cacheMinutes === '5' ? 5 : 60
47 const ttl = ttlMinutes * MIN
48
49 on('session.start', async ($, e, next) => {
50 await $.command.register({ name: 'cache-clock', description: 'Prompt cache clock: on | off | status' })
51 $.clock.every(30000, () => tick($, ttlMinutes))
52 return next(e)
53 })
54
55 on('turn.step', async function* ($, e, next) {
56 const result = yield* next(e)
57 const u = result?.usage
58 if (!u) return result
59 const reads = u.cache_read_input_tokens
60 const writes = u.cache_creation_input_tokens
61 const fresh = u.input_tokens
62 const saved = savedBy(u.model, reads, reads + writes + fresh)
63 // every step counts toward the session totals, subagents included
64 await update($, totals, t => ({
65 reads: t.reads + reads,
66 writes: t.writes + writes,
67 fresh: t.fresh + fresh,
68 saved: t.saved + (saved ?? 0),
69 unpricedReads: t.unpricedReads + (saved === null ? reads : 0),
70 }))
71 // only a main-loop step that touched the cache restarts the countdown
72 if (!e.agentId && reads + writes > 0) {
73 const at = await $.clock.now()
74 await update($, lastCache, () => at)
75 await update($, lastStep, () => ({ reads, writes, fresh, model: u.model }))
76 $.ui.invalidate('ui.render')
77 }
78 return result
79 })
80
81 on('classic.SessionStart', { source: ['clear', 'resume', 'fork'] }, async ($, e, next) => {
82 await update($, lastCache, () => 0)
83 await update($, lastStep, () => null)
84 return next(e)
85 })
86
87 on('command.run', { command: 'cache-clock' }, async ($, e) => {
88 const arg = e.args.trim().toLowerCase()
89 if (arg === 'on' || arg === 'off') {
90 await update($, isOn, () => arg === 'on')
91 $.ui.invalidate('ui.render')
92 return { text: `cache-clock ${arg}.` }
93 }
94 if (arg !== '' && arg !== 'status') return { text: 'Usage: /cache-clock on | off | status' }
95
96 const [on_, at, last, t, now] = await Promise.all([read($, isOn), read($, lastCache), read($, lastStep), read($, totals), $.clock.now()])
97 const left = at ? ttl - (now - at) : 0
98 const lines = [`cache-clock: ${on_ ? 'on' : 'off'}, cache lifetime ${ttlMinutes}m`]
99 lines.push(!at ? 'Cache: no cache activity yet' : left > 0 ? `Cache: warm, ${mins(left)} left` : 'Cache: COLD')
100 lines.push(`Session reads ${tok(t.reads)}, writes ${tok(t.writes)}, fresh input ${tok(t.fresh)} (subagents included)`)
101 if (last) {
102 lines.push(`Reused ${reusedPct(last.reads, last.writes, last.fresh)}% on the last main step`)
103 lines.push(`Cold rewrite ${cost(coldRewrite(last, last.reads + last.writes, ttlMinutes))} for ${tok(last.reads + last.writes)} cached`)
104 }
105 lines.push(
106 `Saved ${t.unpricedReads > 0 ? 'at least ' : ''}≈ ${usd(t.saved)} by cache reads vs uncached input` +
107 (t.unpricedReads > 0 ? ` (${tok(t.unpricedReads)} read tokens on models with no known price)` : ''),
108 )
109 lines.push('Dollar figures are estimates from list prices.')
110 return { text: lines.join('\n') }
111 })
112
113 on('ui.render', { component: 'AbovePrompt' }, async ($, e, next) => {
114 const [active, at, last] = await Promise.all([read($, isOn), read($, lastCache), read($, lastStep)])
115 if (e.props.hasSurvey || !active || !at || !last) return next(e)
116
117 // keep every other mod's lines: ours goes on top, theirs stay beneath
118 let below: Awaited<ReturnType<typeof next>> | undefined
119 try {
120 below = await next(e)
121 } catch {
122 below = undefined
123 }
124
125 const { Box, Text, Button } = $.ui.resolve(e)
126 const left = ttl - ((await $.clock.now()) - at)
127 const color = left <= 0 ? RED : left <= AMBER_AT ? AMBER : GREEN
128 const cached = last.reads + last.writes
129 const compact = async () => {
130 try {
131 const r = await $.session.compact()
132 $.ui.toast(
133 r.skip
134 ? `Compact skipped: ${r.skip}`
135 : r.tokensBefore !== undefined && r.tokensAfter !== undefined
136 ? `Compacted: ${tok(r.tokensBefore)} to ${tok(r.tokensAfter)} tokens.`
137 : 'Compacted.',
138 )
139 } catch (err) {
140 $.ui.toast(`Compact failed: ${String((err as Error)?.message ?? err).slice(0, 100)}`)
141 }
142 }
143
144 const line = (
145 <Box>
146 <Text bold color={CHIP_FG} backgroundColor={CHIP_BG}>
147 {' CACHE ● '}
148 </Text>
149 <Text bold color={color}>
150 {left > 0 ? ` warm ${mins(left)} ` : ' COLD '}
151 </Text>
152 <Text color={color}>{bar(left, ttl)}</Text>
153 <Text color={GREY}>
154 {` ${reusedPct(last.reads, last.writes, last.fresh)}% reused · ${tok(cached)} cached · cold rewrite ${cost(coldRewrite(last, cached, ttlMinutes))} `}
155 </Text>
156 <Button key="compact" label="Compact" onPress={compact} />
157 </Box>
158 )
159 if (!below) return line
160 return (
161 <Box flexDirection="column">
162 {line}
163 {below}
164 </Box>
165 )
166 })
167}
168hooks/cache.ts 69 lines1export const MIN = 60000
2
3// USD per million tokens, copied from the claude-api skill's model table (cached 2026-10-06).
4// write5 / write60 are the 5-minute (1.25x input) and 1-hour (2x input) cache-write prices.
5export type Rates = { input: number; write5: number; write60: number; read: number }
6
7const TABLE: Record<string, Rates> = {
8 'claude-fable-5-1': { input: 10, write5: 12.5, write60: 20, read: 0.25 },
9 'claude-mythos-5-1': { input: 10, write5: 12.5, write60: 20, read: 0.25 },
10 'claude-fable-5': { input: 10, write5: 12.5, write60: 20, read: 1 },
11 'claude-opus-5-5': { input: 4, write5: 5, write60: 8, read: 0.2 },
12 'claude-opus-5': { input: 5, write5: 6.25, write60: 10, read: 0.5 },
13 'claude-opus-4-8': { input: 5, write5: 6.25, write60: 10, read: 0.5 },
14 'claude-opus-4-7': { input: 5, write5: 6.25, write60: 10, read: 0.5 },
15 'claude-opus-4-6': { input: 5, write5: 6.25, write60: 10, read: 0.5 },
16 'claude-sonnet-5-5': { input: 2, write5: 2.5, write60: 4, read: 0.2 },
17 'claude-sonnet-5': { input: 2, write5: 2.5, write60: 4, read: 0.2 },
18 'claude-sonnet-4-6': { input: 3, write5: 3.75, write60: 6, read: 0.3 },
19 'claude-haiku-4-5': { input: 1, write5: 1.25, write60: 2, read: 0.1 },
20}
21
22// Haiku 5.5 has two rate cards chosen by prompt length: 100K tokens or fewer, or longer.
23const HAIKU_55_SHORT: Rates = { input: 0.1, write5: 0.125, write60: 0.2, read: 0.01 }
24const HAIKU_55_LONG: Rates = { input: 0.5, write5: 0.625, write60: 1, read: 0.05 }
25
26// strips "[1m]", a date suffix and a cloud-provider prefix; anything not in the table stays unknown
27export const canonical = (id: string) =>
28 id
29 .replace(/\[1m\]$/, '')
30 .replace(/^(?:[a-z]{2}\.)?anthropic\./, '')
31 .replace(/-\d{8}$/, '')
32
33export function rates(model: string, promptTokens: number): Rates | null {
34 const id = canonical(model)
35 if (id === 'claude-haiku-5-5') return promptTokens > 100000 ? HAIKU_55_LONG : HAIKU_55_SHORT
36 return TABLE[id] ?? null
37}
38
39// what rewriting `cached` tokens into a cold cache costs, or null when the model has no known price
40export function coldCost(model: string, cached: number, promptTokens: number, ttlMinutes: number): number | null {
41 const r = rates(model, promptTokens)
42 if (!r) return null
43 return (cached * (ttlMinutes > 5 ? r.write60 : r.write5)) / 1e6
44}
45
46// what the cache reads saved against paying the full input price, or null when the model has no known price
47export function savedBy(model: string, reads: number, promptTokens: number): number | null {
48 const r = rates(model, promptTokens)
49 if (!r) return null
50 return (reads * (r.input - r.read)) / 1e6
51}
52
53export const reusedPct = (reads: number, writes: number, fresh: number) => {
54 const total = reads + writes + fresh
55 return total > 0 ? Math.round((reads / total) * 100) : 0
56}
57
58export const tok = (n: number) =>
59 n >= 1e6 ? `${(n / 1e6).toFixed(n >= 1e7 ? 0 : 1)}M` : n >= 1e3 ? `${(n / 1e3).toFixed(n >= 1e5 ? 0 : 1)}k` : String(n)
60
61export const usd = (n: number) => (n > 0 && n < 0.005 ? '<$0.01' : `$${n.toFixed(2)}`)
62
63export const mins = (ms: number) => `${Math.max(1, Math.ceil(ms / MIN))}m`
64
65export const bar = (left: number, ttl: number, cells = 6) => {
66 const filled = Math.max(0, Math.min(cells, Math.floor((left / ttl) * cells)))
67 return '▰'.repeat(filled) + '▱'.repeat(cells - filled)
68}
69types/index.d.ts 9 lines1export type Last = { reads: number; writes: number; fresh: number; model: string }
2export type Totals = { reads: number; writes: number; fresh: number; saved: number; unpricedReads: number }
3
4declare module 'claude-code' {
5 interface PluginState {
6 'cache-clock': { lastCache: number; toastedFor: number; last: Last | null; totals: Totals; isOn: boolean }
7 }
8}
9