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

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.
| Mod | Was er macht | Befehl |
|---|---|---|
| cache-countdown | Zeigt ü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-radius | Hä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-steps | Schlägt nach jeder Antwort drei nächste Schritte vor. Ein Klick schickt den Schritt als Prompt. | /vorschlaege |
| todo-list | Arbeitsliste im Seitenfenster. Aufgaben einreihen, ohne Claude zu unterbrechen. Mit Auto arbeitet Claude sie nacheinander ab. | /todo |
| grill | Stellt 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.
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"
}
}
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.
git -C "$env:USERPROFILE\.claude\mods" pull
Danach Claude Code neu starten. Kommt ein neuer Mod dazu, install.ps1 noch einmal ausführen.
<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.
claude plugin validate .\cache-countdown
version in plugin.json erhöhen und einen Eintrag in CHANGELOG.md schreiben.Neue Mods am einfachsten mit dem Claude-Code-Skill plugin-authoring bauen.
hooks/register.tsx 189 lines1import { 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}
189types/index.d.ts 10 lines1declare module 'claude-code' {
2 interface PluginState {
3 'cache-countdown': {
4 lastActivity: number
5 tick: number
6 autoCompact: boolean
7 }
8 }
9}
10