Session cost and rate-limit usage in the status line; toasts when the cost crosses a threshold; /cost-meter summary

Keeps the session cost and your most used rate-limit window in the status line, and warns once when the cost passes a threshold.
$1.23 · 5h 42%: session cost in USD plus the rate-limit window with the highest usage (5h, 7d, spend, or the raw kind name). Parts that are unavailable are left out; nothing is shown if both are.Session cost passed $5: now $5.42) the first time the cost reaches the threshold.| Command | Effect |
|---|---|
/cost-meter | Prints the cost, every rate-limit window (percent used, reset time when known) and the session duration |
| Field | Type | Default | Meaning |
|---|---|---|---|
thresholdUsd | number | 5 | Toast once when the session cost crosses this amount (USD) |
claude --plugin-dir /path/to/ModsTools/mods/cost-meter
/cost-meter prints Cost: not available.claude plugin validate mods/cost-meter
claude plugin test mods/cost-meter # 6 testshooks/register.ts 40 lines1import type { EngineInterface, Register } from 'claude-code'
2
3import { statusLine, summary } from './format'
4
5const DEFAULT_THRESHOLD = 5
6
7async function refresh($: EngineInterface, threshold: number, alerted: { done: boolean }): Promise<void> {
8 const usage = await $.session.usage()
9 const usd = usage.cost?.usd
10 $.ui.status(statusLine(usd, usage.rateLimits))
11 if (usd !== undefined && usd >= threshold && !alerted.done) {
12 alerted.done = true
13 $.ui.toast(`Session cost passed $${threshold}: now $${usd.toFixed(2)}`)
14 }
15}
16
17export const register: Register = (on, options) => {
18 const threshold = typeof options?.thresholdUsd === 'number' ? options.thresholdUsd : DEFAULT_THRESHOLD
19 const alerted = { done: false }
20
21 on('session.start', async ($, e, next) => {
22 await $.command.register({ name: 'cost-meter', description: 'cost-meter: session cost, rate-limit windows and session duration' })
23 await refresh($, threshold, alerted)
24
25 return next(e)
26 })
27
28 on('turn.complete', async ($, e, next) => {
29 if (e.agentId === undefined) await refresh($, threshold, alerted)
30
31 return next(e)
32 })
33
34 on('command.run', { command: 'cost-meter' }, async $ => {
35 const usage = await $.session.usage()
36
37 return { text: summary(usage.cost?.usd, usage.rateLimits, usage.startedAt, await $.clock.now()) }
38 })
39}
40hooks/format.ts 36 lines1export type Limit = { kind: string; percentUsed: number; resetsAt?: string }
2
3const NAMES: Record<string, string> = { five_hour: '5h', seven_day: '7d', spend_limit: 'spend' }
4
5export const limitName = (kind: string): string => NAMES[kind] ?? kind
6
7export function worstLimit(limits: readonly Limit[]): Limit | undefined {
8 return limits.reduce<Limit | undefined>((worst, l) => (worst === undefined || l.percentUsed > worst.percentUsed ? l : worst), undefined)
9}
10
11export function statusLine(usd: number | undefined, limits: readonly Limit[]): string | undefined {
12 const worst = worstLimit(limits)
13 const parts = [
14 usd === undefined ? undefined : `$${usd.toFixed(2)}`,
15 worst === undefined ? undefined : `${limitName(worst.kind)} ${Math.round(worst.percentUsed)}%`,
16 ].filter((p): p is string => p !== undefined)
17
18 return parts.length === 0 ? undefined : parts.join(' · ')
19}
20
21export function duration(ms: number): string {
22 const min = Math.max(0, Math.floor(ms / 60_000))
23
24 return min < 60 ? `${min}m` : `${Math.floor(min / 60)}h${String(min % 60).padStart(2, '0')}m`
25}
26
27export function summary(usd: number | undefined, limits: readonly Limit[], startedAt: number, now: number): string {
28 const lines = [
29 `Cost: ${usd === undefined ? 'not available' : `$${usd.toFixed(2)}`}`,
30 ...limits.map(l => `Rate limit ${limitName(l.kind)}: ${l.percentUsed}% used${l.resetsAt === undefined ? '' : `, resets ${l.resetsAt}`}`),
31 `Session duration: ${duration(now - startedAt)}`,
32 ]
33
34 return lines.join('\n')
35}
36