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

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.
En el prompt de una sesión de terminal:
/plugin install cost-budget --marketplace Jmezas/claude-mods
/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.hooks/register.ts 158 lines1import { 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}
158types/index.d.ts 19 lines1export 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