kimal3 0.2.10 → 0.3.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 (66) hide show
  1. package/README.md +135 -0
  2. package/dist/api.js +16 -7
  3. package/dist/arbeitsplatz-dateien.js +23 -19
  4. package/dist/argumente.js +25 -97
  5. package/dist/befehle/arbeitsplatz.js +50 -68
  6. package/dist/befehle/firma-daten.js +24 -26
  7. package/dist/befehle/kontext-fragen.js +57 -47
  8. package/dist/befehle/kontext-gemeinsam.js +84 -40
  9. package/dist/befehle/kontext-hoch.js +40 -46
  10. package/dist/befehle/kontext-runter.js +70 -51
  11. package/dist/befehle/login.js +23 -9
  12. package/dist/befehle/logout.js +9 -4
  13. package/dist/befehle/projekt-liste.js +24 -29
  14. package/dist/befehle/projekt-neu.js +25 -31
  15. package/dist/befehle/skill-install.js +78 -49
  16. package/dist/befehle/skill-tausch.js +28 -0
  17. package/dist/befehle/wache.js +12 -6
  18. package/dist/befehle/werkbank.js +98 -87
  19. package/dist/befehle-katalog-en.js +185 -0
  20. package/dist/befehle-katalog.js +10 -49
  21. package/dist/befehlswoerter.js +69 -0
  22. package/dist/einstellungen-mischen.js +2 -2
  23. package/dist/einstellungen-schreiben.js +8 -5
  24. package/dist/fehler.js +5 -5
  25. package/dist/firmen-liste.js +8 -5
  26. package/dist/format.js +41 -0
  27. package/dist/fragen-lesen.js +3 -3
  28. package/dist/hilfe.js +24 -0
  29. package/dist/kimal3.js +77 -71
  30. package/dist/kontext-dateien.js +30 -23
  31. package/dist/kontext-hoch-zeile.js +5 -3
  32. package/dist/kontext-rat.js +151 -0
  33. package/dist/kontext-stand.js +64 -210
  34. package/dist/nachricht.js +39 -15
  35. package/dist/projekt-neu-firma.js +9 -9
  36. package/dist/projekt-zeilen.js +40 -54
  37. package/dist/slash-befehle.js +36 -0
  38. package/dist/sprache.js +114 -0
  39. package/dist/texte/allgemein.js +99 -0
  40. package/dist/texte/arbeitsplatz.js +135 -0
  41. package/dist/texte/kontext-hoch.js +128 -0
  42. package/dist/texte/kontext.js +171 -0
  43. package/dist/texte/projekte.js +126 -0
  44. package/dist/texte/skill.js +87 -0
  45. package/dist/texte/wache.js +36 -0
  46. package/dist/texte/werkbank.js +161 -0
  47. package/dist/wache-rechnung.js +27 -40
  48. package/dist/werkbank-name.js +28 -10
  49. package/dist/wort.js +98 -0
  50. package/package.json +2 -2
  51. package/skill/SKILL.md +9 -9
  52. package/skill/commands/k3.md +1 -1
  53. package/skill/commands/k3feierabend.md +99 -97
  54. package/skill/commands/k3kontext.md +1 -1
  55. package/skill/commands/k3kurs.md +6 -5
  56. package/skill/commands/k3start.md +2 -1
  57. package/skill/en/SKILL.md +451 -0
  58. package/skill/en/commands/k3.md +106 -0
  59. package/skill/en/commands/k3analyse.md +59 -0
  60. package/skill/en/commands/k3context.md +92 -0
  61. package/skill/en/commands/k3course.md +54 -0
  62. package/skill/en/commands/k3list.md +26 -0
  63. package/skill/en/commands/k3project.md +50 -0
  64. package/skill/en/commands/k3start.md +47 -0
  65. package/skill/en/commands/k3wrapup.md +99 -0
  66. package/dist/zz-kaputt.js +0 -1
@@ -11,6 +11,9 @@
11
11
  * NodeNext (Import-Pfade enden auf `.js`), die App mit dem Bundler. Eine Datei
12
12
  * ohne eigene Importe liest sich aus beiden Welten ohne Uebersetzung.
13
13
  *
14
+ * 🌐 Englisch steht in `befehle-katalog-en.ts` daneben (Teil 3, T3-11) -- eine Datei waere
15
+ * ueber 300 Code-Zeilen. Die Terminal-Hilfe baut `hilfe.ts` aus beiden.
16
+ *
14
17
  * ⚠️ Der Katalog beschreibt, WAS ES GIBT — nicht, wer es darf. Die Pro-Schranke
15
18
  * sitzt in der Route und im Befehl, nicht hier.
16
19
  */
@@ -62,7 +65,7 @@ export const BEFEHLE = [
62
65
  terminal: "kimal3 arbeitsplatz neu",
63
66
  details: [
64
67
  "Holt die Vorlage von deinem Pro-Konto und legt sie hier als echten Ordner an — CLAUDE.md, Takt-Befehle, die Werkbank für deine Projekte. Dasselbe Paket, das auch als Download auf /pro liegt.",
65
- "Der Ordner heißt immer KI-Arbeitsplatz — mehrere Firmen trennst du über den Ordner darüber, z. B. Kunden/ARNO/KI-Arbeitsplatz. Gib deinen Vornamen mit, dann heißt deine Werkbank darin werkbank-sabine: kimal3 arbeitsplatz neu sabine.",
68
+ "Der Ordner heißt KI-Arbeitsplatz (bei einer englischsprachigen Firma AI-Workplace) — mehrere Firmen trennst du über den Ordner darüber, z. B. Kunden/ARNO/KI-Arbeitsplatz. Gib deinen Vornamen mit, dann heißt deine Werkbank darin werkbank-sabine (bei einem englischen Konto workbench-sabine): kimal3 arbeitsplatz neu sabine.",
66
69
  "Gibt es den Zielordner schon und ist er nicht leer, bricht der Befehl ab, statt etwas zu überschreiben — deine Arbeit ist sicher. Geklappt hat es, wenn er die Zahl der angelegten Dateien nennt.",
67
70
  "⚠️ Er legt auch die Firmenebene an (kontext/ mit Marke, Systemen, Sicherheit). Steht die schon, brauchst du kimal3 werkbank neu — sonst entsteht eine zweite Firma in der ersten.",
68
71
  ],
@@ -87,7 +90,7 @@ export const BEFEHLE = [
87
90
  "Für alle außer der ersten Person: Der Firmen-Ordner steht schon (kontext/ mit Marke, Systemen, Sicherheit) — du brauchst nur deinen eigenen Arbeitsplatz darin.",
88
91
  "🔴 Er fasst die Firmenebene nicht an. Kein Byte in kontext/, CLAUDE.md oder README.md — auch nicht schreibend „identisch drüber“.",
89
92
  "Er läuft nur, wenn du wirklich in einem Firmen-Ordner stehst. Fehlt die Firmenebene daneben, bricht er ab und sagt es — eine Werkbank ohne sie zeigt ins Leere, weil ihre Einstellungen auf ../kontext verweisen.",
90
- "Gib deinen Vornamen mit — der Ordner trägt ihn: aus sabine wird werkbank-sabine. Ohne Angabe bildet der Server ihn aus deiner Anmelde-Adresse. So sieht man später, wem welche Werkbank gehört.",
93
+ "Gib deinen Vornamen mit — der Ordner trägt ihn: aus sabine wird werkbank-sabine (bei einem englischen Konto workbench-sabine). Ohne Angabe bildet der Server ihn aus deiner Anmelde-Adresse. So sieht man später, wem welche Werkbank gehört.",
91
94
  ],
92
95
  },
93
96
  /*
@@ -180,9 +183,11 @@ export const BEFEHLE = [
180
183
  /*
181
184
  🔴 Dieser Eintrag steht bewusst NICHT im "Weg, den ein Mensch geht" --
182
185
  niemand muss ihn selbst tippen. Er wird bei jedem Start von Claude Code
183
- von selbst aufgerufen (Aufgabe 8) und steht hier trotzdem, weil
184
- `tippen()` in `wache-rechnung.ts` seine `!`-Schreibweise braucht, wenn die
185
- Wache selbst einmal auf ihren eigenen Befehl verweisen muss.
186
+ von selbst aufgerufen (Aufgabe 8). Er steht hier noch, obwohl `tippen()`
187
+ in `wache-rechnung.ts` seit Teil 3 (Task 15) die Befehlswoerter aus
188
+ `befehlswoerter.ts` liest und den Katalog nicht mehr braucht. Die Zeile
189
+ bleibt sichtbar (Task 16): Die deutsche Hilfe bleibt Zeichen fuer Zeichen,
190
+ und ein Mensch, der `kimal3 wache` sucht, findet es hier.
186
191
  */
187
192
  {
188
193
  befehl: "kimal3 wache",
@@ -209,47 +214,3 @@ export const BEFEHLE = [
209
214
  ],
210
215
  },
211
216
  ];
212
- /**
213
- * Die Terminal-Hilfe, gebaut aus dem Katalog.
214
- *
215
- * Die Spalten richten sich an der laengsten Befehlszeile aus — von Hand
216
- * gesetzte Leerzeichen waeren beim naechsten laengeren Befehl krumm.
217
- *
218
- * 📌 Sie nimmt bewusst NUR `befehl` und `beschreibung`: Im Terminal ist eine
219
- * Uebersicht gefragt, kein Handbuch. Die langen `details` haben ihren Platz auf
220
- * `/pro`, wo man sie auf- und zuklappen kann — `--help` je Befehl bleibt der
221
- * Weg im Terminal.
222
- */
223
- export function hilfeText() {
224
- const breite = Math.max(...BEFEHLE.map((b) => b.befehl.length));
225
- const zeilen = BEFEHLE.map((b) => ` ${b.befehl.padEnd(breite + 2)}${b.beschreibung}`).join("\n");
226
- return `
227
- kimal3 — trag deine KI-Projekte dort ein, wo sie entstehen.
228
-
229
- ${zeilen}
230
-
231
- Mehr zu einem Befehl: kimal3 projekt neu --help
232
- `;
233
- }
234
- /**
235
- * Wie ein Befehl **getippt** wird — aus dem Katalog geholt, nicht abgeschrieben.
236
- *
237
- * 🔑 **Warum das eine eigene Funktion ist** (2026-08-20): Auf `/pro` stand
238
- * `const ANMELDEN = "! kimal3 login"` als Zeichenkette im Seitencode — eine
239
- * zweite Wahrheit ueber einen Befehl, den der Katalog schon fuehrt. Wer im
240
- * Katalog umbenennt, laesst sie stehen, und die Seite zeigt weiter den alten
241
- * Aufruf. Dieselbe Klasse Fehler wie der fest getippte `cd`-Pfad in Runde 3.
242
- *
243
- * ⚠️ Sie **wirft**, statt einen leeren Text zu liefern. Die Aufrufer sind
244
- * Modul-Konstanten — ein falscher Name fliegt damit beim `npm run build` auf,
245
- * nicht erst beim Kunden vor einem Kopier-Kasten, in dem nichts steht.
246
- * *Ein stiller Fehler auf einer bezahlten Seite ist teurer als ein lauter im
247
- * Build.*
248
- */
249
- export function tippBefehl(befehl) {
250
- const eintrag = BEFEHLE.find((b) => b.befehl === befehl);
251
- if (!eintrag) {
252
- throw new Error(`Kein Katalog-Eintrag fuer „${befehl}". Steht der Befehl noch in BEFEHLE?`);
253
- }
254
- return eintrag.tippen;
255
- }
@@ -0,0 +1,69 @@
1
+ export const BEFEHLSWOERTER = {
2
+ login: { de: "login", en: "login" },
3
+ logout: { de: "logout", en: "logout" },
4
+ "skill install": { de: "skill install", en: "skill install" }, // kein-anzeigetext: Befehlswörter (Spec Abschnitt 6)
5
+ "arbeitsplatz neu": { de: "arbeitsplatz neu", en: "workplace new" }, // kein-anzeigetext: Befehlswörter (Spec Abschnitt 6)
6
+ "werkbank neu": { de: "werkbank neu", en: "workbench new" }, // kein-anzeigetext: Befehlswörter (Spec Abschnitt 6)
7
+ "kontext runter": { de: "kontext runter", en: "context download" }, // kein-anzeigetext: Befehlswörter (Spec Abschnitt 6)
8
+ "kontext hoch": { de: "kontext hoch", en: "context upload" }, // kein-anzeigetext: Befehlswörter (Spec Abschnitt 6)
9
+ "kontext fragen": { de: "kontext fragen", en: "context questions" }, // kein-anzeigetext: Befehlswörter (Spec Abschnitt 6)
10
+ "projekt neu": { de: "projekt neu", en: "project new" }, // kein-anzeigetext: Befehlswörter (Spec Abschnitt 6)
11
+ "projekt liste": { de: "projekt liste", en: "project list" }, // kein-anzeigetext: Befehlswörter (Spec Abschnitt 6)
12
+ "firma daten": { de: "firma daten", en: "company data" }, // kein-anzeigetext: Befehlswörter (Spec Abschnitt 6)
13
+ wache: { de: "wache", en: "watch" },
14
+ "wache an": { de: "wache an", en: "watch on" }, // kein-anzeigetext: Befehlswörter (Spec Abschnitt 6)
15
+ "wache aus": { de: "wache aus", en: "watch off" }, // kein-anzeigetext: Befehlswörter (Spec Abschnitt 6)
16
+ };
17
+ /**
18
+ * Welcher Befehl steht vorne in `argv` -- und was bleibt dahinter?
19
+ * Zwei Woerter vor einem: `watch on` ist `wache an`, nicht `wache` mit Rest `on`.
20
+ * `null` heisst: kein Befehl, auch kein halber. Ob der Rest zum Befehl passt, entscheidet der
21
+ * Verteiler (`kimal3 login extra` bleibt dort unbekannt, wie bisher).
22
+ */
23
+ export function erkenneBefehl(argv) {
24
+ const namen = Object.keys(BEFEHLSWOERTER);
25
+ for (const laenge of [2, 1]) {
26
+ if (argv.length < laenge)
27
+ continue;
28
+ const vorne = argv.slice(0, laenge).join(" ");
29
+ for (const name of namen) {
30
+ const w = BEFEHLSWOERTER[name];
31
+ if (w.de.split(" ").length !== laenge)
32
+ continue;
33
+ if (vorne === w.de || vorne === w.en)
34
+ return { name, rest: argv.slice(laenge) };
35
+ }
36
+ }
37
+ return null;
38
+ }
39
+ /** Das Befehlswort in der Sprache des Satzes, der es nennt -- ohne `kimal3` davor. */
40
+ export function befehlIn(name, sprache) {
41
+ return BEFEHLSWOERTER[name][sprache];
42
+ }
43
+ /** Englischer Schalter -> deutscher Name, unter dem der Wert abgelegt und zum Server geschickt wird. */
44
+ export const SCHALTER_EN = {
45
+ company: "firma",
46
+ "company-id": "firma-id",
47
+ "no-company": "ohne-firma",
48
+ overwrite: "ueberschreiben",
49
+ title: "titel",
50
+ description: "beschreibung",
51
+ icon: "symbol",
52
+ hours: "stunden",
53
+ result: "ergebnis",
54
+ revenue: "umsatz",
55
+ impact: "wirkung",
56
+ "impact-label": "wirkung-label",
57
+ "live-since": "live-seit",
58
+ };
59
+ /** `company` -> `firma`; ein deutscher (oder unbekannter) Name bleibt, wie er ist. */
60
+ export function deutscherSchalter(getippt) {
61
+ return Object.hasOwn(SCHALTER_EN, getippt) ? SCHALTER_EN[getippt] : getippt;
62
+ }
63
+ /** Der Schalter in der Sprache des Satzes, der ihn nennt (ohne `--`). */
64
+ export function schalterIn(deutsch, sprache) {
65
+ if (sprache === "de")
66
+ return deutsch;
67
+ const treffer = Object.entries(SCHALTER_EN).find(([, d]) => d === deutsch);
68
+ return treffer ? treffer[0] : deutsch;
69
+ }
@@ -13,7 +13,7 @@
13
13
  * Woran wir einen Eintrag als UNSEREN erkennen -- unabhaengig davon, welche
14
14
  * Argumente wir spaeter einmal anhaengen. Siehe `istUnserer`.
15
15
  */
16
- export const WACHE_MARKE = "kimal3 wache";
16
+ export const WACHE_MARKE = "kimal3 wache"; // kein-anzeigetext: Startbefehl in Claudes Einstellungen, bleibt deutsch (T3-5)
17
17
  /**
18
18
  * Der Befehl, der in die Einstellungen eingetragen wird.
19
19
  *
@@ -37,7 +37,7 @@ export const WACHE_MARKE = "kimal3 wache";
37
37
  * der JSON-Form, die Claude Code fuer Startbefehle vorsieht, und die wird
38
38
  * AUCH BEI RUECKGABEWERT 127 gelesen (ebenfalls gemessen).
39
39
  */
40
- export const WACHE_BEFEHL = `${WACHE_MARKE} --hook`;
40
+ export const WACHE_BEFEHL = `${WACHE_MARKE} --hook`; // kein-anzeigetext: Startbefehl in Claudes Einstellungen, bleibt deutsch (T3-5)
41
41
  /**
42
42
  * Wie lange Claude auf die Wache wartet, in Sekunden.
43
43
  *
@@ -2,6 +2,9 @@ import { copyFileSync, existsSync, mkdirSync, readFileSync, writeFileSync } from
2
2
  import { homedir } from "node:os";
3
3
  import { join } from "node:path";
4
4
  import { WACHE_BEFEHL, mischeEinstellungen } from "./einstellungen-mischen.js";
5
+ /** Die Kontrolle nach dem Schreiben fand unseren Eintrag nicht -- eigener Fall, damit `detail` `null` sein kann. */
6
+ class EintragFehlt extends Error {
7
+ }
5
8
  export function einstellungenDatei() {
6
9
  return join(homedir(), ".claude", "settings.json");
7
10
  }
@@ -24,7 +27,7 @@ export function trageWacheEin() {
24
27
  -- wer beide gleich behandelt, legt eine neue an und der Mensch
25
28
  verliert alles, was drinstand.
26
29
  */
27
- return { art: "gescheitert", grund: `nicht lesbar (${e.message})` };
30
+ return { art: "gescheitert", schritt: "lesen", detail: e.message };
28
31
  }
29
32
  }
30
33
  const ergebnis = mischeEinstellungen(alt, WACHE_BEFEHL);
@@ -43,7 +46,7 @@ export function trageWacheEin() {
43
46
  ist, im Fehlerfall zurueckzukoennen -- ohne Kopie gibt es kein Zurueck,
44
47
  und dann ist Schreiben die schlechtere Wahl als Nichtstun.
45
48
  */
46
- return { art: "gescheitert", grund: `keine Sicherungskopie moeglich (${e.message})` };
49
+ return { art: "gescheitert", schritt: "sichern", detail: e.message };
47
50
  }
48
51
  }
49
52
  // --- Schritt 3: schreiben ---
@@ -53,19 +56,19 @@ export function trageWacheEin() {
53
56
  }
54
57
  catch (e) {
55
58
  zurueck(gabEsSchon);
56
- return { art: "gescheitert", grund: `nicht schreibbar (${e.message})` };
59
+ return { art: "gescheitert", schritt: "schreiben", detail: e.message };
57
60
  }
58
61
  // --- Schritt 4: zuruecklesen und erneut pruefen ---
59
62
  try {
60
63
  const zurueckgelesen = readFileSync(datei, "utf8");
61
64
  const geparst = JSON.parse(zurueckgelesen);
62
65
  if (!zurueckgelesen.includes(WACHE_BEFEHL) || geparst === null) {
63
- throw new Error("unser Eintrag steht nicht in der zurueckgelesenen Datei");
66
+ throw new EintragFehlt();
64
67
  }
65
68
  }
66
69
  catch (e) {
67
70
  zurueck(gabEsSchon);
68
- return { art: "gescheitert", grund: `Kontrolle nach dem Schreiben fehlgeschlagen (${e.message})` };
71
+ return { art: "gescheitert", schritt: "kontrolle", detail: e instanceof EintragFehlt ? null : e.message };
69
72
  }
70
73
  return { art: "eingetragen" };
71
74
  }
package/dist/fehler.js CHANGED
@@ -8,6 +8,7 @@
8
8
  * zieht ueber `anmeldung.ts` das Dateisystem nach; wer von dort importiert,
9
9
  * kann die Rechnung nicht mehr allein pruefen.
10
10
  */
11
+ import { ALLGEMEIN_TEXTE } from "./texte/allgemein.js";
11
12
  /**
12
13
  * ⚠️ **404 ist KEIN Eingabefehler** (QA-Befund B-7, 2026-08-18).
13
14
  *
@@ -33,11 +34,11 @@
33
34
  * bereits "nochmal versuchen kann helfen" -- und ein paar Minuten spaeter ist
34
35
  * das Deployment durch.
35
36
  */
36
- export function schlussFehler(status, meldung) {
37
+ export function schlussFehler(status, meldung, sprache) {
38
+ const t = ALLGEMEIN_TEXTE[sprache];
37
39
  if (status === 404) {
38
40
  return {
39
- text: "Diese Version der App kennt den Befehl noch nicht. " +
40
- "Hast du gerade aktualisiert, versuch es in ein paar Minuten noch einmal.",
41
+ text: t.appDoesNotKnowCommand,
41
42
  wert: 3,
42
43
  };
43
44
  }
@@ -57,8 +58,7 @@ export function schlussFehler(status, meldung) {
57
58
  */
58
59
  if (status === 408 || status === 429) {
59
60
  return {
60
- text: "Der Server ist gerade überlastet oder hat zu lange gebraucht. " +
61
- "Versuch es in einem Moment noch einmal.",
61
+ text: t.serverOverloaded,
62
62
  wert: 3,
63
63
  };
64
64
  }
@@ -6,6 +6,8 @@
6
6
  * reine Rechnung, der Import bricht das also nicht.
7
7
  */
8
8
  import { alsWort } from "./argumente.js";
9
+ import { befehlIn, schalterIn } from "./befehlswoerter.js";
10
+ import { ALLGEMEIN_TEXTE } from "./texte/allgemein.js";
9
11
  /**
10
12
  * Aus den Namen der eigenen Firmen die Zeilen bauen, die unter „Deine Firmen:"
11
13
  * stehen.
@@ -31,7 +33,8 @@ import { alsWort } from "./argumente.js";
31
33
  * Diese Funktion sorgt nur dafuer, dass niemand mehr einen Ausweg vorgespielt
32
34
  * bekommt, den es nicht gibt.
33
35
  */
34
- export function firmenZeilen(namen) {
36
+ export function firmenZeilen(namen, s) {
37
+ const t = ALLGEMEIN_TEXTE[s];
35
38
  const anzahl = (name) => namen.filter((n) => n === name).length;
36
39
  const gesehen = new Set();
37
40
  const zeilen = [];
@@ -44,10 +47,10 @@ export function firmenZeilen(namen) {
44
47
  // Fertiger Befehl statt blosser Liste: Der naechste Schritt ist dann
45
48
  // Kopieren statt Abtippen -- und ein Name mit Leerzeichen braucht
46
49
  // Anfuehrungszeichen, an die sonst niemand denkt.
47
- zeilen.push(` kimal3 firma daten --firma ${alsWort(name, true)}`);
50
+ zeilen.push(t.companyLine(befehlIn("firma daten", s), schalterIn("firma", s), alsWort(name, true))); // kein-anzeigetext: Befehlsschluessel
48
51
  }
49
52
  else {
50
- zeilen.push(` „${name}" — kommt ${wieOft}× vor und ist über den Namen nicht wählbar.`);
53
+ zeilen.push(t.nameNotUnique(name, wieOft));
51
54
  }
52
55
  }
53
56
  return zeilen;
@@ -67,8 +70,8 @@ export function firmenZeilen(namen) {
67
70
  * kopierfertige Befehle. Der Name steht als Kommentar dahinter -- eine nackte
68
71
  * UUID sagt einem Menschen nichts, und zwei davon untereinander noch weniger.
69
72
  */
70
- export function firmenIdZeilen(auswahl) {
71
- return auswahl.map((f, i) => ` kimal3 firma daten --firma-id "${f.id}" # ${f.name} (${i + 1}.)`);
73
+ export function firmenIdZeilen(auswahl, s) {
74
+ return auswahl.map((f, i) => ALLGEMEIN_TEXTE[s].companyIdLine(befehlIn("firma daten", s), schalterIn("firma-id", s), f.id, f.name, i + 1));
72
75
  }
73
76
  /**
74
77
  * Die Namen der eigenen Firmen aus einer `409`-Antwort -- was nicht als Text
package/dist/format.js ADDED
@@ -0,0 +1,41 @@
1
+ const REGION = { de: "de-DE", en: "en-GB" };
2
+ const MONATE_DE = [
3
+ "Januar", "Februar", "März", "April", "Mai", "Juni", // kein-anzeigetext: deutsche Monatsnamen als Handliste (T3-13, ICU-unabhaengig); Englisch kommt aus Intl
4
+ "Juli", "August", "September", "Oktober", "November", "Dezember", // kein-anzeigetext: siehe Zeile 13
5
+ ];
6
+ /**
7
+ * "2026-07-01" -> "1. Juli 2026" / "1 July 2026". Aus der Zeichenkette, nie ueber die Ortszeit
8
+ * (ein ISO-Datum ohne Uhrzeit gilt als UTC-Mitternacht und kippt sonst einen Tag zurueck).
9
+ * ⚠️ Erwartet ein REINES Datum (`JJJJ-MM-TT`); passt es nicht, steht lieber das rohe Datum da
10
+ * als ein kaputter Satz ("NaN. August 2026", der Fehler vom 2026-08-12).
11
+ */
12
+ export function datumLang(isoDatum, s) {
13
+ const [jahr, monat, tag] = isoDatum.split("-").map(Number);
14
+ // 🔑 Deutsch aus der Handliste (T3-13): Die deutsche Ausgabe haengt nicht an der ICU-Fassung des Rechners.
15
+ if (s === "de") {
16
+ const name = MONATE_DE[monat - 1];
17
+ return !name || !jahr || !Number.isFinite(tag) ? isoDatum : `${tag}. ${name} ${jahr}`;
18
+ }
19
+ if (!jahr || !(monat >= 1 && monat <= 12) || !tag)
20
+ return isoDatum;
21
+ return new Intl.DateTimeFormat(REGION[s], { day: "numeric", month: "long", year: "numeric", timeZone: "UTC" })
22
+ .format(new Date(Date.UTC(jahr, monat - 1, tag, 12)));
23
+ }
24
+ /**
25
+ * "1200" -> "1.200 €" / "€1,200"; "0.1" -> "0,10 €" / "€0.10". Cent nur, wenn welche dastehen:
26
+ * "1.200,00 €" taeuscht eine Genauigkeit vor, "0 €" bei 0,10 € verschweigt eine, die dasteht.
27
+ * Euro bleibt Euro (Teil 1).
28
+ */
29
+ export function euro(wert, s) {
30
+ const stellen = Math.round(wert * 100) % 100 !== 0 ? 2 : 0;
31
+ if (s === "de") {
32
+ return `${new Intl.NumberFormat("de-DE", { minimumFractionDigits: stellen, maximumFractionDigits: stellen }).format(wert)} €`;
33
+ }
34
+ return new Intl.NumberFormat("en-GB", {
35
+ style: "currency", currency: "EUR", minimumFractionDigits: stellen, maximumFractionDigits: stellen,
36
+ }).format(wert);
37
+ }
38
+ /** "6" -> "6", "6.5" -> "6,5" / "6.5". Die Spalte ist numeric(6,2). */
39
+ export function stunden(wert, s) {
40
+ return new Intl.NumberFormat(REGION[s], { maximumFractionDigits: 2 }).format(wert);
41
+ }
@@ -12,7 +12,7 @@
12
12
  🔴 WARUM DAS NICHT NUR BEQUEM, SONDERN DAS EINZIG EHRLICHE IST: Die CLI kann
13
13
  nichts aus `src/` importieren (eigenes Paket, eigenes tsconfig -- s. Kopf von
14
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
15
+ Fragen, und sie friert in dem Moment ein, in dem `npm publish` laeuft: Der
16
16
  Kunde hat dann eine CLI von gestern und Dateien von heute. Die Datei auf
17
17
  seiner Platte kommt dagegen IMMER vom Server -- sie ist die einzige Quelle,
18
18
  die nicht gegenueber dem abweichen kann, was er wirklich vor sich hat.
@@ -158,14 +158,14 @@ export function fragenAus(text) {
158
158
  *
159
159
  * 🔑 **WORAN ERKANNT WIRD: an mindestens einem Anker, der zu DIESER Datei
160
160
  * gehoert.** Die Anker sind je Datei benannt (`marke_anrede`, `firma_angebot`,
161
- * `kurz_kunden` …), und `FIRMEN_TEXTE` (`kontext-stand.ts`) fuehrt je Datei
161
+ * `kurz_kunden` …), und `firmenTexte` (`kontext-stand.ts`) fuehrt je Datei
162
162
  * ihren Praefix. Damit faellt nicht nur die voellig fremde Datei durch, sondern
163
163
  * auch der haeufigere Unfall: `marke.md` ueber `firma.md` kopiert. Ein blosses
164
164
  * "irgendein Anker steht drin" haette genau den durchgewunken.
165
165
  *
166
166
  * ⚠️ **Dass die Praefixe wirklich passen, ist keine Verabredung, sondern eine
167
167
  * Messung:** `check:kontext` haelt jeden Anker aus `FRAGEN` (der App) gegen den
168
- * Praefix, den `FIRMEN_TEXTE` fuer seine Datei nennt.
168
+ * Praefix, den `firmenTexte` fuer seine Datei nennt.
169
169
  *
170
170
  * 📌 **Der Preis, ausgesprochen:** Wer ALLE Anker einer Datei loescht und nur
171
171
  * die Ueberschriften stehen laesst, bekommt hier ein Nein, obwohl die App den
package/dist/hilfe.js ADDED
@@ -0,0 +1,24 @@
1
+ import { BEFEHLE } from "./befehle-katalog.js";
2
+ import { anzeigeIn } from "./befehle-katalog-en.js";
3
+ import { ALLGEMEIN_TEXTE } from "./texte/allgemein.js";
4
+ /**
5
+ * Die Terminal-Hilfe, gebaut aus dem Katalog (umgezogen aus `befehle-katalog.ts`, Teil 3,
6
+ * T3-11: Sie braucht jetzt beide Katalog-Dateien, und der Katalog bleibt importfrei).
7
+ *
8
+ * Die Spalten richten sich an der laengsten Befehlszeile aus -- von Hand gesetzte Leerzeichen
9
+ * waeren beim naechsten laengeren Befehl krumm.
10
+ *
11
+ * 📌 Sie nimmt bewusst NUR `befehl` und `beschreibung`: Im Terminal ist eine Uebersicht gefragt,
12
+ * kein Handbuch. Die langen `details` haben ihren Platz auf `/pro`.
13
+ *
14
+ * 🔴 Deutsch Zeichen fuer Zeichen wie vorher; Englisch mit EINER Zeile mehr (T3-13): die deutschen
15
+ * Befehlswoerter laufen ebenfalls. Der Rahmen (Leerzeichen, Zeilenumbrueche) steht IN den
16
+ * Textschluesseln, nicht hier (TU-2).
17
+ */
18
+ export function hilfeText(s) {
19
+ const t = ALLGEMEIN_TEXTE[s];
20
+ const eintraege = BEFEHLE.map((b) => anzeigeIn(b, s));
21
+ const breite = Math.max(...eintraege.map((e) => e.befehl.length));
22
+ const zeilen = eintraege.map((e) => ` ${e.befehl.padEnd(breite + 2)}${e.beschreibung}`).join("\n");
23
+ return `${t.helpTagline}${zeilen}${t.helpGermanWordsToo}${t.helpMore}`;
24
+ }
package/dist/kimal3.js CHANGED
@@ -16,10 +16,14 @@ import { BASIS_URL, leseAnmeldung } from "./anmeldung.js";
16
16
  import { gemerkterHinweis, lokalerKontextStandJetzt, NichtAngemeldet } from "./api.js";
17
17
  import { leseGesehen, merkeGesehen } from "./gesehen.js";
18
18
  import { ausfallMeldung, hinweisAnhaengen } from "./wache-rechnung.js";
19
+ import { WACHE_TEXTE } from "./texte/wache.js";
19
20
  import { leseWacheDatei, sitzungJetzt, wacheAusgefallen } from "./wache-schalter.js";
20
- import { formatiere, heuteAus, skillMeldung, skillZustand, sollZeigen, } from "./nachricht.js";
21
+ import { formatiere, heuteAus, skillZustand, sollZeigen, } from "./nachricht.js";
21
22
  import { eigeneVersion } from "./version.js";
22
- import { hilfeText } from "./befehle-katalog.js";
23
+ import { sprache } from "./sprache.js";
24
+ import { ALLGEMEIN_TEXTE } from "./texte/allgemein.js";
25
+ import { hilfeText } from "./hilfe.js";
26
+ import { erkenneBefehl } from "./befehlswoerter.js";
23
27
  import { arbeitsplatzNeu } from "./befehle/arbeitsplatz.js";
24
28
  import { werkbankNeu } from "./befehle/werkbank.js";
25
29
  import { firmaDaten } from "./befehle/firma-daten.js";
@@ -135,7 +139,9 @@ function gegenUnbehandelteKanalfehlerAbsichern(kanal, meldenAuf) {
135
139
  return;
136
140
  schonGemeldet = true;
137
141
  try {
138
- meldenAuf.write(`kimal3: Ausgabekanal-Fehler -- ${fehler.message}\n`);
142
+ // 🌐 Teil 3: die Sprache wird erst JETZT gelesen (zur Schreibzeit); der Empfaenger selbst
143
+ // bleibt auf Modulebene vor der ersten Zeile angemeldet.
144
+ meldenAuf.write(ALLGEMEIN_TEXTE[sprache()].outputChannelError(fehler.message));
139
145
  }
140
146
  catch {
141
147
  // Auch der Fehlerkanal kann kaputt sein -- dann bleibt wenigstens der
@@ -149,7 +155,6 @@ gegenUnbehandelteKanalfehlerAbsichern(process.stderr, null);
149
155
  // `befehle-katalog.ts` und wird von dort AUCH im Pro-Bereich der App angezeigt.
150
156
  // Zwei Listen ueber dieselben Befehle driften — die eine bekaeme den naechsten
151
157
  // Befehl, die andere nicht, und keiner der beiden Orte wuesste davon.
152
- const HILFE = hilfeText();
153
158
  /**
154
159
  * Hilfe plus Zustand.
155
160
  *
@@ -189,8 +194,9 @@ function versionsZeilen() {
189
194
  // Zeile zeigt einem Menschen etwas an, und da ist "unbekannt" die ehrliche
190
195
  // Antwort. Der andere Aufrufer RECHNET mit dem Wert und braucht dort das
191
196
  // Gegenteil -- er will lieber gar keine Nummer als eine erfundene.
192
- const version = eigeneVersion() ?? "unbekannt";
193
- return ` kimal3 ${version}\n spricht mit ${BASIS_URL}`;
197
+ const t = ALLGEMEIN_TEXTE[sprache()];
198
+ const version = eigeneVersion() ?? t.versionUnknown;
199
+ return t.versionLines(version, BASIS_URL);
194
200
  }
195
201
  /**
196
202
  * Welcher Update-Hinweis soll ans Ende -- oder keiner?
@@ -254,12 +260,12 @@ function hinweisText() {
254
260
  // verrottet lautlos (Befund der vierten Review-Runde).
255
261
  const datei = skillDatei();
256
262
  const inhalt = existsSync(datei) ? readFileSync(datei, "utf8") : null;
257
- if (skillZustand(inhalt, eigene) === "veraltet") {
263
+ if (skillZustand(inhalt, eigene, sprache()) === "veraltet") {
258
264
  // Auch dieser Hinweis nur einmal am Tag, unter einer festen Kennung.
259
265
  // Sie kann mit keiner Nachrichten-UUID kollidieren.
260
266
  const gesehen = leseGesehen();
261
267
  if (gesehen["skill-veraltet"] !== heute) {
262
- return { text: skillMeldung(), kennung: "skill-veraltet", tag: heute };
268
+ return { text: WACHE_TEXTE[sprache()].skillOutdated, kennung: "skill-veraltet", tag: heute };
263
269
  }
264
270
  }
265
271
  /*
@@ -281,7 +287,7 @@ function hinweisText() {
281
287
  // Auch dieser Hinweis hoechstens einmal am Tag, unter fester Kennung.
282
288
  const gesehen = leseGesehen();
283
289
  if (gesehen["wache-ausgefallen"] !== heute) {
284
- return { text: ausfallMeldung(), kennung: "wache-ausgefallen", tag: heute };
290
+ return { text: ausfallMeldung(sprache()), kennung: "wache-ausgefallen", tag: heute };
285
291
  }
286
292
  }
287
293
  }
@@ -463,17 +469,61 @@ function endeMitHinweis(code) {
463
469
  * nicht mehr Auskunft, sondern **weniger Behauptung**.
464
470
  */
465
471
  function zustandsZeile() {
472
+ const t = ALLGEMEIN_TEXTE[sprache()];
466
473
  const a = leseAnmeldung();
467
- return a
468
- ? ` ● Ausweis hinterlegt für ${a.email}\n` +
469
- " (ob er noch gilt, zeigt erst der nächste Befehl)"
470
- : " ● Nicht angemeldet. → kimal3 login";
474
+ return a ? t.credentialsStored(a.email) : t.notSignedIn;
475
+ }
476
+ /** `null` = der Rest passt nicht zu diesem Befehl -> "kenne ich nicht", wie bisher. */
477
+ async function fuehreAus(name, rest) {
478
+ const allein = rest.length === 0;
479
+ switch (name) {
480
+ case "login": return allein ? login() : null;
481
+ case "logout": return allein ? logout() : null;
482
+ case "skill install": return allein ? skillInstall() : null;
483
+ case "projekt neu": return projektNeu(rest);
484
+ case "projekt liste": return allein ? projektListe() : null;
485
+ // ⚠️ Rest wird durchgereicht, nicht auf Gleichheit geprueft: `firma daten --firma X` gehoert dazu.
486
+ case "firma daten": return firmaDaten(rest);
487
+ case "arbeitsplatz neu": return arbeitsplatzNeu(rest);
488
+ case "werkbank neu": return werkbankNeu(rest);
489
+ case "kontext runter": return kontextRunter(rest);
490
+ case "kontext hoch": return kontextHoch(rest);
491
+ case "kontext fragen": return kontextFragen(rest);
492
+ case "wache an": return allein ? wacheSchalten(true) : null;
493
+ case "wache aus": return allein ? wacheSchalten(false) : null;
494
+ case "wache":
495
+ // 🔴 Der Startbefehl aus `~/.claude/settings.json`, den `skill install` eintraegt (T3-5:
496
+ // eingetragen wird immer `kimal3 wache --hook`; `kimal3 watch --hook` laeuft genauso).
497
+ // Er verpackt die Meldung anders -- Begruendung an `wache()`.
498
+ // ⚠️ Ein Mensch tippt das nie; der Katalog nennt ihn deshalb auch nicht.
499
+ if (rest.length === 1 && rest[0] === "--hook")
500
+ return wache(true);
501
+ /*
502
+ 🔴 NACHSICHT GEGENUEBER FLAGGEN, DIE ES HIER NOCH NICHT GIBT.
503
+ Der Eintrag in `~/.claude/settings.json` ueberlebt jede Neuinstallation --
504
+ auch eine, die eine AELTERE Fassung bringt. Steht dort eines Tages
505
+ `wache --irgendwas`, das diese Fassung nicht kennt, faellt der Aufruf ohne
506
+ diese Zeile in den Unbekannt-Zweig: die volle Hilfe bei JEDEM Claude-Start,
507
+ Rueckgabewert 1, und `merkeSitzung` wird nie erreicht -- woraufhin
508
+ `wacheAusgefallen` bei jedem weiteren Befehl einen Rat gibt, der nichts
509
+ behebt. Aus einem fehlenden Zusatz wird so ein lauter Dauerschaden.
510
+ 🔑 Warum die MENSCHEN-Form die richtige Rueckfallebene ist: Sie endet
511
+ normal mit 0, ihre Meldung kommt also an. Die JSON-Form waere fuer einen
512
+ Menschen, der aus Neugier eine Flagge tippt, nur Kauderwelsch.
513
+ ⚠️ Fuer bereits veroeffentlichte Fassungen ist das nicht mehr zu retten --
514
+ `0.2.3` kennt `--hook` nicht. Diese Zeile sorgt dafuer, dass die naechste
515
+ Flagge die Falle nicht erneut stellt. (Befund der Durchsicht, 2026-09-09.)
516
+ */
517
+ if (rest.every((a) => a.startsWith("--")))
518
+ return wache(false);
519
+ return null;
520
+ }
471
521
  }
472
522
  async function haupt() {
473
523
  const argv = process.argv.slice(2);
474
524
  const wort = argv.join(" ");
475
525
  if (argv.length === 0 || wort === "--help" || wort === "-h") {
476
- console.log(HILFE);
526
+ console.log(hilfeText(sprache()));
477
527
  console.log(zustandsZeile() + "\n");
478
528
  return 0;
479
529
  }
@@ -482,61 +532,17 @@ async function haupt() {
482
532
  console.log(zustandsZeile() + "\n");
483
533
  return 0;
484
534
  }
485
- if (wort === "login")
486
- return login();
487
- if (wort === "logout")
488
- return logout();
489
- if (argv[0] === "projekt" && argv[1] === "neu")
490
- return projektNeu(argv.slice(2));
491
- if (wort === "projekt liste")
492
- return projektListe();
493
- // ⚠️ Praefix-Vergleich wie bei `projekt neu` und NICHT `wort === "firma daten"`:
494
- // `wort` enthaelt die Optionen mit, `firma daten --firma X` fiele bei einem
495
- // Gleichheits-Vergleich durch und meldete "Den Befehl kenne ich nicht".
496
- if (argv[0] === "firma" && argv[1] === "daten")
497
- return firmaDaten(argv.slice(2));
498
- if (wort === "skill install")
499
- return skillInstall();
500
- if (argv[0] === "arbeitsplatz" && argv[1] === "neu")
501
- return arbeitsplatzNeu(argv.slice(2));
502
- if (argv[0] === "werkbank" && argv[1] === "neu")
503
- return werkbankNeu(argv.slice(2));
504
- if (argv[0] === "kontext" && argv[1] === "runter")
505
- return kontextRunter(argv.slice(2));
506
- if (argv[0] === "kontext" && argv[1] === "hoch")
507
- return kontextHoch(argv.slice(2));
508
- if (argv[0] === "kontext" && argv[1] === "fragen")
509
- return kontextFragen(argv.slice(2));
510
- if (wort === "wache")
511
- return wache(false);
512
- // 🔴 Der Startbefehl aus `~/.claude/settings.json`, den `skill install`
513
- // eintraegt. Er verpackt die Meldung anders -- Begruendung an `wache()`.
514
- // ⚠️ Ein Mensch tippt das nie; der Katalog nennt ihn deshalb auch nicht.
515
- if (wort === "wache --hook")
516
- return wache(true);
517
- if (wort === "wache aus")
518
- return wacheSchalten(false);
519
- if (wort === "wache an")
520
- return wacheSchalten(true);
521
- /*
522
- 🔴 NACHSICHT GEGENUEBER FLAGGEN, DIE ES HIER NOCH NICHT GIBT.
523
- Der Eintrag in `~/.claude/settings.json` ueberlebt jede Neuinstallation --
524
- auch eine, die eine AELTERE Fassung bringt. Steht dort eines Tages
525
- `wache --irgendwas`, das diese Fassung nicht kennt, faellt der Aufruf ohne
526
- diese Zeile in den Unbekannt-Zweig: die volle Hilfe bei JEDEM Claude-Start,
527
- Rueckgabewert 1, und `merkeSitzung` wird nie erreicht -- woraufhin
528
- `wacheAusgefallen` bei jedem weiteren Befehl einen Rat gibt, der nichts
529
- behebt. Aus einem fehlenden Zusatz wird so ein lauter Dauerschaden.
530
- 🔑 Warum die MENSCHEN-Form die richtige Rueckfallebene ist: Sie endet
531
- normal mit 0, ihre Meldung kommt also an. Die JSON-Form waere fuer einen
532
- Menschen, der aus Neugier eine Flagge tippt, nur Kauderwelsch.
533
- ⚠️ Fuer bereits veroeffentlichte Fassungen ist das nicht mehr zu retten --
534
- `0.2.3` kennt `--hook` nicht. Diese Zeile sorgt dafuer, dass die naechste
535
- Flagge die Falle nicht erneut stellt. (Befund der Durchsicht, 2026-09-09.)
536
- */
537
- if (argv[0] === "wache" && argv.slice(1).every((a) => a.startsWith("--")))
538
- return wache(false);
539
- console.error(`Den Befehl „${wort}" kenne ich nicht.\n${HILFE}`);
535
+ // 🌐 Teil 3 (T3-3): ein Verteiler fuer beide Sprachen. Was heute nur ALLEIN gilt
536
+ // (`login`, `projekt liste`, `wache an` …), gilt auch weiterhin nur allein -- `kimal3 login
537
+ // extra` bleibt unbekannt. Der grosse Kommentar zur Nachsicht bei `wache --irgendwas` steht
538
+ // unten an `fuehreAus` und gilt unveraendert.
539
+ const erkannt = erkenneBefehl(argv);
540
+ if (erkannt) {
541
+ const code = await fuehreAus(erkannt.name, erkannt.rest);
542
+ if (code !== null)
543
+ return code;
544
+ }
545
+ console.error(`${ALLGEMEIN_TEXTE[sprache()].unknownCommand(wort)}${hilfeText(sprache())}`);
540
546
  return 1;
541
547
  }
542
548
  haupt()
@@ -564,10 +570,10 @@ haupt()
564
570
  // Node laeuft aus, sobald die Kanaele leer sind.
565
571
  .catch((f) => {
566
572
  if (f instanceof NichtAngemeldet) {
567
- console.error("Bitte zuerst `kimal3 login` ausführen.");
573
+ console.error(ALLGEMEIN_TEXTE[sprache()].signInFirst);
568
574
  process.exitCode = 2;
569
575
  return;
570
576
  }
571
- console.error("kimal3 ist gerade nicht erreichbar.");
577
+ console.error(ALLGEMEIN_TEXTE[sprache()].unreachable);
572
578
  process.exitCode = 3;
573
579
  });