SLOPSHOPPER

usage-watch

A status line of the session's usage and a warning when a plan window fills: session.measure pins `ctx 42% | 5h 61% | 7d 18% | $0.42` with $.ui.status and…

newtoaststatus
A shopper browsing a rack in a slop shop
Preview · a replayed session in a sandbox
claude · ~/work/app · usage-watch
› 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 ────────────────────────────────────────────────────────────────────────────────────────────────────────────────────── › ? for shortcuts ⚠ usage-watch: ctx 49% | 5h 31% | $0.42
README

usage-watch

セッションの使用量をステータス行に出し、プラン枠が埋まってきたら警告する Mod。

何をするか

  • session.measure を購読して、ctx 42% | 5h 61% | 7d 18% | $0.42 という 1 行を $.ui.status() でプロンプト下にピン留めする。
  • レート制限の窓 (five_hour / seven_day / spend_limit) が 80% / 90% を上向きに 跨いだ瞬間だけ $.ui.toast() で警告する。跨いだ段は $.store に保存するので、 再起動しても同じ段で二度鳴らない。下がれば再武装する。
  • session.start で $.session.usage() を 1 回だけ読み、その戻り値そのままを PROOF-usage.json に書く (本の中で実物の形を見せるため)。
  • session.measure が走るたびに PROOF-measure.log へ 1 行追記する。

読み込み方

関数フックは早期アクセス機能なので CLAUDE_CODE_ENABLE_FUNCTION_HOOKS=1 が要る。

claude --plugin-dir C:/Users/lifes/kasegu/mods/usage-watch

ヘッドレスで確かめるなら:

claude -p "hello" --plugin-dir C:/Users/lifes/kasegu/mods/usage-watch --output-format text

-p では画面に何も描かれない ($.ui.status はデバッグログに no status row in a headless session と残るだけ)。証拠はプルーフファイルで見る。

手が届く範囲 (Claude Code 2.1.278 で検証済み)

❯ ./register.ts hooks: session.start, session.measure
❯ ./register.ts calls: $.fs.exists, $.fs.read, $.fs.write, $.session.usage, $.store.get, $.store.set, $.ui.status, $.ui.toast
✔ Validation passed

観測専用のイベントしか取らない。ツール呼び出しもプロンプトも見ないし、書き込むのは 自分のディレクトリ配下の 2 つのプルーフファイルだけ。ネットワークには一切触れない。

テスト

claude plugin test .

tests/register.test.ts が、ステータス行の文字列・プルーフログの追記・ 80/90% の立ち上がりエッジ (79 → 83 → 88 → 91 で 2 回だけ鳴ること) を検証する。

Source 1 files
hooks/register.ts 70 lines
1import type { Register } from 'claude-code'
2
3/** The steps a window is warned at, highest first, so 93% reports 90 once. */
4const STEPS = [90, 80] as const
5
6/** The $.store key holding the last step each window was warned at. */
7const KEY = 'crossedSteps'
8
9/** Short names for the windows the engine reports. */
10const SHORT: Record<string, string> = {
11  five_hour: '5h',
12  seven_day: '7d',
13  spend_limit: 'spend',
14}
15
16/** The highest step `percent` has reached, or 0 below them all. */
17const stepOf = (percent: number): number => STEPS.find(step => percent >= step) ?? 0
18
19export const register: Register = on => {
20  // Mirrors the store, so a burst of measurements does not re-read it. The
21  // store is what survives a restart; this is what makes the edge cheap.
22  let crossed: Record<string, number> = {}
23
24  on('session.start', async ($, e, next) => {
25    const r = await next(e)
26    const saved = await $.store.get(KEY)
27    if (saved !== null && typeof saved === 'object') {
28      crossed = { ...(saved as Record<string, number>) }
29    }
30    // Read the op once so the book can print the real shape of what it answers.
31    const usage = await $.session.usage()
32    const empty = usage.rateLimits.length === 0
33    await $.fs.write(
34      `${$.plugin.root}/PROOF-usage.json`,
35      `${JSON.stringify(usage, null, 2)}\n` +
36        `// rateLimits was ${empty ? 'EMPTY' : `${usage.rateLimits.length} window(s)`}\n`,
37    )
38    return r
39  }).catch(($, e, next) => (next.called ? undefined : next(e)))
40
41  on('session.measure', async ($, e, next) => {
42    const r = await next(e)
43
44    const parts = [`ctx ${e.context.percent ?? 0}%`]
45    for (const window of e.rateLimits) {
46      parts.push(`${SHORT[window.kind] ?? window.kind} ${window.percentUsed}%`)
47    }
48    if (e.cost !== undefined) parts.push(`$${e.cost.usd.toFixed(2)}`)
49    const text = parts.join(' | ')
50    $.ui.status(text)
51
52    // Rising edge only: a window that falls back below a step re-arms it.
53    for (const window of e.rateLimits) {
54      const step = stepOf(window.percentUsed)
55      if (step > (crossed[window.kind] ?? 0)) {
56        $.ui.toast(`usage-watch: ${SHORT[window.kind] ?? window.kind} at ${window.percentUsed}% of the window`)
57      }
58      crossed[window.kind] = step
59    }
60    await $.store.set(KEY, crossed)
61
62    const path = `${$.plugin.root}/PROOF-measure.log`
63    const line = `${new Date().toISOString()} changed=[${e.changed.join(',')}] ${text}\n`
64    const prior = (await $.fs.exists(path)) ? await $.fs.read(path) : ''
65    await $.fs.write(path, `${prior}${line}`)
66
67    return r
68  }).catch(($, e, next) => (next.called ? undefined : next(e)))
69}
70