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
@@ -1,18 +1,24 @@
1
1
  import { hostname } from "node:os";
2
2
  import { feld, ruf } from "../api.js";
3
3
  import { BASIS_URL, schreibeAnmeldung } from "../anmeldung.js";
4
+ import { sprache } from "../sprache.js";
5
+ import { ALLGEMEIN_TEXTE } from "../texte/allgemein.js";
6
+ import { stelleSkillUm } from "./skill-tausch.js";
4
7
  const warte = (ms) => new Promise((f) => setTimeout(f, ms));
5
8
  export async function login() {
9
+ // 🌐 Die Sprache wird bei JEDER Ausgabe neu gelesen: Die Abhol-Antwort nennt die Kontosprache
10
+ // (Task 5) und kann sie mitten im Lauf aendern -- die Adresse vorher kam noch in der alten.
11
+ const t = () => ALLGEMEIN_TEXTE[sprache()];
6
12
  const start = await ruf("/api/cli/login/start", { koerper: { bezeichnung: hostname() } });
7
13
  const nutzerCode = feld(start.koerper, "nutzer_code");
8
14
  const geraeteCode = feld(start.koerper, "geraete_code");
9
15
  if (start.status !== 200 || !nutzerCode || !geraeteCode) {
10
- console.error("kimal3 ist gerade nicht erreichbar.");
16
+ console.error(t().unreachable);
11
17
  return 3;
12
18
  }
13
- console.log(`\n Öffne ${BASIS_URL}/cli`);
14
- console.log(` und gib diesen Code ein: ${nutzerCode}\n`);
15
- console.log(" Warte auf die Bestätigung … (Abbruch mit Strg+C)");
19
+ console.log(t().loginOpen(BASIS_URL));
20
+ console.log(t().loginEnterCode(nutzerCode));
21
+ console.log(t().loginWaiting);
16
22
  const endeUm = Date.now() + 10 * 60 * 1000;
17
23
  while (Date.now() < endeUm) {
18
24
  await warte(2000);
@@ -34,7 +40,7 @@ export async function login() {
34
40
  if (a.status >= 500)
35
41
  continue;
36
42
  if (a.status === 410) {
37
- console.error("\nDer Anmeldecode ist abgelaufen. Bitte `kimal3 login` noch einmal.");
43
+ console.error(t().loginExpired);
38
44
  return 2;
39
45
  }
40
46
  // 🔴 Dieser Zweig MUSS hier oben stehen. Ohne ihn faellt eine 402 durch bis
@@ -48,19 +54,27 @@ export async function login() {
48
54
  // Schleife fragt also einfach weiter, statt aufzugeben. Genau dafuer ist
49
55
  // sie da. (Review-Befund 2026-08-14, s. `src/app/cli/actions.ts`.)
50
56
  if (a.status === 402) {
51
- console.error(`\n${feld(a.koerper, "meldung") ?? "Dafür brauchst du ein Pro-Konto."}`);
57
+ console.error(`\n${feld(a.koerper, "meldung") ?? t().needsPro}`);
52
58
  return 4;
53
59
  }
54
60
  const schluessel = feld(a.koerper, "schluessel");
55
61
  const email = feld(a.koerper, "email");
56
62
  if (a.status === 200 && schluessel && email) {
57
63
  schreibeAnmeldung({ schluessel, email });
58
- console.log(`\n✓ Angemeldet als ${email}.`);
64
+ console.log(t().signedInAs(email));
65
+ // 🌐 Ab hier ist die Kontosprache bekannt (die Abhol-Antwort nennt sie). Liegt Claudes Anleitung in der
66
+ // anderen Sprache -- `skill install` lief auf /pro VOR dem Login --, stellt der Login sie um. Der Login
67
+ // bleibt gelungen, was immer dabei passiert (Lagen-Tabelle in docs/qa/2026-10-02-mehrsprachig-teil-4-messungen.md).
68
+ const umstellung = stelleSkillUm(sprache());
69
+ if (umstellung === "umgestellt")
70
+ console.log(t().skillSwitched);
71
+ if (umstellung === "unvollstaendig")
72
+ console.error(t().skillSwitchIncomplete);
59
73
  return 0;
60
74
  }
61
- console.error("\nDas hat nicht geklappt.");
75
+ console.error(t().loginFailed);
62
76
  return 3;
63
77
  }
64
- console.error("\nDer Anmeldecode ist abgelaufen. Bitte `kimal3 login` noch einmal.");
78
+ console.error(t().loginExpired);
65
79
  return 2;
66
80
  }
@@ -1,5 +1,7 @@
1
1
  import { leseAnmeldung, loescheAnmeldung } from "../anmeldung.js";
2
2
  import { ruf } from "../api.js";
3
+ import { sprache } from "../sprache.js";
4
+ import { ALLGEMEIN_TEXTE } from "../texte/allgemein.js";
3
5
  /**
4
6
  * Abmelden — erst beim Server widerrufen, DANN die Datei loeschen.
5
7
  *
@@ -8,26 +10,29 @@ import { ruf } from "../api.js";
8
10
  * Server gueltig, und nichts wuerde davon berichten.
9
11
  */
10
12
  export async function logout() {
13
+ let t = ALLGEMEIN_TEXTE[sprache()];
11
14
  const a = leseAnmeldung();
12
15
  if (!a) {
13
16
  // Kein Fehler: Der Zielzustand ist bereits erreicht. Deshalb 0.
14
- console.log("Auf diesem Rechner war keine Anmeldung.");
17
+ console.log(t.noSignIn);
15
18
  return 0;
16
19
  }
17
20
  try {
18
21
  const antwort = await ruf("/api/cli/logout", { mitAusweis: true });
22
+ // 🌐 Die Antwort kann die Kontosprache umgestellt haben (Spec 3.2, Schritt 3): Was ab hier gedruckt wird, in der neuen.
23
+ t = ALLGEMEIN_TEXTE[sprache()];
19
24
  if (antwort.status !== 200 && antwort.status !== 401) {
20
- console.error("kimal3 ist gerade nicht erreichbar. Es wurde nichts geändert.");
25
+ console.error(t.unreachableNothingChanged);
21
26
  return 3;
22
27
  }
23
28
  // 401 heisst: Der Schluessel gilt ohnehin nicht mehr (schon widerrufen
24
29
  // oder abgelaufen). Der Zielzustand stimmt also — die Datei darf weg.
25
30
  }
26
31
  catch {
27
- console.error("kimal3 ist gerade nicht erreichbar. Es wurde nichts geändert.");
32
+ console.error(t.unreachableNothingChanged);
28
33
  return 3;
29
34
  }
30
35
  loescheAnmeldung();
31
- console.log(`Abgemeldet. (${a.email})`);
36
+ console.log(t.signedOut(a.email));
32
37
  return 0;
33
38
  }
@@ -1,6 +1,10 @@
1
1
  import { feld, ruf } from "../api.js";
2
+ import { sprache } from "../sprache.js";
3
+ import { befehlIn, schalterIn } from "../befehlswoerter.js";
4
+ import { ALLGEMEIN_TEXTE } from "../texte/allgemein.js";
5
+ import { PROJEKTE_TEXTE } from "../texte/projekte.js";
2
6
  import { schlussFehler } from "../fehler.js";
3
- import { MAX_PROJEKTE_ANZEIGE, antwortHatProjekteFeld, titelFeldLesen, projekteAusAntwort, projektZeilen, titelAusAlterAntwort, } from "../projekt-zeilen.js";
7
+ import { MAX_PROJEKTE_ANZEIGE, antwortHatProjekteFeld, titelFeldLesen, projekteAusAntwort, projektZeilen, titelAusAlterAntwort, titelListeZeilen, werkzeugZuAltZeilen, } from "../projekt-zeilen.js";
4
8
  import { BASIS_URL } from "../anmeldung.js";
5
9
  /**
6
10
  * Die eigenen Projekte anzeigen.
@@ -15,7 +19,10 @@ import { BASIS_URL } from "../anmeldung.js";
15
19
  * Werkzeug hergibt, laesst Claude entweder raten oder sich entschuldigen.*
16
20
  */
17
21
  export async function projektListe() {
18
- const antwort = await ruf("/api/cli/projekt", { methode: "GET", mitAusweis: true });
22
+ const antwort = await ruf("/api/cli/projekt", { methode: "GET", mitAusweis: true }); // kein-anzeigetext: HTTP-Methode
23
+ // 🌐 Die Antwort kann die Kontosprache umgestellt haben (Spec 3.2, Schritt 3): Was ab hier gedruckt wird, in der neuen.
24
+ const s = sprache();
25
+ const t = PROJEKTE_TEXTE[s];
19
26
  if (antwort.status === 200) {
20
27
  // 🔴 Ein `200` mit unlesbarem Rumpf ist KEIN Erfolg -- dieselbe Wache wie
21
28
  // in `firma-daten.ts`: `ruf` verschluckt Parse-Fehler zu `null`, und ein
@@ -43,16 +50,11 @@ export async function projektListe() {
43
50
  const alt = titelAusAlterAntwort(antwort.koerper);
44
51
  if (alt) {
45
52
  if (alt.length === 0) {
46
- console.log("Du hast noch keine Projekte eingetragen.");
47
- console.log(` Eintragen: kimal3 projekt neu --titel "…"`);
53
+ console.log(t.noProjects);
54
+ console.log(t.addHint(befehlIn("projekt neu", s), schalterIn("titel", s))); // kein-anzeigetext: Befehlsschluessel
48
55
  return 0;
49
56
  }
50
- console.log(`\n Deine Projekte (${alt.length} von ${MAX_PROJEKTE_ANZEIGE}):`);
51
- alt.forEach((t, i) => console.log(` ${i + 1}. ${t}`));
52
- console.log("\n (Diese Version der App liefert noch keine Zahlen zu den");
53
- console.log(" Projekten. Du hast nichts falsch gemacht — sie kommen mit");
54
- console.log(" dem nächsten Update der App von selbst dazu.)");
55
- console.log(`\n Ansehen: ${BASIS_URL}/profil\n`);
57
+ titelListeZeilen(alt, "alt", s, BASIS_URL).forEach((z) => console.log(z));
56
58
  return 0;
57
59
  }
58
60
  /*
@@ -75,35 +77,28 @@ export async function projektListe() {
75
77
  */
76
78
  const titel = titelFeldLesen(antwort.koerper);
77
79
  if (titel && titel.length > 0) {
78
- console.log(`\n Deine Projekte (${titel.length} von ${MAX_PROJEKTE_ANZEIGE}):`);
79
- titel.forEach((t, i) => console.log(` ${i + 1}. ${t}`));
80
- console.log("\n (Die Zahlen dazu kann diese Fassung von kimal3 nicht lesen —");
81
- console.log(" die App ist neuer als das Werkzeug. Du hast nichts falsch");
82
- console.log(" gemacht; `npm install -g kimal3` holt sie.)");
83
- console.log(`\n Ansehen: ${BASIS_URL}/profil\n`);
80
+ titelListeZeilen(titel, "neu", s, BASIS_URL).forEach((z) => console.log(z));
84
81
  return 0;
85
82
  }
86
- console.error("Diese Fassung von kimal3 versteht die Antwort der App nicht.");
87
- console.error("Du hast nichts falsch gemacht — das Werkzeug ist älter als die App.");
88
- console.error(" npm install -g kimal3");
83
+ werkzeugZuAltZeilen(s).forEach((z) => console.error(z));
89
84
  return 1;
90
85
  }
91
- console.error("Die Antwort war nicht lesbar. Bitte gleich noch einmal.");
86
+ console.error(ALLGEMEIN_TEXTE[s].unreadableAnswer);
92
87
  return 3;
93
88
  }
94
89
  // Eine leere Liste ist KEIN Fehler — sie ist eine gueltige Antwort auf
95
90
  // eine gueltige Frage. Deshalb 0 und ein Satz, kein Fehlertext.
96
91
  if (projekte.length === 0) {
97
- console.log("Du hast noch keine Projekte eingetragen.");
98
- console.log(` Eintragen: kimal3 projekt neu --titel "…"`);
92
+ console.log(t.noProjects);
93
+ console.log(t.addHint(befehlIn("projekt neu", s), schalterIn("titel", s))); // kein-anzeigetext: Befehlsschluessel
99
94
  return 0;
100
95
  }
101
- console.log(`\n Deine Projekte (${projekte.length} von ${MAX_PROJEKTE_ANZEIGE}):\n`);
102
- projektZeilen(projekte).forEach((z) => console.log(z));
103
- console.log(`\n Ansehen: ${BASIS_URL}/profil\n`);
96
+ console.log(t.projectsHeaderBlank(projekte.length, MAX_PROJEKTE_ANZEIGE));
97
+ projektZeilen(projekte, s).forEach((z) => console.log(z));
98
+ console.log(t.viewOnline(BASIS_URL));
104
99
  return 0;
105
100
  }
106
- const meldung = feld(antwort.koerper, "meldung") ?? "Das hat nicht geklappt.";
101
+ const meldung = feld(antwort.koerper, "meldung") ?? ALLGEMEIN_TEXTE[s].genericFailure;
107
102
  // 402 vor 401: Beide heissen "kommst hier nicht rein", verlangen aber
108
103
  // Gegenteiliges. Wer sie zusammenwirft, raet dem Menschen `kimal3 login` —
109
104
  // und der meldet sich dann erfolgreich an, ohne dass es etwas aendert.
@@ -112,11 +107,11 @@ export async function projektListe() {
112
107
  return 4;
113
108
  }
114
109
  if (antwort.status === 401) {
115
- console.error(`${meldung} Bitte \`kimal3 login\` ausführen.`);
110
+ console.error(`${meldung}${ALLGEMEIN_TEXTE[s].pleaseSignIn}`);
116
111
  return 2;
117
112
  }
118
113
  if (antwort.status === 409 && feld(antwort.koerper, "grund") === "kein-profil") {
119
- console.error(`${meldung} Leg es hier an: ${BASIS_URL}/profil`);
114
+ console.error(`${meldung}${t.createProfile(BASIS_URL)}`);
120
115
  return 1;
121
116
  }
122
117
  // ⚠️ Hier landet auch die `503` "Konto gerade nicht prüfbar" — und sie MUSS
@@ -128,7 +123,7 @@ export async function projektListe() {
128
123
  // 🔑 Den Rest entscheidet `schlussFehler` fuer alle Befehle gemeinsam --
129
124
  // insbesondere die `404` (B-7), die hier frueher auf die `1` fiel und damit
130
125
  // "Eingabe passt nicht" meldete, obwohl die Eingabe stimmte.
131
- const schluss = schlussFehler(antwort.status, meldung);
126
+ const schluss = schlussFehler(antwort.status, meldung, s);
132
127
  console.error(schluss.text);
133
128
  return schluss.wert;
134
129
  }
@@ -1,4 +1,7 @@
1
1
  import { feld, ruf } from "../api.js";
2
+ import { sprache } from "../sprache.js";
3
+ import { ALLGEMEIN_TEXTE } from "../texte/allgemein.js";
4
+ import { PROJEKTE_TEXTE } from "../texte/projekte.js";
2
5
  import { schlussFehler } from "../fehler.js";
3
6
  import { BASIS_URL } from "../anmeldung.js";
4
7
  import { zerlege } from "../argumente.js";
@@ -8,34 +11,22 @@ const OPTIONEN = [
8
11
  "titel", "beschreibung", "symbol", "stunden", "ergebnis", "umsatz",
9
12
  "wirkung", "wirkung-label", "link", "live-seit", "firma", "firma-id",
10
13
  ];
11
- // exportiert, damit `check:cli` die HILFE als AUSGABE-Text prueft (§24
12
- // erlaubt das: nicht die Anleitung, die den Text beschreibt, sondern der
13
- // Text, der wirklich auf dem Bildschirm des Kunden landet).
14
- export const HILFE = `
15
- kimal3 projekt neu --titel "Angebots-Generator" [Optionen]
16
-
17
- --titel Pflicht. 2–80 Zeichen.
18
- --beschreibung Bis 300 Zeichen.
19
- --symbol Eines der bekannten Symbole, z. B. bot, mail, workflow.
20
- --stunden Zeitersparnis pro Woche, 0–168.
21
- --ergebnis Ergebnis pro Monat in Euro, z. B. 1200 oder 1200,50.
22
- --umsatz Umsatz pro Monat in Euro.
23
- --wirkung Kurz, bis 20 Zeichen, z. B. "-70%".
24
- --wirkung-label Wozu, bis 40 Zeichen, z. B. "Angebotszeit".
25
- --link Vollstaendige Adresse mit http:// oder https://.
26
- --live-seit Datum als JJJJ-MM-TT.
27
- --firma "Name" Wenn du zu mehreren Firmen gehoerst: welche das Projekt bekommt.
28
- --firma-id "…" Falls zwei deiner Firmen gleich heissen.
29
- --ohne-firma Das Projekt gehoert keiner Firma (steht nur auf deinem Profil).
30
- `;
14
+ // exportiert, damit `check:cli` die Hilfe als AUSGABE-Text prueft (§24 erlaubt das: nicht die
15
+ // Anleitung, die den Text beschreibt, sondern der Text, der wirklich auf dem Bildschirm des Kunden
16
+ // landet).
17
+ export function hilfe(s) {
18
+ return PROJEKTE_TEXTE[s].newHelp;
19
+ }
31
20
  export async function projektNeu(argv) {
21
+ let s = sprache();
22
+ let t = PROJEKTE_TEXTE[s];
32
23
  // ⚠️ VOR dem Zerlegen abfragen, nicht danach.
33
24
  // Der Bauplan hatte "help" in der Optionsliste und fragte hinterher. Das
34
25
  // konnte nie greifen: `zerlege` verlangt zu jeder Option einen Wert, also
35
26
  // scheitert `--help` schon dort mit "fehlt ihr Wert". Ein Schalter ist keine
36
27
  // Option mit Wert — er gehoert vor den Zerleger.
37
28
  if (argv.includes("--help") || argv.includes("-h")) {
38
- console.log(HILFE);
29
+ console.log(hilfe(s));
39
30
  return 0;
40
31
  }
41
32
  // ⚠️ `zerlege` kennt keine Optionen ohne Wert -- `--ohne-firma` wird
@@ -43,21 +34,21 @@ export async function projektNeu(argv) {
43
34
  // in kontext-hoch.ts. Die Rechnung dahinter steht in projekt-neu-firma.ts,
44
35
  // damit `check:cli` sie pruefen kann (Final-Review F-1).
45
36
  const { rest, ohneFirma } = ohneFirmaAusArgv(argv);
46
- const z = zerlege(rest, OPTIONEN);
37
+ const z = zerlege(rest, OPTIONEN, s);
47
38
  if (!z.ok) {
48
39
  console.error(z.meldung);
49
40
  return 1;
50
41
  }
51
42
  const o = z.werte.optionen;
52
43
  if (!o.titel) {
53
- console.error("Es fehlt --titel.\n" + HILFE);
44
+ console.error(t.missingTitle + hilfe(s));
54
45
  return 1;
55
46
  }
56
47
  // 🔴 Final-Review F-3: `--ohne-firma` zusammen mit `--firma`/`--firma-id`
57
48
  // liesse die Route den Schalter still gewinnen lassen (`firmenWunschAus` in
58
49
  // route.ts prueft `ohne_firma` zuerst). Hier abweisen, bevor der Wunsch
59
50
  // ueberhaupt losgeschickt wird.
60
- const wunsch = firmenKoerperAus(o.firma, o["firma-id"], ohneFirma);
51
+ const wunsch = firmenKoerperAus(o.firma, o["firma-id"], ohneFirma, s);
61
52
  if (!wunsch.ok) {
62
53
  console.error(wunsch.meldung);
63
54
  return 1;
@@ -73,14 +64,17 @@ export async function projektNeu(argv) {
73
64
  ...wunsch.koerper,
74
65
  },
75
66
  });
67
+ // 🌐 Die Antwort kann die Kontosprache umgestellt haben (Spec 3.2, Schritt 3): Was ab hier gedruckt wird, in der neuen.
68
+ s = sprache();
69
+ t = PROJEKTE_TEXTE[s];
76
70
  if (antwort.status === 201) {
77
71
  const titel = feld(antwort.koerper, "titel") ?? o.titel;
78
72
  const anzahl = antwort.koerper?.anzahl;
79
- console.log(`✓ „${titel}" ins Erfolgsprofil eingetragen (Projekt ${anzahl} von 12).`);
80
- console.log(` Ansehen: ${BASIS_URL}/profil`);
73
+ console.log(t.projectAdded(titel, anzahl));
74
+ console.log(t.viewOnlineShort(BASIS_URL));
81
75
  return 0;
82
76
  }
83
- const meldung = feld(antwort.koerper, "meldung") ?? "Das hat nicht geklappt.";
77
+ const meldung = feld(antwort.koerper, "meldung") ?? ALLGEMEIN_TEXTE[s].genericFailure;
84
78
  // 402 vor 401: Beide heissen "kommst hier nicht rein", verlangen aber
85
79
  // Gegenteiliges. Wer sie zusammenwirft, raet dem Menschen `kimal3 login` —
86
80
  // und der meldet sich dann erfolgreich an, ohne dass es etwas aendert.
@@ -89,11 +83,11 @@ export async function projektNeu(argv) {
89
83
  return 4;
90
84
  }
91
85
  if (antwort.status === 401) {
92
- console.error(`${meldung} Bitte \`kimal3 login\` ausführen.`);
86
+ console.error(`${meldung}${ALLGEMEIN_TEXTE[s].pleaseSignIn}`);
93
87
  return 2;
94
88
  }
95
89
  if (antwort.status === 409 && feld(antwort.koerper, "grund") === "kein-profil") {
96
- console.error(`${meldung} Leg es hier an: ${BASIS_URL}/profil`);
90
+ console.error(`${meldung}${t.createProfile(BASIS_URL)}`);
97
91
  return 1;
98
92
  }
99
93
  if (antwort.status === 409) {
@@ -101,7 +95,7 @@ export async function projektNeu(argv) {
101
95
  // Meldung und die Firmen als fertige Befehle. Der Befehl in der Zeile
102
96
  // traegt ALLE getippten Optionen mit, nicht nur --titel (Final-Review
103
97
  // F-2) -- sonst verliert der Kunde beim Kopieren --stunden, --ergebnis &co.
104
- return absage(antwort, `projekt neu ${befehlszeileOhneFirma(o)}`);
98
+ return absage(antwort, "projekt neu", ` ${befehlszeileOhneFirma(o, s)}`); // kein-anzeigetext: Befehlsschluessel
105
99
  }
106
100
  // ⚠️ Hier landet auch die `503` "Konto gerade nicht prüfbar" — und sie MUSS
107
101
  // hier landen, nicht oben bei der 402. Rueckgabewert 3 heisst "nochmal
@@ -112,7 +106,7 @@ export async function projektNeu(argv) {
112
106
  // 🔑 Den Rest entscheidet `schlussFehler` fuer alle Befehle gemeinsam --
113
107
  // insbesondere die `404` (B-7), die hier frueher auf die `1` fiel und damit
114
108
  // "Eingabe passt nicht" meldete, obwohl die Eingabe stimmte.
115
- const schluss = schlussFehler(antwort.status, meldung);
109
+ const schluss = schlussFehler(antwort.status, meldung, s);
116
110
  console.error(schluss.text);
117
111
  return schluss.wert;
118
112
  }
@@ -4,8 +4,11 @@ import { fileURLToPath } from "node:url";
4
4
  import { join } from "node:path";
5
5
  import { einstellungenDatei, trageWacheEin } from "../einstellungen-schreiben.js";
6
6
  import { merkeSitzung, sitzungJetzt } from "../wache-schalter.js";
7
- import { STEMPEL_MARKE } from "../nachricht.js";
7
+ import { stempelZeile } from "../nachricht.js";
8
8
  import { eigeneVersion } from "../version.js";
9
+ import { sprache } from "../sprache.js";
10
+ import { andereSpracheZuRaeumen, skillQuelle } from "../slash-befehle.js";
11
+ import { SKILL_TEXTE } from "../texte/skill.js";
9
12
  export const VERALTETE_BEFEHLE = [
10
13
  /*
11
14
  🔴 **Der ERSTE Eintrag ueberhaupt** (2026-08-28). `/k3firma` heisst jetzt
@@ -45,7 +48,7 @@ export function skillOrdner() {
45
48
  return join(homedir(), ".claude", "skills", "kimal3");
46
49
  }
47
50
  export function skillDatei() {
48
- return join(skillOrdner(), "SKILL.md");
51
+ return join(skillOrdner(), "SKILL.md"); // kein-anzeigetext: Dateiname
49
52
  }
50
53
  /**
51
54
  * Haengt die eigene Versionsnummer ans ENDE des Skill-Textes.
@@ -77,12 +80,14 @@ export function skillDatei() {
77
80
  * ⚠️ Ohne eigene Nummer GAR KEIN Stempel. Ein erfundener Ersatzwert waere
78
81
  * schlimmer als keiner: Die CLI vergliche spaeter gegen ihre eigene Erfindung.
79
82
  * Ohne Stempel antwortet `skillZustand` "veraltet" -- die harmlose Seite.
83
+ *
84
+ * 🌐 Seit Teil 4 traegt der englische Stempel ` sprache=en`; der deutsche ist Zeichen fuer Zeichen der alte.
80
85
  */
81
- function mitStempel(inhalt) {
86
+ export function mitStempel(inhalt, s) {
82
87
  const eigene = eigeneVersion();
83
88
  if (eigene === null)
84
89
  return inhalt;
85
- return `${inhalt.trimEnd()}\n\n<!-- ${STEMPEL_MARKE}${eigene} -->\n`;
90
+ return `${inhalt.trimEnd()}\n\n${stempelZeile(eigene, s)}\n`;
86
91
  }
87
92
  /**
88
93
  * Legt das Skill UND die Slash-Befehle GLOBAL ab, damit Claude dieses Werkzeug
@@ -103,7 +108,9 @@ function mitStempel(inhalt) {
103
108
  * `dist/befehle/skill-install.js` -> zwei Ebenen hoch -> `skill/…`.
104
109
  */
105
110
  export async function skillInstall() {
106
- const quelle = fileURLToPath(new URL("../../skill/SKILL.md", import.meta.url));
111
+ const s = sprache();
112
+ const t = SKILL_TEXTE[s];
113
+ const quelle = fileURLToPath(new URL(`../../${skillQuelle(s).skill}`, import.meta.url)); // kein-anzeigetext: Paketpfad
107
114
  if (!existsSync(quelle)) {
108
115
  /*
109
116
  🔴 **`1`, nicht `3`** (Review-Befund, 2026-08-18). Ein unvollstaendiges
@@ -112,8 +119,8 @@ export async function skillInstall() {
112
119
  Text, und wuerde einen Rat geben, der garantiert nicht hilft. Genau der
113
120
  Fehlertyp, den dieser Zweig bei B-5, B-7 und L-7 behoben hat.
114
121
  */
115
- console.error("Das Skill liegt nicht im Paket. Bitte kimal3 neu installieren:");
116
- console.error(" npm install -g kimal3");
122
+ console.error(t.skillMissing);
123
+ console.error(" npm install -g kimal3"); // kein-anzeigetext: Befehlszeile zum Kopieren
117
124
  return 1;
118
125
  }
119
126
  // 🔑 GLOBAL, nicht projektlokal (Fund von Dennis, 2026-08-13).
@@ -161,15 +168,15 @@ export async function skillInstall() {
161
168
  Der Stempel spricht ueber die SKILL.md, nicht ueber den ganzen Lauf.
162
169
  */
163
170
  const inhalt = readFileSync(quelle, "utf8");
164
- writeFileSync(ziel, mitStempel(inhalt), "utf8");
171
+ writeFileSync(ziel, mitStempel(inhalt, s), "utf8");
165
172
  }
166
173
  catch (fehler) {
167
- console.error(`Das Skill liess sich nicht schreiben: ${String(fehler)}`);
174
+ console.error(t.skillWriteFailed(String(fehler)));
168
175
  return 3;
169
176
  }
170
- console.log(gabEsSchon ? "✓ Skill aktualisiert." : "✓ Skill angelegt.");
177
+ console.log(gabEsSchon ? t.skillUpdated : t.skillCreated);
171
178
  console.log(` ${ziel}`);
172
- const namen = slashBefehleInstallieren();
179
+ const namen = slashBefehleInstallieren(s, false);
173
180
  if (namen === "kaputt") {
174
181
  /*
175
182
  ⚠️ **Was hier zaehlt, ist die TEILWIRKUNG** (Review-Runde 2, 2026-08-18):
@@ -178,31 +185,31 @@ export async function skillInstall() {
178
185
  nun gilt. *Ein halber Erfolg ist weder ein Erfolg noch ein Totalausfall
179
186
  -- er ist ein eigener Zustand und braucht einen eigenen Satz.*
180
187
  */
181
- console.error(" Das Skill ist installiert, die Slash-Befehle sind es NICHT.");
188
+ console.error(t.skillOnlyInstalled);
182
189
  return 1;
183
190
  }
184
191
  const { geschrieben, uebersprungen, unlesbar, gestoert } = namen;
185
192
  if (geschrieben.length > 0) {
186
- console.log(`✓ ${geschrieben.length} Slash-Befehle: ${geschrieben.map((n) => `/${n}`).join(" · ")}`);
193
+ console.log(t.commandsInstalled(geschrieben.length, geschrieben.map((n) => `/${n}`).join(" · ")));
187
194
  console.log(` ${join(homedir(), ".claude", "commands")}`);
188
195
  }
189
196
  // ⚠️ Uebersprungene Dateien gehoeren in die AUSGABE, nicht nur in die Logik.
190
197
  // Ein Mensch, dem stillschweigend ein Befehl fehlt, sucht den Fehler bei
191
198
  // sich. (Review-Runde 2, 2026-08-18.)
192
199
  if (uebersprungen.length > 0) {
193
- console.log(`⚠ ${uebersprungen.length} übersprungen: ${uebersprungen.map((n) => `/${n}`).join(" · ")}`);
194
- console.log(" Dort liegt bereits eine Datei, die nicht von kimal3 stammt —");
195
- console.log(" sie bleibt unangetastet. Wer die kimal3-Fassung will, benennt");
196
- console.log(" die eigene um und führt den Befehl erneut aus.");
200
+ console.log(t.skipped(uebersprungen.length, uebersprungen.map((n) => `/${n}`).join(" · ")));
201
+ console.log(t.skipped1);
202
+ console.log(t.skipped2);
203
+ console.log(t.skipped3);
197
204
  }
198
205
  // 🔴 Ein LESEFEHLER bekommt einen eigenen Satz (Review-Runde 6): Er sieht
199
206
  // aus wie Fremdbesitz, ist aber keiner -- und der Rat "benenn deine Datei
200
207
  // um" waere hier falsch.
201
208
  if (unlesbar.length > 0) {
202
- console.log(`⚠ ${unlesbar.length} nicht lesbar: ${unlesbar.map((n) => `/${n}`).join(" · ")}`);
203
- console.log(" Diese Dateien liessen sich nicht öffnen — vielleicht sind sie");
204
- console.log(" gerade in Benutzung. Sie wurden nicht angefasst; ein zweiter");
205
- console.log(" Versuch gleich kann klappen.");
209
+ console.log(t.unreadable(unlesbar.length, unlesbar.map((n) => `/${n}`).join(" · ")));
210
+ console.log(t.unreadable1);
211
+ console.log(t.unreadable2);
212
+ console.log(t.unreadable3);
206
213
  }
207
214
  /*
208
215
  🔴 **Kein Slash-Befehl installiert ist kein Erfolg** (Review-Runde 5,
@@ -218,8 +225,8 @@ export async function skillInstall() {
218
225
  */
219
226
  if (geschrieben.length === 0) {
220
227
  const wiederholbar = gestoert || unlesbar.length > 0;
221
- console.error(" Es wurde KEIN Slash-Befehl installiert.");
222
- console.error(" Das Skill selbst ist installiert und funktioniert.");
228
+ console.error(t.noCommandInstalled1);
229
+ console.error(t.noCommandInstalled2);
223
230
  return wiederholbar ? 3 : 1;
224
231
  }
225
232
  /*
@@ -229,8 +236,8 @@ export async function skillInstall() {
229
236
  Teilwirkung weg und meldete alles als misslungen.
230
237
  */
231
238
  if (gestoert) {
232
- console.error(" Nicht alle Slash-Befehle konnten geschrieben werden.");
233
- console.error(" Die oben genannten stehen; ein zweiter Lauf holt den Rest.");
239
+ console.error(t.notAllWritten1);
240
+ console.error(t.notAllWritten2);
234
241
  return 3;
235
242
  }
236
243
  /*
@@ -246,7 +253,7 @@ export async function skillInstall() {
246
253
  Hinweis, den kein Befolgen aufloesen konnte.
247
254
  */
248
255
  if (unlesbar.length > 0) {
249
- console.error(" Der Lauf ist damit unvollstaendig -- ein zweiter Versuch holt den Rest.");
256
+ console.error(t.runIncomplete);
250
257
  return 3;
251
258
  }
252
259
  /*
@@ -283,33 +290,31 @@ export async function skillInstall() {
283
290
  merkeSitzung(sitzungJetzt());
284
291
  }
285
292
  if (wache.art === "eingetragen") {
286
- console.log(" Die taegliche kimal3-Pruefung ist eingetragen. Sie meldet sich nur, wenn etwas ansteht.");
293
+ console.log(t.watchSetUp);
287
294
  }
288
295
  else if (wache.art === "stand-schon") {
289
- console.log(" Die taegliche kimal3-Pruefung steht schon in deinen Einstellungen.");
296
+ console.log(t.watchAlready);
290
297
  }
291
298
  else if (wache.art === "abgelehnt") {
292
299
  console.error("");
293
- console.error(" Die taegliche kimal3-Pruefung konnte NICHT eingetragen werden.");
294
- console.error(` Deine Claude-Einstellungen sind nicht lesbar: ${einstellungenDatei()}`);
295
- console.error(wache.grund === "kein-gueltiges-json"
296
- ? " Die Datei ist kein gueltiges JSON. Sie wurde NICHT angefasst."
297
- : " Die Datei hat eine Form, die wir nicht verstehen. Sie wurde NICHT angefasst.");
298
- console.error(" Wiederholen hilft nicht -- erst die Datei in Ordnung bringen.");
300
+ console.error(t.watchFailed);
301
+ console.error(t.settingsUnreadable(einstellungenDatei()));
302
+ console.error(wache.grund === "kein-gueltiges-json" ? t.settingsNotJson : t.settingsStrangeShape);
303
+ console.error(t.settingsRetryNoHelp);
299
304
  return 1;
300
305
  }
301
306
  else if (wache.art === "gescheitert") {
302
307
  console.error("");
303
- console.error(" Die taegliche kimal3-Pruefung konnte NICHT eingetragen werden.");
304
- console.error(` ${einstellungenDatei()}: ${wache.grund}`);
305
- console.error(" Deine bisherigen Einstellungen sind unveraendert.");
308
+ console.error(t.watchFailed);
309
+ console.error(t.settingsFailedLine(einstellungenDatei(), ursacheDes(wache, t)));
310
+ console.error(t.settingsUnchanged);
306
311
  return 1;
307
312
  }
308
313
  if (geschrieben.includes("k3")) {
309
- console.log(" Tipp /k3 in Claude Code. (Taucht nichts auf, einmal neu starten.)");
314
+ console.log(t.tipK3);
310
315
  }
311
316
  else {
312
- console.log(` Tipp /${geschrieben[0]} in Claude Code. (Taucht nichts auf, einmal neu starten.)`);
317
+ console.log(t.tipOther(geschrieben[0]));
313
318
  }
314
319
  return 0;
315
320
  }
@@ -326,16 +331,22 @@ export async function skillInstall() {
326
331
  * Wiederholen hilft NIE) bzw. `"stoerung"` (Schreiben ging schief, Wiederholen
327
332
  * kann helfen). *Zwei Fehlerfaelle, die Gegenteiliges verlangen, duerfen nicht
328
333
  * denselben Namen tragen.*
334
+ *
335
+ * 🌐 `leise`: `kimal3 login` stellt den Satz nach der Anmeldung auf die Kontosprache um (`skill-tausch.ts`)
336
+ * und sagt dazu EINE Zeile -- die Einzelmeldungen dieses Laufs gehoeren dann nicht auf den Bildschirm.
329
337
  */
330
- function slashBefehleInstallieren() {
331
- const quellOrdner = fileURLToPath(new URL("../../skill/commands", import.meta.url));
338
+ export function slashBefehleInstallieren(s, leise) {
339
+ const t = SKILL_TEXTE[s];
340
+ const quellOrdner = fileURLToPath(new URL(`../../${skillQuelle(s).befehle}`, import.meta.url)); // kein-anzeigetext: Paketpfad
332
341
  if (!existsSync(quellOrdner)) {
333
- console.error("Die Slash-Befehle liegen nicht im Paket. Bitte kimal3 neu installieren.");
342
+ if (!leise)
343
+ console.error(t.commandsMissing);
334
344
  return "kaputt";
335
345
  }
336
346
  const dateien = readdirSync(quellOrdner).filter((n) => n.endsWith(".md")).sort();
337
347
  if (dateien.length === 0) {
338
- console.error("Im Paket liegt kein einziger Slash-Befehl. Bitte kimal3 neu installieren.");
348
+ if (!leise)
349
+ console.error(t.commandsNone);
339
350
  return "kaputt";
340
351
  }
341
352
  const zielOrdner = join(homedir(), ".claude", "commands");
@@ -390,7 +401,8 @@ function slashBefehleInstallieren() {
390
401
  `geschrieben` und `uebersprungen` sind ausserhalb des `try` deklariert,
391
402
  genau dafuer.
392
403
  */
393
- console.error(`Die Slash-Befehle liessen sich nicht alle schreiben: ${String(fehler)}`);
404
+ if (!leise)
405
+ console.error(t.commandsWriteFailed(String(fehler)));
394
406
  gestoert = true;
395
407
  }
396
408
  /*
@@ -426,9 +438,11 @@ function slashBefehleInstallieren() {
426
438
  immer den Dateinamen. Ohne die Angleichung schluege der Vergleich fehl, und
427
439
  ein Eintrag ohne `.md` bliebe fuer immer liegen.
428
440
  */
429
- const alsDatei = (n) => (n.endsWith(".md") ? n : `${n}.md`);
441
+ const alsDatei = (n) => (n.endsWith(".md") ? n : `${n}.md`); // kein-anzeigetext: Dateiname
430
442
  const ersatzLiegt = VERALTETE_BEFEHLE.filter((e) => e.ersetztDurch === null || geschrieben.includes(alsDatei(e.ersetztDurch))).map((e) => e.alt);
431
- raeumeVeralteteAuf(zielOrdner, ersatzLiegt);
443
+ // 🌐 Teil 4 (T4-3): Die eigenen Dateien der anderen Sprache gehen denselben Weg wie umbenannte Befehle --
444
+ // nur, wenn ihr Ersatz in diesem Lauf wirklich geschrieben wurde, und nur mit unserer Marke (`raeumeVeralteteAuf`).
445
+ raeumeVeralteteAuf(zielOrdner, [...ersatzLiegt, ...andereSpracheZuRaeumen(geschrieben, s)], leise);
432
446
  return {
433
447
  geschrieben: geschrieben.map((n) => n.replace(/\.md$/, "")),
434
448
  uebersprungen: uebersprungen.map((n) => n.replace(/\.md$/, "")),
@@ -436,6 +450,20 @@ function slashBefehleInstallieren() {
436
450
  gestoert,
437
451
  };
438
452
  }
453
+ /**
454
+ * Die Ursache, warum die Wache nicht eingetragen werden konnte, in der Sprache des Menschen.
455
+ * `einstellungen-schreiben.ts` nennt nur den SCHRITT (und die Meldung des Systems); den Satz dazu
456
+ * liefert die Textdatei -- dort steht er in beiden Sprachen.
457
+ */
458
+ export function ursacheDes(wache, t) {
459
+ if (wache.schritt === "kontrolle")
460
+ return t.stepCheck(wache.detail);
461
+ if (wache.schritt === "lesen")
462
+ return t.stepRead(wache.detail);
463
+ if (wache.schritt === "sichern")
464
+ return t.stepBackup(wache.detail);
465
+ return t.stepWrite(wache.detail);
466
+ }
439
467
  /** Liest eine Datei — `null`, wenn sie sich nicht lesen laesst. */
440
468
  function leseWennMoeglich(pfad) {
441
469
  try {
@@ -465,7 +493,7 @@ function leseWennMoeglich(pfad) {
465
493
  * Gibt zurueck, was wirklich entfernt wurde -- eine Behauptung waere hier
466
494
  * genau das Falsche.
467
495
  */
468
- export function raeumeVeralteteAuf(zielOrdner, veraltet) {
496
+ export function raeumeVeralteteAuf(zielOrdner, veraltet, leise = false) {
469
497
  const entfernt = [];
470
498
  for (const eintrag of veraltet) {
471
499
  /*
@@ -474,7 +502,7 @@ export function raeumeVeralteteAuf(zielOrdner, veraltet) {
474
502
  ein" -- gejoint wird aber ein DATEIname. Wer `"k3alt"` eintraegt, bekam
475
503
  keinen Fehler: nichts wird entfernt, nichts gemeldet.
476
504
  */
477
- const alt = eintrag.endsWith(".md") ? eintrag : `${eintrag}.md`;
505
+ const alt = eintrag.endsWith(".md") ? eintrag : `${eintrag}.md`; // kein-anzeigetext: Dateiname
478
506
  const weg = join(zielOrdner, alt);
479
507
  try {
480
508
  if (!existsSync(weg))
@@ -487,7 +515,8 @@ export function raeumeVeralteteAuf(zielOrdner, veraltet) {
487
515
  }
488
516
  rmSync(weg);
489
517
  entfernt.push(alt);
490
- console.log(` (veralteten Befehl /${alt.replace(/\.md$/, "")} entfernt)`);
518
+ if (!leise)
519
+ console.log(SKILL_TEXTE[sprache()].outdatedRemoved(alt.replace(/\.md$/, "")));
491
520
  }
492
521
  catch {
493
522
  // Bewusst still: Die Befehle sind installiert. Eine liegengebliebene