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.
- package/dist/api.js +6 -9
- package/dist/argumente.js +35 -7
- package/dist/befehle/firma-daten.js +113 -0
- package/dist/befehle/projekt-liste.js +90 -9
- package/dist/befehle/projekt-neu.js +8 -2
- package/dist/befehle/skill-install.js +271 -6
- package/dist/befehle-katalog.js +112 -0
- package/dist/fehler.js +66 -0
- package/dist/firmen-liste.js +52 -0
- package/dist/kimal3.js +51 -12
- package/dist/projekt-zeilen.js +253 -0
- package/package.json +27 -16
- package/skill/SKILL.md +125 -43
- package/skill/commands/k3.md +44 -0
- package/skill/commands/k3firma.md +57 -0
- package/skill/commands/k3liste.md +26 -0
- package/skill/commands/k3projekt.md +44 -0
|
@@ -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 {
|
|
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
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
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.
|
|
4
|
-
"description": "Trag deine KI-Projekte dort ein, wo sie entstehen: im Terminal.",
|
|
5
|
-
"type": "module",
|
|
6
|
-
"bin": {
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
"
|
|
10
|
-
"
|
|
11
|
-
"
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
"
|
|
15
|
-
|
|
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
|
+
}
|