kimal3 0.6.4 → 0.6.5

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.
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "kimal3-logo",
3
- "version": "0.4.0",
3
+ "version": "0.5.0",
4
4
  "description": "KImal3-Logo über dem Eingabefeld, öffnet beim Start das Projekt in der App, benennt den Chat nach seinem Thema · KImal3 logo above the prompt, opens the project in the app on start, names the chat after its topic",
5
5
  "author": { "name": "KImal3", "url": "https://kimal3.de" },
6
6
  "types": "./types/index.d.ts"
@@ -0,0 +1,8 @@
1
+ // Wo `kimal3` auf diesem Rechner liegt und welche Fassung es hat. `kimal3` ersetzt beim Ablegen die Zeile
2
+ // `const ABLAGE: Ablage = KEINE_ABLAGE` in register.tsx durch die echten Werte (`mitAblage` in `cli/src/logo-ablegen.ts`,
3
+ // Bauplan 2026-10-23, Abschnitt 1.1). Hier im Paket bleibt sie leer: Ein Mod ohne Ablage (Entwicklung) zeigt keine
4
+ // Fassung und fragt npm nicht (U-9). Die Felder stehen in `cli/src/logo-ablegen.ts` noch einmal (`Ablage`) -- die CLI
5
+ // kann diese Datei nicht einbinden; `check:cli` hält beide Feldlisten gleich.
6
+ export type Ablage = { fassung: string; node: string; npm: string; kimal3: string; kimal3Ordner: string }
7
+
8
+ export const KEINE_ABLAGE: Ablage = { fassung: '', node: '', npm: '', kimal3: '', kimal3Ordner: '' }
@@ -1,10 +1,15 @@
1
1
  import type { Register } from 'claude-code'
2
+ import { KEINE_ABLAGE, type Ablage } from './ablage'
2
3
  import { appOeffnen } from './app-oeffnen'
3
4
  import { chatBenennen } from './chat-benennen'
5
+ import { updateClaudeFragen, updateInstallieren, updateZustand } from './update'
6
+ import { satzWeicht, TEXTE, type Zustand } from './update-rechnung'
4
7
 
5
8
  // Die Sprache des Kontos. `kimal3 einrichten` setzt diese Zeile beim Ablegen in `~/.kimal3/claude-plugins` und prüft
6
9
  // danach, dass sie gesetzt ist (`cli/src/logo-ablegen.ts`).
7
10
  const SPRACHE: 'de' | 'en' = 'de'
11
+ // Fassung und Orte von `kimal3` auf diesem Rechner; beim Ablegen gesetzt wie die Sprache (hooks/ablage.ts).
12
+ const ABLAGE: Ablage = KEINE_ABLAGE
8
13
 
9
14
  // Icon: docs/design/kimal3-icon-animated.svg, unverändert bis auf die viewBox:
10
15
  // Sie schneidet den leeren Rand um die drei Dreiecke weg (Original: 0 0 80 80).
@@ -33,11 +38,8 @@ const MARKEN_CYAN = '#00C3FF'
33
38
  // grau dahinter, wenn es dem Terminal keine Links zutraut — FORCE_HYPERLINK hilft nicht.
34
39
  const APP = 'https://app.kimal3.de/kurs'
35
40
 
36
- // Vor und nach der Wortmarke. Englisch: „AI Workplace“, nie „workspace“ (Spec, Global Constraints).
37
- const TEXT = {
38
- de: { vor: 'Dein ', nach: '-Arbeitsplatz', alt: 'KImal3-Logo' },
39
- en: { vor: 'Your ', nach: ' AI Workplace', alt: 'KImal3 logo' },
40
- }[SPRACHE]
41
+ // Vor und nach der Wortmarke: aus update-rechnung.ts, damit die Breitenrechnung denselben Satz zählt (`satzWeicht`).
42
+ const TEXT = { ...TEXTE[SPRACHE], alt: { de: 'KImal3-Logo', en: 'KImal3 logo' }[SPRACHE] }
41
43
 
42
44
  export const register: Register = (on, options) => {
43
45
  // Beim Start die Projektseite in der App öffnen (hooks/app-oeffnen.ts).
@@ -45,34 +47,55 @@ export const register: Register = (on, options) => {
45
47
  // Den Chat nach seinem Thema benennen, sobald es klar ist (hooks/chat-benennen.ts).
46
48
  chatBenennen(on, options)
47
49
 
48
- on('ui.render', { component: 'AbovePrompt' }, ($, e, next) => {
50
+ on('ui.render', { component: 'AbovePrompt' }, async ($, e, next) => {
49
51
  if (e.props.hasSurvey) {
50
52
  return next(e)
51
53
  }
54
+ // Ohne Ablage (Entwicklung) `null`: keine Fassung, kein Knopf, die Leiste wie früher (U-9).
55
+ const zustand = await updateZustand($, ABLAGE, e.props.isWorking)
56
+ const { Button, Text } = $.ui.resolve(e)
57
+
58
+ // Hinter dem Satz: die eigene Fassung, gedimmt, und was das Update gerade zu sagen hat (Bauplan 1.4).
59
+ // Kein Tastenkürzel an den Knöpfen (Spec P-12): eine Ziffer im leeren Eingabefeld darf kein Update anstoßen.
60
+ const dahinter = (z: Zustand) => [
61
+ <Text dimColor>{`v${z.fassung}`}</Text>,
62
+ ...(z.art === 'knopf' ? [<Button key="update" label={TEXT.knopf} onPress={() => void updateInstallieren($, ABLAGE, SPRACHE)} />] : []),
63
+ ...(z.art === 'laeuft' ? [<Text dimColor>{TEXT.laeuft(z.ziel)}</Text>] : []),
64
+ ...(z.art === 'geklappt' ? [<Text>{TEXT.geklappt(z.ziel)}</Text>] : []),
65
+ ...(z.art === 'fehler'
66
+ ? [<Text>{TEXT.fehler}</Text>, <Button key="fragen" label={TEXT.fragen} onPress={() => void updateClaudeFragen($, SPRACHE)} />]
67
+ : []),
68
+ ...(z.art === 'gesperrt' ? [<Text>{TEXT.gesperrt}</Text>] : []),
69
+ ]
52
70
 
53
71
  // Im Terminal gibt es kein Svg: drei Dreiecke als Zeichen, die Wortmarke wie auf kimal3.de
54
72
  // („KI" in Schriftfarbe, „mal3" im Marken-Cyan). Das Cyan steht als Farbwert, nicht als „cyan" —
55
73
  // das Terminal-Cyan ist je nach Farbschema ein anderes, oft ein blasses Türkis.
74
+ // Ist es zu schmal, weicht der Satz; Fassung, Knopf und Zustand bleiben (U-14).
56
75
  if (e.surface === 'terminal') {
57
- const { Box, Link, Text } = $.ui.resolve(e)
76
+ const { Box, Link } = $.ui.resolve(e)
77
+ const weicht = zustand !== null && satzWeicht(zustand, SPRACHE, e.props.bodyColumns)
58
78
  return (
59
79
  <Box flexDirection="row" gap={1}>
60
80
  <Text color={MARKEN_CYAN} bold>{'▲▲▲'}</Text>
61
- <Text bold>
62
- {TEXT.vor}
63
- <Link href={APP}>
64
- {'KI'}
65
- <Text color={MARKEN_CYAN}>{'mal3'}</Text>
66
- {TEXT.nach}
67
- </Link>
68
- {'.'}
69
- </Text>
81
+ {weicht ? null : (
82
+ <Text bold>
83
+ {TEXT.vor}
84
+ <Link href={APP}>
85
+ {'KI'}
86
+ <Text color={MARKEN_CYAN}>{'mal3'}</Text>
87
+ {TEXT.nach}
88
+ </Link>
89
+ {'.'}
90
+ </Text>
91
+ )}
92
+ {zustand === null ? null : dahinter(zustand)}
70
93
  </Box>
71
94
  )
72
95
  }
73
96
 
74
97
  // isInteractive: erst damit spielt die Leiste die CSS-Animation ab, sonst steht das Icon still.
75
- const { Box, Link, Svg, Text } = $.ui.resolve(e)
98
+ const { Box, Link, Svg } = $.ui.resolve(e)
76
99
  return (
77
100
  <Box flexDirection="row" alignItems="center" gap={1}>
78
101
  <Svg source={ICON} alt={TEXT.alt} width={24} height={22} isInteractive />
@@ -81,6 +104,7 @@ export const register: Register = (on, options) => {
81
104
  <Link href={APP}>{`KImal3${TEXT.nach}`}</Link>
82
105
  {'.'}
83
106
  </Text>
107
+ {zustand === null ? null : dahinter(zustand)}
84
108
  </Box>
85
109
  )
86
110
  })
@@ -0,0 +1,175 @@
1
+ // Die reinen Rechnungen und Texte hinter „Update installieren“ in der Leiste (Bauplan 2026-10-23-update-in-der-leiste.md,
2
+ // Abschnitte 1.2 bis 1.4). Kein `$`, kein Haken: Den Ablauf baut `hooks/update.ts`. Herausgelöst, damit `check:logo`
3
+ // jede Entscheidung gegen von Hand geschriebene Soll-Werte prüft (messen-und-pruefen §17), statt sie nur live zu sehen.
4
+
5
+ const MINUTE = 60_000
6
+ // Nach einem gescheiterten npm-Anruf (Netz, Firmen-Spiegel) frühestens nach einer Stunde wieder fragen (U-12).
7
+ const FEHLER_PAUSE = 60 * MINUTE
8
+ // Der Lauf dauert höchstens 10 + 2 + 2 Minuten (npm, `--version`, `skill install`); danach gilt eine Sperre als liegen
9
+ // geblieben — etwa weil Claude mittendrin geschlossen wurde und `finally` nie lief (Bauplan 1.3 Punkt 3, A-5).
10
+ const SPERRE_DAUER = 20 * MINUTE
11
+ const FERTIG_DAUER = 24 * 60 * MINUTE
12
+ // Was Claude Code im Terminal womöglich grau hinter den Link hängt: `https://app.kimal3.de/kurs` (Spec P-4, S-22).
13
+ // Der Mod weiß nicht, ob es geschieht, also rechnet er immer damit.
14
+ const KURS_ADRESSE = 26
15
+
16
+ /** Nur `1.2.3` gilt (Positivliste, Spec 1.4 Punkt 1): keine Vorabfassung, kein `v`, nichts, was in einen Befehl rutscht. */
17
+ export function istFassung(s: unknown): s is string {
18
+ return typeof s === 'string' && /^\d+\.\d+\.\d+$/.test(s)
19
+ }
20
+
21
+ /** `a` ist neuer als `b`, Zahl für Zahl — als Text wäre `"0.10.0" < "0.9.0"`. Ungültig auf einer Seite: `false` (kein Knopf). */
22
+ export function istNeuer(a: string, b: string): boolean {
23
+ if (!istFassung(a) || !istFassung(b)) return false
24
+ const x = a.split('.').map(Number)
25
+ const y = b.split('.').map(Number)
26
+ for (let i = 0; i < 3; i++) if (x[i] !== y[i]) return x[i] > y[i]
27
+ return false
28
+ }
29
+
30
+ /**
31
+ * Die drei Befehle in fester Reihenfolge (Bauplan 1.3 Punkt 4), jeder als Wortliste für `$.process.run` ohne Kommandozeile.
32
+ * Von außen kommt nur `ziel`, und nur als Fassung (§23). `null`, wenn `ziel` keine ist oder ein Pfad fehlt — ein leeres
33
+ * `npm` heißt „beim Ablegen nicht gefunden“, dann geht der Knopf den Weg über Claude (U-10).
34
+ */
35
+ export function befehle(ablage: { node: string; npm: string; kimal3: string }, ziel: string): string[][] | null {
36
+ const { node, npm, kimal3 } = ablage
37
+ if (!istFassung(ziel) || node === '' || npm === '' || kimal3 === '') return null
38
+ return [
39
+ [node, npm, 'install', '-g', `kimal3@${ziel}`],
40
+ [node, kimal3, '--version'],
41
+ [node, kimal3, 'skill', 'install'],
42
+ ]
43
+ }
44
+
45
+ /**
46
+ * Was sich der Mod über Sitzungen hinweg merkt (`$.store`), damit npm höchstens einmal am Tag gefragt wird.
47
+ * `tag`: deutscher Kalendertag der letzten gelungenen Antwort (`2026-10-23`) · `neueste`: was npm da sagte — ein zweites
48
+ * Fenster am selben Tag zeigt den Knopf daraus, ohne zu fragen · `fehlerAm`: Zeitpunkt (ms) des letzten Fehlschlags.
49
+ */
50
+ export type Merkzettel = { tag?: string; neueste?: string; fehlerAm?: number }
51
+
52
+ /** Der Kalendertag in deutscher Zeit, wie `heuteInDeutschland` in der App (`src/app/(erfolgsprofil)/typen.ts`). */
53
+ export function deutscherTag(jetzt: number): string {
54
+ return new Intl.DateTimeFormat('sv-SE', { timeZone: 'Europe/Berlin' }).format(new Date(jetzt))
55
+ }
56
+
57
+ /** npm fragen? Einmal je deutschem Kalendertag (U-11), nach einem Fehlschlag höchstens einmal je Stunde (U-12). */
58
+ export function sollFragen(merkzettel: Merkzettel, jetzt: number): boolean {
59
+ if (merkzettel.tag === deutscherTag(jetzt)) return false
60
+ return merkzettel.fehlerAm === undefined || jetzt - merkzettel.fehlerAm >= FEHLER_PAUSE
61
+ }
62
+
63
+ /** Läuft ein Update (`$.store`, auch aus einem anderen Fenster)? `seit` in ms. */
64
+ export type Sperre = { ziel: string; seit: number }
65
+
66
+ /**
67
+ * Die Sperre gilt 20 Minuten und nur, solange der Mod selbst noch älter als `ziel` ist: Ein Mod, der die neue Fassung
68
+ * schon trägt, hat das Update hinter sich, eine übrig gebliebene Sperre darf ihm den Stand nicht verdecken (U-5, U-6b).
69
+ */
70
+ export function sperreGilt(sperre: Sperre | undefined, jetzt: number, eigeneFassung: string): boolean {
71
+ return sperre !== undefined && jetzt - sperre.seit < SPERRE_DAUER && istNeuer(sperre.ziel, eigeneFassung)
72
+ }
73
+
74
+ /** Ein Update hat geklappt (`$.store`), Claude ist noch nicht neu gestartet. `am` in ms. */
75
+ export type Fertig = { ziel: string; am: number }
76
+
77
+ /**
78
+ * „✓ installiert – Claude neu starten“ gilt, solange der Mod älter als `ziel` ist (U-6b) — und höchstens 24 Stunden: Hat
79
+ * ein Tag mit Neustarts den Mod nicht auf `ziel` gebracht, stimmt der Haken offenbar nicht (etwa startet eine andere
80
+ * Node-Fassung `kimal3`), und die Leiste zeigt wieder, was npm sagt. Ein Wert in anderer Form (bis zum 23.10. ein bloßer
81
+ * Text) gilt nicht.
82
+ */
83
+ export function fertigGilt(fertig: unknown, jetzt: number, eigeneFassung: string): fertig is Fertig {
84
+ const f = (fertig ?? {}) as Record<string, unknown>
85
+ return istFassung(f.ziel) && typeof f.am === 'number' && jetzt - f.am < FERTIG_DAUER && istNeuer(f.ziel, eigeneFassung)
86
+ }
87
+
88
+ /**
89
+ * Hat `kimal3 --version` nach dem Install wirklich die neue Fassung? Die Ausgabe ist mehrzeilig und eingerückt
90
+ * (` kimal3 0.6.6` und darunter weitere Zeilen); eine Zeile muss nach `trim` GENAU `kimal3 X` lauten — `kimal3 0.6.66`
91
+ * zählt nicht für `0.6.6` (Bauplan 1.3 Punkt 4, QA-1).
92
+ */
93
+ export function versionStimmt(ausgabe: string, ziel: string): boolean {
94
+ return istFassung(ziel) && ausgabe.split(/\r?\n/).some(zeile => zeile.trim() === `kimal3 ${ziel}`)
95
+ }
96
+
97
+ // Vor und nach der Wortmarke „KImal3“; register.tsx zeichnet die Wortmarke dazwischen als Link. Englisch: „AI Workplace“,
98
+ // nie „workspace“ (Spec, Global Constraints). Eine Quelle für Leiste und `satzWeicht`, damit die Breite mit dem Satz
99
+ // rechnet, der wirklich dasteht.
100
+ const VOR_NACH = {
101
+ de: { vor: 'Dein ', nach: '-Arbeitsplatz' },
102
+ en: { vor: 'Your ', nach: ' AI Workplace' },
103
+ }
104
+
105
+ // Wortlaute (Bauplan 1.4).
106
+ export const TEXTE = {
107
+ de: {
108
+ ...VOR_NACH.de,
109
+ satz: `${VOR_NACH.de.vor}KImal3${VOR_NACH.de.nach}.`,
110
+ knopf: 'Update installieren',
111
+ frage: (von: string, auf: string) =>
112
+ `Es laufen: npm install -g kimal3@${auf} und kimal3 skill install. Dauert etwa eine Minute, danach Claude neu starten. kimal3 von ${von} auf ${auf} aktualisieren?`,
113
+ ja: 'Installieren',
114
+ nein: 'Abbrechen',
115
+ laeuft: (ziel: string) => `kimal3 ${ziel} wird installiert …`,
116
+ geklappt: (ziel: string) => `✓ ${ziel} installiert – Claude neu starten`,
117
+ fehler: 'Update hat nicht geklappt.',
118
+ fragen: 'Claude fragen',
119
+ bitteFehler: (ziel: string, meldung: string) =>
120
+ `Das kimal3-Update auf ${ziel} hat nicht geklappt. Hier die Meldung:\n\n${meldung}\n\nWas ist los, und was kann ich tun?`,
121
+ bitteOhneNpm: (ziel: string) => `Aktualisiere kimal3 auf ${ziel}: erst npm install -g kimal3@${ziel}, danach kimal3 skill install.`,
122
+ gesperrt: 'Update hier gesperrt – frag deine IT.',
123
+ },
124
+ en: {
125
+ ...VOR_NACH.en,
126
+ satz: `${VOR_NACH.en.vor}KImal3${VOR_NACH.en.nach}.`,
127
+ knopf: 'Install update',
128
+ frage: (von: string, auf: string) =>
129
+ `Runs: npm install -g kimal3@${auf} and kimal3 skill install. Takes about a minute, then restart Claude. Update kimal3 from ${von} to ${auf}?`,
130
+ ja: 'Install',
131
+ nein: 'Cancel',
132
+ laeuft: (ziel: string) => `Installing kimal3 ${ziel} …`,
133
+ geklappt: (ziel: string) => `✓ ${ziel} installed – restart Claude`,
134
+ fehler: "The update didn't work.",
135
+ fragen: 'Ask Claude',
136
+ bitteFehler: (ziel: string, meldung: string) =>
137
+ `The kimal3 update to ${ziel} didn't work. Here is the message:\n\n${meldung}\n\nWhat's wrong, and what can I do?`,
138
+ bitteOhneNpm: (ziel: string) => `Update kimal3 to ${ziel}: first npm install -g kimal3@${ziel}, then kimal3 skill install.`,
139
+ gesperrt: 'Update blocked here – ask your IT.',
140
+ },
141
+ }
142
+
143
+ /** Was die Leiste hinter der eigenen Fassung zeigt; `still` = nichts (kein Update, npm stumm, Claude arbeitet). */
144
+ export type Zustand = { fassung: string } & (
145
+ | { art: 'still' | 'knopf' | 'fehler' | 'gesperrt' }
146
+ | { art: 'laeuft' | 'geklappt'; ziel: string }
147
+ )
148
+
149
+ // Ein Knopf im Terminal ist `[ Beschriftung ]` (Typen von Claude Code, `ButtonProps`).
150
+ const knopf = (text: string) => `[ ${text} ]`
151
+
152
+ /** Die Teile hinter der Fassung, wie das Terminal sie zeichnet. */
153
+ function zustandsteile(zustand: Zustand, sprache: 'de' | 'en'): string[] {
154
+ const t = TEXTE[sprache]
155
+ switch (zustand.art) {
156
+ case 'still': return []
157
+ case 'knopf': return [knopf(t.knopf)]
158
+ case 'laeuft': return [t.laeuft(zustand.ziel)]
159
+ case 'geklappt': return [t.geklappt(zustand.ziel)]
160
+ case 'fehler': return [t.fehler, knopf(t.fragen)]
161
+ case 'gesperrt': return [t.gesperrt]
162
+ }
163
+ }
164
+
165
+ /**
166
+ * Muss im Terminal der Satz „Dein KImal3-Arbeitsplatz.“ weichen? Ja, wenn `▲▲▲`, Satz, die womöglich angehängte
167
+ * Kurs-Adresse, `v0.6.5` und der Zustandsteil nicht in `bodyColumns` passen — Fassung, Knopf und Zustand weichen nie
168
+ * (Bauplan 1.4, U-14). Zwischen zwei Teilen steht je ein Leerzeichen (`gap={1}` in register.tsx).
169
+ */
170
+ export function satzWeicht(zustand: Zustand, sprache: 'de' | 'en', bodyColumns: number): boolean {
171
+ const teile = ['▲▲▲', TEXTE[sprache].satz, `v${zustand.fassung}`, ...zustandsteile(zustand, sprache)]
172
+ // Zeichen statt UTF-16-Einheiten; ▲, ✓, – und … sind im Terminal je eine Spalte breit.
173
+ const breite = teile.reduce((summe, teil) => summe + [...teil].length, teile.length - 1) + KURS_ADRESSE
174
+ return breite > bodyColumns
175
+ }
@@ -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
  }
@@ -1,11 +1,12 @@
1
1
  import { cpSync, existsSync, readdirSync, readFileSync, renameSync, rmSync, writeFileSync } from "node:fs";
2
- import { homedir } from "node:os";
3
- import { basename, dirname, join } from "node:path";
2
+ import { homedir, platform } from "node:os";
3
+ import { basename, dirname, join, posix, win32 } from "node:path";
4
4
  import { fileURLToPath } from "node:url";
5
5
  import { EINRICHTEN_TEXTE } from "./texte/einrichten.js";
6
+ import { eigeneVersion } from "./version.js";
6
7
  /**
7
8
  * Legt den Plugin-Katalog mit dem Logo-Mod in `~/.kimal3/claude-plugins` ab und setzt dort die
8
- * Sprache des Kontos. Claude Code liest den Mod aus genau diesem Ordner (Katalog `kimal3`,
9
+ * Sprache des Kontos und die Ablage (`mitAblage`). Claude Code liest den Mod aus genau diesem Ordner (Katalog `kimal3`,
9
10
  * `plugins-mischen.ts`).
10
11
  *
11
12
  * 🔑 Warum ein fester Ordner im Benutzerordner und nicht der Ordner im installierten npm-Paket:
@@ -24,10 +25,64 @@ export function mitSprache(quelltext, sprache) {
24
25
  return null;
25
26
  return quelltext.replace(SPRACH_ZEILE, `const SPRACHE: 'de' | 'en' = '${sprache}'`); // kein-anzeigetext: Quelltext des Mods
26
27
  }
28
+ /** Die Zeile im Mod, die beim Ablegen die Ablage bekommt; im Paket steht dort die leere. */
29
+ export const ABLAGE_ZEILE = "const ABLAGE: Ablage = KEINE_ABLAGE"; // kein-anzeigetext: Quelltext des Mods
30
+ /**
31
+ * Reine Rechnung: der Mod-Text mit der Ablage, oder `null`, wenn die Zeile nicht genau einmal dasteht. Ohne Ablage
32
+ * (keine eigene Fassung) bleibt die leere Zeile stehen -- der Mod zeigt dann wie bisher nur das Logo (U-9).
33
+ * 🔴 `split`/`join`, nie `replace(zeile, text)`: Im Ersatz-Text eines `replace` sind `$&` und `$'` Befehle, und ein
34
+ * Ordnername darf beides enthalten -- dann stünde fremder Quelltext im Mod (messen-und-pruefen §23, U-16). `JSON.stringify`
35
+ * macht aus jedem Wert ein gültiges Stück Quelltext, auch mit `"` und `\` (Windows-Pfade).
36
+ */
37
+ export function mitAblage(quelltext, ablage) {
38
+ const teile = quelltext.split(ABLAGE_ZEILE);
39
+ if (teile.length !== 2)
40
+ return null;
41
+ return ablage === null ? quelltext : teile.join(`const ABLAGE: Ablage = ${JSON.stringify(ablage)}`); // kein-anzeigetext: Quelltext des Mods
42
+ }
43
+ /**
44
+ * Reine Rechnung hinter `ablageWerte`. Ohne eigene Fassung keine Ablage (`null`).
45
+ * `npm` ist das `npm-cli.js`, das zu DIESEM Node gehört (Windows daneben, sonst unter `lib/`), und nur dann gesetzt, wenn
46
+ * es die Datei gibt und `kimal3` global installiert läuft. Sonst `""`: Der Knopf bittet dann Claude (U-10), statt ein
47
+ * `npm install -g` anzustoßen, nach dem `kimal3 --version` doch die alte Zahl sagte.
48
+ * 🔑 Global heißt, als Erlaubnisliste (messen-und-pruefen §23): `…/node_modules/kimal3/…` mit genau EINEM `node_modules`,
49
+ * ohne `_npx` (der Zwischenspeicher von `npx`), und über dem `node_modules` kein `package.json` (sonst ist es der Ordner
50
+ * eines Projekts). Ein Start aus dem Repo (`cli/dist`) ist damit auch nicht global.
51
+ */
52
+ export function ablageAus(lage) {
53
+ if (lage.fassung === null)
54
+ return null;
55
+ const p = lage.system === "win32" ? win32 : posix;
56
+ const npm = lage.system === "win32"
57
+ ? p.join(p.dirname(lage.node), "node_modules", "npm", "bin", "npm-cli.js") // kein-anzeigetext: Pfad
58
+ : p.join(p.dirname(p.dirname(lage.node)), "lib", "node_modules", "npm", "bin", "npm-cli.js"); // kein-anzeigetext: Pfad
59
+ const teile = lage.kimal3.split(/[\\/]/);
60
+ const i = teile.indexOf("node_modules");
61
+ const global = i > 0 && teile.lastIndexOf("node_modules") === i && teile[i + 1] === "kimal3" && !teile.includes("_npx")
62
+ && !lage.gibtEs(p.join(teile.slice(0, i).join(p.sep), "package.json"));
63
+ return {
64
+ fassung: lage.fassung,
65
+ node: lage.node,
66
+ npm: global && lage.gibtEs(npm) ? npm : "",
67
+ kimal3: lage.kimal3,
68
+ kimal3Ordner: p.join(lage.heim, ".kimal3"), // kein-anzeigetext: Ordnername
69
+ };
70
+ }
71
+ /** Die Ablage dieses Rechners, im laufenden `kimal3` gemessen (Bauplan 2026-10-23, Tabelle in 1.1). */
72
+ export function ablageWerte() {
73
+ return ablageAus({
74
+ fassung: eigeneVersion(),
75
+ node: process.execPath,
76
+ kimal3: fileURLToPath(new URL("./kimal3.js", import.meta.url)), // kein-anzeigetext: Dateiname
77
+ heim: homedir(),
78
+ system: platform(),
79
+ gibtEs: existsSync,
80
+ });
81
+ }
27
82
  /**
28
83
  * 🔴 NIE EIN HALBER ORDNER (Durchsicht-Funde 2 und 9): Claudes Einstellungen zeigen auf `katalogOrdner()`,
29
84
  * und eine laufende Claude-Sitzung hat den Mod daraus geladen. Deshalb:
30
- * 1. Sprache setzen, BEVOR irgendetwas angefasst wird -- fehlt die Zeile, bleibt alles, wie es war.
85
+ * 1. Sprache und Ablage setzen, BEVOR irgendetwas angefasst wird -- fehlt eine der Zeilen, bleibt alles, wie es war.
31
86
  * 2. Steht schon genau das da, was entstehen soll: nichts anfassen (Lage P-8). Das ist auch der Normalfall
32
87
  * eines zweiten Laufs, und so kommt er gar nicht erst in die Nähe der Windows-Sperren unten.
33
88
  * 3. Den neuen Ordner daneben fertig bauen (`….neu`), dann tauschen: alten Ordner wegschieben, neuen an
@@ -46,7 +101,8 @@ export function logoAblegen(sprache) {
46
101
  const neu = `${ziel}.neu`; // kein-anzeigetext: Ordnername
47
102
  const modDatei = (ordner) => join(ordner, "kimal3-logo", "hooks", "register.tsx");
48
103
  try {
49
- const text = mitSprache(readFileSync(modDatei(quelle), "utf8"), sprache);
104
+ const mitSpr = mitSprache(readFileSync(modDatei(quelle), "utf8"), sprache);
105
+ const text = mitSpr === null ? null : mitAblage(mitSpr, ablageWerte());
50
106
  if (text === null)
51
107
  return { ok: false, detail: modDatei(quelle) };
52
108
  if (stehtSchonDa(quelle, ziel, text)) {
@@ -102,7 +158,7 @@ export function frischeLogoAuf(s) {
102
158
  else if (ablage.stand === "alt")
103
159
  console.log(t.logoNotRefreshed);
104
160
  }
105
- /** Gleiche Dateien, gleicher Inhalt -- der Mod mit der Sprache des Kontos, alles andere wie im Paket. */
161
+ /** Gleiche Dateien, gleicher Inhalt -- der Mod mit Sprache und Ablage, alles andere wie im Paket. */
106
162
  function stehtSchonDa(quelle, ziel, modText) {
107
163
  if (!existsSync(ziel))
108
164
  return false;
@@ -2,8 +2,8 @@ const de = {
2
2
  accessExpired: (login) => "🔴🔴🔴 kimal3: Dein Zugang ist abgelaufen — zum Beispiel nach einem neuen Passwort " +
3
3
  `oder einer längeren Pause. Melde dich neu an mit: ${login} 🔴🔴🔴`,
4
4
  messageFromKimal3: (text) => `Nachricht von kimal3: „${text}“`,
5
- newVersion: (neu, eigene) => `🟡🟡🟡 kimal3: Es gibt eine neuere Version: ${neu} (du hast ${eigene}). ` +
6
- `Holen mit: ! npm install -g kimal3@${neu} 🟡🟡🟡`,
5
+ newVersion: (neu, eigene, skill) => `🟡🟡🟡 kimal3: Es gibt eine neuere Version: ${neu} (du hast ${eigene}). ` +
6
+ `Holen mit: ! npm install -g kimal3@${neu} — danach: ${skill} 🟡🟡🟡`,
7
7
  skillsOld: (befehl) => "🟡🟡🟡 kimal3: Deine /k3-Befehle in Claude sind veraltet oder in einer anderen Sprache als dein Konto. " +
8
8
  `Auffrischen: ${befehl} 🟡🟡🟡`,
9
9
  instructionToClaude: "Anweisung an Claude: Gib die Zeilen oben dem Menschen als ERSTE Zeilen deiner nächsten " +
@@ -20,8 +20,8 @@ const en = {
20
20
  accessExpired: (login) => "🔴🔴🔴 kimal3: Your sign-in has expired, for example after a new password " +
21
21
  `or a long break. Sign in again with: ${login} 🔴🔴🔴`,
22
22
  messageFromKimal3: (text) => `Message from kimal3: “${text}”`,
23
- newVersion: (neu, eigene) => `🟡🟡🟡 kimal3: A newer version is available: ${neu} (you have ${eigene}). ` +
24
- `Install it with: ! npm install -g kimal3@${neu} 🟡🟡🟡`,
23
+ newVersion: (neu, eigene, skill) => `🟡🟡🟡 kimal3: A newer version is available: ${neu} (you have ${eigene}). ` +
24
+ `Install it with: ! npm install -g kimal3@${neu} — then: ${skill} 🟡🟡🟡`,
25
25
  skillsOld: (befehl) => "🟡🟡🟡 kimal3: Your /k3 commands in Claude are out of date or in a different language from your account. " +
26
26
  `Update them: ${befehl} 🟡🟡🟡`,
27
27
  instructionToClaude: "Instruction to Claude: Show the lines above to the person verbatim, in bold, as the FIRST lines of " +
@@ -49,7 +49,9 @@ export function wacheBefunde(lage, s) {
49
49
  istAelter(lage.eigeneVersion, lage.neuesteVersion)) {
50
50
  befunde.push({
51
51
  art: "version",
52
- zeile: T.newVersion(lage.neuesteVersion, lage.eigeneVersion),
52
+ // Zwei Befehle GETRENNT, nie mit `&&` verbunden: Die Windows-PowerShell 5.1 kennt `&&` nicht, und
53
+ // ohne `skill install` bleiben Claudes /k3-Befehle auf der alten Fassung (Spec Update in der Leiste, E-4).
54
+ zeile: T.newVersion(lage.neuesteVersion, lage.eigeneVersion, tippen("skill install", s)), // kein-anzeigetext: Befehlsname
53
55
  });
54
56
  }
55
57
  if (lage.skillVeraltet) {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "kimal3",
3
- "version": "0.6.4",
3
+ "version": "0.6.5",
4
4
  "description": "Trag deine KI-Projekte dort ein, wo sie entstehen: im Terminal. · Log your AI projects where they happen: in the terminal.",
5
5
  "type": "module",
6
6
  "bin": {