kimal3 0.1.6 → 0.2.1

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 CHANGED
@@ -37,6 +37,9 @@ kimal3 login Diesen Rechner mit deinem Konto verbinden
37
37
  kimal3 skill install Claude beibringen, dieses Werkzeug zu benutzen — und dir die /k3-Befehle geben
38
38
  kimal3 arbeitsplatz neu Den ganzen KI-Arbeitsplatz anlegen — Firmenebene und Werkbank
39
39
  kimal3 werkbank neu Nur eine eigene Werkbank in einer bestehenden Firma anlegen
40
+ kimal3 kontext runter Den Firmenkontext holen — CLAUDE.md und die fünf Dateien in kontext/
41
+ kimal3 kontext fragen Zeigen, welche Fragen über deine Firma noch unbeantwortet sind
42
+ kimal3 kontext hoch Deine lokalen Änderungen an den fünf Kontext-Dateien hochschicken
40
43
  kimal3 projekt neu … Ein Projekt ins Erfolgsprofil eintragen
41
44
  kimal3 projekt liste Deine eingetragenen Projekte anzeigen
42
45
  kimal3 firma daten Die Daten deiner Firma holen, damit Claude sie auswerten kann
package/dist/api.js CHANGED
@@ -1,9 +1,97 @@
1
1
  import { BASIS_URL, leseAnmeldung } from "./anmeldung.js";
2
2
  import { hinweisAusAntwort } from "./nachricht.js";
3
3
  import { eigeneVersion } from "./version.js";
4
+ import { findeArbeitsplatz, leseMerkzettelDatei } from "./kontext-dateien.js";
5
+ import { bekannterStandAus, istKopfwertSicher } from "./kontext-stand.js";
4
6
  export class NichtAngemeldet extends Error {
5
7
  constructor() { super("nicht angemeldet"); }
6
8
  }
9
+ /**
10
+ * Der EINE Weg, auf dem diese Datei den lokalen Merkzettel liest -- Firmen-ID
11
+ * und bekannter Stand, oder `null`, wenn sich beides nicht sicher bilden
12
+ * laesst (Task 10, Step 1).
13
+ *
14
+ * 🔑 Zwei Verbraucher, EINE Leseart (A-4): `kontextStandKopf()` baut daraus
15
+ * den Kopf `X-Kimal3-Kontext-Stand` VOR dem Netzaufruf,
16
+ * `lokalerKontextStandJetzt()` liefert denselben Stand am ENDE des Befehls an
17
+ * den Update-Hinweis. Eine zweite, eigene Leseart waere eine zweite Wahrheit
18
+ * ueber dieselbe Datei.
19
+ *
20
+ * ⚠️ WIRFT NIE -- dieselbe Regel wie beim Update-Kanal insgesamt: Ein
21
+ * Nebenschauplatz darf keinen Befehl umwerfen. `findeArbeitsplatz` und
22
+ * `leseMerkzettelDatei` werfen zwar selbst schon nicht (s. ihre eigenen
23
+ * Kommentare), das `try` ist trotzdem das Netz fuer den Fall, dass sich das
24
+ * einmal aendert.
25
+ *
26
+ * 🔴 **Fix-Runde 1, Fund 1:** `firma_id` und jeder `stand` kommen aus dem
27
+ * Merkzettel auf der Platte des Kunden -- einer Datei, die ausdruecklich zum
28
+ * Von-Hand-Aufmachen gedacht ist (s. Kopf-Kommentar an `pruefsumme` in
29
+ * `kontext-stand.ts`). `leseMerkzettel` prueft nur `typeof === "string"`, nie
30
+ * den INHALT. Ohne `istKopfwertSicher` wuerde ein einziges Nicht-ASCII-
31
+ * Zeichen darin `headers.set(...)` in `ruf()` unten zum Werfen bringen --
32
+ * und zwar bei JEDEM folgenden Befehl, nicht nur bei `kontext`-Befehlen. Der
33
+ * Kunde saehe "kimal3 ist gerade nicht erreichbar" und suchte im Netz, waehrend
34
+ * der Fehler auf seiner eigenen Platte sitzt.
35
+ *
36
+ * 🔑 **Warum stillschweigend `null`, keine Warnung:** Dieselbe Antwort wie
37
+ * bei den drei anderen Wirklichkeiten, die diese Funktion schon zu `null`
38
+ * zusammenfaltet (kein Arbeitsplatz, keine bekannte `firma_id` in allen drei
39
+ * Lesarten von Task 10a). Eine Warnung liefe bei JEDEM Befehl erneut, bis der
40
+ * Merkzettel neu geschrieben wird (naechstes `kontext runter`/`kontext hoch`
41
+ * heilt das automatisch) -- und ein Nebenschauplatz, der ploetzlich auf jedem
42
+ * Befehl eine unerwartete Zeile ausgibt, ist selbst ein Verstoss gegen die
43
+ * Regel, die ihn ueberhaupt rechtfertigt ("darf keinen Befehl umwerfen" ist
44
+ * eng verwandt mit "darf keinen Befehl verunstalten"). *Wie viele
45
+ * verschiedene Wirklichkeiten enden auf `null` -- und ist jede ein sicherer
46
+ * Ausgang?* Vier, und JEDE fuehrt zum selben, harmlosen Ergebnis: kein Kopf,
47
+ * kein Hinweis, der Befehl laeuft unveraendert weiter.
48
+ */
49
+ function lokaleKontextLage() {
50
+ try {
51
+ const ordner = findeArbeitsplatz(process.cwd());
52
+ if (ordner === null)
53
+ return null;
54
+ const merkzettel = leseMerkzettelDatei(ordner);
55
+ if (merkzettel.firma_id === null)
56
+ return null;
57
+ return { firmaId: merkzettel.firma_id, stand: bekannterStandAus(merkzettel) };
58
+ }
59
+ catch {
60
+ return null;
61
+ }
62
+ }
63
+ /**
64
+ * Welchen Firmenstand traegt der Merkzettel auf dieser Platte GERADE JETZT?
65
+ *
66
+ * 🔴 WOZU DAS GUT IST (offener Punkt A-4): Der Wert, den `kontextStandKopf()`
67
+ * unten in den Kopf schreibt, wird VOR dem `fetch` gelesen -- er ist der
68
+ * Stand, den die CLI hatte, als der Befehl begann. Genau daran hing der
69
+ * Fehler: Nach `kimal3 kontext runter` riet der Hinweis am Ende zu genau dem
70
+ * Befehl, der gerade gelaufen war. Diese Funktion ist deshalb kein zweiter
71
+ * Weg zum selben Wert, sondern derselbe Weg zu einem SPAETEREN Zeitpunkt --
72
+ * ausgelesen wird sie in `hinweisText()` (`kimal3.ts`), also nachdem der
73
+ * Befehl den Merkzettel neu geschrieben hat.
74
+ *
75
+ * ⚠️ SIE DARF NICHTS ZWISCHENSPEICHERN. Ein gemerkter Wert waere wieder der
76
+ * Stand von vorhin, und der ganze Fix waere umsonst -- deshalb liest sie bei
77
+ * jedem Aufruf erneut von der Platte. `check:nachrichten` misst genau das.
78
+ *
79
+ * `null` heisst "wir wissen den Stand nicht" (kein Arbeitsplatz, keine
80
+ * Firmen-ID im Merkzettel, Datei unlesbar) -- und "wir wissen es nicht" darf
81
+ * nie einen Hinweis unterdruecken (s. `hinweisErledigt` in `nachricht.ts`).
82
+ */
83
+ export function lokalerKontextStandJetzt() {
84
+ return lokaleKontextLage()?.stand ?? null;
85
+ }
86
+ function kontextStandKopf() {
87
+ const lage = lokaleKontextLage();
88
+ if (lage === null)
89
+ return null;
90
+ const wert = `${lage.firmaId}:${lage.stand}`;
91
+ if (!istKopfwertSicher(wert))
92
+ return null;
93
+ return wert;
94
+ }
7
95
  /**
8
96
  * Ein Aufruf gegen die App. `mitAusweis` haengt den Schluessel an.
9
97
  *
@@ -40,6 +128,30 @@ export async function ruf(pfad, optionen = {}) {
40
128
  const version = eigeneVersion();
41
129
  if (version !== null)
42
130
  kopf["X-Kimal3-Version"] = version;
131
+ /*
132
+ 🔴 DER ZWEITE KOPF (Task 10) -- der lokal bekannte Firmenstand, damit der
133
+ Server einen firmenbezogenen Hinweis anhaengen kann ("deine Firma hat den
134
+ Kontext geaendert"). Der Server weiss nicht, was auf DIESER Platte liegt
135
+ -- deshalb schickt die CLI ihn mit, statt dass der Server raet.
136
+
137
+ ⚠️ ASCII PFLICHT, aus demselben Grund wie der Versions-Kopf oben: die
138
+ Firmen-ID ist eine UUID, der Stand ein ISO-Zeitstempel oder `"leer"" --
139
+ beides ASCII. Der Firmenname geht NIE hier mit, nur die ID; den Namen
140
+ loest der Server fuer den Hinweistext selbst auf (Step 4, US-8).
141
+
142
+ 🔴 GAR KEIN KOPF, wenn sich kein Arbeitsplatz findet ODER der lokale
143
+ Merkzettel keine Firmen-ID kennt -- letzteres kollabiert bewusst DREI
144
+ Wirklichkeiten in einen Wert (altes Merkzettel-Format ohne das Feld,
145
+ kaputtes JSON, ausdruecklich `null`, s. `leseMerkzettel` in
146
+ `kontext-stand.ts`). Diese Funktion unterscheidet sie nicht: Alle drei
147
+ heissen "wir wissen die Firma nicht sicher", und "kein Kopf" ist in
148
+ JEDEM der drei Faelle die richtige, sichere Antwort -- kein Hinweis
149
+ bleibt der Vorgabewert, auch wenn er einen der drei Faelle zu Unrecht
150
+ (aber folgenlos) mit erwischt.
151
+ */
152
+ const kontextKopf = kontextStandKopf();
153
+ if (kontextKopf !== null)
154
+ kopf["X-Kimal3-Kontext-Stand"] = kontextKopf;
43
155
  const methode = optionen.methode ?? "POST";
44
156
  // Content-Type nur, wenn wirklich etwas im Rumpf steht. Ein GET mit
45
157
  // "Content-Type: application/json" und ohne Rumpf ist eine Luege ueber die
@@ -1,7 +1,36 @@
1
1
  import { existsSync, mkdirSync, readdirSync, statSync, writeFileSync } from "node:fs";
2
2
  import { dirname, join, resolve } from "node:path";
3
- import { feld, ruf } from "./api.js";
4
- import { schlussFehler } from "./fehler.js";
3
+ import { ruf } from "./api.js";
4
+ /*
5
+ 🔴 **Ja, hierher -- und nicht in eine zweite eigene Absage** (Punkt A-3,
6
+ 2026-08-26). `absage` ist die Ausgabe, die `kimal3 kontext runter` und
7
+ `kimal3 kontext hoch` schon benutzen: Sie trennt 402 vor 401, schickt den
8
+ Rest an `schlussFehler` und nennt bei `409` die eigenen Firmen als **fertige
9
+ Befehle zum Kopieren**. Genau das braucht `arbeitsplatz neu` jetzt auch.
10
+
11
+ Der bequeme Weg waere gewesen, die vier Zweige hier abzutippen. Dann gaebe es
12
+ zwei Absage-Sprachen fuer dieselbe Frage („welche Firma meinst du?") -- und
13
+ der Kunde muesste zweimal dasselbe lernen. Der Preis ist ein Import aus
14
+ `befehle/` in ein Modul darueber.
15
+
16
+ 🔴 **DIESER IMPORT SCHLIESST EINEN KREIS -- und der Kommentar behauptete hier
17
+ einen Nachmittag lang das Gegenteil** („er ist kreisfrei"). Nachgemessen
18
+ (Review-Befund 4, 2026-08-26): `arbeitsplatz-dateien` -> `befehle/
19
+ kontext-gemeinsam` -> `kontext-dateien` (holt `findeArbeitsplatz`) ->
20
+ `arbeitsplatz-dateien` (holt `istUnsicherenPfad`). Der Kreis ist da.
21
+
22
+ ⚠️ **Er ist heute harmlos, und nur deshalb:** In keinem der drei Module
23
+ laeuft beim LADEN etwas, das ein anderes braucht -- es werden ausschliesslich
24
+ Funktionen definiert und erst beim Aufruf benutzt. Wer hier oder in einem der
25
+ beiden anderen Module Arbeit auf die oberste Ebene legt (eine Konstante, die
26
+ eine importierte Funktion aufruft), bekommt `undefined` statt einer Funktion,
27
+ und zwar erst zur Laufzeit beim Kunden.
28
+
29
+ *Ein Kommentar, der Kreisfreiheit behauptet, laedt genau dazu ein* -- er
30
+ liest sich wie eine gepruefte Zusage. Deshalb steht der Kreis jetzt hier,
31
+ statt weg-behauptet zu werden.
32
+ */
33
+ import { absage } from "./befehle/kontext-gemeinsam.js";
5
34
  /*
6
35
  🔁 Die Ableitung „welcher Unterordner ist die Werkbank" wohnt seit dem
7
36
  2026-08-20 in `werkbank-name.ts` -- gemeinsam mit der App, die sie fuer `/pro`
@@ -10,6 +39,34 @@ import { schlussFehler } from "./fehler.js";
10
39
  Kopien des Namens, nur besser getarnt.*
11
40
  */
12
41
  export { werkbankAusDateien as werkbankOrdner } from "./werkbank-name.js";
42
+ /**
43
+ * Die Optionen, die `arbeitsplatz neu` und `werkbank neu` zur Firmenwahl
44
+ * annehmen -- **dieselbe Liste** wie bei `kontext runter`/`kontext hoch`,
45
+ * weitergereicht statt abgeschrieben.
46
+ *
47
+ * ⚠️ Zwei eigene Listen hätten dieselbe Frage („welche Firma meinst du?") bei
48
+ * vier Befehlen aus zwei Quellen beantwortet. Eine davon würde eines Tages
49
+ * `--firmen-id` heißen, und niemand merkte es beim Lesen.
50
+ */
51
+ export { ERLAUBT as FIRMEN_OPTIONEN } from "./befehle/kontext-gemeinsam.js";
52
+ /**
53
+ * Aus dem Wunsch die Suchparameter -- **rein**, damit `check:firmenwahl` sie
54
+ * ohne Netz messen kann.
55
+ *
56
+ * 🔑 `URLSearchParams` und keine selbstgebaute Zeichenkette: Ein Firmenname
57
+ * darf `&`, `=`, `?` und Leerzeichen enthalten (`Blume & Co`), und von Hand
58
+ * zusammengesetzt zerbräche daran die Anfrage -- ohne Fehlermeldung, mit einer
59
+ * Antwort für die falsche Firma.
60
+ */
61
+ export function firmenSuche(wunsch) {
62
+ const suche = new URLSearchParams();
63
+ if (wunsch.firma !== undefined)
64
+ suche.set("firma", wunsch.firma);
65
+ if (wunsch.firmaId !== undefined)
66
+ suche.set("firma-id", wunsch.firmaId);
67
+ const text = suche.toString();
68
+ return text === "" ? "" : `?${text}`;
69
+ }
13
70
  /**
14
71
  * Holt die Vorlage von der App — **eine** Route für beide Befehle.
15
72
  *
@@ -17,9 +74,20 @@ export { werkbankAusDateien as werkbankOrdner } from "./werkbank-name.js";
17
74
  * Dateien, dieselbe Pro-Prüfung, derselbe Ausweis. Eine zweite Route wäre eine
18
75
  * zweite Tür, die man einzeln absichern und einzeln nachmessen müsste — für
19
76
  * exakt denselben Inhalt. Der Ausschnitt entsteht hier auf dem Rechner.
77
+ *
78
+ * 🔴 **Beide Parameter sind PFLICHT** (Punkt A-3, 2026-08-26). `wunsch` könnte
79
+ * bequem entfallen, wenn niemand eine Firma nennt -- und genau so bleibt eine
80
+ * gebaute Rechnung an der Aufrufstelle tot, ohne dass ein Compiler oder ein
81
+ * Wächter etwas merkt (dieselbe Falle wie bei `lageFuerDatei`, Punkt A-1).
82
+ * `befehl` nennt den Befehl, den die 409-Absage zum Kopieren anbietet -- stünde
83
+ * dort fest `kontext runter`, bekäme ein Mensch bei `werkbank neu` einen Rat,
84
+ * der seine Werkbank nicht anlegt.
20
85
  */
21
- export async function vorlageHolen() {
22
- const antwort = await ruf("/api/cli/arbeitsplatz", { methode: "GET", mitAusweis: true });
86
+ export async function vorlageHolen(befehl, wunsch) {
87
+ const antwort = await ruf(`/api/cli/arbeitsplatz${firmenSuche(wunsch)}`, {
88
+ methode: "GET",
89
+ mitAusweis: true,
90
+ });
23
91
  if (antwort.status === 200) {
24
92
  const daten = alsVorlageAntwort(antwort.koerper);
25
93
  if (daten === null) {
@@ -28,24 +96,18 @@ export async function vorlageHolen() {
28
96
  }
29
97
  return { ok: true, daten };
30
98
  }
31
- const meldung = feld(antwort.koerper, "meldung") ?? "Das hat nicht geklappt.";
32
- // 402 VOR 401: Beide heissen "kommst hier nicht rein", verlangen aber
33
- // Gegenteiliges. Wer sie zusammenwirft, raet `kimal3 login` -- und der
34
- // meldet sich dann erfolgreich an, ohne dass es etwas aendert.
35
- if (antwort.status === 402) {
36
- console.error(meldung);
37
- return { ok: false, wert: 4 };
38
- }
39
- if (antwort.status === 401) {
40
- console.error(`${meldung} Bitte \`kimal3 login\` ausführen.`);
41
- return { ok: false, wert: 2 };
42
- }
43
- // 🔑 Den Rest entscheidet `schlussFehler` fuer alle Befehle gemeinsam --
44
- // insbesondere die `404` (die App kennt die Route noch nicht) und `429`/
45
- // `408` (ueberlastet, wiederholbar).
46
- const schluss = schlussFehler(antwort.status, meldung);
47
- console.error(schluss.text);
48
- return { ok: false, wert: schluss.wert };
99
+ /*
100
+ 🔴 **Hier standen dieselben vier Zweige noch einmal abgetippt** (402 vor
101
+ 401, Rest an `schlussFehler`) -- Zeichen für Zeichen das, was `absage`
102
+ ohnehin tut, nur ohne den `409`-Zweig. Dadurch fehlte genau die Auskunft,
103
+ um die es bei Punkt A-3 geht: welche Firma gemeint ist.
104
+
105
+ ⚠️ **Und das Weglassen war unsichtbar**, denn `409` kam von dieser Route
106
+ bis zum 2026-08-26 gar nicht -- sie hat die Frage still selbst entschieden.
107
+ *Ein Zweig, den es nie zu sehen gab, fehlt niemandem, bis der Server
108
+ anfängt zu fragen.*
109
+ */
110
+ return { ok: false, wert: absage(antwort, befehl) };
49
111
  }
50
112
  /** Prueft die Antwort-Form, ohne `any` -- eine falsche Form ist unlesbar, keine Ausnahme. */
51
113
  function alsVorlageAntwort(koerper) {
package/dist/argumente.js CHANGED
@@ -6,6 +6,156 @@
6
6
  * Uebergehen. Wer `--titl` tippt, soll es erfahren, statt ein Projekt ohne
7
7
  * Titel anzulegen.
8
8
  */
9
+ /**
10
+ * Trennt die **vorderen Woerter** von den Optionen.
11
+ *
12
+ * 🔴 **Warum es das seit dem 2026-08-26 gibt** (Punkt A-3): `arbeitsplatz neu`
13
+ * und `werkbank neu` haben ein Wort ohne `--` -- den Vornamen
14
+ * (`kimal3 arbeitsplatz neu sabine`). `zerlege` lehnt genau das ab, und zwar
15
+ * absichtlich (s. B-4 oben). Beide Befehle brauchen jetzt zusaetzlich
16
+ * `--firma`/`--firma-id`, also braucht es einen Schnitt VOR `zerlege`, statt
17
+ * dessen Strenge aufzuweichen.
18
+ *
19
+ * Geschnitten wird beim **ersten** Wort, das mit `--` beginnt. Alles davor
20
+ * sind Woerter, alles ab dort geht unveraendert an `zerlege` -- samt seiner
21
+ * Strenge: Ein Wort ohne `--` HINTER einer Option bleibt ein Fehler
22
+ * (`kimal3 arbeitsplatz neu --firma A muell`).
23
+ *
24
+ * ⚠️ **Nur `--`, nicht `-`.** Ein einzelnes `-x` bleibt ein Wort und laeuft in
25
+ * die Namenspruefung des Befehls. Das ist die heutige Bedeutung: `-h` faengt
26
+ * jeder Befehl selbst ab, bevor er hier ankommt, und ein `-force` soll die
27
+ * Meldung „damit kann ich hier nichts anfangen" bekommen und keine
28
+ * Options-Meldung ueber etwas, das gar keine Option ist.
29
+ */
30
+ export function trenneNamenUndOptionen(argv) {
31
+ const erste = argv.findIndex((teil) => teil.startsWith("--"));
32
+ if (erste === -1)
33
+ return { woerter: argv, optionen: [] };
34
+ return { woerter: argv.slice(0, erste), optionen: argv.slice(erste) };
35
+ }
36
+ /**
37
+ * Die getippten Optionen wieder zu einer Zeile machen, die man kopieren kann.
38
+ *
39
+ * 🔴 **Gegen Review-Befund 3 zu A-3 (2026-08-26).** Der Vorschlag bei einem
40
+ * mehrteiligen Namen entsteht, BEVOR die Optionen zerlegt sind -- er kannte
41
+ * also nur die Woerter. Aus `arbeitsplatz neu sabine mueller --firma "Blume &
42
+ * Co"` wurde der Rat `kimal3 arbeitsplatz neu sabine-mueller`, **ohne die
43
+ * Firma**. Wer ihn kopierte, lief demselben `409` ein zweites Mal in die Arme.
44
+ *
45
+ * 🔑 Das ist die Kehrseite der Sache, die derselbe Commit eine Bildschirmhoehe
46
+ * tiefer ausdruecklich richtig macht („der getippte Vorname reist in der Absage
47
+ * mit"). *Ein Rat, der die Haelfte der Eingabe vergisst, ist ein Rat in eine
48
+ * zweite Absage.*
49
+ *
50
+ * ⚠️ **Anfuehrungszeichen, wo sie noetig sind.** Ein Firmenname mit Leerzeichen
51
+ * („Blume & Co") ergaebe ohne sie eine Zeile, die beim Einfuegen in etwas ganz
52
+ * anderes zerfaellt. Geprueft wird auf Leerzeichen und auf das leere Wort --
53
+ * beides kommt aus einem echten Firmennamen, nicht aus einem Sonderfall.
54
+ */
55
+ export function alsBefehlszeile(optionen) {
56
+ /*
57
+ 🔴 **`(teil) => alsWort(teil)` und NICHT `map(alsWort)`** -- gefunden vom
58
+ eigenen Waechter, Minuten nach dem Einbauen (2026-08-27). `Array.map`
59
+ reicht jedem Aufruf DREI Werte: Element, Listenplatz, ganze Liste. Der
60
+ Listenplatz landete damit auf dem zweiten Parameter `immer` -- und weil
61
+ jeder Platz ausser dem ersten wahr ist, bekam ab dem zweiten Wort jedes
62
+ seine Anfuehrungszeichen: `--firma "Nordwind"` statt `--firma Nordwind`.
63
+ *Ein Parameter mit Vorgabewert ist eine offene Tuer fuer jeden, der
64
+ ungefragt ein zweites Argument mitschickt.*
65
+ */
66
+ return optionen.map((teil) => alsWort(teil)).join(" ");
67
+ }
68
+ /**
69
+ * EIN Wort so in eine Befehlszeile setzen, dass es beim Kopieren wieder genau
70
+ * dieses Wort ergibt.
71
+ *
72
+ * 🔴 **Gegen Befund 4 der zweiten Review-Runde (2026-08-27).** An NEUN Stellen
73
+ * stand dieselbe Zeile — `wert.includes(" ") ? `"${wert}"` : wert` — und alle
74
+ * neun hatten dieselbe Lücke: Ein Wert, der selbst ein Anführungszeichen
75
+ * enthält, sprengt die Anführungszeichen, die man um ihn herum setzt.
76
+ *
77
+ * Der Fall: `kimal3 arbeitsplatz neu sabine --firma 'Blume "Nord" GmbH'`. Der
78
+ * angebotene Rat lautete danach `--firma "Blume "Nord" GmbH"`. Wer ihn
79
+ * kopiert, gibt seiner Kommandozeile **drei** Wörter statt einem und bekommt
80
+ * eine andere, verwirrendere Absage als die, die er gerade gelesen hat.
81
+ * *Ein Rat, der beim Kopieren zerfällt, ist ein Rat in eine zweite Absage* —
82
+ * genau die Lehre, gegen die `alsBefehlszeile` oben gebaut wurde.
83
+ *
84
+ * 🔑 **Warum einfache Anführungszeichen und keine Maskierung mit `\`:**
85
+ * `'…'` bedeutet in beiden Kommandozeilen dasselbe, die auf den Rechnern
86
+ * unserer Kunden laufen — in der von Mac/Linux (`sh`) wie in der von Windows
87
+ * (PowerShell). Eine Maskierung mit `\"` gilt nur in der ersten; in
88
+ * PowerShell müsste man das Zeichen verdoppeln. *Eine Bauform, die nur auf
89
+ * einem der beiden Rechner hält, ist in einem Produkt für beide keine
90
+ * Bauform.*
91
+ *
92
+ * 🔑 **`immer` heisst „setz die Anfuehrungszeichen auch dann, wenn es ohne
93
+ * ginge".** Die Zeilen unter „Deine Firmen:" haben sie seit jeher, und dort
94
+ * gehoeren sie hin: Sie sind zum Kopieren UND zum Weitertippen gedacht -- wer
95
+ * „Suedwind" durch „Blume & Co" ersetzt, hat die Anfuehrungszeichen dann schon
96
+ * stehen. Beim Zusammenbauen einer getippten Zeile (`alsBefehlszeile`) und
97
+ * beim `cd` waere das umgekehrt eine Aenderung an dem, was der Kunde bisher
98
+ * sieht -- deshalb dort die Vorgabe `false`.
99
+ *
100
+ * ⚠️ **Die eine Lage, die offen bleibt:** ein Wert mit BEIDEN Sorten
101
+ * Anführungszeichen. Dafür gibt es keine Schreibweise, die in beiden
102
+ * Kommandozeilen dasselbe heißt. Der Rückfall maskiert nach `sh`-Regeln — und
103
+ * für genau diesen Fall gibt es ohnehin `--firma-id`, das ohne jedes
104
+ * Sonderzeichen auskommt.
105
+ */
106
+ export function alsWort(wert, immer = false) {
107
+ if (wert === "")
108
+ return '""';
109
+ /*
110
+ Was eine Kommandozeile nicht wörtlich stehen lässt: Leerzeichen (trennen),
111
+ Anführungszeichen beider Sorten (zitieren) -- und `$` wie ````
112
+ (rechnen).
113
+
114
+ ⚠️ Der Rückwärtsstrich ist bewusst NICHT dabei: In einem Firmennamen
115
+ kommt er nicht vor, und ihn mitzunehmen hieße, ihn auch maskieren zu
116
+ müssen -- was in PowerShell wieder falsch wäre.
117
+ */
118
+ /*
119
+ 🔴 **EINE ERLAUBNISLISTE, KEINE VERBOTSLISTE** (Befund 1 der FÜNFTEN
120
+ Review-Runde, 2026-08-27) -- und die Verbotsliste hatte drei Runden
121
+ gebraucht, um dreimal zu lecken: erst fehlten die Anführungszeichen, dann
122
+ `$` und das Akzent-Zeichen, dann `&`, `;`, `|`, Klammern, `<`, `>`.
123
+
124
+ Der Fall, der es aufgedeckt hat: eine Firma namens `Blume&Co` -- **ohne**
125
+ Leerzeichen, also fiel sie durch jede Prüfung. Die angebotene Zeile hieß
126
+ `--firma Blume&Co`; in PowerShell ist das ein Syntaxfehler, in `sh`
127
+ startet es `--firma Blume` im Hintergrund und führt danach `Co` aus.
128
+
129
+ 🔑 *Eine Liste dessen, was gefährlich ist, ist immer unvollständig -- die
130
+ Liste dessen, was harmlos ist, kann man zu Ende schreiben.* Also
131
+ umgekehrt: Buchstaben (auch Umlaute), Ziffern und eine Handvoll
132
+ unverfänglicher Satzzeichen bleiben unangetastet, **alles andere wird
133
+ zitiert**. Ein Anführungszeichen zu viel ist folgenlos; eines zu wenig
134
+ hat jetzt dreimal einen Rat zerlegt.
135
+ */
136
+ if (!immer && /^[A-Za-z0-9À-ÿ._\/@:+-]+$/.test(wert))
137
+ return wert;
138
+ /*
139
+ 🔴 **`$` UND ```` MÜSSEN AUS DEN DOPPELTEN ANFÜHRUNGSZEICHEN HERAUS**
140
+ (Befund 4 der dritten Review-Runde, 2026-08-27). Doppelte Anführungszeichen
141
+ halten Leerzeichen zusammen, aber sie schützen NICHT vor dem Rechnen: Eine
142
+ Firma namens `Cash$$ GmbH` ergab die Zeile
143
+ `--firma "Cash$$ GmbH"`, und `$$` ist in `sh` die Nummer des laufenden
144
+ Programms. Der Kunde kopiert den angebotenen Befehl und liest danach
145
+ *„Zu ‚Cash1234 GmbH' gehörst du nicht."* -- wieder ein Rat, der beim
146
+ Kopieren zerfällt, nur eine Stufe subtiler als der Fall, gegen den diese
147
+ Funktion gebaut wurde.
148
+
149
+ Deshalb: doppelte Anführungszeichen nur, wenn wirklich nichts Rechnendes
150
+ drinsteht. Sonst die einfachen, die in beiden Kommandozeilen alles wörtlich
151
+ nehmen.
152
+ */
153
+ if (!/["$\`]/.test(wert))
154
+ return `"${wert}"`;
155
+ if (!wert.includes("'"))
156
+ return `'${wert}'`;
157
+ return `"${wert.replace(/(["$\`])/g, "\\$1")}"`;
158
+ }
9
159
  export function zerlege(argv, erlaubt) {
10
160
  const optionen = {};
11
161
  for (let i = 0; i < argv.length; i++) {
@@ -1,5 +1,8 @@
1
1
  import { resolve } from "node:path";
2
- import { dateienSchreiben, vorlageHolen, werkbankOrdner, istUnsichererOrdnername, } from "../arbeitsplatz-dateien.js";
2
+ import { FIRMEN_OPTIONEN, dateienSchreiben, vorlageHolen, werkbankOrdner, istUnsichererOrdnername, } from "../arbeitsplatz-dateien.js";
3
+ import { alsBefehlszeile, alsWort, trenneNamenUndOptionen, zerlege } from "../argumente.js";
4
+ import { fragenAus, traegtFragenVon } from "../fragen-lesen.js";
5
+ import { FIRMEN_TEXTE } from "../kontext-stand.js";
3
6
  import { werkbankOrdnerAus, werkbankUmbenennen } from "../werkbank-name.js";
4
7
  /**
5
8
  * Holt die KI-Arbeitsplatz-Vorlage vom Server und legt sie lokal an --
@@ -23,7 +26,7 @@ import { werkbankOrdnerAus, werkbankUmbenennen } from "../werkbank-name.js";
23
26
  * `../arbeitsplatz-dateien.ts`, weil `werkbank neu` dieselbe braucht.
24
27
  */
25
28
  const HILFE = `
26
- kimal3 arbeitsplatz neu [dein-vorname]
29
+ kimal3 arbeitsplatz neu [dein-vorname] [--firma "Name" | --firma-id "…"]
27
30
 
28
31
  Holt deine KI-Arbeitsplatz-Vorlage und legt sie hier als Ordner an --
29
32
  die ganze Familie: Firmenebene, Werkbank, Beispielprojekt.
@@ -35,6 +38,13 @@ const HILFE = `
35
38
  wird "werkbank-sabine". Ohne Angabe steht dort der Name,
36
39
  den der Server aus deiner Anmelde-Adresse gebildet hat.
37
40
 
41
+ --firma "Name" Noetig, wenn du zu mehreren Firmen gehoerst -- der
42
+ Firmenkontext kommt gleich mit in den Ordner.
43
+ Die Absage nennt dir die Namen als fertige Befehle.
44
+
45
+ --firma-id "…" Fuer den Fall, dass zwei deiner Firmen GLEICH heissen.
46
+ Auch diese Absage nennt dir fertige Befehle.
47
+
38
48
  🔴 Bricht ab, wenn der Zielordner schon existiert und NICHT leer ist --
39
49
  eigene Arbeit wird dadurch nie versehentlich ueberschrieben.
40
50
 
@@ -76,12 +86,33 @@ const HILFE = `
76
86
  * Runde 3 zu PR #24 auf `/pro` gefunden hat.
77
87
  */
78
88
  function hinweisFuer(ordnerName) {
79
- const ziel = ordnerName.includes(" ") ? `"${ordnerName}"` : ordnerName;
89
+ const ziel = alsWort(ordnerName);
80
90
  return (" Steht dein Arbeitsplatz schon und dir fehlt nur die eigene Werkbank?\n" +
81
91
  " Dann geh HINEIN und leg sie dort an:\n" +
82
92
  ` cd ${ziel}\n` +
83
93
  " kimal3 werkbank neu <dein-vorname>");
84
94
  }
95
+ export function fragenstandAus(dateien) {
96
+ let offen = 0;
97
+ let gesamt = 0;
98
+ for (const text of FIRMEN_TEXTE) {
99
+ const geliefert = dateien.find((d) => d.pfad === text.pfad);
100
+ if (geliefert === undefined)
101
+ return { art: "unbekannt" };
102
+ if (!traegtFragenVon(geliefert.inhalt, text.ankerPraefix))
103
+ return { art: "unbekannt" };
104
+ // ⚠️ `fragenAus` nimmt genau EIN Argument -- trotzdem steht hier die
105
+ // ausgeschriebene Form. `filter`/`map` reichen der Funktion drei Werte, und
106
+ // ein zweiter Parameter bekaeme spaeter stillschweigend den Listenplatz
107
+ // (`alsWort`, 2026-08-27).
108
+ const fragen = fragenAus(geliefert.inhalt);
109
+ gesamt += fragen.length;
110
+ offen += fragen.filter((f) => f.antwort === "").length;
111
+ }
112
+ // 🔑 Null Fragen in fuenf Dateien, die ihre Anker alle tragen, gibt es nicht
113
+ // -- und selbst dann waere „nichts offen" die ehrliche Antwort.
114
+ return offen === 0 ? { art: "keine" } : { art: "offen", offen, gesamt };
115
+ }
85
116
  export async function arbeitsplatzNeu(argv) {
86
117
  if (argv.includes("--help") || argv.includes("-h")) {
87
118
  console.log(HILFE);
@@ -97,37 +128,64 @@ export async function arbeitsplatzNeu(argv) {
97
128
  `werkbank-arno-ki`. Diese Meldungen und die Hilfe sind die einzigen Stellen,
98
129
  an denen er das erfaehrt, bevor es passiert.
99
130
  */
100
- const nameArg = argv[0];
131
+ /*
132
+ 🔴 **Erst schneiden, dann zerlegen** (Punkt A-3, 2026-08-26). Dieser Befehl
133
+ hat ein Wort OHNE `--` (den Vornamen), und `zerlege` lehnt genau das ab.
134
+ Deshalb trennt `trenneNamenUndOptionen` beim ersten `--`: davor die
135
+ Woerter, ab dort die Optionen. Die Strenge von `zerlege` bleibt damit
136
+ unangetastet -- sie war teuer erkauft (B-3 und B-4, 2026-08-18).
137
+ */
138
+ const { woerter, optionen: optionsArgv } = trenneNamenUndOptionen(argv);
139
+ const nameArg = woerter[0];
101
140
  if (nameArg !== undefined && nameArg.trim() === "") {
102
141
  console.error(`Ein leerer Name ist kein gueltiger Name.\n${HILFE}`);
103
142
  return 1;
104
143
  }
105
- if (nameArg !== undefined && nameArg.startsWith("--")) {
106
- console.error(`Die Option „${nameArg}" kenne ich nicht.\n${HILFE}`);
107
- return 1;
108
- }
109
- if (argv.length > 1) {
144
+ if (woerter.length > 1) {
110
145
  /*
111
146
  🔴 **Der Vorschlag darf keine Optionen einsammeln** (Review-Runde 5 zu
112
147
  PR #25). Hier stand `argv.join("-")` -- aus `arbeitsplatz neu sabine --force`
113
- wurde damit der Rat `arbeitsplatz neu sabine---force`. Die `--`-Wache ein paar
114
- Zeilen hoeher sieht nur `argv[0]`.
148
+ wurde damit der Rat `arbeitsplatz neu sabine---force`.
115
149
  *Ein Rat, der ein falsches Kommando erzeugt, ist schlimmer als keiner* --
116
150
  dieselbe Klasse wie die Absagen aus Runde 1 und 2, nur kleiner.
117
151
 
118
152
  Deshalb: Sieht irgendein Wort wie eine Option aus, gibt es gar keinen
119
153
  Vorschlag. Ein Mensch, der `--force` tippt, sucht keine Schreibhilfe fuer
120
154
  seinen Vornamen.
155
+
156
+ ⚠️ Seit dem 2026-08-26 stehen die `--`-Optionen schon gar nicht mehr in
157
+ `woerter` (der Schnitt oben). Der Filter bleibt trotzdem: Er faengt das
158
+ EINFACHE `-` (`-force`), das kein Schnitt-Zeichen ist und das der
159
+ Vorschlag genauso wenig einsammeln darf.
160
+ */
161
+ const einfacheWoerter = woerter.filter((w) => !w.startsWith("-"));
162
+ /*
163
+ 🔴 **Die getippten Optionen reisen mit** (Review-Befund 3, 2026-08-26).
164
+ Ohne sie riet die Zeile zu einem Befehl, der bei einem Menschen in zwei
165
+ Firmen sofort wieder in dieselbe Absage laeuft -- s. `alsBefehlszeile`.
121
166
  */
122
- const woerter = argv.filter((w) => !w.startsWith("-"));
123
- const vorschlag = woerter.length === argv.length && woerter.length > 1
167
+ const optionsAnhang = optionsArgv.length === 0 ? "" : ` ${alsBefehlszeile(optionsArgv)}`;
168
+ const vorschlag = einfacheWoerter.length === woerter.length
124
169
  ? `Besteht dein Name aus mehreren Woertern? Dann schreib ihn zusammen:\n` +
125
- ` kimal3 arbeitsplatz neu ${woerter.join("-")}\n`
170
+ ` kimal3 arbeitsplatz neu ${einfacheWoerter.join("-")}${optionsAnhang}\n`
126
171
  : "";
127
- console.error(`Mit „${argv[1]}" kann ich hier nichts anfangen -- nur EIN Name ist erlaubt.\n` +
172
+ console.error(`Mit „${woerter[1]}" kann ich hier nichts anfangen -- nur EIN Name ist erlaubt.\n` +
128
173
  `${vorschlag}${HILFE}`);
129
174
  return 1;
130
175
  }
176
+ /*
177
+ 🔑 Erst JETZT die Optionen -- nach den Namens-Absagen, damit ein Mensch mit
178
+ zwei Vornamen die Schreibhilfe bekommt und nicht eine Options-Meldung.
179
+ */
180
+ const zerlegt = zerlege(optionsArgv, FIRMEN_OPTIONEN);
181
+ if (!zerlegt.ok) {
182
+ console.error(`${zerlegt.meldung}\n${HILFE}`);
183
+ return 1;
184
+ }
185
+ const wunsch = {
186
+ firma: zerlegt.werte.optionen.firma,
187
+ firmaId: zerlegt.werte.optionen["firma-id"],
188
+ };
131
189
  /*
132
190
  Der Name wird UMGESCHRIEBEN, nicht abgelehnt (s. `werkbankOrdnerAus`) --
133
191
  "Sabine Mueller" ist ein gueltiger Name, nur kein gueltiger Ordner. `null`
@@ -146,7 +204,24 @@ export async function arbeitsplatzNeu(argv) {
146
204
  return 1;
147
205
  }
148
206
  }
149
- const holen = await vorlageHolen();
207
+ /*
208
+ 🔴 **HIER ENTSCHEIDET SICH, OB UEBERHAUPT ETWAS ENTSTEHT** (Punkt A-3).
209
+ Gehoert der Mensch zu mehreren Firmen und hat keine genannt, antwortet die
210
+ Route `409`, `vorlageHolen` gibt `1` zurueck -- und `dateienSchreiben`
211
+ weiter unten wird nie erreicht. Es wird also **kein halber Ordner**
212
+ angelegt, den er danach von Hand wegraeumen muesste.
213
+ */
214
+ /*
215
+ 🔑 **Der getippte Vorname reist in der Absage mit.** Wer
216
+ `kimal3 arbeitsplatz neu sabine` tippt und die Rueckfrage bekommt, soll die
217
+ fertige Zeile kopieren koennen, ohne seinen Namen ein zweites Mal
218
+ hineinzuschreiben -- sonst hiesse seine Werkbank nach dem zweiten Versuch
219
+ anders als beim ersten gewollt, und niemand saehe, woran es lag.
220
+ */
221
+ const befehlsname = nameArg === undefined
222
+ ? "arbeitsplatz neu"
223
+ : `arbeitsplatz neu ${alsWort(nameArg)}`;
224
+ const holen = await vorlageHolen(befehlsname, wunsch);
150
225
  if (!holen.ok)
151
226
  return holen.wert;
152
227
  const daten = holen.daten;
@@ -253,8 +328,55 @@ export async function arbeitsplatzNeu(argv) {
253
328
  // zuverlaessig ins Leere.*
254
329
  const werkbank = werkbankOrdner(dateien);
255
330
  const cdZiel = werkbank !== null ? `${ordnerName}/${werkbank}` : ordnerName;
256
- console.log(` cd ${cdZiel.includes(" ") ? `"${cdZiel}"` : cdZiel}`);
331
+ console.log(` cd ${alsWort(cdZiel)}`);
257
332
  console.log(" claude");
258
333
  console.log(' dann "Lass uns einrichten" tippen');
334
+ /*
335
+ 🔴 **HIER RISS DIE KETTE.**
336
+ Bis zum 2026-08-28 endete dieser Befehl immer mit denselben drei Zeilen: Der
337
+ Mensch hat einen Ordner, ein Einrichtungsgespraech -- und einen Stapel
338
+ unbeantworteter Fragen ueber seine Firma, von denen ihm niemand erzaehlt.
339
+ *Er hat den Ordner und das Gefuehl, fertig zu sein.*
340
+
341
+ ⚠️ Hier stand kurz „siebzehn Fragen", abgeschrieben aus der Spec. Gemessen
342
+ am 2026-08-28 sind es **sechzehn** (4 + 3 + 4 + 2 + 3 Anker in den fuenf
343
+ Vorlagen). Deshalb steht die Zahl unten NICHT im Text, sondern kommt aus
344
+ derselben Zaehlung, die auch `kimal3 kontext fragen` ausgibt.
345
+ *Eine Zahl im Fliesstext ist eine zweite Wahrheit, die still verrottet.*
346
+
347
+ 🔑 **Der Rat haengt an der Lage, nicht am Befehl.** Nur wenn wirklich Fragen
348
+ offen sind, steht `/k3kontext` da. Bei einer Firma, die ihre fuenf Texte
349
+ schon gepflegt hat, bleibt die Ausgabe Zeichen fuer Zeichen die von vorher.
350
+ *Ein Rat, der immer erscheint, ist keine Auskunft, sondern Deko* -- so
351
+ ausdruecklich in US-1 der Spec
352
+ (`docs/superpowers/specs/2026-08-28-k3-befehle-design.md`).
353
+
354
+ ⚠️ **Er sagt NICHT „und schick es hoch".** Ob dieser Mensch Admin seiner
355
+ Firma ist, steht in dieser Antwort nicht drin -- `/api/cli/arbeitsplatz`
356
+ liefert `wurzel` und `dateien`, sonst nichts. Die Schwesterroute
357
+ `/api/cli/kontext` traegt eine `rolle`, diese nicht. Also raet die Ausgabe
358
+ zu dem Schritt, den JEDER gehen kann: die Fragen beantworten.
359
+
360
+ 🔴 **Was danach passiert, deutet `/k3kontext` an der MELDUNG von
361
+ `kimal3 kontext hoch` -- nicht an der Zahl.** Hier stand bis zur zweiten
362
+ Review-Runde „der deutet Rueckgabewert 1 als ‚du bist kein Admin'", und
363
+ genau diese Gleichung hat dieselbe Scheibe an zwei anderen Stellen als
364
+ falsch geloescht: `kontext hoch` gibt `1` auch dann zurueck, wenn die Firma
365
+ erfolgreich gespeichert hat und nur der lokale Merkzettel scheiterte
366
+ (`kontext-hoch.ts`, Merkzettel-Zweig). *Ein Kommentar, der ein Verhalten erklaert, wird
367
+ beim naechsten Lesen abgenickt -- dieser haette den Fehler zurueckgeholt,
368
+ den der Commit daneben entfernt.*
369
+
370
+ ⚠️ Der Ordner ist zu diesem Zeitpunkt geschrieben; ein Zaehlfehler darf
371
+ daran nichts mehr aendern. Deshalb steht das hier ganz unten und kann nur
372
+ noch schweigen, nie abbrechen.
373
+ */
374
+ const stand = fragenstandAus(dateien);
375
+ if (stand.art === "offen") {
376
+ console.log("");
377
+ console.log(` Danach: ${stand.offen} von ${stand.gesamt} Fragen über deine Firma sind noch offen.`);
378
+ console.log(" Claude stellt sie dir einzeln und trägt deine Antworten ein:");
379
+ console.log(" /k3kontext");
380
+ }
259
381
  return 0;
260
382
  }