Cuenta regresiva del prompt cache desde la última respuesta del modelo

Mods para la terminal de Claude Code.
Línea de estado con el uso de contexto, los límites de uso de 5 horas y 7 días (con lo que falta para que se reinicien) y el costo de la sesión.
ctx 42% (84k/200k) · 5h 23.5% ↻2h05m · 7d 61% ↻3d3h · $1.23
/cost, calculado a precio de lista de la API. Con una suscripción no se cobra: sirve como referencia.Cuenta regresiva del prompt cache desde la última respuesta del modelo, y qué parte de esa respuesta salió del cache.
cache ● 47:12 · hit 98%
cache ○ frío (venció hace 3:05) · hit 98%
Claude Code no les dice a los mods cuánto dura el cache, así que es una opción: ttl, 1h por defecto (lo normal con suscripción) o 5m (lo normal con una API key). Se cambia desde /config.
En la terminal de Claude Code:
/plugin install usage-meter --marketplace nima8998/claude-mods
/plugin install cache-clock --marketplace nima8998/claude-mods
Desde una terminal común:
claude plugin install usage-meter --marketplace nima8998/claude-mods
claude plugin install cache-clock --marketplace nima8998/claude-mods
En la app de escritorio, donde /plugin no funciona: hacé clic en el + que está al lado del cuadro del prompt, después en Plugins, Add marketplace con nima8998/claude-mods y Add plugin.
La primera vez Claude Code pregunta si querés agregar el catálogo: respondé y y elegí el alcance de usuario para que los mods se carguen en todas tus sesiones.
hooks/register.ts 66 lines1import type { EngineInterface, ModelUsage, Register } from 'claude-code'
2
3import type { Hit } from '../types'
4
5const last = { plugin: 'cache-clock', key: 'last' } as const
6
7const TTL_MS: Record<string, number> = { '1h': 3_600_000, '5m': 300_000 }
8
9export function hitPercent(usage: ModelUsage): number | null {
10 const total = usage.input_tokens + usage.cache_read_input_tokens + usage.cache_creation_input_tokens
11 return total === 0 ? null : Math.round((usage.cache_read_input_tokens / total) * 100)
12}
13
14export function clock(ms: number): string {
15 const seconds = Math.ceil(ms / 1000)
16 return `${Math.floor(seconds / 60)}:${String(seconds % 60).padStart(2, '0')}`
17}
18
19export function line(hit: Hit | null, ttlMs: number, now: number): string {
20 if (hit === null) return 'cache —'
21
22 const left = ttlMs - (now - hit.at)
23 const ratio = hit.hitPercent === null ? '' : ` · hit ${hit.hitPercent}%`
24
25 if (left <= 0) return `cache ○ frío (venció hace ${clock(-left)})${ratio}`
26 return `cache ● ${clock(left)}${ratio}`
27}
28
29async function show($: EngineInterface, ttlMs: number) {
30 const { value = null } = await $.state.get(last)
31 $.ui.status(line(value, ttlMs, await $.clock.now()))
32}
33
34export const register: Register = (on, options) => {
35 const ttlMs = TTL_MS[String(options.ttl)] ?? 3_600_000
36
37 on('session.start', async ($, e, next) => {
38 await show($, ttlMs)
39 $.clock.every(1000, () => void show($, ttlMs))
40
41 return next(e)
42 })
43
44 // A resumed transcript starts its clock at its last response, not at the resume.
45 on('classic.SessionStart', async ($, e, next) => {
46 if (e.seconds_since_last_response !== undefined) {
47 const at = (await $.clock.now()) - e.seconds_since_last_response * 1000
48 await $.state.set(last, { at, hitPercent: null })
49 }
50
51 return next(e)
52 }).catch(($, e, next) => next(e))
53
54 // Every main-thread response reads or rewrites the cached prefix, which restarts its TTL.
55 on('turn.step', async function* ($, e, next) {
56 const response = yield* next(e)
57
58 if (e.agentId === undefined && response.usage !== null) {
59 await $.state.set(last, { at: await $.clock.now(), hitPercent: hitPercent(response.usage) })
60 await show($, ttlMs)
61 }
62
63 return response
64 })
65}
66types/index.d.ts 9 lines1/** The last main-thread response: when it arrived and how much of its prompt the cache served. */
2export type Hit = { at: number; hitPercent: number | null }
3
4declare module 'claude-code' {
5 interface PluginState {
6 'cache-clock': { last: Hit | null }
7 }
8}
9