SLOPSHOPPER

cache-band

Claude Code mod: one line above the prompt with the prompt-cache read rate of the last turn, this session and the whole Mac over the last hour. macOS only; the…

bandmodelprocesstimer
v0.1.0MITupdated 2026-10-17mlabo-org/cache-band
A shopper browsing a rack in a slop shop
Preview · a replayed session in a sandbox
claude · ~/work/app · cache-band
› 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 cache last 93% ⟨Claude Code's own drawing⟩ ────────────────────────────────────────────────────────────────────────────────────────────────────────────────────── › ? for shortcuts

Draws

Band
cache last 93% ⟨Claude Code's own drawing⟩
README

cache-band

日本語

A Claude Code mod that draws one line above the prompt with the prompt-cache read rate: of the last turn, of this session, and of every Claude Code session on this Mac over the last hour.

cache last 99% session 84% (read 244k / write 46k) │ all 60m 97% (read 19.1M / write 548k)

Built for one person's Mac and published as a reference. It works anywhere the requirements below are met, and every requirement is checked by a command.

Which setup is yours

cache-band is the companion of Mini System Monitor (a macOS app with its own window). The monitor works without the band; the band needs the monitor for two of its three figures.

Your situationWhat to doWhat you get
You already use Mini System MonitorInstall steps 2, 3, 4. If step 2 opens the monitor's window instead of printing JSON, your build is older than the flag: update it first (step 1, with --replace)All three figures. The monitor's window and settings are not changed
You do not use it yetInstall steps 1 to 4All three figures. You also get the monitor's window (CPU, memory, Codex and Claude quota), but it does not have to be open for the band
You do not want the monitorOnly step 3last only, with nothing said about the monitor

The monitor's README has the same split from its side: Use it alone or with cache-band.

Requirements

Needed forWhatCheck
everythingmacOS (Mini System Monitor needs macOS 14 or later)sw_vers -productVersion
everythingClaude Code with mods (the plugin hooks API). Tested on 2.1.299, in the terminal and the desktop app's Code tabclaude --version
session and all 60mMini System Monitor at commit 1f92771 (2026-10-17) or later, installed at /Applications/Mini System Monitor.appsee Install, step 2
building the monitorThe Rust toolchain pinned by the monitor's rust-toolchain.toml, and Xcode Command Line Toolscargo --version, xcode-select -p

last needs nothing but Claude Code. With no monitor installed, the band shows last alone and says nothing about the monitor: leaving it out is a choice, not a fault. A monitor that is installed but too old or failing is named in the band (see Troubleshooting), since its owner expects the figures.

The monitor has no prebuilt download; it is built from source. Why the mod needs it: a mod's file reads stop at 4 MiB, and a long session's transcript is larger (30 MiB and more), so the mod cannot count transcripts itself. The monitor already counts them for its own window and prints the same totals as JSON, so the band and the monitor always agree.

Install

Run these in order. Each step says what success looks like.

1. Build and install Mini System Monitor

Skip to step 2 if it is already installed.

git clone https://github.com/mlabo-org/mini-system-monitor-rs.git
cd mini-system-monitor-rs && cargo fetch --locked
Scripts/install-app.sh --destination "/Applications/Mini System Monitor.app"

Success: the last line is Installed and verified: /Applications/Mini System Monitor.app. The installer refuses to overwrite an existing app; to update one, quit it and add --replace (the old app is kept beside it as Mini System Monitor.previous-*.app).

2. Check that the monitor has --claude-cache-json

"/Applications/Mini System Monitor.app/Contents/MacOS/mini-system-monitor-rs" --claude-cache-json

Success: one line of JSON that starts with {"window": and exit code 0, and no window opens. If the monitor's window opens instead, that build is older than the flag: quit it and update it as in step 1 with --replace.

3. Install the mod

In a Claude Code session:

/plugin install cache-band --marketplace mlabo-org/cache-band

Or from your shell:

claude plugin marketplace add mlabo-org/cache-band
claude plugin install cache-band@cache-band

Success: Successfully installed plugin: cache-band@cache-band. The note 2 userConfig options not yet set that follows is expected: both options have defaults (see Options), so nothing needs configuring unless the monitor is installed somewhere other than /Applications.

4. Start a new session

The band appears with the first reply of a new session, not on the empty new-session screen: mods run once the session has started.

What it shows

  • last: the last turn of the main conversation (subagent turns are left out), from the usage Claude Code reports when the turn ends.
  • session: this session's transcript and its subagent transcripts, from the start of the session.
  • all 60m: every Claude Code transcript on this Mac (terminal and the desktop Code tab) over the last 60 minutes.

The rate is cache reads ÷ all input, where all input is uncached input + cache reads + cache writes; write is cache creation. Green from 90%, yellow from 70%, red below. * after a rate means some records could not be read, so the total may be low.

It refreshes when each turn ends and every 30 seconds. The bands of other mods stay beneath it.

Reading the numbers: a new session's first turn is often around 50%, because its own context is written to the cache once. The rate then climbs turn by turn. A sudden drop of last in the middle of a conversation means the cache was not reused, for example after a pause of more than 5 minutes, a model switch, or a compaction.

Options

Set them in the /config panel, or with claude plugin install cache-band@cache-band --config KEY=VALUE.

KeyDefaultMeaning
languageautoThe language of the band's words (see Language); ja or en fixes it.
monitorPath/Applications/Mini System Monitor.app/Contents/MacOS/mini-system-monitor-rsThe monitor's executable, inside its app bundle. Change it only when the app is installed elsewhere.

Language

The band's own words (last, session, all 60m, read, write and the monitor messages) follow the language you use. The cache label stays English. With language at auto, the default:

  • In the desktop Code tab, the band follows the Claude desktop app's display language. It is read from locale in the app's own ~/Library/Application Support/Claude/config.json: an internal file, not a documented interface, which also holds the app's sign-in cache, so the mod reads it again only when its modification time moves and keeps nothing but locale.
  • In the terminal (and in the desktop when the app's language cannot be read), it follows Claude Code's own language setting (the language Claude replies in), then the locale (LC_ALL, LC_MESSAGES, LANG). With neither, it is English.

Japanese and English are written into the mod. Any other language is translated from English the first time it appears, by one small model call (haiku, through the session's own client, so it counts against your usage). Until it lands, the band shows English. The translation is kept in the plugin's $.store per language and reused by every session, and translated again only when the English words change. A translation that loses a {placeholder}, runs too long for the band, or is not well-formed is thrown away, and that language then stays in English, with no further call, until the English words change.

A switch shows without a new session: a change of Claude Code's language in /config at once, any other change within 3 seconds. This is the same rule as context-git-band.

Troubleshooting

The band showsCauseFix
no cache line at allThe session has not started yet, or the mod is not installedSend a message; then check claude plugin list for cache-band
session/all -- (Mini System Monitor too old; update it)The executable does not contain --claude-cache-jsonQuit the monitor and reinstall it from the latest source with --replace
session/all -- (Mini System Monitor failed)The monitor ran but exited with an error or printed something that is not its JSONRun the command in step 2 yourself and read its error
last only, never session or all 60mNo monitor at monitorPath (shown this way on purpose)Install the monitor (step 1), or set monitorPath to where it is

The mod never runs a monitor that lacks the flag: before the first run it checks that the executable contains the text --claude-cache-json, because an older monitor ignores its arguments and opens its window.

What it reads and runs

  • Runs /usr/bin/grep on the monitor's executable (the check above) until it passes, and again after a failed run; then the monitor with --claude-cache-json --session <session id>. With no monitor at monitorPath, it runs nothing.
  • The monitor reads projects/**/*.jsonl under CLAUDE_CONFIG_DIR (default ~/.claude) and prints token counts only. Nothing is written, sent or kept beyond the current session's band.
  • For the band's language (see Language): Claude Code's settings, LC_ALL, LC_MESSAGES, LANG, and locale from the desktop app's config.json. Only for a language other than Japanese and English, one haiku call translates the band's English words, kept in the plugin's store.

Check

claude plugin validate /path/to/cache-band
claude plugin test /path/to/cache-band

License

MIT

Source 2 files
hooks/register.tsx 367 lines
1import { atom, read, update } from 'claude-code'
2import type { EngineInterface, Register, TurnUsage } from 'claude-code'
3
4import type { CacheProblem, CacheReading, CacheTotals, Words } from '../types'
5
6const reading = atom({ plugin: 'cache-band', key: 'reading' } as const, null)
7const lastTurn = atom({ plugin: 'cache-band', key: 'lastTurn' } as const, null)
8const problem = atom({ plugin: 'cache-band', key: 'problem' } as const, null)
9
10// Mini System Monitor owns the counting: the same code that fills its Claude view
11// prints it as JSON, so the band and the monitor never disagree
12export const DEFAULT_MONITOR = '/Applications/Mini System Monitor.app/Contents/MacOS/mini-system-monitor-rs'
13const FLAG = '--claude-cache-json'
14
15// The monitor reads every transcript touched in the last hour; a few hundred ms
16const REFRESH_EVERY_MS = 30_000
17
18// The language the band's words are in, as a key (`ja`, `en`, `fr`, `zh-tw`, or a
19// language name): Claude Code's (every surface), and the desktop app's display
20// language, which the desktop Code tab prefers
21const language = atom({ plugin: 'cache-band', key: 'language' } as const, null)
22const appLanguage = atom({ plugin: 'cache-band', key: 'appLanguage' } as const, null)
23// Tables translated from EN, by language key
24const translations = atom({ plugin: 'cache-band', key: 'translations' } as const, null)
25
26// How often a change of either is picked up
27const LANGUAGE_CHECK_MS = 3_000
28
29// The band's own words as templates; `{name}` is filled in. Japanese and English
30// are written here; any other language is translated from English once (see
31// ensureWords). The `cache` label stays English.
32const EN: Words = {
33  lastTurn: 'last', session: 'session', window: 'all {minutes}m', read: 'read', write: 'write',
34  outdated: 'session/all -- (Mini System Monitor too old; update it)',
35  failed: 'session/all -- (Mini System Monitor failed)',
36}
37
38const JA: Words = {
39  lastTurn: '直近', session: 'セッション', window: '全体{minutes}分', read: '読', write: '書',
40  outdated: 'セッション・全体 -- (Mini System Monitor が古い。更新が必要)',
41  failed: 'セッション・全体 -- (Mini System Monitor の実行に失敗)',
42}
43
44const WRITTEN: Record<string, Words> = { en: EN, ja: JA }
45
46export const wordsFor = (key: string | null, translated: Record<string, Words> | null) =>
47  (key === null ? undefined : (WRITTEN[key] ?? translated?.[key])) ?? EN
48
49type MonitorTotals = { input: number; cache_read: number; cache_write: number; partial: boolean }
50
51const totalsOf = (raw: MonitorTotals | null | undefined): CacheTotals | null =>
52  raw == null ? null : { input: raw.input, cacheRead: raw.cache_read, cacheWrite: raw.cache_write, partial: raw.partial }
53
54export const parseMonitor = (stdout: string): CacheReading => {
55  const json = JSON.parse(stdout) as {
56    window_minutes: number
57    window: MonitorTotals | null
58    session?: (MonitorTotals & { found: boolean }) | null
59  }
60  return {
61    window: totalsOf(json.window),
62    session: json.session?.found ? totalsOf(json.session) : null,
63    windowMinutes: json.window_minutes,
64  }
65}
66
67// A turn's usage in the monitor's terms: input is uncached + cache reads + writes
68export const turnTotals = (usage: TurnUsage): CacheTotals => ({
69  input: usage.input_tokens + usage.cache_read_input_tokens + usage.cache_creation_input_tokens,
70  cacheRead: usage.cache_read_input_tokens,
71  cacheWrite: usage.cache_creation_input_tokens,
72  partial: false,
73})
74
75export const rate = (totals: CacheTotals) =>
76  totals.input === 0 ? null : Math.floor((totals.cacheRead / totals.input) * 100)
77
78export const rateColor = (percent: number) => (percent >= 90 ? 'green' : percent >= 70 ? 'yellow' : 'red')
79
80export const short = (n: number) =>
81  n >= 1_000_000 ? `${(n / 1_000_000).toFixed(1)}M` : n >= 1000 ? `${Math.round(n / 1000)}k` : `${n}`
82
83export const monitorPathOf = (option: unknown) =>
84  typeof option === 'string' && option.trim() !== '' ? option.trim() : DEFAULT_MONITOR
85
86// The monitor path whose executable is known to have the flag. A monitor from
87// before the flag ignores its arguments and opens its window, so it is never run
88// until its file is seen to contain the flag's name.
89let supported: string | null = null
90
91async function check($: EngineInterface, monitor: string): Promise<CacheProblem | null> {
92  if (supported === monitor) return null
93  if (!(await $.fs.exists(monitor))) return 'missing'
94  const { exitCode } = await $.process.run(['/usr/bin/grep', '-q', '-a', '-F', '--', FLAG, monitor], { timeoutMs: 10_000 })
95  if (exitCode !== 0) return 'outdated'
96  supported = monitor
97  return null
98}
99
100async function refresh($: EngineInterface, option: unknown) {
101  const monitor = monitorPathOf(option)
102  try {
103    const found = await check($, monitor)
104    if (found !== null) {
105      await update($, problem, () => found)
106      return
107    }
108    const id = await $.session.id()
109    const { exitCode, stdout } = await $.process.run([monitor, FLAG, '--session', id], { timeoutMs: 10_000 })
110    if (exitCode !== 0) throw new Error(`exit ${exitCode}`)
111    const next = parseMonitor(stdout)
112    await update($, reading, () => next)
113    await update($, problem, () => null)
114  } catch {
115    // Check again next time: the monitor may have been removed or replaced
116    supported = null
117    await update($, problem, () => 'failed')
118  }
119}
120
121// A language as a key: `ja` and `en` for the written tables, else a lowercased
122// locale tag (`fr`, `pt-br`, `zh-tw`, `sr-latin`) or language name (`french`);
123// null when it names no language (`C`, `POSIX`, a bare `UTF-8`)
124export const languageKey = (raw: string): string | null => {
125  const lowered = raw.trim().toLowerCase()
126  // A locale's modifier names its script (sr_RS@latin); its encoding names nothing
127  const modifier = /@([a-z]+)/.exec(lowered)?.[1]
128  const text = lowered.replace(/[.@].*$/, '').replace(/_/g, '-')
129  if (text === '' || text === 'c' || text === 'posix' || /^utf-?8$/.test(text)) return null
130  if (/^(japanese|日本語)/.test(text) || /^ja(?:$|-)/.test(text)) return 'ja'
131  if (/^english/.test(text) || /^en(?:$|-)/.test(text)) return 'en'
132  const tag = /^([a-z]{2,3})(?:-([a-z0-9]+))?$/.exec(text)
133  if (tag === null) return text
134  const [, base = text, region] = tag
135  // Keep what changes the written language: zh-tw/zh-hant, pt-br, a script modifier
136  if (modifier !== undefined) return `${base}-${modifier}`
137  return region !== undefined && (base === 'zh' || base === 'pt') ? `${base}-${region}` : base
138}
139
140// `auto` follows Claude Code's own `language` setting (the language Claude replies
141// in), then the locale variables; English with nothing to go by
142async function claudeLanguage($: EngineInterface): Promise<string> {
143  const { language: replyLanguage } = (await $.settings.read()) as { language?: unknown }
144  const fromSetting = typeof replyLanguage === 'string' ? languageKey(replyLanguage) : null
145  if (fromSetting !== null) return fromSetting
146  const locale = (await $.env.get('LC_ALL')) || (await $.env.get('LC_MESSAGES')) || (await $.env.get('LANG')) || ''
147  return languageKey(locale) ?? 'en'
148}
149
150// The desktop app keeps its display language as `locale` in its own config.json,
151// an internal file that also holds its sign-in cache: read again only when its
152// mtime moves, and nothing but `locale` kept. Null where there is no such app.
153let appConfigMtime = -1
154
155async function readAppLanguage($: EngineInterface): Promise<string | null | undefined> {
156  const home = await $.env.get('HOME')
157  if (home === undefined) return null
158  const path = `${home}/Library/Application Support/Claude/config.json`
159  try {
160    const { mtimeMs } = await $.fs.stat(path)
161    if (mtimeMs === appConfigMtime) return undefined
162    appConfigMtime = mtimeMs
163    const { locale } = JSON.parse(String(await $.fs.read(path))) as { locale?: unknown }
164    return typeof locale === 'string' ? languageKey(locale) : null
165  } catch {
166    return null
167  }
168}
169
170// A translated table is kept in the store per language, stamped with the English
171// it came from, so a change of EN translates again
172const hashText = (text: string) => {
173  let hash = 5381
174  for (let i = 0; i < text.length; i += 1) hash = ((hash * 33) ^ text.charCodeAt(i)) >>> 0
175  return hash.toString(36)
176}
177const EN_VERSION = hashText(JSON.stringify(EN))
178const TRANSLATE_CLAIM_MS = 60_000
179const translating = new Set<string>()
180
181const placeholders = (template: string) => (template.match(/\{\w+\}/g) ?? []).sort().join()
182
183// A translation is taken whole or not at all: every key, every placeholder, one
184// line each, and no longer than the band can hold
185export const checkWords = (value: unknown): Words | null => {
186  if (typeof value !== 'object' || value === null) return null
187  const table = value as Record<string, unknown>
188  for (const key of Object.keys(EN) as (keyof Words)[]) {
189    const text = table[key]
190    if (typeof text !== 'string' || text.trim() === '' || /[\n\[\]]/.test(text)) return null
191    if (placeholders(text) !== placeholders(EN[key])) return null
192    if (text.length > Math.max(24, EN[key].length * 2.5)) return null
193  }
194  return Object.fromEntries((Object.keys(EN) as (keyof Words)[]).map(key => [key, String(table[key])])) as Words
195}
196
197// A language's table as kept: its words, or `failed` when its translation did not
198// hold, which keeps that language in English
199type StoredWords = { version: string; words?: Words; failed?: true }
200
201// Makes the table for `key` ready: written, already translated, kept in the
202// store, or translated now by one small model call (the band shows English
203// until it lands). A failure fixes English for that language, in every session,
204// until the English words change; nothing is tried again before then.
205async function ensureWords($: EngineInterface, key: string | null) {
206  if (key === null || key in WRITTEN || translating.has(key)) return
207  translating.add(key)
208  let claimKey: string | undefined
209  let isDone = false
210  const storeKey = `words:${key}`
211  try {
212    if ((await read($, translations))?.[key] !== undefined) return
213    const stored = (await $.store.get(storeKey)) as Partial<StoredWords> | undefined
214    if (stored?.version === EN_VERSION && stored.failed === true) return
215    const kept = stored?.version === EN_VERSION ? checkWords(stored.words) : null
216    if (kept !== null) {
217      await update($, translations, current => ({ ...current, [key]: kept }))
218      return
219    }
220    const now = await $.clock.now()
221    const claim = `translating:${key}`
222    if (Number((await $.store.get(claim)) ?? 0) > now) return
223    await $.store.set(claim, now + TRANSLATE_CLAIM_MS)
224    claimKey = claim
225    const result = await $.model.complete({
226      model: 'haiku',
227      system: 'You translate the user interface strings of a one-line status bar in a developer tool.',
228      prompt:
229        `Translate the values of this JSON object into the language "${key}" (a language name or a locale tag). ` +
230        'Keep every key. Keep each {placeholder} exactly as written. Keep "--" and the name Mini System Monitor ' +
231        'as they are. Make each value as short as a status bar needs. ' +
232        'Answer with the JSON object only.\n\n' +
233        JSON.stringify(EN),
234      maxTokens: 1024,
235      timeoutMs: 30_000,
236    })
237    if (!result.isAnswered) return
238    const json = /\{[\s\S]*\}/.exec(result.text)?.[0]
239    const words = json === undefined ? null : checkWords(JSON.parse(json))
240    if (words === null) return
241    await $.store.set(storeKey, { version: EN_VERSION, words })
242    await update($, translations, current => ({ ...current, [key]: words }))
243    isDone = true
244  } catch {
245    // English stays
246  } finally {
247    translating.delete(key)
248    if (claimKey !== undefined) {
249      await $.store.delete(claimKey).catch(() => undefined)
250      if (!isDone) await $.store.set(storeKey, { version: EN_VERSION, failed: true }).catch(() => undefined)
251    }
252  }
253}
254
255// Settles both languages and makes their tables ready; `ja` or `en` in the
256// option fixes them on every surface
257async function refreshLanguage($: EngineInterface, option: unknown) {
258  if (option === 'ja' || option === 'en') {
259    await update($, language, () => option)
260    await update($, appLanguage, () => null)
261    return
262  }
263  const claude = await claudeLanguage($)
264  await update($, language, current => (current === claude ? current : claude))
265  const app = await readAppLanguage($)
266  if (app !== undefined) await update($, appLanguage, current => (current === app ? current : app))
267  void ensureWords($, claude)
268  void ensureWords($, await read($, appLanguage))
269}
270
271export const register: Register = (on, options) => {
272  on('session.start', async ($, e, next) => {
273    await refreshLanguage($, options.language)
274    $.clock.every(LANGUAGE_CHECK_MS, () => void refreshLanguage($, options.language).catch(() => undefined))
275    void refresh($, options.monitorPath).catch(() => undefined)
276    // A reload drops the old timer with the old module
277    $.clock.every(REFRESH_EVERY_MS, () => void refresh($, options.monitorPath).catch(() => undefined))
278
279    return next(e)
280  })
281
282  // A `/config` change of Claude Code's language shows at once
283  on('config.set', async ($, e, next) => {
284    const result = await next(e)
285    await refreshLanguage($, options.language).catch(() => undefined)
286
287    return result
288  }).catch(($, e, next) => next(e))
289
290  on('turn.complete', async ($, e, next) => {
291    const result = await next(e)
292    // Subagent turns count in the session and window figures, not as "last"
293    if (e.agentId === undefined && e.usage !== undefined) {
294      const totals = turnTotals(e.usage)
295      await update($, lastTurn, () => totals)
296    }
297    void refresh($, options.monitorPath).catch(() => undefined)
298
299    return result
300  }).catch(($, e, next) => next(e))
301
302  on('ui.render', { component: 'AbovePrompt' }, async ($, e, next) => {
303    if (e.props.hasSurvey) {
304      return next(e)
305    }
306
307    const { Box, Text } = $.ui.resolve(e)
308    const app = e.surface === 'desktop' ? await read($, appLanguage) : null
309    const t = wordsFor(app ?? (await read($, language)), await read($, translations))
310    const current = await read($, reading)
311    const turn = await read($, lastTurn)
312    // The band is one site shared by every plugin: keep what the ones beneath draw
313    const below = await next(e)
314
315    const part = (label: string, totals: CacheTotals | null, withCounts: boolean) => {
316      const percent = totals === null ? null : rate(totals)
317      if (totals === null || percent === null) return null
318      return (
319        <Box key={label}>
320          <Text dimColor>{label} </Text>
321          <Text color={rateColor(percent)}>{percent}%</Text>
322          {totals.partial ? <Text dimColor>*</Text> : null}
323          {withCounts ? (
324            <Text dimColor>
325              {' '}
326              ({t.read} {short(totals.cacheRead)} / {t.write} {short(totals.cacheWrite)})
327            </Text>
328          ) : null}
329          <Text> </Text>
330        </Box>
331      )
332    }
333
334    // The last turn needs no monitor, so it shows whatever the monitor's state.
335    // No monitor at all is a choice, not a fault: its figures are left out without
336    // a word. A monitor that is there but too old or failing is named, since its
337    // owner expects the figures.
338    const trouble = await read($, problem)
339    const last = part(t.lastTurn, turn, false)
340    const rest =
341      trouble === 'missing' ? null : trouble !== null ? (
342        <Text color="yellow">{t[trouble]}</Text>
343      ) : (
344        <Box>
345          {part(t.session, current?.session ?? null, true)}
346          {current?.window == null ? null : <Text dimColor>│ </Text>}
347          {part(t.window.replace('{minutes}', String(current?.windowMinutes ?? 60)), current?.window ?? null, true)}
348        </Box>
349      )
350    const row =
351      last === null && (rest === null || (trouble === null && current === null)) ? null : (
352        <Box>
353          <Text dimColor>cache </Text>
354          {last}
355          {rest}
356        </Box>
357      )
358
359    return (
360      <Box flexDirection="column">
361        {row ?? null}
362        {below ?? null}
363      </Box>
364    )
365  })
366}
367
types/index.d.ts 37 lines
1// One count of input tokens, as Mini System Monitor reports it: `input` is all
2// input (uncached + cache reads + cache writes), `cacheRead` and `cacheWrite`
3// are parts of it
4export type CacheTotals = {
5  input: number
6  cacheRead: number
7  cacheWrite: number
8  partial: boolean
9}
10
11export type CacheReading = {
12  session: CacheTotals | null
13  window: CacheTotals | null
14  windowMinutes: number
15}
16
17// Why the session and Mac-wide figures are missing: no executable at the monitor
18// path, a monitor from before --claude-cache-json, or a run that failed
19export type CacheProblem = 'missing' | 'outdated' | 'failed'
20
21// The band's own words, as templates with {placeholders}
22export type Words = {
23  lastTurn: string
24  session: string
25  window: string
26  read: string
27  write: string
28  outdated: string
29  failed: string
30}
31
32declare module 'claude-code' {
33  interface PluginState {
34    'cache-band': { reading: CacheReading | null; lastTurn: CacheTotals | null; problem: CacheProblem | null; language: string | null; appLanguage: string | null; translations: Record<string, Words> | null }
35  }
36}
37