SLOPSHOPPER

usage-meter

Status-line meter for context fill, 5-hour and weekly plan limits, and session cost, plus /usage-meter for Remote Control.

newcommandtoaststatus
★ 3v0.1.0MITupdated 2026-10-01LeeHigma0201/claude-code-mods/mods/usage-meter
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 › /usage-meter ⎿ usage-meter: Usage meter ⎿ usage-meter: - context: 49% of 200,000 tokens (97,400 used) ⎿ usage-meter: - 5h limit: 31% used ⎿ usage-meter: - session cost (API-equivalent, not subscription usage): $0.42 ────────────────────────────────────────────────────────────────────────────────────────────────────────────────────── › ? for shortcuts ⚠ usage-meter: ctx 49% · 5h 31% · $0.42
README

usage-meter

Shows where the session stands against its limits. It is read-only and never nudges you to wrap up.

  • Status line, refreshed after every turn and whenever a plan window moves a point: ctx 42% · 5h 81% · 7d 23% · $3.10. That is context fill, the 5-hour and 7-day plan windows, and session cost. Cost is the API-equivalent figure, not subscription usage; /money owns real spend.
  • Toast the first time a plan window crosses 80% and 95% (once per window per reset).
  • /usage-meter: the same readings with reset times, as a transcript line. Use this one from the phone over Remote Control, where the status line isn't drawn.

Plan windows show only on a subscription, after the first response of the session.

Hooks: session.start (registers the command), session.measure, command.run. Calls: $.session.usage, $.ui.status, $.ui.toast, $.clock.now, $.command.register.

Test: claude plugin test . (3 tests). Uninstall: see the repo README.

Source 1 files
hooks/register.ts 82 lines
1import type { Register, SessionContextUsage, SessionCost, SessionRateLimit } from 'claude-code'
2
3// Plan-limit percentages that raise a one-time toast per window.
4const THRESHOLDS = [80, 95]
5const LABELS: Record<string, string> = { five_hour: '5h', seven_day: '7d', spend_limit: 'spend' }
6
7type Usage = { context: SessionContextUsage; rateLimits: SessionRateLimit[]; cost?: SessionCost }
8
9// Toasts already shown, keyed by window, reset time and threshold.
10const warned = new Set<string>()
11
12export function label(kind: string): string {
13  return LABELS[kind] ?? kind
14}
15
16export function statusText(u: Usage): string {
17  const parts: string[] = []
18  if (u.context.percent !== undefined) parts.push(`ctx ${u.context.percent}%`)
19  for (const r of u.rateLimits) parts.push(`${label(r.kind)} ${r.percentUsed}%`)
20  if (u.cost) parts.push(`$${u.cost.usd.toFixed(2)}`)
21  return parts.join(' · ')
22}
23
24function resetIn(iso: string | undefined, now: number): string {
25  if (!iso) return ''
26  const ms = Date.parse(iso) - now
27  if (!Number.isFinite(ms) || ms <= 0) return ''
28  const h = Math.floor(ms / 3_600_000)
29  const m = Math.round((ms % 3_600_000) / 60_000)
30  return h >= 24 ? ` (resets in ${Math.floor(h / 24)}d ${h % 24}h)` : ` (resets in ${h}h ${m}m)`
31}
32
33export function reportText(u: Usage, now: number): string {
34  const lines = ['Usage meter']
35  const ctx = u.context
36  lines.push(
37    ctx.percent === undefined
38      ? `- context: no reading yet (window ${ctx.window.toLocaleString('en-US')} tokens)`
39      : `- context: ${ctx.percent}% of ${ctx.window.toLocaleString('en-US')} tokens (${(ctx.tokens ?? 0).toLocaleString('en-US')} used)`,
40  )
41  if (u.rateLimits.length === 0) lines.push('- plan limits: no reading yet (none reported, or not on a subscription)')
42  for (const r of u.rateLimits) lines.push(`- ${label(r.kind)} limit: ${r.percentUsed}% used${resetIn(r.resetsAt, now)}`)
43  lines.push(u.cost ? `- session cost (API-equivalent, not subscription usage): $${u.cost.usd.toFixed(2)}` : '- session cost: not tracked here')
44  return lines.join('\n')
45}
46
47// Thresholds crossed for the first time this module load.
48export function newWarnings(limits: SessionRateLimit[]): string[] {
49  const out: string[] = []
50  for (const r of limits) {
51    for (const t of THRESHOLDS) {
52      const key = `${r.kind}|${r.resetsAt ?? ''}|${t}`
53      if (r.percentUsed >= t && !warned.has(key)) {
54        warned.add(key)
55        out.push(`${label(r.kind)} plan limit at ${r.percentUsed}%`)
56      }
57    }
58  }
59  return out
60}
61
62export const register: Register = on => {
63  on('session.start', async ($, e, next) => {
64    await $.command.register({
65      name: 'usage-meter',
66      description: 'Show context fill, plan-limit windows and session cost',
67    })
68    return next(e)
69  })
70
71  on('session.measure', async ($, e, next) => {
72    $.ui.status(statusText(e) || undefined)
73    for (const w of newWarnings(e.rateLimits)) $.ui.toast(w)
74    return next(e)
75  })
76
77  on('command.run', { command: 'usage-meter' }, async $ => {
78    const u = await $.session.usage()
79    return { text: reportText(u, await $.clock.now()) }
80  })
81}
82