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,182 @@
1
+ # Recipe — visual-rescue hook (Path 3)
2
+
3
+ `browser-do.sh` exposes a hook seam BETWEEN Phase-13 fingerprint-rescue
4
+ failure and the cloud-LLM fall-through. The hook decides whether a cached
5
+ selector is still semantically the right target — typically by looking at
6
+ a screenshot through a local VLM. When the hook says yes, the cache is
7
+ preserved + no cloud-LLM round-trip happens.
8
+
9
+ ## When this fires
10
+
11
+ ```
12
+ cached selector + page fingerprint match → 0 LLM tokens
13
+ ↓ DOM diff detected
14
+ Phase-13 silent fingerprint rescue → 0 LLM tokens
15
+ ↓ rescue failed (no good selector candidate)
16
+ [HOOK FIRES HERE] ← Path 3
17
+ ↓ hook returns "yes" → keep cache, 0 cloud tokens
18
+ ↓ hook returns "no" / unreachable → fall through
19
+ cloud LLM ref-resolution → 1 LLM round-trip
20
+ ```
21
+
22
+ ## Enabling the hook
23
+
24
+ Two env vars must be set when invoking `browser-do --intent ...`:
25
+
26
+ ```bash
27
+ export BROWSER_SKILL_VISION_FALLBACK=1
28
+ export BROWSER_SKILL_VISUAL_RESCUE_CMD=/abs/path/to/your-hook.sh
29
+ chmod +x "${BROWSER_SKILL_VISUAL_RESCUE_CMD}"
30
+ ```
31
+
32
+ If either is unset (or the hook isn't executable), the tier is skipped
33
+ silently and behaviour matches today's baseline.
34
+
35
+ ### Fast start — use the bundled default probe
36
+
37
+ A ready-to-use canonical probe ships at `scripts/lib/visual-rescue-default.sh`
38
+ (text-mode v1 — see "Modes" below). Wire it directly:
39
+
40
+ ```bash
41
+ export BROWSER_SKILL_VISION_FALLBACK=1
42
+ export BROWSER_SKILL_VISUAL_RESCUE_CMD="$(realpath scripts/lib/visual-rescue-default.sh)"
43
+ # Optional — defaults to http://127.0.0.1:8080 (matches `bash scripts/browser-vlm.sh start`)
44
+ # export BROWSER_SKILL_VLM_PORT=8080
45
+ ```
46
+
47
+ That gets Path 3 live with zero custom code. Confirm wiring via doctor:
48
+
49
+ ```bash
50
+ bash scripts/browser-doctor.sh | grep "local VLM"
51
+ # ok: local VLM reachable @ http://127.0.0.1:8080 …
52
+ ```
53
+
54
+ ## Modes
55
+
56
+ **v1 — text-mode (the bundled default).** Reads the accessibility-tree YAML
57
+ snapshot, sends `(intent, selector, snapshot-text)` to the local LLM, asks
58
+ yes/no. Cheap (~200ms total), works against any OpenAI-compatible endpoint
59
+ (local llama-server, vLLM, ollama, etc — not just VLMs).
60
+
61
+ **v2 — vision-mode (planned).** Crops a screenshot of the cached element's
62
+ bbox, sends image to a VLM, asks yes/no. Stronger signal for visual-only UIs
63
+ (canvas elements, custom-painted widgets), but needs screenshot-from-live-
64
+ session infrastructure that isn't shipped yet. Roll your own per the example
65
+ below until v2 lands.
66
+
67
+ ## Hook contract
68
+
69
+ The hook is invoked with **three positional args**:
70
+
71
+ ```bash
72
+ your-hook.sh SITE INTENT CACHED_SELECTOR
73
+ ```
74
+
75
+ - `SITE` — registered site name (e.g. `prod-app`)
76
+ - `INTENT` — natural-language intent from the original `browser-do --intent` call
77
+ - `CACHED_SELECTOR` — the CSS selector that was retrieved from `~/.browser-skill/memory/<site>/archetypes/<id>.json` (the one Phase-13 rescue couldn't salvage)
78
+
79
+ The hook must write **exactly `yes` or `no` to stdout** (single line, no
80
+ JSON envelope). Any other output is treated as "no". Exit code:
81
+
82
+ - `0` + stdout `yes` → cache preserved; verb dispatch reports success
83
+ - `0` + stdout `no` → fall through to cloud LLM
84
+ - non-zero exit → fall through (treated as "unreachable")
85
+
86
+ Stderr is ignored by `browser-do`; use it for hook-internal logging.
87
+
88
+ ## Reference implementation — llama.cpp + Qwen3-VL-4B
89
+
90
+ A minimum hook that screenshots the current page (via `browser-snapshot.sh`
91
+ + `browser-inspect.sh --screenshot`) and asks a local Qwen3-VL through
92
+ `llama-server` is shown below. This is a SAMPLE — write your own to taste.
93
+
94
+ ```bash
95
+ #!/usr/bin/env bash
96
+ # ~/.browser-skill/hooks/visual-rescue-llama.sh
97
+ set -euo pipefail
98
+ site="$1"; intent="$2"; selector="$3"
99
+
100
+ vlm_host="${BROWSER_SKILL_VLM_HOST:-127.0.0.1}"
101
+ vlm_port="${BROWSER_SKILL_VLM_PORT:-8080}"
102
+ endpoint="http://${vlm_host}:${vlm_port}/v1/chat/completions"
103
+
104
+ # Quick reachability gate — silent skip if VLM not running.
105
+ if ! curl -sfm 2 "http://${vlm_host}:${vlm_port}/health" >/dev/null; then
106
+ printf 'no\n'
107
+ exit 1
108
+ fi
109
+
110
+ # Take a transient screenshot via the inspect verb (Phase 7 captures dir).
111
+ # Find the most-recent screenshot file under captures/.
112
+ SCRIPTS_DIR="${BROWSER_SKILL_SCRIPTS_DIR:-${HOME}/.claude/skills/browser-automation-skill/scripts}"
113
+ bash "${SCRIPTS_DIR}/browser-inspect.sh" --site "${site}" --screenshot --capture \
114
+ >/dev/null 2>&1 || { printf 'no\n'; exit 1; }
115
+ captures_dir="${BROWSER_SKILL_HOME:-${HOME}/.browser-skill}/captures"
116
+ latest_id="$(jq -r '.latest' "${captures_dir}/_index.json" 2>/dev/null)"
117
+ png_path="${captures_dir}/${latest_id}/inspect-screenshot.png"
118
+ [ -f "${png_path}" ] || { printf 'no\n'; exit 1; }
119
+
120
+ # Ask the VLM: yes/no probe.
121
+ b64="$(base64 -i "${png_path}" | tr -d '\n')"
122
+ prompt="The user wants to: '${intent}'. The cached element was at CSS selector '${selector}'. Looking at this page, is there still a target element that matches the intent? Answer with ONLY 'yes' or 'no'."
123
+
124
+ resp="$(curl -sS -m 30 "${endpoint}" -H 'Content-Type: application/json' \
125
+ -d "$(jq -n --arg img "data:image/png;base64,${b64}" --arg p "${prompt}" '
126
+ {model:"q",max_tokens:5,messages:[{role:"user",content:[
127
+ {type:"text",text:$p},
128
+ {type:"image_url",image_url:{url:$img}}
129
+ ]}]}')" 2>/dev/null)"
130
+
131
+ completion="$(printf '%s' "${resp}" | jq -r '.choices[0].message.content // ""' 2>/dev/null)"
132
+ case "${completion,,}" in
133
+ *yes*) printf 'yes\n' ;;
134
+ *) printf 'no\n' ;;
135
+ esac
136
+ ```
137
+
138
+ Smoke test the hook in isolation:
139
+
140
+ ```bash
141
+ echo "site=prod-app intent='click submit' selector='button.submit'"
142
+ ~/.browser-skill/hooks/visual-rescue-llama.sh prod-app 'click submit' 'button.submit'
143
+ # → yes / no
144
+ ```
145
+
146
+ ## Telemetry
147
+
148
+ When the hook reports "yes" and `browser-do` accepts the rescue:
149
+
150
+ - A `_kind:"visual_rescue"` event line appears on stdout (machine-readable)
151
+ - A separate event lands in `~/.browser-skill/memory/stats.jsonl` with
152
+ `gen_ai_tool_name:"browser-do.visual_rescue"` and `rescued:true`. Run
153
+ `browser-stats report --route browser-do` to see your visual-rescue rate.
154
+
155
+ When the hook reports "no" or fails, NO visual_rescue event is emitted
156
+ (the original Phase-13 cache_miss + fail_count path runs unchanged).
157
+
158
+ ## Cost frame
159
+
160
+ Rough numbers from the Phase-14 bench session (M3 Pro + Qwen3-VL-4B-q4_K_M):
161
+
162
+ | Step | Latency | Cloud tokens |
163
+ |---|---:|---:|
164
+ | Screenshot via inspect | ~1.5 s | 0 |
165
+ | base64 + curl + VLM yes/no | ~0.4 s | 0 |
166
+ | **Path 3 total** | **~2 s** | **0** |
167
+ | Cloud LLM ref-resolution (alternative) | ~1 s | full prompt + response |
168
+
169
+ Path 3 wins when avoided cloud-LLM cost exceeds local latency cost. For
170
+ high-volume cache flows (your registered prod sites with repeat actions),
171
+ this is "always" — the 2 s pays off after one avoided LLM round-trip.
172
+
173
+ ## Why a hook, not a built-in
174
+
175
+ The skill is intentionally agnostic about HOW the visual probe reasons —
176
+ some users will want screenshot-crop + Qwen3-VL, others UI-TARS-7B,
177
+ others a yes/no LLM-as-judge against a text snapshot. Hardcoding one
178
+ approach would close the design space. The hook contract lets each user
179
+ ship their preferred probe without forking the skill.
180
+
181
+ A built-in default probe is planned for a future release; this recipe
182
+ will become the canonical reference once it lands.
@@ -0,0 +1,42 @@
1
+ {
2
+ "_doc": "Anthropic model prices in USD per token. Sourced from docs.anthropic.com/pricing as of 2026-05-15. Used by `browser-stats report` to convert token counts into $ at query time (re-priceable). Override path via env: BROWSER_STATS_PRICES_FILE.",
3
+ "_last_updated": "2026-05-15",
4
+ "models": {
5
+ "claude-opus-4-7": {
6
+ "input": 0.000005,
7
+ "output": 0.000025,
8
+ "cache_create_5m": 0.00000625,
9
+ "cache_create_1h": 0.00001000,
10
+ "cache_create": 0.00000625,
11
+ "cache_read": 0.00000050
12
+ },
13
+ "claude-sonnet-4-6": {
14
+ "input": 0.000003,
15
+ "output": 0.000015,
16
+ "cache_create_5m": 0.00000375,
17
+ "cache_create_1h": 0.00000600,
18
+ "cache_create": 0.00000375,
19
+ "cache_read": 0.00000030
20
+ },
21
+ "claude-haiku-4-5": {
22
+ "input": 0.000001,
23
+ "output": 0.000005,
24
+ "cache_create_5m": 0.00000125,
25
+ "cache_create_1h": 0.00000200,
26
+ "cache_create": 0.00000125,
27
+ "cache_read": 0.00000010
28
+ }
29
+ },
30
+ "modifiers": {
31
+ "_doc": "Multipliers applied at report time. `inference_geo=us` adds 1.1x; fast_mode is 6x and stacks; batch is 0.5x (mutually exclusive with fast_mode).",
32
+ "inference_geo_us": 1.1,
33
+ "fast_mode": 6.0,
34
+ "batch": 0.5
35
+ },
36
+ "server_tools": {
37
+ "_doc": "Server-side tool use billed per request, not tokens.",
38
+ "web_search_request": 0.01,
39
+ "web_fetch_request": 0.0,
40
+ "code_execution_session_hour": 0.08
41
+ }
42
+ }
@@ -0,0 +1,77 @@
1
+ {
2
+ "$schema": "https://json-schema.org/draft/2020-12/schema",
3
+ "$id": "https://github.com/xicv/browser-automation-skill/references/stats-schema.json",
4
+ "title": "browser-automation-skill stats event",
5
+ "description": "One JSONL line per adapter invocation written to ${BROWSER_SKILL_HOME}/memory/stats.jsonl. Field naming follows OpenInference + OTel GenAI v1.40 conventions (snake_case dot-name flattening) for forward-compat with OTLP exporters and Langfuse/Phoenix.",
6
+ "type": "object",
7
+ "required": [
8
+ "schema_version",
9
+ "ts",
10
+ "span_id",
11
+ "trace_id",
12
+ "verb",
13
+ "adapter_route",
14
+ "outcome",
15
+ "rc",
16
+ "duration_ms",
17
+ "argv_bytes",
18
+ "stdout_bytes",
19
+ "stderr_bytes"
20
+ ],
21
+ "properties": {
22
+ "schema_version": {"type": "integer", "const": 1},
23
+ "ts": {"type": "string", "format": "date-time", "description": "ISO 8601 with ms precision (UTC)."},
24
+ "span_id": {"type": "string", "pattern": "^[0-9a-f]{16}$"},
25
+ "trace_id": {"type": "string", "pattern": "^[0-9a-f]{16}$"},
26
+ "parent_span_id": {"type": ["string", "null"]},
27
+ "session_id": {"type": ["string", "null"], "description": "$CLAUDE_SESSION_ID if injected."},
28
+
29
+ "gen_ai_operation_name": {"type": "string", "const": "execute_tool"},
30
+ "gen_ai_tool_name": {"type": "string", "description": "e.g. 'chrome-devtools-mcp.click'. OTel-compliant."},
31
+ "gen_ai_tool_type": {"type": "string", "enum": ["function", "extension", "datastore"]},
32
+
33
+ "verb": {"type": "string", "description": "browser-* verb (open, click, fill, extract, ...)."},
34
+ "adapter_route": {"type": "string", "enum": ["chrome-devtools-mcp", "playwright-cli", "playwright-lib", "obscura"]},
35
+ "site": {"type": ["string", "null"]},
36
+
37
+ "selector_kind": {"type": "string", "enum": ["a11y_ref", "css", "xpath", "role", "text", "none"]},
38
+ "selector_value": {"type": ["string", "null"]},
39
+
40
+ "duration_ms": {"type": "integer", "minimum": 0},
41
+ "argv_bytes": {"type": "integer", "minimum": 0, "description": "Total argv bytes — input-token proxy when SDK fields absent."},
42
+ "stdout_bytes": {"type": "integer", "minimum": 0, "description": "Captured stdout bytes — output-token proxy."},
43
+ "stderr_bytes": {"type": "integer", "minimum": 0},
44
+ "rc": {"type": "integer", "description": "Adapter exit code (lib/common.sh EXIT_* table)."},
45
+
46
+ "outcome": {"type": "string", "enum": ["success", "fail", "partial", "skipped"]},
47
+ "failure_mode": {
48
+ "type": ["string", "null"],
49
+ "enum": [null,
50
+ "element_not_found", "element_ambiguous", "wrong_element_acted",
51
+ "stale_ref", "action_timeout", "navigation_mismatch",
52
+ "js_not_ready", "network_error", "captcha_blocked",
53
+ "auth_required", "popup_intercept", "extraction_mismatch",
54
+ "oblivious_success", "unknown_failure"
55
+ ],
56
+ "description": "Phase 14 (Bundle #3) added unknown_failure as the catch-all so rc!=0 events with no classifier match surface in stats tune histograms instead of being dropped as null."
57
+ },
58
+
59
+ "model": {"type": ["string", "null"], "description": "From $CLAUDE_MODEL env. Null when invoked outside Claude Code."},
60
+ "service_tier": {"type": ["string", "null"], "enum": [null, "standard", "priority", "batch"]},
61
+
62
+ "gen_ai_usage_input_tokens": {"type": ["integer", "null"], "minimum": 0},
63
+ "gen_ai_usage_output_tokens": {"type": ["integer", "null"], "minimum": 0},
64
+ "gen_ai_usage_cache_read_input_tokens": {"type": ["integer", "null"], "minimum": 0},
65
+ "gen_ai_usage_cache_creation_input_tokens":{"type": ["integer", "null"], "minimum": 0},
66
+
67
+ "post_condition_target_type": {"type": ["string", "null"], "enum": [null, "url", "element_path", "element_value"]},
68
+ "post_condition_matcher": {"type": ["string", "null"], "enum": [null, "exact", "include", "semantic"]},
69
+ "post_condition_expected": {"type": ["string", "null"]},
70
+ "post_condition_observed": {"type": ["string", "null"]},
71
+ "post_condition_hit": {"type": ["boolean", "null"]},
72
+
73
+ "rescued": {"type": ["boolean", "null"], "description": "Phase 13 fingerprint rescue outcome. true=cache silently healed (selector overwritten, fail_count reset); false=rescue attempted but failed; null=rescue not attempted (default for adapter-direct events)."},
74
+ "fingerprint_from_selector": {"type": ["string", "null"], "description": "Phase 13: the cached selector that went stale. Null when rescued != true."},
75
+ "fingerprint_to_selector": {"type": ["string", "null"], "description": "Phase 13: the rescued selector that replaced the stale one. Null when rescued != true."}
76
+ }
77
+ }
@@ -0,0 +1,8 @@
1
+ # Tool versions (autogenerated — do not edit; run scripts/regenerate-docs.sh)
2
+
3
+ | Tool | Version pin | Install hint | Cheatsheet |
4
+ |---|---|---|---|
5
+ | chrome-devtools-mcp | 0.x | npm i -g chrome-devtools-mcp (or run via 'npx chrome-devtools-mcp@latest' over stdio MCP) | [references/chrome-devtools-mcp-cheatsheet.md](../references/chrome-devtools-mcp-cheatsheet.md) |
6
+ | obscura | 0.x | download release from https://github.com/h4ckf0r0day/obscura/releases (no Chrome/Node required); keep obscura + obscura-worker side-by-side | [references/obscura-cheatsheet.md](../references/obscura-cheatsheet.md) |
7
+ | playwright-cli | 1.49.x | npm i -g playwright @playwright/test @playwright/cli && playwright install chromium | [references/playwright-cli-cheatsheet.md](../references/playwright-cli-cheatsheet.md) |
8
+ | playwright-lib | 1.59.x | npm i -g playwright @playwright/test && playwright install chromium | [references/playwright-lib-cheatsheet.md](../references/playwright-lib-cheatsheet.md) |
@@ -0,0 +1,113 @@
1
+ #!/usr/bin/env bash
2
+ # add-site — register a site profile under sites/<name>.json.
3
+ set -euo pipefail
4
+ IFS=$'\n\t'
5
+ umask 077
6
+
7
+ SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
8
+ # shellcheck source=lib/common.sh
9
+ # shellcheck disable=SC1091
10
+ source "${SCRIPT_DIR}/lib/common.sh"
11
+ # shellcheck source=lib/site.sh
12
+ # shellcheck disable=SC1091
13
+ source "${SCRIPT_DIR}/lib/site.sh"
14
+ init_paths
15
+
16
+ name=""; url=""; viewport="1280x800"; user_agent=""; stealth="false"
17
+ default_session=""; default_tool=""; label=""
18
+ force=0; dry_run=0
19
+
20
+ usage() {
21
+ cat <<'USAGE'
22
+ Usage: add-site --name NAME --url URL [options]
23
+
24
+ --name NAME site name (required, used as filename)
25
+ --url URL site URL (must start with http:// or https://)
26
+ --viewport WxH viewport (default 1280x800)
27
+ --user-agent UA override user agent
28
+ --stealth set stealth flag (default false)
29
+ --default-session NAME default session for verbs that omit --session
30
+ --default-tool NAME default tool for verbs that omit --tool
31
+ --label TEXT human-readable description
32
+ --force overwrite an existing site
33
+ --dry-run print planned action; write nothing
34
+ -h, --help this message
35
+ USAGE
36
+ }
37
+
38
+ while [ $# -gt 0 ]; do
39
+ case "$1" in
40
+ --name) name="$2"; shift 2 ;;
41
+ --url) url="$2"; shift 2 ;;
42
+ --viewport) viewport="$2"; shift 2 ;;
43
+ --user-agent) user_agent="$2"; shift 2 ;;
44
+ --stealth) stealth="true"; shift ;;
45
+ --default-session) default_session="$2"; shift 2 ;;
46
+ --default-tool) default_tool="$2"; shift 2 ;;
47
+ --label) label="$2"; shift 2 ;;
48
+ --force) force=1; shift ;;
49
+ --dry-run) dry_run=1; shift ;;
50
+ -h|--help) usage; exit 0 ;;
51
+ *) die "${EXIT_USAGE_ERROR}" "unknown flag: $1" ;;
52
+ esac
53
+ done
54
+
55
+ [ -n "${name}" ] || { usage; die "${EXIT_USAGE_ERROR}" "--name is required"; }
56
+ [ -n "${url}" ] || { usage; die "${EXIT_USAGE_ERROR}" "--url is required"; }
57
+ case "${url}" in
58
+ http://*|https://*) ;;
59
+ *) die "${EXIT_USAGE_ERROR}" "url must start with http:// or https:// (got: ${url})" ;;
60
+ esac
61
+ [[ "${viewport}" =~ ^[0-9]+x[0-9]+$ ]] \
62
+ || die "${EXIT_USAGE_ERROR}" "viewport must be WIDTHxHEIGHT (got: ${viewport})"
63
+ # Defense in depth: validate name fields the script is about to embed in paths.
64
+ assert_safe_name "${name}" "site-name"
65
+ [ -z "${default_session}" ] || assert_safe_name "${default_session}" "default-session"
66
+ vw="${viewport%x*}"; vh="${viewport#*x}"
67
+
68
+ started_at_ms="$(now_ms)"
69
+
70
+ if site_exists "${name}" && [ "${force}" -ne 1 ]; then
71
+ die "${EXIT_USAGE_ERROR}" "site already exists: ${name} (use --force to overwrite)"
72
+ fi
73
+
74
+ profile_json="$(jq -nc \
75
+ --arg n "${name}" \
76
+ --arg u "${url}" \
77
+ --argjson vw "${vw}" --argjson vh "${vh}" \
78
+ --arg ua "${user_agent}" \
79
+ --argjson stealth "${stealth}" \
80
+ --arg ds "${default_session}" \
81
+ --arg dt "${default_tool}" \
82
+ --arg lbl "${label}" \
83
+ '{
84
+ name: $n, url: $u,
85
+ viewport: {width: $vw, height: $vh},
86
+ user_agent: (if $ua == "" then null else $ua end),
87
+ stealth: $stealth,
88
+ default_session: (if $ds == "" then null else $ds end),
89
+ default_tool: (if $dt == "" then null else $dt end),
90
+ label: $lbl,
91
+ schema_version: 1
92
+ }')"
93
+
94
+ now_ts="$(now_iso)"
95
+ meta_json="$(jq -nc \
96
+ --arg n "${name}" \
97
+ --arg now "${now_ts}" \
98
+ '{name: $n, created_at: $now, last_used_at: $now}')"
99
+
100
+ if [ "${dry_run}" -eq 1 ]; then
101
+ ok "dry-run: would write ${SITES_DIR}/${name}.json"
102
+ duration_ms=$(( $(now_ms) - started_at_ms ))
103
+ summary_json verb=add-site tool=none why=dry-run status=ok would_run=true \
104
+ site="${name}" duration_ms="${duration_ms}"
105
+ exit "${EXIT_OK}"
106
+ fi
107
+
108
+ site_save "${name}" "${profile_json}" "${meta_json}"
109
+ ok "site added: ${name}"
110
+
111
+ duration_ms=$(( $(now_ms) - started_at_ms ))
112
+ summary_json verb=add-site tool=none why=write-profile status=ok \
113
+ site="${name}" duration_ms="${duration_ms}"
@@ -0,0 +1,106 @@
1
+ #!/usr/bin/env bash
2
+ # scripts/browser-assert.sh — verify-style assertion verb (Phase 9 part 1-ii).
3
+ #
4
+ # Usage:
5
+ # bash scripts/browser-assert.sh --selector CSS --text-contains TEXT \
6
+ # [--site NAME] [--tool NAME] [--dry-run]
7
+ #
8
+ # Thin wrapper: shells to `bash scripts/browser-extract.sh --selector CSS`
9
+ # (subprocess; routes through router + chrome-devtools-mcp by default);
10
+ # parses the extracted text; bash-side compares against --text-contains
11
+ # predicate. NO new tool_assert function on adapters — composition over ABI
12
+ # extension (per design doc §6 + plan-doc 2026-05-10-phase-09-part-1-ii §A1).
13
+ #
14
+ # Exit codes:
15
+ # 0 — assertion passed
16
+ # 13 — EXIT_ASSERTION_FAILED — predicate did not match
17
+ # 2 — EXIT_USAGE_ERROR (missing required flag)
18
+ # 1 — EXIT_GENERIC_ERROR (extract subprocess failed)
19
+
20
+ set -euo pipefail
21
+ IFS=$'\n\t'
22
+
23
+ SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
24
+ # shellcheck source=lib/common.sh
25
+ # shellcheck disable=SC1091
26
+ source "${SCRIPT_DIR}/lib/common.sh"
27
+ # shellcheck source=lib/output.sh
28
+ # shellcheck disable=SC1091
29
+ source "${SCRIPT_DIR}/lib/output.sh"
30
+ # shellcheck source=lib/verb_helpers.sh
31
+ # shellcheck disable=SC1091
32
+ source "${SCRIPT_DIR}/lib/verb_helpers.sh"
33
+
34
+ init_paths
35
+
36
+ SUMMARY_T0="$(now_ms)"; export SUMMARY_T0
37
+
38
+ parse_verb_globals "$@"
39
+
40
+ selector=""
41
+ text_contains=""
42
+ i=0
43
+ while [ "${i}" -lt "${#REMAINING_ARGV[@]}" ]; do
44
+ case "${REMAINING_ARGV[i]}" in
45
+ --selector)
46
+ selector="${REMAINING_ARGV[i+1]:-}"
47
+ [ -n "${selector}" ] || die "${EXIT_USAGE_ERROR}" "--selector requires a value"
48
+ i=$((i + 2))
49
+ ;;
50
+ --text-contains)
51
+ text_contains="${REMAINING_ARGV[i+1]:-}"
52
+ [ -n "${text_contains}" ] || die "${EXIT_USAGE_ERROR}" "--text-contains requires a value"
53
+ i=$((i + 2))
54
+ ;;
55
+ *)
56
+ i=$((i + 1))
57
+ ;;
58
+ esac
59
+ done
60
+
61
+ [ -n "${selector}" ] || die "${EXIT_USAGE_ERROR}" "assert requires --selector CSS"
62
+ [ -n "${text_contains}" ] || die "${EXIT_USAGE_ERROR}" "assert requires --text-contains TEXT"
63
+
64
+ if [ "${ARG_DRY_RUN:-0}" = "1" ]; then
65
+ ok "dry-run: would assert selector=${selector} text_contains=${text_contains}"
66
+ emit_summary verb=assert tool=none why=dry-run status=ok \
67
+ selector="${selector}" text_contains="${text_contains}" dry_run=true
68
+ exit 0
69
+ fi
70
+
71
+ # Compose: shell to browser-extract.sh to get the selector's text. The extract
72
+ # verb's stdout is one event line + one summary line. We parse the event line.
73
+ set +e
74
+ extract_out="$(bash "${SCRIPT_DIR}/browser-extract.sh" --selector "${selector}" 2>&1)"
75
+ extract_rc=$?
76
+ set -e
77
+
78
+ if [ "${extract_rc}" -ne 0 ]; then
79
+ emit_summary verb=assert tool=extract why=composition status=error \
80
+ selector="${selector}" text_contains="${text_contains}" \
81
+ error="extract subprocess failed (rc=${extract_rc})"
82
+ exit "${EXIT_GENERIC_ERROR}"
83
+ fi
84
+
85
+ # Find the extract event line; collect all matched text. The shipped extract
86
+ # event shape is {"event":"extract","selector":"...","matches":["Welcome","Hello"]}
87
+ # (matches[] is array of strings — see tests/fixtures/chrome-devtools-mcp/
88
+ # 05efe417...json). Fall through to .text for the playwright-cli shape.
89
+ got_text="$(
90
+ printf '%s\n' "${extract_out}" \
91
+ | jq -r -s '
92
+ map(select(.event == "extract")) | .[0] |
93
+ (.text // (.matches // []) | if type == "array" then join("\n") else . end)' 2>/dev/null \
94
+ || printf ''
95
+ )"
96
+
97
+ if printf '%s' "${got_text}" | grep -qF -- "${text_contains}"; then
98
+ emit_summary verb=assert tool=extract why=composition status=ok \
99
+ selector="${selector}" text_contains="${text_contains}"
100
+ exit 0
101
+ fi
102
+
103
+ emit_summary verb=assert tool=extract why=composition status=error \
104
+ selector="${selector}" text_contains="${text_contains}" \
105
+ expected="${text_contains}" got="${got_text}"
106
+ exit "${EXIT_ASSERTION_FAILED}"
@@ -0,0 +1,68 @@
1
+ #!/usr/bin/env bash
2
+ # scripts/browser-audit.sh — run a Lighthouse / perf-trace audit.
3
+ # Usage: bash scripts/browser-audit.sh [--site NAME] [--tool NAME] [--dry-run]
4
+ # [--raw] [--lighthouse] [--perf-trace]
5
+ #
6
+ # Routes to chrome-devtools-mcp by default (post-1d router promotion — only
7
+ # adapter with `lighthouse_audit` and `performance_*` MCP tools per parent
8
+ # spec Appendix B). `--lighthouse` is the implicit default; `--perf-trace`
9
+ # can coexist.
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
+ verb_argv=("${REMAINING_ARGV[@]}")
37
+
38
+ # Default to --lighthouse when neither flag is provided. Adapter still sees
39
+ # the flag in argv so router rules + capability filter can react.
40
+ if ! _has_flag --lighthouse "${verb_argv[@]}" && ! _has_flag --perf-trace "${verb_argv[@]}"; then
41
+ verb_argv+=(--lighthouse)
42
+ fi
43
+
44
+ if [ "${ARG_DRY_RUN:-0}" = "1" ]; then
45
+ ok "dry-run: would run audit"
46
+ emit_summary verb=audit tool=none why=dry-run status=ok dry_run=true
47
+ exit 0
48
+ fi
49
+
50
+ picked="$(pick_tool audit "${verb_argv[@]}")"
51
+ tool_name="${picked%%$'\t'*}"
52
+ why="${picked#*$'\t'}"
53
+
54
+ source_picked_adapter "${tool_name}"
55
+
56
+ set +e
57
+ adapter_out="$(invoke_with_retry audit "${verb_argv[@]}")"
58
+ adapter_rc=$?
59
+ set -e
60
+
61
+ [ -n "${adapter_out}" ] && printf '%s\n' "${adapter_out}"
62
+
63
+ if [ "${adapter_rc}" -eq 0 ]; then
64
+ emit_summary verb=audit tool="${tool_name}" why="${why}" status=ok
65
+ exit 0
66
+ fi
67
+ emit_summary verb=audit tool="${tool_name}" why="${why}" status=error
68
+ exit "${adapter_rc}"