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,172 @@
1
+ /**
2
+ * `plan/stand.json` lesen und nach dem Hochladen nachtragen (Spec 2026-10-21-projektseite-planung, Abschnitt 3).
3
+ *
4
+ * 🔑 Die Datei gehoert Claude und liegt unter Git -- diese Datei prueft sie NICHT. Ob ein Plan stimmt,
5
+ * entscheidet allein der Server (`lesePlanText`); hier geht es nur darum, ob es ueberhaupt ein JSON-Objekt
6
+ * ist, damit ein Tippfehler im Klammerwerk ohne Netz und ohne Anmeldung auffaellt.
7
+ *
8
+ * ⚠️ Ein BOM am Anfang (Windows-Editoren setzen eines) wird beim Lesen entfernt: `JSON.parse` lehnt es ab,
9
+ * und der Server meldete sonst „kein JSON" fuer eine Datei, die im Editor einwandfrei aussieht.
10
+ */
11
+ import { readFileSync, realpathSync, statSync, writeFileSync } from "node:fs";
12
+ import { isAbsolute, join, relative, resolve } from "node:path";
13
+ export const PLAN_PFAD = "plan/stand.json"; // kein-anzeigetext: Dateipfad
14
+ const ohneBom = (text) => (text.startsWith("\uFEFF") ? text.slice(1) : text);
15
+ function alsObjekt(text) {
16
+ try {
17
+ const json = JSON.parse(text);
18
+ return json !== null && typeof json === "object" && !Array.isArray(json) ? json : null;
19
+ }
20
+ catch {
21
+ return null;
22
+ }
23
+ }
24
+ export function lesePlanDatei(ordner) {
25
+ let text;
26
+ try {
27
+ text = ohneBom(readFileSync(join(ordner, PLAN_PFAD), "utf8"));
28
+ }
29
+ catch {
30
+ return { ok: false, grund: "fehlt" };
31
+ }
32
+ const json = alsObjekt(text);
33
+ return json === null ? { ok: false, grund: "kein-json" } : { ok: true, text, json };
34
+ }
35
+ /**
36
+ * Traegt `projekt_id` und `basis` ein und leert `uebernommen` -- alles andere bleibt, wie es ist.
37
+ * 🔑 `uebernommenLeeren` ist PFLICHT, nicht optional: Nach `503 teilweise` ist der Plan gespeichert, die
38
+ * Rueckmeldungen aber noch nicht markiert (Ruling 10). Dann muss `basis` neu, `uebernommen` aber stehen
39
+ * bleiben -- sonst gingen sie beim zweiten Lauf verloren, und mit altem `basis` lehnte er als „veraltet" ab.
40
+ * Fehlen die beiden Felder, kommen sie hinter `projekt` (dort stehen sie im Beispiel der Spec), nicht ans Ende.
41
+ * Eingerueckt mit 2 Leerzeichen; Windows-Zeilenenden bleiben Windows-Zeilenenden.
42
+ * `false` heisst: nicht geschrieben (Datei weg, kein JSON mehr, Schreibschutz).
43
+ */
44
+ export function schreibeNachHochladen(ordner, projektId, hochgeladenAm, uebernommenLeeren) {
45
+ const pfad = join(ordner, PLAN_PFAD);
46
+ try {
47
+ const roh = readFileSync(pfad, "utf8");
48
+ const alt = alsObjekt(ohneBom(roh));
49
+ if (alt === null)
50
+ return false;
51
+ const neu = {};
52
+ for (const [schluessel, wert] of Object.entries(alt)) {
53
+ neu[schluessel] = wert;
54
+ if (schluessel === "projekt") {
55
+ neu.projekt_id = projektId;
56
+ neu.basis = hochgeladenAm;
57
+ }
58
+ }
59
+ neu.projekt_id = projektId;
60
+ neu.basis = hochgeladenAm;
61
+ if (uebernommenLeeren)
62
+ neu.uebernommen = [];
63
+ const text = `${JSON.stringify(neu, null, 2)}\n`;
64
+ writeFileSync(pfad, roh.includes("\r\n") ? text.replace(/\n/g, "\r\n") : text, "utf8");
65
+ return true;
66
+ }
67
+ catch {
68
+ return false;
69
+ }
70
+ }
71
+ export const SPEC_TEXT_GRENZE_JE = 300 * 1024; // kein-anzeigetext: Bytes
72
+ export const SPEC_TEXT_GRENZE_ZUSAMMEN = 4 * 1024 * 1024; // kein-anzeigetext: Bytes
73
+ /**
74
+ * Darf diese Datei gelesen werden? Nur ein relativer Pfad, der nach dem Aufloesen (auch einer Verknuepfung) im
75
+ * Projektordner bleibt und auf `.md` endet. 🔴 Positivliste: erst alles ausser dem Erlaubten abweisen -- die
76
+ * Schreibweisen fuer „woanders" (Laufwerk, Netzwerkpfad, Wurzel, `..`) sind nicht zu Ende aufzaehlbar.
77
+ * `wirklich` ist `realpathSync` (im Test ersetzt) und liefert `null`, wo es die Datei nicht gibt.
78
+ */
79
+ export function specPfadPruefen(ordner, datei, wirklich) {
80
+ if (datei === null || datei.trim() === "")
81
+ return { ok: false, grund: "keine_datei" };
82
+ if (!/\.md$/i.test(datei))
83
+ return { ok: false, grund: "ausserhalb" };
84
+ // Laufwerk, Wurzel und Netzwerkpfad gelten auf JEDEM System als „woanders", auch wo `isAbsolute` sie nicht kennt.
85
+ if (isAbsolute(datei) || /^[A-Za-z]:/.test(datei) || /^[\\/]/.test(datei))
86
+ return { ok: false, grund: "ausserhalb" };
87
+ const innen = (wurzel, ziel) => {
88
+ const r = relative(wurzel, ziel);
89
+ return r !== "" && r.split(/[\\/]/)[0] !== ".." && !isAbsolute(r);
90
+ };
91
+ const ziel = resolve(ordner, datei);
92
+ if (!innen(resolve(ordner), ziel))
93
+ return { ok: false, grund: "ausserhalb" };
94
+ const echt = wirklich(ziel);
95
+ if (echt !== null && !innen(wirklich(resolve(ordner)) ?? resolve(ordner), echt))
96
+ return { ok: false, grund: "ausserhalb" };
97
+ return { ok: true, pfad: echt ?? ziel };
98
+ }
99
+ const wirklichOderNull = (p) => {
100
+ try {
101
+ return realpathSync(p);
102
+ }
103
+ catch {
104
+ return null;
105
+ }
106
+ };
107
+ /** `stand.json` -> die Koepfe der Specs; Unlesbares wird uebersprungen (ob der Plan stimmt, sagt allein der Server). */
108
+ export function specKoepfeAus(json) {
109
+ if (!Array.isArray(json.specs))
110
+ return [];
111
+ return json.specs.flatMap((s) => {
112
+ if (!s || typeof s !== "object")
113
+ return [];
114
+ const r = s;
115
+ if (typeof r.schluessel !== "string")
116
+ return [];
117
+ return [{ schluessel: r.schluessel, titel: typeof r.titel === "string" ? r.titel : r.schluessel, datei: typeof r.datei === "string" ? r.datei : null }];
118
+ });
119
+ }
120
+ /** Wie gross der Text im JSON-Rumpf wird (mit Maskierung) -- danach wird der groesste gewaehlt, der herausfaellt. */
121
+ const rumpfBytes = (text) => Buffer.byteLength(JSON.stringify(text), "utf8");
122
+ /**
123
+ * Liest den Text jeder Spec. Jede Spec bekommt GENAU einen Eintrag: den Text oder den Grund, warum keiner da ist.
124
+ * Passt der fertige Anfrage-Rumpf nicht in 4 MB, werden die groessten Texte zu `zu_gross`, bis er passt (US-16:
125
+ * „am fertigen Anfrage-Rumpf"). `rumpf` baut genau den Rumpf, den `plan hoch` schickt (Stand, Schalter, Texte) --
126
+ * dieselbe Funktion baut ihn dort auch, damit gemessen wird, was hinausgeht (Befund M-3).
127
+ */
128
+ export function leseSpecTexte(ordner, specs, rumpf) {
129
+ const keineTextdatei = new Set();
130
+ const eintraege = specs.map((s, i) => {
131
+ const ohne = (grund) => ({ schluessel: s.schluessel, grund, pfad: s.datei });
132
+ const pruefung = specPfadPruefen(ordner, s.datei, wirklichOderNull);
133
+ if (!pruefung.ok)
134
+ return ohne(pruefung.grund);
135
+ try {
136
+ const status = statSync(pruefung.pfad);
137
+ if (!status.isFile())
138
+ return ohne("fehlt");
139
+ if (status.size > SPEC_TEXT_GRENZE_JE)
140
+ return ohne("zu_gross");
141
+ const text = readFileSync(pruefung.pfad, "utf8");
142
+ if (Buffer.byteLength(text, "utf8") > SPEC_TEXT_GRENZE_JE)
143
+ return ohne("zu_gross");
144
+ // Ein Nullzeichen nimmt Postgres in `text` nicht an -- der Server lehnte den ganzen Aufruf ab (Befund M-1).
145
+ if (text.includes("\u0000")) {
146
+ keineTextdatei.add(i);
147
+ return ohne("fehlt");
148
+ }
149
+ return { schluessel: s.schluessel, text: ohneBom(text) };
150
+ }
151
+ catch {
152
+ return ohne("fehlt");
153
+ }
154
+ });
155
+ const groesse = () => Buffer.byteLength(JSON.stringify(rumpf(eintraege)), "utf8");
156
+ while (groesse() > SPEC_TEXT_GRENZE_ZUSAMMEN) {
157
+ let groesster = -1;
158
+ let groessterBytes = -1;
159
+ eintraege.forEach((e, i) => {
160
+ const b = "text" in e ? rumpfBytes(e.text) : -1;
161
+ if (b > groessterBytes) {
162
+ groesster = i;
163
+ groessterBytes = b;
164
+ }
165
+ });
166
+ if (groesster < 0)
167
+ break;
168
+ eintraege[groesster] = { schluessel: specs[groesster].schluessel, grund: "zu_gross", pfad: specs[groesster].datei };
169
+ }
170
+ const ohne = eintraege.flatMap((e, i) => "grund" in e ? [{ titel: specs[i].titel, grund: keineTextdatei.has(i) ? "keine_textdatei" : e.grund, pfad: e.pfad }] : []);
171
+ return { eintraege, ohne };
172
+ }
@@ -1,9 +1,11 @@
1
1
  export const SLASH_PAARE = [
2
2
  { de: "k3", en: "k3" },
3
3
  { de: "k3start", en: "k3start" },
4
+ { de: "k3zeigen", en: "k3show" }, // kein-anzeigetext: Befehlsname (Spec 2026-10-18-k3zeigen-design.md)
4
5
  { de: "k3feierabend", en: "k3wrapup" }, // kein-anzeigetext: Befehlsname (Spec Teil 4, Abschnitt 6)
5
6
  { de: "k3kontext", en: "k3context" }, // kein-anzeigetext: Befehlsname
6
7
  { de: "k3projekt", en: "k3project" }, // kein-anzeigetext: Befehlsname
8
+ { de: "k3plan", en: "k3plan" }, // kein-anzeigetext: Befehlsname, in beiden Sprachen gleich (Spec 2026-10-21-projektseite-planung, C-7)
7
9
  { de: "k3liste", en: "k3list" }, // kein-anzeigetext: Befehlsname
8
10
  { de: "k3analyse", en: "k3analyse" }, // britisch, in beiden Sprachen gleich
9
11
  { de: "k3kurs", en: "k3course" }, // kein-anzeigetext: Befehlsname
@@ -50,11 +50,16 @@ const de = {
50
50
  settingsInvalid: "deine Claude-Einstellungen (~/.claude/settings.json) sind kein gültiges JSON. Ich habe sie nicht angefasst.",
51
51
  settingsForeign: "in deinen Claude-Einstellungen (~/.claude/settings.json) steht etwas in einer Form, die ich nicht kenne. Ich habe sie nicht angefasst.",
52
52
  settingsFailed: (ursache) => `deine Claude-Einstellungen (~/.claude/settings.json): ${ursache}.`,
53
- // Logo (Durchsicht 2, Funde 1 und 3): Scheitert der Ordner, kommt superpowers trotzdem; hält Claude ihn fest, gilt die alte Fassung.
53
+ // Logo (Durchsicht 2, Funde 1 und 3): Scheitert der Ordner, kommt superpowers trotzdem; hält ein anderes Programm ihn fest, bleibt die alte Fassung (`logoOldVersion`).
54
54
  logoNotWritten: (detail) => ` Den Ordner für das KImal3-Logo konnte ich nicht anlegen oder erneuern (${detail}).`,
55
55
  // Ohne Git kein superpowers (Durchsicht 3, Fund 5); die Anleitung zitiert den Anfang dieser Zeile.
56
56
  superpowersNeedsGit: " superpowers braucht Git — es kommt mit dem nächsten `kimal3 einrichten`, sobald Git da ist.",
57
- logoOldVersion: " Das KImal3-Logo bleibt vorerst in der bisherigen Fassung, weil Claude den Ordner gerade benutzt. Beende Claude ganz und starte danach `kimal3 einrichten` noch einmal.",
57
+ // Die Sperre setzt ein anderes Programm mit Arbeitsordner im Logo-Ordner, nicht Claude -- deshalb kein Rat, Claude zu
58
+ // beenden (für App-Nutzer ein Kreis; Spec 2026-10-18, Abschnitt 1.2).
59
+ logoOldVersion: " Das Logo konnte gerade nicht aufgefrischt werden, weil ein anderes Programm seinen Ordner benutzt. Es kommt beim nächsten `kimal3 einrichten`.",
60
+ // Auffrischen bei `login` und `skill install` (`frischeLogoAuf`); scheitert es, sagt es `logoNotWritten`.
61
+ logoRefreshed: " Das Logo über dem Eingabefeld ist aufgefrischt. Ist es eingeschaltet, siehst du es nach dem nächsten Neustart von Claude.",
62
+ logoNotRefreshed: " Das Logo konnte gerade nicht aufgefrischt werden, weil ein anderes Programm seinen Ordner benutzt. Es kommt beim nächsten `kimal3 skill install`.",
58
63
  };
59
64
  const en = {
60
65
  codeLine: (code) => `\n ${M.en.code} ${code}`,
@@ -101,6 +106,8 @@ const en = {
101
106
  settingsFailed: (ursache) => `your Claude settings (~/.claude/settings.json): ${ursache}.`,
102
107
  logoNotWritten: (detail) => ` I couldn't create or renew the folder for the KImal3 logo (${detail}).`,
103
108
  superpowersNeedsGit: " superpowers needs Git — it comes with the next `kimal3 setup` once Git is there.",
104
- logoOldVersion: " The KImal3 logo stays on its previous version for now, because Claude is using the folder. Quit Claude completely, then run `kimal3 setup` again.",
109
+ logoOldVersion: " The logo couldn't be refreshed just now, because another program is using its folder. It comes with the next `kimal3 setup`.",
110
+ logoRefreshed: " The logo above the prompt is refreshed. If it is switched on, you'll see it after the next restart of Claude.",
111
+ logoNotRefreshed: " The logo couldn't be refreshed just now, because another program is using its folder. It comes with the next `kimal3 skill install`.",
105
112
  };
106
113
  export const EINRICHTEN_TEXTE = { de, en };
@@ -0,0 +1,93 @@
1
+ const de = {
2
+ // ── plan.ts ──
3
+ listHelp: `
4
+ kimal3 plan
5
+
6
+ Holt die offenen Rückmeldungen zu deiner Planung aus der App: jedes
7
+ „passt nicht“ und jedes notierte Thema mit seiner Nummer, dazu die
8
+ bestätigten Stories der aktuellen Runden.
9
+
10
+ Liest plan/stand.json im Ordner, um das Projekt zu finden. Ändert nichts.
11
+ `,
12
+ fileMissing: "Im Ordner liegt keine plan/stand.json. `/k3plan` legt sie an.",
13
+ noJson: "Der Plan ist nicht lesbar: plan/stand.json enthält kein gültiges JSON-Objekt.",
14
+ openVerdict: (nummer, spec, story, runde, notiz) => `#${nummer} · passt nicht · ${spec} / ${story} (Runde ${runde}): ${notiz}`,
15
+ openTopic: (nummer, notiz) => `#${nummer} · Thema: ${notiz}`,
16
+ noOpen: "Keine offenen Rückmeldungen.",
17
+ confirmed: (liste) => `Bestätigt: ${liste}`,
18
+ confirmedItem: (spec, story, runde) => `${spec} / ${story} (Runde ${runde})`,
19
+ view: (adresse) => `Ansehen: ${adresse}`,
20
+ // ── plan-hoch.ts ──
21
+ uploadHelp: `
22
+ kimal3 plan hoch [--ueberschreiben] [--probe]
23
+
24
+ Schickt plan/stand.json an dein Projekt in der App. Danach trägt es
25
+ projekt_id und basis in die Datei ein und leert uebernommen.
26
+
27
+ --probe Prüft alles wie beim Hochladen und speichert nichts.
28
+
29
+ --ueberschreiben Speichert auch dann, wenn in der App inzwischen ein
30
+ neuerer Plan liegt (zwei Arbeitskopien).
31
+ `,
32
+ uploaded: (titel, stories, deinTest) => `✓ Planung für „${titel}“ hochgeladen: ${stories === 1 ? "1 Story" : `${stories} Stories`}, ` +
33
+ `${deinTest === 1 ? "1 wartet" : `${deinTest} warten`} auf deinen Test.`,
34
+ dryRun: (stories, uebernommen) => `Probelauf: würde ${stories === 1 ? "1 Story" : `${stories} Stories`} hochladen, ` +
35
+ `${uebernommen === 1 ? "1 Rückmeldung" : `${uebernommen} Rückmeldungen`} als übernommen melden. Gespeichert wurde nichts.`,
36
+ // US-16: Eine Spec ohne Text ist ein Hinweis, kein Fehlschlag -- darum steht der Grund in Klammern hinter ihrem Namen.
37
+ specTextOhneGrund: (grund, pfad) => grund === "fehlt" ? `${pfad ?? ""} nicht gefunden`
38
+ : grund === "keine_datei" ? "keine Datei angegeben"
39
+ : grund === "ausserhalb" ? "Pfad außerhalb des Projekts"
40
+ : grund === "keine_textdatei" ? "keine Textdatei"
41
+ : "größer als 300 KB",
42
+ specTexte: (angekommen, gesamt, ohne) => `Spec-Texte hochgeladen: ${angekommen} von ${gesamt}.${ohne.length === 0 ? "" : ` Ohne Text: ${ohne.join(", ")}.`}`,
43
+ specTextOhneEintrag: (titel, grundText) => `„${titel}“ (${grundText})`,
44
+ idNowInFile: "projekt_id und basis stehen jetzt in plan/stand.json.",
45
+ writeBackFailed: (projektId, hochgeladenAm, leeren) => "⚠ Die Planung ist gespeichert, aber plan/stand.json ließ sich nicht nachtragen. Trag selbst ein: " +
46
+ `"projekt_id": "${projektId}", "basis": "${hochgeladenAm}"${leeren ? ', "uebernommen": []' : ""}`,
47
+ };
48
+ const en = {
49
+ listHelp: `
50
+ kimal3 plan
51
+
52
+ Fetches the open responses to your planning from the app: every
53
+ “doesn't work” and every noted topic with its number, plus the
54
+ confirmed stories of the current rounds.
55
+
56
+ Reads plan/stand.json in this folder to find the project. Changes nothing.
57
+ `,
58
+ fileMissing: "There's no plan/stand.json in this folder. `/k3plan` creates it.",
59
+ noJson: "The plan can't be read: plan/stand.json doesn't contain a valid JSON object.",
60
+ openVerdict: (nummer, spec, story, runde, notiz) => `#${nummer} · doesn't work · ${spec} / ${story} (round ${runde}): ${notiz}`,
61
+ openTopic: (nummer, notiz) => `#${nummer} · Topic: ${notiz}`,
62
+ noOpen: "No open responses.",
63
+ confirmed: (liste) => `Confirmed: ${liste}`,
64
+ confirmedItem: (spec, story, runde) => `${spec} / ${story} (round ${runde})`,
65
+ view: (adresse) => `View: ${adresse}`,
66
+ uploadHelp: `
67
+ kimal3 plan upload [--overwrite] [--dry-run]
68
+
69
+ Sends plan/stand.json to your project in the app. After that it enters
70
+ projekt_id and basis in the file and empties uebernommen.
71
+
72
+ --dry-run Checks everything like an upload and saves nothing.
73
+
74
+ --overwrite Saves even if the app already holds a newer plan
75
+ (two working copies).
76
+
77
+ The German option names (--ueberschreiben, --probe) work too.
78
+ `,
79
+ uploaded: (titel, stories, deinTest) => `✓ Planning for "${titel}" uploaded: ${stories === 1 ? "1 story" : `${stories} stories`}, ${deinTest} waiting for your test.`,
80
+ dryRun: (stories, uebernommen) => `Dry run: would upload ${stories === 1 ? "1 story" : `${stories} stories`} and mark ` +
81
+ `${uebernommen === 1 ? "1 response" : `${uebernommen} responses`} as handled. Nothing was saved.`,
82
+ specTextOhneGrund: (grund, pfad) => grund === "fehlt" ? `${pfad ?? ""} not found`
83
+ : grund === "keine_datei" ? "no file given"
84
+ : grund === "ausserhalb" ? "path outside the project"
85
+ : grund === "keine_textdatei" ? "not a text file"
86
+ : "larger than 300 KB",
87
+ specTexte: (angekommen, gesamt, ohne) => `Spec texts uploaded: ${angekommen} of ${gesamt}.${ohne.length === 0 ? "" : ` Without text: ${ohne.join(", ")}.`}`,
88
+ specTextOhneEintrag: (titel, grundText) => `"${titel}" (${grundText})`,
89
+ idNowInFile: "projekt_id and basis are now in plan/stand.json.",
90
+ writeBackFailed: (projektId, hochgeladenAm, leeren) => "⚠ The planning is saved, but plan/stand.json couldn't be updated. Enter it yourself: " +
91
+ `"projekt_id": "${projektId}", "basis": "${hochgeladenAm}"${leeren ? ', "uebernommen": []' : ""}`,
92
+ };
93
+ export const PLAN_TEXTE = { de, en };
@@ -0,0 +1,189 @@
1
+ const TEIL_DE = { aufnehmer: "Aufnehmer", ffmpeg: "FFmpeg", modell: "Sprachmodell" };
2
+ const TEIL_EN = { aufnehmer: "Recorder", ffmpeg: "FFmpeg", modell: "Speech model" };
3
+ const SCHRITT_DE = { uv: "uv", ...TEIL_DE };
4
+ const SCHRITT_EN = { uv: "uv", aufnehmer: "recorder", ffmpeg: "FFmpeg", modell: "speech model" };
5
+ const mehrzahl = (n, eins, viele) => `${n} ${n === 1 ? eins : viele}`;
6
+ const de = {
7
+ nurWindows: "`kimal3 zeigen` gibt es bisher nur unter Windows; der Mac folgt.",
8
+ helpStatus: "\n kimal3 zeigen status\n\n Zeigt, ob der Aufnehmer eingerichtet ist und ob gerade aufgenommen wird.\n Liest nur, ändert nichts.\n",
9
+ helpStart: "\n kimal3 zeigen start \"<deine Aufgabe in einem Satz>\" [--hoechstens <minuten>]\n\n Startet eine Aufnahme von Bildschirm, Ton, Klicks und Tasten im Ordner\n material/aufnahmen/ deines Projekts. Sie stoppt spätestens nach 15 Minuten\n von selbst (--hoechstens: weniger, 1 bis 15).\n",
10
+ helpStopp: "\n kimal3 zeigen stopp\n\n Stoppt die laufende Aufnahme und nennt nur Zahlen: Dauer, Klicks, Tasten, Ton.\n",
11
+ helpAuswerten: "\n kimal3 zeigen auswerten [<ordner>] [--sprache de|en] [--ohne-getipptes]\n\n Macht aus der Aufnahme auf diesem Rechner eine Zeitleiste (zeitleiste.md):\n was gesagt, geklickt und getippt wurde. Ohne Ordner: die jüngste Aufnahme\n des Projekts. --ohne-getipptes lässt alles Getippte aus.\n",
12
+ helpBild: "\n kimal3 zeigen bild <m:ss> [<ordner>]\n\n Legt ein Bildschirmfoto der Aufnahme zu diesem Zeitpunkt in bilder/ ab\n (höchstens 1600 Pixel breit). Ohne Ordner: die jüngste Aufnahme des Projekts.\n",
13
+ helpEinrichten: "\n kimal3 zeigen einrichten\n\n Richtet einmal ein, was für Aufnahmen fehlt: uv, den Aufnehmer, FFmpeg und das\n Sprachmodell (zusammen etwa 2,1 GB). Nur in deinem Benutzerordner, ohne\n Adminrechte. Was schon da ist, wird nicht noch einmal geladen.\n",
14
+ schrittUv: "uv wird eingerichtet …",
15
+ schrittAufnehmer: (fassung) => `Aufnehmer ${fassung} wird eingerichtet …`,
16
+ schrittFfmpeg: "FFmpeg wird eingerichtet …",
17
+ schrittModell: "Sprachmodell (1,6 GB) wird geladen — das dauert einige Minuten …",
18
+ nichtGeklappt: (schritt) => `Schritt ${SCHRITT_DE[schritt]} hat nicht geklappt.`,
19
+ allesDa: (fassung) => `Alles schon da: uv, Aufnehmer ${fassung}, FFmpeg, Sprachmodell.`,
20
+ fertigEingerichtet: (fassung) => `Eingerichtet — jetzt ist alles da: uv, Aufnehmer ${fassung}, FFmpeg, Sprachmodell.`,
21
+ eingerichtet: "Eingerichtet:",
22
+ uvDa: (fassung) => ` uv ${fassung}`,
23
+ uvFehlt: " uv fehlt",
24
+ aufnehmerDa: (fassung) => ` Aufnehmer ${fassung}`,
25
+ aufnehmerAlt: (fassung, soll) => ` Aufnehmer ${fassung} (gebraucht wird ${soll})`,
26
+ aufnehmerFehlt: " Aufnehmer fehlt",
27
+ ffmpeg: (da) => ` FFmpeg ${da ? "da" : "fehlt"}`,
28
+ modell: (da) => ` Sprachmodell ${da ? "da" : "fehlt"}`,
29
+ teilUngeprueft: (teil) => ` ${TEIL_DE[teil]}: ohne Aufnehmer nicht prüfbar`,
30
+ teilUnbekannt: (teil) => ` ${TEIL_DE[teil]}: kann ich nicht feststellen`,
31
+ aufnahme: "Aufnahme:",
32
+ laeuftSeit: (uhr, minuten, ordner, ende) => ` Läuft seit ${uhr} (${mehrzahl(minuten, "Minute", "Minuten")}), Ordner \`${ordner}\`, stoppt spätestens ${ende}.`,
33
+ laeuftFremd: " Läuft (in einer anderen Sitzung gestartet).",
34
+ keineLaeuft: " Keine Aufnahme läuft.",
35
+ letzteSelbst: (ordner, minuten) => ` Die letzte (\`${ordner}\`) hat nach ${mehrzahl(minuten, "Minute", "Minuten")} von selbst gestoppt.`,
36
+ letzteAbgebrochen: (ordner) => ` Die letzte (\`${ordner}\`) ist abgebrochen.`,
37
+ unbekannt: "Ob gerade aufgenommen wird, kann ich nicht feststellen.",
38
+ ortHier: (aufnahmen) => `Aufnahmen landen hier in: ${aufnahmen}`,
39
+ ortWerkbank: "Wechsle zuerst in ein Projekt (`projekte/<name>`) — Claude kann eins anlegen.",
40
+ ortZuLang: (laenge, hoechstens) => `Der Projektordner liegt zu tief für den Aufnehmer (${laenge} Zeichen, höchstens ${hoechstens}). Ein Projekt näher am Laufwerk hilft.`,
41
+ satzFehlt: "Sag in einem Satz, welche Aufgabe du vormachst:",
42
+ hoechstensFalsch: (wert) => `„--hoechstens ${wert}" verstehe ich nicht — bitte eine ganze Zahl von 1 bis 15 (Minuten).`,
43
+ schonLaeuft: (uhr, ordner) => `Es läuft schon eine Aufnahme (seit ${uhr}, Ordner \`${ordner}\`).`,
44
+ schonLaeuftFremd: "Es läuft schon eine Aufnahme (in einer anderen Sitzung gestartet).",
45
+ startetGerade: (ordner) => `Gerade startet schon eine Aufnahme (Ordner \`${ordner}\`).`,
46
+ unbekanntKeinStart: "Ob gerade aufgenommen wird, kann ich nicht feststellen — deshalb starte ich keine zweite.",
47
+ nichtEingerichtet: "Der Aufnehmer ist noch nicht eingerichtet: `kimal3 zeigen einrichten`.",
48
+ ordnerNichtAnlegbar: (ordner) => `Den Aufnahmeordner kann ich nicht anlegen: ${ordner}`,
49
+ laeuftJetzt: (ordner, ende) => `Die Aufnahme läuft (Ordner \`${ordner}\`), stoppt spätestens um ${ende}.`,
50
+ nichtGestartet: (grund, protokoll) => `Die Aufnahme ist nicht gestartet: ${grund}. Protokoll: ${protokoll}`,
51
+ endeUnbestaetigt: "Ob der Aufnehmer noch läuft, sagt `kimal3 zeigen status`.",
52
+ keineAufnahmeLaeuft: "Es läuft keine Aufnahme.",
53
+ zahlen: (dauer, klicks, tasten, ton) => `${dauer} Minuten, ${mehrzahl(klicks, "Klick", "Klicks")}, ${mehrzahl(tasten, "Taste", "Tasten")}, ${ton ? "Ton ist da" : "kein Ton"}.`,
54
+ gestoppt: (zahlen) => `Aufnahme gestoppt: ${zahlen}`,
55
+ selbstGestoppt: (minuten, zahlen) => `Die Aufnahme hat nach ${mehrzahl(minuten, "Minute", "Minuten")} von selbst gestoppt: ${zahlen}`,
56
+ nichtSauber: (grund) => `Die Aufnahme ist nicht sauber abgeschlossen: ${grund}.`,
57
+ // Ob der Ton da ist, sagen die Zahlen; bis 21.10. stand hier fest „der Ton ist verloren“ — auch wenn er da war.
58
+ abgebrochen: (grund, zahlen) => `Die Aufnahme ist abgebrochen: ${grund}.${zahlen === null ? "" : ` ${zahlen}`}`,
59
+ stoppUnklar: "Die Aufnahme ließ sich nicht stoppen — ob sie noch läuft, sagt `kimal3 zeigen status`.",
60
+ wirdAbgeschlossen: "Die Aufnahme wird noch abgeschlossen — gleich noch einmal `kimal3 zeigen stopp`.",
61
+ fremdLaeuft: "Es läuft eine Aufnahme, die ich hier nicht zuordnen kann. Sieh mit `kimal3 zeigen status` nach.",
62
+ keineAufnahmeHier: "In diesem Projekt finde ich keine Aufnahme (`material/aufnahmen/`).",
63
+ ordnerFehlt: (ordner) => `Den Aufnahmeordner gibt es nicht: ${ordner}`,
64
+ laeuftNoch: "Die Aufnahme läuft noch — erst `kimal3 zeigen stopp`.",
65
+ spracheFalsch: (wert) => `„--sprache ${wert}" kenne ich nicht — de oder en.`,
66
+ werteAus: "Werte aus (das dauert etwa so lange wie die Aufnahme) …",
67
+ zeitleisteFertig: (dauer, saetze, klicks, tippstellen, passwortfelder, datei) => `Zeitleiste fertig: ${dauer} Minuten, ${mehrzahl(saetze, "Satz", "Sätze")}, ${mehrzahl(klicks, "Klick", "Klicks")}, ` +
68
+ `${mehrzahl(tippstellen, "Stelle", "Stellen")} mit getipptem Text` +
69
+ (passwortfelder > 0 ? `, ${mehrzahl(passwortfelder, "Passwortfeld", "Passwortfelder")} ausgeblendet` : "") +
70
+ `. Datei: \`${datei}\`.`,
71
+ keineSprache: "Ich habe keine Sprache erkannt — war das Mikrofon stumm oder das falsche gewählt?",
72
+ keinTon: "Die Aufnahme hat keine Tonspur — ohne Ton entsteht keine Zeitleiste. Eine neue Aufnahme hilft.",
73
+ zeitleisteFehler: (grund) => `Die Zeitleiste ist nicht entstanden: ${grund}.`,
74
+ zeitFalsch: (wert) => `„${wert}" verstehe ich nicht als Zeitpunkt — bitte so: 0:26 oder 1:02:05.`,
75
+ bildFertig: (datei) => `Bild: ${datei}`,
76
+ bildFehler: (grund) => `Das Bild ist nicht entstanden: ${grund}.`,
77
+ gruende: {
78
+ "ffmpeg-fehlt": "FFmpeg fehlt — `kimal3 zeigen einrichten` holt es",
79
+ "kein-mikrofon": "ich finde kein Mikrofon",
80
+ "bildschirm-gesperrt": "der Bildschirm ist gesperrt oder nicht lesbar",
81
+ "pfad-zu-lang": "der Ordnerpfad ist zu lang für den Aufnehmer",
82
+ "startet-nicht": "der Aufnehmer ist nicht angelaufen",
83
+ "modell-fehlt": "das Sprachmodell fehlt — `kimal3 zeigen einrichten` holt es",
84
+ "nicht-beendet": "der Aufnehmer ließ sich nicht beenden",
85
+ "keine-aufnahme": "in dem Ordner liegt keine Aufnahme",
86
+ "aufnahme-unlesbar": "die Aufnahme ist nicht lesbar",
87
+ erkennung: "die Spracherkennung ist gescheitert",
88
+ "zeit-ungueltig": "der Zeitpunkt ist ungültig",
89
+ "kein-video": "die Aufnahme hat kein Video",
90
+ "kein-bild": "an dieser Stelle gibt es kein Bild — vielleicht liegt der Zeitpunkt nach dem Ende der Aufnahme",
91
+ "laden-gescheitert": "das Herunterladen ist gescheitert",
92
+ unvollstaendig: "das Sprachmodell ist nicht vollständig angekommen",
93
+ "ordner-fehlt": "der Aufnahmeordner fehlt",
94
+ "schreiben-gescheitert": "die Zeitleiste ließ sich nicht schreiben",
95
+ unbekannt: "den Grund kenne ich nicht",
96
+ },
97
+ };
98
+ const en = {
99
+ nurWindows: "`kimal3 show` is Windows-only for now; the Mac follows.",
100
+ helpStatus: "\n kimal3 show status\n\n Shows whether the recorder is set up and whether a recording is running.\n Only reads, changes nothing.\n",
101
+ helpStart: "\n kimal3 show start \"<your task in one sentence>\" [--at-most <minutes>]\n\n Starts a recording of screen, sound, clicks and keys in your project's\n material/aufnahmen/ folder. It stops by itself after 15 minutes at the latest\n (--at-most: less, 1 to 15).\n",
102
+ helpStopp: "\n kimal3 show stop\n\n Stops the running recording and reports numbers only: length, clicks, keys, sound.\n",
103
+ helpAuswerten: "\n kimal3 show evaluate [<folder>] [--language de|en] [--without-typed]\n\n Turns the recording into a timeline on this computer (zeitleiste.md):\n what was said, clicked and typed. Without a folder: the project's latest\n recording. --without-typed leaves out everything typed.\n",
104
+ helpBild: "\n kimal3 show picture <m:ss> [<folder>]\n\n Saves a screenshot of the recording at that moment in bilder/\n (at most 1600 pixels wide). Without a folder: the project's latest recording.\n",
105
+ helpEinrichten: "\n kimal3 show setup\n\n Sets up once what recordings still need: uv, the recorder, FFmpeg and the\n speech model (about 2.1 GB together). Only in your user folder, no admin\n rights. Whatever is already there isn't downloaded again.\n",
106
+ schrittUv: "Setting up uv …",
107
+ schrittAufnehmer: (fassung) => `Setting up recorder ${fassung} …`,
108
+ schrittFfmpeg: "Setting up FFmpeg …",
109
+ schrittModell: "Loading the speech model (1.6 GB) — this takes a few minutes …",
110
+ nichtGeklappt: (schritt) => `Step ${SCHRITT_EN[schritt]} didn't work.`,
111
+ allesDa: (fassung) => `Everything is already there: uv, recorder ${fassung}, FFmpeg, speech model.`,
112
+ fertigEingerichtet: (fassung) => `Set up — now everything is there: uv, recorder ${fassung}, FFmpeg, speech model.`,
113
+ eingerichtet: "Set up:",
114
+ uvDa: (fassung) => ` uv ${fassung}`,
115
+ uvFehlt: " uv missing",
116
+ aufnehmerDa: (fassung) => ` Recorder ${fassung}`,
117
+ aufnehmerAlt: (fassung, soll) => ` Recorder ${fassung} (${soll} is needed)`,
118
+ aufnehmerFehlt: " Recorder missing",
119
+ ffmpeg: (da) => ` FFmpeg ${da ? "present" : "missing"}`,
120
+ modell: (da) => ` Speech model ${da ? "present" : "missing"}`,
121
+ teilUngeprueft: (teil) => ` ${TEIL_EN[teil]}: can't be checked without the recorder`,
122
+ teilUnbekannt: (teil) => ` ${TEIL_EN[teil]}: I can't tell`,
123
+ aufnahme: "Recording:",
124
+ laeuftSeit: (uhr, minuten, ordner, ende) => ` Running since ${uhr} (${minuten} ${minuten === 1 ? "minute" : "minutes"}), folder \`${ordner}\`, stops at ${ende} at the latest.`,
125
+ laeuftFremd: " Running (started in another session).",
126
+ keineLaeuft: " No recording is running.",
127
+ letzteSelbst: (ordner, minuten) => ` The last one (\`${ordner}\`) stopped by itself after ${minuten} ${minuten === 1 ? "minute" : "minutes"}.`,
128
+ letzteAbgebrochen: (ordner) => ` The last one (\`${ordner}\`) broke off.`,
129
+ unbekannt: "I can't tell whether a recording is running.",
130
+ ortHier: (aufnahmen) => `Recordings go here: ${aufnahmen}`,
131
+ ortWerkbank: "Switch into a project first (`projects/<name>`) — Claude can create one.",
132
+ ortZuLang: (laenge, hoechstens) => `The project folder is nested too deeply for the recorder (${laenge} characters, at most ${hoechstens}). A project closer to the drive helps.`,
133
+ satzFehlt: "Say in one sentence which task you're showing:",
134
+ hoechstensFalsch: (wert) => `I don't understand “--at-most ${wert}” — please a whole number from 1 to 15 (minutes).`,
135
+ schonLaeuft: (uhr, ordner) => `A recording is already running (since ${uhr}, folder \`${ordner}\`).`,
136
+ schonLaeuftFremd: "A recording is already running (started in another session).",
137
+ startetGerade: (ordner) => `A recording is already starting (folder \`${ordner}\`).`,
138
+ unbekanntKeinStart: "I can't tell whether a recording is running — so I won't start a second one.",
139
+ nichtEingerichtet: "The recorder isn't set up yet: `kimal3 show setup`.",
140
+ ordnerNichtAnlegbar: (ordner) => `I can't create the recording folder: ${ordner}`,
141
+ laeuftJetzt: (ordner, ende) => `The recording is running (folder \`${ordner}\`), stops at ${ende} at the latest.`,
142
+ nichtGestartet: (grund, protokoll) => `The recording didn't start: ${grund}. Log: ${protokoll}`,
143
+ endeUnbestaetigt: "Whether the recorder is still running: `kimal3 show status`.",
144
+ keineAufnahmeLaeuft: "No recording is running.",
145
+ zahlen: (dauer, klicks, tasten, ton) => `${dauer} minutes, ${klicks} ${klicks === 1 ? "click" : "clicks"}, ${tasten} ${tasten === 1 ? "key" : "keys"}, ${ton ? "audio present" : "no audio"}.`,
146
+ gestoppt: (zahlen) => `Recording stopped: ${zahlen}`,
147
+ selbstGestoppt: (minuten, zahlen) => `The recording stopped by itself after ${minuten} ${minuten === 1 ? "minute" : "minutes"}: ${zahlen}`,
148
+ nichtSauber: (grund) => `The recording wasn't closed cleanly: ${grund}.`,
149
+ abgebrochen: (grund, zahlen) => `The recording broke off: ${grund}.${zahlen === null ? "" : ` ${zahlen}`}`,
150
+ stoppUnklar: "The recording couldn't be stopped — whether it's still running: `kimal3 show status`.",
151
+ wirdAbgeschlossen: "The recording is still being finished — run `kimal3 show stop` again in a moment.",
152
+ fremdLaeuft: "A recording is running that I can't match here. Check with `kimal3 show status`.",
153
+ keineAufnahmeHier: "I can't find a recording in this project (`material/aufnahmen/`).",
154
+ ordnerFehlt: (ordner) => `The recording folder doesn't exist: ${ordner}`,
155
+ laeuftNoch: "The recording is still running — first `kimal3 show stop`.",
156
+ spracheFalsch: (wert) => `I don't know “--language ${wert}” — de or en.`,
157
+ werteAus: "Evaluating (this takes about as long as the recording) …",
158
+ zeitleisteFertig: (dauer, saetze, klicks, tippstellen, passwortfelder, datei) => `Timeline ready: ${dauer} minutes, ${saetze} ${saetze === 1 ? "sentence" : "sentences"}, ${klicks} ${klicks === 1 ? "click" : "clicks"}, ` +
159
+ `${tippstellen} ${tippstellen === 1 ? "place" : "places"} with typed text` +
160
+ (passwortfelder > 0 ? `, ${passwortfelder} ${passwortfelder === 1 ? "password field" : "password fields"} hidden` : "") +
161
+ `. File: \`${datei}\`.`,
162
+ keineSprache: "I didn't recognise any speech — was the microphone muted or the wrong one selected?",
163
+ keinTon: "The recording has no audio track — without sound there's no timeline. A new recording helps.",
164
+ zeitleisteFehler: (grund) => `The timeline wasn't created: ${grund}.`,
165
+ zeitFalsch: (wert) => `I don't understand “${wert}” as a point in time — please like this: 0:26 or 1:02:05.`,
166
+ bildFertig: (datei) => `Picture: ${datei}`,
167
+ bildFehler: (grund) => `The picture wasn't created: ${grund}.`,
168
+ gruende: {
169
+ "ffmpeg-fehlt": "FFmpeg is missing — `kimal3 show setup` gets it",
170
+ "kein-mikrofon": "I can't find a microphone",
171
+ "bildschirm-gesperrt": "the screen is locked or can't be read",
172
+ "pfad-zu-lang": "the folder path is too long for the recorder",
173
+ "startet-nicht": "the recorder didn't get going",
174
+ "modell-fehlt": "the speech model is missing — `kimal3 show setup` gets it",
175
+ "nicht-beendet": "the recorder couldn't be stopped",
176
+ "keine-aufnahme": "there's no recording in that folder",
177
+ "aufnahme-unlesbar": "the recording can't be read",
178
+ erkennung: "speech recognition failed",
179
+ "zeit-ungueltig": "the point in time isn't valid",
180
+ "kein-video": "the recording has no video",
181
+ "kein-bild": "there's no picture at that point — perhaps the moment is after the end of the recording",
182
+ "laden-gescheitert": "the download failed",
183
+ unvollstaendig: "the speech model didn't arrive completely",
184
+ "ordner-fehlt": "the recording folder is missing",
185
+ "schreiben-gescheitert": "the timeline could not be written",
186
+ unbekannt: "I don't know the reason",
187
+ },
188
+ };
189
+ export const ZEIGEN_TEXTE = { de, en };