SLOPSHOPPER

cache-frio

Avisa antes de um envio que regrava o cache frio (parado além do ttl, contexto acima de 100k) e mostra contexto e tempo de cache em /cache-frio.

newcommandtoastprompt
A shopper browsing a rack in a slop shop
Preview · a replayed session in a sandbox
claude · ~/work/app · cache-frio
› 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 › /cache-frio ⎿ cache-frio: ctx 97k · cache 60min · 93% hit · ttl 60 min · aviso ligado ────────────────────────────────────────────────────────────────────────────────────────────────────────────────────── › ? for shortcuts
README

claude-code-mods

Cinco mods para o Claude Code, escritos como plugins de function hooks. Desenham barras e faixas acima do prompt, avisam sobre cache e uso do plano, sobem um servidor de prévia para HTML e lembram o modelo de usar o seu design system.

Funcionam no terminal e na aba Code do app desktop. Textos e comandos estão em português.

Painel de progresso

Pré-requisitos

  • Claude Code recente, com suporte a function hooks.
  • A variável CLAUDE_CODE_ENABLE_FUNCTION_HOOKS=1 no bloco env do ~/.claude/settings.json. Sem ela nenhum dos mods carrega.
{
  "env": {
    "CLAUDE_CODE_ENABLE_FUNCTION_HOOKS": "1"
  }
}

Se o arquivo já tem um bloco env, acrescente só a linha da variável.

Instalação

Num terminal com claude aberto, instale cada mod que quiser:

/plugin install progresso --marketplace Kelvi-Maycon/claude-code-mods
/plugin install barra-uso --marketplace Kelvi-Maycon/claude-code-mods
/plugin install cache-frio --marketplace Kelvi-Maycon/claude-code-mods
/plugin install previa --marketplace Kelvi-Maycon/claude-code-mods
/plugin install ds-primeiro --marketplace Kelvi-Maycon/claude-code-mods

Na primeira vez o Claude Code pergunta se quer adicionar o marketplace: responda y. Depois escolha o escopo do usuário (o primeiro da lista). O mod já roda na sessão atual e nas próximas.

Instalado no escopo do usuário pelo terminal, o mod carrega também na aba Code do app desktop. O comando /plugin install só existe no terminal.

Mods

progresso

Barras de progresso

Barras de progresso acima do prompt. O modelo recebe uma ferramenta (progresso) e uma regra curta no system prompt: em tarefa com vários passos, cria uma barra com o plano e a atualiza conforme avança. Cada barra mostra o passo ativo, a etapa, o percentual e o estado (andamento, esperando você, falhou, concluída).

  • Subagentes e comandos Bash em background aparecem como faixas embaixo da barra, com tipo, modelo, esforço, ferramenta em uso, custo estimado e tempo.
  • Painel lateral "Progresso" com custo, tokens, tempo, agentes rodando, concluídos, com falha e planejados. Clicar num agente abre a conversa dele, e dá para mandar mensagem a ele por ali.
  • Som ao concluir, ao falhar e quando o modelo espera uma decisão sua.
  • Se o turno vai terminar com uma barra aberta, o mod pede ao modelo que a feche antes (uma vez por barra).

Comandos:

  • /progresso: mostra ou esconde as barras. Aceita on, off ou painel (abre o painel lateral).
  • /progresso-demo: demonstração de 30 segundos com todos os estados, faixas, painel e sons.
  • /progresso-limpar: remove todas as barras e faixas.

Ícones por tipo de agente: executor-leve, executor, executor-pesado, executor-design, investigador e leitor têm robôs próprios. Explore usa a lupa do investigador. general-purpose, Plan, claude, fork e qualquer outro nome usam o robô padrão, com o nome do tipo em cinza. Modelo e esforço vêm do frontmatter (name, model, effort) dos arquivos em ~/.claude/agents/.

Um passo cujo título termina com o tipo do agente entre parênteses, como Revisar tudo (executor-pesado), aparece no painel como agente planejado.

Ícones

barra-uso

Uma linha de pílulas acima do campo:

  • 5H e 7D: uso do plano nas janelas de 5 horas e 7 dias, com o tempo até renovar.
  • CTX: contexto usado até a compactação automática, pelo limite que o Claude Code informa (275k quando ele não informa nenhum).
  • CACHE: tempo restante do cache de prompt (TTL de 60 minutos, fixo) e taxa de acerto.
  • DS on/off: só aparece com o ds-primeiro configurado. Clicar alterna o DS nesta sessão.

Comando: /uso, com on ou off.

cache-frio

Quando a sessão ficou parada além do TTL do cache e o contexto passa de 100k tokens, o próximo envio regrava o cache inteiro e custa mais. O mod segura esse envio uma vez, devolve o texto ao campo e avisa. Enviar de novo dentro de 3 minutos passa direto. Comandos de barra (/compact, /clear) nunca são segurados.

Comando: /cache-frio mostra contexto e tempo de cache. /cache-frio ttl <minutos> ajusta o TTL (padrão 60), /cache-frio off e /cache-frio on desligam e ligam o aviso.

previa

Quando o modelo grava ou edita um .html, o mod sobe um python3 -m http.server na pasta do arquivo, a partir da porta 8765, e mostra a URL acima do prompt com botões para abrir, copiar e parar. O modelo recebe uma linha dizendo que o servidor já está no ar, para não abrir outro. Comandos Bash que matariam o servidor (kill, pkill, killall no processo dele) são negados.

Arquivos em /tmp e /var/folders não viram prévia.

Comando: /previa <caminho> serve uma pasta, /previa status lista os servidores, /previa parar encerra.

Requisitos: python3 e lsof no PATH. O botão Abrir usa o comando open do macOS; em Linux ele não faz nada e o botão de copiar continua funcionando.

ds-primeiro

No primeiro pedido de peça visual da sessão (página, landing, dashboard, slide, tela, componente...), anexa ao contexto uma linha pedindo que o modelo carregue a skill do seu design system antes de criar. Antes do envio aparece uma faixa avisando, com o botão "Tirar deste envio". A detecção é por verbos e alvos em português.

Não anexa quando o pedido já cita um design system, uma identidade visual, a sua skill, o rótulo ou um dos termos extras. Depois de compactar ou de /clear, volta a anexar no próximo pedido visual.

Sem configuração o mod não faz nada: não anexa, não mostra faixa e não registra comando. Para ligar, configure as opções do plugin. A tela de opções aparece na instalação, e depois fica em /config:

OpçãoO que éExemplo
skillNome da skill do design system. Vazio desliga o mod.meu-design-system
rotuloNome exibido na faixa, no toast e na linha anexada. Vazio usa o nome da skill.Minha Marca
ignorarTermos extras, separados por vírgula, que dispensam a linha quando citados.marca do cliente, sem ds

As opções ficam salvas no ~/.claude/settings.json, em pluginConfigs.

Comando: /ds mostra o estado. /ds on e /ds off valem para a sessão; /ds sempre on e /ds sempre off mudam o padrão.

Atualizar

claude plugin update <mod>@claude-code-mods

Depois, numa sessão aberta, rode /reload-plugins.

Desinstalar

claude plugin uninstall <mod>@claude-code-mods

Desenvolvimento

Cada plugin fica em plugins/<nome>/, com os hooks em hooks/ e os testes em tests/.

claude plugin validate plugins/progresso
claude plugin test plugins/progresso

O tsconfig.json de cada plugin estende .claude-plugin/types/tsconfig.json, que o Claude Code gera no primeiro carregamento do plugin. Num clone novo o editor e o tsc acusam tipos ausentes até esse primeiro carregamento (ou um claude plugin validate). A pasta gerada fica fora do git.

Os sons do progresso são gerados por plugins/progresso/sounds/gerar.py.

Créditos

A estrutura das barras do progresso foi portada do plan-progress (zycck/claude-mods), de Kirill Serditov, sob licença MIT. O texto da licença está em plugins/progresso/licenses/plan-progress-LICENSE.

Licença

MIT. Veja LICENSE.

Source 2 files
hooks/register.tsx 226 lines
1import { atom, read, update } from 'claude-code'
2import type { EngineInterface, Register } from 'claude-code'
3
4import type { Sessao } from '../types'
5
6const NOVA: Sessao = { fim: 0, ctx: 0, hit: null, rodando: false, avisadoEm: 0, liberado: false }
7const sessao = atom({ plugin: 'cache-frio', key: 'sessao' } as const, NOVA)
8
9const TTL_PADRAO_MIN = 60
10const CTX_MINIMO = 100_000
11const JANELA_SEGUNDO_ENVIO_MS = 3 * 60_000
12const MINUTO = 60_000
13// Fim do último turno, ctx e hit por id de sessão, para a retomada depois de
14// relançar o app; `$.clock.now()` é ms desde a época, comparável entre processos.
15const CHAVE_SESSAO = 'sessao:'
16const MAX_SESSOES = 30
17
18type Guardada = Pick<Sessao, 'fim' | 'ctx' | 'hit'>
19
20const k = (tokens: number) => `${Math.round(tokens / 1000)}k`
21
22const duracao = (ms: number) => {
23  const min = Math.floor(ms / MINUTO)
24
25  return min < 60 ? `${min} min` : `${Math.floor(min / 60)}h${String(min % 60).padStart(2, '0')}`
26}
27
28let ttlMin = TTL_PADRAO_MIN
29let avisa = true
30
31const cache = (s: Sessao, agora: number) => {
32  const resta = s.rodando ? ttlMin * MINUTO : s.fim + ttlMin * MINUTO - agora
33
34  return resta > 0 ? `cache ${Math.ceil(resta / MINUTO)}min` : 'cache frio'
35}
36
37async function linha($: EngineInterface) {
38  const s = await read($, sessao)
39
40  if (s.fim === 0) {
41    return undefined
42  }
43
44  const texto = cache(s, await $.clock.now())
45  const comCtx = s.ctx > 0 ? `ctx ${k(s.ctx)} · ${texto}` : texto
46
47  return s.hit != null ? `${comCtx} · ${s.hit}% hit` : comCtx
48}
49
50async function guarda($: EngineInterface, s: Sessao) {
51  const feita: Guardada = { fim: s.fim, ctx: s.ctx, hit: s.hit ?? null }
52  await $.store.set(CHAVE_SESSAO + (await $.session.id()), feita)
53}
54
55async function restaura($: EngineInterface) {
56  const feita = (await $.store.get(CHAVE_SESSAO + (await $.session.id()))) as Guardada | undefined
57
58  if (feita !== undefined) {
59    await update($, sessao, s => (feita.fim > s.fim ? { ...NOVA, ...feita } : s))
60  }
61}
62
63// O contexto encolheu: o ctx conhecido zera até o próximo turno.
64async function zeraCtx($: EngineInterface) {
65  const s = await read($, sessao)
66  const zerada: Sessao = { ...s, ctx: 0 }
67  await update($, sessao, () => zerada)
68
69  if (s.fim > 0) {
70    await guarda($, zerada)
71  }
72}
73
74async function poda($: EngineInterface) {
75  const chaves = (await $.store.keys()).filter(chave => chave.startsWith(CHAVE_SESSAO))
76
77  if (chaves.length <= MAX_SESSOES) {
78    return
79  }
80
81  const fins = await Promise.all(
82    chaves.map(async chave => ({ chave, fim: ((await $.store.get(chave)) as Guardada).fim })),
83  )
84  fins.sort((a, b) => b.fim - a.fim)
85  await Promise.all(fins.slice(MAX_SESSOES).map(velha => $.store.delete(velha.chave)))
86}
87
88export const register: Register = on => {
89  on('session.start', async ($, e, next) => {
90    ttlMin = Number(await $.store.get('ttl')) || TTL_PADRAO_MIN
91    avisa = (await $.store.get('aviso')) !== false
92    await $.command.register({
93      name: 'cache-frio',
94      description: 'Aviso de cache frio: estado, ttl em minutos, off e on',
95      argumentHint: '[ttl <minutos> | off | on]',
96    })
97    await restaura($)
98    await poda($)
99
100    return next(e)
101  })
102
103  on('command.run', { command: 'cache-frio' }, async ($, e) => {
104    const [acao, valor] = e.args.trim().toLowerCase().split(/\s+/)
105
106    if (acao === 'ttl') {
107      const min = Number(valor)
108
109      if (!Number.isInteger(min) || min < 1) {
110        return { text: 'Use /cache-frio ttl <minutos>, com um número inteiro a partir de 1.' }
111      }
112
113      ttlMin = min
114      await $.store.set('ttl', min)
115    } else if (acao === 'off' || acao === 'on') {
116      avisa = acao === 'on'
117      await $.store.set('aviso', avisa)
118    } else if (acao) {
119      return { text: 'Use /cache-frio, /cache-frio ttl <minutos>, /cache-frio off ou /cache-frio on.' }
120    }
121
122    const estado = (await linha($)) ?? 'sem turno nesta sessão ainda'
123
124    return { text: `${estado} · ttl ${ttlMin} min · aviso ${avisa ? 'ligado' : 'desligado'}` }
125  })
126
127  on('turn.start', async ($, e, next) => {
128    await update($, sessao, s => ({ ...s, rodando: true }))
129
130    return next(e)
131  })
132
133  // O uso de `turn.complete` soma todas as requisições do turno: serve para o
134  // hit, não para o ctx, que é o `context.tokens` da sessão (última requisição).
135  on('turn.complete', async ($, e, next) => {
136    if (e.agentId === undefined) {
137      const [agora, uso, s] = await Promise.all([$.clock.now(), $.session.usage(), read($, sessao)])
138      const lidos = e.usage?.cache_read_input_tokens ?? 0
139      const entrada = lidos + (e.usage?.cache_creation_input_tokens ?? 0) + (e.usage?.input_tokens ?? 0)
140      const fechada: Sessao = {
141        ...NOVA,
142        fim: agora,
143        ctx: uso.context.tokens ?? s.ctx,
144        hit: entrada > 0 ? Math.round((100 * lidos) / entrada) : (s.hit ?? null),
145      }
146      await update($, sessao, () => fechada)
147      await guarda($, fechada)
148    }
149
150    return next(e)
151  })
152
153  on('session.compact', async ($, e, next) => {
154    const feito = await next(e)
155
156    if (e.agentId === undefined && e.trigger !== 'precompute' && feito.skip === undefined) {
157      await zeraCtx($)
158    }
159
160    return feito
161  })
162
163  // A sessão que segue (depois de /clear, de uma retomada ou de relançar o app)
164  // tem outro histórico: o estado zera e volta do store pelo id dela.
165  on('session.end', async ($, e, next) => {
166    await update($, sessao, () => NOVA)
167
168    return next(e)
169  })
170
171  on('prompt.submit', async ($, e, next) => {
172    // Só o Enter do usuário com a sessão ociosa (`composer` no terminal, `sdk` no
173    // app desktop); comando de barra passa, para /compact e /clear nunca serem segurados.
174    const doUsuario = e.origin.kind === 'composer' || e.origin.kind === 'sdk'
175
176    if (!avisa || !doUsuario || e.turnId !== undefined || e.text.startsWith('/')) {
177      return next(e)
178    }
179
180    let s = await read($, sessao)
181
182    // Retomada sem `session.start` (troca de sessão no mesmo processo).
183    if (s.fim === 0) {
184      await restaura($)
185      s = await read($, sessao)
186    }
187
188    if (s.fim === 0 || s.liberado || s.ctx <= CTX_MINIMO) {
189      return next(e)
190    }
191
192    const agora = await $.clock.now()
193    const parado = agora - s.fim
194
195    if (parado <= ttlMin * MINUTO) {
196      return next(e)
197    }
198
199    const libera = () => update($, sessao, x => ({ ...x, liberado: true }))
200
201    if (s.avisadoEm > 0 && agora - s.avisadoEm <= JANELA_SEGUNDO_ENVIO_MS) {
202      await libera()
203
204      return next(e)
205    }
206
207    const custo = `Cache frio: parado há ${duracao(parado)}. Este envio regrava ~${k(s.ctx)} tokens.`
208    const temAnexo = (e.attachments?.length ?? 0) > 0
209    // Só segura o envio com o texto já de volta no campo: anexo não volta por
210    // `prompt.fill`, e um campo que recusa o texto também não (diálogo aberto, ou
211    // o app desktop, que não tem campo na engine: no_composer). Aí só avisa.
212    const voltou = !temAnexo && (await $.prompt.fill({ text: e.text })).isFilled
213
214    if (!voltou) {
215      await libera()
216      $.ui.toast(custo, { timeoutMs: 8000 })
217
218      return next(e)
219    }
220
221    await update($, sessao, x => ({ ...x, avisadoEm: agora }))
222
223    return { drop: `${custo} Enter de novo envia; ou compacte antes (/compact) ou abra sessão nova.` }
224  })
225}
226
types/index.d.ts 22 lines
1export type Sessao = {
2  /** Fim do último turno da conversa principal, em ms; 0 antes do primeiro. */
3  fim: number
4  /** Tokens de entrada da última requisição principal; 0 quando desconhecido. */
5  ctx: number
6  /** Parte da entrada do último turno principal lida do cache, de 0 a 100; null sem dado. */
7  hit: number | null
8  rodando: boolean
9  /** Quando o envio frio foi segurado, em ms; 0 se ainda não foi. */
10  avisadoEm: number
11  /** O período frio atual já foi avisado e liberado. */
12  liberado: boolean
13}
14
15declare module 'claude-code' {
16  interface PluginState {
17    'cache-frio': {
18      sessao: Sessao
19    }
20  }
21}
22