kimal3 0.1.5 β†’ 0.2.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.
@@ -0,0 +1,178 @@
1
+ /*
2
+ Die Fragen aus einer Kontext-Datei lesen -- die reine Rechnung hinter
3
+ `kimal3 kontext fragen` und hinter dem Abbruch von `kimal3 kontext hoch`.
4
+
5
+ πŸ”‘ WOHER DIE FRAGEN KOMMEN -- die eine Entscheidung dieser Datei:
6
+ **Aus der Datei selbst, nicht aus einem Katalog.** Jede ausgelieferte
7
+ Kontext-Datei traegt ihre Fragen bereits vollstaendig bei sich: einen Anker
8
+ (`<!-- k3:marke_anrede -->`), darunter die Frage als `## `-Ueberschrift,
9
+ darunter unsere Hinweiszeilen mit `>`, darunter die Antwort des Menschen.
10
+ Das Format beschreibt sich selbst.
11
+
12
+ πŸ”΄ WARUM DAS NICHT NUR BEQUEM, SONDERN DAS EINZIG EHRLICHE IST: Die CLI kann
13
+ nichts aus `src/` importieren (eigenes Paket, eigenes tsconfig -- s. Kopf von
14
+ `kontext-stand.ts`). Ein Katalog in `cli/src/` waere eine ZWEITE Fassung der
15
+ 17 Fragen, und sie friert in dem Moment ein, in dem `npm publish` laeuft: Der
16
+ Kunde hat dann eine CLI von gestern und Dateien von heute. Die Datei auf
17
+ seiner Platte kommt dagegen IMMER vom Server -- sie ist die einzige Quelle,
18
+ die nicht gegenueber dem abweichen kann, was er wirklich vor sich hat.
19
+
20
+ ⚠️ DIESE DATEI IMPORTIERT NICHTS -- dieselbe Auflage wie `kontext-stand.ts`.
21
+ Nur so kann `scripts/check-kontext.mts` sie neben `FRAGEN` (aus der App) in
22
+ denselben Lauf holen und beide gegeneinander messen.
23
+
24
+ πŸ“Œ Der Preis, ausgesprochen: Wer den ANKER einer Frage aus seiner Datei
25
+ loescht, die Ueberschrift aber stehen laesst, hat eine Frage, die hier nicht
26
+ mehr auftaucht. Die App faengt diesen Fall ab (sie erkennt die Frage am
27
+ Wortlaut) und setzt den Anker beim naechsten Speichern wieder ein. Die
28
+ Ausgabe von `kontext fragen` sagt deshalb "Fragen in deinen Dateien" und
29
+ nicht "alle Fragen deiner Firma" -- sie behauptet nur, was sie messen kann.
30
+ */
31
+ /**
32
+ * `<!-- k3:anrede -->` bzw. `<!-- k3:unwoerter optional -->`.
33
+ *
34
+ * ⚠️ Woertlich dasselbe Muster wie `ANKER_MUSTER` in
35
+ * `src/app/firma/kontext-zerlegen.ts`. Es steht hier ein zweites Mal, weil
36
+ * diese Datei nichts importieren darf (s. Kopf) -- dass beide dasselbe meinen,
37
+ * misst `check:kontext`, indem er die ECHTEN Vorlagen durch beide Seiten
38
+ * schickt und die Ergebnisse vergleicht.
39
+ */
40
+ const ANKER_MUSTER = /^<!--\s*k3:([a-z0-9_]+)(\s+optional)?\s*-->$/;
41
+ function istHinweis(zeile) {
42
+ return zeile.trimStart().startsWith(">");
43
+ }
44
+ function istUeberschrift(zeile) {
45
+ return zeile.startsWith("## ");
46
+ }
47
+ /**
48
+ * Alle Fragen, die in `text` stehen -- in der Reihenfolge der Datei.
49
+ *
50
+ * Der Durchgang ist zeilenweise und haelt drei Dinge auseinander:
51
+ * 1. **Anker** -- er merkt sich, wessen Frage als Naechstes kommt.
52
+ * 2. **Ueberschrift** -- steht ein Anker offen, beginnt hier seine Frage.
53
+ * 3. **Darunter** -- die unmittelbar folgenden `>`-Zeilen sind UNSER Hinweis,
54
+ * alles Weitere bis zur naechsten Ueberschrift ist die Antwort.
55
+ *
56
+ * πŸ”‘ DER HINWEIS ZAEHLT NUR UNMITTELBAR UNTER DER UEBERSCHRIFT -- genau wie in
57
+ * `scripts/arbeitsplatz-inhalt.mts`, wo derselbe Katalog fuer die App entsteht.
58
+ * Sonst kaeme eine `>`-Zeile, die ein Mensch mitten in seine Antwort setzt,
59
+ * als unser Hinweis heraus.
60
+ *
61
+ * πŸ”΄ AUS DER ANTWORT FALLEN TROTZDEM ALLE `>`-ZEILEN HERAUS, auch spaetere --
62
+ * das ist die Regel, nach der auch die App zerlegt (`zerlege`). Ein Zitatblock
63
+ * gehoert uns; ginge er als Antwort durch, stuende unser eigener Satz als
64
+ * Aussage der Firma in der Datenbank.
65
+ *
66
+ * ⚠️ Ein Anker OHNE Ueberschrift darunter zaehlt nicht -- er wird vom naechsten
67
+ * Anker oder von der naechsten Ueberschrift ueberschrieben. *Ein Anker, der auf
68
+ * eine Ueberschrift drei Absaetze weiter zeigt, ist kein Anker, sondern ein
69
+ * Zufall, der aussieht wie Absicht.*
70
+ */
71
+ export function fragenAus(text) {
72
+ const zeilen = text.split(/\r?\n/);
73
+ const fragen = [];
74
+ let offenerAnker = null;
75
+ let aktuell = null;
76
+ let hinweisNochOffen = false;
77
+ const hinweise = [];
78
+ const antwort = [];
79
+ /** Die laufende Frage abschliessen -- Hinweis und Antwort festschreiben. */
80
+ const schliesse = () => {
81
+ if (aktuell === null)
82
+ return;
83
+ aktuell.hinweis = hinweise.join(" ").trim();
84
+ aktuell.antwort = antwort.join("\n").trim();
85
+ aktuell = null;
86
+ hinweise.length = 0;
87
+ antwort.length = 0;
88
+ };
89
+ for (const zeile of zeilen) {
90
+ const treffer = ANKER_MUSTER.exec(zeile.trim());
91
+ if (treffer) {
92
+ schliesse();
93
+ offenerAnker = { anker: treffer[1], pflicht: !treffer[2] };
94
+ continue;
95
+ }
96
+ if (istUeberschrift(zeile)) {
97
+ schliesse();
98
+ if (offenerAnker) {
99
+ aktuell = {
100
+ anker: offenerAnker.anker,
101
+ ueberschrift: zeile.slice(3).trim(),
102
+ hinweis: "",
103
+ pflicht: offenerAnker.pflicht,
104
+ antwort: "",
105
+ };
106
+ hinweisNochOffen = true;
107
+ fragen.push(aktuell);
108
+ }
109
+ offenerAnker = null;
110
+ continue;
111
+ }
112
+ /*
113
+ πŸ”΄ **RULING K-14: EIN ANKER, UNTER DEM NICHT UNMITTELBAR SEINE FRAGE
114
+ STEHT, ZAEHLT NICHT.** Leerzeilen sind erlaubt, Text nicht. Ohne diese
115
+ drei Zeilen machte ein verwaister Anker die naechstbeste Ueberschrift zur
116
+ Frage -- und `kimal3 kontext fragen` stellte einem Menschen eine Frage,
117
+ die niemand geschrieben hat. Dieselbe Regel wie im Erzeuger
118
+ (`frageZeileNach` in `scripts/arbeitsplatz-inhalt.mts`).
119
+
120
+ ⚠️ Die App ist an dieser Stelle NACHSICHTIGER: `zerlege` haengt einen
121
+ verwaisten Anker an die naechste Ueberschrift, wie weit sie auch weg ist.
122
+ Der Unterschied ist gewollt und folgenlos, solange keine ausgelieferte
123
+ Datei einen verwaisten Anker hat -- und genau das prueft
124
+ `check:arbeitsplatz` fuer jede Vorlage.
125
+ */
126
+ if (offenerAnker && zeile.trim() !== "")
127
+ offenerAnker = null;
128
+ if (aktuell === null)
129
+ continue;
130
+ if (istHinweis(zeile)) {
131
+ // Nur der Block DIREKT unter der Ueberschrift ist unser Hinweis;
132
+ // spaetere `>`-Zeilen fallen weg, gehoeren aber auch nicht zur Antwort.
133
+ if (hinweisNochOffen)
134
+ hinweise.push(zeile.trimStart().slice(1).trim());
135
+ continue;
136
+ }
137
+ // ⚠️ SCHON EINE LEERZEILE BEENDET DEN HINWEISBLOCK -- woertlich dieselbe
138
+ // Grenze wie im Erzeuger (`katalogAus` in `scripts/arbeitsplatz-inhalt.mts`).
139
+ // Zoege sie hier weiter, kaeme aus einer Datei mit einem Zitat weiter unten
140
+ // ein anderer Hinweis heraus als in der App, und `check:kontext` haette
141
+ // recht, wenn er rot wird.
142
+ hinweisNochOffen = false;
143
+ antwort.push(zeile);
144
+ }
145
+ schliesse();
146
+ return fragen;
147
+ }
148
+ /**
149
+ * Sieht dieser Text aus wie die Datei, die er sein soll?
150
+ *
151
+ * πŸ”΄ **DER ABBRUCH VON `kimal3 kontext hoch` HAENGT AN DIESER ZEILE** (Spec,
152
+ * Abschnitt 11). Ohne sie schickt der Befehl JEDEN Dateiinhalt hoch, den er an
153
+ * der Stelle findet. Die App zerlegt ihn dann gegen den Katalog DIESER Datei,
154
+ * findet keine einzige Antwort -- und legt den kompletten Text als "Zusatz"
155
+ * ab. Die Firma haette danach einen Zusatz statt eines Firmentextes, und die
156
+ * CLI haette dazu einen Haken gemeldet. *Ein Fehlerfall darf nie denselben
157
+ * Namen tragen wie ein Erfolgsfall.*
158
+ *
159
+ * πŸ”‘ **WORAN ERKANNT WIRD: an mindestens einem Anker, der zu DIESER Datei
160
+ * gehoert.** Die Anker sind je Datei benannt (`marke_anrede`, `firma_angebot`,
161
+ * `kurz_kunden` …), und `FIRMEN_TEXTE` (`kontext-stand.ts`) fuehrt je Datei
162
+ * ihren Praefix. Damit faellt nicht nur die voellig fremde Datei durch, sondern
163
+ * auch der haeufigere Unfall: `marke.md` ueber `firma.md` kopiert. Ein blosses
164
+ * "irgendein Anker steht drin" haette genau den durchgewunken.
165
+ *
166
+ * ⚠️ **Dass die Praefixe wirklich passen, ist keine Verabredung, sondern eine
167
+ * Messung:** `check:kontext` haelt jeden Anker aus `FRAGEN` (der App) gegen den
168
+ * Praefix, den `FIRMEN_TEXTE` fuer seine Datei nennt.
169
+ *
170
+ * πŸ“Œ **Der Preis, ausgesprochen:** Wer ALLE Anker einer Datei loescht und nur
171
+ * die Ueberschriften stehen laesst, bekommt hier ein Nein, obwohl die App den
172
+ * Text noch am Wortlaut erkennen wuerde. Das ist die sichere Seite: Es geht
173
+ * nichts verloren, die Meldung sagt warum, und `kimal3 kontext runter` stellt
174
+ * die Datei wieder her. Die andere Seite waere der stille Datenverlust oben.
175
+ */
176
+ export function traegtFragenVon(text, ankerPraefix) {
177
+ return fragenAus(text).some((f) => f.anker.startsWith(ankerPraefix));
178
+ }
@@ -0,0 +1,85 @@
1
+ import { mkdirSync, readFileSync, writeFileSync } from "node:fs";
2
+ import { homedir } from "node:os";
3
+ import { join } from "node:path";
4
+ /**
5
+ * Welche Nachricht an welchem Tag schon im Terminal stand.
6
+ *
7
+ * πŸ”‘ WARUM EINE EIGENE DATEI UND NICHT IN anmeldung.json: `kimal3 logout`
8
+ * loescht die Anmeldedatei. Laege der Merkzettel darin, kaeme nach jedem
9
+ * Abmelden jede Nachricht von vorn -- und ausgerechnet der Mensch, der sich
10
+ * gerade neu anmeldet, bekaeme die Wand.
11
+ *
12
+ * πŸ”΄ BEIDE FUNKTIONEN WERFEN NIE. Ein kaputter oder schreibgeschuetzter
13
+ * Merkzettel darf keinen Befehl scheitern lassen (Spec US-8). Schlimmstenfalls
14
+ * sieht der Kunde denselben Hinweis zweimal -- das ist kein Schaden, ein
15
+ * abgebrochener `projekt neu` schon.
16
+ */
17
+ const ORDNER = join(homedir(), ".kimal3");
18
+ const DATEI = join(ORDNER, "gesehen.json");
19
+ export function leseGesehen() {
20
+ try {
21
+ const roh = JSON.parse(readFileSync(DATEI, "utf8"));
22
+ if (!roh || typeof roh !== "object" || Array.isArray(roh))
23
+ return {};
24
+ const raus = {};
25
+ for (const [k, v] of Object.entries(roh)) {
26
+ // Nur Zeichenketten uebernehmen. Wer die Datei von Hand bearbeitet hat,
27
+ // soll damit hoechstens seinen eigenen Merkzettel verlieren.
28
+ if (typeof v === "string")
29
+ raus[k] = v;
30
+ }
31
+ return raus;
32
+ }
33
+ catch {
34
+ return {};
35
+ }
36
+ }
37
+ export function merkeGesehen(id, tag) {
38
+ try {
39
+ const alles = leseGesehen();
40
+ /*
41
+ πŸ”‘ ERST LOESCHEN, DANN SETZEN -- damit der Eintrag ans ENDE rutscht.
42
+ Ohne die erste Zeile behaelt ein schon vorhandener Schluessel seinen
43
+ alten Platz, und `slice(-20)` unten wirft dann ausgerechnet den weg, den
44
+ wir gerade geschrieben haben (Befund der vierten Review-Runde: der
45
+ Kommentar unten versprach genau das Gegenteil).
46
+
47
+ Die Folge waere klein -- derselbe Hinweis erschiene einmal zu viel --,
48
+ aber sie steht im Widerspruch zu einer Zusage im Code. Und eine Zusage
49
+ im Code ist das, was der naechste Leser glaubt, statt nachzurechnen.
50
+ */
51
+ delete alles[id];
52
+ alles[id] = tag;
53
+ /*
54
+ ⚠️ Nur die letzten 20 behalten. Ohne diese Zeile waechst die Datei mit
55
+ jeder je gezeigten Nachricht -- langsam, aber ohne Ende, und niemand
56
+ raeumt sie je auf.
57
+
58
+ πŸ”‘ Warum das gefahrlos ist: `Object.entries` gibt Zeichenketten-Schluessel
59
+ in Einfuegereihenfolge zurueck, und der gerade gesetzte Eintrag ist immer
60
+ dabei -- entweder als letzter (neu) oder an seinem alten Platz
61
+ (aktualisiert). Was hinten abfaellt, ist die aelteste Nachricht; die
62
+ schlimmste Folge waere, dass sie noch einmal erscheint.
63
+ */
64
+ const paare = Object.entries(alles).slice(-20);
65
+ /*
66
+ ⚠️ `mode: 0o700` wie in anmeldung.ts -- und das ist KEINE Kosmetik
67
+ (Befund der ersten Review-Runde): `mkdirSync` mit `recursive: true`
68
+ aendert die Rechte eines Ordners, den es schon gibt, NICHT MEHR. Wer
69
+ diesen Ordner also zuerst anlegt, legt seine Rechte fuer immer fest.
70
+
71
+ Der Fall ist echt: Ein Mensch mit altem Skill, aber ohne Anmeldung,
72
+ tippt `kimal3` -- der Skill-Hinweis feuert, dieser Merkzettel entsteht,
73
+ und `~/.kimal3` bekaeme die Vorgabe-Rechte (auf einem gemeinsam
74
+ genutzten Rechner meist 0755). Ein spaeteres `kimal3 login` koennte sie
75
+ nicht mehr enger ziehen. Der Ausweis selbst bliebe zwar 0600 -- aber die
76
+ Haertung des Ordners waere still verloren, und "still" ist hier das
77
+ Schlimme.
78
+ */
79
+ mkdirSync(ORDNER, { recursive: true, mode: 0o700 });
80
+ writeFileSync(DATEI, JSON.stringify(Object.fromEntries(paare), null, 2), "utf8");
81
+ }
82
+ catch {
83
+ /* Merkzettel nicht schreibbar -- der Befehl laeuft trotzdem durch. */
84
+ }
85
+ }
package/dist/kimal3.js CHANGED
@@ -11,17 +11,24 @@
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 { readFileSync } from "node:fs";
14
+ import { existsSync, readFileSync } from "node:fs";
15
15
  import { BASIS_URL, leseAnmeldung } from "./anmeldung.js";
16
- import { NichtAngemeldet } from "./api.js";
16
+ import { gemerkterHinweis, NichtAngemeldet } from "./api.js";
17
+ import { leseGesehen, merkeGesehen } from "./gesehen.js";
18
+ import { formatiere, heuteAus, skillMeldung, skillZustand, sollZeigen, } from "./nachricht.js";
19
+ import { eigeneVersion } from "./version.js";
17
20
  import { hilfeText } from "./befehle-katalog.js";
18
21
  import { arbeitsplatzNeu } from "./befehle/arbeitsplatz.js";
22
+ import { werkbankNeu } from "./befehle/werkbank.js";
19
23
  import { firmaDaten } from "./befehle/firma-daten.js";
24
+ import { kontextFragen } from "./befehle/kontext-fragen.js";
25
+ import { kontextRunter } from "./befehle/kontext-runter.js";
26
+ import { kontextHoch } from "./befehle/kontext-hoch.js";
20
27
  import { login } from "./befehle/login.js";
21
28
  import { logout } from "./befehle/logout.js";
22
29
  import { projektListe } from "./befehle/projekt-liste.js";
23
30
  import { projektNeu } from "./befehle/projekt-neu.js";
24
- import { skillInstall } from "./befehle/skill-install.js";
31
+ import { skillDatei, skillInstall } from "./befehle/skill-install.js";
25
32
  // πŸ“Œ Kein handgeschriebener Block mehr (2026-08-18): Die Liste wohnt in
26
33
  // `befehle-katalog.ts` und wird von dort AUCH im Pro-Bereich der App angezeigt.
27
34
  // Zwei Listen ueber dieselben Befehle driften β€” die eine bekaeme den naechsten
@@ -57,20 +64,167 @@ const HILFE = hilfeText();
57
64
  * eine Ebene unter `cli/`.
58
65
  */
59
66
  function versionsZeilen() {
60
- let version = "unbekannt";
67
+ // πŸ“Œ Seit dem 2026-08-21 wird die Nummer NICHT mehr hier gelesen, sondern in
68
+ // `version.ts` -- sie hat einen zweiten Aufrufer bekommen (den Skill-Stempel
69
+ // in `skill install`). Zwei Lesestellen fuer dieselbe Zahl waeren genau das
70
+ // Muster, gegen das der Kommentar oben schon einmal geschrieben wurde.
71
+ //
72
+ // "unbekannt" bleibt hier stehen und wandert NICHT in `version.ts`: Diese
73
+ // Zeile zeigt einem Menschen etwas an, und da ist "unbekannt" die ehrliche
74
+ // Antwort. Der andere Aufrufer RECHNET mit dem Wert und braucht dort das
75
+ // Gegenteil -- er will lieber gar keine Nummer als eine erfundene.
76
+ const version = eigeneVersion() ?? "unbekannt";
77
+ return ` kimal3 ${version}\n spricht mit ${BASIS_URL}`;
78
+ }
79
+ /**
80
+ * Welcher Update-Hinweis soll ans Ende -- oder keiner?
81
+ *
82
+ * πŸ”‘ WARUM stderr (entschieden wird es erst beim Schreiben, gemeint ist es
83
+ * hier): Dort stoert der Hinweis keine Weiterverarbeitung --
84
+ * `kimal3 firma daten > firma.json` bleibt sauberes JSON --, und Claude sieht
85
+ * ihn trotzdem, denn es liest beide Kanaele.
86
+ *
87
+ * πŸ”΄ WARUM ALLES IN EINEM try STECKT: Diese Funktion laeuft NACH dem
88
+ * eigentlichen Befehl. Wenn sie wirft, faellt ein bereits erfolgreicher Befehl
89
+ * auf Rueckgabewert 3 -- und Claude gaebe den Rat "gleich noch einmal" fuer
90
+ * etwas, das laengst erledigt ist. Die Zahlen sind laut dem Kopf dieser Datei
91
+ * die eigentliche Schnittstelle; ein Nebenschauplatz darf sie nicht anfassen.
92
+ */
93
+ function hinweisText() {
61
94
  try {
62
- const roh = JSON.parse(readFileSync(new URL("../package.json", import.meta.url), "utf8"));
63
- if (roh && typeof roh === "object" && "version" in roh) {
64
- const v = roh.version;
65
- if (typeof v === "string")
66
- version = v;
95
+ const eigene = eigeneVersion();
96
+ // Ohne eigene Nummer ist jeder Vergleich geraten -- dann lieber schweigen.
97
+ // Das ist der Fall "package.json unlesbar", und da hat der Kunde groessere
98
+ // Sorgen als einen Update-Hinweis.
99
+ if (eigene === null)
100
+ return null;
101
+ const heute = heuteAus(new Date());
102
+ const h = gemerkterHinweis();
103
+ if (h && sollZeigen(h, eigene, leseGesehen(), heute)) {
104
+ // Hoechstens EIN Hinweis je Aufruf -- zwei sind eine Wand.
105
+ return { text: formatiere(h), kennung: h.id, tag: heute };
106
+ }
107
+ /*
108
+ Weg 2: ohne Netz.
109
+ ⚠️ Er greift immer dann, wenn oben NICHTS gezeigt wird -- nicht nur,
110
+ wenn gar kein Netz-Hinweis ankam (praezisiert nach der dritten
111
+ Review-Runde). Auch "die Nachricht stand heute schon da" und "sie gilt
112
+ fuer diese Fassung nicht" fuehren hierher. Das ist gewollt: Es bleibt
113
+ bei einem Hinweis je Aufruf.
114
+ *Hier stand vorher "nur, wenn kein Netz-Hinweis kam" -- eine Bedingung,
115
+ die der Code nicht hat. Genau der Kommentartyp, den dieses Projekt schon
116
+ zweimal teuer bezahlt hat.*
117
+ */
118
+ // Der Pfad kommt aus `skill-install.ts` -- dort wird er geschrieben, hier
119
+ // gelesen. Zwei Zusammenbauten desselben Ortes driften, und der zweite
120
+ // verrottet lautlos (Befund der vierten Review-Runde).
121
+ const datei = skillDatei();
122
+ const inhalt = existsSync(datei) ? readFileSync(datei, "utf8") : null;
123
+ if (skillZustand(inhalt, eigene) === "veraltet") {
124
+ // Auch dieser Hinweis nur einmal am Tag, unter einer festen Kennung.
125
+ // Sie kann mit keiner Nachrichten-UUID kollidieren.
126
+ const gesehen = leseGesehen();
127
+ if (gesehen["skill-veraltet"] !== heute) {
128
+ return { text: skillMeldung(), kennung: "skill-veraltet", tag: heute };
129
+ }
67
130
  }
68
131
  }
69
132
  catch {
70
- // Kein Absturz, wenn die Datei fehlt: "unbekannt" ist eine ehrliche
71
- // Antwort, ein Stapelabzug waere keine.
133
+ /* Ein Nebenschauplatz darf den Befehl nicht umwerfen. */
134
+ }
135
+ return null;
136
+ }
137
+ /**
138
+ * Den Hinweis schreiben und ERST DANN aussteigen.
139
+ *
140
+ * πŸ”΄ WARUM NICHT EINFACH `console.error` UND `process.exit`: Auf Windows ist
141
+ * `process.stderr` auch bei Roehren und Terminals ASYNCHRON, und
142
+ * `process.exit()` wirft ausstehende Schreibvorgaenge weg (Befund der ersten
143
+ * Review-Runde). Der Hinweis ist das Letzte, was geschrieben wird -- er waere
144
+ * damit genau das Erste, was verlorengeht. *Ein Merkmal, dessen ganzer Wert an
145
+ * der letzten Zeile haengt, darf diese Zeile nicht dem Zufall ueberlassen.*
146
+ *
147
+ * `process.exitCode` wird vorher gesetzt: Sollte der Rueckruf wider Erwarten
148
+ * ausbleiben und der Prozess von selbst enden, stimmt die Zahl trotzdem -- und
149
+ * die Zahl ist laut dem Kopf dieser Datei die eigentliche Schnittstelle.
150
+ */
151
+ function endeMitHinweis(code) {
152
+ try {
153
+ process.exitCode = code;
154
+ /*
155
+ πŸ”΄ NUR NACH EINEM GELUNGENEN BEFEHL (Befund der sechsten Review-Runde).
156
+ `haupt()` WIRFT bei den meisten Fehlern nicht, sondern gibt eine Zahl
157
+ zurueck -- 1 fuer einen unbekannten Befehl, 2 und 4 aus den Befehlen
158
+ selbst. Der Hinweis landete damit unter Fehlermeldungen, obwohl der
159
+ `.catch`-Zweig unten und der Wrapper auf dem Server beide das Gegenteil
160
+ begruenden: "Eine Fehlermeldung ist der schlechteste Augenblick fuer eine
161
+ Nebenbemerkung."
162
+
163
+ ⚠️ Schlimmer als die Optik ist die Nebenwirkung: Der gescheiterte Lauf
164
+ verbrauchte den EINEN Platz des Tages. Wer sich vertippt und danach
165
+ richtig tippt, saehe die Nachricht dann heute gar nicht mehr.
166
+ */
167
+ const raus = code === 0 ? hinweisText() : null;
168
+ /*
169
+ πŸ”΄ BEIDE KANAELE LEEREN, NICHT NUR STDERR. Die erste Fassung dieser
170
+ Funktion sicherte stderr -- und liess den haeufigeren Weg (kein Hinweis)
171
+ bei einem nackten `process.exit` stehen. Genau der Fehler, gegen den sie
172
+ geschrieben war, stand also noch daneben.
173
+
174
+ Es trifft naemlich vor allem STDOUT: `firma daten` schreibt ein
175
+ mehrere Kilobyte grosses JSON mit `console.log` und landet dann hier.
176
+ Auf Windows sind beide Kanaele asynchron; `process.exit()` wirft
177
+ Ausstehendes weg. Das Ergebnis waere das schlimmstmoegliche: ein
178
+ abgeschnittenes JSON MIT Rueckgabewert 0 -- Claude liest es als
179
+ gelungene Auskunft.
180
+
181
+ πŸ”‘ Warum ueberhaupt noch `process.exit` und kein natuerliches Ende:
182
+ Nach einem `fetch` haelt Node seine Verbindungen eine Weile offen. Der
183
+ Befehl wuerde sekundenlang scheinbar haengen, nachdem er fertig ist.
184
+
185
+ Die Notbremse ist `unref`t: Sie haelt die Schleife nicht am Leben,
186
+ feuert aber, falls ein Kanal wirklich haengt. Und weil `exitCode` schon
187
+ oben steht, stimmt die Zahl in JEDEM dieser Wege -- auch dann, wenn der
188
+ Prozess von selbst endet.
189
+
190
+ πŸ”΄ UND DIE REIHENFOLGE IST DER PUNKT (Befund der sechsten Review-Runde):
191
+ ERST stdout leeren, DANN den Hinweis auf stderr schreiben. Die beiden
192
+ Kanaele haben eigene Puffer, und ihre Reihenfolge ist nur dann sicher,
193
+ wenn beide synchron sind -- auf einer POSIX-Roehre ist stdout es NICHT.
194
+ Bei `kimal3 firma daten 2>&1 | tee firma.json` mit mehr als 64 KiB
195
+ Nutzlast koennte die kleine stderr-Zeile also VOR dem grossen JSON
196
+ ankommen und mitten hineinrutschen. Genau das verbietet US-1 ("nie
197
+ mittendrin"), und es waere eine Ausgabe, die SKILL.md als Firmendaten
198
+ zu lesen anweist.
199
+ πŸ“Œ Auf Windows liess es sich nicht herstellen -- dort sind Roehren
200
+ synchron. *Eine Reihenfolge, die nur auf dem Rechner haelt, auf dem man
201
+ sie gemessen hat, ist keine Reihenfolge, sondern ein Zufall.*
202
+ */
203
+ setTimeout(() => process.exit(code), 2000).unref();
204
+ process.stdout.write("", () => {
205
+ if (!raus) {
206
+ process.stderr.write("", () => process.exit(code));
207
+ return;
208
+ }
209
+ // Erst schreiben, DANN vermerken -- und nur, wenn kein Fehler zurueckkam.
210
+ process.stderr.write(`${raus.text}\n`, (fehler) => {
211
+ if (!fehler)
212
+ merkeGesehen(raus.kennung, raus.tag);
213
+ process.exit(code);
214
+ });
215
+ });
216
+ }
217
+ catch {
218
+ /*
219
+ ⚠️ Auch das Schreiben selbst kann werfen -- ein geschlossener
220
+ Ausgabekanal genuegt. Ohne dieses `catch` liefe der Wurf in den
221
+ `.catch`-Zweig unten, und der macht aus einem GELUNGENEN Befehl eine
222
+ 3 ("gleich noch einmal"). Das ist genau das, was der Kommentar an
223
+ `hinweisText` verspricht, dass es nie passiert -- versprochen war es
224
+ dort aber nur fuer die halbe Strecke.
225
+ */
226
+ process.exit(code);
72
227
  }
73
- return ` kimal3 ${version}\n spricht mit ${BASIS_URL}`;
74
228
  }
75
229
  /**
76
230
  * Was diese Zeile sagen KANN β€” und was nicht.
@@ -126,11 +280,22 @@ async function haupt() {
126
280
  return skillInstall();
127
281
  if (argv[0] === "arbeitsplatz" && argv[1] === "neu")
128
282
  return arbeitsplatzNeu(argv.slice(2));
283
+ if (argv[0] === "werkbank" && argv[1] === "neu")
284
+ return werkbankNeu(argv.slice(2));
285
+ if (argv[0] === "kontext" && argv[1] === "runter")
286
+ return kontextRunter(argv.slice(2));
287
+ if (argv[0] === "kontext" && argv[1] === "hoch")
288
+ return kontextHoch(argv.slice(2));
289
+ if (argv[0] === "kontext" && argv[1] === "fragen")
290
+ return kontextFragen(argv.slice(2));
129
291
  console.error(`Den Befehl β€ž${wort}" kenne ich nicht.\n${HILFE}`);
130
292
  return 1;
131
293
  }
132
294
  haupt()
133
- .then((code) => process.exit(code))
295
+ .then((code) => endeMitHinweis(code))
296
+ // ⚠️ Im Fehlerzweig bewusst KEIN Hinweis. Wer gerade eine Fehlermeldung
297
+ // liest, braucht keine zweite Zeile ueber Updates -- und ein Netz-Hinweis
298
+ // kann hier ohnehin nicht angekommen sein, denn genau das Netz hat versagt.
134
299
  .catch((f) => {
135
300
  if (f instanceof NichtAngemeldet) {
136
301
  console.error("Bitte zuerst `kimal3 login` ausfΓΌhren.");