Dashboard /helfer und Band über dem Prompt: Kontingente von Claude, Codex und Antigravity, FreeLLMAPI, Free Claude Code und laufende Hintergrund-Aufträge auf…

English summary: A Claude Code mod that adds a /helfer dashboard and a status band above the prompt. It shows usage limits of Claude, OpenAI Codex and Google Antigravity, the local FreeLLMAPI and Free Claude Code proxies, and background jobs started by the user's own helper scripts. Everything is read locally; nothing is sent anywhere unless HELFER_LINKS=1 is set (see below). Sections for tools that are not installed show "nicht eingerichtet" (not set up).
~/.codex/sessions.Offen gelegt für die Prüfung und für alle, die sie installieren:
helfer-status --json. Liegt eine ausführbare Datei unter ~/.local/bin/helfer-status, nimmt sie diese (eigene Anpassung des Nutzers), sonst das mitgelieferte bin/helfer-status (Python 3).~/.codex/sessions, ~/.local/state/helfer/auftraege.jsonl, die Protokolldatei .helfer.log im Ordner eines Auftrags, die FreeLLMAPI-Datenbank unter ~/Library/Application Support/FreeLLMAPI/freeapi.db (mit sqlite3 -readonly). Schlüssel werden nicht gelesen.agy -p /usage (Antigravity-Kontingent) und ~/.local/bin/ki-weiche (eigene Empfehlung des Nutzers).GET http://127.0.0.1:8082/admin/api/status für Free Claude Code (Port über FCC_PORT). Nur Status, keine Schlüssel.HELFER_LINKS=1. Dann lädt /artikel die Artikelliste von https://stevennoack.de/index.json und das Dashboard zeigt Links zu stevennoack.de. Ohne die Variable gibt es keinen Abruf.ERGEBNIS.md eines Auftrags mit open -a TextEdit (macOS).~/.local/state/helfer/claude-kontingent.json mit den Prozentwerten und Rücksetzzeiten des Claude-Kontingents (sonst nichts). Die Datei liest das optionale Skript ki-weiche des Nutzers. Andere Dateien schreibt die Mod nicht.Die Prüfung von Anthropic meldet in hooks/register.tsx einen Programmaufruf, dessen Befehl sie nicht ganz lesen kann (Zeile mit $.process.run([bin, '--json'])). bin ist genau einer von zwei festen Pfaden: ~/.local/bin/helfer-status, falls vorhanden, sonst ${CLAUDE_PLUGIN_ROOT}/bin/helfer-status aus diesem Plugin. Das Argument ist immer --json. Der zweite Aufruf, open -a TextEdit <Pfad>, öffnet nur eine ERGEBNIS.md zum Lesen. Die Mod nimmt keine Befehle von einem Server an und reicht keine Netzwerkdaten an Programme weiter.
session.start: startet den Datenlieferanten alle 10 Sekunden und registriert die Befehle /helfer (und mit HELFER_LINKS=1 auch /artikel).command.run für helfer und artikel: öffnet das Dashboard bzw. die Artikelliste.ui.render für das eigene Panel und AbovePrompt: zeichnet das Dashboard, die Artikelliste und das Band über dem Prompt aus den gelesenen Daten. Die Hooks lesen und verändern keine anderen Aufrufe.Die Mod erhebt, speichert und überträgt keine personenbezogenen Daten. Alles bleibt auf dem eigenen Rechner. Die einzige Verbindung ins Internet ist der Abruf der öffentlichen Artikelliste https://stevennoack.de/index.json, und den gibt es nur mit HELFER_LINKS=1. Dabei werden keine Daten aus dem Rechner mitgeschickt. Die Mod schickt keine Eingaben ab und ändert keine Einstellungen.
Claude Code mit Mod-Unterstützung, Python 3. Optional: codex, agy, FreeLLMAPI, Free Claude Code.
Installation, Anpassung und das JSON-Format von helfer-status stehen im README des Repos: https://github.com/MacStenk/claude-mods
MIT, siehe LICENSE.
hooks/register.tsx 403 lines1import { atom, read, update } from 'claude-code'
2import type { Register } from 'claude-code'
3
4import type { ArtikelEintrag, Auftrag, FccStand, Fenster, Stand } from '../types'
5
6const auftraege = atom({ plugin: 'helfer-band', key: 'auftraege' } as const, [] as Auftrag[])
7const jetzt = atom({ plugin: 'helfer-band', key: 'jetzt' } as const, 0)
8const stand = atom({ plugin: 'helfer-band', key: 'stand' } as const, null as Stand | null)
9const claude = atom({ plugin: 'helfer-band', key: 'claude' } as const, [] as Fenster[])
10const artikel = atom({ plugin: 'helfer-band', key: 'artikel' } as const, [] as ArtikelEintrag[])
11const PANE = 'helfer-dashboard'
12const ARTIKEL_PANE = 'helfer-artikel'
13
14export function datumFormatiert(ts: number | string): string {
15 const d = new Date(typeof ts === 'number' ? (ts > 1e11 ? ts : ts * 1000) : ts)
16 if (isNaN(d.getTime())) return ''
17 const tag = String(d.getDate()).padStart(2, '0')
18 const monat = String(d.getMonth() + 1).padStart(2, '0')
19 const jahr = d.getFullYear()
20 return `${tag}.${monat}.${jahr}`
21}
22
23export function sortierteArtikel(entries: ArtikelEintrag[]): ArtikelEintrag[] {
24 const zeit = (v: number | string | undefined): number => {
25 if (typeof v === 'number') return v > 1e11 ? v : v * 1000
26 if (typeof v === 'string') {
27 const n = Number(v)
28 if (!isNaN(n)) return n > 1e11 ? n : n * 1000
29 const t = new Date(v).getTime()
30 return isNaN(t) ? 0 : t
31 }
32 return 0
33 }
34 return [...entries].sort((a, b) => zeit(b.published_at) - zeit(a.published_at))
35}
36
37export function sichtbareArtikel(entries: ArtikelEintrag[], rows?: number): ArtikelEintrag[] {
38 const room = Math.max(1, (rows ?? 24) - 4)
39 return entries.slice(0, room)
40}
41
42const ZIEL_NAME: Record<string, string> = {
43 'agy-sonnet': 'helfer auto → agy mit Claude Sonnet (Antigravity)',
44 'agy-gemini': 'helfer auto → agy mit Gemini (Antigravity)',
45 'claude-code': 'helfer auto → Claude Code (dein Abo)',
46 codex: 'helfer auto → Codex (OpenAI-Abo)',
47 keins: 'nichts frei, Aufträge warten',
48}
49
50export function wann(ziel: Date, jetzt: Date = new Date()): string {
51 const hhmm = `${String(ziel.getHours()).padStart(2, '0')}:${String(ziel.getMinutes()).padStart(2, '0')}`
52 const tag = (d: Date) => new Date(d.getFullYear(), d.getMonth(), d.getDate()).getTime()
53 const diffTage = Math.round((tag(ziel) - tag(jetzt)) / 86400000)
54 const min = Math.round((ziel.getTime() - jetzt.getTime()) / 60000)
55 const abstand = min <= 0 ? '' : min < 60 ? ` (in ${min} Min.)` : min < 1440 ? ` (in ${Math.floor(min / 60)} Std. ${min % 60} Min.)` : ''
56 if (diffTage === 0) return `heute ${hhmm}${abstand}`
57 if (diffTage === 1) return `morgen ${hhmm}${abstand}`
58 const wt = ['So.', 'Mo.', 'Di.', 'Mi.', 'Do.', 'Fr.', 'Sa.'][ziel.getDay()]
59 return `${wt} ${String(ziel.getDate()).padStart(2, '0')}.${String(ziel.getMonth() + 1).padStart(2, '0')}. ${hhmm}`
60}
61
62export function empfehlung(st: Stand, cl: Fenster[]): string[] {
63 if (st.weiche && (st.weiche.schreiben || st.weiche.bauen)) {
64 const z: string[] = []
65 const n = (w: { ziel: string } | null | undefined) => (w ? ZIEL_NAME[w.ziel] ?? w.ziel : 'unbekannt')
66 if (st.weiche.schreiben) z.push(`Schreiben: ${n(st.weiche.schreiben)}`)
67 if (st.weiche.bauen) z.push(`Bauen: ${n(st.weiche.bauen)}`)
68 z.push('Einzelfragen, Texte, Klassifizieren: FreeLLMAPI (127.0.0.1:31415)')
69 const woche = cl.find(f => f.kind === 'seven_day')?.percentUsed ?? 0
70 if (woche >= 80) z.push(`Claude sparen: Woche bei ${Math.round(woche)} %, nur planen und prüfen`)
71 return z
72 }
73 const k = st.kontingent.codex
74 const woche = cl.find(f => f.kind === 'seven_day')?.percentUsed ?? 0
75 const z: string[] = []
76 z.push(k && k.fenster_prozent >= 95 ? `Bauen: helfer adler (Codex frei ab ${uhr(k.fenster_reset)})` : 'Bauen: helfer codex')
77 z.push('Einzelfragen, Texte, Klassifizieren: FreeLLMAPI (127.0.0.1:31415)')
78 if (woche >= 80) z.push(`Claude sparen: Woche bei ${Math.round(woche)} %, nur planen und prüfen`)
79 return z
80}
81
82export function zahl(n: number): string {
83 return n.toLocaleString('de-DE')
84}
85
86export type FccZeile = {
87 text: string
88 farbe?: 'green' | 'red'
89 dim?: boolean
90}
91
92export function fccZeilen(fcc: FccStand | null | undefined): FccZeile[] {
93 if (!fcc) {
94 return [{ text: 'nicht gestartet (App ‚Free Claude Code‘ oder `fcc-server`)', dim: true }]
95 }
96 const zeilen: FccZeile[] = []
97 zeilen.push({ text: `${fcc.laeuft ? 'läuft' : 'gestoppt'}${fcc.modell ? ` · Modell ${fcc.modell}` : ''}` })
98 if (fcc.bereit && fcc.bereit.length > 0) {
99 zeilen.push({ text: `bereit: ${fcc.bereit.join(', ')}`, farbe: 'green' })
100 }
101 if (fcc.fehler && fcc.fehler.length > 0) {
102 zeilen.push({ text: `Start fehlgeschlagen: ${fcc.fehler.join(', ')}`, farbe: 'red' })
103 }
104 zeilen.push({ text: `${fcc.eingerichtet} Anbieter eingerichtet, ${fcc.modelle} Modelle` })
105 return zeilen
106}
107
108function uhr(ts: number): string {
109 const d = new Date(ts * 1000)
110 return `${String(d.getHours()).padStart(2, '0')}:${String(d.getMinutes()).padStart(2, '0')}`
111}
112
113export function restText(alle: { wer: string; start: number; ende: number | null; rc: number | null }[], a: { wer: string; start: number }, nun: number): string {
114 const fertig = alle.filter(x => x.wer === a.wer && x.ende !== null && x.rc === 0).map(x => (x.ende as number) - x.start)
115 if (fertig.length === 0) return ''
116 const ueblich = Math.round(fertig.reduce((s, d) => s + d, 0) / fertig.length)
117 const bisher = nun - a.start
118 return bisher < ueblich ? `, noch etwa ${dauer(ueblich - bisher)}` : `, länger als üblich (sonst ${dauer(ueblich)})`
119}
120
121export function balken(prozent: number, breite = 20): string {
122 const voll = Math.max(0, Math.min(breite, Math.round((prozent / 100) * breite)))
123 return '█'.repeat(voll) + '░'.repeat(breite - voll)
124}
125
126// Beendete Aufträge bleiben so lange sichtbar
127const NACHLAUF_S = 30 * 60
128
129export function auswerten(text: string, nun: number): Auftrag[] {
130 const byId = new Map<string, Auftrag>()
131 for (const zeile of text.split('\n')) {
132 if (!zeile.trim()) continue
133 let z: Record<string, unknown>
134 try { z = JSON.parse(zeile) } catch { continue }
135 const id = String(z.id ?? '')
136 if (!id) continue
137 if (typeof z.start === 'number') {
138 byId.set(id, {
139 id, wer: String(z.wer ?? '?'), ordner: String(z.ordner ?? ''), auftrag: String(z.auftrag ?? ''),
140 start: z.start, ende: byId.get(id)?.ende ?? null, rc: byId.get(id)?.rc ?? null,
141 })
142 } else if (typeof z.ende === 'number') {
143 const a = byId.get(id)
144 if (a) { a.ende = z.ende; a.rc = typeof z.rc === 'number' ? z.rc : null }
145 }
146 }
147 return [...byId.values()]
148 .filter(a => a.ende === null ? nun - a.start < 24 * 3600 : nun - a.ende < NACHLAUF_S)
149 .sort((a, b) => b.start - a.start)
150}
151
152export function dauer(s: number): string {
153 const m = Math.floor(s / 60)
154 if (m < 1) return `${s} s`
155 return m >= 60 ? `${Math.floor(m / 60)} Std. ${m % 60} Min.` : `${m} Min.`
156}
157
158export const register: Register = on => {
159 on('session.start', async ($, e, next) => {
160 const home = (await $.env.get('HOME')) ?? ''
161 const pfad = `${home}/.local/state/helfer/auftraege.jsonl`
162 const laden = async () => {
163 const nun = Math.floor((await $.clock.now()) / 1000)
164 let text = ''
165 try { text = await $.fs.read(pfad) } catch { text = '' }
166 await update($, auftraege, () => auswerten(text, nun))
167 await update($, jetzt, () => nun)
168 }
169 await laden()
170 $.clock.every(5000, () => { void laden() })
171
172 const statusLaden = async () => {
173 try {
174 let bin = `${home}/.local/bin/helfer-status`
175 let hatLocal = false
176 try {
177 await $.fs.stat(bin)
178 hatLocal = true
179 } catch {
180 hatLocal = false
181 }
182 if (!hatLocal) {
183 const pluginRoot = (await $.env.get('CLAUDE_PLUGIN_ROOT')) ?? $.plugin.root ?? ''
184 if (pluginRoot) {
185 bin = `${pluginRoot}/bin/helfer-status`
186 }
187 }
188 const r = await $.process.run([bin, '--json'])
189 if (r.exitCode === 0) await update($, stand, () => JSON.parse(r.stdout) as Stand)
190 const u = await $.session.usage()
191 await update($, claude, () => u.rateLimits.map(f => ({ kind: f.kind, percentUsed: f.percentUsed, resetsAt: f.resetsAt })))
192 // Für ki-weiche: Claude-Code-Kontingent als Datei (nur Prozentwerte)
193 try { await $.fs.write(`${home}/.local/state/helfer/claude-kontingent.json`, JSON.stringify(u.rateLimits.map(f => ({ kind: f.kind, percentUsed: f.percentUsed, resetsAt: f.resetsAt })))) } catch { /* ohne Datei nimmt ki-weiche Claude Code nicht */ }
194 } catch { /* nächster Versuch in 10 s */ }
195 }
196 void statusLaden()
197 $.clock.every(10000, () => { void statusLaden() })
198
199 const linksAktiv = Boolean(await $.env.get('HELFER_LINKS'))
200 if (linksAktiv) {
201 const artikelLaden = async () => {
202 try {
203 const r = await $.http.fetch('https://stevennoack.de/index.json')
204 if (r.ok) {
205 const daten = JSON.parse(r.text) as { entries?: ArtikelEintrag[] }
206 if (Array.isArray(daten.entries)) {
207 await update($, artikel, () => sortierteArtikel(daten.entries!))
208 }
209 }
210 } catch {
211 /* Fehler still abfangen */
212 }
213 }
214 void artikelLaden()
215 $.clock.every(10 * 60 * 1000, () => { void artikelLaden() })
216
217 await $.command.register({ name: 'artikel', description: 'Steven Noacks Artikel (stevennoack.de)' })
218 }
219
220 await $.command.register({ name: 'helfer', description: 'Dashboard: Helfer-Aufträge, Codex-Kontingent, FreeLLMAPI' })
221 return next(e)
222 })
223
224 on('command.run', { command: 'helfer' }, async $ => {
225 await $.ui.open({ id: PANE, title: 'Helfer' })
226 return { text: 'Helfer-Dashboard geöffnet.' }
227 })
228
229 on('command.run', { command: 'artikel' }, async $ => {
230 const linksAktiv = Boolean(await $.env.get('HELFER_LINKS'))
231 if (!linksAktiv) {
232 return { text: 'Artikel-Panel ist deaktiviert (HELFER_LINKS nicht gesetzt).' }
233 }
234 await $.ui.open({ id: ARTIKEL_PANE, title: 'Artikel' })
235 return { text: 'Artikel-Panel geöffnet.' }
236 })
237
238 on('ui.render', { component: 'Pane', requestId: PANE }, async ($, e) => {
239 const { Box, Button, Link, Text } = $.ui.resolve(e)
240 const st = await read($, stand)
241 if (!st) return <Text dimColor>Lade helfer-status …</Text>
242 const k = st.kontingent.codex
243 const cl = await read($, claude)
244 const farbe = (p: number) => (p >= 90 ? 'red' : p >= 70 ? 'yellow' : 'green')
245 const name: Record<string, string> = { five_hour: '5-Std.-Fenster', seven_day: 'Woche ' }
246 const port = String(st.fcc?.port || (await $.env.get('FCC_PORT')) || '8082')
247 const linksAktiv = Boolean(await $.env.get('HELFER_LINKS'))
248 return (
249 <Box flexDirection="column" gap={1}>
250 {linksAktiv ? (
251 <Box flexDirection="row" gap={2}>
252 <Link href="https://stevennoack.de/schreiben/">Schreiben</Link>
253 <Link href="https://stevennoack.de/logbuch/">Logbuch</Link>
254 <Link href="https://stevennoack.de/freigabe/">Freigaben</Link>
255 <Link href="https://stevennoack.de/">Website</Link>
256 <Button
257 key="pane-artikel"
258 label="Artikel"
259 onPress={async () => {
260 await $.ui.open({ id: ARTIKEL_PANE, title: 'Artikel' })
261 }}
262 />
263 </Box>
264 ) : null}
265 <Box flexDirection="column" borderStyle="round" paddingX={1}>
266 <Text bold>Jetzt nehmen</Text>
267 {empfehlung(st, cl).map(z => <Text key={z}>→ {z}</Text>)}
268 </Box>
269 <Box flexDirection="column" borderStyle="round" paddingX={1}>
270 <Text bold>Claude (dieses Abo)</Text>
271 {cl.length === 0 && <Text dimColor>noch keine Messung in dieser Sitzung</Text>}
272 {cl.map(f => (
273 <Text key={f.kind}>{name[f.kind] ?? f.kind} <Text color={farbe(f.percentUsed)}>{balken(f.percentUsed)}</Text> {Math.round(f.percentUsed)} %{f.resetsAt ? <Text dimColor> wieder voll {wann(new Date(f.resetsAt))}</Text> : null}</Text>
274 ))}
275 </Box>
276 <Box flexDirection="column" borderStyle="round" paddingX={1}>
277 <Text bold>Codex-Abo (OpenAI)</Text>
278 {k ? (
279 <Box flexDirection="column">
280 <Text>5-Std.-Fenster <Text color={farbe(k.fenster_prozent)}>{balken(k.fenster_prozent)}</Text> {Math.round(k.fenster_prozent)} % <Text dimColor>frei {wann(new Date(k.fenster_reset * 1000))}</Text></Text>
281 <Text>Woche <Text color={farbe(k.woche_prozent)}>{balken(k.woche_prozent)}</Text> {Math.round(k.woche_prozent)} %</Text>
282 </Box>
283 ) : <Text dimColor>nicht eingerichtet</Text>}
284 </Box>
285 <Box flexDirection="column" borderStyle="round" paddingX={1}>
286 <Text bold>Antigravity / agy (Google-Abo)</Text>
287 {!st.kontingent.agy && <Text dimColor>nicht eingerichtet</Text>}
288 {(st.kontingent.agy ?? []).map(g => {
289 const benutzt = 100 - g.rest
290 return (
291 <Text key={g.gruppe + g.fenster}>{(g.gruppe.startsWith('Gemini') ? 'Gemini' : 'Claude/GPT').padEnd(11)}{g.fenster === '5h' ? '5 Std.' : 'Woche '} <Text color={farbe(benutzt)}>{balken(benutzt)}</Text> {Math.round(benutzt)} %{g.reset ? <Text dimColor> wieder voll {wann(new Date(g.reset))}</Text> : null}</Text>
292 )
293 })}
294 </Box>
295 <Box flexDirection="column" borderStyle="round" paddingX={1}>
296 <Text bold>FreeLLMAPI {st.freellmapi && st.freellmapi.anbieter.length > 0 ? <Text dimColor>({st.freellmapi.anbieter.length} von {st.freellmapi.im_katalog ?? '?'} Anbietern eingerichtet)</Text> : null}</Text>
297 {(!st.freellmapi || st.freellmapi.anbieter.length === 0) && <Text dimColor>nicht eingerichtet</Text>}
298 {(st.freellmapi?.anbieter ?? []).map(a => {
299 const quote = a.anfragen_stunde ? Math.round((a.ok_stunde / a.anfragen_stunde) * 100) : 100
300 return (
301 <Text key={a.anbieter}>
302 <Text color={a.aktiv ? 'green' : 'gray'}>{a.aktiv ? '●' : '○'}</Text> {a.anbieter.padEnd(16)} letzte Std. <Text color={farbe(100 - quote)}>{a.ok_stunde}/{a.anfragen_stunde} ok</Text> <Text dimColor>heute {a.ok}/{a.anfragen}, {zahl(a.tokens)} Tokens{a.sperren ? `, ${a.sperren} gesperrt` : ''}</Text>
303 </Text>
304 )
305 })}
306 </Box>
307 <Box flexDirection="column" borderStyle="round" paddingX={1}>
308 <Box flexDirection="row" gap={2}>
309 <Text bold>Free Claude Code (127.0.0.1:{port})</Text>
310 <Link href={`http://127.0.0.1:${port}/admin`}>Admin</Link>
311 </Box>
312 {fccZeilen(st.fcc).map((z, idx) => (
313 <Text key={String(idx)} color={z.farbe} dimColor={z.dim}>{z.text}</Text>
314 ))}
315 </Box>
316 <Box flexDirection="column" borderStyle="round" paddingX={1}>
317 <Text bold>Aufträge (24 Std.)</Text>
318 {st.auftraege === null && <Text dimColor>nicht eingerichtet</Text>}
319 {st.auftraege !== null && st.auftraege.length === 0 && <Text dimColor>keine</Text>}
320 {(st.auftraege ?? []).slice(0, 8).map(a => {
321 const ort = a.ordner.split('/').filter(Boolean).pop() ?? a.ordner
322 const laeuft = a.ende === null
323 const f = laeuft ? 'yellow' : a.rc === 0 ? 'green' : 'red'
324 const z = laeuft ? `läuft ${dauer(st.jetzt - a.start)}${restText(st.auftraege ?? [], a, st.jetzt)}` : a.rc === 0 ? `fertig nach ${dauer((a.ende ?? st.jetzt) - a.start)}` : `Fehler ${a.rc}`
325 return (
326 <Box key={a.id} flexDirection="column">
327 <Box flexDirection="row" gap={1}>
328 <Text><Text color={f}>{laeuft ? '⚙' : a.rc === 0 ? '✓' : '✗'} {a.wer}</Text> {ort} · {a.auftrag} · <Text color={f}>{z}</Text>{a.tokens ? <Text dimColor> · {zahl(a.tokens)} Tokens, {a.runden} Runden</Text> : null}</Text>
329 <Button
330 key={`bericht-${a.id}`}
331 label="Bericht öffnen"
332 onPress={async () => {
333 const pfad = `${a.ordner}/ERGEBNIS.md`
334 try {
335 await $.fs.stat(pfad)
336 await $.process.run(['open', '-a', 'TextEdit', pfad])
337 } catch {
338 $.ui.toast(`Kein Bericht in ${a.ordner}`)
339 }
340 }}
341 />
342 </Box>
343 {a.letzte_zeile ? <Text dimColor> {a.letzte_zeile.slice(0, 110)}</Text> : null}
344 </Box>
345 )
346 })}
347 </Box>
348 <Text dimColor>aktualisiert {uhr(st.jetzt)} · Esc schließt</Text>
349 </Box>
350 )
351 })
352
353 on('ui.render', { component: 'Pane', requestId: ARTIKEL_PANE }, async ($, e) => {
354 const { Box, Link, Text } = $.ui.resolve(e)
355 const alle = await read($, artikel)
356 const sichtbar = sichtbareArtikel(alle, e.viewport?.rows)
357 return (
358 <Box flexDirection="column" gap={1}>
359 <Box flexDirection="column" borderStyle="round" paddingX={1}>
360 <Text bold>Artikel ({alle.length})</Text>
361 {alle.length === 0 && <Text dimColor>Lade Artikel von stevennoack.de …</Text>}
362 {sichtbar.map(a => (
363 <Text key={a.slug}>
364 <Text dimColor>{datumFormatiert(a.published_at)}</Text>{' '}
365 <Link href={`https://stevennoack.de/artikel/${a.slug}/`} label={a.title}>
366 {a.title}
367 </Link>
368 </Text>
369 ))}
370 </Box>
371 <Text dimColor>Esc schließt</Text>
372 </Box>
373 )
374 })
375
376 on('ui.render', { component: 'AbovePrompt' }, async ($, e, next) => {
377 const liste = await read($, auftraege)
378 if (liste.length === 0) return next(e)
379 const nun = await read($, jetzt)
380 const standJetzt = await read($, stand)
381 const { Box, Text } = $.ui.resolve(e)
382 return (
383 <Box flexDirection="column">
384 {liste.slice(0, 5).map(a => {
385 const ort = a.ordner.split('/').filter(Boolean).pop() ?? a.ordner
386 const laeuft = a.ende === null
387 const ok = a.rc === 0
388 const zeichen = laeuft ? '⚙' : ok ? '✓' : '✗'
389 const farbe = laeuft ? 'yellow' : ok ? 'green' : 'red'
390 const zustand = laeuft ? `läuft ${dauer(nun - a.start)}${restText(standJetzt?.auftraege ?? liste, a, nun)}` : ok ? `fertig nach ${dauer((a.ende ?? nun) - a.start)}` : `Fehler ${a.rc} nach ${dauer((a.ende ?? nun) - a.start)}`
391 return (
392 <Text key={a.id}>
393 <Text color={farbe}>{zeichen} {a.wer}</Text>
394 <Text dimColor> {ort} · {a.auftrag} · </Text>
395 <Text color={farbe}>{zustand}</Text>
396 </Text>
397 )
398 })}
399 </Box>
400 )
401 })
402}
403types/index.d.ts 53 lines1export type Auftrag = {
2 id: string
3 wer: string
4 ordner: string
5 auftrag: string
6 start: number
7 ende: number | null
8 rc: number | null
9}
10
11export type StandAuftrag = Auftrag & { tokens: number | null; runden: number | null; letzte_zeile: string }
12export type Anbieter = { anbieter: string; schluessel: number; aktiv: number; ok: number; anfragen: number; tokens: number; sperren: number; ok_stunde: number; anfragen_stunde: number }
13export type Fenster = { kind: string; percentUsed: number; resetsAt?: string }
14export type WeicheWahl = { ziel: string; grund?: string } | null
15export type FccStand = {
16 laeuft: boolean
17 modell: string
18 bereit: string[]
19 fehler: string[]
20 eingerichtet: number
21 modelle: number
22 port?: number | string
23}
24export type Stand = {
25 weiche?: { schreiben: WeicheWahl; bauen: WeicheWahl } | null
26 jetzt: number
27 kontingent: { agy: { gruppe: string; fenster: string; rest: number; reset: string | null }[] | null; codex: { fenster_prozent: number; fenster_reset: number; woche_prozent: number; woche_reset: number } | null }
28 freellmapi: { anbieter: Anbieter[]; im_katalog: number | null } | null
29 fcc?: FccStand | null
30 auftraege: StandAuftrag[] | null
31}
32
33export type ArtikelEintrag = {
34 slug: string
35 title: string
36 published_at: number | string
37 type?: string
38 summary?: string
39 url?: string
40}
41
42declare module 'claude-code' {
43 interface PluginState {
44 'helfer-band': {
45 auftraege: Auftrag[]
46 jetzt: number
47 stand: Stand | null
48 claude: Fenster[]
49 artikel: ArtikelEintrag[]
50 }
51 }
52}
53