niahere 0.2.85 → 0.2.86
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
package/skills/qa/SKILL.md
CHANGED
|
@@ -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
|
|
package/skills/qa/playwright.md
CHANGED
|
@@ -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
|
|
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
|
-
- **
|
|
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
|
-
-
|
|
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.
|
|
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
|
|
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
|
|
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
|
-
|
|
|
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
|
|
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-
|
|
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-
|
|
32
|
+
- Primary model: `ggml-medium.bin`
|
|
20
33
|
- Default location expected by this skill:
|
|
21
|
-
- `"$HOME/.cache/whisper-cpp/models/ggml-
|
|
22
|
-
-
|
|
23
|
-
-
|
|
24
|
-
|
|
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-
|
|
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
|
|
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 `
|
|
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)
|