SLOPSHOPPER

medidor

Banda sobre el prompt: modelo, esfuerzo, contexto, límites de 5 h y semanal con su reinicio, costo y duración de la sesión

newbandcommandtoasttimer
★ 1v0.1.0MITupdated 2026-10-16gabriel9004/claude-code-mods/medidor
A shopper browsing a rack in a slop shop
Preview · a replayed session in a sandbox
claude · ~/work/app · medidor
› 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 › /medidor ⎿ medidor: Medidor en modo compacto. Caché de 1 h (suscripción). ◆ Opus 5.5 · esfuerzo — │ ctx ▰▰▱▱▱ 49% │ 5h ▰▰▱▱▱ 31% ↻ ya (09:53) │ 7d — caché ○ sin peticiones aún: la primera escribe la caché ────────────────────────────────────────────────────────────────────────────────────────────────────────────────────── › ? for shortcuts

Draws

Band
◆ Opus 5.5 · esfuerzo — │ ctx ▰▰▱▱▱ 49% │ 5h ▰▰▱▱▱ 31% ↻ ya (09:53) │ 7d — caché ○ sin peticiones aún: la primera escribe la caché
README

claude-code-mods

Dos mods para Claude Code que muestran en pantalla lo que normalmente no se ve: qué hacen tus subagentes, cuánto te queda de cuota y cómo va la caché de prompts.

El panel de agent-bots en la app de escritorio, con agentes y tareas de ejemplo.

Aviso: la API de mods de Claude Code es de acceso anticipado y puede cambiar entre versiones. Probados con Claude Code 2.1.292 y 2.1.299.

ModQué haceDónde se ve
agent-botsPanel lateral «Agentes y tareas»: cada subagente (y cada comando en segundo plano) como un robot animado, la lista de tareas de la sesión en vivo y tres anillos: cuota de 5 h, cuota semanal y caché de promptsApp de escritorio (en terminal, versión de texto)
medidorBanda sobre el prompt: modelo y esfuerzo, contexto, límites de 5 h y semana con su reinicio, costo, duración y estado de la caché con cuenta regresivaTerminal

Los dos se complementan: medidor solo dibuja en la terminal y agent-bots hace lo mismo en la app de escritorio. Puedes instalar solo uno.

Instalación

/plugin marketplace add gabriel9004/claude-code-mods
/plugin install agent-bots@claude-code-mods
/plugin install medidor@claude-code-mods
/reload-plugins

Luego /tareas o /bots abre el panel y /medidor [completo|compacto|oculto] cambia la banda.

Qué lee cada mod

Un mod se ejecuta con acceso a la sesión, así que conviene saber qué toca. Ninguno envía datos a ningún sitio ni guarda nada en disco.

  • agent-bots: los subagentes y herramientas que usa la sesión, la lista de tareas, los porcentajes de cuota (session.usage), el uso de tokens de cada petición para la caché y tres variables de entorno de solo lectura (FORCE_PROMPT_CACHING_5M, CLAUDE_CODE_PROMPT_CACHE_TTL, ENABLE_PROMPT_CACHING_1H). Registra una herramienta propia, tareas, y solo añade una línea al prompt de sistema si la sesión no trae ninguna herramienta de tareas.
  • medidor: modelo, esfuerzo, uso de contexto y de cuota, costo, tokens de cada petición y las mismas tres variables de entorno.

Desarrollo

Cada mod trae sus pruebas:

cd agent-bots && claude plugin validate . && claude plugin test .
cd ../medidor && claude plugin validate .

Licencia y créditos

MIT. La lógica de la caché toma ideas de otro proyecto MIT; ver NOTICE.md.


English

Two Claude Code mods: agent-bots (a desktop side panel with animated robots for subagents, a live task list and quota/cache rings) and medidor (a terminal band with model, context, 5 h / weekly limits, cost and prompt-cache countdown). The mods API is early access and may change.

/plugin marketplace add gabriel9004/claude-code-mods
/plugin install agent-bots@claude-code-mods
/plugin install medidor@claude-code-mods

MIT licensed. The UI text is in Spanish.

Source 3 files
hooks/register.tsx 307 lines
1import { atom, read, update } from 'claude-code'
2import type { EngineInterface, Register } from 'claude-code'
3
4import type { Medida, Modo, Muestra, Ttl, Ventana } from '../types'
5import { aciertos, aconsejar, decidirTtl, observarTtl, reloj, restanteMs, tokens, tokensPrompt } from './cache'
6import type { Consejo } from './cache'
7
8const medida = atom({ plugin: 'medidor', key: 'medida' } as const, null)
9const modelo = atom({ plugin: 'medidor', key: 'modelo' } as const, '')
10const esfuerzo = atom({ plugin: 'medidor', key: 'esfuerzo' } as const, null)
11const ahora = atom({ plugin: 'medidor', key: 'ahora' } as const, 0)
12const modo = atom({ plugin: 'medidor', key: 'modo' } as const, 'completo')
13const avisado = atom({ plugin: 'medidor', key: 'avisado' } as const, 0)
14const muestras = atom({ plugin: 'medidor', key: 'muestras' } as const, [])
15const ttlActual = atom({ plugin: 'medidor', key: 'ttl' } as const, '5m')
16const ttlFuente = atom({ plugin: 'medidor', key: 'ttlFuente' } as const, 'por defecto')
17
18const DIAS = ['dom', 'lun', 'mar', 'mié', 'jue', 'vie', 'sáb']
19const MODOS: Modo[] = ['completo', 'compacto', 'oculto']
20const AVISO_CACHE_MS = 60_000 // amarillo y aviso cuando falta esto para que venza
21const COMPACTAR_DESDE = 100_000 // con más tokens, una caché vencida sugiere /compact
22const AVISAR_DESDE = 20_000 // prompts menores no merecen aviso de vencimiento
23
24// «claude-opus-5-5[1m]» → «Opus 5.5 · 1M»
25function nombreModelo(id: string): string {
26  if (!id) return '…'
27  const largo = /\[1m\]/i.test(id) ? ' · 1M' : ''
28  const limpio = id.replace(/\[.*?\]/g, '').replace(/^claude-/, '')
29  const m = limpio.match(/^([a-z]+)-(\d+)(?:-(\d+))?/i)
30  if (!m || !m[1] || !m[2]) return limpio + largo
31  const familia = m[1].charAt(0).toUpperCase() + m[1].slice(1)
32  return `${familia} ${m[2]}${m[3] && m[3].length <= 2 ? '.' + m[3] : ''}${largo}`
33}
34
35function color(pct: number | undefined): string {
36  if (pct === undefined) return 'gray'
37  if (pct >= 90) return 'red'
38  if (pct >= 70) return 'yellow'
39  return 'green'
40}
41
42function barra(pct: number | undefined, celdas: number): [string, string] {
43  const llenas = Math.max(0, Math.min(celdas, Math.round(((pct ?? 0) / 100) * celdas)))
44  return ['▰'.repeat(llenas), '▱'.repeat(celdas - llenas)]
45}
46
47function duracion(ms: number): string {
48  if (ms <= 0) return 'ya'
49  const min = Math.floor(ms / 60_000)
50  const d = Math.floor(min / 1440)
51  const h = Math.floor((min % 1440) / 60)
52  const m = min % 60
53  if (d > 0) return `${d}d ${h}h`
54  if (h > 0) return `${h}h ${String(m).padStart(2, '0')}m`
55  return `${m}m`
56}
57
58function hora(fecha: Date): string {
59  return `${String(fecha.getHours()).padStart(2, '0')}:${String(fecha.getMinutes()).padStart(2, '0')}`
60}
61
62function reinicio(v: Ventana | undefined, t: number, conDia: boolean): string {
63  if (!v?.resetsAt) return ''
64  const f = new Date(v.resetsAt)
65  const cuando = conDia ? `${DIAS[f.getDay()]} ${hora(f)}` : hora(f)
66  return `${duracion(f.getTime() - t)} (${cuando})`
67}
68
69const COLOR_CONSEJO: Record<Consejo['tipo'], string> = {
70  fria: 'gray',
71  'sin-cache': 'gray',
72  caliente: 'green',
73  pronto: 'yellow',
74  vencida: 'red',
75  fallo: 'red',
76}
77const ICONO_CONSEJO: Record<Consejo['tipo'], string> = {
78  fria: '○',
79  'sin-cache': '○',
80  caliente: '●',
81  pronto: '▲',
82  vencida: '✖',
83  fallo: '✖',
84}
85
86async function medir($: EngineInterface) {
87  const u = await $.session.usage()
88  const m: Medida = {
89    startedAt: u.startedAt,
90    tokens: u.context.tokens,
91    window: u.context.window,
92    percent: u.context.percent,
93    limites: u.rateLimits.map(r => ({ kind: r.kind, percentUsed: r.percentUsed, resetsAt: r.resetsAt })),
94    usd: u.cost?.usd,
95  }
96  await update($, medida, () => m)
97  await update($, ahora, () => Date.now())
98  return m
99}
100
101export const register: Register = on => {
102  // Variables del módulo: se reinician en cada recarga, a propósito.
103  let env: { force5m?: string; ttlVar?: string; enable1h?: string } = {}
104  let observado: Ttl | undefined
105  let marcaReloj = ''
106  let avisadaCache = 0
107
108  on('session.start', async ($, e, next) => {
109    await $.command.register({
110      name: 'medidor',
111      description: 'Banda de uso: /medidor [completo|compacto|oculto] (sin argumento, alterna)',
112    })
113    const nada = () => undefined
114    env = {
115      force5m: await $.env.get('FORCE_PROMPT_CACHING_5M').catch(nada),
116      ttlVar: await $.env.get('CLAUDE_CODE_PROMPT_CACHE_TTL').catch(nada),
117      enable1h: await $.env.get('ENABLE_PROMPT_CACHING_1H').catch(nada),
118    }
119    const id = await $.session.model()
120    await update($, modelo, () => id)
121    const m = await medir($)
122    const base = decidirTtl(env, m.limites)
123    await update($, ttlActual, () => base.ttl)
124    await update($, ttlFuente, () => base.fuente)
125
126    // Cada segundo: redibuja solo si cambió lo que se ve (minuto de los
127    // reinicios o segundo de la cuenta regresiva de la caché), y avisa a 60 s.
128    $.clock.every(1000, async () => {
129      const t = Date.now()
130      const lista = await read($, muestras)
131      const ultima = lista[lista.length - 1]
132      const ttl = await read($, ttlActual)
133      const resta = ultima ? restanteMs(ultima, ttl, t) : 0
134      const marca = `${Math.floor(t / 60_000)}|${Math.ceil(resta / 1000)}`
135      if (marca !== marcaReloj) {
136        marcaReloj = marca
137        await update($, ahora, () => t)
138      }
139      if (ultima && tokensPrompt(ultima) >= AVISAR_DESDE && resta > 0 && resta <= AVISO_CACHE_MS && avisadaCache !== ultima.inicio) {
140        avisadaCache = ultima.inicio
141        $.ui.toast(`La caché vence en ${reloj(resta)}: cualquier mensaje la renueva; si no, se reescriben ${tokens(tokensPrompt(ultima))}`)
142      }
143    })
144    return next(e)
145  })
146
147  on('session.measure', async ($, e, next) => {
148    const m = await medir($)
149    const cinco = m.limites.find(l => l.kind === 'five_hour')
150    if (cinco) {
151      const ya = await read($, avisado)
152      const umbral = cinco.percentUsed >= 95 ? 95 : cinco.percentUsed >= 80 ? 80 : 0
153      if (umbral > ya) $.ui.toast(`Límite de 5 h al ${cinco.percentUsed}%: se reinicia en ${reinicio(cinco, Date.now(), false)}`)
154      if (umbral !== ya) await update($, avisado, () => umbral)
155    }
156    return next(e)
157  })
158
159  // Cada petición del hilo principal (los subagentes tienen su propio prefijo):
160  // modelo, esfuerzo y qué hizo la caché con ella.
161  on('turn.step', async function* ($, e, next) {
162    if (e.agentId) return yield* next(e)
163    const inicio = Date.now()
164    await update($, modelo, () => e.model)
165    await update($, esfuerzo, () => (e.effort === undefined ? null : String(e.effort)))
166    const r = yield* next(e)
167    if (r.usage) {
168      const nueva: Muestra = {
169        modelo: r.usage.model || e.model,
170        inicio,
171        leido: r.usage.cache_read_input_tokens,
172        escrito: r.usage.cache_creation_input_tokens,
173        nuevo: r.usage.input_tokens,
174      }
175      const lista = [...(await read($, muestras)), nueva].slice(-2)
176      await update($, muestras, () => lista)
177      // La cuenta puede cambiar en plena sesión (suscripción agotada → créditos).
178      const base = decidirTtl(env, ((await read($, medida))?.limites) ?? [])
179      observado = observarTtl(lista[0] === nueva ? undefined : lista[0], nueva, observado)
180      await update($, ttlActual, () => observado ?? base.ttl)
181      await update($, ttlFuente, () => (observado ? 'observado en el tráfico' : base.fuente))
182    }
183    return r
184  })
185
186  on('command.run', { command: 'medidor' }, async ($, e) => {
187    const pedido = e.args.trim().toLowerCase() as Modo
188    const actual = await read($, modo)
189    const nuevo = MODOS.includes(pedido) ? pedido : MODOS[(MODOS.indexOf(actual) + 1) % MODOS.length]
190    await update($, modo, () => nuevo)
191    await medir($)
192    const ttl = await read($, ttlActual)
193    const fuente = await read($, ttlFuente)
194    return { text: `Medidor en modo ${nuevo}. Caché de ${ttl === '1h' ? '1 h' : '5 min'} (${fuente}).` }
195  })
196
197  on('ui.render', { component: 'AbovePrompt' }, async ($, e, next) => {
198    const m = await read($, medida)
199    const vista = await read($, modo)
200    // Solo en la terminal: en la app de escritorio lo muestra el panel de agent-bots (si lo tienes instalado).
201    if (e.surface !== 'terminal') return next(e)
202    if (e.props.hasSurvey || vista === 'oculto' || m === null) return next(e)
203
204    const { Box, Text } = $.ui.resolve(e)
205    const t = (await read($, ahora)) || Date.now()
206    const id = await read($, modelo)
207    const esf = await read($, esfuerzo)
208    const ancho = e.viewport?.columns ?? 120
209    const compacto = vista === 'compacto' || ancho < 100
210    const celdas = compacto ? 5 : 8
211    const sep = <Text dimColor> │ </Text>
212
213    const ctx = m.percent
214    const [cl, cv] = barra(ctx, celdas)
215    const cinco = m.limites.find(l => l.kind === 'five_hour')
216    const semana = m.limites.find(l => l.kind === 'seven_day')
217    const otros = m.limites.filter(l => l.kind !== 'five_hour' && l.kind !== 'seven_day')
218
219    // Caché de prompts
220    const lista = await read($, muestras)
221    const ultima = lista[lista.length - 1]
222    const previa = lista.length > 1 ? lista[0] : undefined
223    const ttl = await read($, ttlActual)
224    const consejo = aconsejar(ultima, previa, ttl, t, AVISO_CACHE_MS, COMPACTAR_DESDE)
225    const resta = ultima ? restanteMs(ultima, ttl, t) : 0
226    const pctCache = ultima ? Math.round(aciertos(ultima) * 100) : undefined
227    const colorReloj = resta <= AVISO_CACHE_MS ? 'red' : resta / (ttl === '1h' ? 3_600_000 : 300_000) <= 0.4 ? 'yellow' : 'green'
228    const [kl, kv] = barra(pctCache, celdas)
229
230    const limite = (etq: string, v: Ventana | undefined, conDia: boolean) => {
231      if (!v) return <Text dimColor>{etq} —</Text>
232      const [ll, lv] = barra(v.percentUsed, celdas)
233      return (
234        <Text>
235          <Text bold>{etq} </Text>
236          <Text color={color(v.percentUsed)}>{ll}</Text>
237          <Text dimColor>{lv}</Text>
238          <Text color={color(v.percentUsed)}> {v.percentUsed}%</Text>
239          {v.resetsAt && <Text dimColor> ↻ {reinicio(v, t, conDia)}</Text>}
240        </Text>
241      )
242    }
243
244    const cache = ultima ? (
245      <Text>
246        <Text bold>caché </Text>
247        <Text color={COLOR_CONSEJO[consejo.tipo]}>{kl}</Text>
248        <Text dimColor>{kv}</Text>
249        <Text color={COLOR_CONSEJO[consejo.tipo]}> {pctCache}%</Text>
250        {!compacto && (
251          <Text dimColor>
252            {' '}leído {tokens(ultima.leido)} · escrito {tokens(ultima.escrito)} · nuevo {tokens(ultima.nuevo)}
253          </Text>
254        )}
255        <Text color={colorReloj}> ⏱ {reloj(resta)}</Text>
256        <Text dimColor> {ttl === '1h' ? '1h' : '5m'}</Text>
257        <Text color={COLOR_CONSEJO[consejo.tipo]}> {ICONO_CONSEJO[consejo.tipo]} </Text>
258        <Text color={consejo.tipo === 'caliente' ? undefined : COLOR_CONSEJO[consejo.tipo]} dimColor={consejo.tipo === 'caliente'}>
259          {compacto ? consejo.texto.split(':')[0] : consejo.texto}
260        </Text>
261      </Text>
262    ) : (
263      <Text dimColor>caché ○ {consejo.texto}</Text>
264    )
265
266    return (
267      <Box flexDirection="column">
268        <Box flexDirection="row" flexWrap="wrap">
269          <Text>
270            <Text color="magenta" bold>◆ {nombreModelo(id)}</Text>
271            <Text dimColor> · </Text>
272            <Text color="cyan">{esf ?? 'esfuerzo —'}</Text>
273          </Text>
274          {sep}
275          <Text>
276            <Text bold>ctx </Text>
277            <Text color={color(ctx)}>{cl}</Text>
278            <Text dimColor>{cv}</Text>
279            <Text color={color(ctx)}> {ctx === undefined ? '—' : `${ctx}%`}</Text>
280            {!compacto && m.tokens !== undefined && (
281              <Text dimColor> {tokens(m.tokens)}/{tokens(m.window)}</Text>
282            )}
283          </Text>
284          {sep}
285          {limite('5h', cinco, false)}
286          {sep}
287          {limite('7d', semana, true)}
288          {otros.map(o => (
289            <Text key={o.kind}>
290              <Text dimColor> │ </Text>
291              {limite(o.kind, o, true)}
292            </Text>
293          ))}
294          {!compacto && (
295            <Text>
296              <Text dimColor> │ </Text>
297              {m.usd !== undefined && <Text color="green">${m.usd.toFixed(2)}</Text>}
298              <Text dimColor> · {duracion(t - m.startedAt)}</Text>
299            </Text>
300          )}
301        </Box>
302        {cache}
303      </Box>
304    )
305  })
306}
307
hooks/cache.ts 111 lines
1// Lógica pura de la caché de prompts (sin `$`). Ideas tomadas de
2// davila7/claude-code-templates · observability/prompt-cache-control (MIT,
3// commit 8da17d67), reescritas y simplificadas para el medidor:
4//   - la caché vive 5 min por defecto y 1 h si se pide; cada lectura la renueva
5//     gratis y la vida se cuenta desde el INICIO de la petición
6//   - prompt = input_tokens (sin caché) + cache_read + cache_creation
7//   - escribir cuesta 1,25× (5 min) o 2× (1 h) la entrada; leer ≈ 0,1×: lo caro es
8//     que venza con un contexto grande
9//   - un cambio del prefijo (modelo, esfuerzo, herramientas, system prompt,
10//     CLAUDE.md) hace que la siguiente petición escriba en vez de leer
11import type { Muestra, Ttl } from '../types'
12
13export type Consejo = {
14  tipo: 'fria' | 'sin-cache' | 'caliente' | 'pronto' | 'vencida' | 'fallo'
15  texto: string
16}
17
18const HOLGURA_MS = 10_000
19
20export const ttlMs = (ttl: Ttl) => (ttl === '1h' ? 3_600_000 : 300_000)
21export const tokensPrompt = (m: Muestra) => m.leido + m.escrito + m.nuevo
22export const aciertos = (m: Muestra) => (tokensPrompt(m) === 0 ? 0 : m.leido / tokensPrompt(m))
23
24export function restanteMs(m: Muestra, ttl: Ttl, ahora: number): number {
25  if (m.leido + m.escrito === 0) return 0
26  return Math.max(0, m.inicio + ttlMs(ttl) - ahora)
27}
28
29/** Duración que pide Claude Code según el entorno y la cuenta (orden de su documentación). */
30export function decidirTtl(
31  env: { force5m?: string; ttlVar?: string; enable1h?: string },
32  limites: { kind: string; percentUsed: number }[],
33): { ttl: Ttl; fuente: string } {
34  const si = (v?: string) => v === '1' || v?.toLowerCase() === 'true'
35  if (si(env.force5m)) return { ttl: '5m', fuente: 'FORCE_PROMPT_CACHING_5M' }
36  if (env.ttlVar === '5m' || env.ttlVar === '1h') return { ttl: env.ttlVar, fuente: 'CLAUDE_CODE_PROMPT_CACHE_TTL' }
37  if (si(env.enable1h)) return { ttl: '1h', fuente: 'ENABLE_PROMPT_CACHING_1H' }
38  const plan = limites.filter(l => l.kind === 'five_hour' || l.kind === 'seven_day')
39  if (plan.length === 0) return { ttl: '5m', fuente: 'sin suscripción visible' }
40  if (plan.some(l => l.percentUsed >= 100)) return { ttl: '5m', fuente: 'créditos de uso' }
41  return { ttl: '1h', fuente: 'suscripción' }
42}
43
44/**
45 * Lo que el tráfico dice de la duración: un acierto más de 5 min después de la
46 * petición anterior prueba 1 h; un fallo entre 5 y 60 min con el mismo modelo y
47 * un prompt que no encogió apunta a 5 min (más débil: un prefijo cambiado se ve igual).
48 */
49export function observarTtl(prev: Muestra | undefined, cur: Muestra, conocido: Ttl | undefined): Ttl | undefined {
50  if (!prev || prev.leido + prev.escrito === 0 || cur.modelo !== prev.modelo) return conocido
51  const hueco = cur.inicio - prev.inicio
52  const antes = tokensPrompt(prev)
53  if (hueco <= ttlMs('5m') + HOLGURA_MS) return conocido
54  if (cur.leido >= antes * 0.5) return '1h'
55  if (conocido === '1h') return conocido
56  const vencio = cur.escrito > 0 && tokensPrompt(cur) >= antes * 0.7 && hueco < ttlMs('1h') + HOLGURA_MS
57  return vencio ? '5m' : conocido
58}
59
60/** Por qué una petición que debía leer la caché la escribió; undefined si no falló. */
61export function causaFallo(prev: Muestra | undefined, cur: Muestra, ttl: Ttl): string | undefined {
62  if (!prev) return undefined
63  const antes = tokensPrompt(prev)
64  // Un prompt que encogió es /compact o /clear, no un fallo.
65  if (antes === 0 || tokensPrompt(cur) < antes * 0.7) return undefined
66  if (cur.leido >= antes * 0.5 || cur.escrito === 0) return undefined
67  if (cur.modelo !== prev.modelo) return 'cambió el modelo'
68  if (cur.inicio - prev.inicio > ttlMs(ttl)) return `había vencido (${ttl === '1h' ? '1 h' : '5 min'})`
69  return 'cambió el prefijo (esfuerzo, herramientas, system prompt o CLAUDE.md)'
70}
71
72export function aconsejar(
73  ultima: Muestra | undefined,
74  previa: Muestra | undefined,
75  ttl: Ttl,
76  ahora: number,
77  avisoMs: number,
78  compactarDesde: number,
79): Consejo {
80  if (!ultima) return { tipo: 'fria', texto: 'sin peticiones aún: la primera escribe la caché' }
81  if (ultima.leido + ultima.escrito === 0) return { tipo: 'sin-cache', texto: 'esta petición no usó caché' }
82  const resta = restanteMs(ultima, ttl, ahora)
83  const tam = tokensPrompt(ultima)
84  if (resta <= 0) {
85    return tam >= compactarDesde
86      ? { tipo: 'vencida', texto: `vencida: el próximo mensaje reescribe ${tokens(tam)}; /compact antes, o /clear si terminaste` }
87      : { tipo: 'vencida', texto: `vencida: solo ${tokens(tam)} por reconstruir, sigue` }
88  }
89  if (resta <= avisoMs) return { tipo: 'pronto', texto: 'vence pronto: cualquier mensaje la renueva gratis' }
90  const fallo = causaFallo(previa, ultima, ttl)
91  if (fallo) return { tipo: 'fallo', texto: `falló: ${fallo}` }
92  return { tipo: 'caliente', texto: 'caliente: sigue' }
93}
94
95export function tokens(n: number): string {
96  if (n < 1000) return String(n)
97  if (n < 100_000) return `${(n / 1000).toFixed(1).replace(/\.0$/, '')}k`
98  if (n < 1_000_000) return `${Math.round(n / 1000)}k`
99  return `${(n / 1_000_000).toFixed(1).replace(/\.0$/, '')}M`
100}
101
102/** m:ss, o h:mm:ss desde una hora. */
103export function reloj(ms: number): string {
104  const t = Math.max(0, Math.ceil(ms / 1000))
105  const h = Math.floor(t / 3600)
106  const m = Math.floor((t % 3600) / 60)
107  const s = t % 60
108  const dos = (n: number) => String(n).padStart(2, '0')
109  return h > 0 ? `${h}:${dos(m)}:${dos(s)}` : `${m}:${dos(s)}`
110}
111
types/index.d.ts 41 lines
1export type Ventana = { kind: string; percentUsed: number; resetsAt?: string }
2
3export type Medida = {
4  startedAt: number
5  tokens?: number
6  window: number
7  percent?: number
8  limites: Ventana[]
9  usd?: number
10}
11
12export type Modo = 'completo' | 'compacto' | 'oculto'
13
14export type Ttl = '5m' | '1h'
15
16/** Una petición del hilo principal, como la reportó la API. */
17export type Muestra = {
18  modelo: string
19  /** ms desde la época al empezar la petición: la vida de la caché cuenta desde aquí */
20  inicio: number
21  leido: number
22  escrito: number
23  nuevo: number
24}
25
26declare module 'claude-code' {
27  interface PluginState {
28    medidor: {
29      medida: Medida | null
30      modelo: string
31      esfuerzo: string | null
32      ahora: number
33      modo: Modo
34      avisado: number
35      muestras: Muestra[]
36      ttl: Ttl
37      ttlFuente: string
38    }
39  }
40}
41