kimal3 0.4.0-durchstich.1 → 0.4.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.
Files changed (39) hide show
  1. package/README.md +2 -0
  2. package/claude-plugins/.claude-plugin/marketplace.json +8 -0
  3. package/claude-plugins/README.md +7 -0
  4. package/claude-plugins/kimal3-logo/.claude-plugin/plugin.json +6 -0
  5. package/claude-plugins/kimal3-logo/hooks/hooks.json +1 -0
  6. package/claude-plugins/kimal3-logo/hooks/register.tsx +80 -0
  7. package/coach/de/kimal3-coach.md +167 -0
  8. package/coach/en/kimal3-coach.md +167 -0
  9. package/dist/arbeitsplatz-dateien.js +1 -1
  10. package/dist/arbeitsplatz-finden.js +115 -0
  11. package/dist/arbeitsplatz-suche.js +176 -0
  12. package/dist/befehle/einrichten.js +311 -32
  13. package/dist/befehle/kurs.js +77 -0
  14. package/dist/befehle/login.js +8 -1
  15. package/dist/befehle/skill-install.js +16 -2
  16. package/dist/befehle-katalog-en.js +12 -0
  17. package/dist/befehle-katalog.js +12 -0
  18. package/dist/befehlswoerter.js +4 -0
  19. package/dist/coach-ablegen.js +130 -0
  20. package/dist/coach-rechnung.js +40 -0
  21. package/dist/einstellungen-mischen.js +68 -28
  22. package/dist/einstellungen-schreiben.js +48 -12
  23. package/dist/kimal3.js +6 -3
  24. package/dist/logo-ablegen.js +117 -0
  25. package/dist/plugins-mischen.js +78 -0
  26. package/dist/texte/coach.js +27 -0
  27. package/dist/texte/einrichten-marken.js +2 -2
  28. package/dist/texte/einrichten.js +73 -2
  29. package/dist/texte/kurs.js +21 -0
  30. package/dist/wache-schalter.js +30 -5
  31. package/package.json +29 -27
  32. package/skill/SKILL.md +7 -3
  33. package/skill/commands/k3feierabend.md +2 -3
  34. package/skill/commands/k3kurs.md +41 -53
  35. package/skill/commands/k3start.md +4 -3
  36. package/skill/en/SKILL.md +7 -3
  37. package/skill/en/commands/k3course.md +41 -54
  38. package/skill/en/commands/k3start.md +4 -3
  39. package/skill/en/commands/k3wrapup.md +3 -4
@@ -5,6 +5,7 @@ import { sprache } from "../sprache.js";
5
5
  import { anmeldeAdresse, oeffneImBrowser } from "../browser.js";
6
6
  import { ALLGEMEIN_TEXTE } from "../texte/allgemein.js";
7
7
  import { EINRICHTEN_TEXTE } from "../texte/einrichten.js";
8
+ import { frischeCoachAuf } from "../coach-ablegen.js";
8
9
  import { stelleSkillUm } from "./skill-tausch.js";
9
10
  const warte = (ms) => new Promise((f) => setTimeout(f, ms));
10
11
  // 🌐 Die Sprache wird bei JEDER Ausgabe neu gelesen: Die Abhol-Antwort nennt die Kontosprache
@@ -27,7 +28,13 @@ export async function login() {
27
28
  console.log(t().loginOpen(BASIS_URL));
28
29
  console.log(t().loginEnterCode(start.nutzerCode));
29
30
  console.log(t().loginWaiting);
30
- return warteAufBestaetigung(start.geraeteCode);
31
+ const code = await warteAufBestaetigung(start.geraeteCode);
32
+ // Der Coach zieht mit der Kontosprache mit -- nur beim BEFEHL `kimal3 login`, nicht in der geteilten Warteschleife:
33
+ // `einrichten` meldet anschließend seinen eigenen Coach, eine zweite Zeile wäre doppelt (Gesamt-Durchsicht, Fund 4;
34
+ // dasselbe Muster wie `skillInstallBefehl`). Frischt nur auf, legt nie an (Spec C-13b).
35
+ if (code === 0)
36
+ frischeCoachAuf(sprache());
37
+ return code;
31
38
  }
32
39
  /**
33
40
  * Die Anmeldung für `kimal3 einrichten` (Spec 2026-10-11, S-2): Code und Adresse auf eigenen
@@ -3,12 +3,13 @@ import { homedir } from "node:os";
3
3
  import { fileURLToPath } from "node:url";
4
4
  import { join } from "node:path";
5
5
  import { einstellungenDatei, trageWacheEin } from "../einstellungen-schreiben.js";
6
- import { merkeSitzung, sitzungJetzt } from "../wache-schalter.js";
6
+ import { merkeEinrichtung, sitzungJetzt } from "../wache-schalter.js";
7
7
  import { stempelZeile } from "../nachricht.js";
8
8
  import { eigeneVersion } from "../version.js";
9
9
  import { sprache } from "../sprache.js";
10
10
  import { andereSpracheZuRaeumen, skillQuelle } from "../slash-befehle.js";
11
11
  import { SKILL_TEXTE } from "../texte/skill.js";
12
+ import { frischeCoachAuf } from "../coach-ablegen.js";
12
13
  export const VERALTETE_BEFEHLE = [
13
14
  /*
14
15
  🔴 **Der ERSTE Eintrag ueberhaupt** (2026-08-28). `/k3firma` heisst jetzt
@@ -286,8 +287,10 @@ export async function skillInstall() {
286
287
  taegliche Pruefung laeuft nicht" -- ausgerechnet in dem Augenblick, in
287
288
  dem er sie gerade eingerichtet hat. *Der erste Eindruck waere ein
288
289
  Fehlalarm.*
290
+ ⚠️ Als EINRICHTUNG, nicht als "nachgesehen" -- sonst schweigt die Wache in
291
+ dieser Sitzung auch beim Neustart (Sandbox-Lauf 14.10., S-3; `merkeEinrichtung`).
289
292
  */
290
- merkeSitzung(sitzungJetzt());
293
+ merkeEinrichtung(sitzungJetzt());
291
294
  }
292
295
  if (wache.art === "eingetragen") {
293
296
  console.log(t.watchSetUp);
@@ -318,6 +321,17 @@ export async function skillInstall() {
318
321
  }
319
322
  return 0;
320
323
  }
324
+ /**
325
+ * Der Befehl `kimal3 skill install`: die Anleitung ablegen und einen VORHANDENEN Coach mitziehen (Update, Sprachwechsel;
326
+ * angelegt wird keiner). Bewusst hier und nicht in `skillInstall()`: `einrichten` ruft das auch, hat seinen Coach aber
327
+ * schon selbst abgelegt und gemeldet -- ein zweites Auffrischen druckte dessen Zeile noch einmal (bei fehlender
328
+ * Trenn-Marke auch die Ersatzdatei ein zweites Mal).
329
+ */
330
+ export async function skillInstallBefehl() {
331
+ const code = await skillInstall();
332
+ frischeCoachAuf(sprache());
333
+ return code;
334
+ }
321
335
  /**
322
336
  * Kopiert die Slash-Befehle nach `~/.claude/commands/`.
323
337
  *
@@ -118,6 +118,18 @@ export const BEFEHLE_EN = {
118
118
  "What it's really for: Claude looks here BEFORE adding a project. Without that check, the same project would sooner or later appear twice on a public page that carries your name.",
119
119
  ],
120
120
  },
121
+ "kimal3 kurs": {
122
+ stempel: "3a5c7600",
123
+ befehl: "kimal3 course",
124
+ tippen: "/k3course",
125
+ terminal: "kimal3 course",
126
+ beschreibung: "Show where you are in the mini-course",
127
+ rhythmusOderSatz: "where am I in the course",
128
+ details: [
129
+ "Shows which parts you have finished, what is next and what is still open — and for each step whether you tick it off or Claude sets it.",
130
+ "Claude reads this when you come back to the course and picks up exactly where you left off.",
131
+ ],
132
+ },
121
133
  "kimal3 firma daten": {
122
134
  stempel: "3195364c",
123
135
  befehl: "kimal3 company data",
@@ -167,6 +167,18 @@ export const BEFEHLE = [
167
167
  "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.",
168
168
  ],
169
169
  },
170
+ {
171
+ befehl: "kimal3 kurs",
172
+ beschreibung: "Zeigen, wo du im Mini-Kurs stehst",
173
+ wer: "claude",
174
+ ausloeser: "wo stehe ich im Kurs",
175
+ tippen: "/k3kurs",
176
+ terminal: "kimal3 kurs",
177
+ details: [
178
+ "Zeigt, welche Beats du fertig hast, was als Nächstes dran ist und was noch offen ist — und bei jedem Schritt, ob du ihn abhakst oder Claude ihn setzt.",
179
+ "Claude liest das, wenn du in den Kurs zurückkehrst, und macht genau dort weiter, wo du aufgehört hast.",
180
+ ],
181
+ },
170
182
  {
171
183
  befehl: "kimal3 firma daten",
172
184
  beschreibung: "Die Daten deiner Firma holen, damit Claude sie auswerten kann",
@@ -11,6 +11,7 @@ export const BEFEHLSWOERTER = {
11
11
  "projekt neu": { de: "projekt neu", en: "project new" }, // kein-anzeigetext: Befehlswörter (Spec Abschnitt 6)
12
12
  "projekt liste": { de: "projekt liste", en: "project list" }, // kein-anzeigetext: Befehlswörter (Spec Abschnitt 6)
13
13
  "firma daten": { de: "firma daten", en: "company data" }, // kein-anzeigetext: Befehlswörter (Spec Abschnitt 6)
14
+ kurs: { de: "kurs", en: "course" }, // kein-anzeigetext: Befehlswörter (Spec Abschnitt 6)
14
15
  wache: { de: "wache", en: "watch" },
15
16
  "wache an": { de: "wache an", en: "watch on" }, // kein-anzeigetext: Befehlswörter (Spec Abschnitt 6)
16
17
  "wache aus": { de: "wache aus", en: "watch off" }, // kein-anzeigetext: Befehlswörter (Spec Abschnitt 6)
@@ -56,6 +57,9 @@ export const SCHALTER_EN = {
56
57
  impact: "wirkung",
57
58
  "impact-label": "wirkung-label",
58
59
  "live-since": "live-seit",
60
+ // `kimal3 einrichten` / `setup` (Spec 2026-10-16, Bauform 6)
61
+ workplace: "arbeitsplatz",
62
+ workbench: "werkbank",
59
63
  };
60
64
  /** `company` -> `firma`; ein deutscher (oder unbekannter) Name bleibt, wie er ist. */
61
65
  export function deutscherSchalter(getippt) {
@@ -0,0 +1,130 @@
1
+ import { mkdirSync, readFileSync, renameSync, rmSync, statSync, writeFileSync } from "node:fs";
2
+ import { homedir } from "node:os";
3
+ import { dirname, join } from "node:path";
4
+ import { fileURLToPath } from "node:url";
5
+ import { coachNachher } from "./coach-rechnung.js";
6
+ import { COACH_TEXTE } from "./texte/coach.js";
7
+ /**
8
+ * Der Coach im Regel-Ordner (Spec 2026-10-16): Claude Code lädt jede Datei in `~/.claude/rules/` bei jedem Start mit,
9
+ * in jedem Ordner -- dort wirkt er, ohne dass `~/.claude/CLAUDE.md` angefasst wird (die gehört dem Menschen).
10
+ * Was in der Datei steht, rechnet `coach-rechnung.ts` (rein, bewacht von `check:wache`); hier wird nur gelesen und
11
+ * geschrieben. Gemessen am gebauten Programm von `check:cli-live`.
12
+ */
13
+ export const coachPfad = () => join(homedir(), ".claude", "rules", "kimal3-coach.md"); // kein-anzeigetext: Dateiname
14
+ /** Fehlt die Trenn-Marke, landet die neue Fassung HIER -- außerhalb des Regel-Ordners, sonst lüden zwei Coaches (UX-10). */
15
+ export const coachErsatzPfad = () => join(homedir(), ".kimal3", "kimal3-coach-neu.md"); // kein-anzeigetext: Dateiname
16
+ /** Der Coach aus dem alten Mini-Kurs, und wohin Claude ihn nach einem Ja verschiebt (S-14) -- `kimal3` schreibt ihn nie. */
17
+ export const alterKursCoachPfad = () => join(homedir(), ".claude", "CLAUDE.md"); // kein-anzeigetext: Dateiname
18
+ export const alterKursCoachAblage = () => join(homedir(), ".kimal3", "alter-kurs-coach.md"); // kein-anzeigetext: Dateiname
19
+ /**
20
+ * Legt den Coach ab oder frischt ihn auf. `null` = es liegt keiner da, und dieser Aufruf darf keinen anlegen
21
+ * (nur `einrichten` darf das; `skill install` und `login` frischen nur auf).
22
+ * 🔑 Wirft nie: Jeder Lese- oder Schreibfehler wird `gescheitert` mit der Meldung des Systems -- die Einrichtung läuft
23
+ * weiter (C-10), und der Mensch erfährt den Grund statt einer Stapelspur.
24
+ */
25
+ export function legeCoachAb(s, darfAnlegen) {
26
+ const pfad = coachPfad();
27
+ try {
28
+ const da = statSync(pfad, { throwIfNoEntry: false });
29
+ // Ein Ordner unter dem Namen gehört nicht uns -- nichts anfassen, wie bei einer fremden Datei (C-11).
30
+ if (da !== undefined && !da.isFile())
31
+ return { art: "fremd", pfad, spracheGewechselt: false };
32
+ if (da === undefined && !darfAnlegen)
33
+ return null;
34
+ // Die Vorlage erst NACH dem `null`: Wer keinen Coach hat, soll von `login` und `skill install` auch dann nichts hören,
35
+ // wenn die Vorlage im Paket fehlt (Gesamt-Durchsicht, Fund 6).
36
+ // `dist/coach-ablegen.js` -> `../coach/<sprache>/…` im installierten Paket (`files` in package.json).
37
+ const vorlage = readFileSync(fileURLToPath(new URL(`../coach/${s}/kimal3-coach.md`, import.meta.url)), "utf8"); // kein-anzeigetext: Paketpfad
38
+ const ergebnis = coachNachher(da === undefined ? null : readFileSync(pfad, "utf8"), vorlage);
39
+ switch (ergebnis.art) {
40
+ case "neu":
41
+ mkdirSync(dirname(pfad), { recursive: true });
42
+ tauscheEin(pfad, ergebnis.inhalt);
43
+ return { art: "angelegt", pfad, spracheGewechselt: false };
44
+ case "aktualisiert":
45
+ tauscheEin(pfad, ergebnis.inhalt);
46
+ return { art: "aktualisiert", pfad, spracheGewechselt: ergebnis.spracheVorher !== null && ergebnis.spracheVorher !== s };
47
+ case "ohne-trenner": {
48
+ // Das Original bleibt Byte für Byte (C-8): Ohne Trenn-Marke wüssten wir nicht, wo seine Regeln anfangen.
49
+ const ersatz = coachErsatzPfad();
50
+ mkdirSync(dirname(ersatz), { recursive: true });
51
+ writeFileSync(ersatz, ergebnis.inhalt, "utf8");
52
+ return { art: "ohne-trenner", pfad, ersatz };
53
+ }
54
+ case "vorlage-ohne-trenner":
55
+ return { art: "gescheitert", pfad, grund: COACH_TEXTE[s].templateWithoutSeparator };
56
+ default:
57
+ return { art: ergebnis.art, pfad, spracheGewechselt: false };
58
+ }
59
+ }
60
+ catch (e) {
61
+ return { art: "gescheitert", pfad, grund: e.message };
62
+ }
63
+ }
64
+ /**
65
+ * Erst in eine Nebendatei, dann tauschen (Durchsicht Aufgabe 5, Fund 1): `writeFileSync` leert die Datei, BEVOR es
66
+ * schreibt -- bei voller Platte wären die gelernten Regeln weg, und der nächste Lauf hielte den leeren Rest für fremd.
67
+ * 🔑 `kimal3-coach.md.neu` endet nicht auf `.md`: Claude lädt sie nie als zweiten Coach, auch wenn sie liegen bliebe.
68
+ * Scheitert ein Schritt, kommt die Nebendatei weg (ein Ordner unter dem Namen ist nicht unserer und bleibt), und der
69
+ * Fehler geht an `legeCoachAb`, das ihn zu `gescheitert` macht.
70
+ */
71
+ function tauscheEin(pfad, inhalt) {
72
+ const neben = `${pfad}.neu`; // kein-anzeigetext: Dateiname
73
+ try {
74
+ writeFileSync(neben, inhalt, "utf8");
75
+ renameSync(neben, pfad);
76
+ }
77
+ catch (e) {
78
+ try {
79
+ rmSync(neben);
80
+ }
81
+ catch {
82
+ // Lag nie da, oder es ist ein Ordner -- dann nichts anfassen.
83
+ }
84
+ throw e;
85
+ }
86
+ }
87
+ /** Liegt unter dem Coach-Pfad eine DATEI (unsere oder eine fremde)? Daran hängt `beat2-coach` (E-5, E-9). Wirft nie. */
88
+ export function coachDateiLiegt() {
89
+ try {
90
+ return statSync(coachPfad(), { throwIfNoEntry: false })?.isFile() === true;
91
+ }
92
+ catch {
93
+ return false;
94
+ }
95
+ }
96
+ /**
97
+ * Die eine Zeile zu einer Ablage, in der Sprache des Laufs -- für `einrichten` (Task 5), `skill install` und `login`.
98
+ * `sprachwechselSagen`: Nur `login` und `skill install` sagen einen Sprachwechsel mit eigenem Satz (Spec 2.6) -- er
99
+ * sagt, was der Mensch dort sonst nicht merkt: dass seine Regeln den Wechsel überlebt haben. `einrichten` bleibt bei der
100
+ * Zeile mit `Coach:`, an der die Anleitung liest. Welche Fälle ein Aufruf verschweigt (`gleich`, `fremd` beim
101
+ * Auffrischen), entscheidet der Aufrufer.
102
+ */
103
+ export function meldeCoach(ablage, s, sprachwechselSagen = false) {
104
+ if (ablage === null)
105
+ return;
106
+ const t = COACH_TEXTE[s];
107
+ if (ablage.art === "angelegt")
108
+ console.log(t.setUp(ablage.pfad));
109
+ else if (ablage.art === "gleich")
110
+ console.log(t.upToDate);
111
+ else if (ablage.art === "aktualisiert")
112
+ console.log(sprachwechselSagen && ablage.spracheGewechselt ? t.languageSwitched : t.updated);
113
+ else if (ablage.art === "fremd")
114
+ console.log(t.foreign(ablage.pfad));
115
+ else if (ablage.art === "ohne-trenner")
116
+ console.log(t.noSeparator(ablage.pfad, ablage.ersatz));
117
+ else if (ablage.art === "gescheitert")
118
+ console.log(t.failed(ablage.grund));
119
+ }
120
+ /**
121
+ * Für `skill install` und `login`: einen VORHANDENEN Coach auffrischen (Update, Sprachwechsel), nie einen anlegen --
122
+ * das darf nur `einrichten` (Spec C-13b: Auffrischen ist kein Anlegen). Schweigt, wenn nichts zu sagen ist: kein Coach,
123
+ * ein fremder, ein schon aktueller. Ein Sprachwechsel bekommt seinen eigenen Satz (Spec 2.6).
124
+ */
125
+ export function frischeCoachAuf(s) {
126
+ const ablage = legeCoachAb(s, false);
127
+ if (ablage === null || ablage.art === "gleich" || ablage.art === "fremd")
128
+ return;
129
+ meldeCoach(ablage, s, true);
130
+ }
@@ -0,0 +1,40 @@
1
+ // Ein BOM (U+FEFF) vor der ersten Zeile setzen manche Windows-Editoren; er gehört nicht zur Marke.
2
+ const KOPF = /^\uFEFF?<!-- kimal3:coach/;
3
+ const KOPF_SPRACHE = /^\uFEFF?<!-- kimal3:coach sprache=(de|en)(?![A-Za-z])/;
4
+ // Nur am ZEILENANFANG, ohne Einrückung: Die Kopfzeile nennt die Trenn-Marke im Fließtext, und eine gelernte Regel darf
5
+ // es auch. 🔑 Exportiert, weil `check:cli` die Vorlagen mit GENAU diesem Ausdruck prüft -- eine eigene, nachsichtigere
6
+ // Erkennung dort ließe eine eingerückte Marke grün durch, die diese Rechnung nicht findet (Gesamt-Durchsicht, Fund 3).
7
+ export const TRENNER = /^<!-- kimal3:gelernte-regeln/m;
8
+ const ALTER_KURS_COACH = /^\uFEFF?# CLAUDE\.md — (Dein Claude-Coach|Your Claude coach)/;
9
+ export const istUnserCoach = (inhalt) => KOPF.test(inhalt);
10
+ export function coachSprache(inhalt) {
11
+ const treffer = KOPF_SPRACHE.exec(inhalt);
12
+ return treffer ? treffer[1] : null;
13
+ }
14
+ // Der Kurs-Coach von früher (Beat 2 des Mini-Kurses) beginnt mit dieser Überschrift, deutsch wie englisch.
15
+ export const istAlterKursCoach = (inhalt) => ALTER_KURS_COACH.test(inhalt);
16
+ // Alles bis einschließlich des Zeilenendes der ersten Trenn-Zeile (\n oder \r\n -- das \r steht vor dem \n und
17
+ // gehört damit zur Zeile). Ohne Zeilenende danach reicht die Zeile bis zum Dateiende. -1 = keine Trenn-Zeile.
18
+ function obenBis(text) {
19
+ const treffer = TRENNER.exec(text);
20
+ if (!treffer)
21
+ return -1;
22
+ const umbruch = text.indexOf("\n", treffer.index);
23
+ return umbruch === -1 ? text.length : umbruch + 1;
24
+ }
25
+ export function coachNachher(vorher, vorlage) {
26
+ const vorlageEnde = obenBis(vorlage);
27
+ if (vorlageEnde === -1)
28
+ return { art: "vorlage-ohne-trenner" };
29
+ if (vorher === null)
30
+ return { art: "neu", inhalt: vorlage };
31
+ if (!istUnserCoach(vorher))
32
+ return { art: "fremd" };
33
+ const altEnde = obenBis(vorher);
34
+ if (altEnde === -1)
35
+ return { art: "ohne-trenner", inhalt: vorlage };
36
+ const neu = vorlage.slice(0, vorlageEnde) + vorher.slice(altEnde);
37
+ if (neu === vorher)
38
+ return { art: "gleich" };
39
+ return { art: "aktualisiert", inhalt: neu, spracheVorher: coachSprache(vorher) };
40
+ }
@@ -18,6 +18,8 @@ export const WACHE_MARKE = "kimal3 wache"; // kein-anzeigetext: Startbefehl in C
18
18
  * Der Befehl, der in die Einstellungen eingetragen wird.
19
19
  *
20
20
  * 🔴 NUR EIN BEFEHL, keine Kommandozeilen-Zeichen -- kein `||`, kein `2>/dev/null`.
21
+ * 📌 Die eine Ausnahme seit dem 15.10.: Auf Mac und Linux steht `PATH="…" ` davor (`wacheBefehl` unten). Dort
22
+ * startet Claude Code Startbefehle immer über `sh -c`, und `sh` kennt diese Form; unter Windows bleibt er bloß.
21
23
  * Das sind Bash-Zeichen, und ob auf dem Kundenrechner eine Bash laeuft, wissen
22
24
  * wir nicht. Die Fehlerbehandlung gehoert ins Programm, nicht in die Zeile.
23
25
  * *Ein Befehl, der beim Erbauer laeuft, ist keine Aussage ueber den Rechner
@@ -38,6 +40,31 @@ export const WACHE_MARKE = "kimal3 wache"; // kein-anzeigetext: Startbefehl in C
38
40
  * AUCH BEI RUECKGABEWERT 127 gelesen (ebenfalls gemessen).
39
41
  */
40
42
  export const WACHE_BEFEHL = `${WACHE_MARKE} --hook`; // kein-anzeigetext: Startbefehl in Claudes Einstellungen, bleibt deutsch (T3-5)
43
+ /**
44
+ * Der Startbefehl, wie er auf DIESEM Rechner eingetragen wird. Auf Mac und Linux stehen die Ordner davor, in denen
45
+ * `kimal3` und Node.js liegen (`ordner`, doppelte nur einmal).
46
+ * 🔑 Warum beide: `kimal3` liegt dort, wo npm Programme ablegt -- beim Mac-Weg `~/.kimal3/node/bin`, bei Homebrew
47
+ * `/opt/homebrew/bin`; Node.js selbst liegt bei Homebrew im versionierten Ordner darunter. Ein `kimal3` in einem
48
+ * eigenen npm-Ordner braucht zusätzlich den Ordner von Node.js für seine erste Zeile (`#!/usr/bin/env node`).
49
+ *
50
+ * 🔑 WARUM: Startbefehle laufen mit Claudes eigener Umgebung über `sh -c` und lesen weder `~/.zprofile`
51
+ * noch `~/.zshrc` (code.claude.com/docs/en/hooks, gelesen 15.10.). Liegt Node.js im Benutzerordner
52
+ * (`~/.kimal3/node`, der Mac-Weg ohne Adminrechte) und wurde Claude aus dem Dock gestartet, fände `sh`
53
+ * weder `kimal3` noch das `node` aus dessen erster Zeile -- und die Wache schwiege, ohne dass es jemand merkt.
54
+ * Unter Windows bleibt es beim bloßen Befehl: Dort steht Node.js im Pfad des Systems, und die Wache läuft
55
+ * dort seit Wochen so.
56
+ * 🔴 Nur Zeichen aus einer kurzen Liste kommen in den Befehl (messen-und-pruefen §23); alles andere
57
+ * -- ein Anführungszeichen, ein `$` -- gäbe einen Befehl, den `sh` anders liest. Ein solcher Ordner fällt weg; bleibt
58
+ * keiner übrig, steht der bloße Befehl da.
59
+ */
60
+ export function wacheBefehl(system, ordner) {
61
+ // Ein Wegwerf-Ort fällt weg (Durchsicht 4, Fund 4): `npx kimal3 …` startet aus einem Zwischenspeicher, der bald
62
+ // verschwindet, ein Start aus einem Projektordner aus dessen `node_modules`.
63
+ const sicher = [...new Set(ordner)].filter((o) => /^\/[A-Za-z0-9 ._@+\/-]*$/.test(o) && !/\/(node_modules|_npx)\//.test(`${o}/`));
64
+ if (system === "win32" || sicher.length === 0)
65
+ return WACHE_BEFEHL;
66
+ return `PATH="${sicher.join(":")}:$PATH" ${WACHE_BEFEHL}`; // kein-anzeigetext: Startbefehl in Claudes Einstellungen
67
+ }
41
68
  /**
42
69
  * Wie lange Claude auf die Wache wartet, in Sekunden.
43
70
  *
@@ -48,9 +75,45 @@ export const WACHE_BEFEHL = `${WACHE_MARKE} --hook`; // kein-anzeigetext: Startb
48
75
  * seiner Arbeit ab -- und ausgerechnet unserer soll ihm helfen.*
49
76
  */
50
77
  export const WACHE_ZEITGRENZE = 10;
51
- function istObjekt(wert) {
78
+ export function istObjekt(wert) {
52
79
  return typeof wert === "object" && wert !== null && !Array.isArray(wert);
53
80
  }
81
+ /**
82
+ * Die Einstellungsdatei als Objekt -- oder die Absage. Gemeinsam fuer die Wache und die Plugins
83
+ * (`plugins-mischen.ts`), damit beide eine kaputte Datei gleich behandeln: nicht anfassen.
84
+ */
85
+ export function leseEinstellungen(alt) {
86
+ /*
87
+ Eine fehlende und eine leere Datei sind dasselbe: Es gibt nichts zu
88
+ bewahren, also legen wir an. `null` kommt vom Aufrufer, wenn die Datei gar
89
+ nicht existiert; ein leerer Text entsteht, wenn ein frueherer Lauf sie
90
+ angelegt, aber nicht mehr befuellt hat.
91
+ */
92
+ const leer = alt === null || alt.trim() === "";
93
+ if (leer)
94
+ return { ok: true, objekt: {}, leer };
95
+ let gelesen;
96
+ try {
97
+ // Eine unsichtbare Kennung am Anfang (UTF-8-BOM, z. B. von PowerShell 5.1 geschrieben) macht die
98
+ // Datei nicht kaputt -- `JSON.parse` stolpert aber darüber (QA-Fund 3 zum Bauplan 2026-10-15).
99
+ gelesen = JSON.parse(alt.replace(/^\uFEFF/, ""));
100
+ }
101
+ catch {
102
+ /*
103
+ 🔴 HIER WIRD NICHTS GESCHRIEBEN. Wer eine unlesbare Einstellungsdatei
104
+ ueberschreibt, wirft weg, was der Mensch dort stehen hat -- und
105
+ ausgerechnet der Fall, in dem sie schon beschaedigt ist, ist der, in
106
+ dem er sie am dringendsten braucht. Die Absage geht nach oben, der
107
+ Mensch erfaehrt sie.
108
+ */
109
+ return { ok: false, grund: "kein-gueltiges-json" };
110
+ }
111
+ // Eine Liste oder eine blosse Zahl ist gueltiges JSON, aber keine
112
+ // Einstellungsdatei. Das ist ein DRITTER Fall, nicht der zweite.
113
+ if (!istObjekt(gelesen))
114
+ return { ok: false, grund: "fremde-form" };
115
+ return { ok: true, objekt: gelesen, leer };
116
+ }
54
117
  /**
55
118
  * Ist das ein Eintrag, den WIR frueher einmal geschrieben haben?
56
119
  *
@@ -75,33 +138,10 @@ function istUnserer(eintrag) {
75
138
  return typeof eintrag.command === "string" && eintrag.command.includes(WACHE_MARKE);
76
139
  }
77
140
  export function mischeEinstellungen(alt, befehl) {
78
- /*
79
- Eine fehlende und eine leere Datei sind dasselbe: Es gibt nichts zu
80
- bewahren, also legen wir an. `null` kommt vom Aufrufer, wenn die Datei gar
81
- nicht existiert; ein leerer Text entsteht, wenn ein frueherer Lauf sie
82
- angelegt, aber nicht mehr befuellt hat.
83
- */
84
- const leer = alt === null || alt.trim() === "";
85
- let gelesen = {};
86
- if (!leer) {
87
- try {
88
- gelesen = JSON.parse(alt);
89
- }
90
- catch {
91
- /*
92
- 🔴 HIER WIRD NICHTS GESCHRIEBEN. Wer eine unlesbare Einstellungsdatei
93
- ueberschreibt, wirft weg, was der Mensch dort stehen hat -- und
94
- ausgerechnet der Fall, in dem sie schon beschaedigt ist, ist der, in
95
- dem er sie am dringendsten braucht. Die Absage geht nach oben, der
96
- Mensch erfaehrt sie.
97
- */
98
- return { art: "abgelehnt", grund: "kein-gueltiges-json" };
99
- }
100
- }
101
- // Eine Liste oder eine blosse Zahl ist gueltiges JSON, aber keine
102
- // Einstellungsdatei. Das ist ein DRITTER Fall, nicht der zweite.
103
- if (!istObjekt(gelesen))
104
- return { art: "abgelehnt", grund: "fremde-form" };
141
+ const lesen = leseEinstellungen(alt);
142
+ if (!lesen.ok)
143
+ return { art: "abgelehnt", grund: lesen.grund };
144
+ const { objekt: gelesen, leer } = lesen;
105
145
  const vorher = JSON.stringify(gelesen);
106
146
  const hooks = gelesen.hooks === undefined ? {} : gelesen.hooks;
107
147
  if (!istObjekt(hooks))
@@ -1,7 +1,8 @@
1
1
  import { copyFileSync, existsSync, mkdirSync, readFileSync, writeFileSync } from "node:fs";
2
- import { homedir } from "node:os";
3
- import { join } from "node:path";
4
- import { WACHE_BEFEHL, mischeEinstellungen } from "./einstellungen-mischen.js";
2
+ import { homedir, platform } from "node:os";
3
+ import { dirname, join } from "node:path";
4
+ import { mischeEinstellungen, wacheBefehl } from "./einstellungen-mischen.js";
5
+ import { KATALOG, mischePlugins, pluginStand } from "./plugins-mischen.js";
5
6
  /** Die Kontrolle nach dem Schreiben fand unseren Eintrag nicht -- eigener Fall, damit `detail` `null` sein kann. */
6
7
  class EintragFehlt extends Error {
7
8
  }
@@ -12,6 +13,41 @@ export function sicherungsDatei() {
12
13
  return einstellungenDatei() + ".kimal3-sicherung";
13
14
  }
14
15
  export function trageWacheEin() {
16
+ // Vor den Befehl kommen der Ordner, aus dem `kimal3` gerade gestartet wurde (dort legt npm es ab; bei Homebrew liegt
17
+ // dort auch `node`), und der Ordner des laufenden Node.js (für ein `kimal3` in einem eigenen npm-Ordner) -- Durchsicht 3, Fund 2.
18
+ const befehl = wacheBefehl(platform(), [dirname(process.argv[1] ?? ""), dirname(process.execPath)]);
19
+ return trageEin((alt) => mischeEinstellungen(alt, befehl), (text) => text.includes(JSON.stringify(befehl)), sicherungsDatei());
20
+ }
21
+ /**
22
+ * Die Kopie, die die Plugins vor dem Schreiben anlegen. Eine eigene, weil `kimal3 einrichten` die Plugins
23
+ * VOR der Wache einträgt: So hält diese Kopie Sabines unveränderte Datei (Prüfer-Fund 9 zum Bauplan 2026-10-15).
24
+ */
25
+ export function pluginSicherungsDatei() {
26
+ return einstellungenDatei() + ".kimal3-sicherung-plugins";
27
+ }
28
+ /**
29
+ * superpowers und das Logo eintragen (`plugins-mischen.ts`); `katalogPfad` mit Schrägstrichen vorwärts,
30
+ * `null` ohne Logo-Ordner. 🔑 Die Kontrolle liest die Einschalter selbst, nicht nur Katalogname und Pfad:
31
+ * Fehlte ein Einschalter, meldete die Zeile `Plugins:` sonst „eingetragen“ für etwas, das nie lädt
32
+ * (Durchsicht 2, Fund 7). „Von Sabine ausgeschaltet“ zählt als da -- es steht ja drin.
33
+ */
34
+ export function tragePluginsEin(katalogPfad, mitSuperpowers) {
35
+ return trageEin((alt) => mischePlugins(alt, katalogPfad, mitSuperpowers), (text) => {
36
+ const stand = pluginStand(text);
37
+ if (mitSuperpowers && stand.superpowers === "fehlt")
38
+ return false;
39
+ return katalogPfad === null || (stand.logo !== "fehlt" && text.includes(`"${KATALOG}"`) && text.includes(JSON.stringify(katalogPfad)));
40
+ }, pluginSicherungsDatei());
41
+ }
42
+ /**
43
+ * Der Vier-Schritte-Weg fuer jeden Eintrag in Claudes Einstellungen -- die Wache und die Plugins
44
+ * (`plugins-mischen.ts`) gehen denselben, damit keiner von beiden ohne Sicherung schreibt.
45
+ * `kontrolle` bekommt den zurueckgelesenen Text und sagt, ob unser Eintrag drinsteht.
46
+ */
47
+ export function trageEin(mische, kontrolle,
48
+ // 🔑 Je Schreiber eine eigene Kopie: Schrieben Wache und Plugins in dieselbe, überschriebe die zweite
49
+ // Sabines Originaldatei mit dem Stand nach dem ersten Eintrag (QA-Fund 9).
50
+ sicherung) {
15
51
  const datei = einstellungenDatei();
16
52
  const gabEsSchon = existsSync(datei);
17
53
  // --- Schritt 1: lesen ---
@@ -30,15 +66,15 @@ export function trageWacheEin() {
30
66
  return { art: "gescheitert", schritt: "lesen", detail: e.message };
31
67
  }
32
68
  }
33
- const ergebnis = mischeEinstellungen(alt, WACHE_BEFEHL);
69
+ const ergebnis = mische(alt);
34
70
  if (ergebnis.art === "abgelehnt")
35
71
  return { art: "abgelehnt", grund: ergebnis.grund };
36
72
  if (ergebnis.art === "unveraendert")
37
- return { art: "stand-schon" };
73
+ return { art: "stand-schon", inhalt: alt ?? "" };
38
74
  // --- Schritt 2: Sicherungskopie ---
39
75
  if (gabEsSchon) {
40
76
  try {
41
- copyFileSync(datei, sicherungsDatei());
77
+ copyFileSync(datei, sicherung);
42
78
  }
43
79
  catch (e) {
44
80
  /*
@@ -55,22 +91,22 @@ export function trageWacheEin() {
55
91
  writeFileSync(datei, ergebnis.inhalt, "utf8");
56
92
  }
57
93
  catch (e) {
58
- zurueck(gabEsSchon);
94
+ zurueck(gabEsSchon, sicherung);
59
95
  return { art: "gescheitert", schritt: "schreiben", detail: e.message };
60
96
  }
61
97
  // --- Schritt 4: zuruecklesen und erneut pruefen ---
62
98
  try {
63
99
  const zurueckgelesen = readFileSync(datei, "utf8");
64
100
  const geparst = JSON.parse(zurueckgelesen);
65
- if (!zurueckgelesen.includes(WACHE_BEFEHL) || geparst === null) {
101
+ if (!kontrolle(zurueckgelesen) || geparst === null) {
66
102
  throw new EintragFehlt();
67
103
  }
68
104
  }
69
105
  catch (e) {
70
- zurueck(gabEsSchon);
106
+ zurueck(gabEsSchon, sicherung);
71
107
  return { art: "gescheitert", schritt: "kontrolle", detail: e instanceof EintragFehlt ? null : e.message };
72
108
  }
73
- return { art: "eingetragen" };
109
+ return { art: "eingetragen", inhalt: ergebnis.inhalt };
74
110
  }
75
111
  /**
76
112
  * Die Sicherung zurueckspielen.
@@ -79,11 +115,11 @@ export function trageWacheEin() {
79
115
  * bleibt die halb geschriebene stehen. Das ist die harmlose Seite: Sie
80
116
  * enthielt vorher nichts, was jemandem gehoerte.
81
117
  */
82
- function zurueck(gabEsSchon) {
118
+ function zurueck(gabEsSchon, sicherung) {
83
119
  if (!gabEsSchon)
84
120
  return;
85
121
  try {
86
- copyFileSync(sicherungsDatei(), einstellungenDatei());
122
+ copyFileSync(sicherung, einstellungenDatei());
87
123
  }
88
124
  catch {
89
125
  /* Mehr koennen wir nicht tun -- der Grund steht in der Meldung oben. */
package/dist/kimal3.js CHANGED
@@ -33,9 +33,10 @@ import { kontextRunter } from "./befehle/kontext-runter.js";
33
33
  import { kontextHoch } from "./befehle/kontext-hoch.js";
34
34
  import { login } from "./befehle/login.js";
35
35
  import { logout } from "./befehle/logout.js";
36
+ import { kurs } from "./befehle/kurs.js";
36
37
  import { projektListe } from "./befehle/projekt-liste.js";
37
38
  import { projektNeu } from "./befehle/projekt-neu.js";
38
- import { skillDatei, skillInstall } from "./befehle/skill-install.js";
39
+ import { skillDatei, skillInstallBefehl } from "./befehle/skill-install.js";
39
40
  import { wache, wacheSchalten } from "./befehle/wache.js";
40
41
  /** Eine Meldung pro Programmlauf -- s. "Einmal-Riegel" im Block darunter. */
41
42
  let schonGemeldet = false;
@@ -480,9 +481,10 @@ async function fuehreAus(name, rest) {
480
481
  switch (name) {
481
482
  case "login": return allein ? login() : null;
482
483
  case "logout": return allein ? logout() : null;
483
- case "skill install": return allein ? skillInstall() : null;
484
+ case "skill install": return allein ? skillInstallBefehl() : null;
484
485
  case "projekt neu": return projektNeu(rest);
485
486
  case "projekt liste": return allein ? projektListe() : null;
487
+ case "kurs": return allein ? kurs() : null;
486
488
  // ⚠️ Rest wird durchgereicht, nicht auf Gleichheit geprueft: `firma daten --firma X` gehoert dazu.
487
489
  case "firma daten": return firmaDaten(rest);
488
490
  case "einrichten": return einrichten(rest);
@@ -495,7 +497,8 @@ async function fuehreAus(name, rest) {
495
497
  case "wache aus": return allein ? wacheSchalten(false) : null;
496
498
  case "wache":
497
499
  // 🔴 Der Startbefehl aus `~/.claude/settings.json`, den `skill install` eintraegt (T3-5:
498
- // eingetragen wird immer `kimal3 wache --hook`; `kimal3 watch --hook` laeuft genauso).
500
+ // eingetragen wird immer `kimal3 wache --hook`, auf Mac/Linux mit `PATH="…"` davor -- `wacheBefehl`;
501
+ // `kimal3 watch --hook` laeuft genauso).
499
502
  // Er verpackt die Meldung anders -- Begruendung an `wache()`.
500
503
  // ⚠️ Ein Mensch tippt das nie; der Katalog nennt ihn deshalb auch nicht.
501
504
  if (rest.length === 1 && rest[0] === "--hook")