kimal3 0.9.1 → 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.
- package/coach/de/kimal3-coach.md +2 -2
- package/coach/en/kimal3-coach.md +2 -2
- package/dist/befehle/zeigen-einrichten.js +29 -9
- package/dist/befehle/zeigen-pruefen.js +56 -0
- package/dist/befehle/zeigen-start.js +8 -10
- package/dist/befehle/zeigen.js +20 -8
- package/dist/befehle-katalog-en.js +4 -4
- package/dist/befehle-katalog.js +2 -2
- package/dist/befehlswoerter.js +4 -0
- package/dist/kimal3.js +2 -0
- package/dist/texte/zeigen.js +54 -2
- package/dist/zeigen-freigaben.js +65 -0
- package/dist/zeigen-rechnung.js +72 -19
- package/dist/zeigen-umgebung.js +9 -13
- package/package.json +1 -1
- package/skill/commands/k3plan.md +3 -3
- package/skill/commands/k3zeigen.md +154 -8
- package/skill/en/commands/k3plan.md +3 -3
- package/skill/en/commands/k3show.md +150 -8
- package/zeigen/gemeinsam.py +86 -4
- package/zeigen/starter.py +24 -8
- package/zeigen/werkzeug.py +17 -9
- package/zeigen/zeitleiste.py +59 -16
|
@@ -29,7 +29,8 @@ the cases of this command.
|
|
|
29
29
|
step.** You open a file from the recording for the person only in Notepad, and in a way that
|
|
30
30
|
your call returns at once: in Bash `cmd //c start "" notepad "<path>"`, in PowerShell
|
|
31
31
|
`Start-Process notepad -ArgumentList '"<path>"'`. A bare `notepad "<path>"` blocks until the
|
|
32
|
-
window is closed again.
|
|
32
|
+
window is closed again. On the Mac you open it in TextEdit: `open -e "<path>"` — that returns at
|
|
33
|
+
once. Never in an IDE or an editor that Claude Code is attached to (selected
|
|
33
34
|
lines end up in the conversation there by themselves).
|
|
34
35
|
3. **A failure never looks like a success.** If a `kimal3 show …` doesn't end with `0`, say
|
|
35
36
|
honestly what its message says, and don't carry on as if it had worked.
|
|
@@ -38,6 +39,10 @@ Every `kimal3 show …` runs **in the project folder** (step 3) — the program
|
|
|
38
39
|
through the folder it is started in. If a recording lies in another project, change into it first:
|
|
39
40
|
the project folder is the part of the path before `material/aufnahmen/`.
|
|
40
41
|
|
|
42
|
+
If Claude Code's sandbox is switched on, you run every `kimal3 show …` **outside the sandbox** (in
|
|
43
|
+
the Bash tool with `dangerouslyDisableSandbox`): the program writes to `~/.kimal3` and `~/.local`,
|
|
44
|
+
and that can fail inside the sandbox.
|
|
45
|
+
|
|
41
46
|
## 1. Arrive and look
|
|
42
47
|
|
|
43
48
|
First run `kimal3 show status`. It only reads. **Don't** read the “Set up:” block out to the
|
|
@@ -46,7 +51,8 @@ person — just note whether anything in it is missing (a line with “missing
|
|
|
46
51
|
|
|
47
52
|
| The output says | What you do |
|
|
48
53
|
|---|---|
|
|
49
|
-
|
|
|
54
|
+
| “`kimal3 show` is available on Windows and the Mac.” (exit code 1) | Say exactly that, kindly: showing a task doesn't work on this computer, only on Windows and on the Mac. Offer that the person describes the task in words instead. The command ends here. |
|
|
55
|
+
| “Recording needs macOS 13 or later — this Mac has macOS 12.” or, if the version can't be read, “Recording needs macOS 13 or later.” (exit code 1) | Say exactly that, kindly: on this Mac, showing a task only works with macOS 13 or later. Offer that the person describes the task in words instead. The command ends here. |
|
|
50
56
|
| “I can't tell whether a recording is running.” (exit code 1) | Say honestly that you can't tell right now. **Start nothing.** Offer to look again in a minute. |
|
|
51
57
|
| “Running since 09:12 (3 minutes), folder `…`, stops at 09:27 at the latest.” | A recording from an earlier session is still running. If the folder is a full path outside this project, first change into its project folder. Then ask: “There's still a recording running from 09:12 — are you through with showing the task? Then I'll stop it now.” Yes → step 8. |
|
|
52
58
|
| “Running (started in another session).” | A recording is running that this project can't match. Say so, **don't start a second one**, and suggest checking with `kimal3 show status` once it has ended. |
|
|
@@ -54,6 +60,29 @@ person — just note whether anything in it is missing (a line with “missing
|
|
|
54
60
|
| “No recording is running.” followed by “The last one (`…`) broke off.” | Kindly: the last recording broke off in the middle — the person did nothing wrong. Offer to show the task again. |
|
|
55
61
|
| “No recording is running.” | Carry on. |
|
|
56
62
|
|
|
63
|
+
**Only on the Mac** there is one more line before the last one: “Permissions go to: …”. That is how
|
|
64
|
+
you know, for the whole conversation, that you are on a Mac. Remember the name after it (usually
|
|
65
|
+
“Terminal”) — below it is called **<App>**.
|
|
66
|
+
|
|
67
|
+
- **“Permissions go to: Claude (desktop app)”** → say this **first**, before anything else:
|
|
68
|
+
|
|
69
|
+
> “On the Mac, recording works reliably only from Terminal: in this app macOS sometimes doesn't
|
|
70
|
+
> ask for the permissions at all, and after every Claude update you'd have to grant them again.
|
|
71
|
+
> Shall I open Terminal for you in the right folder? There you type `claude` and then `/k3show` —
|
|
72
|
+
> that's a new conversation, just tell me the task there again. Or shall we try it here anyway?”
|
|
73
|
+
|
|
74
|
+
**Yes** → `open -a Terminal "<project folder>"` (if there is no project yet, the folder the session
|
|
75
|
+
is in right now). Say that Terminal is open now; the command ends here. **“Here anyway”** → carry
|
|
76
|
+
on as usual; <App> is then “Claude”.
|
|
77
|
+
- **“Permissions go to: nobody — macOS can't grant permissions over SSH”** → say exactly that: over
|
|
78
|
+
a remote connection macOS can't grant the permissions for screen, keys and microphone. Suggest
|
|
79
|
+
opening a Terminal on the Mac itself and starting `claude` and `/k3show` there. The command ends
|
|
80
|
+
here.
|
|
81
|
+
- **“Permissions go to: the app you started Claude from”** → ask **right here**, once, what the app
|
|
82
|
+
Claude runs in is called (suggestion: “Terminal?”), and use the answer as <App> — so the right name
|
|
83
|
+
is already in the announcement of step 4. Only after that comes the question about the task (one
|
|
84
|
+
question at a time).
|
|
85
|
+
|
|
57
86
|
The last line says whether the folder is right — remember it for step 3:
|
|
58
87
|
“Recordings go here: …” (right) · “Switch into a project first …” (the session is in the
|
|
59
88
|
workbench) · “The project folder is nested too deeply for the recorder …” (the path is too long).
|
|
@@ -90,28 +119,135 @@ question (step 11). A rough sentence is enough; don't dig.
|
|
|
90
119
|
|
|
91
120
|
## 4. Setting up — only if something is missing
|
|
92
121
|
|
|
93
|
-
If nothing is missing, skip this step without a word about it
|
|
94
|
-
**exactly this first** and wait for the answer:
|
|
122
|
+
If nothing is missing, skip this step without a word about it (on the Mac carry on with step 4b).
|
|
123
|
+
If something is missing, say **exactly this first** and wait for the answer:
|
|
95
124
|
|
|
96
125
|
> “For this I'll set up the recorder once — a free program that records screen, sound, clicks
|
|
97
126
|
> and keys. About 2.1 GB, 10 to 20 minutes depending on your connection, only in your user
|
|
98
127
|
> folder, no admin rights. Because it records keystrokes, your company's antivirus may flag it;
|
|
99
128
|
> on a work computer, check with IT first if in doubt. Shall I?”
|
|
100
129
|
|
|
130
|
+
**On the Mac** you say this sentence instead, with <App> from step 1:
|
|
131
|
+
|
|
132
|
+
> “For this I'll set up the recorder once — a free program that records screen, sound, clicks and
|
|
133
|
+
> keys. About 2 GB, 10 to 20 minutes depending on your connection, only in your user folder. After
|
|
134
|
+
> that you switch on four permissions for <App> in System Settings — I'll show you each one. macOS
|
|
135
|
+
> asks for an admin password along the way (on your own Mac that's your login password), and <App>
|
|
136
|
+
> restarts once. On a work Mac this often only works with IT. Shall I?”
|
|
137
|
+
|
|
101
138
|
- **Yes:** start `kimal3 show setup` **in the background** — it can take longer than the ten
|
|
102
139
|
minutes a command may run in the foreground. Say that it is running and the person can do
|
|
103
140
|
something else meanwhile. **Wait for the notice that it has finished** — don't check in between,
|
|
104
141
|
and don't sleep in a loop. After that:
|
|
105
142
|
- exit code `0` → `kimal3 show status`; if all four parts are there (uv, recorder, FFmpeg,
|
|
106
|
-
speech model), carry on with step 5.
|
|
143
|
+
speech model), carry on with step 5 (on the Mac with step 4b).
|
|
107
144
|
- exit code `1` (“Step … didn't work.”) → name that step, and offer a ready-made
|
|
108
|
-
message to IT: **what** (uv from Astral · the recorder openadapt-capture 1.3.0, MIT licence,
|
|
145
|
+
message to IT (on the Mac the one below): **what** (uv from Astral · the recorder openadapt-capture 1.3.0, MIT licence,
|
|
109
146
|
with Python 3.12 · FFmpeg · the speech model large-v3-turbo from Hugging Face), **what for**
|
|
110
147
|
(showing a work task once on screen and evaluating it on the computer), **where from**
|
|
111
148
|
(astral.sh or winget, pypi.org, huggingface.co), **which folders** (only the user folder, no
|
|
112
149
|
admin rights).
|
|
113
150
|
- **No:** “No problem — you can do it any time later with `/k3show`.” The command ends here.
|
|
114
151
|
|
|
152
|
+
**The message to IT on the Mac:** **what** (uv from Astral, or via Homebrew if it is there · the
|
|
153
|
+
recorder openadapt-capture 1.3.0, MIT licence, with Python 3.12 and the add-on
|
|
154
|
+
pyobjc-framework-AVFoundation 12.2.2 · FFmpeg · the speech model large-v3-turbo from Hugging Face),
|
|
155
|
+
**what for** (showing a work task once on screen and evaluating it on the computer), **where from**
|
|
156
|
+
(astral.sh or Homebrew, pypi.org, github.com, huggingface.co), **which folders** (only the user
|
|
157
|
+
folder) — and on top **the four permissions** under System Settings → Privacy & Security (Screen &
|
|
158
|
+
System Audio Recording, Input Monitoring, Accessibility, Microphone), **for which app** (<App>), and
|
|
159
|
+
that a standard account may not switch on two of them itself (Screen & System Audio Recording,
|
|
160
|
+
Input Monitoring).
|
|
161
|
+
|
|
162
|
+
## 4b. Permissions — only on the Mac
|
|
163
|
+
|
|
164
|
+
On Windows you skip this step. On the Mac (the status has the line “Permissions go to …”) it comes
|
|
165
|
+
**before every** briefing in step 5: without the four permissions the recorder records nothing
|
|
166
|
+
useful. Run `kimal3 show check`.
|
|
167
|
+
|
|
168
|
+
| The output says | What you do |
|
|
169
|
+
|---|---|
|
|
170
|
+
| “All four permissions for <App> are in place.” (exit code 0) | Carry on with step 5 without a word about it. |
|
|
171
|
+
| “Permissions for <App>:” and below it, per permission, “in place”, “missing” or “can't tell” (exit code 1) | Not a failure, but the list of what is still missing → see below. Treat a line with “can't tell” like “missing”, but never call it “in place” or “missing” to the person. |
|
|
172
|
+
| “I can't tell whether the permissions are in place.” (exit code 1) | Say honestly that you can't tell right now. Suggest going through the four switches anyway (below) — it can't do any harm. |
|
|
173
|
+
| “The recorder isn't set up yet: `kimal3 show setup`.” | Back to step 4. |
|
|
174
|
+
|
|
175
|
+
If “Permissions for” is followed only by “this app”, use as <App> the name the person gave you in
|
|
176
|
+
step 1; if you don't have one yet, ask once now (suggestion: “Terminal?”).
|
|
177
|
+
|
|
178
|
+
**If something is missing**, say once beforehand (if fewer than four are missing, say the number
|
|
179
|
+
instead of “four”):
|
|
180
|
+
|
|
181
|
+
> “I'll now show you four switches, one after another. If macOS offers ‘Quit & Reopen’ along the
|
|
182
|
+
> way, click ‘Later’ until all four are on — then we restart once.”
|
|
183
|
+
|
|
184
|
+
Then **for each missing permission**, in the order of the list, one after the other:
|
|
185
|
+
|
|
186
|
+
1. **One sentence on what it is:**
|
|
187
|
+
Screen & System Audio Recording — so the recorder sees what happens on the screen · Input
|
|
188
|
+
Monitoring — so it can write down your clicks and keys · Accessibility — so it can read the names
|
|
189
|
+
of the buttons and fields you click · Microphone — so it hears what you say.
|
|
190
|
+
2. `kimal3 show check --ask <permission>` (`screen`, `input`, `accessibility` or `microphone`) — with
|
|
191
|
+
this macOS puts <App> into the list for this permission. Say beforehand: for the microphone macOS
|
|
192
|
+
asks itself, click “Allow” there; if macOS shows a window for the others, any button in it is
|
|
193
|
+
fine — you'll open the page yourself in a moment. The output is the list again; read **only the
|
|
194
|
+
line of this permission** in it. If it says “in place” (often for the microphone after “Allow”),
|
|
195
|
+
it's done → the next one.
|
|
196
|
+
3. `kimal3 show check --open <permission>` opens exactly this page of System Settings (“I opened
|
|
197
|
+
‘…’ in System Settings.”). If it says “System Settings couldn't be opened.”, name the way by hand:
|
|
198
|
+
Apple menu → System Settings → Privacy & Security → the name of the permission.
|
|
199
|
+
4. **The click path:** “In the list, switch on ‘<App>’.” For the first one, Screen & System Audio
|
|
200
|
+
Recording, more precisely: “in the **upper** section ‘Screen & System Audio Recording’, not under
|
|
201
|
+
‘System Audio Recording Only’”. If <App> is missing from the list: “Click the plus at the bottom →
|
|
202
|
+
Applications → Utilities → Terminal” (for another app: Applications → <App>).
|
|
203
|
+
For **Screen & System Audio Recording** and **Input Monitoring** add: “macOS asks for an admin
|
|
204
|
+
password along the way — on your own Mac that's your login password.” If the person has none (work
|
|
205
|
+
Mac, standard account): offer the message to IT from step 4; the command ends here.
|
|
206
|
+
5. Wait until the person says the switch is on. **Don't check now** — macOS often only reports a
|
|
207
|
+
new permission after the restart. Carry on with the next one.
|
|
208
|
+
|
|
209
|
+
**Once all are on:** `kimal3 show check` again.
|
|
210
|
+
|
|
211
|
+
- “All four permissions for <App> are in place.” → carry on with step 5.
|
|
212
|
+
- Still “missing” (or “can't tell”) for a permission the person has switched on → **first the
|
|
213
|
+
restart** (below). Do **not** send them into the settings again.
|
|
214
|
+
- “missing” for one you haven't gone through yet → back to it, point 1.
|
|
215
|
+
|
|
216
|
+
### The restart
|
|
217
|
+
|
|
218
|
+
**Beforehand**, put the way back into the clipboard — **always** with the folder in which **this
|
|
219
|
+
Claude session was started** (“Primary working directory” in your environment), **never** the
|
|
220
|
+
project folder, even if you are working there now: `claude --continue` only continues the
|
|
221
|
+
conversation that began in exactly that folder.
|
|
222
|
+
|
|
223
|
+
```bash
|
|
224
|
+
printf '%s' "cd '<folder this Claude session was started in>' && claude --continue" | LC_CTYPE=UTF-8 pbcopy
|
|
225
|
+
```
|
|
226
|
+
|
|
227
|
+
(If the folder name contains a `'`, write it as `'\''`. `LC_CTYPE=UTF-8` keeps umlauts and other
|
|
228
|
+
accented letters in the folder name intact.) **Also show** the same line `cd '…' && claude --continue`
|
|
229
|
+
visibly in the chat, as its own code block — if pasting doesn't work, the person types it from
|
|
230
|
+
there. Then say word for word:
|
|
231
|
+
|
|
232
|
+
> “Now quit Terminal completely once: ⌘ + Q (or in the menu ‘Terminal → Quit Terminal’). If
|
|
233
|
+
> Terminal asks whether to terminate running processes: ‘Terminate’. Then reopen Terminal (⌘ +
|
|
234
|
+
> Space, type ‘Terminal’, Enter), paste with ⌘ + V and press Enter. I've already copied the command
|
|
235
|
+
> for you — our conversation continues right here.”
|
|
236
|
+
|
|
237
|
+
Add in the same message: “Then just type ‘continue’, and I'll check whether the permissions are in
|
|
238
|
+
place now.” — in the **same** message as the sentence above: after the restart nothing new comes
|
|
239
|
+
from you until the person writes something. If <App> isn't “Terminal”, put in its name. If Claude runs in the desktop app, this way
|
|
240
|
+
doesn't fit: offer Terminal instead (sentence and `open -a Terminal` from step 1).
|
|
241
|
+
|
|
242
|
+
When the person is back, check — **from the project folder**, like every `kimal3 show …`:
|
|
243
|
+
`cd "<project folder>" && kimal3 show check`.
|
|
244
|
+
|
|
245
|
+
- “All four permissions for <App> are in place.” → carry on with step 5.
|
|
246
|
+
- Still “missing”: open exactly this page again (`--open`) and have them look whether the switch for
|
|
247
|
+
<App> is on. If it was off: have it switched on, then one more restart as above. If it was on: say
|
|
248
|
+
honestly that macOS doesn't accept the permission, and offer the message to IT from step 4. The
|
|
249
|
+
command ends here.
|
|
250
|
+
|
|
115
251
|
## 5. The briefing before the start
|
|
116
252
|
|
|
117
253
|
Say these four points before anything records:
|
|
@@ -124,6 +260,11 @@ Say these four points before anything records:
|
|
|
124
260
|
4. When you're done, come back here and type **“done”**; the recording stops by itself after 15
|
|
125
261
|
minutes at the latest.
|
|
126
262
|
|
|
263
|
+
**On the Mac** point 1 says, instead of “switch on ‘Do not disturb’”: “switch on the ‘Do Not
|
|
264
|
+
Disturb’ Focus (Control Center → Focus)”. And a fifth point is added:
|
|
265
|
+
|
|
266
|
+
5. If macOS asks midway whether <App> may keep recording: click ‘Allow’ and carry on.
|
|
267
|
+
|
|
127
268
|
Then: “Type ‘go’ when you're ready.” — and wait.
|
|
128
269
|
|
|
129
270
|
- **A clear go** — “go”, “ready”, “start” — → step 6.
|
|
@@ -146,6 +287,7 @@ explicitly wants it shorter.
|
|
|
146
287
|
| “A recording is already running (started in another session).” | Ask: “There's still a recording running from another session — shall I stop it so that we can start again?” Yes → `kimal3 show stop`; if it says it can't match it here, say so honestly and suggest waiting until it stops by itself (`kimal3 show status`). |
|
|
147
288
|
| “I can't tell whether a recording is running — so I won't start a second one.” | Say it honestly, force nothing, look again later. |
|
|
148
289
|
| “The recorder isn't set up yet: `kimal3 show setup`.” | Back to step 4. |
|
|
290
|
+
| “The recording didn't start: the … permission is missing — `kimal3 show check` shows what's missing. Log: …” | Name the permission. **Don't open the log.** Back to step 4b. |
|
|
149
291
|
| “The recording didn't start: <reason>. Log: …” | Name the reason. **Don't open the log** — it contains window titles. If “Whether the recorder is still running: `kimal3 show status`.” follows, run `kimal3 show status` and say what it reports. Offer another attempt. |
|
|
150
292
|
| “Switch into a project first …” · “The project folder is nested too deeply …” | Back to step 3. |
|
|
151
293
|
|
|
@@ -174,7 +316,7 @@ before the 10 minutes are up.
|
|
|
174
316
|
| “No recording is running.” | Check with `kimal3 show status`. If it says “The last one (`…`) stopped by itself after … minutes.”, carry on with step 9; otherwise say honestly that there is no recording. |
|
|
175
317
|
| “I can't tell whether a recording is running.” | Say it honestly — **never** “stopped”. Offer to look again in a minute with `kimal3 show status`. |
|
|
176
318
|
| “The recording wasn't closed cleanly: <reason>.” (exit code 1) | Name the reason and offer a new recording. |
|
|
177
|
-
| “The recording broke off: <reason>. …” (exit code 1) | Kindly: the person did nothing wrong. Name the reason and offer a new recording. |
|
|
319
|
+
| “The recording broke off: <reason>. …” (exit code 1) | Kindly: the person did nothing wrong. Name the reason and offer a new recording (if the reason names a permission: step 4b first). |
|
|
178
320
|
| “The recording couldn't be stopped …” · “A recording is running that I can't match here …” | Run `kimal3 show status` and say honestly what it reports — **never** “stopped”. If the session is in the wrong folder, change into the recording's project. |
|
|
179
321
|
|
|
180
322
|
## 9. Evaluating — on the person's computer
|
|
@@ -222,7 +364,7 @@ Then this question, word for word:
|
|
|
222
364
|
> leave out the typed parts?”
|
|
223
365
|
|
|
224
366
|
- **“Open it”:** in Notepad, as in rule 2 (`cmd //c start "" notepad "<full path>"` or
|
|
225
|
-
`Start-Process`), with the full path to `zeitleiste.md` (the project folder plus the path from
|
|
367
|
+
`Start-Process`; on the Mac in TextEdit with `open -e "<full path>"`), with the full path to `zeitleiste.md` (the project folder plus the path from
|
|
226
368
|
step 9). The person reads it themselves; you don't read along. Then ask the question about
|
|
227
369
|
agreeing once more.
|
|
228
370
|
- **“Without the typed parts”:** `kimal3 show evaluate --without-typed` — with the same
|
package/zeigen/gemeinsam.py
CHANGED
|
@@ -1,8 +1,11 @@
|
|
|
1
1
|
# Gemeinsame Teile von werkzeug.py und starter.py: Dateinamen, Umgebung des Aufnehmers, Musterliste der Fehlergruende,
|
|
2
2
|
# JSON lesen/schreiben, Stopp einer Sitzung. Kein eigener Einstieg; laeuft mit dem Python des Aufnehmers.
|
|
3
|
+
import contextlib
|
|
3
4
|
import json
|
|
4
5
|
import os
|
|
5
6
|
import re
|
|
7
|
+
import sys
|
|
8
|
+
import threading
|
|
6
9
|
import time
|
|
7
10
|
|
|
8
11
|
# Der Aufnehmer bekommt einen eigenen Unterordner, in den sonst niemand schreibt: Beim Abschliessen versiegelt er JEDE
|
|
@@ -14,14 +17,30 @@ STARTER = "starter.json"
|
|
|
14
17
|
PROTOKOLL = "aufnahme.log"
|
|
15
18
|
ABBRECHEN = "abbrechen"
|
|
16
19
|
ZUSTAND_AUFNEHMER = "capture-state.json"
|
|
17
|
-
|
|
20
|
+
# Die Plattform, auf der dieses Python laeuft. Tests setzen sie (gemeinsam.PLATTFORM, starter.PLATTFORM); die Rechnungen
|
|
21
|
+
# darunter nehmen sie als Parameter, damit beide Seiten ohne Mac pruefbar sind.
|
|
22
|
+
PLATTFORM = sys.platform
|
|
23
|
+
ANFRAGE_WARTE_SEK = 60 # so lange wartet die Mikrofon-Anfrage auf die Antwort des Menschen im Systemdialog
|
|
24
|
+
_UMGEBUNG = {
|
|
18
25
|
"STOP_SEQUENCES": '[["o","a",".","s","t","o","p"]]', # die Strg-Folge des Aufnehmers beendet sonst bei Strg+C (B1)
|
|
19
26
|
"RECORD_AUDIO_RETAIN_WAVEFORM": "true",
|
|
20
27
|
"PYTHONUNBUFFERED": "1",
|
|
21
28
|
"PYTHONUTF8": "1",
|
|
22
29
|
}
|
|
23
30
|
|
|
24
|
-
|
|
31
|
+
|
|
32
|
+
def umgebung(plattform: str) -> dict:
|
|
33
|
+
"""Umgebung des Aufnehmers. Auf dem Mac zusaetzlich ein FESTER Steuerordner im Heim: Ohne ihn legt der Aufnehmer ihn
|
|
34
|
+
unter $TMPDIR an, und eine zweite Claude-Sitzung (anderes $TMPDIR) fande die laufende Aufnahme nicht."""
|
|
35
|
+
env = dict(_UMGEBUNG)
|
|
36
|
+
if plattform == "darwin":
|
|
37
|
+
env["OPENADAPT_CAPTURE_RUNTIME_DIR"] = os.path.join(os.path.expanduser("~"), ".kimal3", "aufnehmer-steuerung")
|
|
38
|
+
return env
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
# Zwilling von GRUENDE_WINDOWS / GRUENDE_MAC in cli/src/zeigen-rechnung.ts -- gleiche Reihenfolge, gleiche Schalter (der
|
|
42
|
+
# Test vergleicht beide). Die Mac-Muster sind die festen Saetze des Aufnehmers, nie "Accessibility" allein: ein
|
|
43
|
+
# Fenstertitel "Accessibility Settings" ist kein Grund.
|
|
25
44
|
GRUENDE = [
|
|
26
45
|
(re.compile(r"FFmpegUnavailableError|ffmpeg.*not found", re.IGNORECASE), "ffmpeg-fehlt"),
|
|
27
46
|
(re.compile(r"returned success without a non-empty output"), "pfad-zu-lang"),
|
|
@@ -29,10 +48,73 @@ GRUENDE = [
|
|
|
29
48
|
(re.compile(r"DesktopCaptureError|screen layout"), "bildschirm-gesperrt"),
|
|
30
49
|
(re.compile(r"did not become ready|did not start"), "startet-nicht"),
|
|
31
50
|
]
|
|
51
|
+
GRUENDE_MAC = [
|
|
52
|
+
(re.compile(r"FFmpegUnavailableError|ffmpeg.*not found", re.IGNORECASE), "ffmpeg-fehlt"),
|
|
53
|
+
(re.compile(r"denied global input observation"), "freigabe-eingabe"),
|
|
54
|
+
(re.compile(r"denied the ordered input barrier"), "freigabe-bedienungshilfen"),
|
|
55
|
+
(re.compile(r"disabled the input event tap"), "abgriff-abgeschaltet"),
|
|
56
|
+
(re.compile(r"PortAudio|No Default Input Device|sounddevice"), "kein-mikrofon"),
|
|
57
|
+
(re.compile(r"DesktopCaptureError|screen layout"), "bildschirm-gesperrt"),
|
|
58
|
+
(re.compile(r"did not become ready|did not start"), "startet-nicht"),
|
|
59
|
+
]
|
|
60
|
+
|
|
61
|
+
|
|
62
|
+
def fehler_grund(protokoll: str, plattform: str) -> str:
|
|
63
|
+
"""Alles ausser win32 hat die Mac-Liste (wie in TS). Die Plattform ist Pflicht: Still die eigene anzunehmen liess
|
|
64
|
+
eine Windows-Pruefung auf einem echten Mac mit der Mac-Liste laufen (Messung 2, docs/qa/2026-10-24-zeigen-mac-messungen.md)."""
|
|
65
|
+
gruende = GRUENDE if plattform == "win32" else GRUENDE_MAC
|
|
66
|
+
return next((code for muster, code in gruende if muster.search(protokoll)), "unbekannt")
|
|
32
67
|
|
|
33
68
|
|
|
34
|
-
def
|
|
35
|
-
|
|
69
|
+
def _wahrheitswert(wert) -> bool | None:
|
|
70
|
+
"""Nur ein echter Wahrheitswert zaehlt; alles andere ist "weiss ich nicht" (nie "erteilt")."""
|
|
71
|
+
return wert if isinstance(wert, bool) else None
|
|
72
|
+
|
|
73
|
+
|
|
74
|
+
def _mikrofon_status(status) -> bool | None:
|
|
75
|
+
# AVAuthorizationStatus: 0 nicht gefragt, 1 eingeschraenkt, 2 abgelehnt, 3 erteilt. Alles andere: weiss ich nicht.
|
|
76
|
+
if isinstance(status, bool) or not isinstance(status, int):
|
|
77
|
+
return None
|
|
78
|
+
return status == 3 if status in (0, 1, 2, 3) else None
|
|
79
|
+
|
|
80
|
+
|
|
81
|
+
def _frage_mikrofon(av) -> None:
|
|
82
|
+
antwort = threading.Event()
|
|
83
|
+
av.AVCaptureDevice.requestAccessForMediaType_completionHandler_(av.AVMediaTypeAudio, lambda erteilt: antwort.set())
|
|
84
|
+
antwort.wait(ANFRAGE_WARTE_SEK)
|
|
85
|
+
|
|
86
|
+
|
|
87
|
+
def freigaben_abfragen(anfragen: str | None = None) -> dict:
|
|
88
|
+
"""Fragt macOS, was dieser Prozess darf -- ohne etwas zu oeffnen (das Mikrofon wird nur nach seinem Status gefragt).
|
|
89
|
+
Jede Freigabe einzeln: fehlt das Modul oder wirft der Aufruf, ist genau diese `None` ("kann ich nicht feststellen").
|
|
90
|
+
`anfragen` loest VOR dem Abfragen genau diese eine Systemanfrage aus. Waehrend der Aufrufe ist stdout/stderr
|
|
91
|
+
umgeleitet: Eine fremde Zeile darf nie in die eine JSON-Zeile der Ausgabe geraten."""
|
|
92
|
+
def aus(name: str):
|
|
93
|
+
import importlib
|
|
94
|
+
return importlib.import_module(name)
|
|
95
|
+
|
|
96
|
+
def versuche(rechne):
|
|
97
|
+
try:
|
|
98
|
+
return rechne()
|
|
99
|
+
except Exception:
|
|
100
|
+
return None
|
|
101
|
+
|
|
102
|
+
ergebnis = {}
|
|
103
|
+
with open(os.devnull, "w") as stumm, contextlib.redirect_stdout(stumm), contextlib.redirect_stderr(stumm):
|
|
104
|
+
anfrage_fuer = {
|
|
105
|
+
"bildschirm": lambda: aus("Quartz").CGRequestScreenCaptureAccess(),
|
|
106
|
+
"eingabe": lambda: aus("Quartz").CGRequestListenEventAccess(),
|
|
107
|
+
"bedienungshilfen": lambda: (lambda aps: aps.AXIsProcessTrustedWithOptions({aps.kAXTrustedCheckOptionPrompt: True}))(aus("ApplicationServices")),
|
|
108
|
+
"mikrofon": lambda: _frage_mikrofon(aus("AVFoundation")),
|
|
109
|
+
}
|
|
110
|
+
if anfragen in anfrage_fuer: # eine gescheiterte Anfrage aendert nur die Antwort darunter
|
|
111
|
+
versuche(anfrage_fuer[anfragen])
|
|
112
|
+
ergebnis["bildschirm"] = _wahrheitswert(versuche(lambda: aus("Quartz").CGPreflightScreenCaptureAccess()))
|
|
113
|
+
ergebnis["eingabe"] = _wahrheitswert(versuche(lambda: aus("Quartz").CGPreflightListenEventAccess()))
|
|
114
|
+
ergebnis["bedienungshilfen"] = _wahrheitswert(versuche(lambda: aus("ApplicationServices").AXIsProcessTrusted()))
|
|
115
|
+
ergebnis["mikrofon"] = _mikrofon_status(versuche(
|
|
116
|
+
lambda: (lambda av: av.AVCaptureDevice.authorizationStatusForMediaType_(av.AVMediaTypeAudio))(aus("AVFoundation"))))
|
|
117
|
+
return ergebnis
|
|
36
118
|
|
|
37
119
|
|
|
38
120
|
def schreibe_json_atomar(pfad: str, daten: dict) -> None:
|
package/zeigen/starter.py
CHANGED
|
@@ -8,8 +8,8 @@ import sys
|
|
|
8
8
|
import threading
|
|
9
9
|
import time
|
|
10
10
|
|
|
11
|
-
from gemeinsam import (ABBRECHEN, PROTOKOLL, ROH, STARTER,
|
|
12
|
-
sauber, schreibe_json_atomar)
|
|
11
|
+
from gemeinsam import (ABBRECHEN, PLATTFORM, PROTOKOLL, ROH, STARTER, _stoppe_sitzung, fehler_grund, freigaben_abfragen,
|
|
12
|
+
lies_protokoll, sauber, schreibe_json_atomar, umgebung)
|
|
13
13
|
|
|
14
14
|
TAKT_SEK = 2.0 # so oft schaut der Starter nach Sitzung, Abbruch und Frist (Wanduhr, nicht ein Zeitgeber ueber 15 Minuten)
|
|
15
15
|
NACHTOETEN_SEK = 0.5 # Pause zwischen den ersten Versuchen, einen Prozessbaum zu beenden
|
|
@@ -22,12 +22,16 @@ AUFNEHMEN = "import sys; from openadapt_capture.cli import record; record(sys.ar
|
|
|
22
22
|
|
|
23
23
|
|
|
24
24
|
def _starte_kind(befehl: list, ordner: str, protokoll) -> subprocess.Popen:
|
|
25
|
+
env = dict(os.environ, **umgebung(PLATTFORM))
|
|
26
|
+
if PLATTFORM != "win32": # Mac: STARTUPINFO und CREATE_NEW_CONSOLE gibt es dort nicht; eine eigene Sitzung loest vom Starter
|
|
27
|
+
return subprocess.Popen(befehl, cwd=ordner, env=env, stdout=protokoll, stderr=subprocess.STDOUT,
|
|
28
|
+
start_new_session=True)
|
|
25
29
|
# Eigene, VERSTECKTE Konsole: Sonst oeffnen python.exe, sein multiprocessing-Kind und FFmpeg je ein Fenster
|
|
26
30
|
# (gemessen 18.10.: detached + windowsHide allein reicht nicht; starter-probe.py).
|
|
27
31
|
si = subprocess.STARTUPINFO()
|
|
28
32
|
si.dwFlags |= subprocess.STARTF_USESHOWWINDOW
|
|
29
33
|
si.wShowWindow = 0 # SW_HIDE
|
|
30
|
-
return subprocess.Popen(befehl, cwd=ordner, env=
|
|
34
|
+
return subprocess.Popen(befehl, cwd=ordner, env=env, stdout=protokoll,
|
|
31
35
|
stderr=subprocess.STDOUT, startupinfo=si, creationflags=subprocess.CREATE_NEW_CONSOLE)
|
|
32
36
|
|
|
33
37
|
|
|
@@ -40,8 +44,15 @@ def _beende_baum(kind: subprocess.Popen) -> bool:
|
|
|
40
44
|
baum += baum[0].children(recursive=True)
|
|
41
45
|
except psutil.NoSuchProcess:
|
|
42
46
|
baum = []
|
|
43
|
-
|
|
44
|
-
|
|
47
|
+
if PLATTFORM == "win32":
|
|
48
|
+
subprocess.run(["taskkill", "/PID", str(kind.pid), "/T", "/F"], capture_output=True,
|
|
49
|
+
creationflags=subprocess.CREATE_NO_WINDOW)
|
|
50
|
+
else: # Schnappschuss steht oben, VOR dem ersten Signal: ein beendeter Vater gaebe seine Kinder nicht mehr her
|
|
51
|
+
for p in baum:
|
|
52
|
+
try:
|
|
53
|
+
p.terminate()
|
|
54
|
+
except psutil.NoSuchProcess:
|
|
55
|
+
pass
|
|
45
56
|
_, lebend = psutil.wait_procs(baum, timeout=10)
|
|
46
57
|
for p in lebend:
|
|
47
58
|
try:
|
|
@@ -118,7 +129,12 @@ def starte(konfig: dict, befehl: list | None = None) -> None:
|
|
|
118
129
|
melde()
|
|
119
130
|
kind = None
|
|
120
131
|
try:
|
|
121
|
-
|
|
132
|
+
if PLATTFORM == "darwin": # ohne Freigabe nimmt der Aufnehmer nichts auf: gar nicht erst starten
|
|
133
|
+
freigaben = freigaben_abfragen()
|
|
134
|
+
for name in ("bildschirm", "eingabe", "bedienungshilfen"): # None ("weiss nicht") haelt nicht auf
|
|
135
|
+
if freigaben.get(name) is False:
|
|
136
|
+
return melde(zustand="fehlgeschlagen", grund=f"freigabe-{name}")
|
|
137
|
+
python = os.path.join(os.path.dirname(sys.executable), "python.exe") if PLATTFORM == "win32" else sys.executable
|
|
122
138
|
roh = os.path.join(ordner, ROH) # nur der Aufnehmer schreibt dort (gemeinsam.ROH)
|
|
123
139
|
os.makedirs(roh, exist_ok=True)
|
|
124
140
|
with open(os.path.join(ordner, PROTOKOLL), "w", encoding="utf-8") as protokoll:
|
|
@@ -138,7 +154,7 @@ def starte(konfig: dict, befehl: list | None = None) -> None:
|
|
|
138
154
|
if time.time() >= bericht["frist"]:
|
|
139
155
|
if bericht["sitzung"] is None:
|
|
140
156
|
_raeume_weg(kind, melde)
|
|
141
|
-
return melde(zustand="fehlgeschlagen", grund=fehler_grund(lies_protokoll(ordner)))
|
|
157
|
+
return melde(zustand="fehlgeschlagen", grund=fehler_grund(lies_protokoll(ordner), PLATTFORM))
|
|
142
158
|
try:
|
|
143
159
|
_mit_herzschlag(lambda: _stoppe_sitzung(bericht.get("sitzung"), BREMSE_STOPP_SEK), melde)
|
|
144
160
|
except Exception:
|
|
@@ -160,4 +176,4 @@ def _ende(ordner: str, sitzung: str | None, art: str) -> dict:
|
|
|
160
176
|
"""Das Kind ist weg: sauber abgeschlossen -> `art`; mit Sitzung, aber nicht sauber -> abgebrochen; ohne -> fehlgeschlagen."""
|
|
161
177
|
if sitzung is not None and sauber(ordner):
|
|
162
178
|
return {"zustand": art, "grund": None}
|
|
163
|
-
return {"zustand": "abgebrochen" if sitzung else "fehlgeschlagen", "grund": fehler_grund(lies_protokoll(ordner))}
|
|
179
|
+
return {"zustand": "abgebrochen" if sitzung else "fehlgeschlagen", "grund": fehler_grund(lies_protokoll(ordner), PLATTFORM)}
|
package/zeigen/werkzeug.py
CHANGED
|
@@ -4,7 +4,8 @@
|
|
|
4
4
|
# werkzeug.py starten <konfig.json> Starter mit Bremse (von Node mit pythonw abgehaengt gestartet, keine Ausgabe)
|
|
5
5
|
# werkzeug.py stoppen <ordner> -> eine JSON-Zeile: Ergebnis und Zahlen
|
|
6
6
|
# werkzeug.py zustand <ordner|-> -> eine JSON-Zeile: aktive Sitzungen des Aufnehmers, Bericht des Starters
|
|
7
|
-
# werkzeug.py teile -> eine JSON-Zeile: was eingerichtet ist
|
|
7
|
+
# werkzeug.py teile -> eine JSON-Zeile: was eingerichtet ist (auf dem Mac auch: avfoundation)
|
|
8
|
+
# werkzeug.py freigaben [--anfragen <name>] -> eine JSON-Zeile: was macOS diesem Prozess erlaubt (Windows: null)
|
|
8
9
|
# werkzeug.py zeitleiste <ordner> <de|en> [--ohne-getipptes] -> schreibt zeitleiste.md, eine JSON-Zeile mit Zahlen
|
|
9
10
|
# werkzeug.py bild <ordner> <sekunden> -> bilder/<m-ss>.png, eine JSON-Zeile
|
|
10
11
|
# werkzeug.py modell-laden -> eine JSON-Zeile (laedt large-v3-turbo, Netz)
|
|
@@ -15,6 +16,7 @@
|
|
|
15
16
|
# festen Mustern durchsucht (fehler_grund), nie zurueckgegeben -- auch nicht auf stderr, auch nicht im Fehlerfall.
|
|
16
17
|
# Gedeckt von `npm run check:zeigen-python` (scripts/zeigen/test_werkzeug.py). Spec 2026-10-18-k3zeigen-design.md.
|
|
17
18
|
import argparse
|
|
19
|
+
import importlib.util
|
|
18
20
|
import json
|
|
19
21
|
import os
|
|
20
22
|
import sqlite3
|
|
@@ -23,8 +25,8 @@ import time
|
|
|
23
25
|
|
|
24
26
|
sys.path.insert(0, os.path.dirname(os.path.abspath(__file__))) # Nachbardateien auch unter -I/-P, die den Ordner weglassen
|
|
25
27
|
sys.dont_write_bytecode = True # kein __pycache__ neben den Dateien: der Ordner geht so, wie er ist, ins npm-Paket
|
|
26
|
-
from gemeinsam import (ABBRECHEN, ROH, STARTER,
|
|
27
|
-
fehler_grund, lies_json, lies_protokoll, sauber)
|
|
28
|
+
from gemeinsam import (ABBRECHEN, PLATTFORM, ROH, STARTER, ZUSTAND_AUFNEHMER, _stoppe_sitzung, # noqa: E402
|
|
29
|
+
fehler_grund, freigaben_abfragen, lies_json, lies_protokoll, sauber, umgebung)
|
|
28
30
|
from starter import starte # noqa: E402
|
|
29
31
|
|
|
30
32
|
WARTEN_SEK = 15.0 # so lange wartet `stoppen` nach dem Stopp auf einen Starter ohne frischen Herzschlag
|
|
@@ -118,15 +120,15 @@ def stoppen(ordner: str) -> dict:
|
|
|
118
120
|
if aktive is None or (bericht.get("sitzung") in aktive if zustand == "laeuft" else aktive):
|
|
119
121
|
return antwort("unklar")
|
|
120
122
|
if zustand == "startet": # ohne Sitzung gibt es keine Aufnahme, die man abschliessen koennte
|
|
121
|
-
return antwort("abgebrochen", fehler_grund(lies_protokoll(ordner)), aufnahme_zahlen(ordner))
|
|
123
|
+
return antwort("abgebrochen", fehler_grund(lies_protokoll(ordner), PLATTFORM), aufnahme_zahlen(ordner))
|
|
122
124
|
if zustand in ("abgebrochen", "fehlgeschlagen"):
|
|
123
|
-
return antwort("abgebrochen", bericht.get("grund") or fehler_grund(lies_protokoll(ordner)), aufnahme_zahlen(ordner))
|
|
125
|
+
return antwort("abgebrochen", bericht.get("grund") or fehler_grund(lies_protokoll(ordner), PLATTFORM), aufnahme_zahlen(ordner))
|
|
124
126
|
if zustand not in ("gestoppt", "selbst-gestoppt", "laeuft"): # "laeuft" = Starter still, Sitzung nicht mehr aktiv
|
|
125
|
-
return antwort("nicht-sauber", fehler_grund(lies_protokoll(ordner)), aufnahme_zahlen(ordner))
|
|
127
|
+
return antwort("nicht-sauber", fehler_grund(lies_protokoll(ordner), PLATTFORM), aufnahme_zahlen(ordner))
|
|
126
128
|
ergebnis = "selbst-gestoppt" if zustand == "selbst-gestoppt" else "gestoppt"
|
|
127
129
|
zahlen = aufnahme_zahlen(ordner, bericht.get("frist") if ergebnis == "selbst-gestoppt" else None)
|
|
128
130
|
if zahlen is None or not sauber(ordner):
|
|
129
|
-
return antwort("nicht-sauber", fehler_grund(lies_protokoll(ordner)), zahlen)
|
|
131
|
+
return antwort("nicht-sauber", fehler_grund(lies_protokoll(ordner), PLATTFORM), zahlen)
|
|
130
132
|
return antwort(ergebnis, None, zahlen)
|
|
131
133
|
|
|
132
134
|
|
|
@@ -191,7 +193,10 @@ def teile() -> dict:
|
|
|
191
193
|
aufnehmer = version("openadapt-capture")
|
|
192
194
|
except PackageNotFoundError:
|
|
193
195
|
aufnehmer = None
|
|
194
|
-
|
|
196
|
+
daten = {"aufnehmer": aufnehmer, "ffmpeg": _ffmpeg_da(), "modell": modell_da(_hf_cache())}
|
|
197
|
+
if PLATTFORM == "darwin": # die Bruecke zu AVFoundation (Mikrofon-Status) kommt als eigenes Paket mit
|
|
198
|
+
daten["avfoundation"] = importlib.util.find_spec("AVFoundation") is not None
|
|
199
|
+
return daten
|
|
195
200
|
|
|
196
201
|
|
|
197
202
|
def main(argv: list) -> int:
|
|
@@ -201,6 +206,7 @@ def main(argv: list) -> int:
|
|
|
201
206
|
befehle.add_parser("stoppen").add_argument("ordner")
|
|
202
207
|
befehle.add_parser("zustand").add_argument("ordner")
|
|
203
208
|
befehle.add_parser("teile")
|
|
209
|
+
befehle.add_parser("freigaben").add_argument("--anfragen", choices=["bildschirm", "eingabe", "bedienungshilfen", "mikrofon"])
|
|
204
210
|
zl = befehle.add_parser("zeitleiste")
|
|
205
211
|
zl.add_argument("ordner")
|
|
206
212
|
zl.add_argument("sprache", choices=["de", "en"])
|
|
@@ -210,7 +216,7 @@ def main(argv: list) -> int:
|
|
|
210
216
|
bi.add_argument("sekunden", type=float)
|
|
211
217
|
befehle.add_parser("modell-laden")
|
|
212
218
|
args = parser.parse_args(argv)
|
|
213
|
-
os.environ.update(
|
|
219
|
+
os.environ.update(umgebung(PLATTFORM))
|
|
214
220
|
try:
|
|
215
221
|
if args.befehl == "starten":
|
|
216
222
|
konfig = lies_json(args.konfig)
|
|
@@ -224,6 +230,8 @@ def main(argv: list) -> int:
|
|
|
224
230
|
os.chdir(os.path.dirname(os.path.abspath(__file__)) if ordner == "-" or fehlt else ordner)
|
|
225
231
|
if args.befehl == "stoppen":
|
|
226
232
|
daten = stoppen(ordner)
|
|
233
|
+
elif args.befehl == "freigaben": # blockiert nicht laenger als freigaben_abfragen (Node: 20 s, mit --anfragen 90 s)
|
|
234
|
+
daten = {"freigaben": freigaben_abfragen(anfragen=args.anfragen) if PLATTFORM == "darwin" else None}
|
|
227
235
|
elif args.befehl == "zustand":
|
|
228
236
|
daten = zustand(None if ordner == "-" else ordner)
|
|
229
237
|
elif args.befehl in ("zeitleiste", "bild", "modell-laden"):
|