kimal3 0.9.2 → 0.9.3

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.
@@ -182,7 +182,7 @@ Das Board in der App zeigt nur, was du meldest: Wer nicht meldet, lässt eine Ka
182
182
  6. **`/k3plan`:** Specs gegen den Code prüfen und jede Abweichung einzeln melden — dort steht, wie. **Prüfen ist Arbeit:** Eine Story mit offenen Gegenproben (Spalte „Claude-Test offen“), die du gegen den Code prüfst, meldest du vorher als „in Arbeit“, mit `"pruefen":true` — so kommt die Karte dabei nicht ins Gespräch. Stories in „Dein Test“ oder „Fertig“ meldest du beim Abgleich nicht neu.
183
183
  7. **Feierabend:** den Tag abgleichen — dort steht, wie.
184
184
 
185
- 🔑 **Bleibt eine Story unfertig, wenn du aufhörst** (am Ende von `/k3plan` oder im Feierabend), meldest du sie mit `--ohne-sitzung` (`kimal3 plan melde --ohne-sitzung`): Ihr Plan-Stand bleibt `in_arbeit`, aber der Punkt am Projekt leuchtet nicht weiter, obwohl das Fenster offen bleibt — auf dem Board steht sie dann in „Offen“ als angefangen.
185
+ 🔑 **Bleibt eine Story unfertig, wenn du aufhörst** (am Ende von `/k3plan` oder im Feierabend), meldest du sie mit `--ohne-sitzung` (`kimal3 plan melde --ohne-sitzung`): Ihr Plan-Stand bleibt `in_arbeit`, aber der Punkt am Projekt leuchtet nicht weiter, obwohl das Fenster offen bleibt — auf dem Board steht sie dann in „Plan“ als angefangen.
186
186
 
187
187
  „Gebaut“ meldest du erst nach einer Gegenprobe, die in dieser Sitzung gelaufen ist, nie aus dem Gedächtnis.
188
188
  - **Subagenten melden nie.** Der Mod erinnert nur dich, nicht sie: Du meldest „in Arbeit“ **vor** dem Auftrag an einen Subagenten, der
@@ -181,7 +181,7 @@ The board in the app shows only what you report: if you don't, a card sits in "P
181
181
  6. **`/k3plan`:** check the specs against the code and report every deviation individually — it says how. **Checking is work:** a story with pending counter-checks (column "Claude test pending") that you check against the code, you report as "in progress" beforehand, with `"pruefen":true` — so the card doesn't come into discussion meanwhile. Stories under "Your test" or "Done" you do not report again during the reconciliation.
182
182
  7. **Wrap-up:** reconcile the day — it says how.
183
183
 
184
- 🔑 **If a story stays unfinished when you stop** (at the end of `/k3plan` or in the wrap-up), you report it with `--no-session` (`kimal3 plan report --no-session`): its plan state stays `in_arbeit`, but the dot at the project stops glowing although the window stays open — on the board it then sits under "Open" as started.
184
+ 🔑 **If a story stays unfinished when you stop** (at the end of `/k3plan` or in the wrap-up), you report it with `--no-session` (`kimal3 plan report --no-session`): its plan state stays `in_arbeit`, but the dot at the project stops glowing although the window stays open — on the board it then sits in "Plan" as started.
185
185
 
186
186
  You report "built" only after a counter-check that ran in this session, never from memory.
187
187
  - **Subagents never report.** The mod reminds only you, not them: you report "in progress" **before** the job to a subagent that
@@ -1,7 +1,8 @@
1
1
  import { existsSync } from "node:fs";
2
- import { dirname, win32 } from "node:path";
2
+ import { platform } from "node:os";
3
+ import { dirname, posix, win32 } from "node:path";
3
4
  import { zerlege } from "../argumente.js";
4
- import { AUFNEHMER_FASSUNG, aufnehmerArgumente, einrichtPlan, teileAus, uvEinrichtung, } from "../zeigen-rechnung.js";
5
+ import { AUFNEHMER_FASSUNG, aufnehmerArgumente, aufnehmerOrte, einrichtPlan, teileAus, uvEinrichtung, } from "../zeigen-rechnung.js";
5
6
  import { LESEN_MS, aufnehmerPython, findeUv, rufeWerkzeug, stillAusfuehren, uvBinOrdner } from "../zeigen-umgebung.js";
6
7
  import { vorab } from "./zeigen.js";
7
8
  /**
@@ -21,19 +22,34 @@ const ZEIT_MODELL = 60 * MINUTE;
21
22
  /** Was jetzt da ist: uv (Suchpfad + bekannte Orte), und vom Aufnehmer selbst, was er hat. */
22
23
  function bestand() {
23
24
  const p = aufnehmerPython();
24
- return { teile: teileAus(findeUv()?.uv ?? null, p === null ? null : rufeWerkzeug(p, ["teile"], null, LESEN_MS)), umgebungDa: p !== null };
25
+ return { teile: teileAus(findeUv()?.uv ?? null, p === null ? null : rufeWerkzeug(p, ["teile"], null, LESEN_MS), platform()), umgebungDa: p !== null };
26
+ }
27
+ /** Der Paketmanager fuer uv: Windows winget; Mac das erste brew, das antwortet (Apple-Silicon-, Intel-Ort, Suchpfad). */
28
+ function paketmanager() {
29
+ if (platform() === "win32")
30
+ return stillAusfuehren("winget", ["--version"], LESEN_MS) ? "winget" : null; // kein-anzeigetext: Befehl
31
+ // kein-anzeigetext: Befehl
32
+ return ["/opt/homebrew/bin/brew", "/usr/local/bin/brew", "brew"].find((brew) => stillAusfuehren(brew, ["--version"], LESEN_MS)) ?? null;
25
33
  }
26
34
  function richteUvEin() {
27
- const { befehl, argumente } = uvEinrichtung(stillAusfuehren("winget", ["--version"], LESEN_MS)); // kein-anzeigetext: Befehl
28
- stillAusfuehren(befehl, argumente, ZEIT_UV);
35
+ const pm = paketmanager();
36
+ const { befehl, argumente, umgebung } = uvEinrichtung(platform(), pm);
37
+ stillAusfuehren(befehl, argumente, ZEIT_UV, umgebung);
29
38
  // Der Suchpfad dieser Sitzung ist noch der alte: findeUv sieht auch an den bekannten Orten nach.
39
+ if (findeUv() !== null)
40
+ return true;
41
+ // Mac: brew gescheitert (kein Schreibrecht, kein Netz fuer die Formel) -> einmal der offizielle Installer.
42
+ if (pm === null || platform() !== "darwin")
43
+ return false;
44
+ const ersatz = uvEinrichtung("darwin", null);
45
+ stillAusfuehren(ersatz.befehl, ersatz.argumente, ZEIT_UV, ersatz.umgebung);
30
46
  return findeUv() !== null;
31
47
  }
32
48
  function richteAufnehmerEin(umgebungDa) {
33
49
  const uv = findeUv();
34
50
  if (uv === null)
35
51
  return false;
36
- stillAusfuehren(uv.uv, aufnehmerArgumente(umgebungDa), ZEIT_AUFNEHMER);
52
+ stillAusfuehren(uv.uv, aufnehmerArgumente(umgebungDa, platform()), ZEIT_AUFNEHMER);
37
53
  return bestand().teile.aufnehmer === AUFNEHMER_FASSUNG;
38
54
  }
39
55
  function richteFfmpegEin() {
@@ -42,9 +58,13 @@ function richteFfmpegEin() {
42
58
  if (uv === null || p === null)
43
59
  return false;
44
60
  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));
61
+ // `capture` liegt im Programmordner von uv und neben dem Python des Aufnehmers; der erste, den es gibt.
62
+ const system = platform();
63
+ const pfad = system === "win32" ? win32 : posix;
64
+ const namen = aufnehmerOrte(system, null, process.env, "").at(0)?.captureNamen ?? []; // Namen sind vom Ort unabhaengig
65
+ const capture = [bin, dirname(p.python)]
66
+ .flatMap((ordner) => (ordner === null ? [] : namen.map((n) => pfad.join(ordner, n))))
67
+ .find((d) => existsSync(d));
48
68
  if (capture === undefined)
49
69
  return false;
50
70
  stillAusfuehren(capture, ["install-ffmpeg"], ZEIT_FFMPEG); // kein-anzeigetext: Befehl
@@ -0,0 +1,56 @@
1
+ import { spawnSync } from "node:child_process";
2
+ import { platform } from "node:os";
3
+ import { zerlege } from "../argumente.js";
4
+ import { EINSTELLUNGS_SEITE, freigabeApp, freigabeAus, freigabenAusgang } from "../zeigen-freigaben.js";
5
+ import { LESEN_MS, aufnehmerPython, rufeWerkzeug } from "../zeigen-umgebung.js";
6
+ import { freigabenZeilen } from "../texte/zeigen.js";
7
+ import { vorab } from "./zeigen.js";
8
+ /**
9
+ * `kimal3 zeigen pruefen` / `kimal3 show check` (Spec 2026-10-24-k3zeigen-mac-design.md, US-M3): welche der vier
10
+ * macOS-Freigaben fehlen. `--anfragen` laesst macOS fragen, `--oeffnen` oeffnet die Seite der Systemeinstellungen.
11
+ * 🔴 Vom Werkzeug werden nur Wahrheitswerte gelesen (`freigabenAusgang`); keine Zeile gibt etwas Fremdes weiter.
12
+ */
13
+ const ANFRAGE_MS = 90_000; // macOS wartet auf den Klick des Menschen
14
+ export async function zeigenPruefen(rest) {
15
+ const lage = vorab(rest, (t) => t.helpPruefen);
16
+ if (typeof lage === "number")
17
+ return lage;
18
+ const { s, t } = lage;
19
+ if (platform() === "win32") {
20
+ console.log(t.freigabenUnnoetig);
21
+ return 0;
22
+ }
23
+ const z = zerlege(rest, ["oeffnen", "anfragen"], s);
24
+ const oeffnen = z.ok ? z.werte.optionen.oeffnen : undefined;
25
+ const anfragen = z.ok ? z.werte.optionen.anfragen : undefined;
26
+ const oeffnenF = freigabeAus(oeffnen);
27
+ const anfragenF = freigabeAus(anfragen);
28
+ // Unbekannter Wert, beide Schalter zugleich oder ein Zerlegefehler: die Hilfe, nichts wird getan.
29
+ if (!z.ok || (oeffnen !== undefined && oeffnenF === null) || (anfragen !== undefined && anfragenF === null)
30
+ || (oeffnen !== undefined && anfragen !== undefined)) {
31
+ console.error(`${z.ok ? "" : `${z.meldung}\n`}${t.helpPruefen}`);
32
+ return 1;
33
+ }
34
+ // Oeffnen braucht den Aufnehmer nicht und prueft danach nicht.
35
+ if (oeffnenF !== null) {
36
+ const r = spawnSync("open", [EINSTELLUNGS_SEITE[oeffnenF]], { stdio: "ignore" }); // kein-anzeigetext: macOS-Programm
37
+ if (r.error !== undefined || r.status !== 0) {
38
+ console.error(t.freigabeNichtGeoeffnet);
39
+ return 1;
40
+ }
41
+ console.log(t.freigabeGeoeffnet(t.freigabeName[oeffnenF]));
42
+ return 0;
43
+ }
44
+ const p = aufnehmerPython();
45
+ if (p === null) {
46
+ console.error(t.nichtEingerichtet);
47
+ return 1;
48
+ }
49
+ const antwort = anfragenF === null
50
+ ? rufeWerkzeug(p, ["freigaben"], null, LESEN_MS)
51
+ : rufeWerkzeug(p, ["freigaben", "--anfragen", anfragenF], null, ANFRAGE_MS);
52
+ const { stand } = freigabenAusgang(antwort);
53
+ const { zeilen, code } = freigabenZeilen(t, freigabeApp(process.env), stand);
54
+ (code === 0 ? console.log : console.error)(zeilen.join("\n"));
55
+ return code;
56
+ }
@@ -1,9 +1,11 @@
1
1
  import { existsSync, mkdirSync, readFileSync, writeFileSync } from "node:fs";
2
+ import { platform } from "node:os";
2
3
  import { join } from "node:path";
3
4
  import { trenneNamenUndOptionen, zerlege } from "../argumente.js";
4
- import { einrichtPlan, fehlerGrund } from "../zeigen-rechnung.js";
5
+ import { nenntProtokoll } from "../zeigen-freigaben.js";
6
+ import { einrichtPlan, fehlerGrund, teileAus } from "../zeigen-rechnung.js";
5
7
  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";
8
+ import { ermittleZustand, grundSatz, ortHier, uhrzeit, vorab, zeigeOrdner } from "./zeigen.js";
7
9
  /**
8
10
  * `kimal3 zeigen start "<Satz>" [--hoechstens N]` (Spec US-3, US-4, US-5). Reihenfolge: Ort -> laeuft schon etwas? ->
9
11
  * eingerichtet? -> Ordner -> Starter abgehaengt -> warten, bis er "laeuft, Sitzung …" meldet.
@@ -81,12 +83,8 @@ export async function zeigenStart(rest) {
81
83
  }
82
84
  }
83
85
  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;
86
+ // teileAus: auf dem Mac zaehlt ein Aufnehmer ohne AVFoundation als nicht eingerichtet (Spec US-M2)
87
+ const fehlt = !teile?.ok || einrichtPlan(teileAus(uvFassung(), teile, platform())).length > 0;
90
88
  if (p === null || fehlt) {
91
89
  console.error(t.nichtEingerichtet);
92
90
  return 1;
@@ -125,8 +123,8 @@ export async function zeigenStart(rest) {
125
123
  /* ohne Bremse meldet unten "nicht bestaetigt" */
126
124
  }
127
125
  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));
126
+ const grund = bericht?.grund ?? fehlerGrund(liesProtokoll(protokoll), platform());
127
+ console.error(nenntProtokoll(grund) ? t.nichtGestartet(grundSatz(t, grund), protokoll) : t.nichtGestartetOhneProtokoll(grundSatz(t, grund)));
130
128
  // Ohne Bericht (Starter nie angelaufen) ist das Ende genauso wenig bestaetigt wie bei "laeuft noch".
131
129
  if (bericht === null || laeuftNoch(bericht.zustand) || bericht.grund === "nicht-beendet")
132
130
  console.error(t.endeUnbestaetigt);
@@ -1,10 +1,11 @@
1
1
  import { existsSync } from "node:fs";
2
- import { platform } from "node:os";
2
+ import { platform, release } from "node:os";
3
3
  import { dirname, isAbsolute, join, relative, resolve } from "node:path";
4
4
  import { schalterOhneWert, trenneNamenUndOptionen, zerlege } from "../argumente.js";
5
5
  import { sprache } from "../sprache.js";
6
+ import { freigabeApp } from "../zeigen-freigaben.js";
6
7
  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 { AUFNEHMER_FASSUNG, aufnahmeOrt, aufnahmeZustand, bildName, dauerText, gueltigeFassung, macosFassung, starterLebt, stoppAusgang, teileAus, zeitAusText, zustandOhneAufnehmer, } from "../zeigen-rechnung.js";
8
9
  import { LESEN_MS, STOPPEN_MS, aufnehmerPython, juengsterOrdner, leseNotiz, leseStarter, loescheNotiz, rufeWerkzeug, starterFakt, uvFassung, } from "../zeigen-umgebung.js";
9
10
  /** `--help` (auch auf dem Mac), dann die Plattform. Eine Zahl = fertig, sonst die Lage fuer den Befehl. */
10
11
  export function vorab(rest, hilfe) {
@@ -14,8 +15,17 @@ export function vorab(rest, hilfe) {
14
15
  console.log(hilfe(t));
15
16
  return 0;
16
17
  }
17
- if (platform() !== "win32") {
18
- console.error(t.nurWindows);
18
+ const system = platform();
19
+ if (system === "win32")
20
+ return { s, t };
21
+ if (system !== "darwin") {
22
+ console.error(t.nurWindowsUndMac);
23
+ return 1;
24
+ }
25
+ // Mindestens macOS 13 (Darwin-Kern 22); ist die Fassung nicht lesbar, sagt der Satz es ohne Zahl und lehnt ab.
26
+ const mac = macosFassung(release());
27
+ if (mac === null || mac < 13) {
28
+ console.error(t.macZuAlt(mac));
19
29
  return 1;
20
30
  }
21
31
  return { s, t };
@@ -38,8 +48,6 @@ export function uhrzeit(sek) {
38
48
  export function grundSatz(t, code) {
39
49
  return typeof code === "string" && Object.hasOwn(t.gruende, code) ? t.gruende[code] : t.gruende.unbekannt;
40
50
  }
41
- /** Wohnt seit Task 5 in `zeigen-rechnung.ts` (auch `einrichten` braucht sie); hier weitergereicht fuer `start`. */
42
- export { gueltigeFassung };
43
51
  const istZahl = (w) => typeof w === "number" && Number.isFinite(w);
44
52
  const istZaehlung = (w) => istZahl(w) && Number.isInteger(w) && w >= 0;
45
53
  /**
@@ -95,8 +103,10 @@ export async function zeigenStatus(rest) {
95
103
  zeilen.push(t.teilUnbekannt("aufnehmer"), t.teilUnbekannt("ffmpeg"), t.teilUnbekannt("modell"));
96
104
  else {
97
105
  const roh = teile.daten.aufnehmer;
98
- const f = gueltigeFassung(roh);
99
- zeilen.push(roh === null ? t.aufnehmerFehlt
106
+ // Ueber teileAus: Auf dem Mac gilt ein Aufnehmer ohne AVFoundation als nicht eingerichtet (Spec US-M2) -> "fehlt".
107
+ // Unter Windows ist f genau gueltigeFassung(roh), die Ausgabe also wie bisher.
108
+ const f = teileAus(null, teile, platform()).aufnehmer;
109
+ zeilen.push(roh === null || (f === null && gueltigeFassung(roh) !== null) ? t.aufnehmerFehlt
100
110
  : f === null ? t.teilUnbekannt("aufnehmer")
101
111
  : f === AUFNEHMER_FASSUNG ? t.aufnehmerDa(f) : t.aufnehmerAlt(f, AUFNEHMER_FASSUNG));
102
112
  zeilen.push(t.ffmpeg(teile.daten.ffmpeg === true), t.modell(teile.daten.modell === true));
@@ -127,6 +137,8 @@ export async function zeigenStatus(rest) {
127
137
  if (ordner !== null && zustand.letzte === "abgebrochen")
128
138
  zeilen.push(t.letzteAbgebrochen(zeigeOrdner(ordner, ort)));
129
139
  }
140
+ if (platform() === "darwin")
141
+ zeilen.push(t.freigabenGehenAn(freigabeApp(process.env)));
130
142
  if (ort.art === "ort")
131
143
  zeilen.push(t.ortHier(ort.aufnahmen));
132
144
  else
@@ -158,7 +158,7 @@ export const BEFEHLE_EN = {
158
158
  ],
159
159
  },
160
160
  "kimal3 zeigen start": {
161
- stempel: "35a185f9",
161
+ stempel: "39dd03bd",
162
162
  befehl: "kimal3 show start",
163
163
  tippen: "/k3show",
164
164
  terminal: 'kimal3 show start "Send the open quotes to my boss every Monday"',
@@ -168,11 +168,11 @@ export const BEFEHLE_EN = {
168
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
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
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.",
171
+ "Windows and Mac (macOS 13 or later). On the Mac, Claude first checks the four macOS permissions.",
172
172
  ],
173
173
  },
174
174
  "kimal3 zeigen status": {
175
- stempel: "7ebd2b8f",
175
+ stempel: "5d79830b",
176
176
  befehl: "kimal3 show status",
177
177
  tippen: "! kimal3 show status",
178
178
  terminal: "kimal3 show status",
@@ -181,7 +181,7 @@ export const BEFEHLE_EN = {
181
181
  details: [
182
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
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.",
184
+ "It only reads and changes nothing. Windows and Mac (macOS 13 or later).",
185
185
  ],
186
186
  },
187
187
  "kimal3 wache": {
@@ -222,7 +222,7 @@ export const BEFEHLE = [
222
222
  "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.",
223
223
  "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.",
224
224
  "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.",
225
- "Gibt es bisher nur unter Windows.",
225
+ "Für Windows und den Mac (ab macOS 13). Auf dem Mac prüft Claude vorher die vier Freigaben von macOS.",
226
226
  ],
227
227
  },
228
228
  {
@@ -235,7 +235,7 @@ export const BEFEHLE = [
235
235
  details: [
236
236
  "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.",
237
237
  "Kann es nicht feststellen, ob aufgenommen wird, sagt es genau das, mit Rückgabewert 1 — es behauptet dann nie, dass nichts läuft.",
238
- "Es liest nur und ändert nichts. Gibt es bisher nur unter Windows.",
238
+ "Es liest nur und ändert nichts. Für Windows und den Mac (ab macOS 13).",
239
239
  ],
240
240
  },
241
241
  /*
@@ -29,6 +29,7 @@ export const BEFEHLSWOERTER = {
29
29
  "zeigen auswerten": { de: "zeigen auswerten", en: "show evaluate" }, // kein-anzeigetext: Befehlswörter (Spec k3zeigen)
30
30
  "zeigen bild": { de: "zeigen bild", en: "show picture" }, // kein-anzeigetext: Befehlswörter (Spec k3zeigen)
31
31
  "zeigen status": { de: "zeigen status", en: "show status" }, // kein-anzeigetext: Befehlswörter (Spec k3zeigen)
32
+ "zeigen pruefen": { de: "zeigen pruefen", en: "show check" }, // kein-anzeigetext: Befehlswörter (Spec k3zeigen-mac)
32
33
  };
33
34
  /**
34
35
  * Welcher Befehl steht vorne in `argv` -- und was bleibt dahinter?
@@ -78,6 +79,9 @@ export const SCHALTER_EN = {
78
79
  "at-most": "hoechstens",
79
80
  language: "sprache",
80
81
  "without-typed": "ohne-getipptes",
82
+ // `kimal3 show check` (Spec 2026-10-24-k3zeigen-mac-design.md)
83
+ open: "oeffnen",
84
+ ask: "anfragen",
81
85
  // `kimal3 plan upload` (Spec 2026-10-21-projektseite-planung, US-11); `--overwrite` steht schon oben.
82
86
  "dry-run": "probe",
83
87
  // `kimal3 project connect` (Board in Echtzeit, Task 7)
package/dist/kimal3.js CHANGED
@@ -45,6 +45,7 @@ import { projektVerbinden } from "./befehle/projekt-verbinden.js";
45
45
  import { skillDatei, skillInstallBefehl } from "./befehle/skill-install.js";
46
46
  import { wache, wacheSchalten } from "./befehle/wache.js";
47
47
  import { zeigenAuswerten, zeigenBild, zeigenStatus, zeigenStopp } from "./befehle/zeigen.js";
48
+ import { zeigenPruefen } from "./befehle/zeigen-pruefen.js";
48
49
  import { zeigenStart } from "./befehle/zeigen-start.js";
49
50
  import { zeigenEinrichten } from "./befehle/zeigen-einrichten.js";
50
51
  /** Eine Meldung pro Programmlauf -- s. "Einmal-Riegel" im Block darunter. */
@@ -527,6 +528,7 @@ async function fuehreAus(name, rest) {
527
528
  case "zeigen auswerten": return zeigenAuswerten(rest);
528
529
  case "zeigen bild": return zeigenBild(rest);
529
530
  case "zeigen status": return zeigenStatus(rest);
531
+ case "zeigen pruefen": return zeigenPruefen(rest);
530
532
  case "wache an": return allein ? wacheSchalten(true) : null;
531
533
  case "wache aus": return allein ? wacheSchalten(false) : null;
532
534
  case "wache":
@@ -1,15 +1,21 @@
1
+ import { FREIGABEN } from "../zeigen-freigaben.js";
1
2
  const TEIL_DE = { aufnehmer: "Aufnehmer", ffmpeg: "FFmpeg", modell: "Sprachmodell" };
2
3
  const TEIL_EN = { aufnehmer: "Recorder", ffmpeg: "FFmpeg", modell: "Speech model" };
3
4
  const SCHRITT_DE = { uv: "uv", ...TEIL_DE };
4
5
  const SCHRITT_EN = { uv: "uv", aufnehmer: "recorder", ffmpeg: "FFmpeg", modell: "speech model" };
5
6
  const mehrzahl = (n, eins, viele) => `${n} ${n === 1 ? eins : viele}`;
6
7
  const de = {
7
- nurWindows: "`kimal3 zeigen` gibt es bisher nur unter Windows; der Mac folgt.",
8
+ nurWindowsUndMac: "`kimal3 zeigen` gibt es für Windows und den Mac.",
9
+ macZuAlt: (n) => `Für Aufnahmen braucht es macOS 13 oder neuer${n === null ? "" : ` — dieser Mac hat macOS ${n}`}.`,
10
+ freigabenGehenAn: (app) => `Freigaben gehen an: ${app.art === "claude-app" ? "Claude (Desktop-App)"
11
+ : app.art === "ssh" ? "niemanden — über SSH kann macOS keine Freigaben geben"
12
+ : app.art === "terminal" && app.name !== null ? app.name : "die App, aus der du Claude gestartet hast"}`,
8
13
  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
14
  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
15
  helpStopp: "\n kimal3 zeigen stopp\n\n Stoppt die laufende Aufnahme und nennt nur Zahlen: Dauer, Klicks, Tasten, Ton.\n",
11
16
  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
17
  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",
18
+ helpPruefen: "\n kimal3 zeigen pruefen\n kimal3 zeigen pruefen --anfragen <bildschirm|eingabe|bedienungshilfen|mikrofon>\n kimal3 zeigen pruefen --oeffnen <bildschirm|eingabe|bedienungshilfen|mikrofon>\n\n Nur auf dem Mac: zeigt, welche der vier Freigaben für Aufnahmen da sind.\n --anfragen lässt macOS nach einer Freigabe fragen, --oeffnen öffnet ihre Seite\n in den Systemeinstellungen. Unter Windows braucht es keine.\n",
13
19
  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
20
  schrittUv: "uv wird eingerichtet …",
15
21
  schrittAufnehmer: (fassung) => `Aufnehmer ${fassung} wird eingerichtet …`,
@@ -48,6 +54,7 @@ const de = {
48
54
  ordnerNichtAnlegbar: (ordner) => `Den Aufnahmeordner kann ich nicht anlegen: ${ordner}`,
49
55
  laeuftJetzt: (ordner, ende) => `Die Aufnahme läuft (Ordner \`${ordner}\`), stoppt spätestens um ${ende}.`,
50
56
  nichtGestartet: (grund, protokoll) => `Die Aufnahme ist nicht gestartet: ${grund}. Protokoll: ${protokoll}`,
57
+ nichtGestartetOhneProtokoll: (grund) => `Die Aufnahme ist nicht gestartet: ${grund}.`,
51
58
  endeUnbestaetigt: "Ob der Aufnehmer noch läuft, sagt `kimal3 zeigen status`.",
52
59
  keineAufnahmeLaeuft: "Es läuft keine Aufnahme.",
53
60
  zahlen: (dauer, klicks, tasten, ton) => `${dauer} Minuten, ${mehrzahl(klicks, "Klick", "Klicks")}, ${mehrzahl(tasten, "Taste", "Tasten")}, ${ton ? "Ton ist da" : "kein Ton"}.`,
@@ -74,6 +81,15 @@ const de = {
74
81
  zeitFalsch: (wert) => `„${wert}" verstehe ich nicht als Zeitpunkt — bitte so: 0:26 oder 1:02:05.`,
75
82
  bildFertig: (datei) => `Bild: ${datei}`,
76
83
  bildFehler: (grund) => `Das Bild ist nicht entstanden: ${grund}.`,
84
+ freigabenUnnoetig: "Unter Windows braucht es keine Freigaben.",
85
+ freigabeAppName: (app) => (app.art === "claude-app" || app.art === "terminal") && app.name !== null ? app.name : "diese App",
86
+ freigabenAlleDa: (app) => `Alle vier Freigaben für ${app} sind da.`,
87
+ freigabenKopf: (app) => `Freigaben für ${app}:`,
88
+ freigabeName: { bildschirm: "Bildschirm- & Systemaudioaufnahme", eingabe: "Eingabeüberwachung", bedienungshilfen: "Bedienungshilfen", mikrofon: "Mikrofon" },
89
+ freigabeStand: { da: "da", fehlt: "fehlt", unbekannt: "kann ich nicht feststellen" },
90
+ freigabenUnbekannt: "Ob die Freigaben da sind, kann ich nicht feststellen.",
91
+ freigabeGeoeffnet: (name) => `Ich habe ‚${name}' in den Systemeinstellungen geöffnet.`,
92
+ freigabeNichtGeoeffnet: "Die Systemeinstellungen ließen sich nicht öffnen.",
77
93
  gruende: {
78
94
  "ffmpeg-fehlt": "FFmpeg fehlt — `kimal3 zeigen einrichten` holt es",
79
95
  "kein-mikrofon": "ich finde kein Mikrofon",
@@ -92,16 +108,25 @@ const de = {
92
108
  unvollstaendig: "das Sprachmodell ist nicht vollständig angekommen",
93
109
  "ordner-fehlt": "der Aufnahmeordner fehlt",
94
110
  "schreiben-gescheitert": "die Zeitleiste ließ sich nicht schreiben",
111
+ "freigabe-bildschirm": "es fehlt die Freigabe Bildschirm- & Systemaudioaufnahme — `kimal3 zeigen pruefen` zeigt, was fehlt",
112
+ "freigabe-eingabe": "es fehlt die Freigabe Eingabeüberwachung — `kimal3 zeigen pruefen` zeigt, was fehlt",
113
+ "freigabe-bedienungshilfen": "es fehlt die Freigabe Bedienungshilfen — `kimal3 zeigen pruefen` zeigt, was fehlt",
114
+ "abgriff-abgeschaltet": "macOS hat das Mitlesen von Maus und Tastatur abgeschaltet — eine neue Aufnahme hilft meist",
95
115
  unbekannt: "den Grund kenne ich nicht",
96
116
  },
97
117
  };
98
118
  const en = {
99
- nurWindows: "`kimal3 show` is Windows-only for now; the Mac follows.",
119
+ nurWindowsUndMac: "`kimal3 show` is available on Windows and the Mac.",
120
+ macZuAlt: (n) => `Recording needs macOS 13 or later${n === null ? "" : ` — this Mac has macOS ${n}`}.`,
121
+ freigabenGehenAn: (app) => `Permissions go to: ${app.art === "claude-app" ? "Claude (desktop app)"
122
+ : app.art === "ssh" ? "nobody — macOS can't grant permissions over SSH"
123
+ : app.art === "terminal" && app.name !== null ? app.name : "the app you started Claude from"}`,
100
124
  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
125
  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
126
  helpStopp: "\n kimal3 show stop\n\n Stops the running recording and reports numbers only: length, clicks, keys, sound.\n",
103
127
  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
128
  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",
129
+ helpPruefen: "\n kimal3 show check\n kimal3 show check --ask <screen|input|accessibility|microphone>\n kimal3 show check --open <screen|input|accessibility|microphone>\n\n Mac only: shows which of the four permissions for recordings are in place.\n --ask makes macOS ask for a permission, --open opens its page in System\n Settings. Windows needs none.\n",
105
130
  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
131
  schrittUv: "Setting up uv …",
107
132
  schrittAufnehmer: (fassung) => `Setting up recorder ${fassung} …`,
@@ -140,6 +165,7 @@ const en = {
140
165
  ordnerNichtAnlegbar: (ordner) => `I can't create the recording folder: ${ordner}`,
141
166
  laeuftJetzt: (ordner, ende) => `The recording is running (folder \`${ordner}\`), stops at ${ende} at the latest.`,
142
167
  nichtGestartet: (grund, protokoll) => `The recording didn't start: ${grund}. Log: ${protokoll}`,
168
+ nichtGestartetOhneProtokoll: (grund) => `The recording didn't start: ${grund}.`,
143
169
  endeUnbestaetigt: "Whether the recorder is still running: `kimal3 show status`.",
144
170
  keineAufnahmeLaeuft: "No recording is running.",
145
171
  zahlen: (dauer, klicks, tasten, ton) => `${dauer} minutes, ${klicks} ${klicks === 1 ? "click" : "clicks"}, ${tasten} ${tasten === 1 ? "key" : "keys"}, ${ton ? "audio present" : "no audio"}.`,
@@ -165,6 +191,15 @@ const en = {
165
191
  zeitFalsch: (wert) => `I don't understand “${wert}” as a point in time — please like this: 0:26 or 1:02:05.`,
166
192
  bildFertig: (datei) => `Picture: ${datei}`,
167
193
  bildFehler: (grund) => `The picture wasn't created: ${grund}.`,
194
+ freigabenUnnoetig: "Windows needs no permissions.",
195
+ freigabeAppName: (app) => (app.art === "claude-app" || app.art === "terminal") && app.name !== null ? app.name : "this app",
196
+ freigabenAlleDa: (app) => `All four permissions for ${app} are in place.`,
197
+ freigabenKopf: (app) => `Permissions for ${app}:`,
198
+ freigabeName: { bildschirm: "Screen & System Audio Recording", eingabe: "Input Monitoring", bedienungshilfen: "Accessibility", mikrofon: "Microphone" },
199
+ freigabeStand: { da: "in place", fehlt: "missing", unbekannt: "can't tell" },
200
+ freigabenUnbekannt: "I can't tell whether the permissions are in place.",
201
+ freigabeGeoeffnet: (name) => `I opened ‘${name}’ in System Settings.`,
202
+ freigabeNichtGeoeffnet: "System Settings couldn't be opened.",
168
203
  gruende: {
169
204
  "ffmpeg-fehlt": "FFmpeg is missing — `kimal3 show setup` gets it",
170
205
  "kein-mikrofon": "I can't find a microphone",
@@ -183,7 +218,24 @@ const en = {
183
218
  unvollstaendig: "the speech model didn't arrive completely",
184
219
  "ordner-fehlt": "the recording folder is missing",
185
220
  "schreiben-gescheitert": "the timeline could not be written",
221
+ "freigabe-bildschirm": "the Screen & System Audio Recording permission is missing — `kimal3 show check` shows what's missing",
222
+ "freigabe-eingabe": "the Input Monitoring permission is missing — `kimal3 show check` shows what's missing",
223
+ "freigabe-bedienungshilfen": "the Accessibility permission is missing — `kimal3 show check` shows what's missing",
224
+ "abgriff-abgeschaltet": "macOS switched off reading the mouse and keyboard — a new recording usually helps",
186
225
  unbekannt: "I don't know the reason",
187
226
  },
188
227
  };
189
228
  export const ZEIGEN_TEXTE = { de, en };
229
+ /**
230
+ * Die Ausgabe von `kimal3 zeigen pruefen` ohne Schalter, rein aus Stand und Saetzen. `code` 0 nur, wenn alle vier da sind;
231
+ * "weiss nicht" (auch bei einzelnen) ist nie "da". Alle vier unbekannt -> ein Satz statt vier gleicher Zeilen.
232
+ */
233
+ export function freigabenZeilen(t, app, stand) {
234
+ const name = t.freigabeAppName(app);
235
+ if (FREIGABEN.every((f) => stand[f] === "da"))
236
+ return { zeilen: [t.freigabenAlleDa(name)], code: 0 };
237
+ if (FREIGABEN.every((f) => stand[f] === "unbekannt"))
238
+ return { zeilen: [t.freigabenUnbekannt], code: 1 };
239
+ const breite = Math.max(...FREIGABEN.map((f) => t.freigabeName[f].length));
240
+ return { zeilen: [t.freigabenKopf(name), ...FREIGABEN.map((f) => ` ${t.freigabeName[f].padEnd(breite)} ${t.freigabeStand[stand[f]]}`)], code: 1 };
241
+ }
@@ -0,0 +1,65 @@
1
+ /** In dieser Reihenfolge werden sie geprueft und gezeigt. */
2
+ export const FREIGABEN = ["bildschirm", "eingabe", "bedienungshilfen", "mikrofon"]; // kein-anzeigetext: Schluessel
3
+ // Beide Wortsaetze gelten in beiden Sprachen (wie die Befehlswoerter: der Mensch soll nicht an der Sprache scheitern).
4
+ const WERTE = {
5
+ bildschirm: "bildschirm", eingabe: "eingabe", bedienungshilfen: "bedienungshilfen", mikrofon: "mikrofon", // kein-anzeigetext: Schalterwerte
6
+ screen: "bildschirm", input: "eingabe", accessibility: "bedienungshilfen", microphone: "mikrofon", // kein-anzeigetext: Schalterwerte
7
+ };
8
+ /** Wert von `--oeffnen`/`--anfragen` -> Freigabe; unbekannt (auch "constructor") -> null. */
9
+ export const freigabeAus = (w) => (w !== undefined && Object.hasOwn(WERTE, w) ? WERTE[w] : null);
10
+ /** Nennt die Absage von `start` das Protokoll? Nicht bei einer fehlenden Freigabe: Die prueft der Starter, bevor er es anlegt. */
11
+ export const nenntProtokoll = (grund) => !(typeof grund === "string" && grund.startsWith("freigabe-")); // kein-anzeigetext: Code-Anfang
12
+ /**
13
+ * Die Antwort von `werkzeug.py freigaben` -> Stand je Freigabe. Nur ein echtes `true` heisst "da", nur ein echtes
14
+ * `false` "fehlt"; alles andere (null, Zahl, "ja", fehlender Schluessel, kaputte Antwort) heisst "weiss nicht" --
15
+ * nie "da" (Ein Fehlschlag sieht nie aus wie ein Erfolg).
16
+ */
17
+ export function freigabenAusgang(antwort) {
18
+ const roh = antwort.ok ? antwort.daten.freigaben : undefined;
19
+ const quelle = roh !== null && typeof roh === "object" && !Array.isArray(roh) ? roh : {};
20
+ const von = (w) => (w === true ? "da" : w === false ? "fehlt" : "unbekannt");
21
+ const stand = {
22
+ bildschirm: von(quelle.bildschirm), eingabe: von(quelle.eingabe),
23
+ bedienungshilfen: von(quelle.bedienungshilfen), mikrofon: von(quelle.mikrofon),
24
+ };
25
+ return { stand, alleDa: FREIGABEN.every((f) => stand[f] === "da") };
26
+ }
27
+ const BUENDEL = {
28
+ "com.apple.Terminal": "Terminal", // kein-anzeigetext: App-Name
29
+ "com.googlecode.iterm2": "iTerm", // kein-anzeigetext: App-Name
30
+ "com.microsoft.VSCode": "Visual Studio Code", // kein-anzeigetext: App-Name
31
+ "com.todesktop.230313mzl4w4u92": "Cursor", // kein-anzeigetext: App-Name
32
+ "dev.warp.Warp-Stable": "Warp", // kein-anzeigetext: App-Name
33
+ "com.mitchellh.ghostty": "Ghostty", // kein-anzeigetext: App-Name
34
+ };
35
+ const TERM_PROGRAMM = {
36
+ Apple_Terminal: "Terminal", // kein-anzeigetext: App-Name
37
+ "iTerm.app": "iTerm", // kein-anzeigetext: App-Name
38
+ vscode: "Visual Studio Code", // kein-anzeigetext: App-Name
39
+ WarpTerminal: "Warp", // kein-anzeigetext: App-Name
40
+ ghostty: "Ghostty", // kein-anzeigetext: App-Name
41
+ };
42
+ const CLAUDE_APP = "com.anthropic.claudefordesktop"; // kein-anzeigetext: Bundle-ID
43
+ /** Erkennt aus den Umgebungsvariablen, welches Programm die Freigabe braucht. Reihenfolge: Bundle-ID, SSH, TERM_PROGRAM, Claude-App. */
44
+ export function freigabeApp(env) {
45
+ const buendel = env.__CFBundleIdentifier;
46
+ if (buendel === CLAUDE_APP)
47
+ return { art: "claude-app", name: "Claude" }; // kein-anzeigetext: App-Name
48
+ if (buendel !== undefined && Object.hasOwn(BUENDEL, buendel))
49
+ return { art: "terminal", name: BUENDEL[buendel] };
50
+ if (env.SSH_CONNECTION)
51
+ return { art: "ssh", name: null };
52
+ const term = env.TERM_PROGRAM;
53
+ if (term !== undefined && Object.hasOwn(TERM_PROGRAMM, term))
54
+ return { art: "terminal", name: TERM_PROGRAMM[term] };
55
+ if (env.CLAUDE_CODE_ENTRYPOINT === "claude-desktop")
56
+ return { art: "claude-app", name: "Claude" }; // kein-anzeigetext: App-Name
57
+ return { art: "unbekannt", name: null };
58
+ }
59
+ /** Die Seite der Systemeinstellungen, auf der man die Freigabe erteilt. */
60
+ export const EINSTELLUNGS_SEITE = {
61
+ bildschirm: "x-apple.systempreferences:com.apple.preference.security?Privacy_ScreenCapture", // kein-anzeigetext: Systemadresse
62
+ eingabe: "x-apple.systempreferences:com.apple.preference.security?Privacy_ListenEvent", // kein-anzeigetext: Systemadresse
63
+ bedienungshilfen: "x-apple.systempreferences:com.apple.preference.security?Privacy_Accessibility", // kein-anzeigetext: Systemadresse
64
+ mikrofon: "x-apple.systempreferences:com.apple.preference.security?Privacy_Microphone", // kein-anzeigetext: Systemadresse
65
+ };