SLOPSHOPPER

Gauge

Context, cost and rate limits in the footer, warnings at thresholds you set, a compact helper, and /gauge for per-turn figures, the context breakdown and spend…

newspinnercommandtoaststatus
★ 1v0.1.2MITupdated 2026-10-10astrosteveo/claude-plugins/plugins/gauge
A shopper browsing a rack in a slop shop
Preview · a replayed session in a sandbox
claude · ~/work/app · gauge
› 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 › /gauge ⎿ gauge: this session ⎿ gauge: ⎿ gauge: Context 97k of 200k (49%) ⎿ gauge: Cost $0.42 ⎿ gauge: 5h 31% used, resets in 1760003600000 ⎿ gauge: ────────────────────────────────────────────────────────────────────────────────────────────────────────────────────── › ? for shortcuts auto-accept edits · ctx 97k/200k 49% · $0.42 · 5h 31%
README

Gauge

Gauge keeps track of how much context and usage a session spends. The right of the footer, under the prompt, shows the context, the cost and your rate-limit windows. Toasts warn you at thresholds you set. /gauge shows tokens and cost for each turn, what fills the context window, and your spend by day.

The footer

ctx 142k/200k 71% · $1.23 · 5h 31% · 7d 12%

The figures sit beside Claude Code's own mode labels at the right of the footer, and update after each reply and when a rate-limit window moves. A figure with no reading yet is left out. They are dim until a figure passes its warning threshold (below), which turns it yellow, and red from 95%.

Warnings

Each warning fires once when its figure crosses the line. If the figure drops back under, for example after /compact, it warns again the next time it crosses.

SettingDefaultWarns when
warnTokens200000the context passes this many tokens
warnPercent85the context passes this share of the window
limitPercent90a rate-limit window passes this share

Set any of them to 0 to turn it off.

Compact helper

compact sets what happens when the context passes compactTokens (0, the default, means the same as warnTokens):

  • suggest (the default): a toast tells you to run /compact. It replaces the token warning, so you get one toast.
  • auto: Gauge compacts the conversation itself, after the running turn ends.
  • off: nothing.

/gauge

  • /gauge: the current figures, then your last 20 turns, newest first. Each row has the model, uncached input, cache reads, cache writes, output, cache hit rate, cost, how long it ran and how long ago it was. The last row adds them up. Subagent turns are left out of the table, but their cost is in the session total.
  • /gauge context: what fills the context window, by category, largest first. It uses a local estimate. /gauge context full counts exactly, the way /context does.
  • /gauge history: cost, turns and tokens for each project for each of the last 14 days. History is kept for historyDays days (default 90), across sessions.

A turn's cost is how much the session total grew while it ran, so it includes any subagents it ran.

Settings

Change them in /config, or under pluginConfigs.gauge in your settings.

Requirements

Claude Code v2.1.296 or later. Mods are an early access part of Claude Code, so a Claude Code release can break the plugin until it is updated.

Install

/plugin install gauge --marketplace astrosteveo/claude-plugins
Source 3 files
hooks/register.tsx 281 lines
1import { atom, read, update } from 'claude-code'
2import type { EngineInterface, PluginOptions, Register, SessionMeasureInput } from 'claude-code'
3
4import type { Day, Measure, Spend, Turn } from '../types'
5import * as fmt from './format'
6
7const measure = atom({ plugin: 'gauge', key: 'measure' } as const, null)
8const turns = atom({ plugin: 'gauge', key: 'turns' } as const, [])
9const warned = atom({ plugin: 'gauge', key: 'warned' } as const, [])
10const start = atom({ plugin: 'gauge', key: 'start' } as const, null)
11const banked = atom({ plugin: 'gauge', key: 'banked' } as const, 0)
12
13const MAX_TURNS = 100
14const DAY_MS = 86_400_000
15
16type Settings = {
17  warnTokens: number
18  warnPercent: number
19  limitPercent: number
20  compact: 'off' | 'suggest' | 'auto'
21  compactTokens: number
22  historyDays: number
23}
24
25function settings(options: PluginOptions): Settings {
26  const num = (v: unknown, d: number) => (typeof v === 'number' && Number.isFinite(v) && v >= 0 ? v : d)
27  const compact = options.compact === 'off' || options.compact === 'auto' ? options.compact : 'suggest'
28  const warnTokens = num(options.warnTokens, 200_000)
29
30  return {
31    warnTokens,
32    warnPercent: num(options.warnPercent, 85),
33    limitPercent: num(options.limitPercent, 90),
34    compact,
35    compactTokens: num(options.compactTokens, 0) || warnTokens,
36    historyDays: num(options.historyDays, 90),
37  }
38}
39
40function toMeasure(e: Pick<SessionMeasureInput, 'context' | 'rateLimits' | 'cost'>): Measure {
41  return {
42    tokens: e.context.tokens,
43    window: e.context.window,
44    percent: e.context.percent,
45    limits: e.rateLimits.map(l => ({ kind: l.kind, percentUsed: l.percentUsed, resetsAt: l.resetsAt })),
46    usd: e.cost?.usd,
47  }
48}
49
50// What each threshold says once the figure passes it, keyed so each crossing
51// acts once and a figure that drops back re-arms.
52function crossings(m: Measure, s: Settings): Map<string, string> {
53  const past = new Map<string, string>()
54  if (s.warnTokens > 0 && m.tokens !== undefined && m.tokens >= s.warnTokens) {
55    past.set('tokens', `Context passed ${fmt.tokens(s.warnTokens)} tokens: now ${fmt.tokens(m.tokens)}.`)
56  }
57  if (s.warnPercent > 0 && m.percent !== undefined && m.percent >= s.warnPercent) {
58    past.set('percent', `Context is ${Math.round(m.percent)}% full.`)
59  }
60  if (s.compact !== 'off' && s.compactTokens > 0 && m.tokens !== undefined && m.tokens >= s.compactTokens) {
61    past.set(
62      'compact',
63      s.compact === 'auto'
64        ? `Context is ${fmt.tokens(m.tokens)} tokens: compacting.`
65        : `Context is ${fmt.tokens(m.tokens)} tokens. Run /compact to free it.`,
66    )
67  }
68  for (const l of m.limits) {
69    if (s.limitPercent > 0 && l.percentUsed >= s.limitPercent) {
70      past.set(`limit:${l.kind}`, `${fmt.limitName(l.kind)} rate limit is ${Math.round(l.percentUsed)}% used.`)
71    }
72  }
73
74  return past
75}
76
77// Whether a main-loop turn is running, and whether an automatic compaction
78// waits for it to end. A reload starts both over, between turns.
79let isRunning = false
80let isCompactDue = false
81// History writes run one after another so none reads a day another is writing.
82let writing: Promise<void> = Promise.resolve()
83
84function compactNow($: EngineInterface) {
85  isCompactDue = false
86  void $.session
87    .compact()
88    .then(r => {
89      if (r.skip !== undefined) $.ui.toast(`Compaction skipped: ${r.skip}`)
90    })
91    .catch(() => $.ui.toast('Gauge could not compact the conversation.'))
92}
93
94function addToday($: EngineInterface, add: Partial<Spend>) {
95  writing = writing.then(async () => {
96    const project = await $.session.root()
97    const key = `day:${fmt.dayKey(await $.clock.now())}`
98    const day = ((await $.store.get(key)) ?? {}) as Day
99    const was = day[project] ?? { usd: 0, input: 0, output: 0, cacheRead: 0, cacheWrite: 0, turns: 0 }
100    const next = { ...was }
101    for (const k of Object.keys(add) as (keyof Spend)[]) next[k] += add[k] ?? 0
102    await $.store.set(key, { ...day, [project]: next })
103  }).catch(() => {})
104
105  return writing
106}
107
108async function pruneHistory($: EngineInterface, days: number) {
109  const cutoff = fmt.dayKey((await $.clock.now()) - days * DAY_MS)
110  for (const key of await $.store.keys()) {
111    if (key.startsWith('day:') && key.slice(4) < cutoff) await $.store.delete(key)
112  }
113}
114
115// The cost the running or last turn added: the session total less the total
116// as it started.
117async function priceTurn($: EngineInterface, usd: number | undefined) {
118  const began = await read($, start)
119  if (usd === undefined || began === null) return
120  await update($, turns, list =>
121    list.map(t => (t.turnId === began.turnId ? { ...t, usd: Math.max(0, usd - began.usd) } : t)),
122  )
123}
124
125
126export const register: Register = (on, options) => {
127  const s = settings(options)
128
129  on('session.start', async ($, e, next) => {
130    const result = await next(e)
131    try {
132      await $.command.register({
133        name: 'gauge',
134        description: 'Context, cost and rate limits; /gauge context for the breakdown, /gauge history for spend by day',
135      })
136    } catch {
137      // A clash with another command leaves the footer and toasts working.
138    }
139    const usage = await $.session.usage()
140    const m = toMeasure(usage)
141    await update($, measure, () => m)
142    // A new session banks from the cost it starts at, so a resumed session's
143    // earlier spend is not counted twice. A reload keeps what it banked.
144    const { version } = await $.state.get({ plugin: 'gauge', key: 'banked' })
145    if (version === 0) await update($, banked, () => m.usd ?? 0)
146    // The figures draw in the footer; clear the notice an earlier version pinned.
147    $.ui.status(undefined)
148    await pruneHistory($, s.historyDays).catch(() => {})
149
150    return result
151  })
152
153  on('turn.start', async ($, e, next) => {
154    isRunning = true
155    const m = await read($, measure)
156    const usd = m?.usd ?? (await $.session.usage()).cost?.usd
157    await update($, start, () => (usd === undefined ? null : { turnId: e.turnId, usd }))
158
159    return next(e)
160  })
161
162  on('turn.complete', async ($, e, next) => {
163    const result = await next(e)
164    const u = e.usage
165    if (u !== undefined) {
166      void addToday($, {
167        input: u.input_tokens,
168        output: u.output_tokens,
169        cacheRead: u.cache_read_input_tokens,
170        cacheWrite: u.cache_creation_input_tokens,
171        turns: e.agentId === undefined ? 1 : 0,
172      })
173    }
174    if (e.agentId !== undefined) return result
175
176    isRunning = false
177    const turn: Turn = {
178      turnId: e.turnId,
179      at: (await $.clock.now()) - e.durationMs,
180      model: u?.model ?? 'unknown',
181      input: u?.input_tokens ?? 0,
182      cacheRead: u?.cache_read_input_tokens ?? 0,
183      cacheWrite: u?.cache_creation_input_tokens ?? 0,
184      output: u?.output_tokens ?? 0,
185      ms: e.durationMs,
186      isAborted: e.isAborted,
187    }
188    await update($, turns, list => [...list.filter(t => t.turnId !== turn.turnId), turn].slice(-MAX_TURNS))
189    // A measurement that came before the turn's end already holds its cost.
190    await priceTurn($, (await read($, measure))?.usd)
191    if (isCompactDue) compactNow($)
192
193    return result
194  })
195
196  on('session.measure', async ($, e, next) => {
197    const m = toMeasure(e)
198    await update($, measure, () => m)
199
200    if (e.changed.includes('cost') && m.usd !== undefined) {
201      await priceTurn($, m.usd)
202      const was = await read($, banked)
203      // A total below what was banked is a ledger that started over.
204      const added = m.usd >= was ? m.usd - was : m.usd
205      await update($, banked, () => m.usd ?? 0)
206      if (added > 0) void addToday($, { usd: added })
207    }
208
209    const past = crossings(m, s)
210    const before = new Set(await read($, warned))
211    const fresh = [...past.keys()].filter(k => !before.has(k))
212    // The compact notice names the tokens already: one toast, not two.
213    const shown = fresh.includes('compact') ? fresh.filter(k => k !== 'tokens') : fresh
214    for (const key of shown) $.ui.toast(past.get(key) ?? '')
215    await update($, warned, () => [...past.keys()])
216    if (fresh.includes('compact') && s.compact === 'auto') {
217      if (isRunning) isCompactDue = true
218      else compactNow($)
219    }
220
221    return next(e)
222  })
223
224  // The figures at the right of the footer, beside the engine's mode labels:
225  // dim, yellow past a warning threshold, red nearly full.
226  on('ui.render', { component: 'SessionMode' }, async ($, e, next) => {
227    const m = await read($, measure)
228    const parts = m === null ? [] : fmt.statusParts(m, s)
229    if (parts.length === 0) return next(e)
230    const { Box, Text } = $.ui.resolve(e)
231    const modes = e.props.modes.join(' & ')
232
233    return (
234      <Box>
235        {modes !== '' && <Text dimColor>{modes} · </Text>}
236        {parts.flatMap((p, i) => [
237          ...(i > 0 ? [<Text dimColor> · </Text>] : []),
238          p.level === 'ok' ? <Text dimColor>{p.text}</Text> : <Text color={p.level === 'high' ? 'error' : 'warning'}>{p.text}</Text>,
239        ])}
240      </Box>
241    )
242  })
243
244  on('command.run', { command: 'gauge' }, async ($, e) => {
245    const [what = '', detail = ''] = e.args.trim().split(/\s+/)
246
247    if (what === 'context') {
248      const usage = await $.session.usage({
249        breakdown: detail === 'full' ? 'full' : 'summary',
250        columns: e.presentation.columns,
251      })
252      const b = usage.context.breakdown
253      if (b === undefined) return { text: 'No context breakdown is available in this session.' }
254
255      return { text: fmt.breakdown(b.categories, b.totalTokens, b.rawMaxTokens) }
256    }
257
258    if (what === 'history') {
259      await writing
260      const days: [string, Day][] = []
261      const today = await $.clock.now()
262      for (let i = 0; i < 14; i++) {
263        const key = fmt.dayKey(today - i * DAY_MS)
264        const day = (await $.store.get(`day:${key}`)) as Day | undefined
265        if (day !== undefined) days.push([key, day])
266      }
267
268      return { text: `spend, last 14 days\n\n${fmt.history(days)}` }
269    }
270
271    if (what !== '') return { text: 'Usage: /gauge, /gauge context [full], /gauge history' }
272
273    const m = await read($, measure)
274    const list = await read($, turns)
275
276    const now = await $.clock.now()
277
278    return { text: `this session\n\n${fmt.summary(m, now)}\n\n${fmt.turnTable(list.slice(-20), now)}` }
279  })
280}
281
hooks/format.ts 217 lines
1import type { ContextCategory } from 'claude-code'
2
3import type { Day, Limit, Measure, Spend, Turn } from '../types'
4
5// Token counts read short: 950, 12.3k, 142k, 1.2M.
6export function tokens(n: number): string {
7  if (n < 1_000) return String(Math.round(n))
8  if (n < 10_000) return `${trim(n / 1_000)}k`
9  if (n < 1_000_000) return `${Math.round(n / 1_000)}k`
10
11  return `${trim(n / 1_000_000)}M`
12}
13
14function trim(n: number): string {
15  return n.toFixed(1).replace(/\.0$/, '')
16}
17
18// Dollars: $1.23, and under a cent as <$0.01.
19export function usd(n: number): string {
20  if (n > 0 && n < 0.005) return '<$0.01'
21
22  return `$${n.toFixed(2)}`
23}
24
25// Durations read short: 3.2s, 48s, 2m 5s, 1h 10m.
26export function duration(ms: number): string {
27  const s = ms / 1_000
28  if (s < 10) return `${s.toFixed(1)}s`
29  if (s < 60) return `${Math.round(s)}s`
30  const m = Math.floor(s / 60)
31  if (m < 60) return `${m}m ${Math.round(s % 60)}s`
32
33  return `${Math.floor(m / 60)}h ${m % 60}m`
34}
35
36// A rate-limit window's short name: five_hour is 5h, seven_day 7d.
37export function limitName(kind: string): string {
38  const named: Record<string, string> = { five_hour: '5h', seven_day: '7d', spend_limit: 'spend' }
39
40  return named[kind] ?? kind.replace(/_/g, ' ')
41}
42
43export function limit(l: Limit): string {
44  return `${limitName(l.kind)} ${Math.round(l.percentUsed)}%`
45}
46
47// How close a figure is to its limit: under its warning, past it, or nearly full.
48export type Level = 'ok' | 'warn' | 'high'
49export type Part = { text: string; level: Level }
50// The warning thresholds; 0 turns one off.
51export type Lines = { warnTokens: number; warnPercent: number; limitPercent: number }
52
53const FULL = 95
54
55function past(n: number | undefined, line: number): boolean {
56  return line > 0 && n !== undefined && n >= line
57}
58
59// The footer's figures, each with its level: ctx 142k/200k 71%, $1.23, 5h 31%.
60export function statusParts(m: Measure, lines: Lines): Part[] {
61  const parts: Part[] = []
62  if (m.tokens !== undefined) {
63    const pct = m.percent === undefined ? '' : ` ${Math.round(m.percent)}%`
64    const level: Level = past(m.percent, FULL)
65      ? 'high'
66      : past(m.tokens, lines.warnTokens) || past(m.percent, lines.warnPercent)
67        ? 'warn'
68        : 'ok'
69    parts.push({ text: `ctx ${tokens(m.tokens)}/${tokens(m.window)}${pct}`, level })
70  }
71  if (m.usd !== undefined) parts.push({ text: usd(m.usd), level: 'ok' })
72  for (const l of m.limits) {
73    const level: Level = l.percentUsed >= FULL ? 'high' : past(l.percentUsed, lines.limitPercent) ? 'warn' : 'ok'
74    parts.push({ text: limit(l), level })
75  }
76
77  return parts
78}
79
80// The figures as one line: ctx 142k/200k 71% · $1.23 · 5h 31% · 7d 12%.
81export function statusLine(m: Measure): string {
82  return statusParts(m, { warnTokens: 0, warnPercent: 0, limitPercent: 0 })
83    .map(p => p.text)
84    .join(' · ')
85}
86
87// What share of a turn's input the prompt cache served, as a whole percent.
88export function hitRate(input: number, cacheRead: number, cacheWrite: number): number {
89  const all = input + cacheRead + cacheWrite
90
91  return all === 0 ? 0 : Math.round((cacheRead / all) * 100)
92}
93
94// A model id without its family prefix: claude-opus-5-5 is opus-5-5.
95export function model(id: string): string {
96  return id.replace(/^claude-/, '')
97}
98
99function table(rows: string[][], right: boolean[]): string {
100  const widths = (rows[0] ?? []).map((_, i) => Math.max(...rows.map(r => (r[i] ?? '').length)))
101
102  return rows
103    .map(r => r.map((cell, i) => (right[i] ? cell.padStart(widths[i] ?? 0) : cell.padEnd(widths[i] ?? 0))).join('  ').trimEnd())
104    .join('\n')
105}
106
107// How long until a time: 42m, 4h 10m, 6d 7h.
108export function until(at: string, now: number): string {
109  const ms = Date.parse(at) - now
110  if (!Number.isFinite(ms)) return at
111  const m = Math.max(0, Math.round(ms / 60_000))
112  if (m < 60) return `${m}m`
113  if (m < 1_440) return `${Math.floor(m / 60)}h ${m % 60}m`
114
115  return `${Math.floor(m / 1_440)}d ${Math.floor((m % 1_440) / 60)}h`
116}
117
118// The current figures, one per line.
119export function summary(m: Measure | null, now: number): string {
120  if (m === null) return 'No measurement yet: figures arrive after the first reply.'
121  const lines: string[] = []
122  if (m.tokens !== undefined) {
123    const pct = m.percent === undefined ? '' : ` (${Math.round(m.percent)}%)`
124    lines.push(`Context   ${tokens(m.tokens)} of ${tokens(m.window)}${pct}`)
125  } else {
126    lines.push(`Context   window ${tokens(m.window)}`)
127  }
128  if (m.usd !== undefined) lines.push(`Cost      ${usd(m.usd)}`)
129  for (const l of m.limits) {
130    lines.push(`${limitName(l.kind).padEnd(10)}${Math.round(l.percentUsed)}% used${l.resetsAt ? `, resets in ${until(l.resetsAt, now)}` : ''}`)
131  }
132
133  return lines.join('\n')
134}
135
136// The last turns, newest first, with a totals row.
137export function turnTable(turns: Turn[], now: number): string {
138  if (turns.length === 0) return 'No turns yet.'
139  const head = ['model', 'in', 'cache rd', 'cache wr', 'out', 'hit', 'cost', 'time', 'ago']
140  const rows = [...turns].reverse().map(t => [
141    model(t.model),
142    tokens(t.input),
143    tokens(t.cacheRead),
144    tokens(t.cacheWrite),
145    tokens(t.output),
146    `${hitRate(t.input, t.cacheRead, t.cacheWrite)}%`,
147    t.usd === undefined ? '-' : usd(t.usd),
148    t.isAborted ? 'stopped' : duration(t.ms),
149    duration(Math.max(0, now - t.at)),
150  ])
151  const sum = (f: (t: Turn) => number) => turns.reduce((a, t) => a + f(t), 0)
152  const input = sum(t => t.input)
153  const cacheRead = sum(t => t.cacheRead)
154  const cacheWrite = sum(t => t.cacheWrite)
155  const priced = turns.filter(t => t.usd !== undefined)
156  const total = [
157    turns.length === 1 ? '1 turn' : `${turns.length} turns`,
158    tokens(input),
159    tokens(cacheRead),
160    tokens(cacheWrite),
161    tokens(sum(t => t.output)),
162    `${hitRate(input, cacheRead, cacheWrite)}%`,
163    priced.length === 0 ? '-' : usd(priced.reduce((a, t) => a + (t.usd ?? 0), 0)),
164    duration(sum(t => t.ms)),
165    '',
166  ]
167
168  return table([head, ...rows, total], [false, true, true, true, true, true, true, true, true])
169}
170
171// The context window by category, largest first, free space last.
172export function breakdown(categories: readonly ContextCategory[], total: number, max: number): string {
173  const used = categories.filter(c => c.kind === 'used' && c.tokens > 0).sort((a, b) => b.tokens - a.tokens)
174  const rest = categories.filter(c => c.kind === 'free' || c.kind === 'buffer')
175  const share = (n: number) => `${max === 0 ? 0 : Math.round((n / max) * 100)}%`
176  const rows = [...used, ...rest].map(c => [c.name, tokens(c.tokens), share(c.tokens)])
177  const deferred = categories.filter(c => c.kind === 'deferred' && c.tokens > 0)
178  const out = [
179    `context window, ${tokens(total)} of ${tokens(max)} (${share(total)})`,
180    '',
181    table(rows, [false, true, true]),
182  ]
183  if (deferred.length > 0) {
184    out.push('', `Loaded on demand, outside the window: ${deferred.map(c => `${c.name} ${tokens(c.tokens)}`).join(', ')}`)
185  }
186
187  return out.join('\n')
188}
189
190// The history: one row per day and project, newest first, with totals.
191export function history(days: [string, Day][]): string {
192  const rows: string[][] = []
193  const all: Spend = { usd: 0, input: 0, output: 0, cacheRead: 0, cacheWrite: 0, turns: 0 }
194  for (const [date, day] of days) {
195    for (const [project, s] of Object.entries(day).sort((a, b) => b[1].usd - a[1].usd)) {
196      rows.push([date, base(project), usd(s.usd), String(s.turns), tokens(s.input + s.cacheRead + s.cacheWrite), tokens(s.output)])
197      for (const k of Object.keys(all) as (keyof Spend)[]) all[k] += s[k]
198    }
199  }
200  if (rows.length === 0) return 'No history yet.'
201  const total = ['total', '', usd(all.usd), String(all.turns), tokens(all.input + all.cacheRead + all.cacheWrite), tokens(all.output)]
202
203  return table([['day', 'project', 'cost', 'turns', 'in', 'out'], ...rows, total], [false, false, true, true, true, true])
204}
205
206function base(path: string): string {
207  return path.split('/').filter(Boolean).pop() ?? path
208}
209
210// A local calendar day, YYYY-MM-DD.
211export function dayKey(ms: number): string {
212  const d = new Date(ms)
213  const pad = (n: number) => String(n).padStart(2, '0')
214
215  return `${d.getFullYear()}-${pad(d.getMonth() + 1)}-${pad(d.getDate())}`
216}
217
types/index.d.ts 57 lines
1// One rate-limit window as the last measurement reported it.
2export type Limit = { kind: string; percentUsed: number; resetsAt?: string }
3
4// The session's latest measurement. `tokens` and `percent` are absent before
5// the first response; `usd` where the host keeps no cost ledger.
6export type Measure = {
7  tokens?: number
8  window: number
9  percent?: number
10  limits: Limit[]
11  usd?: number
12}
13
14// One main-loop turn: who answered, its four token counts, how long it ran
15// and what it cost. `usd` arrives with the first measurement after the turn.
16export type Turn = {
17  turnId: string
18  at: number
19  model: string
20  input: number
21  cacheRead: number
22  cacheWrite: number
23  output: number
24  ms: number
25  isAborted: boolean
26  usd?: number
27}
28
29// One project's figures for one day of history.
30export type Spend = {
31  usd: number
32  input: number
33  output: number
34  cacheRead: number
35  cacheWrite: number
36  turns: number
37}
38
39// A day of history, keyed by project root.
40export type Day = Record<string, Spend>
41
42declare module 'claude-code' {
43  interface PluginState {
44    gauge: {
45      measure: Measure | null
46      // The last turns, oldest first.
47      turns: Turn[]
48      // The thresholds past which a figure is now, so each crossing acts once.
49      warned: string[]
50      // The session's cost as the running turn started.
51      start: { turnId: string; usd: number } | null
52      // The session cost already added to the history.
53      banked: number
54    }
55  }
56}
57