browser-automation-skill 0.71.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (117) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +144 -0
  3. package/SECURITY.md +39 -0
  4. package/SKILL.md +206 -0
  5. package/bin/cli.mjs +55 -0
  6. package/install.sh +143 -0
  7. package/package.json +54 -0
  8. package/references/adapter-candidates.md +40 -0
  9. package/references/browser-mcp-cheatsheet.md +132 -0
  10. package/references/browser-stats-cheatsheet.md +155 -0
  11. package/references/chrome-devtools-mcp-cheatsheet.md +232 -0
  12. package/references/midscene-integration.md +359 -0
  13. package/references/obscura-cheatsheet.md +103 -0
  14. package/references/playwright-cli-cheatsheet.md +64 -0
  15. package/references/playwright-lib-cheatsheet.md +90 -0
  16. package/references/recipes/add-a-tool-adapter.md +134 -0
  17. package/references/recipes/agent-workflows/README.md +37 -0
  18. package/references/recipes/agent-workflows/cache-driven-bulk-operation.md +110 -0
  19. package/references/recipes/agent-workflows/flow-record-and-replay.md +102 -0
  20. package/references/recipes/agent-workflows/incremental-pattern-discovery.md +125 -0
  21. package/references/recipes/agent-workflows/login-then-scrape.md +100 -0
  22. package/references/recipes/anti-patterns-tool-extension.md +182 -0
  23. package/references/recipes/body-bytes-not-body.md +139 -0
  24. package/references/recipes/cache-write-security.md +210 -0
  25. package/references/recipes/fingerprint-rescue.md +154 -0
  26. package/references/recipes/model-routing.md +143 -0
  27. package/references/recipes/path-security.md +138 -0
  28. package/references/recipes/privacy-canary.md +96 -0
  29. package/references/recipes/visual-rescue-hook.md +182 -0
  30. package/references/stats-prices.json +42 -0
  31. package/references/stats-schema.json +77 -0
  32. package/references/tool-versions.md +8 -0
  33. package/scripts/browser-add-site.sh +113 -0
  34. package/scripts/browser-assert.sh +106 -0
  35. package/scripts/browser-audit.sh +68 -0
  36. package/scripts/browser-baseline.sh +135 -0
  37. package/scripts/browser-click.sh +100 -0
  38. package/scripts/browser-creds-add.sh +254 -0
  39. package/scripts/browser-creds-list.sh +67 -0
  40. package/scripts/browser-creds-migrate.sh +122 -0
  41. package/scripts/browser-creds-remove.sh +69 -0
  42. package/scripts/browser-creds-rotate-totp.sh +109 -0
  43. package/scripts/browser-creds-show.sh +82 -0
  44. package/scripts/browser-creds-totp.sh +94 -0
  45. package/scripts/browser-do.sh +630 -0
  46. package/scripts/browser-doctor.sh +365 -0
  47. package/scripts/browser-drag.sh +90 -0
  48. package/scripts/browser-extract.sh +192 -0
  49. package/scripts/browser-fill.sh +142 -0
  50. package/scripts/browser-flow.sh +316 -0
  51. package/scripts/browser-history.sh +187 -0
  52. package/scripts/browser-hover.sh +92 -0
  53. package/scripts/browser-inspect.sh +188 -0
  54. package/scripts/browser-list-sessions.sh +78 -0
  55. package/scripts/browser-list-sites.sh +42 -0
  56. package/scripts/browser-login.sh +279 -0
  57. package/scripts/browser-mcp.sh +65 -0
  58. package/scripts/browser-migrate.sh +195 -0
  59. package/scripts/browser-open.sh +134 -0
  60. package/scripts/browser-press.sh +80 -0
  61. package/scripts/browser-remove-session.sh +72 -0
  62. package/scripts/browser-remove-site.sh +68 -0
  63. package/scripts/browser-replay.sh +206 -0
  64. package/scripts/browser-route.sh +174 -0
  65. package/scripts/browser-select.sh +122 -0
  66. package/scripts/browser-show-session.sh +57 -0
  67. package/scripts/browser-show-site.sh +37 -0
  68. package/scripts/browser-snapshot.sh +176 -0
  69. package/scripts/browser-stats.sh +522 -0
  70. package/scripts/browser-tab-close.sh +112 -0
  71. package/scripts/browser-tab-list.sh +70 -0
  72. package/scripts/browser-tab-switch.sh +111 -0
  73. package/scripts/browser-upload.sh +132 -0
  74. package/scripts/browser-use.sh +60 -0
  75. package/scripts/browser-vlm.sh +707 -0
  76. package/scripts/browser-wait.sh +97 -0
  77. package/scripts/install-git-hooks.sh +16 -0
  78. package/scripts/lib/capture.sh +356 -0
  79. package/scripts/lib/common.sh +262 -0
  80. package/scripts/lib/credential.sh +237 -0
  81. package/scripts/lib/fingerprint-rescue.js +123 -0
  82. package/scripts/lib/flow.sh +448 -0
  83. package/scripts/lib/flow_record.sh +210 -0
  84. package/scripts/lib/mask.sh +49 -0
  85. package/scripts/lib/memory.sh +427 -0
  86. package/scripts/lib/migrate.sh +390 -0
  87. package/scripts/lib/migrators/README.md +23 -0
  88. package/scripts/lib/migrators/memory/v1_to_v2.sh +15 -0
  89. package/scripts/lib/migrators/recent_urls/README.md +13 -0
  90. package/scripts/lib/migrators/stats/README.md +24 -0
  91. package/scripts/lib/node/chrome-devtools-bridge.mjs +1812 -0
  92. package/scripts/lib/node/mcp-server.mjs +531 -0
  93. package/scripts/lib/node/mcp-tools.json +68 -0
  94. package/scripts/lib/node/playwright-driver.mjs +1104 -0
  95. package/scripts/lib/node/totp-core.mjs +52 -0
  96. package/scripts/lib/node/totp.mjs +52 -0
  97. package/scripts/lib/node/url-pattern-cluster.mjs +102 -0
  98. package/scripts/lib/node/url-pattern-resolver.mjs +77 -0
  99. package/scripts/lib/output.sh +79 -0
  100. package/scripts/lib/router.sh +342 -0
  101. package/scripts/lib/sanitize.sh +107 -0
  102. package/scripts/lib/secret/keychain.sh +91 -0
  103. package/scripts/lib/secret/libsecret.sh +74 -0
  104. package/scripts/lib/secret/plaintext.sh +75 -0
  105. package/scripts/lib/secret_backend_select.sh +57 -0
  106. package/scripts/lib/session.sh +153 -0
  107. package/scripts/lib/site.sh +126 -0
  108. package/scripts/lib/stats.sh +419 -0
  109. package/scripts/lib/tool/.gitkeep +0 -0
  110. package/scripts/lib/tool/chrome-devtools-mcp.sh +349 -0
  111. package/scripts/lib/tool/obscura.sh +249 -0
  112. package/scripts/lib/tool/playwright-cli.sh +155 -0
  113. package/scripts/lib/tool/playwright-lib.sh +106 -0
  114. package/scripts/lib/verb_helpers.sh +222 -0
  115. package/scripts/lib/visual-rescue-default.sh +145 -0
  116. package/scripts/regenerate-docs.sh +99 -0
  117. package/uninstall.sh +51 -0
@@ -0,0 +1,365 @@
1
+ #!/usr/bin/env bash
2
+ # browser-doctor — health check, exits non-zero on issues. Zero network calls.
3
+ set -euo pipefail
4
+ IFS=$'\n\t'
5
+
6
+ SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
7
+ # shellcheck source=lib/common.sh
8
+ # shellcheck disable=SC1091
9
+ source "${SCRIPT_DIR}/lib/common.sh"
10
+ init_paths
11
+
12
+ started_at_ms="$(now_ms)"
13
+ problems=0
14
+
15
+ # Required check: increments problems on miss. Doctor will exit non-zero.
16
+ check_cmd() {
17
+ local cmd="$1" hint="$2"
18
+ if command -v "${cmd}" >/dev/null 2>&1; then
19
+ ok "${cmd} found: $(command -v "${cmd}")"
20
+ else
21
+ warn "${cmd} NOT FOUND"
22
+ warn " remediation: ${hint}"
23
+ problems=$((problems + 1))
24
+ fi
25
+ }
26
+
27
+ # Advisory check: prints status but does NOT increment problems. Use for tools
28
+ # that are required by later phases but optional in the current phase, OR for
29
+ # tools that the user will install when they actually need them.
30
+ check_cmd_advisory() {
31
+ local cmd="$1" hint="$2"
32
+ if command -v "${cmd}" >/dev/null 2>&1; then
33
+ ok "${cmd} found: $(command -v "${cmd}")"
34
+ else
35
+ warn "${cmd} NOT FOUND (advisory only — does not fail doctor)"
36
+ warn " remediation: ${hint}"
37
+ fi
38
+ }
39
+
40
+ check_bash_version() {
41
+ local major="${BASH_VERSINFO[0]:-0}"
42
+ if [ "${major}" -ge 4 ]; then
43
+ ok "bash version: ${BASH_VERSION}"
44
+ else
45
+ warn "bash ${BASH_VERSION} is too old (need >= 4)"
46
+ warn " remediation: brew install bash"
47
+ problems=$((problems + 1))
48
+ fi
49
+ }
50
+
51
+ check_home() {
52
+ if [ ! -d "${BROWSER_SKILL_HOME}" ]; then
53
+ warn "${BROWSER_SKILL_HOME} does not exist"
54
+ warn " remediation: run ./install.sh from the repo root"
55
+ problems=$((problems + 1))
56
+ return 0
57
+ fi
58
+ local mode
59
+ mode="$(file_mode "${BROWSER_SKILL_HOME}")"
60
+ [ -n "${mode}" ] || mode="?"
61
+ if [ "${mode}" != "700" ]; then
62
+ warn "${BROWSER_SKILL_HOME} has mode ${mode}, expected 700"
63
+ warn " remediation: chmod 700 ${BROWSER_SKILL_HOME}"
64
+ problems=$((problems + 1))
65
+ else
66
+ ok "${BROWSER_SKILL_HOME} mode 700"
67
+ fi
68
+ }
69
+
70
+ ok "browser-skill home: ${BROWSER_SKILL_HOME}"
71
+ ok "browser-skill doctor"
72
+
73
+ check_cmd jq "brew install jq (macOS) or apt install jq (Debian)"
74
+ check_cmd python3 "brew install python3 (macOS) or apt install python3"
75
+ check_bash_version
76
+ check_home
77
+ # Tools below are recommended but not required in Phase 1; later phases will
78
+ # elevate these to required and add version-pinning logic.
79
+ check_cmd node "brew install node (>=20) — required by playwright-cli adapter; was advisory in Phase 1-2"
80
+
81
+ check_disk_encryption() {
82
+ case "$(uname -s)" in
83
+ Darwin)
84
+ if command -v fdesetup >/dev/null 2>&1; then
85
+ local status
86
+ status="$(fdesetup status 2>/dev/null || true)"
87
+ case "${status}" in
88
+ *"FileVault is On"*) ok "disk encryption: FileVault on" ;;
89
+ *"FileVault is Off"*) warn "disk encryption: FileVault OFF (advisory — 0600 modes are paper without disk encryption)" ;;
90
+ *) warn "disk encryption: status unknown (fdesetup said: ${status:-empty})" ;;
91
+ esac
92
+ else
93
+ warn "disk encryption: fdesetup not found (cannot verify)"
94
+ fi
95
+ ;;
96
+ Linux)
97
+ if command -v lsblk >/dev/null 2>&1 && lsblk -o NAME,FSTYPE 2>/dev/null | grep -q crypto_LUKS; then
98
+ ok "disk encryption: LUKS-backed volume detected"
99
+ else
100
+ warn "disk encryption: no LUKS volume found (advisory)"
101
+ fi
102
+ ;;
103
+ *)
104
+ warn "disk encryption: unknown OS — please verify manually"
105
+ ;;
106
+ esac
107
+ }
108
+
109
+ check_disk_encryption
110
+
111
+ # --- Adapter aggregation (extension model §5.2) ---
112
+ # Walk lib/tool/*.sh in subshells; collect each adapter's tool_doctor_check.
113
+ # Subshell isolation prevents tool_open / tool_click / etc. from colliding.
114
+ adapters_ok=0
115
+ adapters_failed=0
116
+ adapter_files=("${LIB_TOOL_DIR}"/*.sh)
117
+
118
+ if [ ! -f "${adapter_files[0]}" ]; then
119
+ warn "no adapters found under ${LIB_TOOL_DIR}"
120
+ else
121
+ for adapter_file in "${adapter_files[@]}"; do
122
+ adapter_name="$(basename "${adapter_file}" .sh)"
123
+ result="$(
124
+ # shellcheck source=/dev/null
125
+ source "${adapter_file}" 2>/dev/null
126
+ tool_doctor_check 2>/dev/null
127
+ )" || result='{"ok":false,"error":"adapter source failed"}'
128
+
129
+ jq -c --arg n "${adapter_name}" '. + {check:"adapter",adapter:$n}' <<<"${result}"
130
+
131
+ if [ "$(printf '%s' "${result}" | jq -r .ok 2>/dev/null)" = "true" ]; then
132
+ adapters_ok=$((adapters_ok + 1))
133
+ ok "adapter ${adapter_name}: ok"
134
+ else
135
+ adapters_failed=$((adapters_failed + 1))
136
+ warn "adapter ${adapter_name}: $(printf '%s' "${result}" | jq -r '.error // "failed"')"
137
+ fi
138
+ done
139
+ fi
140
+
141
+ # --- Credentials count (advisory; never fails doctor) ---
142
+ # Phase 5 part 2d: walk ${CREDENTIALS_DIR}/*.json and report per-backend.
143
+ # .secret files are payload, not metadata, so they're skipped.
144
+ creds_total=0
145
+ creds_keychain=0
146
+ creds_libsecret=0
147
+ creds_plaintext=0
148
+ if [ -d "${CREDENTIALS_DIR}" ]; then
149
+ shopt -s nullglob
150
+ for cred_file in "${CREDENTIALS_DIR}"/*.json; do
151
+ creds_total=$((creds_total + 1))
152
+ backend="$(jq -r .backend "${cred_file}" 2>/dev/null || printf 'unknown')"
153
+ case "${backend}" in
154
+ keychain) creds_keychain=$((creds_keychain + 1)) ;;
155
+ libsecret) creds_libsecret=$((creds_libsecret + 1)) ;;
156
+ plaintext) creds_plaintext=$((creds_plaintext + 1)) ;;
157
+ esac
158
+ done
159
+ shopt -u nullglob
160
+ fi
161
+ ok "credentials: ${creds_total} total (keychain: ${creds_keychain}, libsecret: ${creds_libsecret}, plaintext: ${creds_plaintext})"
162
+
163
+ # --- Captures sanitization counter (advisory; never fails doctor) ---
164
+ # Phase 7 part 1-iv: walk ${CAPTURES_DIR}/*/meta.json and count total +
165
+ # sanitized:false. Missing/null .sanitized treated as sanitized=true
166
+ # (forward-compat with pre-7-1-iv captures).
167
+ captures_total=0
168
+ captures_unsanitized=0
169
+ captures_unsanitized_ids=""
170
+ if [ -d "${CAPTURES_DIR}" ]; then
171
+ shopt -s nullglob
172
+ for capture_meta in "${CAPTURES_DIR}"/*/meta.json; do
173
+ captures_total=$((captures_total + 1))
174
+ # Note: don't use `// true` — jq's `//` fires on null OR false, so a
175
+ # legit sanitized=false would resolve to "true". Read raw; missing field
176
+ # surfaces as "null" which is correctly NOT-equal-to-"false" below.
177
+ sanitized="$(jq -r '.sanitized' "${capture_meta}" 2>/dev/null || printf 'null')"
178
+ if [ "${sanitized}" = "false" ]; then
179
+ captures_unsanitized=$((captures_unsanitized + 1))
180
+ capture_id="$(jq -r '.capture_id // "?"' "${capture_meta}" 2>/dev/null || printf '?')"
181
+ captures_unsanitized_ids="${captures_unsanitized_ids:+${captures_unsanitized_ids}, }captures/${capture_id}/"
182
+ fi
183
+ done
184
+ shopt -u nullglob
185
+ fi
186
+ ok "captures: ${captures_total} total (sanitized:false: ${captures_unsanitized})"
187
+ if [ "${captures_unsanitized}" -gt 0 ]; then
188
+ warn "${captures_unsanitized} capture(s) with sanitization disabled — review ${captures_unsanitized_ids}"
189
+ fi
190
+
191
+ # --- Pending migrations (advisory; never fails doctor) ---
192
+ # Phase 10 follow-up. Sources lib/migrate.sh and calls migrate_check, which is
193
+ # read-only by design (no lock acquired; MIG4 invariant from the Phase 10
194
+ # design doc — "doctor never auto-migrates"). Doctor surfaces pending count
195
+ # only; user invokes `browser-migrate run` to apply them.
196
+ # shellcheck source=lib/migrate.sh
197
+ # shellcheck disable=SC1091
198
+ source "${SCRIPT_DIR}/lib/migrate.sh"
199
+ migrations_pending=0
200
+ # migrate_check emits one _kind:migration_needed line per pending migrator,
201
+ # then a summary line. Count the _kind events; ignore the summary.
202
+ mig_out="$(migrate_check 2>/dev/null || true)"
203
+ if [ -n "${mig_out}" ]; then
204
+ migrations_pending="$(printf '%s\n' "${mig_out}" \
205
+ | jq -s 'map(select(._kind == "migration_needed")) | length' 2>/dev/null \
206
+ || printf '0')"
207
+ fi
208
+ jq -nc --argjson n "${migrations_pending}" '{check:"migrations", pending:$n}'
209
+ if [ "${migrations_pending}" -gt 0 ]; then
210
+ warn "${migrations_pending} pending migration(s) — run 'browser-migrate check' for details (advisory; never fails doctor)"
211
+ else
212
+ ok "no pending migrations"
213
+ fi
214
+
215
+ # --- Memory cache hit-rate (advisory; forward-compat read side) ---
216
+ # Phase 11 v2 will tee `verb=do mode=intent` summary lines into
217
+ # ${BROWSER_SKILL_HOME}/memory/events.jsonl. Doctor's read side ships now;
218
+ # absent file → "n/a" line. Lifetime ratio over all events (no time filter
219
+ # until events carry timestamps).
220
+ cache_events_log="${BROWSER_SKILL_HOME}/memory/events.jsonl"
221
+ if [ -f "${cache_events_log}" ]; then
222
+ # Phase 12 part 2 audit: single jq pass extracts both counts (one file
223
+ # read, one fork) instead of two sequential `jq -s` slurps.
224
+ cache_pair="$(jq -s -r '
225
+ [(map(select(.cache_hit == true or .cache_hit == false)) | length),
226
+ (map(select(.cache_hit == true)) | length)] | @tsv
227
+ ' "${cache_events_log}" 2>/dev/null || printf '0\t0')"
228
+ IFS=$'\t' read -r cache_total cache_hits <<<"${cache_pair}"
229
+ cache_total="${cache_total:-0}"
230
+ cache_hits="${cache_hits:-0}"
231
+ if [ "${cache_total}" -gt 0 ]; then
232
+ # Integer-only math; bc not available everywhere and shellcheck dislikes pipe-to-bc.
233
+ cache_rate_pct=$(( cache_hits * 100 / cache_total ))
234
+ ok "memory cache hit rate: ${cache_rate_pct}% (${cache_hits}/${cache_total} events)"
235
+ jq -nc \
236
+ --argjson hits "${cache_hits}" \
237
+ --argjson total "${cache_total}" \
238
+ --argjson pct "${cache_rate_pct}" \
239
+ '{check:"memory_cache", hits:$hits, total:$total, hit_rate_pct:$pct}'
240
+ else
241
+ ok "memory cache hit rate: n/a (events log present but empty)"
242
+ jq -nc '{check:"memory_cache", hits:0, total:0, hit_rate_pct:null}'
243
+ fi
244
+ else
245
+ ok "memory cache hit rate: n/a (no events yet — run 'browser-do --intent' to generate cache observations)"
246
+ jq -nc '{check:"memory_cache", hits:0, total:0, hit_rate_pct:null}'
247
+ fi
248
+
249
+ # --- Tier 3: recent_urls.jsonl line count (advisory; forward-compat read side) ---
250
+ # Parallel to memory_cache check above. Phase 11 v2 Pick A6 (PR #125) added
251
+ # the writer; doctor reports the line count so users see passive observation
252
+ # is actually accumulating. Absent log → 0 entries (not an error).
253
+ recent_urls_log="${BROWSER_SKILL_HOME}/memory/recent_urls.jsonl"
254
+ if [ -f "${recent_urls_log}" ]; then
255
+ recent_urls_count="$(jq -s 'length' "${recent_urls_log}" 2>/dev/null || printf '0')"
256
+ ok "recent_urls: ${recent_urls_count} entries (passive navigation log)"
257
+ else
258
+ recent_urls_count=0
259
+ ok "recent_urls: 0 entries (no navigations yet — run 'browser-open --site SITE --url URL' to populate)"
260
+ fi
261
+ jq -nc --argjson n "${recent_urls_count}" '{check:"recent_urls", count:$n}'
262
+
263
+ # --- Tier 3: stats.jsonl — per-action telemetry health (Phase 12 part 1) ---
264
+ # Parallel to memory_cache + recent_urls checks. Reports event count, success
265
+ # rate over the last 7 days, and an oblivious_success warning when > 0. Absent
266
+ # log → 0 entries (not an error). Doctor never rebuilds the SQLite mirror —
267
+ # that's `browser-stats rebuild`'s job.
268
+ stats_jsonl_log="${BROWSER_SKILL_HOME}/memory/stats.jsonl"
269
+ if [ -f "${stats_jsonl_log}" ]; then
270
+ # Phase 12 part 2 audit: single jq pass extracts {total, success, oblivious}
271
+ # in one file read + one fork. Replaces 3× sequential `jq -s` slurps.
272
+ stats_triple="$(jq -s -r '
273
+ [length,
274
+ (map(select(.outcome == "success")) | length),
275
+ (map(select(.failure_mode == "oblivious_success")) | length)] | @tsv
276
+ ' "${stats_jsonl_log}" 2>/dev/null || printf '0\t0\t0')"
277
+ IFS=$'\t' read -r stats_total stats_success stats_oblivious <<<"${stats_triple}"
278
+ stats_total="${stats_total:-0}"
279
+ stats_success="${stats_success:-0}"
280
+ stats_oblivious="${stats_oblivious:-0}"
281
+ if [ "${stats_total}" -gt 0 ]; then
282
+ stats_success_pct=$(( stats_success * 100 / stats_total ))
283
+ ok "stats events: ${stats_total} (${stats_success_pct}% success)"
284
+ else
285
+ ok "stats events: 0 (log present but empty)"
286
+ stats_success_pct=0
287
+ fi
288
+ if [ "${stats_oblivious}" -gt 0 ]; then
289
+ warn "${stats_oblivious} oblivious_success event(s) — adapter reported ok but post-condition failed; run 'browser-stats report'"
290
+ # Phase 14+ stats-driven pruning surface: count (site, selector)
291
+ # GROUPS that have accumulated ≥3 oblivious_success events overall.
292
+ # Doctor stays date-agnostic so the jq is fast + ISO-8601-precision
293
+ # agnostic; `browser-stats prune --days N` does the date-filtered
294
+ # version when the user investigates.
295
+ prune_count="$(jq -s -r '
296
+ [ .[] | select(.failure_mode == "oblivious_success"
297
+ and .site != null and .selector_value != null) ]
298
+ | group_by([.site, .selector_value])
299
+ | map(select(length >= 3))
300
+ | length
301
+ ' "${stats_jsonl_log}" 2>/dev/null || printf '0')"
302
+ if [ "${prune_count}" -gt 0 ]; then
303
+ warn "${prune_count} cache archetype(s) with ≥3 oblivious_success in last 7d — run 'browser-stats prune' for candidates, '--apply' to disable"
304
+ fi
305
+ fi
306
+ jq -nc \
307
+ --argjson total "${stats_total}" \
308
+ --argjson success "${stats_success}" \
309
+ --argjson oblivious "${stats_oblivious}" \
310
+ --argjson pct "${stats_success_pct:-0}" \
311
+ '{check:"stats", total:$total, success:$success, success_pct:$pct, oblivious_success:$oblivious}'
312
+ else
313
+ ok "stats events: 0 (no telemetry yet — emitted automatically by open/click/fill/snapshot/extract)"
314
+ jq -nc '{check:"stats", total:0, success:0, success_pct:null, oblivious_success:0}'
315
+ fi
316
+
317
+ # --- Phase 14: local VLM reachability (advisory; OPTIONAL stack, never fails doctor) ---
318
+ # Probes the same endpoint scripts/browser-vlm.sh defaults to. Honors the
319
+ # same BROWSER_SKILL_VLM_HOST + BROWSER_SKILL_VLM_PORT env overrides so doctor
320
+ # and the vlm wrapper agree on what "the local VLM" means.
321
+ # Outcomes:
322
+ # reachable → ok: "local VLM reachable @ http://HOST:PORT (advisory)"
323
+ # unreachable → ok: "local VLM not running (advisory — see browser-vlm.sh start)"
324
+ # Never warns. The VLM is opt-in for Path 3 cache-rescue; absence is normal.
325
+ vlm_host="${BROWSER_SKILL_VLM_HOST:-127.0.0.1}"
326
+ vlm_port="${BROWSER_SKILL_VLM_PORT:-8080}"
327
+ vlm_endpoint="http://${vlm_host}:${vlm_port}"
328
+ vlm_reachable="false"
329
+ if curl -sfm 2 "${vlm_endpoint}/health" >/dev/null 2>&1; then
330
+ vlm_reachable="true"
331
+ ok "local VLM reachable @ ${vlm_endpoint} (advisory — Path 3 cache-rescue ready)"
332
+ else
333
+ ok "local VLM not running @ ${vlm_endpoint} (advisory — start via 'bash scripts/browser-vlm.sh start' if Path 3 wanted)"
334
+ fi
335
+ jq -nc --arg endpoint "${vlm_endpoint}" --argjson reachable "${vlm_reachable}" \
336
+ '{check:"local_vlm", endpoint:$endpoint, reachable:$reachable}'
337
+
338
+ duration_ms=$(( $(now_ms) - started_at_ms ))
339
+
340
+ # Status semantics (§5.3 of extension-model spec).
341
+ if [ "${problems}" -gt 0 ]; then
342
+ overall_status="error"
343
+ exit_code="${EXIT_PREFLIGHT_FAILED}"
344
+ elif [ "${adapters_ok}" -eq 0 ] && [ "${adapters_failed}" -gt 0 ]; then
345
+ overall_status="error"
346
+ exit_code="${EXIT_PREFLIGHT_FAILED}"
347
+ elif [ "${adapters_failed}" -gt 0 ]; then
348
+ overall_status="partial"
349
+ exit_code="${EXIT_OK}"
350
+ else
351
+ overall_status="ok"
352
+ exit_code="${EXIT_OK}"
353
+ fi
354
+
355
+ if [ "${overall_status}" = "ok" ]; then
356
+ ok "all checks passed (${adapters_ok} adapter(s) ok)"
357
+ else
358
+ warn "${problems} core problem(s); ${adapters_ok} adapter(s) ok, ${adapters_failed} failed"
359
+ fi
360
+
361
+ summary_json verb=doctor tool=none why=health-check status="${overall_status}" \
362
+ problems="${problems}" \
363
+ adapters_ok="${adapters_ok}" adapters_failed="${adapters_failed}" \
364
+ duration_ms="${duration_ms}"
365
+ exit "${exit_code}"
@@ -0,0 +1,90 @@
1
+ #!/usr/bin/env bash
2
+ # scripts/browser-drag.sh — pointer drag from src element to dst element by ref.
3
+ # Usage: bash scripts/browser-drag.sh [--site NAME] [--tool NAME] [--dry-run]
4
+ # [--raw] --src-ref eA --dst-ref eB
5
+ #
6
+ # Routes to chrome-devtools-mcp by default (Phase 6 part 5). Stateful —
7
+ # requires running daemon (refMap precondition for BOTH src and dst). MCP
8
+ # `drag` tool accepts {src_uid, dst_uid}. Selector-based path is a follow-up
9
+ # sub-part if user demand surfaces.
10
+
11
+ set -euo pipefail
12
+ IFS=$'\n\t'
13
+
14
+ SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
15
+ # shellcheck source=lib/common.sh
16
+ # shellcheck disable=SC1091
17
+ source "${SCRIPT_DIR}/lib/common.sh"
18
+ # shellcheck source=lib/output.sh
19
+ # shellcheck disable=SC1091
20
+ source "${SCRIPT_DIR}/lib/output.sh"
21
+ # shellcheck source=lib/router.sh
22
+ # shellcheck disable=SC1091
23
+ source "${SCRIPT_DIR}/lib/router.sh"
24
+ # shellcheck source=lib/verb_helpers.sh
25
+ # shellcheck disable=SC1091
26
+ source "${SCRIPT_DIR}/lib/verb_helpers.sh"
27
+
28
+ init_paths
29
+
30
+ SUMMARY_T0="$(now_ms)"; export SUMMARY_T0
31
+
32
+ parse_verb_globals "$@"
33
+
34
+ resolve_session_storage_state
35
+
36
+ src_ref="" dst_ref=""
37
+ verb_argv=()
38
+ i=0
39
+ while [ "${i}" -lt "${#REMAINING_ARGV[@]}" ]; do
40
+ case "${REMAINING_ARGV[i]}" in
41
+ --src-ref)
42
+ src_ref="${REMAINING_ARGV[i+1]:-}"
43
+ [ -n "${src_ref}" ] || die "${EXIT_USAGE_ERROR}" "--src-ref requires a value"
44
+ verb_argv+=(--src-ref "${src_ref}")
45
+ i=$((i + 2))
46
+ ;;
47
+ --dst-ref)
48
+ dst_ref="${REMAINING_ARGV[i+1]:-}"
49
+ [ -n "${dst_ref}" ] || die "${EXIT_USAGE_ERROR}" "--dst-ref requires a value"
50
+ verb_argv+=(--dst-ref "${dst_ref}")
51
+ i=$((i + 2))
52
+ ;;
53
+ *)
54
+ verb_argv+=("${REMAINING_ARGV[i]}")
55
+ i=$((i + 1))
56
+ ;;
57
+ esac
58
+ done
59
+
60
+ [ -n "${src_ref}" ] || die "${EXIT_USAGE_ERROR}" "drag requires --src-ref eN"
61
+ [ -n "${dst_ref}" ] || die "${EXIT_USAGE_ERROR}" "drag requires --dst-ref eN"
62
+
63
+ if [ "${ARG_DRY_RUN:-0}" = "1" ]; then
64
+ ok "dry-run: would drag ${src_ref} → ${dst_ref}"
65
+ emit_summary verb=drag tool=none why=dry-run status=ok \
66
+ src_ref="${src_ref}" dst_ref="${dst_ref}" dry_run=true
67
+ exit 0
68
+ fi
69
+
70
+ picked="$(pick_tool drag "${verb_argv[@]}")"
71
+ tool_name="${picked%%$'\t'*}"
72
+ why="${picked#*$'\t'}"
73
+
74
+ source_picked_adapter "${tool_name}"
75
+
76
+ set +e
77
+ adapter_out="$(invoke_with_retry drag "${verb_argv[@]}")"
78
+ adapter_rc=$?
79
+ set -e
80
+
81
+ [ -n "${adapter_out}" ] && printf '%s\n' "${adapter_out}"
82
+
83
+ if [ "${adapter_rc}" -eq 0 ]; then
84
+ emit_summary verb=drag tool="${tool_name}" why="${why}" status=ok \
85
+ src_ref="${src_ref}" dst_ref="${dst_ref}"
86
+ exit 0
87
+ fi
88
+ emit_summary verb=drag tool="${tool_name}" why="${why}" status=error \
89
+ src_ref="${src_ref}" dst_ref="${dst_ref}"
90
+ exit "${adapter_rc}"
@@ -0,0 +1,192 @@
1
+ #!/usr/bin/env bash
2
+ # scripts/browser-extract.sh — extract content via CSS selector / JS / scrape.
3
+ # Usage: bash scripts/browser-extract.sh [--site NAME] [--tool NAME] [--dry-run]
4
+ # [--raw]
5
+ # ( --selector CSS | --eval JS
6
+ # | --scrape [--eval JS] [--concurrency N] URL... )
7
+ #
8
+ # Routes to chrome-devtools-mcp by default for selector / eval (post-1d router
9
+ # promotion — only adapter with `evaluate_script` + `list_network_requests`
10
+ # per parent spec Appendix B). `--scrape` and `--stealth` auto-route to obscura
11
+ # via rule_scrape_flag / rule_stealth_flag (Phase 8-2-i, Path B).
12
+ # Exactly one mode is required: --selector / --eval / --scrape / --stealth.
13
+
14
+ set -euo pipefail
15
+ IFS=$'\n\t'
16
+
17
+ SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
18
+ # shellcheck source=lib/common.sh
19
+ # shellcheck disable=SC1091
20
+ source "${SCRIPT_DIR}/lib/common.sh"
21
+ # shellcheck source=lib/output.sh
22
+ # shellcheck disable=SC1091
23
+ source "${SCRIPT_DIR}/lib/output.sh"
24
+ # shellcheck source=lib/router.sh
25
+ # shellcheck disable=SC1091
26
+ source "${SCRIPT_DIR}/lib/router.sh"
27
+ # shellcheck source=lib/verb_helpers.sh
28
+ # shellcheck disable=SC1091
29
+ source "${SCRIPT_DIR}/lib/verb_helpers.sh"
30
+ # shellcheck source=lib/stats.sh
31
+ # shellcheck disable=SC1091
32
+ source "${SCRIPT_DIR}/lib/stats.sh"
33
+
34
+ init_paths
35
+
36
+ SUMMARY_T0="$(now_ms)"; export SUMMARY_T0
37
+
38
+ parse_verb_globals "$@"
39
+
40
+ resolve_session_storage_state
41
+
42
+ selector="" eval_js="" mode_scrape=0 mode_stealth=0 concurrency=""
43
+ verb_argv=()
44
+ positional_urls=()
45
+ i=0
46
+ while [ "${i}" -lt "${#REMAINING_ARGV[@]}" ]; do
47
+ case "${REMAINING_ARGV[i]}" in
48
+ --selector)
49
+ selector="${REMAINING_ARGV[i+1]:-}"
50
+ [ -n "${selector}" ] || die "${EXIT_USAGE_ERROR}" "--selector requires a value"
51
+ verb_argv+=(--selector "${selector}")
52
+ i=$((i + 2))
53
+ ;;
54
+ --eval)
55
+ eval_js="${REMAINING_ARGV[i+1]:-}"
56
+ [ -n "${eval_js}" ] || die "${EXIT_USAGE_ERROR}" "--eval requires a value"
57
+ verb_argv+=(--eval "${eval_js}")
58
+ i=$((i + 2))
59
+ ;;
60
+ --scrape)
61
+ mode_scrape=1
62
+ verb_argv+=(--scrape)
63
+ i=$((i + 1))
64
+ ;;
65
+ --stealth)
66
+ mode_stealth=1
67
+ verb_argv+=(--stealth)
68
+ i=$((i + 1))
69
+ ;;
70
+ --concurrency)
71
+ concurrency="${REMAINING_ARGV[i+1]:-}"
72
+ [ -n "${concurrency}" ] || die "${EXIT_USAGE_ERROR}" "--concurrency requires a value"
73
+ verb_argv+=(--concurrency "${concurrency}")
74
+ i=$((i + 2))
75
+ ;;
76
+ --*)
77
+ # Unknown flag — passthrough to adapter (defensive; adapter will reject
78
+ # if it doesn't recognise it).
79
+ verb_argv+=("${REMAINING_ARGV[i]}")
80
+ i=$((i + 1))
81
+ ;;
82
+ *)
83
+ # Positional. In --scrape / --stealth mode these are URLs. Outside both
84
+ # modes the verb script has no use for positionals (selector/eval are
85
+ # flag-only).
86
+ if [ "${mode_scrape}" = "1" ] || [ "${mode_stealth}" = "1" ]; then
87
+ positional_urls+=("${REMAINING_ARGV[i]}")
88
+ verb_argv+=("${REMAINING_ARGV[i]}")
89
+ else
90
+ die "${EXIT_USAGE_ERROR}" "unexpected positional arg '${REMAINING_ARGV[i]}' (use --selector / --eval / --scrape / --stealth)"
91
+ fi
92
+ i=$((i + 1))
93
+ ;;
94
+ esac
95
+ done
96
+
97
+ if [ "${mode_scrape}" = "1" ] && [ "${mode_stealth}" = "1" ]; then
98
+ die "${EXIT_USAGE_ERROR}" "--scrape and --stealth are mutually exclusive"
99
+ fi
100
+
101
+ if [ "${mode_scrape}" = "1" ]; then
102
+ [ "${#positional_urls[@]}" -ge 1 ] || die "${EXIT_USAGE_ERROR}" "--scrape requires at least one URL"
103
+ elif [ "${mode_stealth}" = "1" ]; then
104
+ [ "${#positional_urls[@]}" -eq 1 ] || die "${EXIT_USAGE_ERROR}" "--stealth requires exactly one URL"
105
+ [ -n "${eval_js}" ] || die "${EXIT_USAGE_ERROR}" "--stealth requires --eval EXPR"
106
+ elif [ -z "${selector}" ] && [ -z "${eval_js}" ]; then
107
+ die "${EXIT_USAGE_ERROR}" "extract requires --selector CSS, --eval JS, --scrape URL..., or --stealth URL"
108
+ fi
109
+
110
+ if [ "${ARG_DRY_RUN:-0}" = "1" ]; then
111
+ if [ "${mode_scrape}" = "1" ]; then
112
+ ok "dry-run: would scrape ${#positional_urls[@]} URL(s) via obscura"
113
+ emit_summary verb=extract tool=none why=dry-run status=ok mode=scrape \
114
+ total_urls="${#positional_urls[@]}" dry_run=true
115
+ elif [ "${mode_stealth}" = "1" ]; then
116
+ ok "dry-run: would stealth-fetch ${positional_urls[0]} via obscura"
117
+ emit_summary verb=extract tool=none why=dry-run status=ok mode=stealth \
118
+ url="${positional_urls[0]}" dry_run=true
119
+ else
120
+ ok "dry-run: would extract ${selector:-${eval_js}}"
121
+ emit_summary verb=extract tool=none why=dry-run status=ok selector="${selector}" dry_run=true
122
+ fi
123
+ exit 0
124
+ fi
125
+
126
+ picked="$(pick_tool extract "${verb_argv[@]}")"
127
+ tool_name="${picked%%$'\t'*}"
128
+ why="${picked#*$'\t'}"
129
+
130
+ source_picked_adapter "${tool_name}"
131
+
132
+ stats_t0="$(now_ms)"
133
+ set +e
134
+ adapter_out="$(invoke_with_retry extract "${verb_argv[@]}")"
135
+ adapter_rc=$?
136
+ set -e
137
+
138
+ # Phase 12 part 1: per-action telemetry. extract covers chrome-devtools-mcp
139
+ # (default) + obscura (--scrape/--stealth). observed=adapter_out so
140
+ # element_value post-conditions can match extracted text/JSON.
141
+ BROWSER_STATS_OBSERVED="${adapter_out}" \
142
+ stats_run_adapter_emit \
143
+ "extract" "${tool_name}" "${stats_t0}" "${adapter_rc}" "${adapter_out}" "" \
144
+ -- "${verb_argv[@]}" || true
145
+
146
+ [ -n "${adapter_out}" ] && printf '%s\n' "${adapter_out}"
147
+
148
+ if [ "${mode_scrape}" = "1" ]; then
149
+ # Aggregate per-URL events into success/failure counts for the summary line.
150
+ total="${#positional_urls[@]}"
151
+ successful=0
152
+ failed=0
153
+ if [ -n "${adapter_out}" ]; then
154
+ successful="$(printf '%s\n' "${adapter_out}" | jq -s 'map(select(.event=="scrape_url" and (.title // false))) | length' 2>/dev/null || printf '0')"
155
+ failed="$( printf '%s\n' "${adapter_out}" | jq -s 'map(select(.event=="scrape_url" and (.error // false))) | length' 2>/dev/null || printf '0')"
156
+ fi
157
+ if [ "${adapter_rc}" -ne 0 ]; then
158
+ overall_status=error
159
+ elif [ "${failed}" = "0" ]; then
160
+ overall_status=ok
161
+ elif [ "${successful}" = "0" ]; then
162
+ overall_status=error
163
+ else
164
+ overall_status=partial
165
+ fi
166
+ emit_summary verb=extract tool="${tool_name}" why="${why}" \
167
+ status="${overall_status}" mode=scrape \
168
+ total_urls="${total}" successful="${successful}" failed="${failed}"
169
+ [ "${overall_status}" = "ok" ] && exit 0
170
+ exit "${adapter_rc}"
171
+ fi
172
+
173
+ if [ "${mode_stealth}" = "1" ]; then
174
+ if [ "${adapter_rc}" -ne 0 ]; then
175
+ overall_status=error
176
+ elif [ -z "${adapter_out}" ]; then
177
+ overall_status=empty
178
+ else
179
+ overall_status=ok
180
+ fi
181
+ emit_summary verb=extract tool="${tool_name}" why="${why}" \
182
+ status="${overall_status}" mode=stealth url="${positional_urls[0]}"
183
+ [ "${overall_status}" = "ok" ] && exit 0
184
+ exit "${adapter_rc}"
185
+ fi
186
+
187
+ if [ "${adapter_rc}" -eq 0 ]; then
188
+ emit_summary verb=extract tool="${tool_name}" why="${why}" status=ok selector="${selector}"
189
+ exit 0
190
+ fi
191
+ emit_summary verb=extract tool="${tool_name}" why="${why}" status=error selector="${selector}"
192
+ exit "${adapter_rc}"