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.
@@ -0,0 +1,288 @@
1
+ import { existsSync } from "node:fs";
2
+ import { platform } from "node:os";
3
+ import { dirname, isAbsolute, join, relative, resolve } from "node:path";
4
+ import { schalterOhneWert, trenneNamenUndOptionen, zerlege } from "../argumente.js";
5
+ import { sprache } from "../sprache.js";
6
+ import { ZEIGEN_TEXTE } from "../texte/zeigen.js";
7
+ import { AUFNEHMER_FASSUNG, aufnahmeOrt, aufnahmeZustand, bildName, dauerText, gueltigeFassung, starterLebt, stoppAusgang, zeitAusText, zustandOhneAufnehmer, } from "../zeigen-rechnung.js";
8
+ import { LESEN_MS, STOPPEN_MS, aufnehmerPython, juengsterOrdner, leseNotiz, leseStarter, loescheNotiz, rufeWerkzeug, starterFakt, uvFassung, } from "../zeigen-umgebung.js";
9
+ /** `--help` (auch auf dem Mac), dann die Plattform. Eine Zahl = fertig, sonst die Lage fuer den Befehl. */
10
+ export function vorab(rest, hilfe) {
11
+ const s = sprache();
12
+ const t = ZEIGEN_TEXTE[s];
13
+ if (rest.includes("--help") || rest.includes("-h")) {
14
+ console.log(hilfe(t));
15
+ return 0;
16
+ }
17
+ if (platform() !== "win32") {
18
+ console.error(t.nurWindows);
19
+ return 1;
20
+ }
21
+ return { s, t };
22
+ }
23
+ export const sekundenJetzt = () => Date.now() / 1000;
24
+ export const ortHier = (jetzt) => aufnahmeOrt(process.cwd(), jetzt, platform(), existsSync);
25
+ /** Ein Aufnahmeordner zum Zeigen: im Projekt relativ mit `/` (`material/aufnahmen/…`), sonst der volle Pfad. */
26
+ export function zeigeOrdner(ordner, ort) {
27
+ if (ort.art !== "ort")
28
+ return ordner;
29
+ const rel = relative(dirname(dirname(ort.aufnahmen)), ordner);
30
+ return rel === "" || rel.startsWith("..") || isAbsolute(rel) ? ordner : rel.split("\\").join("/");
31
+ }
32
+ /** Epochensekunden -> "09:12" in Ortszeit. */
33
+ export function uhrzeit(sek) {
34
+ const d = new Date(sek * 1000);
35
+ return `${String(d.getHours()).padStart(2, "0")}:${String(d.getMinutes()).padStart(2, "0")}`;
36
+ }
37
+ /** Ein Code vom Werkzeug -> fester Satz. Was nicht in `gruende` steht, wird nie gezeigt, sondern `unbekannt` (R-14, Punkt 9). */
38
+ export function grundSatz(t, code) {
39
+ return typeof code === "string" && Object.hasOwn(t.gruende, code) ? t.gruende[code] : t.gruende.unbekannt;
40
+ }
41
+ /** Wohnt seit Task 5 in `zeigen-rechnung.ts` (auch `einrichten` braucht sie); hier weitergereicht fuer `start`. */
42
+ export { gueltigeFassung };
43
+ const istZahl = (w) => typeof w === "number" && Number.isFinite(w);
44
+ const istZaehlung = (w) => istZahl(w) && Number.isInteger(w) && w >= 0;
45
+ /**
46
+ * Welcher Ordner gehoert zur Frage? Die Notiz zuerst, sonst die juengste Aufnahme im Projekt. Eine Notiz, deren Ordner
47
+ * es nicht mehr gibt, zaehlt nicht (`veraltet`): Ein Aufnehmer, der noch schreibt, haelt seinen Ordner fest.
48
+ */
49
+ function ordnerZurFrage(ort) {
50
+ const gelesen = leseNotiz();
51
+ const notiz = gelesen !== null && existsSync(gelesen.ordner) ? gelesen : null;
52
+ const kandidat = notiz?.ordner ?? (ort.art === "ort" ? juengsterOrdner(ort.aufnahmen) : null);
53
+ return { ordner: kandidat !== null && existsSync(kandidat) ? kandidat : null, notiz, veraltet: gelesen !== null && notiz === null };
54
+ }
55
+ /** Die aktiven Sitzungen aus einer `zustand`-Antwort; `null` = weiss nicht. */
56
+ function aktiveAus(r) {
57
+ const liste = r.ok ? r.daten.aktive_sitzungen : null;
58
+ return Array.isArray(liste) && liste.every((x) => typeof x === "string") ? liste : null;
59
+ }
60
+ /**
61
+ * Laeuft eine Aufnahme? Mit dem Python des Aufnehmers fragt `zustand` ihn selbst; ohne zaehlen nur Notiz und Starter
62
+ * (Ruling R-13, `zustandOhneAufnehmer`).
63
+ */
64
+ export function ermittleZustand(p, ort, jetzt) {
65
+ const { ordner, notiz } = ordnerZurFrage(ort);
66
+ const starterImNotizOrdner = notiz !== null && ordner === notiz.ordner;
67
+ if (p === null) {
68
+ const starter = ordner === null ? null : starterFakt(leseStarter(ordner));
69
+ return { zustand: zustandOhneAufnehmer({ notiz, starter, starterImNotizOrdner }, jetzt), ordner };
70
+ }
71
+ const r = rufeWerkzeug(p, ["zustand", ordner ?? "-"], ordner, LESEN_MS);
72
+ const starter = r.ok ? starterFakt(r.daten.starter) : null;
73
+ return { zustand: aufnahmeZustand({ aktiveSitzungen: aktiveAus(r), notiz, starter, starterImNotizOrdner }, jetzt), ordner };
74
+ }
75
+ // ── status ────────────────────────────────────────────────────────────────────────────────────────────────────────
76
+ export async function zeigenStatus(rest) {
77
+ const lage = vorab(rest, (t) => t.helpStatus);
78
+ if (typeof lage === "number")
79
+ return lage;
80
+ const { s, t } = lage;
81
+ const z = zerlege(rest, [], s);
82
+ if (!z.ok) {
83
+ console.error(`${z.meldung}\n${t.helpStatus}`);
84
+ return 1;
85
+ }
86
+ const jetzt = new Date();
87
+ const ort = ortHier(jetzt);
88
+ const p = aufnehmerPython();
89
+ const uv = uvFassung();
90
+ const zeilen = [t.eingerichtet, uv === null ? t.uvFehlt : t.uvDa(uv)];
91
+ const teile = p === null ? null : rufeWerkzeug(p, ["teile"], null, LESEN_MS);
92
+ if (p === null)
93
+ zeilen.push(t.aufnehmerFehlt, t.teilUngeprueft("ffmpeg"), t.teilUngeprueft("modell"));
94
+ else if (!teile?.ok)
95
+ zeilen.push(t.teilUnbekannt("aufnehmer"), t.teilUnbekannt("ffmpeg"), t.teilUnbekannt("modell"));
96
+ else {
97
+ const roh = teile.daten.aufnehmer;
98
+ const f = gueltigeFassung(roh);
99
+ zeilen.push(roh === null ? t.aufnehmerFehlt
100
+ : f === null ? t.teilUnbekannt("aufnehmer")
101
+ : f === AUFNEHMER_FASSUNG ? t.aufnehmerDa(f) : t.aufnehmerAlt(f, AUFNEHMER_FASSUNG));
102
+ zeilen.push(t.ffmpeg(teile.daten.ffmpeg === true), t.modell(teile.daten.modell === true));
103
+ }
104
+ const { zustand, ordner } = ermittleZustand(p, ort, jetzt.getTime() / 1000);
105
+ let code = 0;
106
+ zeilen.push(t.aufnahme);
107
+ if (zustand.art === "unbekannt") {
108
+ zeilen.push(` ${t.unbekannt}`);
109
+ code = 1;
110
+ }
111
+ else if (zustand.art === "laeuft") {
112
+ const { gestartet, frist } = zustand;
113
+ if (zustand.fremd || gestartet === null || frist === null || zustand.ordner === null)
114
+ zeilen.push(t.laeuftFremd);
115
+ else {
116
+ const minuten = Math.max(0, Math.floor(jetzt.getTime() / 60000 - gestartet / 60));
117
+ zeilen.push(t.laeuftSeit(uhrzeit(gestartet), minuten, zeigeOrdner(zustand.ordner, ort), uhrzeit(frist)));
118
+ }
119
+ }
120
+ else {
121
+ zeilen.push(t.keineLaeuft);
122
+ const roh = ordner === null ? null : leseStarter(ordner);
123
+ if (ordner !== null && zustand.letzte === "selbst-gestoppt") {
124
+ const min = istZahl(roh?.frist) && istZahl(roh?.gestartet) ? Math.round((roh.frist - roh.gestartet) / 60) : 15;
125
+ zeilen.push(t.letzteSelbst(zeigeOrdner(ordner, ort), min));
126
+ }
127
+ if (ordner !== null && zustand.letzte === "abgebrochen")
128
+ zeilen.push(t.letzteAbgebrochen(zeigeOrdner(ordner, ort)));
129
+ }
130
+ if (ort.art === "ort")
131
+ zeilen.push(t.ortHier(ort.aufnahmen));
132
+ else
133
+ zeilen.push(ort.art === "werkbank" ? t.ortWerkbank : t.ortZuLang(ort.laenge, ort.hoechstens));
134
+ console.log(zeilen.join("\n"));
135
+ return code;
136
+ }
137
+ // ── stopp ─────────────────────────────────────────────────────────────────────────────────────────────────────────
138
+ export async function zeigenStopp(rest) {
139
+ const lage = vorab(rest, (t) => t.helpStopp);
140
+ if (typeof lage === "number")
141
+ return lage;
142
+ const { s, t } = lage;
143
+ const z = zerlege(rest, [], s);
144
+ if (!z.ok) {
145
+ console.error(`${z.meldung}\n${t.helpStopp}`);
146
+ return 1;
147
+ }
148
+ const ort = ortHier(new Date());
149
+ const { ordner, notiz, veraltet } = ordnerZurFrage(ort);
150
+ if (veraltet)
151
+ loescheNotiz(); // ihr Ordner ist weg: die Notiz zeigt ins Leere
152
+ const p = aufnehmerPython();
153
+ // Eigene Aufnahme = eine Notiz, oder ein Starter, der noch "startet"/"laeuft" sagt. Eine alte, laengst gestoppte
154
+ // Aufnahme ohne Notiz ist keine laufende.
155
+ const starter = ordner === null ? null : starterFakt(leseStarter(ordner));
156
+ const eigene = ordner !== null && (notiz !== null || starter?.zustand === "startet" || starter?.zustand === "laeuft");
157
+ if (!eigene) {
158
+ // R-13: ohne Aufnehmer kann nichts laufen, was kimal3 gestartet hat. Mit ihm fragen, ob eine FREMDE laeuft (Review, Punkt 2).
159
+ const aktive = p === null ? [] : aktiveAus(rufeWerkzeug(p, ["zustand", "-"], null, LESEN_MS));
160
+ if (aktive === null)
161
+ console.error(t.unbekannt);
162
+ else if (aktive.length > 0)
163
+ console.error(t.fremdLaeuft);
164
+ else
165
+ console.log(t.keineAufnahmeLaeuft);
166
+ return aktive?.length === 0 ? 0 : 1;
167
+ }
168
+ if (p === null) {
169
+ // Eine eigene Aufnahme ist bekannt, der Aufnehmer aber nicht zu finden: stoppen kann ich nicht, "keine" waere gelogen.
170
+ console.error(t.stoppUnklar);
171
+ return 1;
172
+ }
173
+ const r = rufeWerkzeug(p, ["stoppen", ordner], ordner, STOPPEN_MS);
174
+ const d = r.ok ? r.daten : {};
175
+ const zahlen = istZahl(d.dauer_sek) && istZaehlung(d.klicks) && istZaehlung(d.tasten) && typeof d.ton === "boolean"
176
+ ? t.zahlen(dauerText(d.dauer_sek), d.klicks, d.tasten, d.ton) : null;
177
+ const ausgang = stoppAusgang(d.ergebnis, zahlen !== null);
178
+ // Werkzeug stumm, unlesbar oder noch beim Abschliessen: nichts ist bestaetigt -- die Notiz bleibt, damit status und
179
+ // der naechste stopp die Aufnahme weiter finden (Endpruefung, Punkt 2).
180
+ if (ausgang.notizWeg && notiz?.ordner === ordner)
181
+ loescheNotiz();
182
+ const roh = leseStarter(ordner);
183
+ const min = istZahl(roh?.frist) && istZahl(roh?.gestartet) ? Math.round((roh.frist - roh.gestartet) / 60) : 15;
184
+ const satz = {
185
+ keine: t.keineAufnahmeLaeuft,
186
+ gestoppt: t.gestoppt(zahlen ?? ""),
187
+ "selbst-gestoppt": t.selbstGestoppt(min, zahlen ?? ""),
188
+ "nicht-sauber": t.nichtSauber(grundSatz(t, d.grund)),
189
+ abgebrochen: t.abgebrochen(grundSatz(t, d.grund), zahlen),
190
+ "wird-abgeschlossen": t.wirdAbgeschlossen,
191
+ unklar: t.stoppUnklar,
192
+ }[ausgang.satz];
193
+ (ausgang.code === 0 ? console.log : console.error)(satz);
194
+ return ausgang.code;
195
+ }
196
+ // ── auswerten und bild: der Aufnahmeordner ───────────────────────────────────────────────────────────────────────
197
+ /** Der genannte oder der juengste Aufnahmeordner, der nicht mehr laeuft -- sonst ein Satz auf stderr und `null`. */
198
+ function fertigerOrdner(t, genannt, ort) {
199
+ const ordner = genannt !== undefined ? resolve(genannt) : ort.art === "ort" ? juengsterOrdner(ort.aufnahmen) : null;
200
+ if (ordner === null)
201
+ console.error(t.keineAufnahmeHier);
202
+ else if (!existsSync(ordner))
203
+ console.error(t.ordnerFehlt(ordner));
204
+ // Nur ein LEBENDER Starter blockiert (R-3): ein toter mit stehengebliebenem "laeuft" gilt als abgebrochen (Review, Punkt 3).
205
+ else if (starterLebt(starterFakt(leseStarter(ordner)), sekundenJetzt()))
206
+ console.error(t.laeuftNoch);
207
+ else
208
+ return ordner;
209
+ return null;
210
+ }
211
+ export async function zeigenAuswerten(rest) {
212
+ const lage = vorab(rest, (t) => t.helpAuswerten);
213
+ if (typeof lage === "number")
214
+ return lage;
215
+ const { s, t } = lage;
216
+ const ohne = schalterOhneWert(rest, "ohne-getipptes");
217
+ const { woerter, optionen } = trenneNamenUndOptionen(ohne.rest);
218
+ const z = zerlege([...woerter.slice(1), ...optionen], ["sprache"], s);
219
+ if (!z.ok) {
220
+ console.error(`${z.meldung}\n${t.helpAuswerten}`);
221
+ return 1;
222
+ }
223
+ const erkennung = z.werte.optionen.sprache ?? s;
224
+ if (erkennung !== "de" && erkennung !== "en") {
225
+ console.error(t.spracheFalsch(erkennung));
226
+ return 1;
227
+ }
228
+ const ort = ortHier(new Date());
229
+ const ordner = fertigerOrdner(t, woerter[0], ort);
230
+ if (ordner === null)
231
+ return 1;
232
+ const p = aufnehmerPython();
233
+ if (p === null) {
234
+ console.error(t.nichtEingerichtet);
235
+ return 1;
236
+ }
237
+ console.log(t.werteAus);
238
+ const r = rufeWerkzeug(p, ["zeitleiste", ordner, erkennung, ...(ohne.gesetzt ? ["--ohne-getipptes"] : [])], ordner, null);
239
+ const d = r.ok ? r.daten : {};
240
+ const datei = join(ordner, "zeitleiste.md"); // kein-anzeigetext: Dateiname
241
+ if (d.ergebnis === "fertig" && istZahl(d.dauer_sek) && [d.saetze, d.klicks, d.tippstellen, d.passwortfelder].every(istZaehlung) && existsSync(datei)) {
242
+ const n = (w) => w;
243
+ console.log(t.zeitleisteFertig(dauerText(d.dauer_sek), n(d.saetze), n(d.klicks), n(d.tippstellen), n(d.passwortfelder), zeigeOrdner(datei, ort)));
244
+ return 0;
245
+ }
246
+ if (d.ergebnis === "keine-sprache")
247
+ console.error(t.keineSprache);
248
+ else if (d.ergebnis === "kein-ton")
249
+ console.error(t.keinTon);
250
+ else
251
+ console.error(t.zeitleisteFehler(grundSatz(t, d.grund)));
252
+ return 1;
253
+ }
254
+ export async function zeigenBild(rest) {
255
+ const lage = vorab(rest, (t) => t.helpBild);
256
+ if (typeof lage === "number")
257
+ return lage;
258
+ const { s, t } = lage;
259
+ const { woerter, optionen } = trenneNamenUndOptionen(rest);
260
+ const z = zerlege([...woerter.slice(2), ...optionen], [], s);
261
+ if (!z.ok || woerter.length === 0) {
262
+ console.error(`${z.ok ? "" : `${z.meldung}\n`}${t.helpBild}`);
263
+ return 1;
264
+ }
265
+ const sekunden = zeitAusText(woerter[0]);
266
+ if (sekunden === null) {
267
+ console.error(t.zeitFalsch(woerter[0]));
268
+ return 1;
269
+ }
270
+ const ort = ortHier(new Date());
271
+ const ordner = fertigerOrdner(t, woerter[1], ort);
272
+ if (ordner === null)
273
+ return 1;
274
+ const p = aufnehmerPython();
275
+ if (p === null) {
276
+ console.error(t.nichtEingerichtet);
277
+ return 1;
278
+ }
279
+ const r = rufeWerkzeug(p, ["bild", ordner, String(sekunden)], ordner, null);
280
+ // Der Pfad wird hier gebaut, nicht vom Werkzeug uebernommen -- und muss danach wirklich daliegen.
281
+ const datei = join(ordner, "bilder", bildName(sekunden)); // kein-anzeigetext: Ordnername
282
+ if (r.ok && r.daten.ergebnis === "fertig" && existsSync(datei)) {
283
+ console.log(t.bildFertig(datei));
284
+ return 0;
285
+ }
286
+ console.error(t.bildFehler(grundSatz(t, r.ok ? r.daten.grund : null)));
287
+ return 1;
288
+ }
@@ -118,6 +118,20 @@ export const BEFEHLE_EN = {
118
118
  "What it's really for: Claude looks here BEFORE adding a project. Without that check, the same project would sooner or later appear twice on a public page that carries your name.",
119
119
  ],
120
120
  },
121
+ "kimal3 plan": {
122
+ stempel: "5a366449",
123
+ befehl: "kimal3 plan",
124
+ tippen: "/k3plan",
125
+ terminal: "kimal3 plan",
126
+ beschreibung: "Upload planning or fetch responses",
127
+ rhythmusOderSatz: "check my planning",
128
+ details: [
129
+ "kimal3 plan fetches what you've said in the app since the last upload: every “doesn't work” and every noted topic with its number, plus the confirmed stories. Claude sorts them and enters them in plan/stand.json.",
130
+ "kimal3 plan upload sends plan/stand.json to your project and then enters projekt_id and basis in the file itself. With --dry-run it only checks and saves nothing.",
131
+ "If the plan can't be read, a round goes down or the app already holds a newer plan, it refuses and saves nothing at all — the message says what to do. Use --overwrite only if you really mean it.",
132
+ "You'll know it worked when it reports with ✓ how many stories were uploaded and how many are waiting for your test, and gives the link to the planning.",
133
+ ],
134
+ },
121
135
  "kimal3 kurs": {
122
136
  stempel: "3a5c7600",
123
137
  befehl: "kimal3 course",
@@ -143,6 +157,33 @@ export const BEFEHLE_EN = {
143
157
  "⚠️ This is personal data about your colleagues. For use in a real company you need a commercial Claude account with a data processing agreement. If employees' performance is analysed, in Germany this requires the works council's agreement (co-determination).",
144
158
  ],
145
159
  },
160
+ "kimal3 zeigen start": {
161
+ stempel: "35a185f9",
162
+ befehl: "kimal3 show start",
163
+ tippen: "/k3show",
164
+ terminal: 'kimal3 show start "Send the open quotes to my boss every Monday"',
165
+ beschreibung: "Start a recording while you show a task",
166
+ rhythmusOderSatz: "I want to show you a task",
167
+ details: [
168
+ "You do a task once on screen and say out loud what you are doing and why. Claude guides you through it: first the briefing on what gets recorded, then the recording, then the evaluation on your computer.",
169
+ "All screens, sound, clicks and keys are recorded — so don't type a password. The recording lies in the project under material/aufnahmen/ and never ends up in Git; it stops by itself after 15 minutes at the latest.",
170
+ "Claude asks you before reading any of it: only after your yes does the timeline go to Anthropic. At the end the task is written down in the project's plan folder, and Claude suggests deleting the recording.",
171
+ "Windows-only for now.",
172
+ ],
173
+ },
174
+ "kimal3 zeigen status": {
175
+ stempel: "7ebd2b8f",
176
+ befehl: "kimal3 show status",
177
+ tippen: "! kimal3 show status",
178
+ terminal: "kimal3 show status",
179
+ beschreibung: "Check whether a recording is running and whether the recorder is set up",
180
+ rhythmusOderSatz: "when you want to know whether something is being recorded",
181
+ details: [
182
+ "Shows two blocks: what is set up (uv, recorder, FFmpeg, speech model) and whether a recording of your screen is running — with start time, folder and the time at which it stops by itself at the latest.",
183
+ "If it can't tell whether something is being recorded, it says exactly that, with exit code 1 — it then never claims that nothing is running.",
184
+ "It only reads and changes nothing. Windows-only for now.",
185
+ ],
186
+ },
146
187
  "kimal3 wache": {
147
188
  stempel: "2daae4e2",
148
189
  befehl: "kimal3 watch",
@@ -167,6 +167,24 @@ export const BEFEHLE = [
167
167
  "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.",
168
168
  ],
169
169
  },
170
+ /*
171
+ `kimal3 plan` und `kimal3 plan hoch` (Spec 2026-10-21-projektseite-planung) -- EIN Eintrag fuer beide: Claude
172
+ ruft sie nacheinander auf, und ein Mensch sucht hier nach der Planung, nicht nach zwei Befehlen.
173
+ */
174
+ {
175
+ befehl: "kimal3 plan",
176
+ beschreibung: "Planung hochladen oder Rückmeldungen holen",
177
+ wer: "claude",
178
+ ausloeser: "prüf meine Planung",
179
+ tippen: "/k3plan",
180
+ terminal: "kimal3 plan",
181
+ details: [
182
+ "kimal3 plan holt aus der App, was du seit dem letzten Hochladen gesagt hast: jedes „passt nicht“ und jedes notierte Thema mit seiner Nummer, dazu die bestätigten Stories. Claude ordnet sie ein und trägt sie in plan/stand.json ein.",
183
+ "kimal3 plan hoch schickt plan/stand.json an dein Projekt und trägt danach projekt_id und basis selbst in die Datei ein. Mit --probe prüft er nur und speichert nichts.",
184
+ "Ist der Plan nicht lesbar, wird eine Runde kleiner oder liegt in der App schon ein neuerer Plan, lehnt er ab und speichert gar nichts — die Meldung sagt, was zu tun ist. --ueberschreiben nur, wenn du es wirklich willst.",
185
+ "Geklappt hat es, wenn er mit ✓ meldet, wie viele Stories hochgeladen sind und wie viele auf deinen Test warten, und den Link zur Planung nennt.",
186
+ ],
187
+ },
170
188
  {
171
189
  befehl: "kimal3 kurs",
172
190
  beschreibung: "Zeigen, wo du im Mini-Kurs stehst",
@@ -192,6 +210,33 @@ export const BEFEHLE = [
192
210
  "⚠️ 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.",
193
211
  ],
194
212
  },
213
+ {
214
+ befehl: "kimal3 zeigen start",
215
+ beschreibung: "Eine Aufnahme starten, während du eine Aufgabe vormachst",
216
+ wer: "claude",
217
+ ausloeser: "ich will dir eine Aufgabe vormachen",
218
+ tippen: "/k3zeigen",
219
+ terminal: 'kimal3 zeigen start "Offene Angebote jeden Montag an die Chefin schicken"',
220
+ details: [
221
+ "Du machst eine Aufgabe einmal am Bildschirm vor und sagst dabei laut, was du tust und warum. Claude führt dich durch: vorher die Ansage, was aufgenommen wird, dann die Aufnahme, danach das Auswerten auf deinem Rechner.",
222
+ "Aufgenommen werden alle Bildschirme, Ton, Klicks und Tasten — tippe deshalb kein Passwort. Die Aufnahme liegt im Projekt unter material/aufnahmen/ und landet nie in Git; spätestens nach 15 Minuten stoppt sie von selbst.",
223
+ "Bevor Claude etwas davon liest, fragt er dich: Erst nach deinem Ja geht die Zeitleiste an Anthropic. Am Ende steht die Aufgabe in plan/aufgabe-vorgefuehrt.md, und Claude schlägt vor, die Aufnahme zu löschen.",
224
+ "Gibt es bisher nur unter Windows.",
225
+ ],
226
+ },
227
+ {
228
+ befehl: "kimal3 zeigen status",
229
+ beschreibung: "Nachsehen, ob gerade aufgenommen wird und ob der Aufnehmer eingerichtet ist",
230
+ wer: "du",
231
+ rhythmus: "wenn du wissen willst, ob gerade aufgenommen wird",
232
+ tippen: "! kimal3 zeigen status",
233
+ terminal: "kimal3 zeigen status",
234
+ details: [
235
+ "Zeigt zwei Blöcke: was eingerichtet ist (uv, Aufnehmer, FFmpeg, Sprachmodell) und ob gerade eine Aufnahme von deinem Bildschirm läuft — mit Startzeit, Ordner und der Uhrzeit, zu der sie spätestens von selbst stoppt.",
236
+ "Kann es nicht feststellen, ob aufgenommen wird, sagt es genau das, mit Rückgabewert 1 — es behauptet dann nie, dass nichts läuft.",
237
+ "Es liest nur und ändert nichts. Gibt es bisher nur unter Windows.",
238
+ ],
239
+ },
195
240
  /*
196
241
  🔴 Dieser Eintrag steht bewusst NICHT im "Weg, den ein Mensch geht" --
197
242
  niemand muss ihn selbst tippen. Er wird bei jedem Start von Claude Code
@@ -10,11 +10,21 @@ export const BEFEHLSWOERTER = {
10
10
  "kontext fragen": { de: "kontext fragen", en: "context questions" }, // kein-anzeigetext: Befehlswörter (Spec Abschnitt 6)
11
11
  "projekt neu": { de: "projekt neu", en: "project new" }, // kein-anzeigetext: Befehlswörter (Spec Abschnitt 6)
12
12
  "projekt liste": { de: "projekt liste", en: "project list" }, // kein-anzeigetext: Befehlswörter (Spec Abschnitt 6)
13
+ // `kimal3 plan` (Spec 2026-10-21-projektseite-planung, US-9): `plan` heisst in beiden Sprachen gleich.
14
+ plan: { de: "plan", en: "plan" },
15
+ "plan hoch": { de: "plan hoch", en: "plan upload" }, // kein-anzeigetext: Befehlswörter (Spec projektseite-planung)
13
16
  "firma daten": { de: "firma daten", en: "company data" }, // kein-anzeigetext: Befehlswörter (Spec Abschnitt 6)
14
17
  kurs: { de: "kurs", en: "course" }, // kein-anzeigetext: Befehlswörter (Spec Abschnitt 6)
15
18
  wache: { de: "wache", en: "watch" },
16
19
  "wache an": { de: "wache an", en: "watch on" }, // kein-anzeigetext: Befehlswörter (Spec Abschnitt 6)
17
20
  "wache aus": { de: "wache aus", en: "watch off" }, // kein-anzeigetext: Befehlswörter (Spec Abschnitt 6)
21
+ // `kimal3 zeigen` / `show` (Spec 2026-10-18-k3zeigen-design.md, Abschnitt 1)
22
+ "zeigen einrichten": { de: "zeigen einrichten", en: "show setup" }, // kein-anzeigetext: Befehlswörter (Spec k3zeigen)
23
+ "zeigen start": { de: "zeigen start", en: "show start" }, // kein-anzeigetext: Befehlswörter (Spec k3zeigen)
24
+ "zeigen stopp": { de: "zeigen stopp", en: "show stop" }, // kein-anzeigetext: Befehlswörter (Spec k3zeigen)
25
+ "zeigen auswerten": { de: "zeigen auswerten", en: "show evaluate" }, // kein-anzeigetext: Befehlswörter (Spec k3zeigen)
26
+ "zeigen bild": { de: "zeigen bild", en: "show picture" }, // kein-anzeigetext: Befehlswörter (Spec k3zeigen)
27
+ "zeigen status": { de: "zeigen status", en: "show status" }, // kein-anzeigetext: Befehlswörter (Spec k3zeigen)
18
28
  };
19
29
  /**
20
30
  * Welcher Befehl steht vorne in `argv` -- und was bleibt dahinter?
@@ -60,6 +70,12 @@ export const SCHALTER_EN = {
60
70
  // `kimal3 einrichten` / `setup` (Spec 2026-10-16, Bauform 6)
61
71
  workplace: "arbeitsplatz",
62
72
  workbench: "werkbank",
73
+ // `kimal3 show …` (Spec 2026-10-18-k3zeigen-design.md)
74
+ "at-most": "hoechstens",
75
+ language: "sprache",
76
+ "without-typed": "ohne-getipptes",
77
+ // `kimal3 plan upload` (Spec 2026-10-21-projektseite-planung, US-11); `--overwrite` steht schon oben.
78
+ "dry-run": "probe",
63
79
  };
64
80
  /** `company` -> `firma`; ein deutscher (oder unbekannter) Name bleibt, wie er ist. */
65
81
  export function deutscherSchalter(getippt) {
package/dist/kimal3.js CHANGED
@@ -36,8 +36,13 @@ import { logout } from "./befehle/logout.js";
36
36
  import { kurs } from "./befehle/kurs.js";
37
37
  import { projektListe } from "./befehle/projekt-liste.js";
38
38
  import { projektNeu } from "./befehle/projekt-neu.js";
39
+ import { plan } from "./befehle/plan.js";
40
+ import { planHoch } from "./befehle/plan-hoch.js";
39
41
  import { skillDatei, skillInstallBefehl } from "./befehle/skill-install.js";
40
42
  import { wache, wacheSchalten } from "./befehle/wache.js";
43
+ import { zeigenAuswerten, zeigenBild, zeigenStatus, zeigenStopp } from "./befehle/zeigen.js";
44
+ import { zeigenStart } from "./befehle/zeigen-start.js";
45
+ import { zeigenEinrichten } from "./befehle/zeigen-einrichten.js";
41
46
  /** Eine Meldung pro Programmlauf -- s. "Einmal-Riegel" im Block darunter. */
42
47
  let schonGemeldet = false;
43
48
  /**
@@ -484,6 +489,9 @@ async function fuehreAus(name, rest) {
484
489
  case "skill install": return allein ? skillInstallBefehl() : null;
485
490
  case "projekt neu": return projektNeu(rest);
486
491
  case "projekt liste": return allein ? projektListe() : null;
492
+ // `plan` nimmt nur `--help`; ein Wort dahinter (`plan muell`) bleibt unbekannt wie bei `login extra`.
493
+ case "plan": return allein || (rest.length === 1 && (rest[0] === "--help" || rest[0] === "-h")) ? plan(rest) : null;
494
+ case "plan hoch": return planHoch(rest);
487
495
  case "kurs": return allein ? kurs() : null;
488
496
  // ⚠️ Rest wird durchgereicht, nicht auf Gleichheit geprueft: `firma daten --firma X` gehoert dazu.
489
497
  case "firma daten": return firmaDaten(rest);
@@ -493,6 +501,13 @@ async function fuehreAus(name, rest) {
493
501
  case "kontext runter": return kontextRunter(rest);
494
502
  case "kontext hoch": return kontextHoch(rest);
495
503
  case "kontext fragen": return kontextFragen(rest);
504
+ // `kimal3 zeigen` (Spec 2026-10-18-k3zeigen-design.md).
505
+ case "zeigen einrichten": return zeigenEinrichten(rest);
506
+ case "zeigen start": return zeigenStart(rest);
507
+ case "zeigen stopp": return zeigenStopp(rest);
508
+ case "zeigen auswerten": return zeigenAuswerten(rest);
509
+ case "zeigen bild": return zeigenBild(rest);
510
+ case "zeigen status": return zeigenStatus(rest);
496
511
  case "wache an": return allein ? wacheSchalten(true) : null;
497
512
  case "wache aus": return allein ? wacheSchalten(false) : null;
498
513
  case "wache":
@@ -2,6 +2,7 @@ import { cpSync, existsSync, readdirSync, readFileSync, renameSync, rmSync, writ
2
2
  import { homedir } from "node:os";
3
3
  import { basename, dirname, join } from "node:path";
4
4
  import { fileURLToPath } from "node:url";
5
+ import { EINRICHTEN_TEXTE } from "./texte/einrichten.js";
5
6
  /**
6
7
  * Legt den Plugin-Katalog mit dem Logo-Mod in `~/.kimal3/claude-plugins` ab und setzt dort die
7
8
  * Sprache des Kontos. Claude Code liest den Mod aus genau diesem Ordner (Katalog `kimal3`,
@@ -34,8 +35,9 @@ export function mitSprache(quelltext, sprache) {
34
35
  * 🔑 Der weggeschobene Ordner bekommt bei JEDEM Lauf einen frischen Namen (Durchsicht 2, Funde 2 und 4):
35
36
  * Ein Rest, den Windows festhält, blockiert so keinen späteren Lauf, und zurückgeholt wird nur, was
36
37
  * DIESER Lauf weggeschoben hat -- nie ein veralteter Rest von früher.
37
- * Lässt sich der alte nicht wegschieben (Claude hält ihn manchmal fest), bleibt er unversehrt stehen und
38
- * gilt weiter: ein Erfolg mit `alterStand`, den `kimal3 einrichten` in einer Zeile nennt.
38
+ * Lässt sich der alte nicht wegschieben, bleibt er unversehrt stehen und gilt weiter: ein Erfolg mit `stand: "alt"`,
39
+ * den der Aufrufer in einer Zeile nennt. Unter Windows hält ihn ein Programm fest, dessen Arbeitsordner darin liegt;
40
+ * Claude selbst tut es nicht (gemessen 18.10. bei acht laufenden Claude-Prozessen, Spec 2026-10-18, Abschnitt 1.2).
39
41
  */
40
42
  export function logoAblegen(sprache) {
41
43
  // `dist/logo-ablegen.js` -> `../claude-plugins` im installierten Paket (`files` in package.json).
@@ -51,14 +53,14 @@ export function logoAblegen(sprache) {
51
53
  // Auch im Normalfall aufräumen, samt einem halb gebauten `….neu`: sonst blieben Reste für immer (Durchsicht 3/4).
52
54
  rmSync(neu, { recursive: true, force: true });
53
55
  resteWegraeumen(ziel);
54
- return { ok: true, ordner: ziel };
56
+ return { ok: true, ordner: ziel, stand: "gleich" };
55
57
  }
56
58
  rmSync(neu, { recursive: true, force: true });
57
59
  cpSync(quelle, neu, { recursive: true });
58
60
  writeFileSync(modDatei(neu), text, "utf8");
59
61
  if (!existsSync(ziel)) {
60
62
  renameSync(neu, ziel);
61
- return { ok: true, ordner: ziel };
63
+ return { ok: true, ordner: ziel, stand: "neu" };
62
64
  }
63
65
  const alt = `${ziel}.alt-${Date.now()}`; // kein-anzeigetext: Ordnername
64
66
  try {
@@ -66,7 +68,7 @@ export function logoAblegen(sprache) {
66
68
  }
67
69
  catch {
68
70
  rmSync(neu, { recursive: true, force: true });
69
- return { ok: true, ordner: ziel, alterStand: true };
71
+ return { ok: true, ordner: ziel, stand: "alt" };
70
72
  }
71
73
  try {
72
74
  renameSync(neu, ziel);
@@ -76,12 +78,30 @@ export function logoAblegen(sprache) {
76
78
  return { ok: false, detail: e.message };
77
79
  }
78
80
  resteWegraeumen(ziel);
79
- return { ok: true, ordner: ziel };
81
+ return { ok: true, ordner: ziel, stand: "neu" };
80
82
  }
81
83
  catch (e) {
82
84
  return { ok: false, detail: e.message };
83
85
  }
84
86
  }
87
+ /**
88
+ * Für `login` und `skill install` (Spec 2026-10-18, Abschnitt 1.2): ein VORHANDENES Logo auffrischen (Update,
89
+ * Sprachwechsel), nie eines anlegen -- das darf nur `einrichten`, wie beim Coach (`frischeCoachAuf`). Wer das Logo nie
90
+ * eingerichtet hat, hört nichts und bekommt keinen Ordner (L-2). Eine Zeile nur, wenn getauscht wurde, das alte bleiben
91
+ * musste oder etwas scheiterte; sonst Stille. Wirft nie und ändert den Rückgabewert des Befehls nicht (L-6).
92
+ */
93
+ export function frischeLogoAuf(s) {
94
+ if (!existsSync(katalogOrdner()))
95
+ return;
96
+ const ablage = logoAblegen(s);
97
+ const t = EINRICHTEN_TEXTE[s];
98
+ if (!ablage.ok)
99
+ console.log(t.logoNotWritten(ablage.detail));
100
+ else if (ablage.stand === "neu")
101
+ console.log(t.logoRefreshed);
102
+ else if (ablage.stand === "alt")
103
+ console.log(t.logoNotRefreshed);
104
+ }
85
105
  /** Gleiche Dateien, gleicher Inhalt -- der Mod mit der Sprache des Kontos, alles andere wie im Paket. */
86
106
  function stehtSchonDa(quelle, ziel, modText) {
87
107
  if (!existsSync(ziel))