kimal3 0.4.0-durchstich.3 → 0.5.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +6 -0
- package/claude-plugins/.claude-plugin/marketplace.json +8 -8
- package/claude-plugins/README.md +7 -7
- package/claude-plugins/kimal3-logo/.claude-plugin/plugin.json +6 -6
- package/claude-plugins/kimal3-logo/hooks/hooks.json +1 -1
- package/claude-plugins/kimal3-logo/hooks/register.tsx +80 -80
- package/coach/de/kimal3-coach.md +167 -0
- package/coach/en/kimal3-coach.md +167 -0
- package/dist/arbeitsplatz-dateien.js +1 -1
- package/dist/arbeitsplatz-finden.js +115 -0
- package/dist/arbeitsplatz-suche.js +176 -0
- package/dist/befehle/einrichten.js +250 -32
- package/dist/befehle/kurs.js +77 -0
- package/dist/befehle/login.js +12 -1
- package/dist/befehle/skill-install.js +15 -0
- package/dist/befehle/zeigen-einrichten.js +104 -0
- package/dist/befehle/zeigen-start.js +143 -0
- package/dist/befehle/zeigen.js +288 -0
- package/dist/befehle-katalog-en.js +39 -0
- package/dist/befehle-katalog.js +39 -0
- package/dist/befehlswoerter.js +15 -0
- package/dist/coach-ablegen.js +130 -0
- package/dist/coach-rechnung.js +40 -0
- package/dist/kimal3.js +14 -2
- package/dist/logo-ablegen.js +26 -6
- package/dist/slash-befehle.js +1 -0
- package/dist/texte/coach.js +27 -0
- package/dist/texte/einrichten-marken.js +2 -2
- package/dist/texte/einrichten.js +58 -5
- package/dist/texte/kurs.js +21 -0
- package/dist/texte/zeigen.js +189 -0
- package/dist/zeigen-rechnung.js +226 -0
- package/dist/zeigen-umgebung.js +190 -0
- package/package.json +3 -1
- package/skill/SKILL.md +10 -5
- package/skill/commands/k3.md +4 -3
- package/skill/commands/k3feierabend.md +2 -3
- package/skill/commands/k3kurs.md +27 -39
- package/skill/commands/k3start.md +4 -3
- package/skill/commands/k3zeigen.md +303 -0
- package/skill/en/SKILL.md +10 -5
- package/skill/en/commands/k3.md +4 -3
- package/skill/en/commands/k3course.md +27 -40
- package/skill/en/commands/k3show.md +302 -0
- package/skill/en/commands/k3start.md +4 -3
- package/skill/en/commands/k3wrapup.md +3 -4
- package/zeigen/gemeinsam.py +82 -0
- package/zeigen/starter.py +163 -0
- package/zeigen/werkzeug.py +250 -0
- package/zeigen/zeitleiste.py +393 -0
|
@@ -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 };
|
|
@@ -0,0 +1,226 @@
|
|
|
1
|
+
// Reine Rechnungen hinter `kimal3 zeigen` (Aufnahme von Bildschirm und Ton fuer Claude).
|
|
2
|
+
// Kein Dateisystem, kein Prozess, keine Anzeige-Texte: Die Befehle holen die Fakten und fragen hier.
|
|
3
|
+
// Gedeckt von `npm run check:zeigen` (Spec 2026-10-18-k3zeigen-design.md).
|
|
4
|
+
import { win32 } from "node:path";
|
|
5
|
+
const MAX_PFAD_WINDOWS = 170;
|
|
6
|
+
/** "2026-10-19-0912" in Ortszeit. */
|
|
7
|
+
export function ordnerName(jetzt) {
|
|
8
|
+
const z = (n) => String(n).padStart(2, "0");
|
|
9
|
+
return `${jetzt.getFullYear()}-${z(jetzt.getMonth() + 1)}-${z(jetzt.getDate())}-${z(jetzt.getHours())}${z(jetzt.getMinutes())}`;
|
|
10
|
+
}
|
|
11
|
+
/** Index des Projektordners im Segment-Array, -1 wenn keiner, -2 wenn der Pfad bei der Werkbank endet. */
|
|
12
|
+
function projektEnde(segmente) {
|
|
13
|
+
const i = segmente.findIndex((s) => /^(werkbank|workbench)-/i.test(s));
|
|
14
|
+
if (i < 0)
|
|
15
|
+
return -1;
|
|
16
|
+
const projekte = /^werkbank-/i.test(segmente[i]) ? /^projekte$/i : /^projects$/i;
|
|
17
|
+
if (i === segmente.length - 1)
|
|
18
|
+
return -2;
|
|
19
|
+
if (!projekte.test(segmente[i + 1]))
|
|
20
|
+
return -1;
|
|
21
|
+
return i + 2 < segmente.length ? i + 2 : -2;
|
|
22
|
+
}
|
|
23
|
+
/** Wohin die Aufnahme faellt: ins Projekt, in dem man steht -- nie in die Werkbank selbst. */
|
|
24
|
+
export function aufnahmeOrt(start, jetzt, plattform, gibtEs) {
|
|
25
|
+
const trenner = start.includes("\\") ? "\\" : "/";
|
|
26
|
+
const segmente = start.split(/[\\/]/);
|
|
27
|
+
while (segmente.length > 1 && segmente[segmente.length - 1] === "")
|
|
28
|
+
segmente.pop();
|
|
29
|
+
const ende = projektEnde(segmente);
|
|
30
|
+
if (ende === -2)
|
|
31
|
+
return { art: "werkbank" };
|
|
32
|
+
const projekt = (ende >= 0 ? segmente.slice(0, ende + 1) : segmente).join(trenner);
|
|
33
|
+
const aufnahmen = `${projekt}${trenner}material${trenner}aufnahmen`; // kein-anzeigetext: Ordnerpfad
|
|
34
|
+
const basis = `${aufnahmen}${trenner}${ordnerName(jetzt)}`;
|
|
35
|
+
let ordner = basis;
|
|
36
|
+
for (let n = 2; gibtEs(ordner); n++)
|
|
37
|
+
ordner = `${basis}-${n}`;
|
|
38
|
+
if (plattform === "win32" && ordner.length > MAX_PFAD_WINDOWS) {
|
|
39
|
+
return { art: "zu-lang", laenge: ordner.length, hoechstens: MAX_PFAD_WINDOWS };
|
|
40
|
+
}
|
|
41
|
+
return { art: "ort", ordner, aufnahmen };
|
|
42
|
+
}
|
|
43
|
+
/** 93 -> "1:33", 3725 -> "1:02:05". */
|
|
44
|
+
export function dauerText(sekunden) {
|
|
45
|
+
const s = Math.floor(sekunden);
|
|
46
|
+
const h = Math.floor(s / 3600);
|
|
47
|
+
const m = Math.floor((s % 3600) / 60);
|
|
48
|
+
const rest = String(s % 60).padStart(2, "0");
|
|
49
|
+
return h > 0 ? `${h}:${String(m).padStart(2, "0")}:${rest}` : `${m}:${rest}`;
|
|
50
|
+
}
|
|
51
|
+
/** "0:26" -> 26, "1:02:05" -> 3725, "26" -> 26; alles andere null. */
|
|
52
|
+
export function zeitAusText(text) {
|
|
53
|
+
const teile = text.trim().split(":");
|
|
54
|
+
if (teile.length > 3 || !teile.every((t) => /^\d+$/.test(t)))
|
|
55
|
+
return null;
|
|
56
|
+
const zahlen = teile.map(Number);
|
|
57
|
+
if (zahlen.slice(1).some((n) => n >= 60))
|
|
58
|
+
return null;
|
|
59
|
+
return zahlen.reduce((summe, n) => summe * 60 + n, 0);
|
|
60
|
+
}
|
|
61
|
+
/** 26 -> "0-26.png", 3725 -> "1-02-05.png" (Doppelpunkte duerfen nicht in Dateinamen). */
|
|
62
|
+
export function bildName(sekunden) {
|
|
63
|
+
return `${dauerText(sekunden).replaceAll(":", "-")}.png`; // kein-anzeigetext: Dateiname
|
|
64
|
+
}
|
|
65
|
+
// Reihenfolge zaehlt: das erste Muster, das trifft, gewinnt. Das Protokoll enthaelt Fenstertitel -- es wird nie
|
|
66
|
+
// zurueckgegeben oder angezeigt, nur nach diesen festen Mustern durchsucht.
|
|
67
|
+
const GRUENDE = [
|
|
68
|
+
[/FFmpegUnavailableError|ffmpeg.*not found/i, "ffmpeg-fehlt"],
|
|
69
|
+
[/returned success without a non-empty output/, "pfad-zu-lang"],
|
|
70
|
+
[/PortAudio|No Default Input Device|sounddevice/, "kein-mikrofon"],
|
|
71
|
+
[/DesktopCaptureError|screen layout/, "bildschirm-gesperrt"],
|
|
72
|
+
[/did not become ready|did not start/, "startet-nicht"],
|
|
73
|
+
];
|
|
74
|
+
export function fehlerGrund(protokoll) {
|
|
75
|
+
return GRUENDE.find(([muster]) => muster.test(protokoll))?.[1] ?? "unbekannt";
|
|
76
|
+
}
|
|
77
|
+
/** Der Aufnehmer antwortet mit einer JSON-Zeile ganz am Ende; alles andere (auch ein Traceback) ist ein Fehlschlag. */
|
|
78
|
+
export function liesWerkzeugAntwort(rueckgabe, stdout) {
|
|
79
|
+
if (rueckgabe !== 0)
|
|
80
|
+
return { ok: false };
|
|
81
|
+
const zeilen = stdout.split(/\r?\n/).filter((z) => z.trim() !== "");
|
|
82
|
+
const letzte = zeilen[zeilen.length - 1];
|
|
83
|
+
if (letzte === undefined)
|
|
84
|
+
return { ok: false };
|
|
85
|
+
try {
|
|
86
|
+
const daten = JSON.parse(letzte);
|
|
87
|
+
if (typeof daten !== "object" || daten === null || Array.isArray(daten))
|
|
88
|
+
return { ok: false };
|
|
89
|
+
return { ok: true, daten: daten };
|
|
90
|
+
}
|
|
91
|
+
catch {
|
|
92
|
+
return { ok: false };
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
/** So alt darf das Lebenszeichen des Starters hoechstens sein, damit er als lebend gilt (er schreibt alle 2 s). */
|
|
96
|
+
const LEBENSZEICHEN_SEK = 10;
|
|
97
|
+
/** Lebt der Starter nachweislich -- meldet `startet`/`laeuft` UND sein Lebenszeichen ist hoechstens 10 s alt? (R-3) */
|
|
98
|
+
export function starterLebt(s, jetzt) {
|
|
99
|
+
if (s === null || (s.zustand !== "startet" && s.zustand !== "laeuft"))
|
|
100
|
+
return false;
|
|
101
|
+
return s.zuletzt !== undefined && jetzt - s.zuletzt <= LEBENSZEICHEN_SEK;
|
|
102
|
+
}
|
|
103
|
+
/**
|
|
104
|
+
* Die Wahrheit ueber "laeuft" kommt vom Aufnehmer; Notiz und Starter erzaehlen nur dazu.
|
|
105
|
+
* `jetzt` in Epochensekunden. Ausnahme Uebergangsfenster (R-3): Der Aufnehmer meldet noch/schon keine Sitzung, der
|
|
106
|
+
* Starter lebt aber nachweislich (frisches Lebenszeichen) -- dann laeuft etwas, sonst waere "abgebrochen" falsch.
|
|
107
|
+
*/
|
|
108
|
+
export function aufnahmeZustand(f, jetzt) {
|
|
109
|
+
if (f.aktiveSitzungen === null)
|
|
110
|
+
return { art: "unbekannt" };
|
|
111
|
+
// Der Starter konnte den Aufnehmer nicht beenden: Solange er noch lebt (und es weiter versucht) oder der Aufnehmer
|
|
112
|
+
// eine Sitzung meldet, weiss niemand, ob aufgenommen wird. Ein laengst stiller Starter ohne aktive Sitzung blockiert
|
|
113
|
+
// dagegen nicht jeden weiteren Start im Projekt -- er zaehlt unten als abgebrochen (Endpruefung, Punkt 3).
|
|
114
|
+
const frisch = f.starter?.zuletzt !== undefined && jetzt - f.starter.zuletzt <= LEBENSZEICHEN_SEK;
|
|
115
|
+
if (f.starter?.grund === "nicht-beendet" && (frisch || f.aktiveSitzungen.length > 0))
|
|
116
|
+
return { art: "unbekannt" };
|
|
117
|
+
const n = f.notiz;
|
|
118
|
+
const ausNotiz = (sitzung, eigenerOrdner) => n !== null && ((sitzung !== null && n.sitzung === sitzung) || eigenerOrdner)
|
|
119
|
+
? { art: "laeuft", sitzung, ordner: n.ordner, gestartet: n.gestartet, frist: n.frist, fremd: false }
|
|
120
|
+
: { art: "laeuft", sitzung, ordner: null, gestartet: null, frist: null, fremd: true };
|
|
121
|
+
if (f.aktiveSitzungen.length > 0) {
|
|
122
|
+
const eigene = n !== null && f.aktiveSitzungen.includes(n.sitzung) ? n.sitzung : null;
|
|
123
|
+
return ausNotiz(eigene ?? f.aktiveSitzungen[0], false);
|
|
124
|
+
}
|
|
125
|
+
const s = f.starter;
|
|
126
|
+
if (s === null)
|
|
127
|
+
return { art: "keine", letzte: null };
|
|
128
|
+
if (starterLebt(s, jetzt))
|
|
129
|
+
return ausNotiz(s.sitzung ?? null, f.starterImNotizOrdner);
|
|
130
|
+
if (s.zustand === "selbst-gestoppt" || s.zustand === "gestoppt")
|
|
131
|
+
return { art: "keine", letzte: s.zustand };
|
|
132
|
+
return { art: "keine", letzte: "abgebrochen" };
|
|
133
|
+
}
|
|
134
|
+
/**
|
|
135
|
+
* Das Python des Aufnehmers ist nicht zu finden (Ruling R-13). Ohne Notiz und ohne offenen Starter kann nichts laufen,
|
|
136
|
+
* was kimal3 gestartet hat -> wie "keine aktive Sitzung". Gibt es aber eine Notiz oder einen Starter in
|
|
137
|
+
* `startet`/`laeuft`, entscheidet allein sein Lebenszeichen: frisch -> laeuft, sonst weiss es niemand.
|
|
138
|
+
* Steht der Starter in einem ENDZUSTAND, ist die Aufnahme zu, auch mit Notiz (Ruling R-17): Er schreibt ihn erst, wenn
|
|
139
|
+
* sein Kind weg ist. Ausnahme `grund: nicht-beendet` mit frischem Lebenszeichen -- die faengt `aufnahmeZustand` als
|
|
140
|
+
* "weiss nicht" ab.
|
|
141
|
+
*/
|
|
142
|
+
export function zustandOhneAufnehmer(f, jetzt) {
|
|
143
|
+
const mitListe = { ...f, aktiveSitzungen: [] };
|
|
144
|
+
const offen = f.starter?.zustand === "startet" || f.starter?.zustand === "laeuft";
|
|
145
|
+
const zu = f.starter !== null && !offen;
|
|
146
|
+
if (zu || f.notiz === null && !offen)
|
|
147
|
+
return aufnahmeZustand(mitListe, jetzt);
|
|
148
|
+
return starterLebt(f.starter, jetzt) ? aufnahmeZustand(mitListe, jetzt) : { art: "unbekannt" };
|
|
149
|
+
}
|
|
150
|
+
export const AUFNEHMER_FASSUNG = "1.3.0";
|
|
151
|
+
/** Die Fassung des Aufnehmers, wie `teile` sie meldet -- nur in der Form `1.3.0…`, alles andere ist keine Angabe (R-14, Punkt 5). */
|
|
152
|
+
export const gueltigeFassung = (w) => (typeof w === "string" && /^\d+\.\d+\.\d+\S*$/.test(w) ? w : null);
|
|
153
|
+
/**
|
|
154
|
+
* Die Antwort von `werkzeug.py teile` -> `Teile`. Ohne Antwort (kein Aufnehmer-Python) oder mit einer unlesbaren gilt
|
|
155
|
+
* alles ausser uv als fehlend: Einrichten holt es dann (wieder), statt einem stummen Aufnehmer zu glauben.
|
|
156
|
+
*/
|
|
157
|
+
export function teileAus(uv, antwort) {
|
|
158
|
+
if (antwort === null || !antwort.ok)
|
|
159
|
+
return { uv, aufnehmer: null, ffmpeg: false, modell: false };
|
|
160
|
+
const d = antwort.daten;
|
|
161
|
+
return { uv, aufnehmer: gueltigeFassung(d.aufnehmer), ffmpeg: d.ffmpeg === true, modell: d.modell === true };
|
|
162
|
+
}
|
|
163
|
+
export const AUFNEHMER_PAKET = `openadapt-capture[transcribe-fast]==${AUFNEHMER_FASSUNG}`; // kein-anzeigetext: Paketname
|
|
164
|
+
/** `uv tool install …` -- `ersetzen`, sobald schon eine Umgebung des Aufnehmers liegt (andere Fassung, kaputt). */
|
|
165
|
+
export function aufnehmerArgumente(ersetzen) {
|
|
166
|
+
// kein-anzeigetext: Befehlszeile
|
|
167
|
+
return ["tool", "install", "--python", "3.12", ...(ersetzen ? ["--force"] : []), AUFNEHMER_PAKET];
|
|
168
|
+
}
|
|
169
|
+
/**
|
|
170
|
+
* Wie uv auf den Rechner kommt: winget zuerst (Coach-Regel: Paketmanager), sonst der offizielle Installer pro Benutzer.
|
|
171
|
+
* Ohne die beiden Zustimmungsschalter haengt winget auf einem frischen Rechner an einer Frage (Spec US-2).
|
|
172
|
+
*/
|
|
173
|
+
export function uvEinrichtung(winget) {
|
|
174
|
+
// kein-anzeigetext: Befehlszeile
|
|
175
|
+
if (winget)
|
|
176
|
+
return { befehl: "winget", argumente: ["install", "--id", "astral-sh.uv", "-e", "--scope", "user", "--accept-source-agreements", "--accept-package-agreements"] };
|
|
177
|
+
// kein-anzeigetext: Befehlszeile
|
|
178
|
+
return { befehl: "powershell", argumente: ["-ExecutionPolicy", "ByPass", "-c", "irm https://astral.sh/uv/install.ps1 | iex"] };
|
|
179
|
+
}
|
|
180
|
+
/** Was beim Einrichten noch fehlt, in der Reihenfolge, in der es gebraucht wird. */
|
|
181
|
+
export function einrichtPlan(t) {
|
|
182
|
+
const fehlt = [
|
|
183
|
+
[t.uv === null, "uv"],
|
|
184
|
+
[t.aufnehmer !== AUFNEHMER_FASSUNG, "aufnehmer"],
|
|
185
|
+
[!t.ffmpeg, "ffmpeg"],
|
|
186
|
+
[!t.modell, "modell"],
|
|
187
|
+
];
|
|
188
|
+
return fehlt.filter(([f]) => f).map(([, schritt]) => schritt);
|
|
189
|
+
}
|
|
190
|
+
/** Wo uv unter Windows liegen kann, wenn es nicht im PATH steht (frisch installiert, Terminal noch alt). */
|
|
191
|
+
export function uvKandidaten(env, home) {
|
|
192
|
+
const lokal = env.LOCALAPPDATA ?? win32.join(home, "AppData", "Local"); // kein-anzeigetext: Ordnerpfad
|
|
193
|
+
return [
|
|
194
|
+
// kein-anzeigetext: Ordnerpfad
|
|
195
|
+
win32.join(lokal, "Microsoft", "WinGet", "Packages", "astral-sh.uv_Microsoft.Winget.Source_8wekyb3d8bbwe", "uv.exe"),
|
|
196
|
+
win32.join(home, ".local", "bin", "uv.exe"),
|
|
197
|
+
win32.join(home, ".cargo", "bin", "uv.exe"),
|
|
198
|
+
];
|
|
199
|
+
}
|
|
200
|
+
/**
|
|
201
|
+
* Welcher Ordner in `material/aufnahmen/` ist die juengste Aufnahme? Nur Namen von `ordnerName` (mit Endung `-2` …)
|
|
202
|
+
* zaehlen; `-10` kommt nach `-9` (als Text stuende es davor). `istAufnahme` sortiert Ordner aus, in denen keine
|
|
203
|
+
* Aufnahme liegt (ein gescheiterter Start hinterlaesst nur Protokoll und Starter-Bericht) -- sonst verdeckte er die
|
|
204
|
+
* letzte echte fuer auswerten und stopp (Endpruefung, Punkt 4). `null`, wenn keiner passt.
|
|
205
|
+
*/
|
|
206
|
+
export function juengsteAufnahme(namen, istAufnahme) {
|
|
207
|
+
const schluessel = (n) => {
|
|
208
|
+
const m = /^(\d{4}-\d{2}-\d{2}-\d{4})(?:-(\d+))?$/.exec(n);
|
|
209
|
+
return m ? { name: n, basis: m[1], nr: m[2] === undefined ? 1 : Number(m[2]) } : null;
|
|
210
|
+
};
|
|
211
|
+
const passend = namen.map(schluessel).filter((k) => k !== null);
|
|
212
|
+
passend.sort((a, b) => (a.basis === b.basis ? b.nr - a.nr : a.basis < b.basis ? 1 : -1));
|
|
213
|
+
return passend.find((k) => istAufnahme(k.name))?.name ?? null;
|
|
214
|
+
}
|
|
215
|
+
/** `zahlenDa`: Dauer, Klicks, Tasten und Ton kamen in der erwarteten Form. Ohne sie ist auch "gestoppt" nicht bestaetigt. */
|
|
216
|
+
export function stoppAusgang(ergebnis, zahlenDa) {
|
|
217
|
+
if (ergebnis === "keine")
|
|
218
|
+
return { satz: "keine", code: 0, notizWeg: true };
|
|
219
|
+
if ((ergebnis === "gestoppt" || ergebnis === "selbst-gestoppt") && zahlenDa)
|
|
220
|
+
return { satz: ergebnis, code: 0, notizWeg: true };
|
|
221
|
+
if (ergebnis === "nicht-sauber" || ergebnis === "abgebrochen")
|
|
222
|
+
return { satz: ergebnis, code: 1, notizWeg: true };
|
|
223
|
+
if (ergebnis === "wird-abgeschlossen")
|
|
224
|
+
return { satz: ergebnis, code: 1, notizWeg: false };
|
|
225
|
+
return { satz: "unklar", code: 1, notizWeg: false };
|
|
226
|
+
}
|
|
@@ -0,0 +1,190 @@
|
|
|
1
|
+
import { spawn, spawnSync } from "node:child_process";
|
|
2
|
+
import { existsSync, mkdirSync, readFileSync, readdirSync, rmSync, writeFileSync } from "node:fs";
|
|
3
|
+
import { homedir, tmpdir } from "node:os";
|
|
4
|
+
import { join, win32 } from "node:path";
|
|
5
|
+
import { fileURLToPath } from "node:url";
|
|
6
|
+
import { juengsteAufnahme, liesWerkzeugAntwort, starterLebt, uvKandidaten } from "./zeigen-rechnung.js";
|
|
7
|
+
/**
|
|
8
|
+
* Die Umgebung hinter `kimal3 zeigen`: wo der Aufnehmer und sein Python liegen, wie das Werkzeug gerufen wird, und
|
|
9
|
+
* die Notiz `~/.kimal3/zeigen.json` (welcher Ordner zu welcher Sitzung gehoert). Entscheidungen trifft
|
|
10
|
+
* `zeigen-rechnung.ts`; hier wird nur gesucht, gestartet, gelesen und geschrieben.
|
|
11
|
+
* 🔴 Ausgaben fremder Programme werden nie weitergereicht: vom Werkzeug zaehlt nur die eine JSON-Zeile (gelesen von
|
|
12
|
+
* `liesWerkzeugAntwort`), stderr wird verworfen.
|
|
13
|
+
*/
|
|
14
|
+
/** `cli/dist/zeigen-umgebung.js` -> `cli/zeigen/werkzeug.py` (im Paket unter `files`, Task 7). */
|
|
15
|
+
const WERKZEUG = fileURLToPath(new URL("../zeigen/werkzeug.py", import.meta.url)); // kein-anzeigetext: Paketpfad
|
|
16
|
+
function laeuft(befehl, argumente) {
|
|
17
|
+
try {
|
|
18
|
+
const r = spawnSync(befehl, argumente, { encoding: "utf8", windowsHide: true, timeout: 20_000 });
|
|
19
|
+
return r.status === 0 ? r.stdout : null;
|
|
20
|
+
}
|
|
21
|
+
catch {
|
|
22
|
+
return null;
|
|
23
|
+
}
|
|
24
|
+
}
|
|
25
|
+
/** Das erste uv, das antwortet: Suchpfad, dann die bekannten Orte (frisch installiert, Terminal noch alt). */
|
|
26
|
+
export function findeUv() {
|
|
27
|
+
for (const uv of ["uv", ...uvKandidaten(process.env, homedir())]) {
|
|
28
|
+
const version = laeuft(uv, ["--version"]);
|
|
29
|
+
if (version !== null)
|
|
30
|
+
return { uv, version };
|
|
31
|
+
}
|
|
32
|
+
return null;
|
|
33
|
+
}
|
|
34
|
+
/** "uv 0.12.10 (…)" -> "0.12.10"; `null`, wenn es kein uv gibt. */
|
|
35
|
+
export function uvFassung() {
|
|
36
|
+
return /^uv (\d+\.\d+\.\d+\S*)/.exec(findeUv()?.version ?? "")?.[1] ?? null;
|
|
37
|
+
}
|
|
38
|
+
/** Python und pythonw des Aufnehmers: `<uv tool dir>/openadapt-capture/Scripts/`, sonst `%APPDATA%\uv\tools\…`. */
|
|
39
|
+
export function aufnehmerPython() {
|
|
40
|
+
const uv = findeUv();
|
|
41
|
+
const toolDir = uv === null ? null : laeuft(uv.uv, ["tool", "dir"])?.trim() || null;
|
|
42
|
+
const appdata = process.env.APPDATA ?? win32.join(homedir(), "AppData", "Roaming"); // kein-anzeigetext: Ordnerpfad
|
|
43
|
+
// Antwortet uv, gilt sein Ordner allein (so greift `UV_TOOL_DIR`); der Standardort nur, wenn uv selbst fehlt (Task 5:
|
|
44
|
+
// sonst faende ein nachgestellter frischer Rechner den echten Aufnehmer unter %APPDATA% doch).
|
|
45
|
+
const orte = toolDir !== null ? [toolDir] : [win32.join(appdata, "uv", "tools")]; // kein-anzeigetext: Ordnerpfad
|
|
46
|
+
for (const ort of orte) {
|
|
47
|
+
const scripts = win32.join(ort, "openadapt-capture", "Scripts"); // kein-anzeigetext: Ordnerpfad
|
|
48
|
+
const python = win32.join(scripts, "python.exe");
|
|
49
|
+
const pythonw = win32.join(scripts, "pythonw.exe");
|
|
50
|
+
if (existsSync(python) && existsSync(pythonw))
|
|
51
|
+
return { python, pythonw };
|
|
52
|
+
}
|
|
53
|
+
return null;
|
|
54
|
+
}
|
|
55
|
+
/** Wohin uv die Programme seiner Werkzeuge legt (`uv tool dir --bin`, beachtet `UV_TOOL_BIN_DIR`). */
|
|
56
|
+
export function uvBinOrdner(uv) {
|
|
57
|
+
return laeuft(uv, ["tool", "dir", "--bin"])?.trim() || null;
|
|
58
|
+
}
|
|
59
|
+
/**
|
|
60
|
+
* Ein fremdes Programm (winget, uv, capture) ohne Fenster und ohne dass eine Zeile davon irgendwo erscheint -- nicht
|
|
61
|
+
* einmal eingesammelt. `true` nur bei Rueckgabewert 0; ob die Sache danach WIRKLICH da ist, prueft der Aufrufer neu.
|
|
62
|
+
* Arbeitsordner ist der Temp-Ordner: nie das Projekt (eine `uv.toml`/`.env` dort gehoert nicht zum Einrichten).
|
|
63
|
+
*/
|
|
64
|
+
export function stillAusfuehren(befehl, argumente, zeitgrenze) {
|
|
65
|
+
try {
|
|
66
|
+
const r = spawnSync(befehl, argumente, { stdio: "ignore", windowsHide: true, cwd: tmpdir(), env: UMGEBUNG(), timeout: zeitgrenze });
|
|
67
|
+
return r.status === 0;
|
|
68
|
+
}
|
|
69
|
+
catch {
|
|
70
|
+
return false;
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
/** Umgebung jedes Werkzeug-Aufrufs, auch des Starters (R-14, Punkt 6); den Rest setzt das Werkzeug selbst. */
|
|
74
|
+
const UMGEBUNG = () => ({ ...process.env, PYTHONUTF8: "1", PYTHONUNBUFFERED: "1" });
|
|
75
|
+
/** Lesende Aufrufe (`teile`, `zustand`) duerfen nicht haengen: nach 20 s gilt die Antwort als "weiss nicht" (R-14, Punkt 7). */
|
|
76
|
+
export const LESEN_MS = 20_000;
|
|
77
|
+
/**
|
|
78
|
+
* `stoppen` antwortet nach spaetestens 495 s selbst (werkzeug.py, STOPP_SEK + WARTEN_SEK). Diese Grenze ist nur das
|
|
79
|
+
* Netz, falls es haengt: Claude Code bricht einen Werkzeugaufruf nach 10 Minuten ab, und dann kaeme gar kein Satz.
|
|
80
|
+
*/
|
|
81
|
+
export const STOPPEN_MS = 540_000;
|
|
82
|
+
/**
|
|
83
|
+
* Ein Unterbefehl des Werkzeugs, synchron. Arbeitsordner = Aufnahmeordner (nie das Projekt: `.env`-Falle).
|
|
84
|
+
* `zeitgrenze` in ms oder `null` (stoppen und auswerten duerfen dauern). Abgebrochen = `status` null = `{ ok: false }`.
|
|
85
|
+
*/
|
|
86
|
+
export function rufeWerkzeug(p, argumente, arbeitsordner, zeitgrenze) {
|
|
87
|
+
try {
|
|
88
|
+
// stderr wird gar nicht erst eingesammelt: Er wird nie gelesen, und ein Fortschrittsbalken (Modell laden, 1,6 GB)
|
|
89
|
+
// koennte sonst `maxBuffer` sprengen -- dann bricht Node den Aufruf ab (Task 5).
|
|
90
|
+
const r = spawnSync(p.python, [WERKZEUG, ...argumente], {
|
|
91
|
+
encoding: "utf8", windowsHide: true, cwd: arbeitsordner ?? tmpdir(), maxBuffer: 1024 * 1024, env: UMGEBUNG(),
|
|
92
|
+
stdio: ["ignore", "pipe", "ignore"],
|
|
93
|
+
...(zeitgrenze === null ? {} : { timeout: zeitgrenze }),
|
|
94
|
+
});
|
|
95
|
+
return liesWerkzeugAntwort(r.status, r.stdout ?? "");
|
|
96
|
+
}
|
|
97
|
+
catch {
|
|
98
|
+
return { ok: false };
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
/** Startet den Starter abgehaengt mit pythonw (kein Fenster, keine Ausgabe). `false`, wenn schon das Starten scheitert. */
|
|
102
|
+
export function starteStarter(p, konfig, ordner) {
|
|
103
|
+
try {
|
|
104
|
+
const kind = spawn(p.pythonw, [WERKZEUG, "starten", konfig], { cwd: ordner, detached: true, stdio: "ignore", windowsHide: true, env: UMGEBUNG() });
|
|
105
|
+
kind.on("error", () => { });
|
|
106
|
+
kind.unref();
|
|
107
|
+
return true;
|
|
108
|
+
}
|
|
109
|
+
catch {
|
|
110
|
+
return false;
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
// ── Die Notiz ─────────────────────────────────────────────────────────────────────────────────────────────────────
|
|
114
|
+
const ORDNER = join(homedir(), ".kimal3");
|
|
115
|
+
const NOTIZ = join(ORDNER, "zeigen.json"); // kein-anzeigetext: Dateiname
|
|
116
|
+
export function leseNotiz() {
|
|
117
|
+
try {
|
|
118
|
+
const n = JSON.parse(readFileSync(NOTIZ, "utf8"));
|
|
119
|
+
const zahl = (w) => typeof w === "number" && Number.isFinite(w);
|
|
120
|
+
if (typeof n.sitzung !== "string" || typeof n.ordner !== "string" || !zahl(n.gestartet) || !zahl(n.frist))
|
|
121
|
+
return null;
|
|
122
|
+
return { sitzung: n.sitzung, ordner: n.ordner, gestartet: n.gestartet, frist: n.frist };
|
|
123
|
+
}
|
|
124
|
+
catch {
|
|
125
|
+
return null;
|
|
126
|
+
}
|
|
127
|
+
}
|
|
128
|
+
/** `false`, wenn die Notiz nicht geschrieben werden konnte -- die Aufnahme laeuft trotzdem (status findet sie ueber den Ordner). */
|
|
129
|
+
export function schreibeNotiz(n) {
|
|
130
|
+
try {
|
|
131
|
+
mkdirSync(ORDNER, { recursive: true, mode: 0o700 }); // wie wache-schalter.ts: wer den Ordner anlegt, legt die Rechte fest
|
|
132
|
+
writeFileSync(NOTIZ, JSON.stringify(n, null, 2), "utf8");
|
|
133
|
+
return true;
|
|
134
|
+
}
|
|
135
|
+
catch {
|
|
136
|
+
return false;
|
|
137
|
+
}
|
|
138
|
+
}
|
|
139
|
+
export function loescheNotiz() {
|
|
140
|
+
try {
|
|
141
|
+
rmSync(NOTIZ, { force: true });
|
|
142
|
+
}
|
|
143
|
+
catch {
|
|
144
|
+
/* bleibt liegen -- status vergleicht sie ohnehin mit den Sitzungen des Aufnehmers */
|
|
145
|
+
}
|
|
146
|
+
}
|
|
147
|
+
// ── Ordner und Bericht des Starters ───────────────────────────────────────────────────────────────────────────────
|
|
148
|
+
/**
|
|
149
|
+
* Der juengste Aufnahmeordner unter `aufnahmen`, oder `null`. Zaehlt nur, wo eine Aufnahme liegt (`roh/recording.db`)
|
|
150
|
+
* oder ein Starter noch lebt (er startet gerade, die Datei kommt erst noch). `roh/` gehoert allein dem Aufnehmer
|
|
151
|
+
* (gemeinsam.py, ROH: sein Siegel bricht an jeder Datei, die wir dort schreiben).
|
|
152
|
+
*/
|
|
153
|
+
export function juengsterOrdner(aufnahmen) {
|
|
154
|
+
try {
|
|
155
|
+
const namen = readdirSync(aufnahmen, { withFileTypes: true }).filter((e) => e.isDirectory()).map((e) => e.name);
|
|
156
|
+
const istAufnahme = (name) => existsSync(join(aufnahmen, name, "roh", "recording.db")) || // kein-anzeigetext: Dateiname
|
|
157
|
+
starterLebt(starterFakt(leseStarter(join(aufnahmen, name))), Date.now() / 1000);
|
|
158
|
+
const name = juengsteAufnahme(namen, istAufnahme);
|
|
159
|
+
return name === null ? null : join(aufnahmen, name);
|
|
160
|
+
}
|
|
161
|
+
catch {
|
|
162
|
+
return null;
|
|
163
|
+
}
|
|
164
|
+
}
|
|
165
|
+
/** Der Bericht des Starters (`starter.json`), roh; `null`, wenn er fehlt oder kaputt ist. */
|
|
166
|
+
export function leseStarter(ordner) {
|
|
167
|
+
try {
|
|
168
|
+
const d = JSON.parse(readFileSync(join(ordner, "starter.json"), "utf8")); // kein-anzeigetext: Dateiname
|
|
169
|
+
return d && typeof d === "object" && !Array.isArray(d) ? d : null;
|
|
170
|
+
}
|
|
171
|
+
catch {
|
|
172
|
+
return null;
|
|
173
|
+
}
|
|
174
|
+
}
|
|
175
|
+
const STARTER_ZUSTAENDE = ["startet", "laeuft", "gestoppt", "selbst-gestoppt", "abgebrochen", "fehlgeschlagen"];
|
|
176
|
+
/** Bericht des Starters -> `Fakten.starter`; alles, was nicht die erwartete Form hat, faellt weg. */
|
|
177
|
+
export function starterFakt(roh) {
|
|
178
|
+
if (!roh || typeof roh !== "object")
|
|
179
|
+
return null;
|
|
180
|
+
const r = roh;
|
|
181
|
+
const zustand = STARTER_ZUSTAENDE.find((z) => z === r.zustand);
|
|
182
|
+
if (zustand === undefined)
|
|
183
|
+
return null;
|
|
184
|
+
return {
|
|
185
|
+
zustand,
|
|
186
|
+
...(typeof r.sitzung === "string" ? { sitzung: r.sitzung } : {}),
|
|
187
|
+
...(typeof r.zuletzt === "number" && Number.isFinite(r.zuletzt) ? { zuletzt: r.zuletzt } : {}),
|
|
188
|
+
...(typeof r.grund === "string" ? { grund: r.grund } : {}),
|
|
189
|
+
};
|
|
190
|
+
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "kimal3",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.5.0",
|
|
4
4
|
"description": "Trag deine KI-Projekte dort ein, wo sie entstehen: im Terminal. · Log your AI projects where they happen: in the terminal.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"bin": {
|
|
@@ -10,6 +10,8 @@
|
|
|
10
10
|
"dist",
|
|
11
11
|
"skill",
|
|
12
12
|
"claude-plugins",
|
|
13
|
+
"coach",
|
|
14
|
+
"zeigen",
|
|
13
15
|
"README.md"
|
|
14
16
|
],
|
|
15
17
|
"engines": {
|