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.
- 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/register.tsx +41 -17
- 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/dist/logo-ablegen.js +62 -6
- package/dist/texte/wache.js +4 -4
- package/dist/wache-rechnung.js +3 -1
- package/package.json +1 -1
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "kimal3-logo",
|
|
3
|
-
"version": "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.
|
|
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
|
|
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
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
{
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
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
|
|
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
|
}
|
package/dist/logo-ablegen.js
CHANGED
|
@@ -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
|
|
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
|
|
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
|
|
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;
|
package/dist/texte/wache.js
CHANGED
|
@@ -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 " +
|
package/dist/wache-rechnung.js
CHANGED
|
@@ -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
|
-
|
|
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