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
package/README.md
CHANGED
|
@@ -42,8 +42,11 @@ kimal3 kontext fragen Die Fragen über deine Firma beantworten — Claude f
|
|
|
42
42
|
kimal3 kontext hoch Deine lokalen Änderungen an den fünf Firmen-Texten hochschicken
|
|
43
43
|
kimal3 projekt neu … Ein Projekt ins Erfolgsprofil eintragen
|
|
44
44
|
kimal3 projekt liste Deine eingetragenen Projekte anzeigen
|
|
45
|
-
kimal3 plan Planung
|
|
46
|
-
kimal3 plan
|
|
45
|
+
kimal3 plan Den Stand der Planung holen und Änderungen melden
|
|
46
|
+
kimal3 plan melde Eine Änderung an die Projektseite melden (Meldung als JSON von der Eingabe)
|
|
47
|
+
kimal3 plan senden Gemerkte Meldungen nachschicken, die offline nicht ankamen
|
|
48
|
+
kimal3 projekt verbinden Den Projektordner mit seinem Projekt in der App verbinden
|
|
49
|
+
kimal3 plan hoch Nur noch mit --ueberschreiben: den ganzen Stand durch plan/stand.json ersetzen
|
|
47
50
|
kimal3 kurs Zeigen, wo du im Mini-Kurs stehst
|
|
48
51
|
kimal3 firma daten Die Daten deiner Firma holen, damit Claude sie auswerten kann
|
|
49
52
|
kimal3 zeigen start Eine Aufnahme starten, während du eine Aufgabe vormachst
|
|
@@ -184,8 +187,11 @@ kimal3 context questions Answer the questions about your company — Claude tak
|
|
|
184
187
|
kimal3 context upload Upload your local changes to the five company texts
|
|
185
188
|
kimal3 project new … Add a project to your success profile
|
|
186
189
|
kimal3 project list Show the projects you've added
|
|
187
|
-
kimal3 plan
|
|
188
|
-
kimal3 plan
|
|
190
|
+
kimal3 plan Fetch the planning state and report changes
|
|
191
|
+
kimal3 plan report Report a change to the project page (report as JSON from the input)
|
|
192
|
+
kimal3 plan send Send kept reports that didn't arrive while offline
|
|
193
|
+
kimal3 project connect Connect the project folder with its project in the app
|
|
194
|
+
kimal3 plan upload Only with --overwrite: replace the whole state with plan/stand.json
|
|
189
195
|
kimal3 course Show where you are in the mini-course
|
|
190
196
|
kimal3 company data Fetch your company's data so Claude can analyse it
|
|
191
197
|
kimal3 show start Start a recording while you show a task
|
|
@@ -3,6 +3,7 @@
|
|
|
3
3
|
"owner": { "name": "KImal3", "url": "https://kimal3.de" },
|
|
4
4
|
"description": "Eingetragen vom Programm kimal3 · Added by the kimal3 program",
|
|
5
5
|
"plugins": [
|
|
6
|
-
{ "name": "kimal3-logo", "source": "./kimal3-logo", "description": "KImal3-Logo über dem Eingabefeld · KImal3 logo above the prompt" }
|
|
6
|
+
{ "name": "kimal3-logo", "source": "./kimal3-logo", "description": "KImal3-Logo über dem Eingabefeld · KImal3 logo above the prompt" },
|
|
7
|
+
{ "name": "kimal3-plan", "source": "./kimal3-plan", "description": "Hält das Board der Projektseite aktuell · Keeps the project page board current" }
|
|
7
8
|
]
|
|
8
9
|
}
|
package/claude-plugins/README.md
CHANGED
|
@@ -2,6 +2,8 @@
|
|
|
2
2
|
|
|
3
3
|
Diesen Ordner legt `kimal3 einrichten` an und ersetzt ihn bei jedem Lauf. Eigene Änderungen hier gehen dabei verloren.
|
|
4
4
|
Das Logo abschalten: in Claude unter **Plugins** `kimal3-logo` ausschalten — `kimal3` schaltet es danach nicht wieder ein.
|
|
5
|
+
`kimal3-plan` hält das Board der Projektseite in der App aktuell (Punkt, solange die Sitzung arbeitet; Spec-Texte). Abschalten genauso: unter **Plugins** `kimal3-plan` ausschalten.
|
|
5
6
|
|
|
6
7
|
`kimal3 setup` creates this folder and replaces it on every run; your own changes here are lost.
|
|
7
8
|
To turn the logo off: switch off `kimal3-logo` under **Plugins** in Claude — `kimal3` won't switch it back on.
|
|
9
|
+
`kimal3-plan` keeps the board on the app's project page current (dot while the session works; spec texts). Turn it off the same way: switch off `kimal3-plan` under **Plugins**.
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "kimal3-logo",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.5.1",
|
|
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: '' }
|
|
@@ -16,7 +16,10 @@ const ANSICHT = 'ccd_view'
|
|
|
16
16
|
const EINBLENDEN = 'mcp__ccd_view__show_pane'
|
|
17
17
|
const geoeffnet = atom({ plugin: 'kimal3-logo', key: 'appGeoeffnet' } as const, false)
|
|
18
18
|
|
|
19
|
-
/**
|
|
19
|
+
/**
|
|
20
|
+
* Die Adresse für den Inhalt von `plan/projekt.json` oder `plan/stand.json` (`null`: keine Datei) -- beide tragen die
|
|
21
|
+
* Nummer als `projekt_id`. Ohne gültige Nummer der Kurs, wie der Link in der Leiste (S-3, S-4).
|
|
22
|
+
*/
|
|
20
23
|
export function adresseFuer(standJson: string | null): string {
|
|
21
24
|
if (standJson !== null) {
|
|
22
25
|
try {
|
|
@@ -30,6 +33,21 @@ export function adresseFuer(standJson: string | null): string {
|
|
|
30
33
|
return `${APP}/kurs`
|
|
31
34
|
}
|
|
32
35
|
|
|
36
|
+
/**
|
|
37
|
+
* Ist das ein gueltiger Ordnerzettel (`plan/projekt.json`: `format: 1`, Projektnummer, Titel)? Dieselbe Pruefung wie
|
|
38
|
+
* `zettelAus` im Mod `kimal3-plan` und in `cli/src/projekt-ordner.ts` -- ein ungueltiger zaehlt nicht, dann gilt die
|
|
39
|
+
* alte `plan/stand.json`.
|
|
40
|
+
*/
|
|
41
|
+
export function istZettel(text: string | null): boolean {
|
|
42
|
+
if (text === null) return false
|
|
43
|
+
try {
|
|
44
|
+
const z = JSON.parse(text.replace(/^\uFEFF/, '')) as { format?: unknown; projekt_id?: unknown; titel?: unknown } | null
|
|
45
|
+
return z?.format === 1 && typeof z.projekt_id === 'string' && PROJEKTNUMMER.test(z.projekt_id) && typeof z.titel === 'string' && z.titel.trim() !== ''
|
|
46
|
+
} catch {
|
|
47
|
+
return false
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
|
|
33
51
|
/** Die eigene Erlaubnis (unten): nur der Aufruf dieses Mods, nur eine Adresse der App. Herausgelöst, damit `check:logo` die Grenze prüft. */
|
|
34
52
|
export function istEigenerAufruf(plugin: string, input: unknown): boolean {
|
|
35
53
|
const url = (input as { url?: unknown } | null)?.url
|
|
@@ -83,29 +101,39 @@ async function liesDatei($: Engine, pfad: string): Promise<string | null> {
|
|
|
83
101
|
}
|
|
84
102
|
|
|
85
103
|
/**
|
|
86
|
-
*
|
|
104
|
+
* Eine Datei des Projekts, in dem Claude läuft (S-5, S-24, S-25): vom Startordner nach oben, bis zur Wurzel des
|
|
87
105
|
* eigenen Git-Projekts und nie darüber hinaus — sonst bekäme ein Projekt ohne Plan die Seite eines anderen. Ist die
|
|
88
|
-
* Wurzel eine Arbeitskopie (`.git` ist dort eine Datei), zählt danach die
|
|
106
|
+
* Wurzel eine Arbeitskopie (`.git` ist dort eine Datei), zählt danach die Datei ihres Hauptprojekts.
|
|
89
107
|
*/
|
|
90
|
-
async function
|
|
108
|
+
async function findePlanDatei($: Engine, cwd: string, datei: string): Promise<string | null> {
|
|
91
109
|
for (const ordner of ordnerNachOben(cwd)) {
|
|
92
|
-
const
|
|
93
|
-
if (
|
|
110
|
+
const text = await liesDatei($, `${ordner}/${datei}`)
|
|
111
|
+
if (text !== null) return text
|
|
94
112
|
if (!(await $.fs.exists(`${ordner}/.git`))) continue
|
|
95
113
|
// Ein Ordner `.git` lässt sich nicht lesen: echte Projektwurzel, hier ist Schluss.
|
|
96
114
|
const gitdatei = await liesDatei($, `${ordner}/.git`)
|
|
97
115
|
const haupt = gitdatei === null ? null : hauptordnerAusGitdatei(gitdatei)
|
|
98
|
-
return haupt === null ? null : liesDatei($, `${haupt}
|
|
116
|
+
return haupt === null ? null : liesDatei($, `${haupt}/${datei}`)
|
|
99
117
|
}
|
|
100
118
|
return null
|
|
101
119
|
}
|
|
102
120
|
|
|
121
|
+
/**
|
|
122
|
+
* Erst der Ordnerzettel `plan/projekt.json`, dann `plan/stand.json` für alte Ordner (Spec 2026-10-23-brett-in-echtzeit,
|
|
123
|
+
* 1.4, US-8) — dieselbe Reihenfolge wie `kimal3` (`cli/src/projekt-ordner.ts`). Ein ungültiger Zettel zählt nicht.
|
|
124
|
+
*/
|
|
125
|
+
async function adresseJetzt($: Engine): Promise<string> {
|
|
126
|
+
const cwd = await $.session.cwd()
|
|
127
|
+
const zettel = await findePlanDatei($, cwd, 'plan/projekt.json')
|
|
128
|
+
return adresseFuer(istZettel(zettel) ? zettel : await findePlanDatei($, cwd, 'plan/stand.json'))
|
|
129
|
+
}
|
|
130
|
+
|
|
103
131
|
async function oeffne($: Engine): Promise<boolean> {
|
|
104
|
-
const
|
|
132
|
+
const adresse = await adresseJetzt($)
|
|
105
133
|
// Scheitern meldet sich als Scheitern, nie im Chat (S-13): eine Zeile nur im Debug-Protokoll (`to: 'debug'` —
|
|
106
134
|
// ohne steht sie im Chatverlauf, Befund der Durchsicht 23.10.).
|
|
107
135
|
try {
|
|
108
|
-
const antwort = await $.mcp.call(BROWSER, 'navigate', { url:
|
|
136
|
+
const antwort = await $.mcp.call(BROWSER, 'navigate', { url: adresse })
|
|
109
137
|
if (!antwort.isError) {
|
|
110
138
|
await einblenden($)
|
|
111
139
|
return true
|
|
@@ -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
|
-
// danach, dass sie gesetzt ist (`cli/src/
|
|
9
|
+
// danach, dass sie gesetzt ist (`cli/src/mods-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
|
+
}
|