niahere 0.2.85 → 0.2.87

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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "niahere",
3
- "version": "0.2.85",
3
+ "version": "0.2.87",
4
4
  "description": "A personal AI assistant daemon — chat, scheduled jobs, persona system, extensible via skills.",
5
5
  "type": "module",
6
6
  "scripts": {
@@ -25,7 +25,7 @@ allowed-tools:
25
25
 
26
26
  You are a QA engineer. Test web applications like a real user — click everything, fill every form, check every state. Produce a structured report with evidence.
27
27
 
28
- For Playwright MCP reference, see [playwright.md](playwright.md)
28
+ For Playwright MCP reference and the cloned-profile helper workflow, see [playwright.md](playwright.md).
29
29
 
30
30
  ## Setup
31
31
 
@@ -6,7 +6,7 @@
6
6
 
7
7
  ## How Playwright Works Here
8
8
 
9
- Playwright is available as **MCP tools already in your tool list**. You do NOT need to:
9
+ Playwright is usually available as **MCP tools already in your tool list**. You do NOT need to:
10
10
  - Install anything (`npm install playwright`, `npx playwright install` — NO)
11
11
  - Import or require playwright in code
12
12
  - Write scripts or launch a browser manually
@@ -14,7 +14,85 @@ Playwright is available as **MCP tools already in your tool list**. You do NOT n
14
14
 
15
15
  **Just call the tools directly.** They are prefixed `mcp__plugin_playwright_playwright__` in your tool list (e.g., `mcp__plugin_playwright_playwright__browser_navigate`). Search your available tools for `browser_` to see them all.
16
16
 
17
- A persistent Chrome profile at `~/.shared/playwright-profile/` is pre-configured with saved logins. The browser launches automatically on your first tool call and reuses this profile.
17
+ A persistent Chrome profile is available for saved logins. For ordinary browser work, use the MCP tools directly. For parallel agents or sites that block fresh Chromium, use the cloned-profile helper below.
18
+
19
+ ## Cloned Profile Helper
20
+
21
+ Use this when multiple agents need browser access from a warmed profile, or when a site blocks clean Playwright Chromium.
22
+
23
+ Helper:
24
+
25
+ ```bash
26
+ skills/qa/scripts/playwright-profile-clone.sh
27
+ ```
28
+
29
+ Default canonical profile:
30
+
31
+ ```text
32
+ ~/.shared/playwright-user-profile
33
+ ```
34
+
35
+ If that profile does not exist, the helper creates it. If `~/.shared/playwright-config.json` has a `browser.userDataDir`, the helper seeds `playwright-user-profile` from that existing configured profile on first use.
36
+
37
+ ### Open a cloned browser
38
+
39
+ ```bash
40
+ skills/qa/scripts/playwright-profile-clone.sh open
41
+ ```
42
+
43
+ This:
44
+ - generates a random hex run id
45
+ - copies the canonical profile to `~/.shared/playwright-profile-runs/<run-id>`
46
+ - launches Chrome with that copied `--user-data-dir`
47
+ - assigns a free `--remote-debugging-port`
48
+ - prints `PW_PROFILE_RUN_ID`, `PW_USER_DATA_DIR`, `PW_CDP_URL`, and `PW_PROFILE_CLOSE_ACTION`
49
+ - watches the Chrome process; when Chrome exits, commits the run profile back to the canonical profile and removes the run clone
50
+
51
+ Each concurrent `open` gets its own user-data dir and CDP URL.
52
+
53
+ Attach with Playwright when needed:
54
+
55
+ ```js
56
+ const { chromium } = require("playwright");
57
+ const browser = await chromium.connectOverCDP(process.env.PW_CDP_URL);
58
+ const context = browser.contexts()[0];
59
+ const page = context.pages()[0] || await context.newPage();
60
+ ```
61
+
62
+ ### Close behavior
63
+
64
+ Default close behavior is `commit`: when the Chrome process exits, the helper backs up the canonical profile, overwrites it from the run profile, and deletes the run clone.
65
+
66
+ Use `--discard-on-close` for throwaway browser work:
67
+
68
+ ```bash
69
+ skills/qa/scripts/playwright-profile-clone.sh open --discard-on-close
70
+ ```
71
+
72
+ Use `--keep` when debugging or when you want to commit and cleanup manually:
73
+
74
+ ```bash
75
+ skills/qa/scripts/playwright-profile-clone.sh open --keep
76
+ skills/qa/scripts/playwright-profile-clone.sh commit --run-id <run-id>
77
+ skills/qa/scripts/playwright-profile-clone.sh cleanup --run-id <run-id>
78
+ ```
79
+
80
+ `commit` backs up the canonical profile and overwrites it from the run profile. `cleanup` removes the run profile. If multiple runs are active, pass `--run-id`; do not rely on a global current pointer.
81
+
82
+ Atomic commands:
83
+
84
+ ```bash
85
+ skills/qa/scripts/playwright-profile-clone.sh prepare
86
+ skills/qa/scripts/playwright-profile-clone.sh open
87
+ skills/qa/scripts/playwright-profile-clone.sh open --discard-on-close
88
+ skills/qa/scripts/playwright-profile-clone.sh open --keep
89
+ skills/qa/scripts/playwright-profile-clone.sh status --run-id <run-id>
90
+ skills/qa/scripts/playwright-profile-clone.sh commit --run-id <run-id>
91
+ skills/qa/scripts/playwright-profile-clone.sh cleanup --run-id <run-id>
92
+ skills/qa/scripts/playwright-profile-clone.sh prune --keep 100
93
+ ```
94
+
95
+ The helper auto-prunes old run profiles after `prepare`/`open`. Default cap is `100` run dirs. Override it with `PLAYWRIGHT_PROFILE_MAX_RUNS=<count>`, or disable automatic pruning with `PLAYWRIGHT_PROFILE_MAX_RUNS=0` or `PLAYWRIGHT_PROFILE_MAX_RUNS=off`. Pruning skips the current run and any run with a tracked live Chrome PID.
18
96
 
19
97
  ## Quickstart: Open a Browser
20
98
 
@@ -183,23 +261,24 @@ When the snapshot is too large, it auto-saves to a file. Use Grep on the saved f
183
261
  ## Session Management & Persistent Chrome Profile
184
262
 
185
263
  ### Profile Location
186
- - **Persistent profile**: `~/.shared/playwright-profile/` — cookies, logins, and browser state persist across sessions and agents
264
+ - **Canonical cloned profile**: `~/.shared/playwright-user-profile/` — cookies, logins, and browser state used as the source for copied runs
265
+ - **Run profiles**: `~/.shared/playwright-profile-runs/<run-id>/` — per-agent copied profiles
187
266
  - **Config**: `~/.shared/playwright-config.json` — controls browser type, headless mode, and profile path
188
267
  - **MCP registration**: `~/.claude/plugins/.../playwright/.mcp.json` — must include `--config` flag pointing to the config file
189
268
 
190
269
  ### How It Works
191
270
  - Playwright launches a **separate Chrome instance** using the persistent profile — won't conflict with your running Chrome
192
271
  - Log in manually once in the Playwright Chrome window (e.g., App Store Connect, RevenueCat), sessions persist across restarts and new conversations
193
- - Any agent or session that uses Playwright MCP shares the same profile no re-login needed
272
+ - For parallel spawned agents, use copied run profiles. Do not launch multiple Chrome instances against the same user-data dir.
194
273
 
195
274
  ### If Sessions Expire or Browser Won't Connect
196
- - **"Opening in existing browser session" error**: A Chrome instance with this profile is already running. Find and kill it:
275
+ - **"Opening in existing browser session" error**: A Chrome instance with this profile is already running. For cloned runs, open a new clone instead of reusing the same run id. If you intentionally need to stop an old canonical-profile browser:
197
276
  ```bash
198
- ps aux | grep "user-data-dir=/Users/aman/.shared/playwright-profile" | grep -v grep | grep -v "Helper" | awk '{print $2}' | xargs kill
277
+ ps aux | grep "user-data-dir=.*playwright-user-profile" | grep -v grep | grep -v "Helper" | awk '{print $2}' | xargs kill
199
278
  ```
200
279
  Then retry the Playwright action. The persistent profile (cookies/logins) is on disk and survives browser restarts.
201
280
  - **Auth expired?** Navigate to the login page in the Playwright Chrome window and re-login — the new session will persist automatically.
202
- - **Random cache profiles**: If Chrome uses `/Library/Caches/ms-playwright/mcp-chrome-*` instead of the persistent profile, the `--config` flag is missing from the MCP registration. Fix: ensure `~/.claude/plugins/.../playwright/.mcp.json` includes `"--config", "/Users/aman/.claude/playwright-config.json"` in the args array.
281
+ - **Random cache profiles**: If Chrome uses `/Library/Caches/ms-playwright/mcp-chrome-*` instead of the persistent profile, the `--config` flag is missing from the MCP registration. Fix: ensure the Playwright MCP registration includes `"--config", "~/.shared/playwright-config.json"` in the args array.
203
282
 
204
283
  ### Logged-In Services (as of Feb 2026)
205
284
  - Apple App Store Connect (`appstoreconnect.apple.com`)
@@ -230,7 +309,7 @@ When the snapshot is too large, it auto-saves to a file. Use Grep on the saved f
230
309
  | Navigate without waiting | Always `wait_for` before reading content |
231
310
  | Keep browser open indefinitely | Close when done to free resources |
232
311
  | Ignore snapshot file saves | Grep/parse the saved file for large outputs |
233
- | Try to launch/setup browser manually | Just call `browser_navigate` it auto-launches |
312
+ | Launch Chrome manually for ordinary QA | Use `browser_navigate`; use `playwright-profile-clone.sh open` only when a copied warmed profile is needed |
234
313
 
235
314
  ## Debugging Tips
236
315
 
@@ -0,0 +1,535 @@
1
+ #!/usr/bin/env bash
2
+ set -euo pipefail
3
+
4
+ PROFILE_HOME="${PLAYWRIGHT_PROFILE_HOME:-$HOME/.shared}"
5
+ CONFIG_PATH="${PLAYWRIGHT_CONFIG:-$PROFILE_HOME/playwright-config.json}"
6
+ RUNS_DIR="${PLAYWRIGHT_PROFILE_RUNS_DIR:-$PROFILE_HOME/playwright-profile-runs}"
7
+ BACKUPS_DIR="${PLAYWRIGHT_PROFILE_BACKUPS_DIR:-$PROFILE_HOME/playwright-profile-backups}"
8
+ STATE_DIR="$RUNS_DIR/.state"
9
+ MAX_RUNS="${PLAYWRIGHT_PROFILE_MAX_RUNS:-100}"
10
+ SCRIPT_PATH="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)/$(basename "${BASH_SOURCE[0]}")"
11
+ COMMIT_LOCK_HELD=""
12
+
13
+ usage() {
14
+ cat <<'EOF'
15
+ Usage:
16
+ playwright-profile-clone.sh prepare [--run-id <hex>] [--primary <path>]
17
+ playwright-profile-clone.sh open [--run-id <hex>] [--primary <path>] [--commit-on-close|--discard-on-close|--keep]
18
+ playwright-profile-clone.sh commit [--run-id <hex>]
19
+ playwright-profile-clone.sh cleanup [--run-id <hex>]
20
+ playwright-profile-clone.sh prune [--keep <count>|--off]
21
+ playwright-profile-clone.sh status [--run-id <hex>]
22
+
23
+ Environment:
24
+ PW_PRIMARY_PROFILE Canonical source profile override
25
+ PLAYWRIGHT_USER_PROFILE Canonical source profile override
26
+ PLAYWRIGHT_PROFILE_HOME Base dir, default: ~/.shared
27
+ PLAYWRIGHT_CONFIG Config to seed from, default: ~/.shared/playwright-config.json
28
+ PLAYWRIGHT_CHROME Chrome executable override
29
+ PLAYWRIGHT_PROFILE_MAX_RUNS Max run dirs to keep, default: 100; set 0/off to disable auto-prune
30
+ EOF
31
+ }
32
+
33
+ die() {
34
+ echo "error=$*" >&2
35
+ exit 1
36
+ }
37
+
38
+ release_commit_lock() {
39
+ if [ -n "$COMMIT_LOCK_HELD" ]; then
40
+ rmdir "$COMMIT_LOCK_HELD" 2>/dev/null || true
41
+ COMMIT_LOCK_HELD=""
42
+ fi
43
+ }
44
+
45
+ trap release_commit_lock EXIT
46
+
47
+ shell_quote() {
48
+ printf "%q" "$1"
49
+ }
50
+
51
+ random_hex() {
52
+ if command -v openssl >/dev/null 2>&1; then
53
+ openssl rand -hex 4
54
+ else
55
+ node -e "console.log(require('crypto').randomBytes(4).toString('hex'))"
56
+ fi
57
+ }
58
+
59
+ configured_profile() {
60
+ [ -f "$CONFIG_PATH" ] || return 0
61
+ command -v node >/dev/null 2>&1 || return 0
62
+ node - "$CONFIG_PATH" <<'NODE'
63
+ const fs = require("fs");
64
+ const config = JSON.parse(fs.readFileSync(process.argv[2], "utf8"));
65
+ const dir = config?.browser?.userDataDir || config?.browser?.launchOptions?.userDataDir || "";
66
+ if (dir) process.stdout.write(dir);
67
+ NODE
68
+ }
69
+
70
+ resolve_primary() {
71
+ local explicit="$1"
72
+ if [ -n "$explicit" ]; then
73
+ echo "$explicit"
74
+ elif [ -n "${PW_PRIMARY_PROFILE:-}" ]; then
75
+ echo "$PW_PRIMARY_PROFILE"
76
+ elif [ -n "${PLAYWRIGHT_USER_PROFILE:-}" ]; then
77
+ echo "$PLAYWRIGHT_USER_PROFILE"
78
+ else
79
+ echo "$PROFILE_HOME/playwright-user-profile"
80
+ fi
81
+ }
82
+
83
+ has_entries() {
84
+ [ -d "$1" ] && [ -n "$(find "$1" -mindepth 1 -maxdepth 1 -print -quit 2>/dev/null)" ]
85
+ }
86
+
87
+ copy_profile() {
88
+ local source="$1"
89
+ local destination="$2"
90
+ mkdir -p "$destination"
91
+ rsync -a --delete \
92
+ --exclude='Singleton*' \
93
+ --exclude='DevToolsActivePort' \
94
+ --exclude='BrowserMetrics/' \
95
+ --exclude='Crashpad/' \
96
+ --exclude='Default/Cache/' \
97
+ --exclude='Default/Code Cache/' \
98
+ --exclude='Default/GPUCache/' \
99
+ --exclude='Default/DawnGraphiteCache/' \
100
+ --exclude='Default/DawnWebGPUCache/' \
101
+ --exclude='GrShaderCache/' \
102
+ --exclude='GraphiteDawnCache/' \
103
+ --exclude='ShaderCache/' \
104
+ "$source/" "$destination/"
105
+ }
106
+
107
+ acquire_commit_lock() {
108
+ mkdir -p "$BACKUPS_DIR"
109
+ local lock_dir="$BACKUPS_DIR/.commit.lock"
110
+ local deadline=$((SECONDS + 60))
111
+
112
+ while ! mkdir "$lock_dir" 2>/dev/null; do
113
+ if [ "$SECONDS" -ge "$deadline" ]; then
114
+ die "timed out waiting for profile commit lock"
115
+ fi
116
+ sleep 0.2
117
+ done
118
+
119
+ COMMIT_LOCK_HELD="$lock_dir"
120
+ }
121
+
122
+ prune_old_runs() {
123
+ local protected_run_id="${1:-}"
124
+ local max_runs="${2:-$MAX_RUNS}"
125
+ local quiet="${3:-quiet}"
126
+
127
+ RUNS_DIR="$RUNS_DIR" STATE_DIR="$STATE_DIR" PROTECTED_RUN_ID="$protected_run_id" MAX_RUNS="$max_runs" QUIET="$quiet" node <<'NODE'
128
+ const fs = require("fs");
129
+ const path = require("path");
130
+
131
+ const runsDir = process.env.RUNS_DIR;
132
+ const stateDir = process.env.STATE_DIR;
133
+ const protectedRunId = process.env.PROTECTED_RUN_ID || "";
134
+ const rawMax = String(process.env.MAX_RUNS || "100").toLowerCase();
135
+ const quiet = process.env.QUIET === "quiet";
136
+
137
+ if (["0", "off", "false", "none", "disabled"].includes(rawMax)) {
138
+ if (!quiet) console.log("profile_prune=disabled");
139
+ process.exit(0);
140
+ }
141
+
142
+ const max = Number.parseInt(rawMax, 10);
143
+ if (!Number.isFinite(max) || max < 1) {
144
+ console.error(`error=invalid PLAYWRIGHT_PROFILE_MAX_RUNS: ${process.env.MAX_RUNS}`);
145
+ process.exit(1);
146
+ }
147
+
148
+ if (!fs.existsSync(runsDir)) {
149
+ if (!quiet) console.log("profile_pruned=0");
150
+ process.exit(0);
151
+ }
152
+
153
+ function parseState(runId) {
154
+ const file = path.join(stateDir, `${runId}.env`);
155
+ if (!fs.existsSync(file)) return {};
156
+ const state = {};
157
+ for (const line of fs.readFileSync(file, "utf8").split(/\r?\n/)) {
158
+ const match = line.match(/^([A-Z0-9_]+)=(.*)$/);
159
+ if (!match) continue;
160
+ state[match[1]] = match[2].replace(/^'(.*)'$/, "$1").replace(/^"(.*)"$/, "$1");
161
+ }
162
+ return state;
163
+ }
164
+
165
+ function pidIsAlive(pid) {
166
+ const numericPid = Number.parseInt(pid || "", 10);
167
+ if (!numericPid) return false;
168
+ try {
169
+ process.kill(numericPid, 0);
170
+ return true;
171
+ } catch {
172
+ return false;
173
+ }
174
+ }
175
+
176
+ const runs = fs.readdirSync(runsDir, { withFileTypes: true })
177
+ .filter((entry) => entry.isDirectory() && entry.name !== ".state")
178
+ .map((entry) => {
179
+ const runDir = path.join(runsDir, entry.name);
180
+ return {
181
+ id: entry.name,
182
+ dir: runDir,
183
+ mtimeMs: fs.statSync(runDir).mtimeMs,
184
+ state: parseState(entry.name),
185
+ };
186
+ })
187
+ .sort((a, b) => a.mtimeMs - b.mtimeMs);
188
+
189
+ let remaining = runs.length;
190
+ let pruned = 0;
191
+
192
+ for (const run of runs) {
193
+ if (remaining <= max) break;
194
+ if (run.id === protectedRunId) continue;
195
+ if (pidIsAlive(run.state.PW_CHROME_PID)) continue;
196
+
197
+ fs.rmSync(run.dir, { recursive: true, force: true });
198
+ fs.rmSync(path.join(stateDir, `${run.id}.env`), { force: true });
199
+ fs.rmSync(path.join(runsDir, `${run.id}.chrome.log`), { force: true });
200
+ remaining -= 1;
201
+ pruned += 1;
202
+ }
203
+
204
+ if (!quiet) {
205
+ console.log(`profile_pruned=${pruned}`);
206
+ console.log(`profile_runs=${remaining}`);
207
+ console.log(`profile_max_runs=${max}`);
208
+ }
209
+ NODE
210
+ }
211
+
212
+ seed_primary_if_needed() {
213
+ local primary="$1"
214
+ mkdir -p "$(dirname "$primary")"
215
+
216
+ if has_entries "$primary"; then
217
+ return 0
218
+ fi
219
+
220
+ local configured
221
+ configured="$(configured_profile || true)"
222
+ if [ -n "$configured" ] && [ "$configured" != "$primary" ] && has_entries "$configured"; then
223
+ copy_profile "$configured" "$primary"
224
+ else
225
+ mkdir -p "$primary"
226
+ fi
227
+ }
228
+
229
+ state_file() {
230
+ echo "$STATE_DIR/$1.env"
231
+ }
232
+
233
+ write_state() {
234
+ local run_id="$1"
235
+ local status="$2"
236
+ local primary="$3"
237
+ local run_dir="$4"
238
+ local cdp_url="${5:-}"
239
+ local chrome_pid="${6:-}"
240
+ local close_action="${7:-manual}"
241
+
242
+ mkdir -p "$STATE_DIR"
243
+ {
244
+ echo "PW_PROFILE_RUN_ID=$(shell_quote "$run_id")"
245
+ echo "PW_PRIMARY_PROFILE=$(shell_quote "$primary")"
246
+ echo "PW_USER_DATA_DIR=$(shell_quote "$run_dir")"
247
+ echo "PW_CDP_URL=$(shell_quote "$cdp_url")"
248
+ echo "PW_CHROME_PID=$(shell_quote "$chrome_pid")"
249
+ echo "PW_PROFILE_STATUS=$(shell_quote "$status")"
250
+ echo "PW_PROFILE_CLOSE_ACTION=$(shell_quote "$close_action")"
251
+ } >"$(state_file "$run_id")"
252
+ }
253
+
254
+ load_state() {
255
+ local run_id="$1"
256
+ local file
257
+ file="$(state_file "$run_id")"
258
+ [ -f "$file" ] || die "unknown run id: $run_id"
259
+ # shellcheck disable=SC1090
260
+ source "$file"
261
+ }
262
+
263
+ single_active_run_id() {
264
+ mkdir -p "$STATE_DIR"
265
+ local ids=()
266
+ local file
267
+ while IFS= read -r file; do
268
+ ids+=("$(basename "$file" .env)")
269
+ done < <(find "$STATE_DIR" -maxdepth 1 -type f -name '*.env' -print | sort)
270
+
271
+ if [ "${#ids[@]}" -eq 1 ]; then
272
+ echo "${ids[0]}"
273
+ elif [ "${#ids[@]}" -eq 0 ]; then
274
+ die "no active profile runs; pass --run-id"
275
+ else
276
+ printf 'active_runs=' >&2
277
+ printf '%s ' "${ids[@]}" >&2
278
+ printf '\n' >&2
279
+ die "multiple active profile runs; pass --run-id"
280
+ fi
281
+ }
282
+
283
+ resolve_run_id_arg() {
284
+ local explicit="$1"
285
+ if [ -n "$explicit" ]; then
286
+ echo "$explicit"
287
+ elif [ -n "${PW_PROFILE_RUN_ID:-}" ]; then
288
+ echo "$PW_PROFILE_RUN_ID"
289
+ else
290
+ single_active_run_id
291
+ fi
292
+ }
293
+
294
+ print_exports() {
295
+ local run_id="$1"
296
+ local primary="$2"
297
+ local run_dir="$3"
298
+ local cdp_url="${4:-}"
299
+ echo "PW_PROFILE_RUN_ID='${run_id}'"
300
+ echo "PW_PRIMARY_PROFILE='${primary}'"
301
+ echo "PW_USER_DATA_DIR='${run_dir}'"
302
+ if [ -n "$cdp_url" ]; then
303
+ echo "PW_CDP_URL='${cdp_url}'"
304
+ fi
305
+ echo "export PW_PROFILE_RUN_ID PW_PRIMARY_PROFILE PW_USER_DATA_DIR${cdp_url:+ PW_CDP_URL}"
306
+ }
307
+
308
+ prepare_profile() {
309
+ local run_id="$1"
310
+ local primary="$2"
311
+ local run_dir="$RUNS_DIR/$run_id"
312
+
313
+ seed_primary_if_needed "$primary"
314
+ copy_profile "$primary" "$run_dir"
315
+ write_state "$run_id" "prepared" "$primary" "$run_dir"
316
+ prune_old_runs "$run_id"
317
+ print_exports "$run_id" "$primary" "$run_dir"
318
+ }
319
+
320
+ free_port() {
321
+ node <<'NODE'
322
+ const net = require("net");
323
+ const server = net.createServer();
324
+ server.listen(0, "127.0.0.1", () => {
325
+ const port = server.address().port;
326
+ server.close(() => console.log(port));
327
+ });
328
+ NODE
329
+ }
330
+
331
+ chrome_path() {
332
+ if [ -n "${PLAYWRIGHT_CHROME:-}" ]; then
333
+ echo "$PLAYWRIGHT_CHROME"
334
+ elif [ -x "/Applications/Google Chrome.app/Contents/MacOS/Google Chrome" ]; then
335
+ echo "/Applications/Google Chrome.app/Contents/MacOS/Google Chrome"
336
+ elif command -v google-chrome >/dev/null 2>&1; then
337
+ command -v google-chrome
338
+ elif command -v chromium >/dev/null 2>&1; then
339
+ command -v chromium
340
+ elif command -v chromium-browser >/dev/null 2>&1; then
341
+ command -v chromium-browser
342
+ else
343
+ die "could not find Chrome/Chromium; set PLAYWRIGHT_CHROME"
344
+ fi
345
+ }
346
+
347
+ cmd_prepare() {
348
+ local run_id=""
349
+ local primary_arg=""
350
+ while [ "$#" -gt 0 ]; do
351
+ case "$1" in
352
+ --run-id) run_id="${2:-}"; shift 2 ;;
353
+ --primary) primary_arg="${2:-}"; shift 2 ;;
354
+ -h|--help) usage; exit 0 ;;
355
+ *) die "unknown prepare arg: $1" ;;
356
+ esac
357
+ done
358
+ [ -n "$run_id" ] || run_id="$(random_hex)"
359
+ prepare_profile "$run_id" "$(resolve_primary "$primary_arg")"
360
+ }
361
+
362
+ cmd_open() {
363
+ local run_id=""
364
+ local primary_arg=""
365
+ local close_action="commit"
366
+ while [ "$#" -gt 0 ]; do
367
+ case "$1" in
368
+ --run-id) run_id="${2:-}"; shift 2 ;;
369
+ --primary) primary_arg="${2:-}"; shift 2 ;;
370
+ --commit-on-close) close_action="commit"; shift ;;
371
+ --discard-on-close) close_action="discard"; shift ;;
372
+ --keep) close_action="keep"; shift ;;
373
+ -h|--help) usage; exit 0 ;;
374
+ *) die "unknown open arg: $1" ;;
375
+ esac
376
+ done
377
+ [ -n "$run_id" ] || run_id="$(random_hex)"
378
+ local primary run_dir port cdp_url chrome log_file
379
+ primary="$(resolve_primary "$primary_arg")"
380
+ run_dir="$RUNS_DIR/$run_id"
381
+ prepare_profile "$run_id" "$primary" >/dev/null
382
+ port="$(free_port)"
383
+ cdp_url="http://127.0.0.1:$port"
384
+ chrome="$(chrome_path)"
385
+ log_file="$RUNS_DIR/$run_id.chrome.log"
386
+
387
+ "$chrome" \
388
+ --user-data-dir="$run_dir" \
389
+ --remote-debugging-port="$port" \
390
+ --no-first-run \
391
+ --no-default-browser-check \
392
+ about:blank >"$log_file" 2>&1 &
393
+ local chrome_pid=$!
394
+ write_state "$run_id" "opened" "$primary" "$run_dir" "$cdp_url" "$chrome_pid" "$close_action"
395
+ start_close_watchdog "$run_id" "$chrome_pid" "$close_action" "$log_file"
396
+ print_exports "$run_id" "$primary" "$run_dir" "$cdp_url"
397
+ echo "PW_CHROME_PID='${chrome_pid}'"
398
+ echo "PW_CHROME_LOG='${log_file}'"
399
+ echo "PW_PROFILE_CLOSE_ACTION='${close_action}'"
400
+ }
401
+
402
+ commit_profile() {
403
+ local run_id="$1"
404
+ local allow_running="${2:-false}"
405
+ load_state "$run_id"
406
+
407
+ if [ "$allow_running" != "true" ] && [ -n "${PW_CHROME_PID:-}" ] && kill -0 "$PW_CHROME_PID" 2>/dev/null; then
408
+ die "browser still running for run id $run_id; close it before commit"
409
+ fi
410
+
411
+ acquire_commit_lock
412
+ local backup="$BACKUPS_DIR/$run_id-$(date +%Y%m%d%H%M%S)"
413
+ if has_entries "$PW_PRIMARY_PROFILE"; then
414
+ copy_profile "$PW_PRIMARY_PROFILE" "$backup"
415
+ else
416
+ mkdir -p "$backup"
417
+ fi
418
+ copy_profile "$PW_USER_DATA_DIR" "$PW_PRIMARY_PROFILE"
419
+ write_state "$run_id" "committed" "$PW_PRIMARY_PROFILE" "$PW_USER_DATA_DIR" "${PW_CDP_URL:-}" "${PW_CHROME_PID:-}" "${PW_PROFILE_CLOSE_ACTION:-manual}"
420
+ echo "status=committed"
421
+ echo "run_id=$run_id"
422
+ echo "backup=$backup"
423
+ release_commit_lock
424
+ }
425
+
426
+ cleanup_profile() {
427
+ local run_id="$1"
428
+ load_state "$run_id"
429
+ rm -rf "$PW_USER_DATA_DIR" "$(state_file "$run_id")" "$RUNS_DIR/$run_id.chrome.log"
430
+ echo "status=cleaned"
431
+ echo "run_id=$run_id"
432
+ }
433
+
434
+ start_close_watchdog() {
435
+ local run_id="$1"
436
+ local chrome_pid="$2"
437
+ local close_action="$3"
438
+ local log_file="$4"
439
+
440
+ [ "$close_action" != "keep" ] || return 0
441
+
442
+ (
443
+ while kill -0 "$chrome_pid" 2>/dev/null; do
444
+ sleep 0.2
445
+ done
446
+
447
+ if [ "$close_action" = "commit" ]; then
448
+ bash "$SCRIPT_PATH" commit --run-id "$run_id" --assume-closed >>"$log_file" 2>&1 &&
449
+ bash "$SCRIPT_PATH" cleanup --run-id "$run_id" >>"$log_file" 2>&1
450
+ elif [ "$close_action" = "discard" ]; then
451
+ bash "$SCRIPT_PATH" cleanup --run-id "$run_id" >>"$log_file" 2>&1
452
+ fi
453
+ ) >/dev/null 2>&1 &
454
+ }
455
+
456
+ cmd_commit() {
457
+ local run_id_arg=""
458
+ local allow_running="false"
459
+ while [ "$#" -gt 0 ]; do
460
+ case "$1" in
461
+ --run-id) run_id_arg="${2:-}"; shift 2 ;;
462
+ --assume-closed) allow_running="true"; shift ;;
463
+ -h|--help) usage; exit 0 ;;
464
+ *) die "unknown commit arg: $1" ;;
465
+ esac
466
+ done
467
+
468
+ local run_id
469
+ run_id="$(resolve_run_id_arg "$run_id_arg")"
470
+ commit_profile "$run_id" "$allow_running"
471
+ }
472
+
473
+ cmd_cleanup() {
474
+ local run_id_arg=""
475
+ while [ "$#" -gt 0 ]; do
476
+ case "$1" in
477
+ --run-id) run_id_arg="${2:-}"; shift 2 ;;
478
+ -h|--help) usage; exit 0 ;;
479
+ *) die "unknown cleanup arg: $1" ;;
480
+ esac
481
+ done
482
+
483
+ local run_id
484
+ run_id="$(resolve_run_id_arg "$run_id_arg")"
485
+ cleanup_profile "$run_id"
486
+ }
487
+
488
+ cmd_prune() {
489
+ local keep="$MAX_RUNS"
490
+ while [ "$#" -gt 0 ]; do
491
+ case "$1" in
492
+ --keep) keep="${2:-}"; shift 2 ;;
493
+ --off) keep="off"; shift ;;
494
+ -h|--help) usage; exit 0 ;;
495
+ *) die "unknown prune arg: $1" ;;
496
+ esac
497
+ done
498
+
499
+ prune_old_runs "" "$keep" "verbose"
500
+ }
501
+
502
+ cmd_status() {
503
+ local run_id_arg=""
504
+ while [ "$#" -gt 0 ]; do
505
+ case "$1" in
506
+ --run-id) run_id_arg="${2:-}"; shift 2 ;;
507
+ -h|--help) usage; exit 0 ;;
508
+ *) die "unknown status arg: $1" ;;
509
+ esac
510
+ done
511
+
512
+ local run_id
513
+ run_id="$(resolve_run_id_arg "$run_id_arg")"
514
+ cat "$(state_file "$run_id")"
515
+ }
516
+
517
+ main() {
518
+ local command="${1:-}"
519
+ [ -n "$command" ] || { usage; exit 1; }
520
+ shift || true
521
+ mkdir -p "$RUNS_DIR" "$STATE_DIR"
522
+
523
+ case "$command" in
524
+ prepare) cmd_prepare "$@" ;;
525
+ open) cmd_open "$@" ;;
526
+ commit) cmd_commit "$@" ;;
527
+ cleanup) cmd_cleanup "$@" ;;
528
+ prune) cmd_prune "$@" ;;
529
+ status) cmd_status "$@" ;;
530
+ -h|--help) usage ;;
531
+ *) die "unknown command: $command" ;;
532
+ esac
533
+ }
534
+
535
+ main "$@"
@@ -1,6 +1,6 @@
1
1
  ---
2
2
  name: whisper-cpp-transcribe
3
- description: Transcribe or subtitle audio/video files using whisper.cpp (whisper-cpp), with a default large-v3 model path and ffmpeg-based extraction.
3
+ description: Transcribe or subtitle audio/video files using whisper.cpp (whisper-cpp), with a default medium model path and ffmpeg-based extraction.
4
4
  ---
5
5
 
6
6
  ## Overview
@@ -8,29 +8,50 @@ Use this skill when the user wants Whisper.cpp to convert speech in local media
8
8
 
9
9
  ## Quick Start
10
10
  1. Confirm `whisper-cpp` is installed and discoverable (`whisper-cli -h`).
11
+ - If missing on macOS with Homebrew available, install it with `brew install whisper-cpp`.
12
+ - Do not reinstall when present. Use `brew upgrade whisper-cpp` only when the user explicitly asks to update the installed package.
11
13
  2. Resolve input type:
12
14
  - If input is video or unsupported audio format, extract WAV first with `ffmpeg`.
13
15
  - If input is WAV/MP3/OGG/FLAC, pass it directly.
14
- 3. Ensure model path exists: `$HOME/.cache/whisper-cpp/models/ggml-large-v3.bin`.
16
+ 3. Ensure model path exists: `$HOME/.cache/whisper-cpp/models/ggml-medium.bin`.
15
17
  4. Run `whisper-cli` with `-m <model>` and `-f <input>` and requested output flags (`-otxt`, `-osrt`, etc.).
16
18
  5. Clean up temporary extracted files.
17
19
 
20
+ ## Install / Update
21
+ - Check first:
22
+ - `command -v whisper-cli`
23
+ - `whisper-cli -h`
24
+ - If `whisper-cli` is missing and Homebrew is available:
25
+ - `brew install whisper-cpp`
26
+ - If `whisper-cli` is present:
27
+ - do not install again
28
+ - If the user explicitly asks to update the installed package:
29
+ - `brew upgrade whisper-cpp`
30
+
18
31
  ## Default Model
19
- - Primary model: `ggml-large-v3.bin`
32
+ - Primary model: `ggml-medium.bin`
20
33
  - Default location expected by this skill:
21
- - `"$HOME/.cache/whisper-cpp/models/ggml-large-v3.bin"`
22
- - If the file is missing:
23
- - Download on stable Wi-Fi only:
24
- - `curl -L --fail --show-error -o "$HOME/.cache/whisper-cpp/models/ggml-large-v3.bin" https://huggingface.co/ggerganov/whisper.cpp/resolve/main/ggml-large-v3.bin`
34
+ - `"$HOME/.cache/whisper-cpp/models/ggml-medium.bin"`
35
+ - Expected file:
36
+ - size: `1533763059` bytes
37
+ - SHA-256: `6c14d5adee5f86394037b4e4e8b59f1673b6cee10e3cf0b11bbdbee79c156208`
38
+ - If the file is missing or does not match the expected size/hash:
39
+ - Download on stable Wi-Fi only, using a resumable temporary file and atomic rename:
40
+ - `mkdir -p "$HOME/.cache/whisper-cpp/models"`
41
+ - `curl -L -C - --fail --show-error --retry 5 --retry-delay 2 -o "$HOME/.cache/whisper-cpp/models/ggml-medium.bin.tmp" https://huggingface.co/ggerganov/whisper.cpp/resolve/main/ggml-medium.bin`
42
+ - `test "$(wc -c < "$HOME/.cache/whisper-cpp/models/ggml-medium.bin.tmp" | tr -d ' ')" = "1533763059"`
43
+ - `test "$(shasum -a 256 "$HOME/.cache/whisper-cpp/models/ggml-medium.bin.tmp" | cut -d' ' -f1)" = "6c14d5adee5f86394037b4e4e8b59f1673b6cee10e3cf0b11bbdbee79c156208"`
44
+ - `mv "$HOME/.cache/whisper-cpp/models/ggml-medium.bin.tmp" "$HOME/.cache/whisper-cpp/models/ggml-medium.bin"`
25
45
 
26
46
  ## Workflow
27
47
  - Install check:
48
+ - `command -v whisper-cli || brew install whisper-cpp`
28
49
  - `whisper-cli -h`
29
50
  - `ffmpeg -version` (required only when input video or unsupported audio format is used)
30
51
  - Extract audio when needed:
31
52
  - `ffmpeg -y -i "<input>" -ac 1 -ar 16000 -c:a pcm_s16le "<tmp>.wav"`
32
53
  - Transcribe:
33
- - `whisper-cli -m "$HOME/.cache/whisper-cpp/models/ggml-large-v3.bin" -f "<audio>" -otxt -osrt`
54
+ - `whisper-cli -m "$HOME/.cache/whisper-cpp/models/ggml-medium.bin" -f "<audio>" -otxt -osrt`
34
55
  - Optional translation:
35
56
  - append `-tr`
36
57
  - Optional language override:
@@ -39,9 +60,9 @@ Use this skill when the user wants Whisper.cpp to convert speech in local media
39
60
  - `-of "<output-path-without-extension>"`
40
61
 
41
62
  ## Decision Points
42
- - If download bandwidth is constrained, ask before downloading the large-v3 model.
63
+ - If download bandwidth is constrained, ask before downloading the medium model.
43
64
  - If input is a large file and hardware is limited:
44
- - suggest `small`/`base` model first, then run `large-v3` if needed.
65
+ - suggest `small`/`base` model first, then run `medium` if needed.
45
66
  - If only subtitle output is requested:
46
67
  - use `-osrt` or `-ovtt` instead of `-otxt`.
47
68
  - If audio is noisy and transcripts are poor:
@@ -49,6 +70,9 @@ Use this skill when the user wants Whisper.cpp to convert speech in local media
49
70
  - `--vad -vm ggml-silero-v6.2.0.bin` after obtaining the VAD model.
50
71
 
51
72
  ## Validation
73
+ - Confirm model integrity:
74
+ - `wc -c "$HOME/.cache/whisper-cpp/models/ggml-medium.bin"`
75
+ - `shasum -a 256 "$HOME/.cache/whisper-cpp/models/ggml-medium.bin"`
52
76
  - Run one small sample and inspect:
53
77
  - generated `<input>.txt`
54
78
  - generated `<input>.srt` (if requested)
@@ -26,6 +26,7 @@ import { getConfig } from "../utils/config";
26
26
  import { isRetryableApiError, sleep } from "../utils/retry";
27
27
  import { registerActiveHandle, unregisterActiveHandle } from "../core/active-handles";
28
28
  import { resolveJobPrompt } from "../core/job-prompt";
29
+ import { getSdkSkillsSetting } from "../core/skills";
29
30
 
30
31
  const IDLE_TIMEOUT = 10 * 60 * 1000; // 10 minutes
31
32
  const LONG_RUNNING_WARN = 30 * 60 * 1000; // 30 minutes
@@ -330,7 +331,7 @@ export async function createChatEngine(opts: EngineOptions): Promise<ChatEngine>
330
331
  permissionMode: "bypassPermissions",
331
332
  includePartialMessages: true,
332
333
  settingSources: ["project", "user"],
333
- skills: [],
334
+ skills: getSdkSkillsSetting(),
334
335
  };
335
336
  const model = resolveSdkModel(contextModel);
336
337
  if (model) {
@@ -17,6 +17,7 @@ import { ActiveEngine } from "../db/models";
17
17
  import { log } from "../utils/log";
18
18
  import { isRetryableApiError, sleep } from "../utils/retry";
19
19
  import { registerActiveHandle, unregisterActiveHandle } from "./active-handles";
20
+ import { getSdkSkillsSetting } from "./skills";
20
21
 
21
22
  export { buildWorkingMemory } from "./job-prompt";
22
23
 
@@ -119,7 +120,7 @@ export async function runJobWithClaude(
119
120
  cwd,
120
121
  permissionMode: "bypassPermissions",
121
122
  sessionId,
122
- skills: [],
123
+ skills: getSdkSkillsSetting(),
123
124
  };
124
125
 
125
126
  if (model && model !== "default") {
@@ -10,6 +10,8 @@ const PROJECT_ROOT = resolve(import.meta.dir, "../..");
10
10
 
11
11
  export type SkillInfo = { name: string; description: string; source: string };
12
12
 
13
+ export const SDK_SKILLS_SETTING = "all" as const;
14
+
13
15
  const SKILL_DIRS: { dir: string; source: string }[] = [
14
16
  { dir: join(process.cwd(), "skills"), source: "cwd" },
15
17
  { dir: join(PROJECT_ROOT, "skills"), source: "project" },
@@ -71,3 +73,7 @@ export function getSkillsSummary(): string {
71
73
  const lines = skills.map((s) => (s.description ? `- /${s.name}: ${s.description}` : `- /${s.name}`));
72
74
  return `Available skills:\n${lines.join("\n")}`;
73
75
  }
76
+
77
+ export function getSdkSkillsSetting(): typeof SDK_SKILLS_SETTING {
78
+ return SDK_SKILLS_SETTING;
79
+ }