kimal3 0.1.1 → 0.1.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,112 @@
1
+ /**
2
+ * Alle Befehle, die `kimal3` kennt — als DATEN, nicht als Fliesstext.
3
+ *
4
+ * 🔑 Warum das eine eigene Datei ist (Spec US-5, 2026-08-18): Die Liste wird an
5
+ * ZWEI Orten angezeigt — in der Terminal-Hilfe und im aktiven Pro-Bereich der
6
+ * App. Zwei handgeschriebene Listen sind die "dieselbe Zahl an zwei Tueren"-
7
+ * Falle: Der naechste Befehl erschiene in einer und fehlte in der anderen.
8
+ * Genau so war die `check:`-Prueferliste in CLAUDE.md monatelang unvollstaendig.
9
+ *
10
+ * ⚠️ DIESE DATEI IMPORTIERT NICHTS, und das ist Absicht: Die CLI baut mit
11
+ * NodeNext (Import-Pfade enden auf `.js`), die App mit dem Bundler. Eine Datei
12
+ * ohne eigene Importe liest sich aus beiden Welten ohne Uebersetzung.
13
+ *
14
+ * ⚠️ Der Katalog beschreibt, WAS ES GIBT — nicht, wer es darf. Die Pro-Schranke
15
+ * sitzt in der Route und im Befehl, nicht hier.
16
+ */
17
+ export const BEFEHLE = [
18
+ {
19
+ befehl: "kimal3 login",
20
+ beschreibung: "Diesen Rechner mit deinem Konto verbinden",
21
+ wer: "du",
22
+ rhythmus: "einmal pro Rechner",
23
+ beispiel: "kimal3 login",
24
+ details: [
25
+ "Der Befehl zeigt dir eine Adresse und einen kurzen Code. Die Adresse im Browser öffnen, den Code eintippen, fertig — das Terminal merkt die Bestätigung von selbst. Zehn Minuten hast du dafür Zeit.",
26
+ "Danach musst du dich NICHT bei jedem Start neu anmelden: Der Ausweis liegt als Datei in deinem Benutzerordner und übersteht IDE-Neustarts, Rechner-Neustarts und Updates. Bei jedem Aufruf wird er im Hintergrund aufgefrischt, ohne dass du etwas merkst.",
27
+ "Gelegentlich reißt diese Kette trotzdem — dann sagt ein Befehl „Bitte kimal3 login ausführen\". Das ist kein Fehler, sondern der Normalfall nach längerer Pause: einmal wiederholen und weitermachen.",
28
+ ],
29
+ },
30
+ {
31
+ befehl: "kimal3 logout",
32
+ beschreibung: "Verbindung wieder aufheben",
33
+ wer: "du",
34
+ rhythmus: "nur wenn nötig",
35
+ beispiel: "kimal3 logout",
36
+ details: [
37
+ "Hebt die Verbindung dieses einen Rechners auf. Für den Alltag brauchst du ihn nicht.",
38
+ "Sinnvoll ist er, wenn du auf einem fremden oder geteilten Rechner gearbeitet hast — oder wenn ein Gerät abhandenkommt. Deine anderen Rechner bleiben angemeldet.",
39
+ ],
40
+ },
41
+ {
42
+ befehl: "kimal3 projekt neu …",
43
+ beschreibung: "Ein Projekt ins Erfolgsprofil eintragen",
44
+ wer: "claude",
45
+ ausloeser: "trag das als Projekt ein",
46
+ beispiel: 'kimal3 projekt neu --titel "Angebots-Generator" --stunden 4 --ergebnis 1200',
47
+ details: [
48
+ "Wenn du in Claude etwas fertig gebaut hast und sagst „trag das als Projekt ein\", stellt Claude den Befehl selbst zusammen — mit Titel, Zeitersparnis, Ergebnis und allem, was ihr gerade besprochen habt. Das ist der bequeme Weg, weil Claude die Zahlen schon kennt.",
49
+ "Selbst tippen kannst du ihn genauso. Pflicht ist nur --titel; alles andere ist freiwillig. Die vollständige Liste aller Angaben zeigt: kimal3 projekt neu --help",
50
+ "Das Projekt landet in deinem Erfolgsprofil und ist damit Teil deines nachprüfbaren Nachweises.",
51
+ ],
52
+ },
53
+ {
54
+ befehl: "kimal3 projekt liste",
55
+ beschreibung: "Deine eingetragenen Projekte anzeigen",
56
+ wer: "claude",
57
+ ausloeser: "zeig meine Projekte",
58
+ beispiel: "kimal3 projekt liste",
59
+ details: [
60
+ "Zeigt, was schon in deinem Erfolgsprofil steht.",
61
+ "Wofür es wirklich da ist: Claude schaut hier VOR dem Eintragen nach. Ohne diesen Blick stünde dasselbe Projekt irgendwann zweimal auf einer öffentlichen Seite, die deinen Namen trägt.",
62
+ ],
63
+ },
64
+ {
65
+ befehl: "kimal3 firma daten",
66
+ beschreibung: "Die Daten deiner Firma holen, damit Claude sie auswerten kann",
67
+ wer: "claude",
68
+ ausloeser: "werte meine Firma aus",
69
+ beispiel: 'kimal3 firma daten --firma "Nordwind Handel GmbH"',
70
+ details: [
71
+ "Holt Kennzahlen, Menschen und Projekte deiner Firma und gibt sie an Claude weiter. Die Auswertung macht dann Claude — frag einfach los: „Welche drei Projekte bringen den meisten messbaren Ertrag?\" oder „Was sollten wir als Nächstes automatisieren?\"",
72
+ "Gehörst du zu mehreren Firmen, fragt der Befehl nach — er wählt nie still eine aus.",
73
+ "⚠️ Es sind personenbezogene Daten von Kolleginnen und Kollegen. Für den Einsatz in einer echten Firma braucht es ein kommerzielles Claude-Konto mit Auftragsverarbeitungsvertrag; werden Leistungen von Beschäftigten ausgewertet, ist das in Deutschland mitbestimmungspflichtig.",
74
+ ],
75
+ },
76
+ {
77
+ befehl: "kimal3 skill install",
78
+ beschreibung: "Claude beibringen, dieses Werkzeug zu benutzen — und dir die /k3-Befehle geben",
79
+ wer: "du",
80
+ rhythmus: "einmal — und nach jedem Update",
81
+ beispiel: "kimal3 skill install",
82
+ details: [
83
+ "Das ist der Befehl, der aus den anderen erst etwas macht. Er legt ZWEI Dinge in deinem Claude-Ordner ab: eine Anleitung für Claude (das „Skill\") und vier Slash-Befehle für dich.",
84
+ "🤖 Die Anleitung sorgt dafür, dass Claude von selbst weiß, dass es kimal3 gibt — du sagst „das Projekt ist fertig\", er trägt es ein.",
85
+ "🧑 Die Slash-Befehle sind dein eigener Griff: Tipp /k3 in Claude Code, und du siehst die ganze Familie — /k3 · /k3projekt · /k3liste · /k3firma. Damit musst du dir keinen Satz merken.",
86
+ "Er läuft NICHT automatisch. Einmal nach der Installation ausführen — und noch einmal nach jedem Update des Werkzeugs, sonst arbeitet Claude weiter mit der alten Anleitung.",
87
+ "Woran du merkst, dass es geklappt hat: Der Befehl meldet „Skill angelegt\" (oder „aktualisiert\") und zählt die Slash-Befehle auf. Beides kennt Claude ab der NÄCHSTEN Sitzung — die laufende muss also einmal neu gestartet werden.",
88
+ ],
89
+ },
90
+ ];
91
+ /**
92
+ * Die Terminal-Hilfe, gebaut aus dem Katalog.
93
+ *
94
+ * Die Spalten richten sich an der laengsten Befehlszeile aus — von Hand
95
+ * gesetzte Leerzeichen waeren beim naechsten laengeren Befehl krumm.
96
+ *
97
+ * 📌 Sie nimmt bewusst NUR `befehl` und `beschreibung`: Im Terminal ist eine
98
+ * Uebersicht gefragt, kein Handbuch. Die langen `details` haben ihren Platz auf
99
+ * `/pro`, wo man sie auf- und zuklappen kann — `--help` je Befehl bleibt der
100
+ * Weg im Terminal.
101
+ */
102
+ export function hilfeText() {
103
+ const breite = Math.max(...BEFEHLE.map((b) => b.befehl.length));
104
+ const zeilen = BEFEHLE.map((b) => ` ${b.befehl.padEnd(breite + 2)}${b.beschreibung}`).join("\n");
105
+ return `
106
+ kimal3 — trag deine KI-Projekte dort ein, wo sie entstehen.
107
+
108
+ ${zeilen}
109
+
110
+ Mehr zu einem Befehl: kimal3 projekt neu --help
111
+ `;
112
+ }
package/dist/fehler.js ADDED
@@ -0,0 +1,66 @@
1
+ /**
2
+ * Der gemeinsame Schluss aller Befehle: Was tun mit einer Absage, die kein
3
+ * Befehl fuer sich beantworten kann?
4
+ *
5
+ * 🔑 **Warum das hier wohnt und nicht in `api.ts`:** Es ist eine reine
6
+ * Rechnung -- Zahl und Text rein, Zahl und Text raus, kein Netz, keine Datei.
7
+ * Damit ist sie in `check:cli` messbar, ohne einen Server zu starten. `api.ts`
8
+ * zieht ueber `anmeldung.ts` das Dateisystem nach; wer von dort importiert,
9
+ * kann die Rechnung nicht mehr allein pruefen.
10
+ */
11
+ /**
12
+ * ⚠️ **404 ist KEIN Eingabefehler** (QA-Befund B-7, 2026-08-18).
13
+ *
14
+ * Vorher endeten alle drei Befehle auf `status >= 500 ? 3 : 1`. Eine `404`
15
+ * fiel damit auf die `1` -- und `1` heisst laut `skill/SKILL.md` ausdruecklich
16
+ * *"Eingabe passt nicht"*. Antwortet die App aber mit `404`, war die Eingabe
17
+ * fehlerfrei: Die **App** kennt den Befehl noch nicht. Claude folgte der
18
+ * Betriebsanleitung und erklaerte dem Menschen, seine Eingabe stimme nicht --
19
+ * woraufhin der Firmenname, Schreibweise und `--firma` prueft und nichts davon
20
+ * hilft.
21
+ *
22
+ * 🔴 **Gemessen am 2026-08-18:** `/api/cli/firma` antwortete auf
23
+ * `kimal3-app.vercel.app` mit `404`, waehrend `/api/cli/projekt` mit `200`
24
+ * antwortete -- der Befehl war veroeffentlicht, die Route noch nicht
25
+ * ausgeliefert. Genau so sieht der Fall im Alltag aus: im Fenster zwischen
26
+ * `npm publish` und fertigem Deployment, bei einem Rueckbau, und bei jedem
27
+ * neuen Befehl.
28
+ *
29
+ * *Ein Fehlerfall darf nie denselben Namen tragen wie ein anderer, der
30
+ * Gegenteiliges verlangt.* Deshalb bekommt die `404` den `3`-Topf: dort steht
31
+ * bereits "nochmal versuchen kann helfen" -- und ein paar Minuten spaeter ist
32
+ * das Deployment durch.
33
+ */
34
+ export function schlussFehler(status, meldung) {
35
+ if (status === 404) {
36
+ return {
37
+ text: "Diese Version der App kennt den Befehl noch nicht. " +
38
+ "Hast du gerade aktualisiert, versuch es in ein paar Minuten noch einmal.",
39
+ wert: 3,
40
+ };
41
+ }
42
+ /*
43
+ 🔴 **`408` und `429` gehoeren AUCH in den 3-Topf** (Review-Runde 3,
44
+ 2026-08-18). Vorher fiel alles unter 500 auf die `1` -- und die heisst seit
45
+ Runde 2 ausdruecklich "Wiederholen hilft nicht".
46
+
47
+ Beide Zahlen sagen aber genau das Gegenteil: `429` schickt eine
48
+ Vercel-Firewall oder ein Supabase-Rate-Limit, `408` ist eine
49
+ Zeitueberschreitung. Bei beiden ist **Warten die Loesung**, und bei beiden
50
+ kommt der Rumpf oft als HTML -- dann steht dort nicht einmal eine Meldung,
51
+ sondern nur "Das hat nicht geklappt.".
52
+
53
+ *Genau die Form von B-7, nur mit einer anderen Zahl:* Der Mensch hat nichts
54
+ falsch gemacht, das Werkzeug rät ihm trotzdem, es sein zu lassen.
55
+ */
56
+ if (status === 408 || status === 429) {
57
+ return {
58
+ text: "Der Server ist gerade überlastet oder hat zu lange gebraucht. " +
59
+ "Versuch es in einem Moment noch einmal.",
60
+ wert: 3,
61
+ };
62
+ }
63
+ // Alles Uebrige: ein Aussetzer des Servers ist wiederholbar, alles andere
64
+ // ist die Absage, die der Server selbst formuliert hat.
65
+ return { text: meldung, wert: status >= 500 ? 3 : 1 };
66
+ }
@@ -0,0 +1,52 @@
1
+ /**
2
+ * Die Firmen-Auswahl, die eine Absage dem Menschen anbietet.
3
+ *
4
+ * 🔑 Eine reine Rechnung, damit `check:cli` sie ohne Server pruefen kann --
5
+ * dieselbe Ueberlegung wie bei `fehler.ts`.
6
+ */
7
+ /**
8
+ * Aus den Namen der eigenen Firmen die Zeilen bauen, die unter „Deine Firmen:"
9
+ * stehen.
10
+ *
11
+ * ⚠️ **Ein doppelter Name bekommt KEINEN Befehl** (QA-Befund B-5, 2026-08-18).
12
+ * Vorher wurde jeder Name stumpf zu einer kopierfertigen Zeile -- bei zwei
13
+ * gleichnamigen Firmen also **zweimal derselbe Befehl**. Wer ihn ausfuehrte,
14
+ * bekam dieselbe Absage, die erneut denselben Befehl anbot: eine Sackgasse
15
+ * ohne Ausweg.
16
+ *
17
+ * *Ein Vorschlag, der garantiert wieder scheitert, ist schlimmer als gar
18
+ * keiner -- er sieht aus wie ein Ausweg und kostet den Menschen den Versuch.*
19
+ *
20
+ * 🔑 **Nur der mehrdeutige Name verliert seinen Befehl, nicht die ganze
21
+ * Liste.** Wer zu „Nordwind", „Nordwind" und „Suedwind" gehoert, kann
22
+ * „Suedwind" weiterhin waehlen. Dieselbe Lehre steckt schon in der Route
23
+ * (Review-Befund Runde 2): *Eine Sperre, die mehr sperrt als das Mehrdeutige,
24
+ * verlangt vom Menschen etwas, das er nicht tun kann.*
25
+ *
26
+ * ⚠️ Was hier NICHT geloest wird: An die Daten einer doppelt benannten Firma
27
+ * kommt man ueber die CLI weiterhin nicht. Das braeuchte eine Auswahl ueber
28
+ * die ID statt ueber den Namen -- eine eigene Entscheidung, keine Fussnote.
29
+ * Diese Funktion sorgt nur dafuer, dass niemand mehr einen Ausweg vorgespielt
30
+ * bekommt, den es nicht gibt.
31
+ */
32
+ export function firmenZeilen(namen) {
33
+ const anzahl = (name) => namen.filter((n) => n === name).length;
34
+ const gesehen = new Set();
35
+ const zeilen = [];
36
+ for (const name of namen) {
37
+ if (gesehen.has(name))
38
+ continue;
39
+ gesehen.add(name);
40
+ const wieOft = anzahl(name);
41
+ if (wieOft === 1) {
42
+ // Fertiger Befehl statt blosser Liste: Der naechste Schritt ist dann
43
+ // Kopieren statt Abtippen -- und ein Name mit Leerzeichen braucht
44
+ // Anfuehrungszeichen, an die sonst niemand denkt.
45
+ zeilen.push(` kimal3 firma daten --firma "${name}"`);
46
+ }
47
+ else {
48
+ zeilen.push(` „${name}" — kommt ${wieOft}× vor und ist über den Namen nicht wählbar.`);
49
+ }
50
+ }
51
+ return zeilen;
52
+ }
package/dist/kimal3.js CHANGED
@@ -11,24 +11,21 @@
11
11
  * `kimal3 login`, bei 4 hilft genau das nicht. Wer beide auf dieselbe Zahl
12
12
  * legt, laesst Claude den falschen Rat geben.
13
13
  */
14
- import { leseAnmeldung } from "./anmeldung.js";
14
+ import { readFileSync } from "node:fs";
15
+ import { BASIS_URL, leseAnmeldung } from "./anmeldung.js";
15
16
  import { NichtAngemeldet } from "./api.js";
17
+ import { hilfeText } from "./befehle-katalog.js";
18
+ import { firmaDaten } from "./befehle/firma-daten.js";
16
19
  import { login } from "./befehle/login.js";
17
20
  import { logout } from "./befehle/logout.js";
18
21
  import { projektListe } from "./befehle/projekt-liste.js";
19
22
  import { projektNeu } from "./befehle/projekt-neu.js";
20
23
  import { skillInstall } from "./befehle/skill-install.js";
21
- const HILFE = `
22
- kimal3 — trag deine KI-Projekte dort ein, wo sie entstehen.
23
-
24
- kimal3 login Diesen Rechner mit deinem Konto verbinden
25
- kimal3 logout Verbindung wieder aufheben
26
- kimal3 projekt neu … Ein Projekt ins Erfolgsprofil eintragen
27
- kimal3 projekt liste Deine eingetragenen Projekte anzeigen
28
- kimal3 skill install Claude beibringen, dieses Werkzeug zu benutzen
29
-
30
- Mehr zu einem Befehl: kimal3 projekt neu --help
31
- `;
24
+ // 📌 Kein handgeschriebener Block mehr (2026-08-18): Die Liste wohnt in
25
+ // `befehle-katalog.ts` und wird von dort AUCH im Pro-Bereich der App angezeigt.
26
+ // Zwei Listen ueber dieselben Befehle driften — die eine bekaeme den naechsten
27
+ // Befehl, die andere nicht, und keiner der beiden Orte wuesste davon.
28
+ const HILFE = hilfeText();
32
29
  /**
33
30
  * Hilfe plus Zustand.
34
31
  *
@@ -42,6 +39,38 @@ const HILFE = `
42
39
  * ⚠️ Und KEIN Netzaufruf: Wer nur die Hilfe sehen will, soll nicht auf einen
43
40
  * Server warten. Ob die Anmeldung noch gilt, klaert der naechste echte Befehl.
44
41
  */
42
+ /**
43
+ * Welche Version bin ich — und mit wem rede ich?
44
+ *
45
+ * ⚠️ **Die Zahl wird GELESEN, nicht abgetippt** (QA-Befund B-8, 2026-08-18).
46
+ * Stuende sie hier als Text, sagte die CLI nach dem naechsten `npm version`
47
+ * die alte Nummer -- dieselbe Zahl an zwei Tueren mit zwei Werten, genau das
48
+ * Muster, das in diesem Projekt schon zweimal zugeschlagen hat.
49
+ *
50
+ * 🔑 **Warum die Gegenstelle mit dazugehoert:** Der Fall, fuer den man diese
51
+ * Auskunft braucht, ist ein **Versatz** zwischen CLI und App (B-7). Dann
52
+ * helfen erst beide Angaben zusammen: welche CLI laeuft -- und gegen welche
53
+ * App sie spricht. Eine Version allein beantwortet die Frage nur halb.
54
+ *
55
+ * 📌 `../package.json` stimmt aus `dist/` **und** aus `src/`: Beide liegen
56
+ * eine Ebene unter `cli/`.
57
+ */
58
+ function versionsZeilen() {
59
+ let version = "unbekannt";
60
+ try {
61
+ const roh = JSON.parse(readFileSync(new URL("../package.json", import.meta.url), "utf8"));
62
+ if (roh && typeof roh === "object" && "version" in roh) {
63
+ const v = roh.version;
64
+ if (typeof v === "string")
65
+ version = v;
66
+ }
67
+ }
68
+ catch {
69
+ // Kein Absturz, wenn die Datei fehlt: "unbekannt" ist eine ehrliche
70
+ // Antwort, ein Stapelabzug waere keine.
71
+ }
72
+ return ` kimal3 ${version}\n spricht mit ${BASIS_URL}`;
73
+ }
45
74
  function zustandsZeile() {
46
75
  const a = leseAnmeldung();
47
76
  return a
@@ -56,6 +85,11 @@ async function haupt() {
56
85
  console.log(zustandsZeile() + "\n");
57
86
  return 0;
58
87
  }
88
+ if (wort === "--version" || wort === "-v" || wort === "version") {
89
+ console.log(versionsZeilen());
90
+ console.log(zustandsZeile() + "\n");
91
+ return 0;
92
+ }
59
93
  if (wort === "login")
60
94
  return login();
61
95
  if (wort === "logout")
@@ -64,6 +98,11 @@ async function haupt() {
64
98
  return projektNeu(argv.slice(2));
65
99
  if (wort === "projekt liste")
66
100
  return projektListe();
101
+ // ⚠️ Praefix-Vergleich wie bei `projekt neu` und NICHT `wort === "firma daten"`:
102
+ // `wort` enthaelt die Optionen mit, `firma daten --firma X` fiele bei einem
103
+ // Gleichheits-Vergleich durch und meldete "Den Befehl kenne ich nicht".
104
+ if (argv[0] === "firma" && argv[1] === "daten")
105
+ return firmaDaten(argv.slice(2));
67
106
  if (wort === "skill install")
68
107
  return skillInstall();
69
108
  console.error(`Den Befehl „${wort}" kenne ich nicht.\n${HILFE}`);
@@ -0,0 +1,253 @@
1
+ /**
2
+ * Die Zeilen, die `kimal3 projekt liste` ausgibt — und das Lesen der Antwort.
3
+ *
4
+ * 🔑 Eine reine Rechnung, damit `check:cli` sie ohne Server pruefen kann --
5
+ * dieselbe Ueberlegung wie bei `firmen-liste.ts` und `fehler.ts`.
6
+ *
7
+ * ⚠️ **Warum hier eigene Formatierer stehen, obwohl die App `euroBetrag` und
8
+ * `datumLang` schon hat:** Die CLI ist ein eigenstaendiges npm-Paket mit
9
+ * `rootDir: src` -- sie kann aus `../../src` nichts importieren. Das ist kein
10
+ * Versehen, sondern der Preis dafuer, dass das Paket ohne die App baut.
11
+ *
12
+ * 🔴 Und ein Unterschied ist ABSICHT, kein Drift: `euroBetrag` in der App
13
+ * rundet auf ganze Euro, weil eine Monats-Selbstauskunft keine Cent-Genauigkeit
14
+ * hat. Hier werden Cents ANGEZEIGT, sobald welche dastehen. Grund ist die
15
+ * Hausregel "erst runden, DANN entscheiden": Wer 0,10 € eingetragen hat und im
16
+ * Terminal "0 €" liest, haelt seine Eingabe fuer verloren. Das Terminal ist der
17
+ * Ort der Nachschau, die oeffentliche Karte der Ort der Aussage.
18
+ */
19
+ /** Ein Zahlenfeld: `null` heisst "nicht angegeben", `false` heisst "unlesbar". */
20
+ function zahlFeld(o, name) {
21
+ const w = o[name];
22
+ if (w === undefined || w === null)
23
+ return null;
24
+ if (typeof w === "number" && Number.isFinite(w))
25
+ return w;
26
+ // 🔴 Eine Zahl, die als Zeichenkette ankommt, wird NICHT still zu "nicht
27
+ // angegeben". Genau das waere die Faktor-1000-Falle in ihrer leisen Form:
28
+ // Der Mensch sieht ein Projekt ohne Zahlen und glaubt, seine Eingabe sei nie
29
+ // angekommen. *Stilles Weglassen ist der schlechteste der drei Wege.*
30
+ return false;
31
+ }
32
+ /**
33
+ * Die Projekte aus der Antwort lesen.
34
+ *
35
+ * ⚠️ **`null` und `[]` sind zwei verschiedene Wirklichkeiten.** `[]` heisst
36
+ * "du hast noch keine Projekte" -- eine gueltige Antwort auf eine gueltige
37
+ * Frage. `null` heisst "diese Antwort war nicht lesbar". Faellt beides auf
38
+ * denselben Rueckgabewert, feiert der Befehl eine Stoerung als Erfolg und
39
+ * raet zum Eintragen von etwas, das laengst dasteht.
40
+ *
41
+ * 🔑 Auch eine ALTE App-Fassung landet hier auf `null`: Sie antwortet mit
42
+ * `{ "titel": [...] }` und ohne `projekte`. **Das ist aber NICHT das Ende** --
43
+ * `projekt-liste.ts` fragt danach `titelAusAlterAntwort` und zeigt die Liste
44
+ * ohne Zahlen, mit Rueckgabewert `0`. Diese Funktion sagt also nur "ich kann
45
+ * es nicht lesen", nicht "es ist kaputt".
46
+ *
47
+ * ⚠️ Hier stand bis zur Review-Runde 3 (2026-08-18) das Gegenteil: "Der Befehl
48
+ * meldet dann eine unlesbare Antwort und den Rueckgabewert `3`." Das war vor
49
+ * L-7 richtig und danach falsch -- und der widersprechende Text stand vierzig
50
+ * Zeilen ueber der Funktion, die es besser weiss.
51
+ * *In diesem Projekt sind Kommentare tragend* (siehe
52
+ * `docs/regeln/messen-und-pruefen.md`, Abschnitt 11, in genau diesem Zweig
53
+ * hinzugefuegt): Der naechste Leser haette geglaubt, den Rueckfallweg gebe es
54
+ * nicht -- und ihn vielleicht ein zweites Mal gebaut.
55
+ */
56
+ export function projekteAusAntwort(koerper) {
57
+ if (!koerper || typeof koerper !== "object")
58
+ return null;
59
+ const roh = koerper.projekte;
60
+ if (!Array.isArray(roh))
61
+ return null;
62
+ const projekte = [];
63
+ for (const eintrag of roh) {
64
+ if (!eintrag || typeof eintrag !== "object")
65
+ return null;
66
+ const o = eintrag;
67
+ if (typeof o.titel !== "string")
68
+ return null;
69
+ if (typeof o.laeuft !== "boolean")
70
+ return null;
71
+ const liveSeit = o.live_seit;
72
+ if (liveSeit !== null && liveSeit !== undefined && typeof liveSeit !== "string")
73
+ return null;
74
+ const stunden = zahlFeld(o, "zeitersparnis_std_wo");
75
+ const ergebnis = zahlFeld(o, "ergebnis_monat_eur");
76
+ const umsatz = zahlFeld(o, "umsatz_monat_eur");
77
+ if (stunden === false || ergebnis === false || umsatz === false)
78
+ return null;
79
+ projekte.push({
80
+ titel: o.titel,
81
+ live_seit: typeof liveSeit === "string" ? liveSeit : null,
82
+ laeuft: o.laeuft,
83
+ zeitersparnis_std_wo: stunden,
84
+ ergebnis_monat_eur: ergebnis,
85
+ umsatz_monat_eur: umsatz,
86
+ });
87
+ }
88
+ return projekte;
89
+ }
90
+ const MONATE = [
91
+ "Januar", "Februar", "März", "April", "Mai", "Juni",
92
+ "Juli", "August", "September", "Oktober", "November", "Dezember",
93
+ ];
94
+ /**
95
+ * "2026-07-01" → "1. Juli 2026". Bewusst aus der Zeichenkette gerechnet statt
96
+ * ueber `new Date()`: Ein ISO-Datum ohne Uhrzeit gilt als UTC-Mitternacht und
97
+ * kippt sonst je nach Zeitzone einen Tag zurueck -- und ein Terminal steht
98
+ * irgendwo auf der Welt.
99
+ *
100
+ * ⚠️ Erwartet ein REINES Datum (`JJJJ-MM-TT`), wie es die `date`-Spalte
101
+ * `live_seit` liefert. Passt es nicht, steht lieber das rohe Datum da als ein
102
+ * kaputter Satz ("NaN. August 2026" -- der Fehler vom 2026-08-12).
103
+ */
104
+ function datumLang(isoDatum) {
105
+ const [jahr, monat, tag] = isoDatum.split("-");
106
+ const monatsname = MONATE[Number(monat) - 1];
107
+ if (!monatsname || !jahr || !Number.isFinite(Number(tag)))
108
+ return isoDatum;
109
+ return `${Number(tag)}. ${monatsname} ${jahr}`;
110
+ }
111
+ /**
112
+ * "1200" → "1.200 €", "0.1" → "0,10 €".
113
+ *
114
+ * Cents erscheinen nur, wenn welche da sind: "1.200,00 €" taeuscht eine
115
+ * Genauigkeit vor, die niemand behauptet hat -- "0 €" bei 0,10 € verschweigt
116
+ * dagegen eine, die dasteht.
117
+ */
118
+ function euro(wert) {
119
+ const hatCent = Math.round(wert * 100) % 100 !== 0;
120
+ const stellen = hatCent ? 2 : 0;
121
+ return `${new Intl.NumberFormat("de-DE", {
122
+ minimumFractionDigits: stellen,
123
+ maximumFractionDigits: stellen,
124
+ }).format(wert)} €`;
125
+ }
126
+ /** "6" → "6", "6.5" → "6,5". Die Spalte ist numeric(6,2). */
127
+ function stunden(wert) {
128
+ return new Intl.NumberFormat("de-DE", { maximumFractionDigits: 2 }).format(wert);
129
+ }
130
+ /** Was oben in der zweiten Zeile steht: laeuft, geplant oder noch nicht live. */
131
+ function zustand(p) {
132
+ if (p.live_seit === null)
133
+ return "noch nicht live";
134
+ return p.laeuft
135
+ ? `live seit ${datumLang(p.live_seit)}`
136
+ : `geplant ab ${datumLang(p.live_seit)}`;
137
+ }
138
+ /**
139
+ * Zwei Zeilen je Projekt: Titel, darunter die Angaben.
140
+ *
141
+ * 🔑 **Was fehlt, wird als fehlend gezeigt** -- nie durch 0 ersetzt. Ein
142
+ * Projekt ohne Zahlen ist der Normalfall: Nicht jede Automation hat einen
143
+ * Eurobetrag, und eine erfundene 0 stuende spaeter oeffentlich auf der Karte
144
+ * eines Menschen. Eine ausdrueckliche 0 ist dagegen eine ANGABE und wird
145
+ * gezeigt -- deshalb wird auf `null` geprueft, nicht auf Wahrheitswert.
146
+ */
147
+ export function projektZeilen(projekte) {
148
+ const zeilen = [];
149
+ projekte.forEach((p, i) => {
150
+ if (i > 0)
151
+ zeilen.push("");
152
+ zeilen.push(` ${i + 1}. ${p.titel}`);
153
+ const teile = [zustand(p)];
154
+ if (p.zeitersparnis_std_wo !== null) {
155
+ teile.push(`${stunden(p.zeitersparnis_std_wo)} Std./Woche`);
156
+ }
157
+ if (p.ergebnis_monat_eur !== null) {
158
+ teile.push(`${euro(p.ergebnis_monat_eur)} Ergebnis/Monat`);
159
+ }
160
+ if (p.umsatz_monat_eur !== null) {
161
+ teile.push(`${euro(p.umsatz_monat_eur)} Umsatz/Monat`);
162
+ }
163
+ // Nur der Zustand steht da? Dann sagen wir das ausdruecklich, statt eine
164
+ // halbe Zeile hinzustellen, die aussieht, als sei etwas abgeschnitten.
165
+ if (teile.length === 1)
166
+ teile.push("keine Zahlen eingetragen");
167
+ zeilen.push(` ${teile.join(" · ")}`);
168
+ });
169
+ return zeilen;
170
+ }
171
+ /**
172
+ * Liest die ALTE Antwortform (`titel: string[]`, ohne `projekte`).
173
+ *
174
+ * 🔑 **Wofuer das da ist** (Befund L-7, 2026-08-18): Die CLI kann neuer sein
175
+ * als die App -- in jedem Deployment-Fenster zwischen `npm publish` und dem
176
+ * fertigen Vercel-Build. Dann antwortet der Server voellig korrekt, nur in der
177
+ * alten Form. Ohne diese Funktion faellt das in denselben Topf wie eine
178
+ * kaputte Antwort, und die CLI raet zum Wiederholen -- was hier NIE hilft.
179
+ *
180
+ * Gibt `null` zurueck, wenn es auch die alte Form nicht ist. Dann ist die
181
+ * Antwort wirklich unlesbar, und der Rat zum Wiederholen stimmt.
182
+ */
183
+ export function titelAusAlterAntwort(koerper) {
184
+ if (!koerper || typeof koerper !== "object")
185
+ return null;
186
+ const o = koerper;
187
+ // ⚠️ Nur wenn `projekte` FEHLT. Ist es da und war trotzdem unlesbar, ist die
188
+ // Antwort defekt -- dann darf sie nicht als "alte Version" durchgehen.
189
+ if ("projekte" in o)
190
+ return null;
191
+ // Ab Runde 6 stuetzt sich das auf `titelFeldLesen` -- dieselbe Regel darf
192
+ // nicht an zwei Orten stehen.
193
+ return titelFeldLesen(koerper);
194
+ }
195
+ /**
196
+ * Wie viele Projekte ein Erfolgsprofil hoechstens traegt.
197
+ *
198
+ * ⚠️ **Das ist eine KOPIE von `MAX_PROJEKTE` aus
199
+ * `src/app/(erfolgsprofil)/typen.ts`** — die CLI ist ein eigenstaendiges
200
+ * npm-Paket mit `rootDir: src` und kann aus der App nichts importieren.
201
+ *
202
+ * 🔴 **Deshalb misst `npm run check:cli`, dass die beiden Zahlen gleich sind**
203
+ * (Review-Befund, 2026-08-18). Vorher stand die `12` an zwei Stellen in
204
+ * `projekt-liste.ts` fest im Text, und nichts pruefte sie gegen die App: Wer
205
+ * die Grenze dort anhebt, laesst die CLI still weiter "von 12" behaupten.
206
+ * *Dieselbe Zahl an drei Tueren — das Muster, das dieses Projekt schon zweimal
207
+ * eingefangen hat.*
208
+ */
209
+ export const MAX_PROJEKTE_ANZEIGE = 12;
210
+ /**
211
+ * Sagt, ob die Antwort ueberhaupt ein Feld `projekte` mitbringt.
212
+ *
213
+ * 🔑 **Wofuer das da ist** (Review-Runde 4, 2026-08-18): Es schliesst die
214
+ * Vertraeglichkeits-Matrix in der letzten offenen Richtung. Bisher gab es
215
+ * zwei Faelle:
216
+ * - die App ist AELTER (kein `projekte`, nur `titel`) -> `titelAusAlterAntwort`
217
+ * - die Antwort ist Muell (WLAN-Portal, HTML) -> "nicht lesbar", noch mal
218
+ *
219
+ * Der dritte fehlte: **die App ist NEUER als die CLI.** Dann steht `projekte`
220
+ * da, aber ein Eintrag hat eine Form, die diese Fassung nicht kennt --
221
+ * `projekteAusAntwort` steigt beim ersten aus und gibt `null`. Elf gute
222
+ * Projekte verschwinden mit dem zwoelften, und der Mensch bekam den Rat,
223
+ * es gleich noch einmal zu versuchen. **Der Zustand ist aber stabil** --
224
+ * Wiederholen aendert nichts, bis das Werkzeug aktualisiert wird.
225
+ *
226
+ * *Genau die Form von B-5, B-7 und L-7 -- in der einen Richtung, die dieser
227
+ * Zweig noch nicht abgedeckt hatte.*
228
+ */
229
+ export function antwortHatProjekteFeld(koerper) {
230
+ return !!koerper && typeof koerper === "object" && "projekte" in koerper;
231
+ }
232
+ /**
233
+ * Liest das Feld `titel` — **ohne** zu fragen, ob `projekte` daneben steht.
234
+ *
235
+ * 🔑 **Wofuer** (Review-Runde 6, 2026-08-18): Die neue Route liefert `titel`
236
+ * und `projekte` IMMER gemeinsam. Aendert eine spaetere App-Fassung die Form
237
+ * eines Eintrags, steigt `projekteAusAntwort` beim ersten aus -- die zwoelf
238
+ * Titel stehen aber unveraendert daneben. Sie dann nicht zu zeigen und
239
+ * stattdessen "npm install -g kimal3" zu sagen, wirft Daten weg, die man hat.
240
+ *
241
+ * *Wer dem Menschen etwas geben kann, gibt es ihm -- und sagt dazu, was
242
+ * fehlt.*
243
+ */
244
+ export function titelFeldLesen(koerper) {
245
+ if (!koerper || typeof koerper !== "object")
246
+ return null;
247
+ const roh = koerper.titel;
248
+ if (!Array.isArray(roh))
249
+ return null;
250
+ if (!roh.every((t) => typeof t === "string"))
251
+ return null;
252
+ return roh;
253
+ }
package/package.json CHANGED
@@ -1,16 +1,27 @@
1
- {
2
- "name": "kimal3",
3
- "version": "0.1.1",
4
- "description": "Trag deine KI-Projekte dort ein, wo sie entstehen: im Terminal.",
5
- "type": "module",
6
- "bin": { "kimal3": "dist/kimal3.js" },
7
- "files": ["dist", "skill", "README.md"],
8
- "engines": { "node": ">=20" },
9
- "scripts": {
10
- "build": "tsc -p tsconfig.json",
11
- "prepublishOnly": "npm run build"
12
- },
13
- "keywords": ["kimal3", "cli"],
14
- "homepage": "https://kimal3-app.vercel.app/pro",
15
- "license": "SEE LICENSE IN README.md"
16
- }
1
+ {
2
+ "name": "kimal3",
3
+ "version": "0.1.2",
4
+ "description": "Trag deine KI-Projekte dort ein, wo sie entstehen: im Terminal.",
5
+ "type": "module",
6
+ "bin": {
7
+ "kimal3": "dist/kimal3.js"
8
+ },
9
+ "files": [
10
+ "dist",
11
+ "skill",
12
+ "README.md"
13
+ ],
14
+ "engines": {
15
+ "node": ">=20"
16
+ },
17
+ "scripts": {
18
+ "build": "tsc -p tsconfig.json",
19
+ "prepublishOnly": "npm run build"
20
+ },
21
+ "keywords": [
22
+ "kimal3",
23
+ "cli"
24
+ ],
25
+ "homepage": "https://kimal3-app.vercel.app/pro",
26
+ "license": "SEE LICENSE IN README.md"
27
+ }