kimal3 0.6.4 → 0.7.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +10 -4
- package/claude-plugins/.claude-plugin/marketplace.json +2 -1
- package/claude-plugins/README.md +2 -0
- package/claude-plugins/kimal3-logo/.claude-plugin/plugin.json +1 -1
- package/claude-plugins/kimal3-logo/hooks/ablage.ts +8 -0
- package/claude-plugins/kimal3-logo/hooks/app-oeffnen.ts +37 -9
- package/claude-plugins/kimal3-logo/hooks/register.tsx +42 -18
- package/claude-plugins/kimal3-logo/hooks/update-rechnung.ts +175 -0
- package/claude-plugins/kimal3-logo/hooks/update.ts +244 -0
- package/claude-plugins/kimal3-logo/types/index.d.ts +4 -0
- package/claude-plugins/kimal3-plan/.claude-plugin/plugin.json +7 -0
- package/claude-plugins/kimal3-plan/hooks/erinnerung.ts +79 -0
- package/claude-plugins/kimal3-plan/hooks/fehlerzeile.ts +41 -0
- package/claude-plugins/kimal3-plan/hooks/hooks.json +1 -0
- package/claude-plugins/kimal3-plan/hooks/lauf.ts +143 -0
- package/claude-plugins/kimal3-plan/hooks/lebenszeichen.ts +197 -0
- package/claude-plugins/kimal3-plan/hooks/ordner.ts +141 -0
- package/claude-plugins/kimal3-plan/hooks/register.tsx +22 -0
- package/claude-plugins/kimal3-plan/hooks/spec-text.ts +45 -0
- package/claude-plugins/kimal3-plan/types/index.d.ts +10 -0
- package/coach/de/kimal3-coach.md +33 -0
- package/coach/en/kimal3-coach.md +33 -0
- package/dist/argumente.js +12 -0
- package/dist/atomar.js +22 -0
- package/dist/befehle/einrichten.js +4 -4
- package/dist/befehle/login.js +2 -2
- package/dist/befehle/plan-hoch.js +8 -0
- package/dist/befehle/plan-melde.js +105 -0
- package/dist/befehle/plan-senden.js +149 -0
- package/dist/befehle/plan.js +45 -10
- package/dist/befehle/projekt-verbinden.js +141 -0
- package/dist/befehle/skill-install.js +2 -2
- package/dist/befehle-katalog-en.js +6 -6
- package/dist/befehle-katalog.js +8 -7
- package/dist/befehlswoerter.js +9 -0
- package/dist/einstellungen-schreiben.js +1 -1
- package/dist/kimal3.js +22 -0
- package/dist/meldungen-speicher.js +189 -0
- package/dist/{logo-ablegen.js → mods-ablegen.js} +87 -20
- package/dist/plugins-mischen.js +7 -2
- package/dist/projekt-ordner.js +189 -0
- package/dist/texte/einrichten.js +3 -1
- package/dist/texte/meldungen.js +127 -0
- package/dist/texte/plan.js +42 -22
- package/dist/texte/wache.js +4 -4
- package/dist/wache-rechnung.js +3 -1
- package/package.json +1 -1
- package/skill/commands/k3feierabend.md +20 -0
- package/skill/commands/k3plan.md +151 -211
- package/skill/en/commands/k3plan.md +157 -219
- package/skill/en/commands/k3wrapup.md +19 -0
|
@@ -0,0 +1,244 @@
|
|
|
1
|
+
import { atom, read, update, type Engine } from 'claude-code'
|
|
2
|
+
import type { Ablage } from './ablage'
|
|
3
|
+
import {
|
|
4
|
+
befehle, deutscherTag, fertigGilt, istFassung, istNeuer, sollFragen, sperreGilt, TEXTE, versionStimmt,
|
|
5
|
+
type Fertig, type Merkzettel, type Sperre, type Zustand,
|
|
6
|
+
} from './update-rechnung'
|
|
7
|
+
|
|
8
|
+
// Der Ablauf hinter „Update installieren“ in der Leiste (Bauplan docs/superpowers/plans/2026-10-23-update-in-der-leiste.md,
|
|
9
|
+
// Abschnitte 1.2 und 1.3, Szenarien U-1 bis U-20). Die Entscheidungen selbst stehen rein in update-rechnung.ts; hier
|
|
10
|
+
// steht, wann was gefragt, gemerkt und ausgeführt wird. Gezeichnet wird in register.tsx.
|
|
11
|
+
|
|
12
|
+
const MINUTE = 60_000
|
|
13
|
+
const NPM = 'https://registry.npmjs.org/kimal3/latest'
|
|
14
|
+
// `HttpInit` kennt kein Zeitlimit: Was npm nach 5 s nicht beantwortet hat, zählt als Fehlschlag (Bauplan 1.2, A-6).
|
|
15
|
+
const NPM_GEDULD = 5000
|
|
16
|
+
// Was der Mod über Sitzungen und Fenster hinweg weiß (`$.store`, eine Datei je Mod): wann npm gefragt wurde, ob ein
|
|
17
|
+
// Update läuft, und dass eines geklappt hat, aber Claude noch nicht neu gestartet ist.
|
|
18
|
+
const MERKZETTEL = 'updateMerkzettel'
|
|
19
|
+
const SPERRE = 'updateSperre'
|
|
20
|
+
const FERTIG = 'updateFertig'
|
|
21
|
+
// Die zwei Befehle, die der Klick ausführt, so wie Claude sie in seinen Erlaubnis-Regeln sähe (Bauplan 1.3 Punkt 2).
|
|
22
|
+
// Beide Werkzeuge: Eine Firma, die `npm install` nur für PowerShell verbietet, verbietet es trotzdem (A-3).
|
|
23
|
+
const WERKZEUGE = ['Bash', 'PowerShell']
|
|
24
|
+
|
|
25
|
+
// Was die Leiste neu zeichnen soll, sobald es sich ändert (`$.state`, Vertrag in types/index.d.ts).
|
|
26
|
+
const neueste = atom({ plugin: 'kimal3-logo', key: 'updateNeueste' } as const, '')
|
|
27
|
+
const lauf = atom({ plugin: 'kimal3-logo', key: 'updateLauf' } as const, null)
|
|
28
|
+
|
|
29
|
+
/**
|
|
30
|
+
* Die Fehlermeldung für die Bitte an Claude: die letzten 10 Zeilen, höchstens 1500 Zeichen (Bauplan 1.3 Punkt 6).
|
|
31
|
+
* npm schreibt vorne Fortschritt und hinten den Grund; der Grund ist, was Claude braucht.
|
|
32
|
+
*/
|
|
33
|
+
export function meldungKurz(text: string): string {
|
|
34
|
+
return text.trim().split(/\r?\n/).slice(-10).join('\n').slice(-1500)
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
// Was im Store steht, ist Daten von gestern — vielleicht von einer älteren Fassung des Mods. Nur Felder mit dem
|
|
38
|
+
// richtigen Typ zählen; ein `fehlerAm` als Text ergäbe sonst `NaN`, und npm würde nie wieder gefragt.
|
|
39
|
+
function merkzettelAus(roh: unknown): Merkzettel {
|
|
40
|
+
const m = (roh ?? {}) as Record<string, unknown>
|
|
41
|
+
return {
|
|
42
|
+
...(typeof m.tag === 'string' ? { tag: m.tag } : {}),
|
|
43
|
+
...(istFassung(m.neueste) ? { neueste: m.neueste } : {}),
|
|
44
|
+
...(typeof m.fehlerAm === 'number' ? { fehlerAm: m.fehlerAm } : {}),
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
function versionAus(text: string): string | null {
|
|
49
|
+
try {
|
|
50
|
+
const version: unknown = (JSON.parse(text) as { version?: unknown } | null)?.version
|
|
51
|
+
return istFassung(version) ? version : null
|
|
52
|
+
} catch {
|
|
53
|
+
return null
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
function sperreAus(roh: unknown): Sperre | undefined {
|
|
58
|
+
const s = (roh ?? {}) as Record<string, unknown>
|
|
59
|
+
return istFassung(s.ziel) && typeof s.seit === 'number' ? { ziel: s.ziel, seit: s.seit } : undefined
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
// Ein Fehlschlag im Hintergrund gehört ins Debug-Protokoll, nie in den Chat (ohne `to: 'debug'` stünde er dort).
|
|
63
|
+
function protokoll($: Engine, text: string): void {
|
|
64
|
+
$.ui.log(`kimal3-logo: ${text.slice(0, 300)}`, { to: 'debug' })
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
type Sprache = 'de' | 'en'
|
|
68
|
+
// Gegen einen zweiten Klick, solange die Rückfrage noch offen ist oder der Lauf läuft.
|
|
69
|
+
let beschaeftigt = false
|
|
70
|
+
// Der Blick auf npm startet mit dem ersten Zeichnen der Leiste, einmal je Laden des Mods.
|
|
71
|
+
let angestossen = false
|
|
72
|
+
|
|
73
|
+
/** Nach dem Laden: Aufräumen, Wache-Schalter, Merkzettel — und npm fragen, wenn es heute noch nicht geschah. */
|
|
74
|
+
async function beimLaden($: Engine, ablage: Ablage): Promise<void> {
|
|
75
|
+
const fassung = ablage.fassung
|
|
76
|
+
const jetzt = await $.clock.now()
|
|
77
|
+
// U-6b: Ein Mod, der die neue Fassung schon trägt, hat das Update hinter sich; nach 24 Stunden gilt der Haken nicht mehr.
|
|
78
|
+
const fertig = await $.store.get(FERTIG)
|
|
79
|
+
if (fertig !== undefined && !fertigGilt(fertig, jetzt, fassung)) await $.store.delete(FERTIG)
|
|
80
|
+
const sperre = sperreAus(await $.store.get(SPERRE))
|
|
81
|
+
if (sperre !== undefined && !istNeuer(sperre.ziel, fassung)) await $.store.delete(SPERRE)
|
|
82
|
+
|
|
83
|
+
if (await wacheAus($, ablage)) return
|
|
84
|
+
const merkzettel = merkzettelAus(await $.store.get(MERKZETTEL))
|
|
85
|
+
// Ein zweites Fenster am selben Tag zeigt den Knopf aus dem Merkzettel, ohne zu fragen (U-11).
|
|
86
|
+
if (merkzettel.tag === deutscherTag(jetzt) && merkzettel.neueste !== undefined) await update($, neueste, () => merkzettel.neueste ?? '')
|
|
87
|
+
if (sollFragen(merkzettel, jetzt)) await npmFragen($, jetzt)
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
/** `kimal3 wache aus` schaltet auch den Blick auf npm ab (E-8, U-8). Keine oder kaputte Datei: an. */
|
|
91
|
+
async function wacheAus($: Engine, ablage: Ablage): Promise<boolean> {
|
|
92
|
+
try {
|
|
93
|
+
const inhalt: unknown = JSON.parse(await $.fs.read(`${ablage.kimal3Ordner}/wache.json`))
|
|
94
|
+
return (inhalt as { aus?: unknown } | null)?.aus === true
|
|
95
|
+
} catch {
|
|
96
|
+
return false
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
async function npmFragen($: Engine, jetzt: number): Promise<void> {
|
|
101
|
+
// Ein Zeitgeber statt `$.clock.sleep`: Ein `sleep` liefe auf das Zeitbudget des Hakens, aus dem `$` stammt — hier das
|
|
102
|
+
// Zeichnen der Leiste.
|
|
103
|
+
let zuEnde: (wert: null) => void = () => {}
|
|
104
|
+
const zuLangsam = new Promise<null>(aufloesen => { zuEnde = aufloesen })
|
|
105
|
+
const zeitgeber = $.clock.after(NPM_GEDULD, () => zuEnde(null))
|
|
106
|
+
const antwort = await Promise.race([$.http.fetch(NPM).catch(() => null), zuLangsam])
|
|
107
|
+
zeitgeber.cancel()
|
|
108
|
+
const version = versionAus(antwort?.ok ? antwort.text : '')
|
|
109
|
+
// Nur `1.2.3` zählt als Antwort; Vorabfassung, Unsinn, Fehler und Schweigen sind ein Fehlschlag (U-2, U-12).
|
|
110
|
+
if (version === null) {
|
|
111
|
+
protokoll($, `keine gültige Antwort von npm: ${antwort === null ? 'keine Antwort' : `${antwort.status} ${antwort.text}`}`)
|
|
112
|
+
await $.store.set(MERKZETTEL, { fehlerAm: jetzt } satisfies Merkzettel)
|
|
113
|
+
return
|
|
114
|
+
}
|
|
115
|
+
await $.store.set(MERKZETTEL, { tag: deutscherTag(jetzt), neueste: version } satisfies Merkzettel)
|
|
116
|
+
await update($, neueste, () => version)
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
/** Was die Leiste hinter der Fassung zeigt; `null` ohne Ablage (U-9). Schreibt nichts: Es wird gerade gezeichnet. */
|
|
120
|
+
export async function updateZustand($: Engine, ablage: Ablage, claudeArbeitet: boolean): Promise<Zustand | null> {
|
|
121
|
+
const fassung = ablage.fassung
|
|
122
|
+
if (fassung === '') return null
|
|
123
|
+
// Erst danach, nie in diesem Zeichnen (es soll nicht auf npm warten, und schreiben darf es nicht). Am ersten Zeichnen
|
|
124
|
+
// statt an `session.start`: Den Start hört schon hooks/app-oeffnen.ts (je Ereignis ein Haken ohne Auswahl), und
|
|
125
|
+
// ohne Leiste — `claude -p`, Skripte — braucht niemand die Antwort von npm.
|
|
126
|
+
if (!angestossen) {
|
|
127
|
+
angestossen = true
|
|
128
|
+
$.clock.after(0, () => void beimLaden($, ablage).catch(f => protokoll($, `Update-Blick: ${String(f)}`)))
|
|
129
|
+
}
|
|
130
|
+
const jetzt = await $.clock.now()
|
|
131
|
+
const sperre = sperreAus(await $.store.get(SPERRE))
|
|
132
|
+
const laeuft = sperreGilt(sperre, jetzt, fassung)
|
|
133
|
+
const hier = await read($, lauf)
|
|
134
|
+
// „läuft“ aus diesem Fenster nur, solange die Sperre gilt: Sonst ist es liegen geblieben (der Lauf kam nie zu Ende,
|
|
135
|
+
// etwa weil der Mod mittendrin neu geladen wurde), und die Leiste zeigte für immer „wird installiert“.
|
|
136
|
+
if (hier !== null && (hier.art !== 'laeuft' || laeuft)) {
|
|
137
|
+
return hier.art === 'laeuft' || hier.art === 'geklappt' ? { fassung, art: hier.art, ziel: hier.ziel } : { fassung, art: hier.art }
|
|
138
|
+
}
|
|
139
|
+
// Aus einem anderen Fenster (oder vor dem Schließen): geklappt, aber noch nicht neu gestartet (U-6b) — oder es läuft (U-5).
|
|
140
|
+
const fertig = await $.store.get(FERTIG)
|
|
141
|
+
if (fertigGilt(fertig, jetzt, fassung)) return { fassung, art: 'geklappt', ziel: fertig.ziel }
|
|
142
|
+
if (sperre !== undefined && laeuft) return { fassung, art: 'laeuft', ziel: sperre.ziel }
|
|
143
|
+
// Kein Knopf, solange Claude antwortet (U-18): ein Klick mitten in einer Antwort stellte eine Frage quer dazu.
|
|
144
|
+
return istNeuer(await read($, neueste), fassung) && !claudeArbeitet ? { fassung, art: 'knopf' } : { fassung, art: 'still' }
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
/** Der Klick auf „Update installieren“ (Bauplan 1.3). */
|
|
148
|
+
export async function updateInstallieren($: Engine, ablage: Ablage, sprache: Sprache): Promise<void> {
|
|
149
|
+
const t = TEXTE[sprache]
|
|
150
|
+
const fassung = ablage.fassung
|
|
151
|
+
const ziel = await read($, neueste)
|
|
152
|
+
if (beschaeftigt || !istNeuer(ziel, fassung)) return
|
|
153
|
+
beschaeftigt = true
|
|
154
|
+
try {
|
|
155
|
+
// K-4, U-10: Ohne npm-Pfad kein Direktbefehl — Claude bekommt die Bitte und fragt selbst um Erlaubnis.
|
|
156
|
+
const liste = befehle(ablage, ziel)
|
|
157
|
+
if (liste === null) {
|
|
158
|
+
await $.prompt.submit({ text: t.bitteOhneNpm(ziel) })
|
|
159
|
+
return
|
|
160
|
+
}
|
|
161
|
+
// U-3: Abbrechen, „Other“, eine Chat-Nachricht statt einer Antwort und das Wegklicken (Wurf): nichts passiert.
|
|
162
|
+
const antwort = await $.ui.ask(t.frage(fassung, ziel), [t.ja, t.nein]).catch(() => null)
|
|
163
|
+
if (antwort !== t.ja) return
|
|
164
|
+
// U-5: Während der Rückfrage kann ein anderes Fenster angefangen haben — dann zeigt auch dieses „wird installiert“.
|
|
165
|
+
const sperre = sperreAus(await $.store.get(SPERRE))
|
|
166
|
+
if (sperre !== undefined && sperreGilt(sperre, await $.clock.now(), fassung)) {
|
|
167
|
+
await update($, lauf, () => ({ art: 'laeuft', ziel: sperre.ziel, meldung: '' }))
|
|
168
|
+
return
|
|
169
|
+
}
|
|
170
|
+
if (await verboten($, ziel)) {
|
|
171
|
+
await update($, lauf, () => ({ art: 'gesperrt', ziel, meldung: '' }))
|
|
172
|
+
return
|
|
173
|
+
}
|
|
174
|
+
await ausfuehren($, ablage, liste, ziel)
|
|
175
|
+
} catch (fehler) {
|
|
176
|
+
// Etwa `$.store` scheitert (die Sperre lässt sich nicht schreiben): kein Erfolg und kein „gesperrt“, sondern ein Fehler mit Grund.
|
|
177
|
+
await update($, lauf, () => ({ art: 'fehler', ziel, meldung: meldungKurz(String(fehler)) }))
|
|
178
|
+
} finally {
|
|
179
|
+
beschaeftigt = false
|
|
180
|
+
}
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
/**
|
|
184
|
+
* U-4: Verbietet eine Erlaubnis-Regel einen der zwei Befehle in einem der zwei Werkzeuge? `ask` (der Normalfall im
|
|
185
|
+
* Auto-Modus, M-16) ist kein Verbot. ⚠️ Die Abfrage kennt nur die Regeln, nicht die PreToolUse-Hooks einer Firma und
|
|
186
|
+
* nicht den Prüfer des Auto-Modus (Typen von `ToolCheckResult.hook`; Bauplan Abschnitt 4).
|
|
187
|
+
* Gefragt werden nur Werkzeuge, die es hier gibt (ohne PowerShell etwa unter macOS); wirft die Liste, nur `Bash`. Eine
|
|
188
|
+
* Abfrage, die wirft, ist kein Verbot — nur ein ausgesprochenes `deny` sperrt; sonst hielte ein fehlendes Werkzeug
|
|
189
|
+
* jedes Update auf.
|
|
190
|
+
*/
|
|
191
|
+
async function verboten($: Engine, ziel: string): Promise<boolean> {
|
|
192
|
+
const da = await $.tool.list().then(liste => liste.map(w => w.name), () => ['Bash'])
|
|
193
|
+
for (const tool of WERKZEUGE.filter(w => da.includes(w))) {
|
|
194
|
+
for (const command of [`npm install -g kimal3@${ziel}`, 'kimal3 skill install']) {
|
|
195
|
+
const urteil = await $.tool.check({ tool, input: { command } }).catch((fehler: unknown) => {
|
|
196
|
+
protokoll($, `Regel-Abfrage ${tool}: ${String(fehler)}`)
|
|
197
|
+
return null
|
|
198
|
+
})
|
|
199
|
+
if (urteil?.decision === 'deny') return true
|
|
200
|
+
}
|
|
201
|
+
}
|
|
202
|
+
return false
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
/**
|
|
206
|
+
* Die drei Befehle nacheinander, jeder ohne Kommandozeile, im Ordner `~/.kimal3` (nie im Projekt: ein `.npmrc` dort
|
|
207
|
+
* gälte sonst mit). Scheitert eine Stufe, läuft die nächste nicht (U-6, U-7, U-7b). Die Sperre hält andere Fenster ab
|
|
208
|
+
* (U-5) und geht in `finally` weg — auch wenn ein Befehl wirft.
|
|
209
|
+
*/
|
|
210
|
+
async function ausfuehren($: Engine, ablage: Ablage, liste: string[][], ziel: string): Promise<void> {
|
|
211
|
+
await $.store.set(SPERRE, { ziel, seit: await $.clock.now() } satisfies Sperre)
|
|
212
|
+
await update($, lauf, () => ({ art: 'laeuft', ziel, meldung: '' }))
|
|
213
|
+
try {
|
|
214
|
+
const meldung = await stufen($, ablage.kimal3Ordner, liste, ziel).catch((fehler: unknown) => String(fehler))
|
|
215
|
+
if (meldung === null) {
|
|
216
|
+
// Bleibt stehen, bis ein Mod mit dieser Fassung lädt oder 24 Stunden um sind (beimLaden, `fertigGilt`): Erst der
|
|
217
|
+
// Neustart bringt Coach und Befehle.
|
|
218
|
+
await $.store.set(FERTIG, { ziel, am: await $.clock.now() } satisfies Fertig)
|
|
219
|
+
await update($, lauf, () => ({ art: 'geklappt', ziel, meldung: '' }))
|
|
220
|
+
} else {
|
|
221
|
+
await update($, lauf, () => ({ art: 'fehler', ziel, meldung: meldungKurz(meldung) }))
|
|
222
|
+
}
|
|
223
|
+
} finally {
|
|
224
|
+
await $.store.delete(SPERRE)
|
|
225
|
+
}
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
/** `null`: alle drei Stufen geklappt; sonst die Meldung der gescheiterten. */
|
|
229
|
+
async function stufen($: Engine, ordner: string, [npm, version, skill]: string[][], ziel: string): Promise<string | null> {
|
|
230
|
+
const init = (minuten: number) => ({ cwd: ordner, timeoutMs: minuten * MINUTE })
|
|
231
|
+
const a = await $.process.run(npm, init(10))
|
|
232
|
+
if (a.exitCode !== 0) return `${a.stdout}\n${a.stderr}`
|
|
233
|
+
// Hat das npm dieses Node die Fassung wirklich dorthin gelegt, wo `kimal3` liegt? (Homebrew, Volta, A-8)
|
|
234
|
+
const b = await $.process.run(version, init(2))
|
|
235
|
+
if (b.exitCode !== 0 || !versionStimmt(b.stdout, ziel)) return `kimal3 --version:\n${b.stdout}\n${b.stderr}`
|
|
236
|
+
const c = await $.process.run(skill, init(2))
|
|
237
|
+
return c.exitCode !== 0 ? `${c.stdout}\n${c.stderr}` : null
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
/** „Claude fragen“ nach einem Fehlschlag: die Zielfassung und das Ende der Meldung (Bauplan 1.3 Punkt 6). */
|
|
241
|
+
export async function updateClaudeFragen($: Engine, sprache: Sprache): Promise<void> {
|
|
242
|
+
const stand = await read($, lauf)
|
|
243
|
+
if (stand?.art === 'fehler') await $.prompt.submit({ text: TEXTE[sprache].bitteFehler(stand.ziel, stand.meldung) })
|
|
244
|
+
}
|
|
@@ -7,6 +7,10 @@ declare module 'claude-code' {
|
|
|
7
7
|
chatBenannt: boolean
|
|
8
8
|
// Wie oft das Modell in dieser Sitzung schon gefragt wurde, ob das Thema klar ist (höchstens 8, B-8).
|
|
9
9
|
benennenFragen: number
|
|
10
|
+
// Die neueste Fassung, die npm heute genannt hat ('' = keine bekannt); daraus zeichnet die Leiste den Knopf (hooks/update.ts).
|
|
11
|
+
updateNeueste: string
|
|
12
|
+
// Was der Klick in diesem Fenster angestoßen hat; `meldung` = Ende der Fehlermeldung für „Claude fragen“ (hooks/update.ts).
|
|
13
|
+
updateLauf: { art: 'laeuft' | 'geklappt' | 'fehler' | 'gesperrt'; ziel: string; meldung: string } | null
|
|
10
14
|
}
|
|
11
15
|
}
|
|
12
16
|
}
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "kimal3-plan",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"description": "Hält das Board der Projektseite in der App aktuell: Punkt solange die Sitzung arbeitet, Spec-Texte, Erinnerung an Claude · Keeps the board on the app's project page current: dot while the session works, spec texts, reminder to Claude",
|
|
5
|
+
"author": { "name": "KImal3", "url": "https://kimal3.de" },
|
|
6
|
+
"types": "./types/index.d.ts"
|
|
7
|
+
}
|
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
import { atom, read, update, type EngineInterface, type On } from 'claude-code'
|
|
2
|
+
import { TEXTE, type Lauf } from './lauf'
|
|
3
|
+
import { relativImOrdner } from './ordner'
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* Die drei Saetze, die NUR Claude bekommt (Spec 1.3 Aufgabe 3, 1.5, 1.7, S-25, S-26, US-6, US-8; Bauplan R-4).
|
|
7
|
+
* Der Mensch sieht keinen davon (gemessen M-3, Claude-App). Zwei Wege, je nachdem, ob der Satz eine Runde starten soll:
|
|
8
|
+
*
|
|
9
|
+
* - **Erinnerung ans Melden** haengt am Aendern einer Datei (`tool.call` mit `context`): keine eigene Runde, Claude liest
|
|
10
|
+
* ihn direkt nach dem Werkzeug. Nur die Hauptsitzung bekommt ihn: Ein Subagent (`e.agentId`) baut im Auftrag und
|
|
11
|
+
* meldet nicht; er wuerde die 30-Minuten-Sperre verbrauchen, ohne dass die Hauptsitzung je erinnert wird (Ruling). Hoechstens einmal je 30 Minuten je Sitzung, nur in einem Projektordner mit
|
|
12
|
+
* Ordnerzettel (S-30), und nur, wenn die App zuletzt „nichts in Arbeit“ gesagt hat -- weiss der Mod es nicht (noch
|
|
13
|
+
* keine Antwort, App weg), schweigt er.
|
|
14
|
+
* - **Anstoss „Projekt anlegen“** (US-8) und **„gemerkte Meldungen senden“** (R-4) sind eine Zeile im Gespraech
|
|
15
|
+
* (`$.session.append`): Claude liest sie sofort und handelt.
|
|
16
|
+
*/
|
|
17
|
+
const SPERRE_MS = 30 * 60_000
|
|
18
|
+
|
|
19
|
+
// In `$.state`, weil ein Neuladen des Mods die Variablen des Moduls zuruecksetzt -- die Sperre und der einmalige Anstoss
|
|
20
|
+
// sollen das ueberstehen.
|
|
21
|
+
const letzteErinnerung = atom({ plugin: 'kimal3-plan', key: 'letzteErinnerung' } as const, null)
|
|
22
|
+
const angestossen = atom({ plugin: 'kimal3-plan', key: 'anstossGegeben' } as const, false)
|
|
23
|
+
|
|
24
|
+
/** Die Datei eines Schreib-Werkzeugs (`file_path`, bei Notebooks `notebook_path`). */
|
|
25
|
+
export function dateiAus(e: unknown): string | null {
|
|
26
|
+
const o = e as { file_path?: unknown; notebook_path?: unknown }
|
|
27
|
+
const datei = o.file_path ?? o.notebook_path
|
|
28
|
+
return typeof datei === 'string' && datei !== '' ? datei : null
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
export function erinnerung(on: On, lauf: Lauf): void {
|
|
32
|
+
on('tool.call', { tool: ['Write', 'Edit', 'NotebookEdit'] }, async ($, e, next) => {
|
|
33
|
+
const ergebnis = await next(e)
|
|
34
|
+
// Abgelehnt oder gescheitert: Es hat sich nichts geaendert. Ein Subagent: nur die Hauptsitzung wird erinnert.
|
|
35
|
+
if (ergebnis.deny !== undefined || ergebnis.isError === true || e.agentId !== undefined) return ergebnis
|
|
36
|
+
const satz = await satzFuer($, lauf, dateiAus(e))
|
|
37
|
+
return satz === null ? ergebnis : { ...ergebnis, context: [...(ergebnis.context ?? []), satz] }
|
|
38
|
+
})
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
async function satzFuer($: EngineInterface, lauf: Lauf, datei: string | null): Promise<string | null> {
|
|
42
|
+
const lage = lauf.lage
|
|
43
|
+
if (lauf.inArbeit !== false || lage.art !== 'projekt' || lage.zettel === null || datei === null) return null
|
|
44
|
+
if (relativImOrdner(datei, lage.ordner, await $.session.cwd()) === null) return null
|
|
45
|
+
const jetzt = await $.clock.now()
|
|
46
|
+
const zuletzt = await read($, letzteErinnerung)
|
|
47
|
+
if (zuletzt !== null && jetzt - zuletzt < SPERRE_MS) return null
|
|
48
|
+
await update($, letzteErinnerung, () => jetzt)
|
|
49
|
+
return TEXTE[lauf.sprache].erinnerung(lage.zettel.titel)
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
/** Projektordner ohne Ordnerzettel: einmal je Sitzung „leg das Projekt an“ (US-8). */
|
|
53
|
+
export async function anstossOhneZettel($: EngineInterface, lauf: Lauf): Promise<void> {
|
|
54
|
+
if (lauf.lage.art !== 'projekt' || lauf.lage.zettel !== null || (await read($, angestossen))) return
|
|
55
|
+
if (await anClaude($, TEXTE[lauf.sprache].anstoss)) await update($, angestossen, () => true)
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
/** Liegen gemerkte Meldungen (`~/.kimal3/meldungen/*.json`; `.tmp` sind Reste) und ist die App erreichbar: senden lassen. */
|
|
59
|
+
export async function gemerkteMeldungenAnstossen($: EngineInterface, lauf: Lauf): Promise<void> {
|
|
60
|
+
if (lauf.heim === null) return
|
|
61
|
+
const eintraege = await $.fs.list(`${lauf.heim}/.kimal3/meldungen`).catch(() => [])
|
|
62
|
+
if (eintraege.some((d: { kind: string; name: string }) => d.kind === 'file' && d.name.endsWith('.json'))) await anClaude($, TEXTE[lauf.sprache].gemerkt)
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
/** Nach `/clear` beginnt eine neue Sitzung: ihre eigene Sperre, ihr eigener Anstoss. */
|
|
66
|
+
export async function sitzungNeu($: EngineInterface): Promise<void> {
|
|
67
|
+
await update($, letzteErinnerung, () => null)
|
|
68
|
+
await update($, angestossen, () => false)
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
async function anClaude($: EngineInterface, text: string): Promise<boolean> {
|
|
72
|
+
try {
|
|
73
|
+
await $.session.append({ message: { type: 'user', content: [{ type: 'text', text }] } })
|
|
74
|
+
return true
|
|
75
|
+
} catch (fehler) {
|
|
76
|
+
$.ui.log(`kimal3-plan: Satz an Claude nicht angehaengt: ${String(fehler).slice(0, 200)}`, { to: 'debug' })
|
|
77
|
+
return false
|
|
78
|
+
}
|
|
79
|
+
}
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
import type { EngineInterface } from 'claude-code'
|
|
2
|
+
import { TEXTE, type Fehler, type Lauf } from './lauf'
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* Die eine Fehlerzeile des Mods (Spec 1.3, 1.5, US-4, S-11, S-22; Bauplan R-8): in der Statuszeile unter dem
|
|
6
|
+
* Eingabefeld, weil eine farbige Zeile dort in der Claude-App gar nicht erscheint (gemessen M-4). Sie steht, solange der
|
|
7
|
+
* Fehler besteht, und verschwindet danach -- `$.ui.status` wird nur gerufen, wenn sich der Satz AENDERT, so bleibt es
|
|
8
|
+
* bei zehn gescheiterten Versuchen bei EINER Zeile. Bei mehreren Fehlern zeigt sie den ersten nach diesem Vorrang:
|
|
9
|
+
* Wer nicht angemeldet ist, dem hilft „nicht erreichbar“ nichts.
|
|
10
|
+
*/
|
|
11
|
+
const VORRANG: readonly Fehler[] = ['nicht-angemeldet', 'kimal3-fehlt', 'nicht-erreichbar']
|
|
12
|
+
|
|
13
|
+
function zeige($: EngineInterface, lauf: Lauf): void {
|
|
14
|
+
const art = VORRANG.find(f => lauf.fehler.has(f))
|
|
15
|
+
const satz = art === undefined ? undefined : TEXTE[lauf.sprache].fehler[art]
|
|
16
|
+
if (satz === lauf.zeile) return
|
|
17
|
+
lauf.zeile = satz
|
|
18
|
+
$.ui.status(satz)
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
/** Die Fehler nach einem Lebenszeichen: Es prueft Ausweis UND App, sein Ergebnis ersetzt alles Bisherige. */
|
|
22
|
+
export function fehlerSind($: EngineInterface, lauf: Lauf, fehler: readonly Fehler[]): void {
|
|
23
|
+
lauf.fehler = new Set(fehler)
|
|
24
|
+
merkeAusfall(lauf)
|
|
25
|
+
zeige($, lauf)
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
/** Einzelne Fehler kommen hinzu (`true`) oder sind behoben (`false`), etwa nach dem Hochladen eines Spec-Texts. */
|
|
29
|
+
export function fehlerSetzen($: EngineInterface, lauf: Lauf, aenderung: Partial<Record<Fehler, boolean>>): void {
|
|
30
|
+
for (const [art, besteht] of Object.entries(aenderung) as [Fehler, boolean][]) {
|
|
31
|
+
if (besteht) lauf.fehler.add(art)
|
|
32
|
+
else lauf.fehler.delete(art)
|
|
33
|
+
}
|
|
34
|
+
merkeAusfall(lauf)
|
|
35
|
+
zeige($, lauf)
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
/** War die App weg, koennen sich in der Zeit Meldungen angesammelt haben: nach der Rueckkehr nachsehen (R-4). */
|
|
39
|
+
function merkeAusfall(lauf: Lauf): void {
|
|
40
|
+
if (lauf.fehler.has('nicht-erreichbar')) lauf.meldungenPruefen = true
|
|
41
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{ "modules": ["./register.tsx"] }
|
|
@@ -0,0 +1,143 @@
|
|
|
1
|
+
import type { EngineInterface, Timer } from 'claude-code'
|
|
2
|
+
import { liesDatei, normiert, type Ordnerlage } from './ordner'
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* Was die Teile des Mods gemeinsam wissen (ein Objekt je Laden des Mods, angelegt in `register.tsx`), dazu die Saetze in
|
|
6
|
+
* beiden Sprachen und der eine Weg zur App.
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
export type Sprache = 'de' | 'en'
|
|
10
|
+
export type Fehler = 'nicht-angemeldet' | 'kimal3-fehlt' | 'nicht-erreichbar'
|
|
11
|
+
|
|
12
|
+
export type Lauf = {
|
|
13
|
+
/** Die SPRACHE-Zeile aus `register.tsx` -- gilt, wenn `~/.kimal3/sprache.json` fehlt (S-23). */
|
|
14
|
+
vorgabe: Sprache
|
|
15
|
+
sprache: Sprache
|
|
16
|
+
/** Der Benutzerordner mit `/` (`USERPROFILE`, sonst `HOME`) -- dort liegt `.kimal3/` (M-5). */
|
|
17
|
+
heim: string | null
|
|
18
|
+
basis: string
|
|
19
|
+
lage: Ordnerlage
|
|
20
|
+
/** Die letzte Antwort der App auf ein Lebenszeichen: `null`, solange keine gekommen ist (R-5). */
|
|
21
|
+
inArbeit: boolean | null
|
|
22
|
+
/** Zaehlt jede Meldung, die Claude gerade abgeschickt hat: Ein Lebenszeichen, das davor losging, setzt `inArbeit` nicht. */
|
|
23
|
+
meldungen: number
|
|
24
|
+
/** Die Spec-Dateien des Projekts aus dem letzten Lebenszeichen -- nur deren Texte gehen hoch (Datensparsamkeit). */
|
|
25
|
+
specDateien: readonly string[]
|
|
26
|
+
takt: Timer | null
|
|
27
|
+
/** Ein Start laeuft gerade (zwischen erstem `await` und dem Zeitgeber): ein zweiter wartet nicht, er unterbleibt. */
|
|
28
|
+
startet: boolean
|
|
29
|
+
/** Zaehlt jedes Sitzungsende hoch: Eine Antwort, die fuer eine fruehere Sitzung unterwegs war, wirkt nicht mehr. */
|
|
30
|
+
generation: number
|
|
31
|
+
/** `session.end` ist gelaufen: Ein Lebenszeichen, das noch unterwegs war, darf danach nichts mehr tun. */
|
|
32
|
+
beendet: boolean
|
|
33
|
+
/** Fuer diese Projektnummer hat die App `403` gesagt (nicht Eigentuemer, geloescht): nicht weiter fragen. */
|
|
34
|
+
abgelehnt: string | null
|
|
35
|
+
/** Fuer dieses Projekt ging ein Lebenszeichen raus -- nur dann gibt es etwas abzumelden. */
|
|
36
|
+
gemeldetFuer: string | null
|
|
37
|
+
/** Beim naechsten erfolgreichen Lebenszeichen nach gemerkten Meldungen sehen (R-4): beim Start und nach jedem Ausfall. */
|
|
38
|
+
meldungenPruefen: boolean
|
|
39
|
+
fehler: Set<Fehler>
|
|
40
|
+
/** Was gerade in der Statuszeile steht (`undefined`: nichts; `null`: noch nie gesetzt -- die erste Auswertung nach dem
|
|
41
|
+
* Laden ruft `$.ui.status` in jedem Fall und raeumt so eine Zeile ab, die ein frueherer Lauf stehen liess). */
|
|
42
|
+
zeile: string | undefined | null
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
export function neuerLauf(vorgabe: Sprache): Lauf {
|
|
46
|
+
return {
|
|
47
|
+
vorgabe, sprache: vorgabe, heim: null, basis: APP, lage: { art: 'kein-projektordner' }, inArbeit: null, meldungen: 0, specDateien: [],
|
|
48
|
+
takt: null, startet: false, generation: 0, beendet: false, abgelehnt: null, gemeldetFuer: null, meldungenPruefen: true,
|
|
49
|
+
fehler: new Set(), zeile: null,
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
const APP = 'https://app.kimal3.de'
|
|
54
|
+
|
|
55
|
+
// Wortlaut: Spec 1.3 (Erinnerung), 1.7 (Anstoss), S-11, S-22, US-4; Bauplan R-4 (gemerkte Meldungen).
|
|
56
|
+
// Englische Befehlswoerter wie in `cli/src/befehlswoerter.ts` (`project connect`, `plan send`).
|
|
57
|
+
export const TEXTE = {
|
|
58
|
+
de: {
|
|
59
|
+
erinnerung: (titel: string) => `Du arbeitest im Projekt ${titel}, aber diese Sitzung hat keine Story ‚in Arbeit‘ gemeldet. Welche ist dran? Gehört die Änderung zu keiner Spec, notiere sie als Thema oder arbeite weiter.`,
|
|
60
|
+
anstoss: 'Für diesen Projektordner gibt es noch kein Projekt in der App — leg es mit `kimal3 projekt verbinden` an.',
|
|
61
|
+
gemerkt: 'Gemerkte Meldungen warten — führe `kimal3 plan senden` aus.',
|
|
62
|
+
fehler: {
|
|
63
|
+
'nicht-erreichbar': 'Die Projektseite ist gerade nicht erreichbar — ich versuche es gleich wieder.',
|
|
64
|
+
'nicht-angemeldet': 'Du bist bei kimal3 nicht mehr angemeldet — `kimal3 login`.',
|
|
65
|
+
'kimal3-fehlt': 'kimal3 fehlt oder ist zu alt — `npm install -g kimal3@latest`.',
|
|
66
|
+
},
|
|
67
|
+
},
|
|
68
|
+
en: {
|
|
69
|
+
erinnerung: (titel: string) => `You are working in the project ${titel}, but this session hasn't reported a story as ‘in progress’. Which one is it? If the change belongs to no spec, note it as a topic or keep working.`,
|
|
70
|
+
anstoss: 'There is no project in the app for this project folder yet — create it with `kimal3 project connect`.',
|
|
71
|
+
gemerkt: 'Saved reports are waiting — run `kimal3 plan send`.',
|
|
72
|
+
fehler: {
|
|
73
|
+
'nicht-erreichbar': "The project page can't be reached right now — I'll try again shortly.",
|
|
74
|
+
'nicht-angemeldet': "You're no longer signed in to kimal3 — `kimal3 login`.",
|
|
75
|
+
'kimal3-fehlt': 'kimal3 is missing or too old — `npm install -g kimal3@latest`.',
|
|
76
|
+
},
|
|
77
|
+
},
|
|
78
|
+
} as const
|
|
79
|
+
|
|
80
|
+
/**
|
|
81
|
+
* Benutzerordner, Sprache und App-Adresse -- bei jedem Sitzungsstart neu, damit eine umgestellte Kontosprache ohne neues
|
|
82
|
+
* `kimal3 einrichten` gilt (S-23). `KIMAL3_URL` wie in `cli/src/anmeldung.ts` (Messen gegen den lokalen Server).
|
|
83
|
+
*/
|
|
84
|
+
export async function umgebungLesen($: EngineInterface, lauf: Lauf): Promise<void> {
|
|
85
|
+
const heim = (await $.env.get('USERPROFILE')) ?? (await $.env.get('HOME'))
|
|
86
|
+
lauf.heim = heim === undefined || heim === '' ? null : normiert(heim)
|
|
87
|
+
lauf.basis = ((await $.env.get('KIMAL3_URL')) ?? APP).replace(/\/+$/, '')
|
|
88
|
+
lauf.sprache = lauf.vorgabe
|
|
89
|
+
const text = lauf.heim === null ? null : await liesDatei($, `${lauf.heim}/.kimal3/sprache.json`)
|
|
90
|
+
try {
|
|
91
|
+
const s: unknown = text === null ? null : JSON.parse(text.replace(/^\uFEFF/, ''))?.sprache
|
|
92
|
+
if (s === 'de' || s === 'en') lauf.sprache = s
|
|
93
|
+
} catch {
|
|
94
|
+
// kaputte Datei: die SPRACHE-Zeile gilt
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
/**
|
|
99
|
+
* Der Geraeteschluessel aus `~/.kimal3/anmeldung.json` (M-5), bei jedem Gebrauch frisch gelesen -- so wirkt ein
|
|
100
|
+
* `kimal3 login` sofort. 🔴 Der Schluessel wird nur benutzt, nie ausgegeben oder protokolliert.
|
|
101
|
+
* Ohne Schluessel: Gibt es `~/.kimal3/`, ist `kimal3` da und nur abgemeldet (S-11, `kimal3 login`); fehlt der Ordner ganz,
|
|
102
|
+
* fehlt `kimal3` (S-22, installieren) -- Ruling der Fixrunde.
|
|
103
|
+
*/
|
|
104
|
+
export async function liesAusweis($: EngineInterface, lauf: Lauf): Promise<{ schluessel: string } | { fehler: Fehler }> {
|
|
105
|
+
if (lauf.heim === null) return { fehler: 'kimal3-fehlt' }
|
|
106
|
+
const text = await liesDatei($, `${lauf.heim}/.kimal3/anmeldung.json`)
|
|
107
|
+
try {
|
|
108
|
+
const s: unknown = text === null ? null : JSON.parse(text.replace(/^\uFEFF/, ''))?.schluessel
|
|
109
|
+
if (typeof s === 'string' && s !== '') return { schluessel: s }
|
|
110
|
+
} catch {
|
|
111
|
+
// kaputte Datei: wie keine
|
|
112
|
+
}
|
|
113
|
+
const ordnerDa = await $.fs.exists(`${lauf.heim}/.kimal3`).catch(() => false)
|
|
114
|
+
return { fehler: ordnerDa ? 'nicht-angemeldet' : 'kimal3-fehlt' }
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
/** Wie lange ein Abruf an die App hoechstens dauern darf: `$.http.fetch` kennt keine eigene Zeitgrenze. */
|
|
118
|
+
const ZEITGRENZE_MS = 8_000
|
|
119
|
+
|
|
120
|
+
/**
|
|
121
|
+
* Ein Abruf an `/api/mod/*`. `null`: die App war nicht zu erreichen (kein Netz, abgelehnt, oder keine Antwort innerhalb
|
|
122
|
+
* von `grenzeMs`). 🔴 Ohne die Zeitgrenze haengt ein Abruf, der nie antwortet, das Lebenszeichen fuer immer auf --
|
|
123
|
+
* still, ohne Statuszeile (Befund der Durchsicht).
|
|
124
|
+
*/
|
|
125
|
+
export function anApp($: EngineInterface, lauf: Lauf, pfad: string, schluessel: string, rumpf: unknown, grenzeMs = ZEITGRENZE_MS): Promise<{ status: number; text: string } | null> {
|
|
126
|
+
return new Promise(fertig => {
|
|
127
|
+
const frist = $.clock.after(Math.max(0, Math.min(grenzeMs, ZEITGRENZE_MS)), () => fertig(null))
|
|
128
|
+
$.http.fetch(`${lauf.basis}${pfad}`, {
|
|
129
|
+
method: 'POST',
|
|
130
|
+
headers: { Authorization: `Bearer ${schluessel}`, 'Content-Type': 'application/json' },
|
|
131
|
+
body: JSON.stringify(rumpf),
|
|
132
|
+
}).then(
|
|
133
|
+
antwort => {
|
|
134
|
+
frist.cancel()
|
|
135
|
+
fertig({ status: antwort.status, text: antwort.text })
|
|
136
|
+
},
|
|
137
|
+
() => {
|
|
138
|
+
frist.cancel()
|
|
139
|
+
fertig(null)
|
|
140
|
+
},
|
|
141
|
+
)
|
|
142
|
+
})
|
|
143
|
+
}
|