SLOPSHOPPER

usage-meter

Live context, rate limit and cost meter above the prompt

newbandtoasttimer
A shopper browsing a rack in a slop shop
Preview · a replayed session in a sandbox
claude · ~/work/app · usage-meter
› 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 ⟨Claude Code's own drawing⟩ ctx ▓▓▓▓▓░░░░░ 49% (97k/200k) · 5h: 31% (resets NaNm) · $0.42 last turn +0k ctx, +$0.00 ────────────────────────────────────────────────────────────────────────────────────────────────────────────────────── › ? for shortcuts

Draws

Band
⟨Claude Code's own drawing⟩ ctx ▓▓▓▓▓░░░░░ 49% (97k/200k) · 5h: 31% (resets NaNm) · $0.42 last turn +0k ctx, +$0.00
README

usage-meter

A live band above the prompt showing how much of your context, rate limits and money the session is using, and what the last turn added.

ctx ▓▓▓▓▓▓░░░░ 58% (116k/200k)  ·  5h: 23% (resets 2h14m)  ·  7d: 65% (resets 1d17h)  ·  $1.84
last turn +12k ctx, +$0.21

What it shows

ItemMeaning
ctxContext window fill: tokens the last response was answered over, against the model's window
5h / 7dSubscription rate limit windows and the time until each resets (Pro/Max only; absent with an API key)
$Session cost so far, as /cost totals it
last turnContext growth and cost of the last main turn, subagent spend included. Negative after a /compact

A toast warns each time the context crosses 80%.

Access

Reads the session's usage figures only. No network, no files, no model calls, no cost.

Install

/plugin install usage-meter --marketplace TomDiem/claude-code-mods
Source 2 files
hooks/register.tsx 138 lines
1import { atom, read, update } from 'claude-code'
2import type { EngineInterface, Register } from 'claude-code'
3
4import type { Turn, Usage } from '../types'
5
6const usage = atom({ plugin: 'usage-meter', key: 'usage' } as const, null)
7const last = atom({ plugin: 'usage-meter', key: 'last' } as const, null)
8const now = atom({ plugin: 'usage-meter', key: 'now' } as const, 0)
9
10const MINUTE = 60_000
11const WARN_PERCENT = 80
12const LABELS: Record<string, string> = { five_hour: '5h', seven_day: '7d' }
13
14const BAR_WIDTH = 10
15
16const k = (tokens: number) => `${Math.round(tokens / 1000)}k`
17
18const bar = (percent: number) => {
19  const filled = Math.round((Math.min(percent, 100) / 100) * BAR_WIDTH)
20  return '▓'.repeat(filled) + '░'.repeat(BAR_WIDTH - filled)
21}
22
23const dollars = (amount: number) => `$${amount.toFixed(2)}`
24
25const signed = (n: number, unit: (n: number) => string) => `${n < 0 ? '-' : '+'}${unit(Math.abs(n))}`
26
27const duration = (ms: number) => {
28  const minutes = Math.max(0, Math.round(ms / MINUTE))
29  const days = Math.floor(minutes / 1440)
30  const hours = Math.floor((minutes % 1440) / 60)
31
32  if (days > 0) {
33    return `${days}d${hours}h`
34  }
35
36  return hours > 0 ? `${hours}h${minutes % 60}m` : `${minutes}m`
37}
38
39const totals = ({ context, rateLimits, usd }: Usage, time: number) => {
40  const parts: string[] = []
41
42  if (context.tokens !== undefined && context.percent !== undefined) {
43    parts.push(`ctx ${bar(context.percent)} ${context.percent}% (${k(context.tokens)}/${k(context.window)})`)
44  }
45
46  for (const { kind, percentUsed, resetsAt } of rateLimits) {
47    const reset = resetsAt ? ` (resets ${duration(Date.parse(resetsAt) - time)})` : ''
48    parts.push(`${LABELS[kind] ?? kind}: ${percentUsed}%${reset}`)
49  }
50
51  if (usd !== undefined) {
52    parts.push(dollars(usd))
53  }
54
55  return parts.join('  ·  ')
56}
57
58const delta = ({ tokens, usd }: Turn) => `last turn ${signed(tokens, k)} ctx, ${signed(usd, dollars)}`
59
60const snapshot = async ($: EngineInterface): Promise<Turn | null> => {
61  const { context, cost } = await $.session.usage()
62  return context.tokens === undefined ? null : { tokens: context.tokens, usd: cost?.usd ?? 0 }
63}
64
65export const register: Register = on => {
66  let baseline: Turn | null = null
67
68  on('session.start', async ($, e, next) => {
69    const tick = async () => {
70      const time = await $.clock.now()
71      await update($, now, () => time)
72    }
73
74    await tick()
75    $.clock.every(MINUTE, tick)
76
77    return next(e)
78  })
79
80  on('session.measure', async ($, e, next) => {
81    const previous = (await read($, usage))?.context.percent ?? 0
82    const percent = e.context.percent ?? 0
83
84    if (previous < WARN_PERCENT && percent >= WARN_PERCENT) {
85      $.ui.toast(`Context at ${percent}%: consider /compact`)
86    }
87
88    await update($, usage, () => ({ context: e.context, rateLimits: e.rateLimits, usd: e.cost?.usd }))
89
90    return next(e)
91  })
92
93  on('turn.start', async ($, e, next) => {
94    if (!e.agentId) {
95      baseline = await snapshot($)
96    }
97
98    return next(e)
99  })
100
101  on('turn.complete', async ($, e, next) => {
102    const before = baseline
103
104    if (!e.agentId && before) {
105      baseline = null
106      const after = await snapshot($)
107
108      if (after) {
109        await update($, last, () => ({ tokens: after.tokens - before.tokens, usd: after.usd - before.usd }))
110      }
111    }
112
113    return next(e)
114  })
115
116  on('ui.render', { component: 'AbovePrompt' }, async ($, e, next) => {
117    const rest = await next(e)
118    const current = await read($, usage)
119    const turn = await read($, last)
120    const summary = current ? totals(current, await read($, now)) : ''
121    const change = turn ? delta(turn) : ''
122
123    if (e.props.hasSurvey || (!summary && !change)) {
124      return rest
125    }
126
127    const { Box, Text } = $.ui.resolve(e)
128
129    return (
130      <Box flexDirection="column">
131        {rest}
132        {summary ? <Text dimColor>{summary}</Text> : null}
133        {change ? <Text dimColor>{change}</Text> : null}
134      </Box>
135    )
136  })
137}
138
types/index.d.ts 14 lines
1export type Usage = {
2  context: { tokens?: number; window: number; percent?: number }
3  rateLimits: { kind: string; percentUsed: number; resetsAt?: string }[]
4  usd?: number
5}
6
7export type Turn = { tokens: number; usd: number }
8
9declare module 'claude-code' {
10  interface PluginState {
11    'usage-meter': { usage: Usage | null; last: Turn | null; now: number }
12  }
13}
14