kimal3 0.1.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.
package/README.md ADDED
@@ -0,0 +1,74 @@
1
+ # kimal3
2
+
3
+ **Trag deine KI-Projekte dort ein, wo sie entstehen: im Terminal.**
4
+
5
+ Wer mit Claude Code arbeitet, baut echte Dinge — und dokumentiert sie hinterher nicht,
6
+ weil das ein zweiter Arbeitsschritt in einem anderen Fenster wäre. `kimal3` schließt
7
+ diese Lücke: Am Ende einer Sitzung trägt Claude das fertige Projekt selbst in dein
8
+ KImal3-Erfolgsprofil ein. Du gibst es dort nur noch frei.
9
+
10
+ > **Voraussetzung: ein KImal3-Konto mit Pro.**
11
+ > Ohne Pro meldet sich dieses Werkzeug ab, ohne etwas zu tun.
12
+ > Was Pro enthält und wie du es bekommst: **https://kimal3-app.vercel.app/pro**
13
+
14
+ ## Loslegen
15
+
16
+ Zwei Befehle, einmal pro Rechner:
17
+
18
+ ```bash
19
+ npx kimal3 login # diesen Rechner mit deinem Konto verbinden
20
+ npx kimal3 skill install # Claude beibringen, das Werkzeug zu benutzen
21
+ ```
22
+
23
+ `login` zeigt dir einen achtstelligen Code und öffnet den Browser. Dort bestätigst du
24
+ ihn in deinem eingeloggten Konto — so wie bei `gh auth login`. **Es wird kein Passwort
25
+ ins Terminal getippt.**
26
+
27
+ ## Befehle
28
+
29
+ ```
30
+ kimal3 Zeigt, ob dieser Rechner angemeldet ist
31
+ kimal3 login Diesen Rechner mit deinem Konto verbinden
32
+ kimal3 logout Verbindung wieder aufheben
33
+ kimal3 projekt neu … Ein Projekt ins Erfolgsprofil eintragen
34
+ kimal3 projekt liste Deine eingetragenen Projekte anzeigen
35
+ kimal3 skill install Claude beibringen, dieses Werkzeug zu benutzen
36
+ ```
37
+
38
+ Mehr zu einem Befehl: `kimal3 projekt neu --help`
39
+
40
+ ## Was eingetragen wird
41
+
42
+ Nur, was du eingibst — Titel, Beschreibung, optional ein Link. **Dein Code wird nicht
43
+ gelesen, nicht hochgeladen und nicht analysiert.** Das Werkzeug schreibt ausschließlich
44
+ über die KImal3-App und hat keinen eigenen Datenbankzugang.
45
+
46
+ Eingetragene Projekte sind zunächst **nicht öffentlich**. Sie erscheinen in deinem
47
+ Erfolgsprofil, und du entscheidest dort, was sichtbar wird.
48
+
49
+ ## Rückgabewerte
50
+
51
+ Für Skripte und für Claude selbst:
52
+
53
+ | Wert | Bedeutung | Hilft |
54
+ |---|---|---|
55
+ | `0` | hat geklappt | — |
56
+ | `1` | Eingabe unvollständig oder nichts gefunden | Eingabe prüfen |
57
+ | `2` | nicht angemeldet | `kimal3 login` |
58
+ | `3` | gerade nicht erreichbar | später noch einmal |
59
+ | `4` | kein Pro-Konto | erneutes Versuchen hilft **nicht** |
60
+
61
+ Der Unterschied zwischen `3` und `4` ist Absicht: Bei `3` lohnt ein zweiter Versuch,
62
+ bei `4` nie.
63
+
64
+ ## Voraussetzungen
65
+
66
+ Node.js 20 oder neuer. Sonst nichts — das Werkzeug bringt **keine fremden Bibliotheken**
67
+ mit.
68
+
69
+ ## Lizenz
70
+
71
+ Proprietär. Die Nutzung ist Inhabern eines gültigen KImal3-Pro-Kontos gestattet;
72
+ Weitergabe, Veränderung und Weiterverwendung des Codes sind nicht erlaubt.
73
+
74
+ © KImal3 · https://kimal3.de
@@ -0,0 +1,55 @@
1
+ import { chmodSync, mkdirSync, readFileSync, rmSync, writeFileSync } from "node:fs";
2
+ import { homedir } from "node:os";
3
+ import { join } from "node:path";
4
+ /**
5
+ * Wo die App wohnt. Ueberschreibbar fuers lokale Entwickeln:
6
+ * KIMAL3_URL=http://localhost:3000 npx kimal3 projekt liste
7
+ */
8
+ export const BASIS_URL = process.env.KIMAL3_URL ?? "https://kimal3-app.vercel.app";
9
+ const ORDNER = join(homedir(), ".kimal3");
10
+ const DATEI = join(ORDNER, "anmeldung.json");
11
+ export function leseAnmeldung() {
12
+ try {
13
+ const roh = JSON.parse(readFileSync(DATEI, "utf8"));
14
+ if (roh && typeof roh === "object" && "schluessel" in roh && "email" in roh) {
15
+ const a = roh;
16
+ if (typeof a.schluessel === "string" && typeof a.email === "string") {
17
+ return { schluessel: a.schluessel, email: a.email };
18
+ }
19
+ }
20
+ return null;
21
+ }
22
+ catch {
23
+ return null;
24
+ }
25
+ }
26
+ /**
27
+ * ⚠️ `chmod 0600` wirkt auf Mac/Linux. Auf Windows ist die Datei durch das
28
+ * Nutzerprofil geschuetzt, nicht durch diese Zeile — dort ist sie wirkungslos,
29
+ * aber auch nicht schaedlich. Beides gehoert in den Handtest.
30
+ *
31
+ * 🔑 Die Rechte stehen im `mode` der ERZEUGUNG, nicht in einem `chmod`
32
+ * danach (Review-Befund 2026-08-13): `writeFileSync` legt die Datei sonst mit
33
+ * 0644 an, und bis der `chmod` greift, ist der Ausweis auf einem
34
+ * Mehrbenutzer-Linux fuer alle lesbar. Ein Fenster von Millisekunden ist ein
35
+ * Fenster. Der `chmod` bleibt trotzdem stehen — er greift auch dann, wenn die
36
+ * Datei schon existierte und der `mode` deshalb ignoriert wird.
37
+ */
38
+ export function schreibeAnmeldung(a) {
39
+ mkdirSync(ORDNER, { recursive: true, mode: 0o700 });
40
+ writeFileSync(DATEI, JSON.stringify(a, null, 2), { encoding: "utf8", mode: 0o600 });
41
+ try {
42
+ chmodSync(DATEI, 0o600);
43
+ }
44
+ catch { /* Windows */ }
45
+ }
46
+ export function loescheAnmeldung() {
47
+ try {
48
+ rmSync(DATEI);
49
+ }
50
+ catch { /* war schon weg */ }
51
+ }
52
+ /** Nur fuer Meldungen an den Menschen — damit er weiss, wo die Datei liegt. */
53
+ export function anmeldungsPfad() {
54
+ return DATEI;
55
+ }
package/dist/api.js ADDED
@@ -0,0 +1,51 @@
1
+ import { BASIS_URL, leseAnmeldung } from "./anmeldung.js";
2
+ export class NichtAngemeldet extends Error {
3
+ constructor() { super("nicht angemeldet"); }
4
+ }
5
+ /**
6
+ * Ein Aufruf gegen die App. `mitAusweis` haengt den Schluessel an.
7
+ *
8
+ * Netzfehler werden NICHT zu einem Status 0 verbogen — sie werfen, und der
9
+ * Verteiler in kimal3.ts macht daraus Rueckgabewert 3. Ein Netzfehler ist
10
+ * etwas anderes als eine Absage vom Server, und wer beides gleich behandelt,
11
+ * schickt Menschen zur falschen Fehlersuche.
12
+ */
13
+ export async function ruf(pfad, optionen = {}) {
14
+ const kopf = {};
15
+ if (optionen.mitAusweis) {
16
+ const a = leseAnmeldung();
17
+ if (!a)
18
+ throw new NichtAngemeldet();
19
+ kopf.Authorization = `Bearer ${a.schluessel}`;
20
+ }
21
+ const methode = optionen.methode ?? "POST";
22
+ // Content-Type nur, wenn wirklich etwas im Rumpf steht. Ein GET mit
23
+ // "Content-Type: application/json" und ohne Rumpf ist eine Luege ueber die
24
+ // eigene Anfrage — manche Server nehmen das uebel.
25
+ if (methode === "POST")
26
+ kopf["Content-Type"] = "application/json";
27
+ const antwort = await fetch(`${BASIS_URL}${pfad}`, {
28
+ method: methode,
29
+ headers: kopf,
30
+ body: methode === "POST" ? JSON.stringify(optionen.koerper ?? {}) : undefined,
31
+ });
32
+ return { status: antwort.status, koerper: await antwort.json().catch(() => null) };
33
+ }
34
+ /** Holt ein Textfeld aus einer Antwort, ohne `any`. */
35
+ export function feld(koerper, name) {
36
+ if (koerper && typeof koerper === "object" && name in koerper) {
37
+ const w = koerper[name];
38
+ if (typeof w === "string")
39
+ return w;
40
+ }
41
+ return null;
42
+ }
43
+ /** Dasselbe fuer eine Liste von Texten (fuer `projekt liste`). */
44
+ export function textListe(koerper, name) {
45
+ if (koerper && typeof koerper === "object" && name in koerper) {
46
+ const w = koerper[name];
47
+ if (Array.isArray(w) && w.every((x) => typeof x === "string"))
48
+ return w;
49
+ }
50
+ return null;
51
+ }
@@ -0,0 +1,39 @@
1
+ /**
2
+ * Die Befehlszeile zerlegen — von Hand, weil `cli/` keine fremden
3
+ * Bibliotheken hat (Entscheidung aus Abschnitt A der Spec).
4
+ *
5
+ * Bewusst streng: Eine unbekannte Option ist ein FEHLER, kein stilles
6
+ * Uebergehen. Wer `--titl` tippt, soll es erfahren, statt ein Projekt ohne
7
+ * Titel anzulegen.
8
+ */
9
+ export function zerlege(argv, erlaubt) {
10
+ const befehl = [];
11
+ const optionen = {};
12
+ for (let i = 0; i < argv.length; i++) {
13
+ const teil = argv[i];
14
+ if (!teil.startsWith("--")) {
15
+ if (Object.keys(optionen).length > 0) {
16
+ return { ok: false, meldung: `Nach Optionen kommt kein Befehlswort mehr: „${teil}".` };
17
+ }
18
+ befehl.push(teil);
19
+ continue;
20
+ }
21
+ const gleich = teil.indexOf("=");
22
+ const name = gleich === -1 ? teil.slice(2) : teil.slice(2, gleich);
23
+ if (!erlaubt.includes(name)) {
24
+ return { ok: false, meldung: `Die Option „--${name}" kenne ich nicht.` };
25
+ }
26
+ if (gleich !== -1) {
27
+ optionen[name] = teil.slice(gleich + 1);
28
+ continue;
29
+ }
30
+ const wert = argv[i + 1];
31
+ // Eine Zahl mit Minus ist ein Wert; "--irgendwas" ist die naechste Option.
32
+ if (wert === undefined || wert.startsWith("--")) {
33
+ return { ok: false, meldung: `Der Option „--${name}" fehlt ihr Wert.` };
34
+ }
35
+ optionen[name] = wert;
36
+ i++;
37
+ }
38
+ return { ok: true, werte: { befehl, optionen } };
39
+ }
@@ -0,0 +1,66 @@
1
+ import { hostname } from "node:os";
2
+ import { feld, ruf } from "../api.js";
3
+ import { BASIS_URL, schreibeAnmeldung } from "../anmeldung.js";
4
+ const warte = (ms) => new Promise((f) => setTimeout(f, ms));
5
+ export async function login() {
6
+ const start = await ruf("/api/cli/login/start", { koerper: { bezeichnung: hostname() } });
7
+ const nutzerCode = feld(start.koerper, "nutzer_code");
8
+ const geraeteCode = feld(start.koerper, "geraete_code");
9
+ if (start.status !== 200 || !nutzerCode || !geraeteCode) {
10
+ console.error("kimal3 ist gerade nicht erreichbar.");
11
+ return 3;
12
+ }
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)");
16
+ const endeUm = Date.now() + 10 * 60 * 1000;
17
+ while (Date.now() < endeUm) {
18
+ await warte(2000);
19
+ // ⚠️ Diese Schleife laeuft per Definition zehn Minuten ueber ein Netz.
20
+ // Ein einzelner Aussetzer — Vercel-Kaltstart, kurzes 502, WLAN-Huepfer —
21
+ // darf die ganze Anmeldung nicht wegwerfen, waehrend der Mensch im
22
+ // Browser vielleicht gerade bestaetigt. Deshalb: weiterfragen statt
23
+ // abbrechen. Beendet wird nur bei einer EINDEUTIGEN Antwort.
24
+ // (Review-Befund 2026-08-13.)
25
+ let a;
26
+ try {
27
+ a = await ruf("/api/cli/login/abholen", { koerper: { geraete_code: geraeteCode } });
28
+ }
29
+ catch {
30
+ continue;
31
+ }
32
+ if (a.status === 202)
33
+ continue;
34
+ if (a.status >= 500)
35
+ continue;
36
+ if (a.status === 410) {
37
+ console.error("\nDer Anmeldecode ist abgelaufen. Bitte `kimal3 login` noch einmal.");
38
+ return 2;
39
+ }
40
+ // 🔴 Dieser Zweig MUSS hier oben stehen. Ohne ihn faellt eine 402 durch bis
41
+ // ans Ende und meldet "Das hat nicht geklappt." mit Rueckgabewert 3 --
42
+ // Genau die nichtssagende Meldung, die diese Schranke vermeiden soll: Der
43
+ // Mensch erfaehrt nicht, dass ihm ein Konto fehlt, sondern glaubt an einen
44
+ // Serverfehler.
45
+ //
46
+ // 🔑 Und sie kommt hier nur bei einer ECHTEN Absage an: Liess sich der Plan
47
+ // im Browser gar nicht nachsehen, bleibt der Zettel auf "wartet" — diese
48
+ // Schleife fragt also einfach weiter, statt aufzugeben. Genau dafuer ist
49
+ // sie da. (Review-Befund 2026-08-14, s. `src/app/cli/actions.ts`.)
50
+ if (a.status === 402) {
51
+ console.error(`\n${feld(a.koerper, "meldung") ?? "Dafür brauchst du ein Pro-Konto."}`);
52
+ return 4;
53
+ }
54
+ const schluessel = feld(a.koerper, "schluessel");
55
+ const email = feld(a.koerper, "email");
56
+ if (a.status === 200 && schluessel && email) {
57
+ schreibeAnmeldung({ schluessel, email });
58
+ console.log(`\n✓ Angemeldet als ${email}.`);
59
+ return 0;
60
+ }
61
+ console.error("\nDas hat nicht geklappt.");
62
+ return 3;
63
+ }
64
+ console.error("\nDer Anmeldecode ist abgelaufen. Bitte `kimal3 login` noch einmal.");
65
+ return 2;
66
+ }
@@ -0,0 +1,33 @@
1
+ import { leseAnmeldung, loescheAnmeldung } from "../anmeldung.js";
2
+ import { ruf } from "../api.js";
3
+ /**
4
+ * Abmelden — erst beim Server widerrufen, DANN die Datei loeschen.
5
+ *
6
+ * ⚠️ Die Reihenfolge ist der ganze Punkt: Loescht man zuerst, kennt niemand
7
+ * mehr den Schluessel, den man gerade widerrufen wollte. Er bliebe auf dem
8
+ * Server gueltig, und nichts wuerde davon berichten.
9
+ */
10
+ export async function logout() {
11
+ const a = leseAnmeldung();
12
+ if (!a) {
13
+ // Kein Fehler: Der Zielzustand ist bereits erreicht. Deshalb 0.
14
+ console.log("Auf diesem Rechner war keine Anmeldung.");
15
+ return 0;
16
+ }
17
+ try {
18
+ const antwort = await ruf("/api/cli/logout", { mitAusweis: true });
19
+ if (antwort.status !== 200 && antwort.status !== 401) {
20
+ console.error("kimal3 ist gerade nicht erreichbar. Es wurde nichts geändert.");
21
+ return 3;
22
+ }
23
+ // 401 heisst: Der Schluessel gilt ohnehin nicht mehr (schon widerrufen
24
+ // oder abgelaufen). Der Zielzustand stimmt also — die Datei darf weg.
25
+ }
26
+ catch {
27
+ console.error("kimal3 ist gerade nicht erreichbar. Es wurde nichts geändert.");
28
+ return 3;
29
+ }
30
+ loescheAnmeldung();
31
+ console.log(`Abgemeldet. (${a.email})`);
32
+ return 0;
33
+ }
@@ -0,0 +1,53 @@
1
+ import { feld, ruf, textListe } from "../api.js";
2
+ import { BASIS_URL } from "../anmeldung.js";
3
+ /**
4
+ * Die eigenen Projekte anzeigen.
5
+ *
6
+ * 🔑 Wofuer das da ist: Damit Claude VOR dem Eintragen nachsehen kann. Ohne
7
+ * diesen Befehl steht dasselbe Projekt irgendwann zweimal auf einer
8
+ * oeffentlichen Seite mit dem Namen eines Menschen darauf.
9
+ */
10
+ export async function projektListe() {
11
+ const antwort = await ruf("/api/cli/projekt", { methode: "GET", mitAusweis: true });
12
+ if (antwort.status === 200) {
13
+ const titel = textListe(antwort.koerper, "titel");
14
+ if (!titel) {
15
+ console.error("Das hat nicht geklappt.");
16
+ return 3;
17
+ }
18
+ // Eine leere Liste ist KEIN Fehler — sie ist eine gueltige Antwort auf
19
+ // eine gueltige Frage. Deshalb 0 und ein Satz, kein Fehlertext.
20
+ if (titel.length === 0) {
21
+ console.log("Du hast noch keine Projekte eingetragen.");
22
+ console.log(` Eintragen: kimal3 projekt neu --titel "…"`);
23
+ return 0;
24
+ }
25
+ console.log(`\n Deine Projekte (${titel.length} von 12):`);
26
+ titel.forEach((t, i) => console.log(` ${i + 1}. ${t}`));
27
+ console.log(`\n Ansehen: ${BASIS_URL}/profil\n`);
28
+ return 0;
29
+ }
30
+ const meldung = feld(antwort.koerper, "meldung") ?? "Das hat nicht geklappt.";
31
+ // 402 vor 401: Beide heissen "kommst hier nicht rein", verlangen aber
32
+ // Gegenteiliges. Wer sie zusammenwirft, raet dem Menschen `kimal3 login` —
33
+ // und der meldet sich dann erfolgreich an, ohne dass es etwas aendert.
34
+ if (antwort.status === 402) {
35
+ console.error(meldung);
36
+ return 4;
37
+ }
38
+ if (antwort.status === 401) {
39
+ console.error(`${meldung} Bitte \`kimal3 login\` ausführen.`);
40
+ return 2;
41
+ }
42
+ if (antwort.status === 409 && feld(antwort.koerper, "grund") === "kein-profil") {
43
+ console.error(`${meldung} Leg es hier an: ${BASIS_URL}/profil`);
44
+ return 1;
45
+ }
46
+ // ⚠️ Hier landet auch die `503` "Konto gerade nicht prüfbar" — und sie MUSS
47
+ // hier landen, nicht oben bei der 402. Rueckgabewert 3 heisst "nochmal
48
+ // versuchen kann helfen"; 4 heisst laut SKILL.md ausdruecklich "nicht
49
+ // wiederholen". Ein Lesefehler an der Pro-Schranke gehoert in den ersten
50
+ // Topf. (Review-Befund 2026-08-14.)
51
+ console.error(meldung);
52
+ return antwort.status >= 500 ? 3 : 1;
53
+ }
@@ -0,0 +1,82 @@
1
+ import { feld, ruf } from "../api.js";
2
+ import { BASIS_URL } from "../anmeldung.js";
3
+ import { zerlege } from "../argumente.js";
4
+ const OPTIONEN = [
5
+ "titel", "beschreibung", "symbol", "stunden", "ergebnis", "umsatz",
6
+ "wirkung", "wirkung-label", "link", "live-seit",
7
+ ];
8
+ const HILFE = `
9
+ kimal3 projekt neu --titel "Angebots-Generator" [Optionen]
10
+
11
+ --titel Pflicht. 2–80 Zeichen.
12
+ --beschreibung Bis 300 Zeichen.
13
+ --symbol Eines der bekannten Symbole, z. B. bot, mail, workflow.
14
+ --stunden Zeitersparnis pro Woche, 0–168.
15
+ --ergebnis Ergebnis pro Monat in Euro, z. B. 1200 oder 1200,50.
16
+ --umsatz Umsatz pro Monat in Euro.
17
+ --wirkung Kurz, bis 20 Zeichen, z. B. "-70%".
18
+ --wirkung-label Wozu, bis 40 Zeichen, z. B. "Angebotszeit".
19
+ --link Vollstaendige Adresse mit http:// oder https://.
20
+ --live-seit Datum als JJJJ-MM-TT.
21
+ `;
22
+ export async function projektNeu(argv) {
23
+ // ⚠️ VOR dem Zerlegen abfragen, nicht danach.
24
+ // Der Bauplan hatte "help" in der Optionsliste und fragte hinterher. Das
25
+ // konnte nie greifen: `zerlege` verlangt zu jeder Option einen Wert, also
26
+ // scheitert `--help` schon dort mit "fehlt ihr Wert". Ein Schalter ist keine
27
+ // Option mit Wert — er gehoert vor den Zerleger.
28
+ if (argv.includes("--help") || argv.includes("-h")) {
29
+ console.log(HILFE);
30
+ return 0;
31
+ }
32
+ const z = zerlege(argv, OPTIONEN);
33
+ if (!z.ok) {
34
+ console.error(z.meldung);
35
+ return 1;
36
+ }
37
+ const o = z.werte.optionen;
38
+ if (!o.titel) {
39
+ console.error("Es fehlt --titel.\n" + HILFE);
40
+ return 1;
41
+ }
42
+ const antwort = await ruf("/api/cli/projekt", {
43
+ mitAusweis: true,
44
+ koerper: {
45
+ titel: o.titel, beschreibung: o.beschreibung ?? "", icon: o.symbol ?? "",
46
+ wirkung_text: o.wirkung ?? "", wirkung_label: o["wirkung-label"] ?? "",
47
+ zeitersparnis_std_wo: o.stunden ?? "", ergebnis_monat_eur: o.ergebnis ?? "",
48
+ umsatz_monat_eur: o.umsatz ?? "", link_url: o.link ?? "",
49
+ live_seit: o["live-seit"] ?? "",
50
+ },
51
+ });
52
+ if (antwort.status === 201) {
53
+ const titel = feld(antwort.koerper, "titel") ?? o.titel;
54
+ const anzahl = antwort.koerper?.anzahl;
55
+ console.log(`✓ „${titel}" ins Erfolgsprofil eingetragen (Projekt ${anzahl} von 12).`);
56
+ console.log(` Ansehen: ${BASIS_URL}/profil`);
57
+ return 0;
58
+ }
59
+ const meldung = feld(antwort.koerper, "meldung") ?? "Das hat nicht geklappt.";
60
+ // 402 vor 401: Beide heissen "kommst hier nicht rein", verlangen aber
61
+ // Gegenteiliges. Wer sie zusammenwirft, raet dem Menschen `kimal3 login` —
62
+ // und der meldet sich dann erfolgreich an, ohne dass es etwas aendert.
63
+ if (antwort.status === 402) {
64
+ console.error(meldung);
65
+ return 4;
66
+ }
67
+ if (antwort.status === 401) {
68
+ console.error(`${meldung} Bitte \`kimal3 login\` ausführen.`);
69
+ return 2;
70
+ }
71
+ if (antwort.status === 409 && feld(antwort.koerper, "grund") === "kein-profil") {
72
+ console.error(`${meldung} Leg es hier an: ${BASIS_URL}/profil`);
73
+ return 1;
74
+ }
75
+ // ⚠️ Hier landet auch die `503` "Konto gerade nicht prüfbar" — und sie MUSS
76
+ // hier landen, nicht oben bei der 402. Rueckgabewert 3 heisst "nochmal
77
+ // versuchen kann helfen"; 4 heisst laut SKILL.md ausdruecklich "nicht
78
+ // wiederholen". Ein Lesefehler an der Pro-Schranke gehoert in den ersten
79
+ // Topf. (Review-Befund 2026-08-14.)
80
+ console.error(meldung);
81
+ return antwort.status >= 500 ? 3 : 1;
82
+ }
@@ -0,0 +1,44 @@
1
+ import { copyFileSync, existsSync, mkdirSync } from "node:fs";
2
+ import { homedir } from "node:os";
3
+ import { fileURLToPath } from "node:url";
4
+ import { join } from "node:path";
5
+ /**
6
+ * Legt das Skill GLOBAL ab, damit Claude dieses Werkzeug ueberall kennt.
7
+ *
8
+ * Der Pfad zur eigenen Datei kommt ueber `import.meta.url` und NICHT ueber
9
+ * `process.cwd()` — die Quelle liegt im installierten Paket, das Ziel im
10
+ * Ordner des Menschen. Wer beides verwechselt, kopiert eine Datei auf sich
11
+ * selbst.
12
+ *
13
+ * `dist/befehle/skill-install.js` -> zwei Ebenen hoch -> `skill/SKILL.md`.
14
+ */
15
+ export async function skillInstall() {
16
+ const quelle = fileURLToPath(new URL("../../skill/SKILL.md", import.meta.url));
17
+ if (!existsSync(quelle)) {
18
+ console.error("Das Skill liegt nicht im Paket. Bitte kimal3 neu installieren.");
19
+ return 3;
20
+ }
21
+ // 🔑 GLOBAL, nicht projektlokal (Fund von Dennis, 2026-08-13).
22
+ // Gemessen: In ~/.claude/skills liegen 34 Skills, im Projekt selbst kein
23
+ // einziges. Inhaltlich stimmt es auch -- "trag ein fertiges Projekt ins
24
+ // Erfolgsprofil ein" gilt in JEDEM Ordner, nicht in einem. Wer projektlokal
25
+ // installiert, hat das Werkzeug im naechsten Projekt nicht -- und merkt es
26
+ // erst, wenn er dort vergeblich "fertig" sagt.
27
+ const zielOrdner = join(homedir(), ".claude", "skills", "kimal3");
28
+ const ziel = join(zielOrdner, "SKILL.md");
29
+ // Vorher merken: "angelegt" und "aktualisiert" sind zwei verschiedene
30
+ // Wirklichkeiten und bekommen deshalb zwei verschiedene Meldungen.
31
+ const gabEsSchon = existsSync(ziel);
32
+ try {
33
+ mkdirSync(zielOrdner, { recursive: true });
34
+ copyFileSync(quelle, ziel);
35
+ }
36
+ catch (fehler) {
37
+ console.error(`Das Skill liess sich nicht schreiben: ${String(fehler)}`);
38
+ return 3;
39
+ }
40
+ console.log(gabEsSchon ? "✓ Skill aktualisiert." : "✓ Skill angelegt.");
41
+ console.log(` ${ziel}`);
42
+ console.log(" Claude kennt das Werkzeug ab der nächsten Sitzung.");
43
+ return 0;
44
+ }
package/dist/kimal3.js ADDED
@@ -0,0 +1,81 @@
1
+ #!/usr/bin/env node
2
+ /**
3
+ * Der Einstieg. Jeder Befehl liefert eine Zahl zurueck, und die Zahl ist die
4
+ * eigentliche Schnittstelle: Claude liest sie, nicht den Text.
5
+ *
6
+ * 0 erledigt · 1 Eingabe passt nicht · 2 Anmeldung fehlt ·
7
+ * 3 Server nicht erreichbar · 4 angemeldet, aber kein Pro-Zugang
8
+ *
9
+ * 🔑 Warum die 4 eine eigene Zahl braucht: "nicht angemeldet" (2) und "kein
10
+ * Zugang" (4) klingen aehnlich, verlangen aber Gegenteiliges — bei 2 hilft
11
+ * `kimal3 login`, bei 4 hilft genau das nicht. Wer beide auf dieselbe Zahl
12
+ * legt, laesst Claude den falschen Rat geben.
13
+ */
14
+ import { leseAnmeldung } from "./anmeldung.js";
15
+ import { NichtAngemeldet } from "./api.js";
16
+ import { login } from "./befehle/login.js";
17
+ import { logout } from "./befehle/logout.js";
18
+ import { projektListe } from "./befehle/projekt-liste.js";
19
+ import { projektNeu } from "./befehle/projekt-neu.js";
20
+ 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
+ `;
32
+ /**
33
+ * Hilfe plus Zustand.
34
+ *
35
+ * ⚠️ Bewusst KEIN automatischer Login (Frage von Dennis, 2026-08-13, am selben
36
+ * Abend nochmals bestaetigt): Wer `npx kimal3` tippt, um sich das Ding
37
+ * anzusehen, bekaeme sonst unvermittelt einen Anmelde-Vorgang samt Browser.
38
+ * Und was taete der zweite Aufruf, wenn man schon angemeldet ist? Es gaebe
39
+ * zwei Wege zum selben Ergebnis, einer davon unerwartet.
40
+ * *Ein Befehl darf nicht mehr tun, als sein Name verspricht.*
41
+ *
42
+ * ⚠️ Und KEIN Netzaufruf: Wer nur die Hilfe sehen will, soll nicht auf einen
43
+ * Server warten. Ob die Anmeldung noch gilt, klaert der naechste echte Befehl.
44
+ */
45
+ function zustandsZeile() {
46
+ const a = leseAnmeldung();
47
+ return a
48
+ ? ` ● Angemeldet als ${a.email}`
49
+ : " ● Nicht angemeldet. → kimal3 login";
50
+ }
51
+ async function haupt() {
52
+ const argv = process.argv.slice(2);
53
+ const wort = argv.join(" ");
54
+ if (argv.length === 0 || wort === "--help" || wort === "-h") {
55
+ console.log(HILFE);
56
+ console.log(zustandsZeile() + "\n");
57
+ return 0;
58
+ }
59
+ if (wort === "login")
60
+ return login();
61
+ if (wort === "logout")
62
+ return logout();
63
+ if (argv[0] === "projekt" && argv[1] === "neu")
64
+ return projektNeu(argv.slice(2));
65
+ if (wort === "projekt liste")
66
+ return projektListe();
67
+ if (wort === "skill install")
68
+ return skillInstall();
69
+ console.error(`Den Befehl „${wort}" kenne ich nicht.\n${HILFE}`);
70
+ return 1;
71
+ }
72
+ haupt()
73
+ .then((code) => process.exit(code))
74
+ .catch((f) => {
75
+ if (f instanceof NichtAngemeldet) {
76
+ console.error("Bitte zuerst `kimal3 login` ausführen.");
77
+ process.exit(2);
78
+ }
79
+ console.error("kimal3 ist gerade nicht erreichbar.");
80
+ process.exit(3);
81
+ });
package/package.json ADDED
@@ -0,0 +1,13 @@
1
+ {
2
+ "name": "kimal3",
3
+ "version": "0.1.0",
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": { "build": "tsc -p tsconfig.json" },
10
+ "keywords": ["kimal3", "cli"],
11
+ "homepage": "https://kimal3-app.vercel.app/pro",
12
+ "license": "SEE LICENSE IN README.md"
13
+ }
package/skill/SKILL.md ADDED
@@ -0,0 +1,39 @@
1
+ ---
2
+ name: kimal3
3
+ description: Use when the user has finished an AI project and wants it recorded in their kimal3 Erfolgsprofil, or asks to list/add projects there.
4
+ ---
5
+
6
+ # kimal3 — Projekte ins Erfolgsprofil eintragen
7
+
8
+ ## Wann
9
+ Sobald der Mensch sagt, ein Projekt sei fertig, oder ausdrücklich bittet, es einzutragen.
10
+
11
+ ## Erst nachsehen, dann schreiben
12
+ `npx kimal3 projekt liste` — steht es schon da, **nicht** noch einmal eintragen.
13
+
14
+ ## Eintragen
15
+ `npx kimal3 projekt neu --titel "…" [--stunden 6] [--ergebnis 1200] [--symbol bot]`
16
+ Alle Optionen: `npx kimal3 projekt neu --help`
17
+
18
+ ## 🔑 Zahlen nie erfinden
19
+ Zeitersparnis, Umsatz und Ergebnis sind Behauptungen über die Wirklichkeit eines Menschen — und
20
+ sie stehen anschließend **öffentlich** auf einer Seite mit seinem Namen. Ist eine Zahl nicht
21
+ genannt worden: **fragen** oder das Feld weglassen. Niemals schätzen.
22
+
23
+ ## Nicht angemeldet?
24
+ Dem Menschen `npx kimal3 login` zeigen und ihn selbst bestätigen lassen. Die Anmeldung **nie**
25
+ selbst versuchen — sie braucht einen Menschen, der im Browser bestätigt.
26
+
27
+ ## Kein Pro-Zugang (Rückgabewert 4)
28
+ Die Meldung enthält bereits die Adresse. Dem Menschen **die Meldung zeigen** und den Befehl
29
+ **nicht wiederholen** — ein zweiter Versuch ändert nichts und wirkt wie ein Fehler im Werkzeug.
30
+ Auch **nicht** `kimal3 login` vorschlagen: Der Mensch *ist* angemeldet, ihm fehlt das Konto.
31
+
32
+ ## Vorübergehende Störung (Rückgabewert 3)
33
+ Server nicht erreichbar — oder das Konto ließ sich gerade nicht prüfen. Das ist **kein**
34
+ endgültiges Nein: **einmal** wiederholen ist richtig. Klappt es wieder nicht, dem Menschen die
35
+ Meldung zeigen und es dabei belassen.
36
+
37
+ ## Rückgabewerte
38
+ `0` erledigt · `1` Eingabe passt nicht · `2` Anmeldung fehlt · `3` vorübergehende Störung,
39
+ einmal wiederholen · `4` angemeldet, aber kein Pro-Zugang — **nicht** wiederholen