SLOPSHOPPER

reloj-cache

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

newbandtoasttimer
v0.1.0no licenseupdated 2026-10-15licnicolasdubois/mods-academia/reloj-cache
A shopper browsing a rack in a slop shop
Preview · a replayed session in a sandbox
claude · ~/work/app · reloj-cache
› 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 Caché: quedan 60 min · 97 mil tokens [ Compactar ] ────────────────────────────────────────────────────────────────────────────────────────────────────────────────────── › ? for shortcuts

Draws

Band
Caché: quedan 60 min · 97 mil tokens [ Compactar ]
README

Mods de Claude Code

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.

ModQué hace
guardiaNo deja leer ni tocar archivos .env (sí el .env.example) ni correr rm -rf. Si la guardia misma falla, frena igual.
reloj-cacheArriba 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.

Instalarlos

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.

Probarlos

Cada mod trae sus pruebas:

claude plugin validate guardia
claude plugin test guardia

Antes de usarlos en serio

  • Los mods son nuevos y su API cambia entre versiones de Claude Code. Estos se probaron con la 2.1.295 y la 2.1.300. Si uno deja de andar, pedile a Claude que lo ajuste.
  • Un mod que falla se saltea. La guardia tiene un .catch para que, si falla, frene en vez de dejar pasar.
  • La guardia mira el texto de cada comando: evita accidentes, pero no frena a alguien que la quiera esquivar a propósito.
  • 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.
Source 2 files
hooks/register.tsx 103 lines
1import { 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}
103
types/index.d.ts 14 lines
1export 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