kimal3 0.4.0 → 0.6.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 +8 -0
- package/coach/de/kimal3-coach.md +2 -2
- package/coach/en/kimal3-coach.md +2 -2
- package/dist/befehle/einrichten.js +1 -1
- package/dist/befehle/login.js +6 -2
- package/dist/befehle/plan-hoch.js +114 -0
- package/dist/befehle/plan.js +110 -0
- package/dist/befehle/skill-install.js +4 -1
- package/dist/befehle/zeigen-einrichten.js +104 -0
- package/dist/befehle/zeigen-start.js +143 -0
- package/dist/befehle/zeigen.js +288 -0
- package/dist/befehle-katalog-en.js +41 -0
- package/dist/befehle-katalog.js +45 -0
- package/dist/befehlswoerter.js +16 -0
- package/dist/kimal3.js +15 -0
- package/dist/logo-ablegen.js +26 -6
- package/dist/plan-datei.js +172 -0
- package/dist/slash-befehle.js +2 -0
- package/dist/texte/einrichten.js +10 -3
- package/dist/texte/plan.js +93 -0
- package/dist/texte/zeigen.js +189 -0
- package/dist/zeigen-rechnung.js +226 -0
- package/dist/zeigen-umgebung.js +190 -0
- package/package.json +30 -29
- package/skill/SKILL.md +4 -2
- package/skill/commands/k3.md +5 -3
- package/skill/commands/k3plan.md +347 -0
- package/skill/commands/k3start.md +1 -1
- package/skill/commands/k3zeigen.md +303 -0
- package/skill/en/SKILL.md +5 -3
- package/skill/en/commands/k3.md +5 -3
- package/skill/en/commands/k3plan.md +342 -0
- package/skill/en/commands/k3show.md +302 -0
- package/skill/en/commands/k3start.md +1 -1
- package/zeigen/gemeinsam.py +82 -0
- package/zeigen/starter.py +163 -0
- package/zeigen/werkzeug.py +250 -0
- package/zeigen/zeitleiste.py +393 -0
package/README.md
CHANGED
|
@@ -42,8 +42,12 @@ kimal3 kontext fragen Die Fragen über deine Firma beantworten — Claude f
|
|
|
42
42
|
kimal3 kontext hoch Deine lokalen Änderungen an den fünf Firmen-Texten hochschicken
|
|
43
43
|
kimal3 projekt neu … Ein Projekt ins Erfolgsprofil eintragen
|
|
44
44
|
kimal3 projekt liste Deine eingetragenen Projekte anzeigen
|
|
45
|
+
kimal3 plan Planung hochladen oder Rückmeldungen holen
|
|
46
|
+
kimal3 plan hoch Die Planung aus plan/stand.json hochschicken (--probe: nur prüfen)
|
|
45
47
|
kimal3 kurs Zeigen, wo du im Mini-Kurs stehst
|
|
46
48
|
kimal3 firma daten Die Daten deiner Firma holen, damit Claude sie auswerten kann
|
|
49
|
+
kimal3 zeigen start Eine Aufnahme starten, während du eine Aufgabe vormachst
|
|
50
|
+
kimal3 zeigen status Nachsehen, ob gerade aufgenommen wird und ob der Aufnehmer eingerichtet ist
|
|
47
51
|
kimal3 wache Läuft von selbst beim Start von Claude: prüft Zugang, Nachricht, Version, Skill
|
|
48
52
|
kimal3 wache aus Die tägliche Prüfung abschalten — sie sagt dann nichts mehr
|
|
49
53
|
kimal3 wache an Sie wieder einschalten
|
|
@@ -180,8 +184,12 @@ kimal3 context questions Answer the questions about your company — Claude tak
|
|
|
180
184
|
kimal3 context upload Upload your local changes to the five company texts
|
|
181
185
|
kimal3 project new … Add a project to your success profile
|
|
182
186
|
kimal3 project list Show the projects you've added
|
|
187
|
+
kimal3 plan Upload planning or fetch responses
|
|
188
|
+
kimal3 plan upload Upload the planning in plan/stand.json (--dry-run: only check)
|
|
183
189
|
kimal3 course Show where you are in the mini-course
|
|
184
190
|
kimal3 company data Fetch your company's data so Claude can analyse it
|
|
191
|
+
kimal3 show start Start a recording while you show a task
|
|
192
|
+
kimal3 show status Check whether a recording is running and whether the recorder is set up
|
|
185
193
|
kimal3 watch Runs by itself when Claude starts: checks your access, messages, version and skill
|
|
186
194
|
kimal3 watch off Switch the daily check off — it then says nothing at all
|
|
187
195
|
kimal3 watch on Switch it back on
|
package/coach/de/kimal3-coach.md
CHANGED
|
@@ -118,8 +118,8 @@ nutze sie, statt vorschnell loszulegen.
|
|
|
118
118
|
Beispiel Node.js: `winget install OpenJS.NodeJS.LTS` / `brew install node`. Fehlt Homebrew, frag vorher: Es
|
|
119
119
|
einzurichten braucht einmal das Administrator-Passwort.
|
|
120
120
|
**Ohne Adminrechte** (oft am Firmenrechner) geht das nicht. Dann kommt das Werkzeug nur für dieses Konto in den
|
|
121
|
-
Benutzerordner, so wie
|
|
122
|
-
biete eine Nachricht an die IT an.
|
|
121
|
+
Benutzerordner, so wie die kimal3-Einrichtungsanleitung Node.js ohne Adminrechte nach `~/.kimal3/node` legt; sag dazu, dass es
|
|
122
|
+
dann nicht mit `upgrade` mitkommt, und biete eine Nachricht an die IT an.
|
|
123
123
|
**Ist ein Werkzeug schon da, aktualisiere es auf dem Weg, auf dem es gekommen ist** — sonst liegen zwei Fassungen
|
|
124
124
|
nebeneinander, und es ist unklar, welche startet.
|
|
125
125
|
- **Aktualisieren nach der Versionsnummer** (`16.2.10`): **letzte Zahl** = nur Fehler und Sicherheitslücken, zügig
|
package/coach/en/kimal3-coach.md
CHANGED
|
@@ -118,8 +118,8 @@ use them instead of starting off too quickly.
|
|
|
118
118
|
Example Node.js: `winget install OpenJS.NodeJS.LTS` / `brew install node`. If Homebrew is missing, ask first:
|
|
119
119
|
setting it up needs the administrator password once.
|
|
120
120
|
**Without admin rights** (often on a company computer) this does not work. Then the tool goes into the user folder,
|
|
121
|
-
for this account only, the way
|
|
122
|
-
and offer a message to IT.
|
|
121
|
+
for this account only, the way the kimal3 setup guide puts Node.js into `~/.kimal3/node` without admin rights; say that it
|
|
122
|
+
will then not come along with `upgrade`, and offer a message to IT.
|
|
123
123
|
**If a tool is already there, update it the way it came** — otherwise two versions sit side by side, and it is
|
|
124
124
|
unclear which one starts.
|
|
125
125
|
- **Update by version number** (`16.2.10`): **last number** = only bugs and security holes, take it
|
|
@@ -314,7 +314,7 @@ function pluginsEinrichten(git) {
|
|
|
314
314
|
// Ohne Logo-Ordner kommt superpowers trotzdem (Durchsicht 2, Fund 1): eine Zeile zum Logo, dann weiter.
|
|
315
315
|
if (!ablage.ok)
|
|
316
316
|
console.log(t.logoNotWritten(ablage.detail));
|
|
317
|
-
if (ablage.ok && ablage.
|
|
317
|
+
if (ablage.ok && ablage.stand === "alt")
|
|
318
318
|
console.log(t.logoOldVersion);
|
|
319
319
|
// Schrägstriche vorwärts: So steht der Pfad in Claudes Einstellungen auf jedem System gleich (P-13).
|
|
320
320
|
const eintrag = tragePluginsEin(ablage.ok ? ablage.ordner.replace(/\\/g, "/") : null, git);
|
package/dist/befehle/login.js
CHANGED
|
@@ -6,6 +6,7 @@ import { anmeldeAdresse, oeffneImBrowser } from "../browser.js";
|
|
|
6
6
|
import { ALLGEMEIN_TEXTE } from "../texte/allgemein.js";
|
|
7
7
|
import { EINRICHTEN_TEXTE } from "../texte/einrichten.js";
|
|
8
8
|
import { frischeCoachAuf } from "../coach-ablegen.js";
|
|
9
|
+
import { frischeLogoAuf } from "../logo-ablegen.js";
|
|
9
10
|
import { stelleSkillUm } from "./skill-tausch.js";
|
|
10
11
|
const warte = (ms) => new Promise((f) => setTimeout(f, ms));
|
|
11
12
|
// 🌐 Die Sprache wird bei JEDER Ausgabe neu gelesen: Die Abhol-Antwort nennt die Kontosprache
|
|
@@ -31,9 +32,12 @@ export async function login() {
|
|
|
31
32
|
const code = await warteAufBestaetigung(start.geraeteCode);
|
|
32
33
|
// Der Coach zieht mit der Kontosprache mit -- nur beim BEFEHL `kimal3 login`, nicht in der geteilten Warteschleife:
|
|
33
34
|
// `einrichten` meldet anschließend seinen eigenen Coach, eine zweite Zeile wäre doppelt (Gesamt-Durchsicht, Fund 4;
|
|
34
|
-
// dasselbe Muster wie `skillInstallBefehl`). Frischt nur auf, legt nie an (Spec C-13b).
|
|
35
|
-
|
|
35
|
+
// dasselbe Muster wie `skillInstallBefehl`). Frischt nur auf, legt nie an (Spec C-13b). Das Logo genauso (Spec
|
|
36
|
+
// 2026-10-18, L-1, L-8): `einrichten` legt es nach der Anmeldung ohnehin selbst ab.
|
|
37
|
+
if (code === 0) {
|
|
36
38
|
frischeCoachAuf(sprache());
|
|
39
|
+
frischeLogoAuf(sprache());
|
|
40
|
+
}
|
|
37
41
|
return code;
|
|
38
42
|
}
|
|
39
43
|
/**
|
|
@@ -0,0 +1,114 @@
|
|
|
1
|
+
import { feld, ruf } from "../api.js";
|
|
2
|
+
import { schalterOhneWert, zerlege } from "../argumente.js";
|
|
3
|
+
import { sprache } from "../sprache.js";
|
|
4
|
+
import { ALLGEMEIN_TEXTE } from "../texte/allgemein.js";
|
|
5
|
+
import { PLAN_TEXTE } from "../texte/plan.js";
|
|
6
|
+
import { leseSpecTexte, schreibeNachHochladen, specKoepfeAus } from "../plan-datei.js";
|
|
7
|
+
import { planAbsage, planDateiOderAbsage } from "./plan.js";
|
|
8
|
+
const istZahl = (w) => typeof w === "number" && Number.isInteger(w) && w >= 0;
|
|
9
|
+
function hochgeladenAus(koerper) {
|
|
10
|
+
if (!koerper || typeof koerper !== "object")
|
|
11
|
+
return null;
|
|
12
|
+
const k = koerper;
|
|
13
|
+
if (k.probe === true)
|
|
14
|
+
return null;
|
|
15
|
+
if (typeof k.projektId !== "string" || typeof k.titel !== "string" || typeof k.adresse !== "string" || typeof k.hochgeladenAm !== "string")
|
|
16
|
+
return null;
|
|
17
|
+
if (!istZahl(k.stories) || !istZahl(k.deinTest))
|
|
18
|
+
return null;
|
|
19
|
+
return { projektId: k.projektId, titel: k.titel, stories: k.stories, deinTest: k.deinTest, adresse: k.adresse, hochgeladenAm: k.hochgeladenAm, specTexte: istZahl(k.specTexte) ? k.specTexte : null };
|
|
20
|
+
}
|
|
21
|
+
function probeAus(koerper) {
|
|
22
|
+
if (!koerper || typeof koerper !== "object")
|
|
23
|
+
return null;
|
|
24
|
+
const k = koerper;
|
|
25
|
+
return k.probe === true && istZahl(k.stories) && istZahl(k.uebernommen) ? { stories: k.stories, uebernommen: k.uebernommen, specTexte: istZahl(k.specTexte) ? k.specTexte : null } : null;
|
|
26
|
+
}
|
|
27
|
+
/**
|
|
28
|
+
* Die Zeile aus US-16. `angekommen` kommt aus der Antwort des Servers (Eintraege mit Text), nicht aus dem, was
|
|
29
|
+
* die CLI geschickt hat: Eine alte App ohne das Feld antwortet ohne Zahl, und dann steht hier lieber keine Zeile
|
|
30
|
+
* als eine, die ein Hochladen behauptet, das nicht stattgefunden hat.
|
|
31
|
+
*/
|
|
32
|
+
function specTexteZeile(angekommen, gesamt, ohne, s) {
|
|
33
|
+
if (angekommen === null)
|
|
34
|
+
return null;
|
|
35
|
+
const t = PLAN_TEXTE[s];
|
|
36
|
+
return t.specTexte(angekommen, gesamt, ohne.map((o) => t.specTextOhneEintrag(o.titel, t.specTextOhneGrund(o.grund, o.pfad))));
|
|
37
|
+
}
|
|
38
|
+
export async function planHoch(argv) {
|
|
39
|
+
let s = sprache();
|
|
40
|
+
if (argv.includes("--help") || argv.includes("-h")) {
|
|
41
|
+
console.log(PLAN_TEXTE[s].uploadHelp);
|
|
42
|
+
return 0;
|
|
43
|
+
}
|
|
44
|
+
// ⚠️ `zerlege` kennt keine Schalter ohne Wert -- beide VOR der Zerlegung heraus, wie `--ueberschreiben` in kontext-hoch.ts.
|
|
45
|
+
// Was danach uebrig bleibt, ist ein Tippfehler: `zerlege` ohne erlaubte Optionen sagt dazu den passenden Satz.
|
|
46
|
+
const ueber = schalterOhneWert(argv, "ueberschreiben");
|
|
47
|
+
const probe = schalterOhneWert(ueber.rest, "probe");
|
|
48
|
+
const zerlegt = zerlege(probe.rest, [], s);
|
|
49
|
+
if (!zerlegt.ok) {
|
|
50
|
+
console.error(zerlegt.meldung);
|
|
51
|
+
return 1;
|
|
52
|
+
}
|
|
53
|
+
const datei = planDateiOderAbsage(s);
|
|
54
|
+
if (datei === null)
|
|
55
|
+
return 1;
|
|
56
|
+
const hatteNummer = typeof datei.json.projekt_id === "string";
|
|
57
|
+
// Der Text der Specs geht im selben Aufruf mit (US-16); pro Spec liegt entweder der Text oder der Grund vor.
|
|
58
|
+
const koepfe = specKoepfeAus(datei.json);
|
|
59
|
+
// Eine Funktion baut den Rumpf: an ihm misst `leseSpecTexte` die 4 MB, und genau er geht hinaus (US-16).
|
|
60
|
+
const rumpf = (specTexte) => ({ stand: datei.text, ueberschreiben: ueber.gesetzt, probe: probe.gesetzt, specTexte });
|
|
61
|
+
const texte = leseSpecTexte(process.cwd(), koepfe, rumpf);
|
|
62
|
+
const antwort = await ruf("/api/cli/projekt/plan", {
|
|
63
|
+
methode: "POST", // kein-anzeigetext: HTTP-Verb
|
|
64
|
+
mitAusweis: true,
|
|
65
|
+
koerper: rumpf(texte.eintraege),
|
|
66
|
+
});
|
|
67
|
+
// 🌐 Die Antwort kann die Kontosprache umgestellt haben: Was ab hier gedruckt wird, in der neuen.
|
|
68
|
+
s = sprache();
|
|
69
|
+
const t = PLAN_TEXTE[s];
|
|
70
|
+
if (antwort.status !== 200) {
|
|
71
|
+
const code = planAbsage(antwort, s);
|
|
72
|
+
// Ruling 10: `503 teilweise` -- gespeichert, aber die Rueckmeldungen noch nicht markiert. `basis` muss trotzdem
|
|
73
|
+
// nachgetragen werden, sonst lehnt der zweite Lauf als „veraltet" ab; `uebernommen` bleibt fuer genau diesen Lauf stehen.
|
|
74
|
+
// Fehlen Nummer oder Zeitpunkt in der Antwort, wird nichts geschrieben (geraten wird hier nie).
|
|
75
|
+
const projektId = feld(antwort.koerper, "projektId"); // kein-anzeigetext: Feldname der Antwort
|
|
76
|
+
const hochgeladenAm = feld(antwort.koerper, "hochgeladenAm"); // kein-anzeigetext: Feldname der Antwort
|
|
77
|
+
if (antwort.status === 503 && feld(antwort.koerper, "grund") === "teilweise" && projektId !== null && hochgeladenAm !== null &&
|
|
78
|
+
!schreibeNachHochladen(process.cwd(), projektId, hochgeladenAm, false)) {
|
|
79
|
+
console.error(t.writeBackFailed(projektId, hochgeladenAm, false));
|
|
80
|
+
}
|
|
81
|
+
return code;
|
|
82
|
+
}
|
|
83
|
+
if (probe.gesetzt) {
|
|
84
|
+
const p = probeAus(antwort.koerper);
|
|
85
|
+
if (p === null) {
|
|
86
|
+
console.error(ALLGEMEIN_TEXTE[s].unreadableAnswer);
|
|
87
|
+
return 3;
|
|
88
|
+
}
|
|
89
|
+
console.log(t.dryRun(p.stories, p.uebernommen));
|
|
90
|
+
const zeile = specTexteZeile(p.specTexte, koepfe.length, texte.ohne, s);
|
|
91
|
+
if (zeile !== null)
|
|
92
|
+
console.log(zeile);
|
|
93
|
+
return 0;
|
|
94
|
+
}
|
|
95
|
+
const h = hochgeladenAus(antwort.koerper);
|
|
96
|
+
if (h === null) {
|
|
97
|
+
console.error(ALLGEMEIN_TEXTE[s].unreadableAnswer);
|
|
98
|
+
return 3;
|
|
99
|
+
}
|
|
100
|
+
// Erst sagen, was oben angekommen ist, dann, was daneben schiefging (dieselbe Reihenfolge wie in kontext-hoch.ts).
|
|
101
|
+
console.log(t.uploaded(h.titel, h.stories, h.deinTest));
|
|
102
|
+
const zeile = specTexteZeile(h.specTexte, koepfe.length, texte.ohne, s);
|
|
103
|
+
if (zeile !== null)
|
|
104
|
+
console.log(zeile);
|
|
105
|
+
console.log(t.view(h.adresse));
|
|
106
|
+
if (!schreibeNachHochladen(process.cwd(), h.projektId, h.hochgeladenAm, true)) {
|
|
107
|
+
// `1`, nicht `0`: Ohne `basis` in der Datei lehnt das naechste Hochladen als „veraltet" ab -- hier ist noch etwas zu tun.
|
|
108
|
+
console.error(t.writeBackFailed(h.projektId, h.hochgeladenAm, true));
|
|
109
|
+
return 1;
|
|
110
|
+
}
|
|
111
|
+
if (!hatteNummer)
|
|
112
|
+
console.log(t.idNowInFile);
|
|
113
|
+
return 0;
|
|
114
|
+
}
|
|
@@ -0,0 +1,110 @@
|
|
|
1
|
+
import { feld, ruf } from "../api.js";
|
|
2
|
+
import { sprache } from "../sprache.js";
|
|
3
|
+
import { ALLGEMEIN_TEXTE } from "../texte/allgemein.js";
|
|
4
|
+
import { PLAN_TEXTE } from "../texte/plan.js";
|
|
5
|
+
import { schlussFehler } from "../fehler.js";
|
|
6
|
+
import { lesePlanDatei } from "../plan-datei.js";
|
|
7
|
+
const istZahl = (w) => typeof w === "number" && Number.isInteger(w);
|
|
8
|
+
const textOderNull = (w) => typeof w === "string" || w === null;
|
|
9
|
+
function offenAus(w) {
|
|
10
|
+
if (!w || typeof w !== "object")
|
|
11
|
+
return null;
|
|
12
|
+
const r = w;
|
|
13
|
+
if (!istZahl(r.nummer) || (r.art !== "passt_nicht" && r.art !== "thema") || typeof r.notiz !== "string")
|
|
14
|
+
return null;
|
|
15
|
+
if (!textOderNull(r.spec) || !textOderNull(r.story) || !(istZahl(r.runde) || r.runde === null))
|
|
16
|
+
return null;
|
|
17
|
+
// Ein „passt nicht" ohne Story und Runde waere eine Zeile, aus der Claude nichts machen kann.
|
|
18
|
+
if (r.art === "passt_nicht" && (r.spec === null || r.story === null || r.runde === null))
|
|
19
|
+
return null;
|
|
20
|
+
return { nummer: r.nummer, art: r.art, spec: r.spec, story: r.story, runde: r.runde, notiz: r.notiz };
|
|
21
|
+
}
|
|
22
|
+
function bestaetigtAus(w) {
|
|
23
|
+
if (!w || typeof w !== "object")
|
|
24
|
+
return null;
|
|
25
|
+
const r = w;
|
|
26
|
+
return typeof r.spec === "string" && typeof r.story === "string" && istZahl(r.runde) ? { spec: r.spec, story: r.story, runde: r.runde } : null;
|
|
27
|
+
}
|
|
28
|
+
/** `null`, wenn die Antwort nicht die erwartete Form hat (z. B. ein WLAN-Anmeldeportal mit `200`). */
|
|
29
|
+
export function planAntwortAus(koerper) {
|
|
30
|
+
if (!koerper || typeof koerper !== "object")
|
|
31
|
+
return null;
|
|
32
|
+
const k = koerper;
|
|
33
|
+
if (typeof k.adresse !== "string" || !Array.isArray(k.offen) || !Array.isArray(k.bestaetigt))
|
|
34
|
+
return null;
|
|
35
|
+
const offen = k.offen.map((o) => offenAus(o));
|
|
36
|
+
const bestaetigt = k.bestaetigt.map((b) => bestaetigtAus(b));
|
|
37
|
+
if (offen.includes(null) || bestaetigt.includes(null))
|
|
38
|
+
return null;
|
|
39
|
+
return { adresse: k.adresse, offen: offen, bestaetigt: bestaetigt };
|
|
40
|
+
}
|
|
41
|
+
/** Reine Rechnung: die Zeilen, die Claude zu sehen bekommt. */
|
|
42
|
+
export function planZeilen(a, s) {
|
|
43
|
+
const t = PLAN_TEXTE[s];
|
|
44
|
+
const zeilen = a.offen.length === 0
|
|
45
|
+
? [t.noOpen]
|
|
46
|
+
: a.offen.map((o) => (o.art === "thema" ? t.openTopic(o.nummer, o.notiz) : t.openVerdict(o.nummer, o.spec ?? "", o.story ?? "", o.runde ?? 0, o.notiz)));
|
|
47
|
+
if (a.bestaetigt.length > 0)
|
|
48
|
+
zeilen.push(t.confirmed(a.bestaetigt.map((b) => t.confirmedItem(b.spec, b.story, b.runde)).join(", ")));
|
|
49
|
+
zeilen.push(t.view(a.adresse));
|
|
50
|
+
return zeilen;
|
|
51
|
+
}
|
|
52
|
+
/** Die Datei fehlt oder ist kein JSON -- Satz auf stderr, `null`. Sonst die gelesene Datei. */
|
|
53
|
+
export function planDateiOderAbsage(s) {
|
|
54
|
+
const datei = lesePlanDatei(process.cwd());
|
|
55
|
+
if (datei.ok)
|
|
56
|
+
return datei;
|
|
57
|
+
console.error(datei.grund === "fehlt" ? PLAN_TEXTE[s].fileMissing : PLAN_TEXTE[s].noJson);
|
|
58
|
+
return null;
|
|
59
|
+
}
|
|
60
|
+
/**
|
|
61
|
+
* Was beide Befehle mit einer Absage tun. `409`/`422` tragen einen fertigen Satz in der Kontosprache
|
|
62
|
+
* (Projekt unbekannt oder mehrdeutig, Plan unlesbar, Runde kleiner, veraltet) -- woertlich ausgeben, `1`.
|
|
63
|
+
* `503 teilweise` heisst: gespeichert, aber nicht ganz -- `3`, denn ein zweiter Lauf hilft (Spec Abschnitt 5).
|
|
64
|
+
*/
|
|
65
|
+
export function planAbsage(antwort, s) {
|
|
66
|
+
const meldung = feld(antwort.koerper, "meldung") ?? ALLGEMEIN_TEXTE[s].genericFailure;
|
|
67
|
+
if (antwort.status === 402) {
|
|
68
|
+
console.error(meldung);
|
|
69
|
+
return 4;
|
|
70
|
+
}
|
|
71
|
+
if (antwort.status === 401) {
|
|
72
|
+
console.error(`${meldung}${ALLGEMEIN_TEXTE[s].pleaseSignIn}`);
|
|
73
|
+
return 2;
|
|
74
|
+
}
|
|
75
|
+
if (antwort.status === 409 || antwort.status === 422) {
|
|
76
|
+
console.error(meldung);
|
|
77
|
+
return 1;
|
|
78
|
+
}
|
|
79
|
+
// `503 teilweise` landet ueber `schlussFehler` auf derselben `3` wie jeder andere Aussetzer -- mit dem Satz des Servers.
|
|
80
|
+
const schluss = schlussFehler(antwort.status, meldung, s);
|
|
81
|
+
console.error(schluss.text);
|
|
82
|
+
return schluss.wert;
|
|
83
|
+
}
|
|
84
|
+
export async function plan(argv) {
|
|
85
|
+
let s = sprache();
|
|
86
|
+
if (argv.includes("--help") || argv.includes("-h")) {
|
|
87
|
+
console.log(PLAN_TEXTE[s].listHelp);
|
|
88
|
+
return 0;
|
|
89
|
+
}
|
|
90
|
+
const datei = planDateiOderAbsage(s);
|
|
91
|
+
if (datei === null)
|
|
92
|
+
return 1;
|
|
93
|
+
const suche = new URLSearchParams();
|
|
94
|
+
if (typeof datei.json.projekt_id === "string")
|
|
95
|
+
suche.set("projekt-id", datei.json.projekt_id);
|
|
96
|
+
if (typeof datei.json.projekt === "string")
|
|
97
|
+
suche.set("projekt", datei.json.projekt);
|
|
98
|
+
const antwort = await ruf(`/api/cli/projekt/plan?${suche.toString()}`, { methode: "GET", mitAusweis: true }); // kein-anzeigetext: Pfad und HTTP-Methode
|
|
99
|
+
// 🌐 Die Antwort kann die Kontosprache umgestellt haben: Was ab hier gedruckt wird, in der neuen.
|
|
100
|
+
s = sprache();
|
|
101
|
+
if (antwort.status !== 200)
|
|
102
|
+
return planAbsage(antwort, s);
|
|
103
|
+
const a = planAntwortAus(antwort.koerper);
|
|
104
|
+
if (a === null) {
|
|
105
|
+
console.error(ALLGEMEIN_TEXTE[s].unreadableAnswer);
|
|
106
|
+
return 3;
|
|
107
|
+
}
|
|
108
|
+
planZeilen(a, s).forEach((z) => console.log(z));
|
|
109
|
+
return 0;
|
|
110
|
+
}
|
|
@@ -10,6 +10,7 @@ import { sprache } from "../sprache.js";
|
|
|
10
10
|
import { andereSpracheZuRaeumen, skillQuelle } from "../slash-befehle.js";
|
|
11
11
|
import { SKILL_TEXTE } from "../texte/skill.js";
|
|
12
12
|
import { frischeCoachAuf } from "../coach-ablegen.js";
|
|
13
|
+
import { frischeLogoAuf } from "../logo-ablegen.js";
|
|
13
14
|
export const VERALTETE_BEFEHLE = [
|
|
14
15
|
/*
|
|
15
16
|
🔴 **Der ERSTE Eintrag ueberhaupt** (2026-08-28). `/k3firma` heisst jetzt
|
|
@@ -325,11 +326,13 @@ export async function skillInstall() {
|
|
|
325
326
|
* Der Befehl `kimal3 skill install`: die Anleitung ablegen und einen VORHANDENEN Coach mitziehen (Update, Sprachwechsel;
|
|
326
327
|
* angelegt wird keiner). Bewusst hier und nicht in `skillInstall()`: `einrichten` ruft das auch, hat seinen Coach aber
|
|
327
328
|
* schon selbst abgelegt und gemeldet -- ein zweites Auffrischen druckte dessen Zeile noch einmal (bei fehlender
|
|
328
|
-
* Trenn-Marke auch die Ersatzdatei ein zweites Mal).
|
|
329
|
+
* Trenn-Marke auch die Ersatzdatei ein zweites Mal). Das Logo zieht genauso mit, nur ein vorhandenes (Spec 2026-10-18,
|
|
330
|
+
* L-3 bis L-7): `einrichten` legt es selbst ab.
|
|
329
331
|
*/
|
|
330
332
|
export async function skillInstallBefehl() {
|
|
331
333
|
const code = await skillInstall();
|
|
332
334
|
frischeCoachAuf(sprache());
|
|
335
|
+
frischeLogoAuf(sprache());
|
|
333
336
|
return code;
|
|
334
337
|
}
|
|
335
338
|
/**
|
|
@@ -0,0 +1,104 @@
|
|
|
1
|
+
import { existsSync } from "node:fs";
|
|
2
|
+
import { dirname, win32 } from "node:path";
|
|
3
|
+
import { zerlege } from "../argumente.js";
|
|
4
|
+
import { AUFNEHMER_FASSUNG, aufnehmerArgumente, einrichtPlan, teileAus, uvEinrichtung, } from "../zeigen-rechnung.js";
|
|
5
|
+
import { LESEN_MS, aufnehmerPython, findeUv, rufeWerkzeug, stillAusfuehren, uvBinOrdner } from "../zeigen-umgebung.js";
|
|
6
|
+
import { vorab } from "./zeigen.js";
|
|
7
|
+
/**
|
|
8
|
+
* `kimal3 zeigen einrichten` / `kimal3 show setup` (Spec 2026-10-18-k3zeigen-design.md, US-2, Szenarien S-1, S-3, S-15).
|
|
9
|
+
* Holt in dieser Reihenfolge nur, was fehlt: uv, den Aufnehmer 1.3.0, FFmpeg, das Sprachmodell. Ohne Adminrechte.
|
|
10
|
+
* 🔴 Kein fremdes Programm spricht hier mit dem Menschen: winget, uv und capture laufen mit `stdio: "ignore"`, vom
|
|
11
|
+
* Werkzeug zaehlt nur die eine JSON-Zeile. Ob ein Schritt geklappt hat, sagt nie sein Rueckgabewert allein, sondern
|
|
12
|
+
* die neue Bestandsaufnahme danach -- winget meldet z. B. auch "schon installiert" als Fehler.
|
|
13
|
+
*/
|
|
14
|
+
// Obergrenzen je Schritt: grosszuegig (langsame Leitung), aber nie "haengt fuer immer". Der Slash-Befehl startet
|
|
15
|
+
// `einrichten` im Hintergrund, weil Claude Code einen Werkzeugaufruf nach 10 Minuten abbricht (Spec US-2).
|
|
16
|
+
const MINUTE = 60_000;
|
|
17
|
+
const ZEIT_UV = 15 * MINUTE;
|
|
18
|
+
const ZEIT_AUFNEHMER = 45 * MINUTE;
|
|
19
|
+
const ZEIT_FFMPEG = 15 * MINUTE;
|
|
20
|
+
const ZEIT_MODELL = 60 * MINUTE;
|
|
21
|
+
/** Was jetzt da ist: uv (Suchpfad + bekannte Orte), und vom Aufnehmer selbst, was er hat. */
|
|
22
|
+
function bestand() {
|
|
23
|
+
const p = aufnehmerPython();
|
|
24
|
+
return { teile: teileAus(findeUv()?.uv ?? null, p === null ? null : rufeWerkzeug(p, ["teile"], null, LESEN_MS)), umgebungDa: p !== null };
|
|
25
|
+
}
|
|
26
|
+
function richteUvEin() {
|
|
27
|
+
const { befehl, argumente } = uvEinrichtung(stillAusfuehren("winget", ["--version"], LESEN_MS)); // kein-anzeigetext: Befehl
|
|
28
|
+
stillAusfuehren(befehl, argumente, ZEIT_UV);
|
|
29
|
+
// Der Suchpfad dieser Sitzung ist noch der alte: findeUv sieht auch an den bekannten Orten nach.
|
|
30
|
+
return findeUv() !== null;
|
|
31
|
+
}
|
|
32
|
+
function richteAufnehmerEin(umgebungDa) {
|
|
33
|
+
const uv = findeUv();
|
|
34
|
+
if (uv === null)
|
|
35
|
+
return false;
|
|
36
|
+
stillAusfuehren(uv.uv, aufnehmerArgumente(umgebungDa), ZEIT_AUFNEHMER);
|
|
37
|
+
return bestand().teile.aufnehmer === AUFNEHMER_FASSUNG;
|
|
38
|
+
}
|
|
39
|
+
function richteFfmpegEin() {
|
|
40
|
+
const uv = findeUv();
|
|
41
|
+
const p = aufnehmerPython();
|
|
42
|
+
if (uv === null || p === null)
|
|
43
|
+
return false;
|
|
44
|
+
const bin = uvBinOrdner(uv.uv);
|
|
45
|
+
// `capture.exe` liegt im Programmordner von uv und neben dem Python des Aufnehmers; der erste, den es gibt.
|
|
46
|
+
const capture = [bin === null ? null : win32.join(bin, "capture.exe"), win32.join(dirname(p.python), "capture.exe")] // kein-anzeigetext: Dateiname
|
|
47
|
+
.find((d) => d !== null && existsSync(d));
|
|
48
|
+
if (capture === undefined)
|
|
49
|
+
return false;
|
|
50
|
+
stillAusfuehren(capture, ["install-ffmpeg"], ZEIT_FFMPEG); // kein-anzeigetext: Befehl
|
|
51
|
+
return bestand().teile.ffmpeg;
|
|
52
|
+
}
|
|
53
|
+
function richteModellEin() {
|
|
54
|
+
const p = aufnehmerPython();
|
|
55
|
+
if (p === null)
|
|
56
|
+
return false;
|
|
57
|
+
const r = rufeWerkzeug(p, ["modell-laden"], null, ZEIT_MODELL); // kein-anzeigetext: Befehl
|
|
58
|
+
return r.ok && (r.daten.ergebnis === "da" || r.daten.ergebnis === "geladen");
|
|
59
|
+
}
|
|
60
|
+
function zeile(t, schritt) {
|
|
61
|
+
if (schritt === "uv")
|
|
62
|
+
return t.schrittUv;
|
|
63
|
+
if (schritt === "aufnehmer")
|
|
64
|
+
return t.schrittAufnehmer(AUFNEHMER_FASSUNG);
|
|
65
|
+
return schritt === "ffmpeg" ? t.schrittFfmpeg : t.schrittModell;
|
|
66
|
+
}
|
|
67
|
+
export async function zeigenEinrichten(rest) {
|
|
68
|
+
const lage = vorab(rest, (t) => t.helpEinrichten);
|
|
69
|
+
if (typeof lage === "number")
|
|
70
|
+
return lage;
|
|
71
|
+
const { s, t } = lage;
|
|
72
|
+
const z = zerlege(rest, [], s);
|
|
73
|
+
if (!z.ok) {
|
|
74
|
+
console.error(`${z.meldung}\n${t.helpEinrichten}`);
|
|
75
|
+
return 1;
|
|
76
|
+
}
|
|
77
|
+
const anfang = bestand();
|
|
78
|
+
const plan = einrichtPlan(anfang.teile);
|
|
79
|
+
if (plan.length === 0) {
|
|
80
|
+
console.log(t.allesDa(AUFNEHMER_FASSUNG));
|
|
81
|
+
return 0;
|
|
82
|
+
}
|
|
83
|
+
for (const schritt of plan) {
|
|
84
|
+
// FFmpeg und Modell erst nach einem neuen Blick: Ein frisch eingerichteter Aufnehmer findet z. B. ein FFmpeg im Suchpfad.
|
|
85
|
+
if ((schritt === "ffmpeg" || schritt === "modell") && bestand().teile[schritt])
|
|
86
|
+
continue;
|
|
87
|
+
console.log(zeile(t, schritt));
|
|
88
|
+
const geklappt = schritt === "uv" ? richteUvEin()
|
|
89
|
+
: schritt === "aufnehmer" ? richteAufnehmerEin(anfang.umgebungDa)
|
|
90
|
+
: schritt === "ffmpeg" ? richteFfmpegEin() : richteModellEin();
|
|
91
|
+
if (!geklappt) {
|
|
92
|
+
console.error(t.nichtGeklappt(schritt));
|
|
93
|
+
return 1;
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
// Die Schlussmeldung nur, wenn die Bestandsaufnahme danach wirklich alle vier zeigt -- nie aus dem Plan geschlossen.
|
|
97
|
+
const offen = einrichtPlan(bestand().teile);
|
|
98
|
+
if (offen.length > 0) {
|
|
99
|
+
console.error(t.nichtGeklappt(offen[0]));
|
|
100
|
+
return 1;
|
|
101
|
+
}
|
|
102
|
+
console.log(t.fertigEingerichtet(AUFNEHMER_FASSUNG));
|
|
103
|
+
return 0;
|
|
104
|
+
}
|
|
@@ -0,0 +1,143 @@
|
|
|
1
|
+
import { existsSync, mkdirSync, readFileSync, writeFileSync } from "node:fs";
|
|
2
|
+
import { join } from "node:path";
|
|
3
|
+
import { trenneNamenUndOptionen, zerlege } from "../argumente.js";
|
|
4
|
+
import { einrichtPlan, fehlerGrund } from "../zeigen-rechnung.js";
|
|
5
|
+
import { LESEN_MS, aufnehmerPython, leseStarter, rufeWerkzeug, schreibeNotiz, starteStarter, starterFakt, uvFassung } from "../zeigen-umgebung.js";
|
|
6
|
+
import { ermittleZustand, grundSatz, gueltigeFassung, ortHier, uhrzeit, vorab, zeigeOrdner } from "./zeigen.js";
|
|
7
|
+
/**
|
|
8
|
+
* `kimal3 zeigen start "<Satz>" [--hoechstens N]` (Spec US-3, US-4, US-5). Reihenfolge: Ort -> laeuft schon etwas? ->
|
|
9
|
+
* eingerichtet? -> Ordner -> Starter abgehaengt -> warten, bis er "laeuft, Sitzung …" meldet.
|
|
10
|
+
* 🔴 Der Satz geht nur nach `aufgabe.txt`, nie an den Aufnehmer (dessen Befehlszeile zerlegt ihn, Spec US-4).
|
|
11
|
+
* 🔴 Scheitert der Start, zieht `start` die Bremse (`abbrechen`) und wartet, bis der Starter das Ende bestaetigt -- ein
|
|
12
|
+
* Fehlschlag darf keinen Aufnehmer zuruecklassen. Gemeldet wird nur ein fester Grund und der Pfad zum Protokoll.
|
|
13
|
+
*/
|
|
14
|
+
const HOECHSTENS_VORGABE = 15;
|
|
15
|
+
const TAKT_MS = 500;
|
|
16
|
+
const WARTEN_START_MS = 60_000; // der Aufnehmer selbst wartet bis 60 s auf seine Bereitschaft
|
|
17
|
+
const WARTEN_ENDE_MS = 25_000; // R-14, Punkt 4: Wegraeumen samt Nachtoeten braucht laenger als 15 s
|
|
18
|
+
const warte = (ms) => new Promise((fertig) => setTimeout(fertig, ms));
|
|
19
|
+
const laeuftNoch = (zustand) => zustand === "startet" || zustand === "laeuft";
|
|
20
|
+
/** Bis `fertig` den Bericht des Starters annimmt (oder die Zeit um ist); der letzte gelesene Bericht. */
|
|
21
|
+
async function warteAufStarter(ordner, ms, fertig) {
|
|
22
|
+
const bis = Date.now() + ms;
|
|
23
|
+
let fakt = starterFakt(leseStarter(ordner));
|
|
24
|
+
while (!fertig(fakt) && Date.now() < bis) {
|
|
25
|
+
await warte(TAKT_MS);
|
|
26
|
+
fakt = starterFakt(leseStarter(ordner));
|
|
27
|
+
}
|
|
28
|
+
return fakt;
|
|
29
|
+
}
|
|
30
|
+
function legeOrdnerAn(t, aufnahmen, ordner, anzeige) {
|
|
31
|
+
try {
|
|
32
|
+
mkdirSync(aufnahmen, { recursive: true });
|
|
33
|
+
// Git UND Claudes Dateisuche fernhalten (Spec US-3); nur anlegen, was fehlt.
|
|
34
|
+
for (const name of [".gitignore", ".ignore"]) {
|
|
35
|
+
const datei = join(aufnahmen, name);
|
|
36
|
+
if (!existsSync(datei))
|
|
37
|
+
writeFileSync(datei, "*\n", "utf8");
|
|
38
|
+
}
|
|
39
|
+
mkdirSync(ordner); // ohne `recursive`: Gibt es ihn schon, war ein zweiter Start schneller (Review Focus 4)
|
|
40
|
+
return null;
|
|
41
|
+
}
|
|
42
|
+
catch (e) {
|
|
43
|
+
return e.code === "EEXIST" ? t.startetGerade(anzeige) : t.ordnerNichtAnlegbar(ordner);
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
export async function zeigenStart(rest) {
|
|
47
|
+
const lage = vorab(rest, (t) => t.helpStart);
|
|
48
|
+
if (typeof lage === "number")
|
|
49
|
+
return lage;
|
|
50
|
+
const { s, t } = lage;
|
|
51
|
+
const { woerter, optionen } = trenneNamenUndOptionen(rest);
|
|
52
|
+
const satz = woerter.join(" ").trim();
|
|
53
|
+
const z = zerlege(optionen, ["hoechstens"], s);
|
|
54
|
+
if (!z.ok || satz === "") {
|
|
55
|
+
console.error(`${z.ok ? t.satzFehlt : z.meldung}\n${t.helpStart}`);
|
|
56
|
+
return 1;
|
|
57
|
+
}
|
|
58
|
+
const roh = z.werte.optionen.hoechstens ?? String(HOECHSTENS_VORGABE);
|
|
59
|
+
const minuten = /^\d+$/.test(roh) ? Number(roh) : NaN;
|
|
60
|
+
if (!(minuten >= 1 && minuten <= HOECHSTENS_VORGABE)) {
|
|
61
|
+
console.error(t.hoechstensFalsch(roh));
|
|
62
|
+
return 1;
|
|
63
|
+
}
|
|
64
|
+
const jetzt = new Date();
|
|
65
|
+
const ort = ortHier(jetzt);
|
|
66
|
+
if (ort.art !== "ort") {
|
|
67
|
+
console.error(ort.art === "werkbank" ? t.ortWerkbank : t.ortZuLang(ort.laenge, ort.hoechstens));
|
|
68
|
+
return 1;
|
|
69
|
+
}
|
|
70
|
+
const p = aufnehmerPython();
|
|
71
|
+
if (p !== null) {
|
|
72
|
+
const { zustand } = ermittleZustand(p, ort, jetzt.getTime() / 1000);
|
|
73
|
+
if (zustand.art === "unbekannt") {
|
|
74
|
+
console.error(t.unbekanntKeinStart);
|
|
75
|
+
return 1;
|
|
76
|
+
}
|
|
77
|
+
if (zustand.art === "laeuft") {
|
|
78
|
+
const bekannt = !zustand.fremd && zustand.gestartet !== null && zustand.ordner !== null;
|
|
79
|
+
console.error(bekannt ? t.schonLaeuft(uhrzeit(zustand.gestartet), zeigeOrdner(zustand.ordner, ort)) : t.schonLaeuftFremd);
|
|
80
|
+
return 1;
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
const teile = p === null ? null : rufeWerkzeug(p, ["teile"], null, LESEN_MS);
|
|
84
|
+
const fehlt = !teile?.ok || einrichtPlan({
|
|
85
|
+
uv: uvFassung(),
|
|
86
|
+
aufnehmer: gueltigeFassung(teile.daten.aufnehmer),
|
|
87
|
+
ffmpeg: teile.daten.ffmpeg === true,
|
|
88
|
+
modell: teile.daten.modell === true,
|
|
89
|
+
}).length > 0;
|
|
90
|
+
if (p === null || fehlt) {
|
|
91
|
+
console.error(t.nichtEingerichtet);
|
|
92
|
+
return 1;
|
|
93
|
+
}
|
|
94
|
+
const ordner = ort.ordner;
|
|
95
|
+
const anzeige = zeigeOrdner(ordner, ort);
|
|
96
|
+
const absage = legeOrdnerAn(t, ort.aufnahmen, ordner, anzeige);
|
|
97
|
+
if (absage !== null) {
|
|
98
|
+
console.error(absage);
|
|
99
|
+
return 1;
|
|
100
|
+
}
|
|
101
|
+
const konfig = join(ordner, "konfig.json"); // kein-anzeigetext: Dateiname
|
|
102
|
+
try {
|
|
103
|
+
writeFileSync(join(ordner, "aufgabe.txt"), satz, "utf8"); // kein-anzeigetext: Dateiname
|
|
104
|
+
writeFileSync(konfig, JSON.stringify({ ordner, hoechstens_sek: minuten * 60 }), "utf8");
|
|
105
|
+
}
|
|
106
|
+
catch {
|
|
107
|
+
console.error(t.ordnerNichtAnlegbar(ordner));
|
|
108
|
+
return 1;
|
|
109
|
+
}
|
|
110
|
+
const protokoll = join(ordner, "aufnahme.log"); // kein-anzeigetext: Dateiname
|
|
111
|
+
let bericht = starteStarter(p, konfig, ordner)
|
|
112
|
+
? await warteAufStarter(ordner, WARTEN_START_MS, (f) => (f?.zustand === "laeuft" && f.sitzung !== undefined) || (f !== null && !laeuftNoch(f.zustand)))
|
|
113
|
+
: null;
|
|
114
|
+
const lauf = leseStarter(ordner);
|
|
115
|
+
if (bericht?.zustand === "laeuft" && bericht.sitzung !== undefined && typeof lauf?.gestartet === "number" && typeof lauf.frist === "number") {
|
|
116
|
+
schreibeNotiz({ sitzung: bericht.sitzung, ordner, gestartet: lauf.gestartet, frist: lauf.frist });
|
|
117
|
+
console.log(t.laeuftJetzt(anzeige, uhrzeit(lauf.frist)));
|
|
118
|
+
return 0;
|
|
119
|
+
}
|
|
120
|
+
// Nicht gestartet: Bremse ziehen und auf die Bestaetigung warten, dass nichts weiterlaeuft.
|
|
121
|
+
try {
|
|
122
|
+
writeFileSync(join(ordner, "abbrechen"), "", "utf8"); // kein-anzeigetext: Dateiname
|
|
123
|
+
}
|
|
124
|
+
catch {
|
|
125
|
+
/* ohne Bremse meldet unten "nicht bestaetigt" */
|
|
126
|
+
}
|
|
127
|
+
bericht = await warteAufStarter(ordner, WARTEN_ENDE_MS, (f) => f !== null && !laeuftNoch(f.zustand));
|
|
128
|
+
const grund = bericht?.grund ?? fehlerGrund(liesProtokoll(protokoll));
|
|
129
|
+
console.error(t.nichtGestartet(grundSatz(t, grund), protokoll));
|
|
130
|
+
// Ohne Bericht (Starter nie angelaufen) ist das Ende genauso wenig bestaetigt wie bei "laeuft noch".
|
|
131
|
+
if (bericht === null || laeuftNoch(bericht.zustand) || bericht.grund === "nicht-beendet")
|
|
132
|
+
console.error(t.endeUnbestaetigt);
|
|
133
|
+
return 1;
|
|
134
|
+
}
|
|
135
|
+
/** Nur fuer `fehlerGrund` -- das Protokoll traegt Fenstertitel und wird nie gezeigt. */
|
|
136
|
+
function liesProtokoll(pfad) {
|
|
137
|
+
try {
|
|
138
|
+
return readFileSync(pfad, "utf8");
|
|
139
|
+
}
|
|
140
|
+
catch {
|
|
141
|
+
return "";
|
|
142
|
+
}
|
|
143
|
+
}
|