SLOPSHOPPER

cost-budget

Coste en USD y tokens por turno, sesión, día y proyecto, con presupuesto diario y aviso al superarlo. /budget muestra el resumen.

newcommandtoaststatus
v0.1.0MITupdated 2026-10-22Jmezas/claude-mods/cost-budget
A shopper browsing a rack in a slop shop
Preview · a replayed session in a sandbox
claude · ~/work/app · cost-budget
› 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 › /budget ⎿ cost-budget: presupuesto diario: $10.00 ⎿ cost-budget: sesión $0.000 1 turnos in 93.1k (91.0k caché) out 1.5k ⎿ cost-budget: hoy $0.000 1 turnos in 93.1k (91.0k caché) out 1.5k ⎿ cost-budget: mes $0.000 1 turnos in 93.1k (91.0k caché) out 1.5k ⎿ cost-budget: proyecto $0.000 1 turnos in 93.1k (91.0k caché) out 1.5k ⎿ cost-budget: subcomandos: hoy | proyectos | dias | set <usd> | reset ────────────────────────────────────────────────────────────────────────────────────────────────────────────────────── › ? for shortcuts ⚠ cost-budget: coste: turno $0.000 · sesión $0.000 · hoy $0.000 / $10.00
README

cost-budget

Cuánto cuesta cada turno, la sesión, el día y cada proyecto, con un presupuesto diario y aviso al superarlo.

Después de cada turno la línea de estado muestra coste: turno $0.012 · sesión $0.48 · hoy $3.10 / $10.00. El coste en USD sale del mismo contador que /cost; los tokens de entrada, caché y salida se suman por turno. Los totales por día (últimos 90) y por proyecto (carpeta raíz de la sesión) persisten entre sesiones. Si el host no informa coste en USD, se muestran solo tokens.

Al pasar el presupuesto diario aparece un aviso, y otro cada vez que el gasto crece otro 50 % del presupuesto.

Instalación

En el prompt de una sesión de terminal:

/plugin install cost-budget --marketplace Jmezas/claude-mods

Comandos

  • /budget resumen: sesión, hoy, mes y proyecto actual.
  • /budget proyectos los 15 proyectos con más gasto.
  • /budget dias últimos 14 días.
  • /budget set 5 presupuesto diario de 5 USD. También en /config, campo Presupuesto diario.
  • /budget reset borra el historial de días y proyectos.
Source 2 files
hooks/register.ts 158 lines
1import { atom, read, update } from 'claude-code'
2import type { EngineInterface, Register } from 'claude-code'
3
4import type { Ledger, Totals } from '../types'
5
6const EMPTY: Totals = { usd: 0, turns: 0, inputTokens: 0, outputTokens: 0, cacheReadTokens: 0 }
7const session = atom({ plugin: 'cost-budget', key: 'session' } as const, EMPTY)
8
9const pad = (n: number) => String(n).padStart(2, '0')
10const dayOf = (ms: number) => {
11  const d = new Date(ms)
12
13  return `${d.getFullYear()}-${pad(d.getMonth() + 1)}-${pad(d.getDate())}`
14}
15const usd = (n: number) => `$${n.toFixed(n < 1 ? 3 : 2)}`
16const k = (n: number) => (n >= 1000 ? `${(n / 1000).toFixed(1)}k` : String(n))
17
18const add = (a: Totals, b: Totals): Totals => ({
19  usd: a.usd + b.usd,
20  turns: a.turns + b.turns,
21  inputTokens: a.inputTokens + b.inputTokens,
22  outputTokens: a.outputTokens + b.outputTokens,
23  cacheReadTokens: a.cacheReadTokens + b.cacheReadTokens,
24})
25
26const isLedger = (value: unknown): value is Ledger =>
27  typeof value === 'object' &&
28  value !== null &&
29  typeof (value as Ledger).days === 'object' &&
30  typeof (value as Ledger).projects === 'object'
31
32const line = (label: string, t: Totals) =>
33  `${label.padEnd(12)} ${usd(t.usd).padStart(8)}  ${String(t.turns).padStart(4)} turnos  in ${k(t.inputTokens + t.cacheReadTokens).padStart(7)} (${k(t.cacheReadTokens)} caché)  out ${k(t.outputTokens).padStart(6)}`
34
35async function loadLedger($: EngineInterface): Promise<Ledger> {
36  const stored = await $.store.get('ledger')
37
38  return isLedger(stored) ? stored : { days: {}, projects: {} }
39}
40
41export const register: Register = (on, options) => {
42  const budget = Number(options.dailyBudgetUsd) || 0
43  let lastUsd = 0
44  let hasLedger = true
45  let project = ''
46  let warnedAt = 0
47
48  on('session.start', async ($, e, next) => {
49    await $.command.register({
50      name: 'budget',
51      description: 'cost-budget: resumen | hoy | proyectos | dias | set <usd> | reset',
52      argumentHint: '[hoy|proyectos|dias|set <usd>|reset]',
53    })
54    project = await $.session.root()
55    const started = await next(e)
56    const usage = await $.session.usage()
57    hasLedger = usage.cost !== undefined
58    lastUsd = usage.cost?.usd ?? 0
59    if (!hasLedger) $.ui.log('cost-budget: este host no informa coste en USD; se cuentan solo tokens.')
60
61    return started
62  })
63
64  on('turn.complete', async ($, e, next) => {
65    if (e.agentId !== undefined) return next(e)
66    const usage = await $.session.usage()
67    const nowUsd = usage.cost?.usd ?? lastUsd
68    const delta: Totals = {
69      usd: Math.max(0, nowUsd - lastUsd),
70      turns: 1,
71      inputTokens: e.usage?.input_tokens ?? 0,
72      outputTokens: e.usage?.output_tokens ?? 0,
73      cacheReadTokens: e.usage?.cache_read_input_tokens ?? 0,
74    }
75    lastUsd = nowUsd
76
77    const current = add(await read($, session), delta)
78    await update($, session, () => current)
79
80    const day = dayOf(await $.clock.now())
81    const ledger = await loadLedger($)
82    ledger.days[day] = add(ledger.days[day] ?? EMPTY, delta)
83    ledger.projects[project] = add(ledger.projects[project] ?? EMPTY, delta)
84    const days = Object.keys(ledger.days).sort()
85    for (const old of days.slice(0, Math.max(0, days.length - 90))) delete ledger.days[old]
86    await $.store.set('ledger', ledger)
87
88    const today = ledger.days[day] ?? EMPTY
89    $.ui.status(
90      hasLedger
91        ? `coste: turno ${usd(delta.usd)} · sesión ${usd(current.usd)} · hoy ${usd(today.usd)}${budget > 0 ? ` / ${usd(budget)}` : ''}`
92        : `tokens: sesión in ${k(current.inputTokens + current.cacheReadTokens)} out ${k(current.outputTokens)}`,
93    )
94    if (hasLedger && budget > 0 && today.usd >= budget) {
95      const step = Math.floor((today.usd / budget) * 2)
96      if (step > warnedAt) {
97        warnedAt = step
98        $.ui.toast(`cost-budget: hoy llevas ${usd(today.usd)}, presupuesto ${usd(budget)}`)
99      }
100    }
101
102    return next(e)
103  })
104
105  on('command.run', { command: 'budget' }, async ($, e) => {
106    const [verb = '', value = ''] = e.args.trim().split(/\s+/)
107    const ledger = await loadLedger($)
108    const day = dayOf(await $.clock.now())
109
110    if (verb === 'reset') {
111      await $.store.delete('ledger')
112
113      return { text: 'cost-budget: historial de días y proyectos borrado' }
114    }
115    if (verb === 'set') {
116      const n = Number(value)
117      if (!(n >= 0)) return { text: 'uso: /budget set <usd por día>' }
118      await $.config.set({ key: 'cost-budget.dailyBudgetUsd', value: String(n) })
119
120      return { text: `cost-budget: presupuesto diario ${usd(n)} (el plugin se recarga con el nuevo valor)` }
121    }
122    if (verb === 'proyectos' || verb === 'projects') {
123      const rows = Object.entries(ledger.projects)
124        .sort((a, b) => b[1].usd - a[1].usd)
125        .slice(0, 15)
126        .map(([root, t]) => line(root.split('/').pop() ?? root, t))
127
128      return { text: rows.length ? ['coste por proyecto:', ...rows].join('\n') : 'sin datos de proyectos' }
129    }
130    if (verb === 'dias' || verb === 'días' || verb === 'days') {
131      const rows = Object.keys(ledger.days)
132        .sort()
133        .slice(-14)
134        .map(d => line(d, ledger.days[d] ?? EMPTY))
135
136      return { text: rows.length ? ['coste por día (últimos 14):', ...rows].join('\n') : 'sin datos de días' }
137    }
138
139    const current = await read($, session)
140    const today = ledger.days[day] ?? EMPTY
141    const thisProject = ledger.projects[project] ?? EMPTY
142    const month = Object.entries(ledger.days)
143      .filter(([d]) => d.startsWith(day.slice(0, 7)))
144      .reduce((sum, [, t]) => add(sum, t), EMPTY)
145
146    return {
147      text: [
148        hasLedger ? `presupuesto diario: ${budget > 0 ? usd(budget) : 'sin límite'}` : 'este host no informa USD: solo tokens',
149        line('sesión', current),
150        line('hoy', today),
151        line('mes', month),
152        line('proyecto', thisProject),
153        'subcomandos: hoy | proyectos | dias | set <usd> | reset',
154      ].join('\n'),
155    }
156  })
157}
158
types/index.d.ts 19 lines
1export type Totals = {
2  usd: number
3  turns: number
4  inputTokens: number
5  outputTokens: number
6  cacheReadTokens: number
7}
8
9export type Ledger = {
10  days: Record<string, Totals>
11  projects: Record<string, Totals>
12}
13
14declare module 'claude-code' {
15  interface PluginState {
16    'cost-budget': { session: Totals }
17  }
18}
19