SLOPSHOPPER

cache-countdown

Zeigt ueber dem Prompt, wie lange der Prompt-Cache noch warm ist, mit Compact-Knopf und Auto-Compact kurz vor kalt

newbandcommandtoasttimer
v0.2.0MITupdated 2026-10-06broening/claude-mods/cache-countdown
A shopper browsing a rack in a slop shop
Preview · a replayed session in a sandbox
claude · ~/work/app · cache-countdown
› 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 › /compact-cache ⎿ cache-countdown: Verdichte die Unterhaltung ... ⟨Claude Code's own drawing⟩ ● 59m warm ██████████ [ Compact ] [ Auto: an ] ────────────────────────────────────────────────────────────────────────────────────────────────────────────────────── › ? for shortcuts

Draws

Band
⟨Claude Code's own drawing⟩ ● 59m warm ██████████ [ Compact ] [ Auto: an ]
README

claude-mods

Mods für Claude Code. Jeder Mod ist ein kleines Plugin aus TypeScript-Hooks. Er ändert Claude Code von innen: ein Band über dem Eingabefeld, ein Seitenfenster, eigene Slash-Befehle oder eine Rückfrage vor Tool-Aufrufen.

Mods prüfen

Die Mods

ModWas er machtBefehl
cache-countdownZeigt über dem Prompt, wie lange der Prompt-Cache (1 Stunde) noch warm ist: grün, orange ab 15 Min, rot wenn kalt. Knopf Compact und Schalter Auto: verdichtet 2 Minuten vor kalt von selbst./compact-cache
blast-radiusHält gefährliche Shell-Befehle (Bash und PowerShell) an, zum Beispiel rm -rf, Remove-Item -Recurse, git reset --hard, git push --force, DROP TABLE. Fragt vor dem Ausführen nach. Standard aus./ward an/aus
next-stepsSchlägt nach jeder Antwort drei nächste Schritte vor. Ein Klick schickt den Schritt als Prompt./vorschlaege
todo-listArbeitsliste im Seitenfenster. Aufgaben einreihen, ohne Claude zu unterbrechen. Mit Auto arbeitet Claude sie nacheinander ab./todo
grillStellt bis zu 5 Rückfragen zu deinem Plan, eine nach der anderen, mit Antwortknöpfen und KI-Vorschlag./grill [thema]

Hinweis: next-steps und grill rufen das Modell zusätzlich auf ($.model.fork). Das kostet etwas Kontingent.

Voraussetzungen

  • Claude Code mit Mod-Unterstützung (getestet mit 2.1.287)
  • Git
  • Windows: PowerShell 5.1 oder neuer. macOS und Linux: siehe unten.

Installieren (neuer Rechner)

Unter Windows in PowerShell:

git clone https://github.com/broening/claude-mods.git "$env:USERPROFILE\.claude\mods"
& "$env:USERPROFILE\.claude\mods\install.ps1"

Danach Claude Code neu starten. Das Skript trägt alle Mod-Ordner in ~/.claude/settings.json unter env.CLAUDE_CODE_PLUGIN_DIRS ein. Andere Einstellungen bleiben. Eine Sicherung liegt danach in settings.json.bak.

Nur einzelne Mods:

& "$env:USERPROFILE\.claude\mods\install.ps1" -Only cache-countdown,blast-radius

macOS und Linux: Repo nach ~/.claude/mods klonen und in ~/.claude/settings.json eintragen (Trennzeichen :):

{
  "env": {
    "CLAUDE_CODE_PLUGIN_DIRS": "/home/<du>/.claude/mods/cache-countdown:/home/<du>/.claude/mods/blast-radius"
  }
}

Alternative: als Plugin-Marktplatz

Das Repo ist auch ein Claude-Code-Marktplatz (.claude-plugin/marketplace.json). In Claude Code:

/plugin marketplace add broening/claude-mods
/plugin install cache-countdown@claude-mods

Nicht beides gleichzeitig nutzen, sonst lädt ein Mod doppelt.

Aktualisieren

git -C "$env:USERPROFILE\.claude\mods" pull

Danach Claude Code neu starten. Kommt ein neuer Mod dazu, install.ps1 noch einmal ausführen.

Aufbau eines Mods

<mod>/
  .claude-plugin/plugin.json   Name, Version, Beschreibung
  hooks/hooks.json             { "modules": ["./register.tsx"] }
  hooks/register.tsx           die Hooks: export const register: Register = on => { ... }
  types/index.d.ts             Vertrag für den gespeicherten Zustand ($.state)
  tsconfig.json

.claude-plugin/types/ erzeugt Claude Code bei jedem Build selbst. Der Ordner ist nicht im Repo.

Entwickeln

  1. Mod in seinem Ordner ändern. In einer laufenden Sitzung lädt Claude Code ihn nach dem nächsten Turn neu.
  2. Prüfen:
   claude plugin validate .\cache-countdown
  1. version in plugin.json erhöhen und einen Eintrag in CHANGELOG.md schreiben.
  2. Committen und pushen. GitHub Actions prüft alle Mods bei jedem Push.

Neue Mods am einfachsten mit dem Claude-Code-Skill plugin-authoring bauen.

Lizenz

MIT

Source 2 files
hooks/register.tsx 189 lines
1import { atom, read, update } from 'claude-code'
2import type { EngineInterface, Register } from 'claude-code'
3
4// Rest-Warmzeit des 1-Stunden-Prompt-Cache ueber dem Prompt anzeigen, als
5// Ampel: gruen (warm), orange (bald kalt), rot (kalt). Dazu ein Compact-Knopf
6// und ein Auto-Schalter (an/aus).
7//
8// AUTOMATIK: Wenn der Auto-Schalter an ist, verdichtet der Mod 2 Minuten bevor
9// der Cache kalt wird von selbst, damit die naechste Nachricht nicht die ganze
10// grosse Unterhaltung zum vollen Preis neu schickt. Einmal pro Leerlauf.
11// Schalter aus = kein automatisches Verdichten (wenn du fertig bist).
12//
13// Die Cache-API gibt die echte Restzeit nicht her, also zaehlen wir selbst:
14// jede fertige Antwort = frischer Request = Cache wieder warm.
15
16const TTL_MS = 60 * 60 * 1000 // 1 Stunde (Abo). Bei API-Key waeren es 5 Min.
17const WARN_MS = 15 * 60 * 1000 // ab hier "bald kalt" (orange)
18const AUTO_COMPACT_MS = 2 * 60 * 1000 // 2 Min vor kalt automatisch verdichten
19const BAR_WIDTH = 10
20
21const GREEN = '#22c55e'
22const ORANGE = '#f59e0b'
23const RED = '#ef4444'
24const GREY = '#9ca3af'
25
26const lastActivity = atom(
27  { plugin: 'cache-countdown', key: 'lastActivity' } as const,
28  0,
29)
30// Nur zum periodischen Neuzeichnen, damit die Uhr auch im Leerlauf tickt.
31const tick = atom({ plugin: 'cache-countdown', key: 'tick' } as const, 0)
32// Auto-Compact an/aus (pro Sitzung, Start = an).
33const autoCompact = atom(
34  { plugin: 'cache-countdown', key: 'autoCompact' } as const,
35  true,
36)
37
38// Scharf, solange noch nicht in diesem Leerlauf automatisch verdichtet wurde.
39let armed = true
40
41function cacheState(
42  nowMs: number,
43  lastMs: number,
44): { phrase: string; color: string; frac: number } {
45  if (!lastMs) return { phrase: '--', color: GREY, frac: 0 }
46  const remainMs = Math.max(0, TTL_MS - (nowMs - lastMs))
47  const m = Math.floor(remainMs / 60000)
48  const frac = remainMs / TTL_MS
49  if (remainMs <= 0) return { phrase: 'kalt', color: RED, frac: 0 }
50  if (remainMs <= WARN_MS) {
51    return { phrase: `${m}m bald kalt`, color: ORANGE, frac }
52  }
53  return { phrase: `${m}m warm`, color: GREEN, frac }
54}
55
56// Verdichten. In der Desktop-App (SDK-Sitzung) gibt es $.session.compact()
57// nicht; dann laeuft der eingebaute /compact-Befehl (wartet, bis Claude frei ist).
58async function compactNow($: EngineInterface): Promise<void> {
59  try {
60    await $.session.compact()
61  } catch {
62    await $.command.run({ command: 'compact' })
63  }
64}
65
66export const register: Register = on => {
67  on('session.start', async ($, e, next) => {
68    const now = await $.clock.now()
69    await update($, lastActivity, () => now)
70    armed = true
71
72    // Gemerkten Auto-Schalter laden (sitzungsuebergreifend). Standard = an.
73    const savedAuto = await $.store.get('autoCompact')
74    await update($, autoCompact, () => (savedAuto === false ? false : true))
75
76    // Alle 30s: neu zeichnen und pruefen, ob automatisch verdichtet werden muss.
77    $.clock.every(30_000, async () => {
78      void update($, tick, n => n + 1)
79
80      if (!(await read($, autoCompact))) return
81
82      const lastMs = await read($, lastActivity)
83      if (!lastMs) return
84
85      const remainMs = Math.max(0, TTL_MS - ((await $.clock.now()) - lastMs))
86
87      if (armed && remainMs > 0 && remainMs <= AUTO_COMPACT_MS) {
88        armed = false
89        $.ui.toast('Cache fast kalt - verdichte automatisch, um Tokens zu sparen')
90        try {
91          await compactNow($)
92          // Verdichten ist ein frischer Request -> Cache wieder warm.
93          const after = await $.clock.now()
94          await update($, lastActivity, () => after)
95        } catch (err) {
96          $.ui.toast(`Auto-Compact fehlgeschlagen: ${String(err)}`)
97        } finally {
98          armed = true
99        }
100      }
101    })
102
103    await $.command.register({
104      name: 'compact-cache',
105      description: 'Unterhaltung verdichten, um den Prompt-Cache zu sparen',
106    })
107
108    return next(e)
109  })
110
111  // Jede fertige Antwort haelt den Cache warm -> Uhr zuruecksetzen, Automatik neu scharf.
112  on('turn.complete', async ($, e, next) => {
113    const now = await $.clock.now()
114    await update($, lastActivity, () => now)
115    armed = true
116
117    return next(e)
118  })
119
120  // Band ueber dem Prompt. Nur EIN Band ("one instance"), darum reichen wir das
121  // darunterliegende Band (below) durch und haengen unseres an.
122  on('ui.render', { component: 'AbovePrompt' }, async ($, e, next) => {
123    const below = await next(e)
124    if (e.props.hasSurvey) {
125      return below
126    }
127
128    await read($, tick) // abonnieren, damit der Timer neu zeichnet
129    const lastMs = await read($, lastActivity)
130    const now = await $.clock.now()
131    const auto = await read($, autoCompact)
132
133    const { phrase, color, frac } = cacheState(now, lastMs)
134    const filled = Math.max(0, Math.min(BAR_WIDTH, Math.round(frac * BAR_WIDTH)))
135    const fill = '█'.repeat(filled)
136    const track = '░'.repeat(BAR_WIDTH - filled)
137
138    const { Box, Text, Button } = $.ui.resolve(e)
139
140    return (
141      <Box flexDirection="column">
142      {below}
143      <Box>
144        <Text color={color}>{'●'} {phrase}  </Text>
145        <Text color={color}>{fill}</Text>
146        <Text dimColor>{track}</Text>
147        <Text>{'  '}</Text>
148        <Button
149          key="compact"
150          label="Compact"
151          onPress={() => {
152            $.ui.toast('Verdichte die Unterhaltung ...')
153            compactNow($).catch(err =>
154              $.ui.toast(`Compact fehlgeschlagen: ${String(err)}`),
155            )
156          }}
157        />
158        <Text>{' '}</Text>
159        <Button
160          key="auto"
161          label={auto ? 'Auto: an' : 'Auto: aus'}
162          onPress={() => {
163            const nextAuto = !auto
164            void update($, autoCompact, () => nextAuto)
165            void $.store.set('autoCompact', nextAuto) // sitzungsuebergreifend merken
166            $.ui.toast(
167              nextAuto ? 'Auto-Compact eingeschaltet' : 'Auto-Compact ausgeschaltet',
168            )
169          }}
170        />
171      </Box>
172      </Box>
173    )
174  })
175
176  // /compact-cache
177  // Erst nach diesem Befehl verdichten (clock.after), sonst wartet der Befehl
178  // auf sich selbst.
179  on('command.run', { command: 'compact-cache' }, async $ => {
180    $.clock.after(500, () =>
181      compactNow($).catch(err =>
182        $.ui.toast(`Compact fehlgeschlagen: ${String(err)}`),
183      ),
184    )
185
186    return { text: 'Verdichte die Unterhaltung ...' }
187  })
188}
189
types/index.d.ts 10 lines
1declare module 'claude-code' {
2  interface PluginState {
3    'cache-countdown': {
4      lastActivity: number
5      tick: number
6      autoCompact: boolean
7    }
8  }
9}
10