Muestra arriba del prompt cuántos minutos le quedan a la caché y cuántos tokens guarda, y avisa antes de que venza.

Los tres mods de la clase «Mods de Claude Code» de la Academia de Nicolás Dubois. Un mod es código que corre adentro de Claude Code: puede frenar algo antes de que pase, dibujar en pantalla o sumar un comando.
| Mod | Qué hace |
|---|---|
| guardia | No deja leer ni tocar archivos .env (sí el .env.example) ni correr rm -rf. Si la guardia misma falla, frena igual. |
| reloj-cache | Arriba del prompt, cuántos minutos le quedan a la caché y cuántos tokens guarda, con un botón Compactar. Avisa cuando faltan 5 minutos. La duración (60 min) y el aviso (5 min) se cambian en /config. |
| grabando | /grabando prende y apaga el modo grabación: en pantalla tapa mails, teléfonos y montos, en tus mensajes, en las respuestas y en lo que devuelven las herramientas. Se acuerda si estaba prendido. |
En la terminal, adentro de Claude Code (cambiá guardia por reloj-cache o grabando):
/plugin install guardia --marketplace licnicolasdubois/mods-academia
Te pregunta si querés agregar el marketplace (y) y en qué alcance instalarlo: para todos tus proyectos o para uno solo.
Desde la línea de comandos:
claude plugin install guardia --marketplace licnicolasdubois/mods-academia
En la app de escritorio, donde /plugin no anda: el + al lado del prompt → Plugins → Add marketplace con licnicolasdubois/mods-academia → Add plugin.
Cada mod trae sus pruebas:
claude plugin validate guardia
claude plugin test guardia
.catch para que, si falla, frene en vez de dejar pasar.grabando tapa lo que ves en pantalla, no lo que lee Claude: sirve para grabar o compartir pantalla, no es una protección de datos.hooks/register.tsx 103 lines1import { atom, read, update } from 'claude-code'
2import type { EngineInterface, Register, Timer } from 'claude-code'
3
4import type { Lectura } from '../types'
5
6const ultima = atom({ plugin: 'reloj-cache', key: 'ultima' } as const, null)
7// Rewritten on every tick so the band redraws on its own.
8const ahora = atom({ plugin: 'reloj-cache', key: 'ahora' } as const, 0)
9
10const TICK_MS = 30_000
11// A hot reload (an option changed in /config, a saved file) drops the timers. Every event checks
12// the clock is running, so the band never depends on one event to restart it. The variable resets
13// with the timers.
14let tictac: Timer | null = null
15
16const miles = (tokens: number) =>
17 tokens >= 1000 ? `${Math.round(tokens / 1000)} mil` : String(tokens)
18
19// Moves the band's clock and raises the warning once, when the cache is about to lapse.
20async function tick($: EngineInterface, ttlMs: number, avisoMs: number) {
21 const now = await $.clock.now()
22 await update($, ahora, () => now)
23 const last = await read($, ultima)
24 if (last === null || last.isWarned) return
25 const left = ttlMs - (now - last.at)
26 if (left > 0 && left <= avisoMs) {
27 $.ui.toast(`La caché vence en ${Math.ceil(left / 60_000)} min. Mandá lo que tenías pendiente o compactá.`)
28 await update($, ultima, l => (l === null ? l : { ...l, isWarned: true }))
29 }
30}
31
32function asegurarTictac($: EngineInterface, ttlMs: number, avisoMs: number) {
33 if (tictac === null) tictac = $.clock.every(TICK_MS, () => void tick($, ttlMs, avisoMs))
34}
35
36// The band's button: the same compaction /compact runs. The band is hidden while a turn runs,
37// so it is never pressed mid-turn (when the engine would refuse it). The press only schedules it:
38// a compaction outlasts the press's 10 s budget, and work that outlives a dispatch goes on $.clock.
39async function compactar($: EngineInterface) {
40 $.ui.toast('Compactando la conversación…')
41 try {
42 const r = await $.session.compact({})
43 if (r.skip !== undefined) {
44 $.ui.toast(`No se compactó: ${r.skip}`)
45 return
46 }
47 // Until the next answer there is no reading of the new, smaller context.
48 await update($, ultima, () => null)
49 const antes = r.tokensBefore === undefined ? '' : ` (${miles(r.tokensBefore)} → ${miles(r.tokensAfter ?? 0)} tokens)`
50 $.ui.toast(`Conversación compactada${antes}.`)
51 } catch {
52 $.ui.toast('No se pudo compactar ahora. Probá con /compact.')
53 }
54}
55
56export const register: Register = (on, options) => {
57 const ttlMs = Number(options.minutos ?? 60) * 60_000
58 const avisoMs = Number(options.aviso ?? 5) * 60_000
59
60 on('session.start', async ($, e, next) => {
61 asegurarTictac($, ttlMs, avisoMs)
62 return next(e)
63 })
64
65 on('turn.complete', async ($, e, next) => {
66 asegurarTictac($, ttlMs, avisoMs)
67 const done = await next(e)
68 // A subagent's turn does not touch the main conversation's cache.
69 if (e.agentId !== undefined || e.reason === 'error') return done
70 const { context } = await $.session.usage()
71 const lectura: Lectura = { at: await $.clock.now(), tokens: context.tokens ?? 0, isWarned: false }
72 await update($, ultima, () => lectura)
73 await update($, ahora, () => lectura.at)
74 return done
75 })
76
77 on('ui.render', { component: 'AbovePrompt' }, async ($, e, next) => {
78 asegurarTictac($, ttlMs, avisoMs)
79 const last = await read($, ultima)
80 if (e.props.hasSurvey || e.props.isWorking || last === null) return next(e)
81
82 const now = Math.max(await read($, ahora), last.at)
83 const left = ttlMs - (now - last.at)
84 const { Box, Button, Text } = $.ui.resolve(e)
85
86 if (left <= 0) {
87 return (
88 <Box key="reloj">
89 <Text color="red">Caché vencida: el próximo mensaje vuelve a procesar {miles(last.tokens)} tokens.</Text>
90 </Box>
91 )
92 }
93 return (
94 <Box key="reloj" flexDirection="row" justifyContent="space-between">
95 <Text color={left <= avisoMs ? 'yellow' : undefined} dimColor={left > avisoMs}>
96 Caché: quedan {Math.ceil(left / 60_000)} min · {miles(last.tokens)} tokens
97 </Text>
98 <Button key="compactar" label="Compactar" variant={left <= avisoMs ? 'primary' : 'secondary'} onPress={() => void $.clock.after(0, () => void compactar($))} />
99 </Box>
100 )
101 })
102}
103types/index.d.ts 14 lines1export type Lectura = {
2 // When the last answer ended, in $.clock.now() milliseconds.
3 at: number
4 // Input tokens of that answer: what the cache now holds.
5 tokens: number
6 isWarned: boolean
7}
8
9declare module 'claude-code' {
10 interface PluginState {
11 'reloj-cache': { ultima: Lectura | null; ahora: number }
12 }
13}
14