kimal3 0.1.1 → 0.1.3

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,22 +1,61 @@
1
- import { copyFileSync, existsSync, mkdirSync } from "node:fs";
1
+ import { copyFileSync, existsSync, mkdirSync, readFileSync, readdirSync, rmSync } from "node:fs";
2
2
  import { homedir } from "node:os";
3
3
  import { fileURLToPath } from "node:url";
4
4
  import { join } from "node:path";
5
5
  /**
6
- * Legt das Skill GLOBAL ab, damit Claude dieses Werkzeug ueberall kennt.
6
+ * Namen, die frueher einmal ausgeliefert wurden und heute nicht mehr.
7
+ *
8
+ * 🔑 **Wofuer das da ist** (Review-Befund, 2026-08-18): Die Befehle landen im
9
+ * GEMEINSAMEN Ordner `~/.claude/commands/` und werden dort nur ueberschrieben,
10
+ * nie entfernt. Wird ein Befehl spaeter umbenannt oder gestrichen, ueberlebt
11
+ * die alte Datei jedes `skill install` und bietet weiter Anweisungen an, die
12
+ * auf einen nicht mehr vorhandenen Unterbefehl zeigen. `SKILL.md` kann das
13
+ * nicht passieren -- es ist EINE Datei, die ueberschrieben wird.
14
+ *
15
+ * ⚠️ Warum eine Liste und nicht "alles loeschen, was wir nicht ausliefern":
16
+ * Der Ordner gehoert dem Menschen. Ein `k3*.md`, das er selbst angelegt hat,
17
+ * duerfen wir nicht wegraeumen. **Wer einen Befehl umbenennt oder entfernt,
18
+ * traegt den alten Namen hier ein** -- sonst bleibt er beim Nutzer stehen.
19
+ */
20
+ const VERALTETE_BEFEHLE = [];
21
+ /**
22
+ * Woran wir unsere eigenen Befehlsdateien erkennen.
23
+ *
24
+ * Steht die Marke nicht drin, hat ein Mensch die Datei geschrieben -- dann
25
+ * wird sie NICHT ueberschrieben, sondern uebersprungen und gemeldet.
26
+ */
27
+ const MARKE = "kimal3:erzeugt";
28
+ /**
29
+ * Legt das Skill UND die Slash-Befehle GLOBAL ab, damit Claude dieses Werkzeug
30
+ * ueberall kennt -- und damit der Mensch es auch selbst ausloesen kann.
31
+ *
32
+ * 🔑 **Warum zwei Ziele und nicht eins** (Entscheidung Dennis, 2026-08-18):
33
+ * Das Skill ist der Ausloeser fuer CLAUDE ("das Projekt ist fertig" -> Claude
34
+ * erkennt die Absicht). Die Slash-Befehle sind der Ausloeser fuer den
35
+ * MENSCHEN ("/k3firma …" -> er muss sich keinen Satz merken). Das eine ist
36
+ * wahrscheinlich, das andere sicher. Wer nur das Skill installiert, laesst den
37
+ * sicheren Weg weg.
7
38
  *
8
39
  * Der Pfad zur eigenen Datei kommt ueber `import.meta.url` und NICHT ueber
9
40
  * `process.cwd()` — die Quelle liegt im installierten Paket, das Ziel im
10
41
  * Ordner des Menschen. Wer beides verwechselt, kopiert eine Datei auf sich
11
42
  * selbst.
12
43
  *
13
- * `dist/befehle/skill-install.js` -> zwei Ebenen hoch -> `skill/SKILL.md`.
44
+ * `dist/befehle/skill-install.js` -> zwei Ebenen hoch -> `skill/…`.
14
45
  */
15
46
  export async function skillInstall() {
16
47
  const quelle = fileURLToPath(new URL("../../skill/SKILL.md", import.meta.url));
17
48
  if (!existsSync(quelle)) {
18
- console.error("Das Skill liegt nicht im Paket. Bitte kimal3 neu installieren.");
19
- return 3;
49
+ /*
50
+ 🔴 **`1`, nicht `3`** (Review-Befund, 2026-08-18). Ein unvollstaendiges
51
+ Paket wird durch Wiederholen nie vollstaendig. `3` heisst laut SKILL.md
52
+ ausdruecklich "einmal wiederholen" -- Claude liest die ZAHL, nicht den
53
+ Text, und wuerde einen Rat geben, der garantiert nicht hilft. Genau der
54
+ Fehlertyp, den dieser Zweig bei B-5, B-7 und L-7 behoben hat.
55
+ */
56
+ console.error("Das Skill liegt nicht im Paket. Bitte kimal3 neu installieren:");
57
+ console.error(" npm install -g kimal3");
58
+ return 1;
20
59
  }
21
60
  // 🔑 GLOBAL, nicht projektlokal (Fund von Dennis, 2026-08-13).
22
61
  // Gemessen: In ~/.claude/skills liegen 34 Skills, im Projekt selbst kein
@@ -39,6 +78,232 @@ export async function skillInstall() {
39
78
  }
40
79
  console.log(gabEsSchon ? "✓ Skill aktualisiert." : "✓ Skill angelegt.");
41
80
  console.log(` ${ziel}`);
42
- console.log(" Claude kennt das Werkzeug ab der nächsten Sitzung.");
81
+ const namen = slashBefehleInstallieren();
82
+ if (namen === "kaputt") {
83
+ /*
84
+ ⚠️ **Was hier zaehlt, ist die TEILWIRKUNG** (Review-Runde 2, 2026-08-18):
85
+ Das Skill steht oben bereits als angelegt gemeldet. Ohne diesen Satz
86
+ liest der Mensch ein "✓" und danach einen Fehler und weiss nicht, was
87
+ nun gilt. *Ein halber Erfolg ist weder ein Erfolg noch ein Totalausfall
88
+ -- er ist ein eigener Zustand und braucht einen eigenen Satz.*
89
+ */
90
+ console.error(" Das Skill ist installiert, die Slash-Befehle sind es NICHT.");
91
+ return 1;
92
+ }
93
+ const { geschrieben, uebersprungen, unlesbar, gestoert } = namen;
94
+ if (geschrieben.length > 0) {
95
+ console.log(`✓ ${geschrieben.length} Slash-Befehle: ${geschrieben.map((n) => `/${n}`).join(" · ")}`);
96
+ console.log(` ${join(homedir(), ".claude", "commands")}`);
97
+ }
98
+ // ⚠️ Uebersprungene Dateien gehoeren in die AUSGABE, nicht nur in die Logik.
99
+ // Ein Mensch, dem stillschweigend ein Befehl fehlt, sucht den Fehler bei
100
+ // sich. (Review-Runde 2, 2026-08-18.)
101
+ if (uebersprungen.length > 0) {
102
+ console.log(`⚠ ${uebersprungen.length} übersprungen: ${uebersprungen.map((n) => `/${n}`).join(" · ")}`);
103
+ console.log(" Dort liegt bereits eine Datei, die nicht von kimal3 stammt —");
104
+ console.log(" sie bleibt unangetastet. Wer die kimal3-Fassung will, benennt");
105
+ console.log(" die eigene um und führt den Befehl erneut aus.");
106
+ }
107
+ // 🔴 Ein LESEFEHLER bekommt einen eigenen Satz (Review-Runde 6): Er sieht
108
+ // aus wie Fremdbesitz, ist aber keiner -- und der Rat "benenn deine Datei
109
+ // um" waere hier falsch.
110
+ if (unlesbar.length > 0) {
111
+ console.log(`⚠ ${unlesbar.length} nicht lesbar: ${unlesbar.map((n) => `/${n}`).join(" · ")}`);
112
+ console.log(" Diese Dateien liessen sich nicht öffnen — vielleicht sind sie");
113
+ console.log(" gerade in Benutzung. Sie wurden nicht angefasst; ein zweiter");
114
+ console.log(" Versuch gleich kann klappen.");
115
+ }
116
+ /*
117
+ 🔴 **Kein Slash-Befehl installiert ist kein Erfolg** (Review-Runde 5,
118
+ 2026-08-18). Liegen im Ordner lauter fremde `k3*.md`, landet ALLES in
119
+ `uebersprungen`, `geschrieben` ist leer -- und der Lauf endete trotzdem
120
+ auf `0`. Laut der eigenen Tabelle in `SKILL.md` heisst `0` "erledigt".
121
+
122
+ *Ein Fehlerfall darf nie denselben Namen tragen wie ein Erfolgsfall.*
123
+
124
+ ⚠️ Die Zahl haengt am GRUND (Review-Runde 6): War der Ordner nur belegt,
125
+ ist der Zustand stabil -> `1`. Ging das Schreiben schief oder war eine
126
+ Datei nicht lesbar, kann ein zweiter Versuch helfen -> `3`.
127
+ */
128
+ if (geschrieben.length === 0) {
129
+ const wiederholbar = gestoert || unlesbar.length > 0;
130
+ console.error(" Es wurde KEIN Slash-Befehl installiert.");
131
+ console.error(" Das Skill selbst ist installiert und funktioniert.");
132
+ return wiederholbar ? 3 : 1;
133
+ }
134
+ /*
135
+ ⚠️ **Eine unvollstaendige Installation als solche melden** (Review-Runde 6).
136
+ Scheitert `copyFileSync` bei Datei 3 von 4, sind zwei Befehle da -- der
137
+ Lauf ist weder gelungen noch gescheitert. Frueher warf der `catch` die
138
+ Teilwirkung weg und meldete alles als misslungen.
139
+ */
140
+ if (gestoert) {
141
+ console.error(" Nicht alle Slash-Befehle konnten geschrieben werden.");
142
+ console.error(" Die oben genannten stehen; ein zweiter Lauf holt den Rest.");
143
+ return 3;
144
+ }
145
+ /*
146
+ ⚠️ **Nicht auf einen Befehl verweisen, den wir gerade NICHT installiert
147
+ haben** (Review-Runde 3, 2026-08-18, gemessen). Mit einer fremden
148
+ `k3.md` im Ordner stand hier woertlich "Tipp /k3 in Claude Code" --
149
+ ausgerechnet der eine Befehl, den der Lauf soeben abgelehnt hatte.
150
+ *Ein Hinweis auf etwas, das es nicht gibt, sieht aus wie ein Fehler des
151
+ Menschen.*
152
+ */
153
+ if (geschrieben.includes("k3")) {
154
+ console.log(" Tipp /k3 in Claude Code. (Taucht nichts auf, einmal neu starten.)");
155
+ }
156
+ else {
157
+ console.log(` Tipp /${geschrieben[0]} in Claude Code. (Taucht nichts auf, einmal neu starten.)`);
158
+ }
43
159
  return 0;
44
160
  }
161
+ /**
162
+ * Kopiert die Slash-Befehle nach `~/.claude/commands/`.
163
+ *
164
+ * ⚠️ **FLACH, nicht in einen Unterordner** — und das ist keine Bequemlichkeit:
165
+ * Ein Unterordner `kimal3/` macht aus `/k3firma` ein `/kimal3:k3firma`. Dann
166
+ * findet man die Familie nicht mehr, indem man `/k3` tippt, und genau das war
167
+ * die Anforderung (Dennis, 2026-08-18): einmal `/k3` eingeben und alles sehen.
168
+ * Der gemeinsame Namensanfang ERSETZT hier den Ordner.
169
+ *
170
+ * Rueckgabe: die Namen ohne `.md` — oder `"kaputt"` (Paket unvollstaendig,
171
+ * Wiederholen hilft NIE) bzw. `"stoerung"` (Schreiben ging schief, Wiederholen
172
+ * kann helfen). *Zwei Fehlerfaelle, die Gegenteiliges verlangen, duerfen nicht
173
+ * denselben Namen tragen.*
174
+ */
175
+ function slashBefehleInstallieren() {
176
+ const quellOrdner = fileURLToPath(new URL("../../skill/commands", import.meta.url));
177
+ if (!existsSync(quellOrdner)) {
178
+ console.error("Die Slash-Befehle liegen nicht im Paket. Bitte kimal3 neu installieren.");
179
+ return "kaputt";
180
+ }
181
+ const dateien = readdirSync(quellOrdner).filter((n) => n.endsWith(".md")).sort();
182
+ if (dateien.length === 0) {
183
+ console.error("Im Paket liegt kein einziger Slash-Befehl. Bitte kimal3 neu installieren.");
184
+ return "kaputt";
185
+ }
186
+ const zielOrdner = join(homedir(), ".claude", "commands");
187
+ const geschrieben = [];
188
+ const uebersprungen = [];
189
+ const unlesbar = [];
190
+ let gestoert = false;
191
+ try {
192
+ mkdirSync(zielOrdner, { recursive: true });
193
+ for (const datei of dateien) {
194
+ const ziel = join(zielOrdner, datei);
195
+ /*
196
+ 🔴 **Fremde Dateien NICHT ueberschreiben** (Review-Runde 2, 2026-08-18).
197
+ Der Ordner gehoert dem Menschen. Der Kommentar bei VERALTETE_BEFEHLE
198
+ argumentiert, dass wir eine selbstgeschriebene `k3*.md` nicht loeschen
199
+ duerfen -- *Ueberschreiben zerstoert sie genauso gruendlich*, nur
200
+ unauffaelliger.
201
+
202
+ Woran wir unsere eigenen erkennen: an der Marke `kimal3:erzeugt`. Ein
203
+ blosser Inhaltsvergleich taugt nicht -- eine AELTERE Fassung von uns
204
+ unterscheidet sich auch, und die soll ja gerade ersetzt werden.
205
+ */
206
+ if (existsSync(ziel)) {
207
+ const vorhanden = leseWennMoeglich(ziel);
208
+ /*
209
+ ⚠️ **Ein LESEFEHLER ist kein Fremdbesitz** (Review-Runde 6,
210
+ 2026-08-18). Vorher wurde ein gescheitertes `readFileSync` zu einem
211
+ leeren Text -- dann fehlt die Marke zwangslaeufig, und der Mensch las
212
+ "dort liegt eine Datei, die nicht von kimal3 stammt" fuer eine Datei,
213
+ die kimal3 selbst geschrieben hat. *Ein Fehlerfall unter dem Namen
214
+ eines anderen, zum vierten Mal in diesem Zweig.*
215
+ */
216
+ if (vorhanden === null) {
217
+ unlesbar.push(datei);
218
+ continue;
219
+ }
220
+ if (!vorhanden.includes(MARKE)) {
221
+ uebersprungen.push(datei);
222
+ continue;
223
+ }
224
+ }
225
+ copyFileSync(join(quellOrdner, datei), ziel);
226
+ geschrieben.push(datei);
227
+ }
228
+ }
229
+ catch (fehler) {
230
+ /*
231
+ ⚠️ **Die Teilwirkung ueberlebt den Fehler** (Review-Runde 6, 2026-08-18).
232
+ Vorher warf dieser `catch` alles weg und meldete "stoerung" -- auch wenn
233
+ zwei von vier Befehlen bereits geschrieben waren. Der Mensch hoerte "die
234
+ Slash-Befehle sind NICHT installiert", waehrend die Haelfte dastand.
235
+ `geschrieben` und `uebersprungen` sind ausserhalb des `try` deklariert,
236
+ genau dafuer.
237
+ */
238
+ console.error(`Die Slash-Befehle liessen sich nicht alle schreiben: ${String(fehler)}`);
239
+ gestoert = true;
240
+ }
241
+ raeumeVeralteteAuf(zielOrdner, VERALTETE_BEFEHLE);
242
+ return {
243
+ geschrieben: geschrieben.map((n) => n.replace(/\.md$/, "")),
244
+ uebersprungen: uebersprungen.map((n) => n.replace(/\.md$/, "")),
245
+ unlesbar: unlesbar.map((n) => n.replace(/\.md$/, "")),
246
+ gestoert,
247
+ };
248
+ }
249
+ /** Liest eine Datei — `null`, wenn sie sich nicht lesen laesst. */
250
+ function leseWennMoeglich(pfad) {
251
+ try {
252
+ return readFileSync(pfad, "utf8");
253
+ }
254
+ catch {
255
+ return null;
256
+ }
257
+ }
258
+ /**
259
+ * Entfernt Befehlsdateien, die frueher einmal ausgeliefert wurden.
260
+ *
261
+ * 🔴 **Warum das eine eigene, exportierte Funktion ist** (Review-Runde 3,
262
+ * 2026-08-18): `VERALTETE_BEFEHLE` ist leer, also ist diese Schleife noch nie
263
+ * gelaufen -- und `check:cli` "belegte" sie nur ueber eine Textsuche. Genau
264
+ * die Falle aus der Projekt-Notiz *wache-am-echten-fehlerfall-pruefen*.
265
+ * Mit dem Zielordner als Parameter laesst sie sich gegen einen Wegwerf-Ordner
266
+ * mit einer echten Datei fahren.
267
+ *
268
+ * 🔴 **Und sie loescht NUR eigene Dateien** (Review-Runde 6, 2026-08-18).
269
+ * Vorher entfernte sie blind, was auf der Liste stand -- waehrend die
270
+ * Kopierschleife siebzig Zeilen hoeher genau solche Dateien verschont und dem
271
+ * Menschen sagt "sie bleibt unangetastet". *Zwei Haelften desselben Befehls
272
+ * mit gegensaetzlicher Haltung zum selben Ordner.* Wird `/k3liste` spaeter
273
+ * umbenannt, verloere ein Mensch mit eigener `k3liste.md` sie ohne Rueckfrage.
274
+ *
275
+ * Gibt zurueck, was wirklich entfernt wurde -- eine Behauptung waere hier
276
+ * genau das Falsche.
277
+ */
278
+ export function raeumeVeralteteAuf(zielOrdner, veraltet) {
279
+ const entfernt = [];
280
+ for (const eintrag of veraltet) {
281
+ /*
282
+ 🔴 **Beide Schreibweisen annehmen** (Review-Runde 5, 2026-08-18). Der
283
+ Doc-Block sagt "wer einen Befehl umbenennt, traegt den alten NAMEN hier
284
+ ein" -- gejoint wird aber ein DATEIname. Wer `"k3alt"` eintraegt, bekam
285
+ keinen Fehler: nichts wird entfernt, nichts gemeldet.
286
+ */
287
+ const alt = eintrag.endsWith(".md") ? eintrag : `${eintrag}.md`;
288
+ const weg = join(zielOrdner, alt);
289
+ try {
290
+ if (!existsSync(weg))
291
+ continue;
292
+ const inhalt = leseWennMoeglich(weg);
293
+ if (inhalt === null || !inhalt.includes(MARKE)) {
294
+ // Nicht von uns (oder nicht lesbar) -> nicht anfassen. Still, weil es
295
+ // der Normalfall ist, dass dort gar nichts liegt.
296
+ continue;
297
+ }
298
+ rmSync(weg);
299
+ entfernt.push(alt);
300
+ console.log(` (veralteten Befehl /${alt.replace(/\.md$/, "")} entfernt)`);
301
+ }
302
+ catch {
303
+ // Bewusst still: Die Befehle sind installiert. Eine liegengebliebene
304
+ // alte Datei ist ein Schoenheitsfehler, kein Grund, den Erfolg zu
305
+ // widerrufen. (Review-Runde 2.)
306
+ }
307
+ }
308
+ return entfernt;
309
+ }
@@ -0,0 +1,112 @@
1
+ /**
2
+ * Alle Befehle, die `kimal3` kennt — als DATEN, nicht als Fliesstext.
3
+ *
4
+ * 🔑 Warum das eine eigene Datei ist (Spec US-5, 2026-08-18): Die Liste wird an
5
+ * ZWEI Orten angezeigt — in der Terminal-Hilfe und im aktiven Pro-Bereich der
6
+ * App. Zwei handgeschriebene Listen sind die "dieselbe Zahl an zwei Tueren"-
7
+ * Falle: Der naechste Befehl erschiene in einer und fehlte in der anderen.
8
+ * Genau so war die `check:`-Prueferliste in CLAUDE.md monatelang unvollstaendig.
9
+ *
10
+ * ⚠️ DIESE DATEI IMPORTIERT NICHTS, und das ist Absicht: Die CLI baut mit
11
+ * NodeNext (Import-Pfade enden auf `.js`), die App mit dem Bundler. Eine Datei
12
+ * ohne eigene Importe liest sich aus beiden Welten ohne Uebersetzung.
13
+ *
14
+ * ⚠️ Der Katalog beschreibt, WAS ES GIBT — nicht, wer es darf. Die Pro-Schranke
15
+ * sitzt in der Route und im Befehl, nicht hier.
16
+ */
17
+ export const BEFEHLE = [
18
+ {
19
+ befehl: "kimal3 login",
20
+ beschreibung: "Diesen Rechner mit deinem Konto verbinden",
21
+ wer: "du",
22
+ rhythmus: "einmal pro Rechner",
23
+ beispiel: "kimal3 login",
24
+ details: [
25
+ "Der Befehl zeigt dir eine Adresse und einen kurzen Code. Die Adresse im Browser öffnen, den Code eintippen, fertig — das Terminal merkt die Bestätigung von selbst. Zehn Minuten hast du dafür Zeit.",
26
+ "Danach musst du dich NICHT bei jedem Start neu anmelden: Der Ausweis liegt als Datei in deinem Benutzerordner und übersteht IDE-Neustarts, Rechner-Neustarts und Updates. Bei jedem Aufruf wird er im Hintergrund aufgefrischt, ohne dass du etwas merkst.",
27
+ "Gelegentlich reißt diese Kette trotzdem — dann sagt ein Befehl „Bitte kimal3 login ausführen\". Das ist kein Fehler, sondern der Normalfall nach längerer Pause: einmal wiederholen und weitermachen.",
28
+ ],
29
+ },
30
+ {
31
+ befehl: "kimal3 logout",
32
+ beschreibung: "Verbindung wieder aufheben",
33
+ wer: "du",
34
+ rhythmus: "nur wenn nötig",
35
+ beispiel: "kimal3 logout",
36
+ details: [
37
+ "Hebt die Verbindung dieses einen Rechners auf. Für den Alltag brauchst du ihn nicht.",
38
+ "Sinnvoll ist er, wenn du auf einem fremden oder geteilten Rechner gearbeitet hast — oder wenn ein Gerät abhandenkommt. Deine anderen Rechner bleiben angemeldet.",
39
+ ],
40
+ },
41
+ {
42
+ befehl: "kimal3 projekt neu …",
43
+ beschreibung: "Ein Projekt ins Erfolgsprofil eintragen",
44
+ wer: "claude",
45
+ ausloeser: "trag das als Projekt ein",
46
+ beispiel: 'kimal3 projekt neu --titel "Angebots-Generator" --stunden 4 --ergebnis 1200',
47
+ details: [
48
+ "Wenn du in Claude etwas fertig gebaut hast und sagst „trag das als Projekt ein\", stellt Claude den Befehl selbst zusammen — mit Titel, Zeitersparnis, Ergebnis und allem, was ihr gerade besprochen habt. Das ist der bequeme Weg, weil Claude die Zahlen schon kennt.",
49
+ "Selbst tippen kannst du ihn genauso. Pflicht ist nur --titel; alles andere ist freiwillig. Die vollständige Liste aller Angaben zeigt: kimal3 projekt neu --help",
50
+ "Das Projekt landet in deinem Erfolgsprofil und ist damit Teil deines nachprüfbaren Nachweises.",
51
+ ],
52
+ },
53
+ {
54
+ befehl: "kimal3 projekt liste",
55
+ beschreibung: "Deine eingetragenen Projekte anzeigen",
56
+ wer: "claude",
57
+ ausloeser: "zeig meine Projekte",
58
+ beispiel: "kimal3 projekt liste",
59
+ details: [
60
+ "Zeigt, was schon in deinem Erfolgsprofil steht.",
61
+ "Wofür es wirklich da ist: Claude schaut hier VOR dem Eintragen nach. Ohne diesen Blick stünde dasselbe Projekt irgendwann zweimal auf einer öffentlichen Seite, die deinen Namen trägt.",
62
+ ],
63
+ },
64
+ {
65
+ befehl: "kimal3 firma daten",
66
+ beschreibung: "Die Daten deiner Firma holen, damit Claude sie auswerten kann",
67
+ wer: "claude",
68
+ ausloeser: "werte meine Firma aus",
69
+ beispiel: 'kimal3 firma daten --firma "Nordwind Handel GmbH"',
70
+ details: [
71
+ "Holt Kennzahlen, Menschen und Projekte deiner Firma und gibt sie an Claude weiter. Die Auswertung macht dann Claude — frag einfach los: „Welche drei Projekte bringen den meisten messbaren Ertrag?\" oder „Was sollten wir als Nächstes automatisieren?\"",
72
+ "Gehörst du zu mehreren Firmen, fragt der Befehl nach — er wählt nie still eine aus.",
73
+ "⚠️ Es sind personenbezogene Daten von Kolleginnen und Kollegen. Für den Einsatz in einer echten Firma braucht es ein kommerzielles Claude-Konto mit Auftragsverarbeitungsvertrag; werden Leistungen von Beschäftigten ausgewertet, ist das in Deutschland mitbestimmungspflichtig.",
74
+ ],
75
+ },
76
+ {
77
+ befehl: "kimal3 skill install",
78
+ beschreibung: "Claude beibringen, dieses Werkzeug zu benutzen — und dir die /k3-Befehle geben",
79
+ wer: "du",
80
+ rhythmus: "einmal — und nach jedem Update",
81
+ beispiel: "kimal3 skill install",
82
+ details: [
83
+ "Das ist der Befehl, der aus den anderen erst etwas macht. Er legt ZWEI Dinge in deinem Claude-Ordner ab: eine Anleitung für Claude (das „Skill\") und vier Slash-Befehle für dich.",
84
+ "🤖 Die Anleitung sorgt dafür, dass Claude von selbst weiß, dass es kimal3 gibt — du sagst „das Projekt ist fertig\", er trägt es ein.",
85
+ "🧑 Die Slash-Befehle sind dein eigener Griff: Tipp /k3 in Claude Code, und du siehst die ganze Familie — /k3 · /k3projekt · /k3liste · /k3firma. Damit musst du dir keinen Satz merken.",
86
+ "Er läuft NICHT automatisch. Einmal nach der Installation ausführen — und noch einmal nach jedem Update des Werkzeugs, sonst arbeitet Claude weiter mit der alten Anleitung.",
87
+ "Woran du merkst, dass es geklappt hat: Der Befehl meldet „Skill angelegt\" (oder „aktualisiert\") und zählt die Slash-Befehle auf. Beides kennt Claude ab der NÄCHSTEN Sitzung — die laufende muss also einmal neu gestartet werden.",
88
+ ],
89
+ },
90
+ ];
91
+ /**
92
+ * Die Terminal-Hilfe, gebaut aus dem Katalog.
93
+ *
94
+ * Die Spalten richten sich an der laengsten Befehlszeile aus — von Hand
95
+ * gesetzte Leerzeichen waeren beim naechsten laengeren Befehl krumm.
96
+ *
97
+ * 📌 Sie nimmt bewusst NUR `befehl` und `beschreibung`: Im Terminal ist eine
98
+ * Uebersicht gefragt, kein Handbuch. Die langen `details` haben ihren Platz auf
99
+ * `/pro`, wo man sie auf- und zuklappen kann — `--help` je Befehl bleibt der
100
+ * Weg im Terminal.
101
+ */
102
+ export function hilfeText() {
103
+ const breite = Math.max(...BEFEHLE.map((b) => b.befehl.length));
104
+ const zeilen = BEFEHLE.map((b) => ` ${b.befehl.padEnd(breite + 2)}${b.beschreibung}`).join("\n");
105
+ return `
106
+ kimal3 — trag deine KI-Projekte dort ein, wo sie entstehen.
107
+
108
+ ${zeilen}
109
+
110
+ Mehr zu einem Befehl: kimal3 projekt neu --help
111
+ `;
112
+ }
package/dist/fehler.js ADDED
@@ -0,0 +1,66 @@
1
+ /**
2
+ * Der gemeinsame Schluss aller Befehle: Was tun mit einer Absage, die kein
3
+ * Befehl fuer sich beantworten kann?
4
+ *
5
+ * 🔑 **Warum das hier wohnt und nicht in `api.ts`:** Es ist eine reine
6
+ * Rechnung -- Zahl und Text rein, Zahl und Text raus, kein Netz, keine Datei.
7
+ * Damit ist sie in `check:cli` messbar, ohne einen Server zu starten. `api.ts`
8
+ * zieht ueber `anmeldung.ts` das Dateisystem nach; wer von dort importiert,
9
+ * kann die Rechnung nicht mehr allein pruefen.
10
+ */
11
+ /**
12
+ * ⚠️ **404 ist KEIN Eingabefehler** (QA-Befund B-7, 2026-08-18).
13
+ *
14
+ * Vorher endeten alle drei Befehle auf `status >= 500 ? 3 : 1`. Eine `404`
15
+ * fiel damit auf die `1` -- und `1` heisst laut `skill/SKILL.md` ausdruecklich
16
+ * *"Eingabe passt nicht"*. Antwortet die App aber mit `404`, war die Eingabe
17
+ * fehlerfrei: Die **App** kennt den Befehl noch nicht. Claude folgte der
18
+ * Betriebsanleitung und erklaerte dem Menschen, seine Eingabe stimme nicht --
19
+ * woraufhin der Firmenname, Schreibweise und `--firma` prueft und nichts davon
20
+ * hilft.
21
+ *
22
+ * 🔴 **Gemessen am 2026-08-18:** `/api/cli/firma` antwortete auf
23
+ * `kimal3-app.vercel.app` mit `404`, waehrend `/api/cli/projekt` mit `200`
24
+ * antwortete -- der Befehl war veroeffentlicht, die Route noch nicht
25
+ * ausgeliefert. Genau so sieht der Fall im Alltag aus: im Fenster zwischen
26
+ * `npm publish` und fertigem Deployment, bei einem Rueckbau, und bei jedem
27
+ * neuen Befehl.
28
+ *
29
+ * *Ein Fehlerfall darf nie denselben Namen tragen wie ein anderer, der
30
+ * Gegenteiliges verlangt.* Deshalb bekommt die `404` den `3`-Topf: dort steht
31
+ * bereits "nochmal versuchen kann helfen" -- und ein paar Minuten spaeter ist
32
+ * das Deployment durch.
33
+ */
34
+ export function schlussFehler(status, meldung) {
35
+ if (status === 404) {
36
+ return {
37
+ text: "Diese Version der App kennt den Befehl noch nicht. " +
38
+ "Hast du gerade aktualisiert, versuch es in ein paar Minuten noch einmal.",
39
+ wert: 3,
40
+ };
41
+ }
42
+ /*
43
+ 🔴 **`408` und `429` gehoeren AUCH in den 3-Topf** (Review-Runde 3,
44
+ 2026-08-18). Vorher fiel alles unter 500 auf die `1` -- und die heisst seit
45
+ Runde 2 ausdruecklich "Wiederholen hilft nicht".
46
+
47
+ Beide Zahlen sagen aber genau das Gegenteil: `429` schickt eine
48
+ Vercel-Firewall oder ein Supabase-Rate-Limit, `408` ist eine
49
+ Zeitueberschreitung. Bei beiden ist **Warten die Loesung**, und bei beiden
50
+ kommt der Rumpf oft als HTML -- dann steht dort nicht einmal eine Meldung,
51
+ sondern nur "Das hat nicht geklappt.".
52
+
53
+ *Genau die Form von B-7, nur mit einer anderen Zahl:* Der Mensch hat nichts
54
+ falsch gemacht, das Werkzeug rät ihm trotzdem, es sein zu lassen.
55
+ */
56
+ if (status === 408 || status === 429) {
57
+ return {
58
+ text: "Der Server ist gerade überlastet oder hat zu lange gebraucht. " +
59
+ "Versuch es in einem Moment noch einmal.",
60
+ wert: 3,
61
+ };
62
+ }
63
+ // Alles Uebrige: ein Aussetzer des Servers ist wiederholbar, alles andere
64
+ // ist die Absage, die der Server selbst formuliert hat.
65
+ return { text: meldung, wert: status >= 500 ? 3 : 1 };
66
+ }
@@ -0,0 +1,70 @@
1
+ /**
2
+ * Die Firmen-Auswahl, die eine Absage dem Menschen anbietet.
3
+ *
4
+ * 🔑 Eine reine Rechnung, damit `check:cli` sie ohne Server pruefen kann --
5
+ * dieselbe Ueberlegung wie bei `fehler.ts`.
6
+ */
7
+ /**
8
+ * Aus den Namen der eigenen Firmen die Zeilen bauen, die unter „Deine Firmen:"
9
+ * stehen.
10
+ *
11
+ * ⚠️ **Ein doppelter Name bekommt KEINEN Befehl** (QA-Befund B-5, 2026-08-18).
12
+ * Vorher wurde jeder Name stumpf zu einer kopierfertigen Zeile -- bei zwei
13
+ * gleichnamigen Firmen also **zweimal derselbe Befehl**. Wer ihn ausfuehrte,
14
+ * bekam dieselbe Absage, die erneut denselben Befehl anbot: eine Sackgasse
15
+ * ohne Ausweg.
16
+ *
17
+ * *Ein Vorschlag, der garantiert wieder scheitert, ist schlimmer als gar
18
+ * keiner -- er sieht aus wie ein Ausweg und kostet den Menschen den Versuch.*
19
+ *
20
+ * 🔑 **Nur der mehrdeutige Name verliert seinen Befehl, nicht die ganze
21
+ * Liste.** Wer zu „Nordwind", „Nordwind" und „Suedwind" gehoert, kann
22
+ * „Suedwind" weiterhin waehlen. Dieselbe Lehre steckt schon in der Route
23
+ * (Review-Befund Runde 2): *Eine Sperre, die mehr sperrt als das Mehrdeutige,
24
+ * verlangt vom Menschen etwas, das er nicht tun kann.*
25
+ *
26
+ * ⚠️ Was hier NICHT geloest wird: An die Daten einer doppelt benannten Firma
27
+ * kommt man ueber die CLI weiterhin nicht. Das braeuchte eine Auswahl ueber
28
+ * die ID statt ueber den Namen -- eine eigene Entscheidung, keine Fussnote.
29
+ * Diese Funktion sorgt nur dafuer, dass niemand mehr einen Ausweg vorgespielt
30
+ * bekommt, den es nicht gibt.
31
+ */
32
+ export function firmenZeilen(namen) {
33
+ const anzahl = (name) => namen.filter((n) => n === name).length;
34
+ const gesehen = new Set();
35
+ const zeilen = [];
36
+ for (const name of namen) {
37
+ if (gesehen.has(name))
38
+ continue;
39
+ gesehen.add(name);
40
+ const wieOft = anzahl(name);
41
+ if (wieOft === 1) {
42
+ // Fertiger Befehl statt blosser Liste: Der naechste Schritt ist dann
43
+ // Kopieren statt Abtippen -- und ein Name mit Leerzeichen braucht
44
+ // Anfuehrungszeichen, an die sonst niemand denkt.
45
+ zeilen.push(` kimal3 firma daten --firma "${name}"`);
46
+ }
47
+ else {
48
+ zeilen.push(` „${name}" — kommt ${wieOft}× vor und ist über den Namen nicht wählbar.`);
49
+ }
50
+ }
51
+ return zeilen;
52
+ }
53
+ /**
54
+ * Die Zeilen für den Fall, dass zwei Firmen **gleich heissen**.
55
+ *
56
+ * 🔑 **Das ist die zweite Haelfte von B-5** (2026-08-18 spaet). Die erste
57
+ * Haelfte nahm dem doppelten Namen seinen Befehl, damit niemand einen Ausweg
58
+ * vorgespielt bekommt, den es nicht gibt. Damit war die Irrefuehrung weg --
59
+ * aber an die Daten kam man immer noch nicht. Die Absage sagte „benenne eine
60
+ * davon um", und das setzt **Admin-Rechte in einer fremden Firma** voraus.
61
+ *
62
+ * *Eine Absage ohne Ausweg ist eine Sackgasse mit Begruendung.*
63
+ *
64
+ * Jetzt liefert die Route die IDs mit, und hier entstehen daraus wieder
65
+ * kopierfertige Befehle. Der Name steht als Kommentar dahinter -- eine nackte
66
+ * UUID sagt einem Menschen nichts, und zwei davon untereinander noch weniger.
67
+ */
68
+ export function firmenIdZeilen(auswahl) {
69
+ return auswahl.map((f, i) => ` kimal3 firma daten --firma-id "${f.id}" # ${f.name} (${i + 1}.)`);
70
+ }
package/dist/kimal3.js CHANGED
@@ -11,24 +11,21 @@
11
11
  * `kimal3 login`, bei 4 hilft genau das nicht. Wer beide auf dieselbe Zahl
12
12
  * legt, laesst Claude den falschen Rat geben.
13
13
  */
14
- import { leseAnmeldung } from "./anmeldung.js";
14
+ import { readFileSync } from "node:fs";
15
+ import { BASIS_URL, leseAnmeldung } from "./anmeldung.js";
15
16
  import { NichtAngemeldet } from "./api.js";
17
+ import { hilfeText } from "./befehle-katalog.js";
18
+ import { firmaDaten } from "./befehle/firma-daten.js";
16
19
  import { login } from "./befehle/login.js";
17
20
  import { logout } from "./befehle/logout.js";
18
21
  import { projektListe } from "./befehle/projekt-liste.js";
19
22
  import { projektNeu } from "./befehle/projekt-neu.js";
20
23
  import { skillInstall } from "./befehle/skill-install.js";
21
- const HILFE = `
22
- kimal3 — trag deine KI-Projekte dort ein, wo sie entstehen.
23
-
24
- kimal3 login Diesen Rechner mit deinem Konto verbinden
25
- kimal3 logout Verbindung wieder aufheben
26
- kimal3 projekt neu … Ein Projekt ins Erfolgsprofil eintragen
27
- kimal3 projekt liste Deine eingetragenen Projekte anzeigen
28
- kimal3 skill install Claude beibringen, dieses Werkzeug zu benutzen
29
-
30
- Mehr zu einem Befehl: kimal3 projekt neu --help
31
- `;
24
+ // 📌 Kein handgeschriebener Block mehr (2026-08-18): Die Liste wohnt in
25
+ // `befehle-katalog.ts` und wird von dort AUCH im Pro-Bereich der App angezeigt.
26
+ // Zwei Listen ueber dieselben Befehle driften — die eine bekaeme den naechsten
27
+ // Befehl, die andere nicht, und keiner der beiden Orte wuesste davon.
28
+ const HILFE = hilfeText();
32
29
  /**
33
30
  * Hilfe plus Zustand.
34
31
  *
@@ -42,10 +39,60 @@ const HILFE = `
42
39
  * ⚠️ Und KEIN Netzaufruf: Wer nur die Hilfe sehen will, soll nicht auf einen
43
40
  * Server warten. Ob die Anmeldung noch gilt, klaert der naechste echte Befehl.
44
41
  */
42
+ /**
43
+ * Welche Version bin ich — und mit wem rede ich?
44
+ *
45
+ * ⚠️ **Die Zahl wird GELESEN, nicht abgetippt** (QA-Befund B-8, 2026-08-18).
46
+ * Stuende sie hier als Text, sagte die CLI nach dem naechsten `npm version`
47
+ * die alte Nummer -- dieselbe Zahl an zwei Tueren mit zwei Werten, genau das
48
+ * Muster, das in diesem Projekt schon zweimal zugeschlagen hat.
49
+ *
50
+ * 🔑 **Warum die Gegenstelle mit dazugehoert:** Der Fall, fuer den man diese
51
+ * Auskunft braucht, ist ein **Versatz** zwischen CLI und App (B-7). Dann
52
+ * helfen erst beide Angaben zusammen: welche CLI laeuft -- und gegen welche
53
+ * App sie spricht. Eine Version allein beantwortet die Frage nur halb.
54
+ *
55
+ * 📌 `../package.json` stimmt aus `dist/` **und** aus `src/`: Beide liegen
56
+ * eine Ebene unter `cli/`.
57
+ */
58
+ function versionsZeilen() {
59
+ let version = "unbekannt";
60
+ try {
61
+ const roh = JSON.parse(readFileSync(new URL("../package.json", import.meta.url), "utf8"));
62
+ if (roh && typeof roh === "object" && "version" in roh) {
63
+ const v = roh.version;
64
+ if (typeof v === "string")
65
+ version = v;
66
+ }
67
+ }
68
+ catch {
69
+ // Kein Absturz, wenn die Datei fehlt: "unbekannt" ist eine ehrliche
70
+ // Antwort, ein Stapelabzug waere keine.
71
+ }
72
+ return ` kimal3 ${version}\n spricht mit ${BASIS_URL}`;
73
+ }
74
+ /**
75
+ * Was diese Zeile sagen KANN — und was nicht.
76
+ *
77
+ * 🔴 **Sie liest nur die Datei, sie fragt den Server nicht** (Befund L-9,
78
+ * 2026-08-18). Hier stand vorher „● Angemeldet als …" — und genau das hat am
79
+ * selben Abend zehn Minuten Fehlersuche gekostet: Der Ausweis war
80
+ * serverseitig nicht mehr gueltig, die Zeile behauptete trotzdem munter
81
+ * „Angemeldet", und die Suche begann an der falschen Stelle.
82
+ *
83
+ * *Eine Behauptung ist keine Messung.* Der Zustand, den die Datei kennt,
84
+ * heisst „hier liegt ein Ausweis" — ob der Server ihn noch annimmt, weiss
85
+ * allein der Server.
86
+ *
87
+ * ⚠️ Bewusst wird trotzdem NICHT nachgefragt: `--version` soll ohne Netz
88
+ * antworten und darf keine Sekunde haengen. Der ehrliche Weg ist deshalb
89
+ * nicht mehr Auskunft, sondern **weniger Behauptung**.
90
+ */
45
91
  function zustandsZeile() {
46
92
  const a = leseAnmeldung();
47
93
  return a
48
- ? ` ● Angemeldet als ${a.email}`
94
+ ? ` ● Ausweis hinterlegt für ${a.email}\n` +
95
+ " (ob er noch gilt, zeigt erst der nächste Befehl)"
49
96
  : " ● Nicht angemeldet. → kimal3 login";
50
97
  }
51
98
  async function haupt() {
@@ -56,6 +103,11 @@ async function haupt() {
56
103
  console.log(zustandsZeile() + "\n");
57
104
  return 0;
58
105
  }
106
+ if (wort === "--version" || wort === "-v" || wort === "version") {
107
+ console.log(versionsZeilen());
108
+ console.log(zustandsZeile() + "\n");
109
+ return 0;
110
+ }
59
111
  if (wort === "login")
60
112
  return login();
61
113
  if (wort === "logout")
@@ -64,6 +116,11 @@ async function haupt() {
64
116
  return projektNeu(argv.slice(2));
65
117
  if (wort === "projekt liste")
66
118
  return projektListe();
119
+ // ⚠️ Praefix-Vergleich wie bei `projekt neu` und NICHT `wort === "firma daten"`:
120
+ // `wort` enthaelt die Optionen mit, `firma daten --firma X` fiele bei einem
121
+ // Gleichheits-Vergleich durch und meldete "Den Befehl kenne ich nicht".
122
+ if (argv[0] === "firma" && argv[1] === "daten")
123
+ return firmaDaten(argv.slice(2));
67
124
  if (wort === "skill install")
68
125
  return skillInstall();
69
126
  console.error(`Den Befehl „${wort}" kenne ich nicht.\n${HILFE}`);