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,349 @@
1
+ # scripts/lib/tool/chrome-devtools-mcp.sh — Chrome DevTools MCP tool adapter.
2
+ #
3
+ # Implements the Tool Adapter Extension Model contract from
4
+ # docs/superpowers/specs/2026-04-30-tool-adapter-extension-model-design.md §2.
5
+ #
6
+ # Identity: tool_metadata, tool_capabilities, tool_doctor_check
7
+ # Verb dispatch: tool_open, tool_click, tool_fill, tool_snapshot,
8
+ # tool_inspect, tool_audit, tool_extract, tool_eval
9
+ #
10
+ # Path A introduction: this adapter is reachable only via
11
+ # `--tool=chrome-devtools-mcp`. Router promotion (Path B) for verbs like
12
+ # `inspect`, `audit`, and capture-flag variants of primitives (per parent spec
13
+ # Appendix B) is deferred to phase-05 part 1d.
14
+ #
15
+ # Architecture (phase-05 parts 1b / 1c / 1c-ii):
16
+ # Verb-dispatch shells to a node ESM bridge at
17
+ # scripts/lib/node/chrome-devtools-bridge.mjs which mirrors playwright-lib's
18
+ # playwright-driver.mjs:
19
+ # - Stub mode (BROWSER_SKILL_LIB_STUB=1): bridge looks up sha256(argv) in
20
+ # tests/fixtures/chrome-devtools-mcp/<sha>.json and echoes the contents.
21
+ # - Real mode (one-shot): bridge spawns ${CHROME_DEVTOOLS_MCP_BIN} per call,
22
+ # does the MCP initialize handshake, dispatches one tools/call, exits.
23
+ # Used for stateless verbs (open / snapshot / eval / audit) when no daemon.
24
+ # - Real mode (daemon, part 1c-ii): bridge daemon-start spawns a long-lived
25
+ # MCP child, holds the eN↔uid ref map, exposes verb dispatch over TCP
26
+ # loopback IPC. Stateful verbs (click / fill) require a running daemon and
27
+ # route through it. State at ${BROWSER_SKILL_HOME}/cdt-mcp-daemon.json.
28
+ #
29
+ # CHROME_DEVTOOLS_MCP_BIN env var semantics: in part 1 this was "the binary
30
+ # the adapter shells to (real or stub)"; in part 1b it shifts to "the upstream
31
+ # MCP server binary the bridge spawns in real mode". In stub mode it is
32
+ # unused. The shift is documented in CHANGELOG and the cheatsheet.
33
+ #
34
+ # Adapters are LEAVES — never source another adapter (AP-2). Shared logic
35
+ # factors into scripts/lib/<concern>.sh (sibling to lib/tool/).
36
+
37
+ [ -n "${_BROWSER_TOOL_CHROME_DEVTOOLS_MCP_LOADED:-}" ] && return 0
38
+ readonly _BROWSER_TOOL_CHROME_DEVTOOLS_MCP_LOADED=1
39
+
40
+ # Required by spec 2026-05-01-token-efficient-adapter-output-design §8: every
41
+ # adapter sources output.sh so verb-dispatch emits JSON via emit_summary /
42
+ # emit_event rather than hand-rolled printf. Lint tier 3 enforces this.
43
+ # shellcheck source=../output.sh
44
+ # shellcheck disable=SC1091
45
+ source "$(dirname "${BASH_SOURCE[0]}")/../output.sh"
46
+
47
+ readonly _BROWSER_TOOL_CHROME_DEVTOOLS_MCP_NODE_BIN="${BROWSER_SKILL_NODE_BIN:-node}"
48
+ readonly _BROWSER_TOOL_CHROME_DEVTOOLS_MCP_BRIDGE="$(dirname "${BASH_SOURCE[0]}")/../node/chrome-devtools-bridge.mjs"
49
+ readonly _BROWSER_TOOL_CHROME_DEVTOOLS_MCP_MCP_SERVER_BIN="${CHROME_DEVTOOLS_MCP_BIN:-chrome-devtools-mcp}"
50
+
51
+ # --- Identity functions ---
52
+
53
+ tool_metadata() {
54
+ cat <<'EOF'
55
+ {
56
+ "name": "chrome-devtools-mcp",
57
+ "abi_version": 1,
58
+ "version_pin": "0.x",
59
+ "cheatsheet_path": "references/chrome-devtools-mcp-cheatsheet.md",
60
+ "install_hint": "npm i -g chrome-devtools-mcp (or run via 'npx chrome-devtools-mcp@latest' over stdio MCP)"
61
+ }
62
+ EOF
63
+ }
64
+
65
+ tool_capabilities() {
66
+ cat <<'EOF'
67
+ {
68
+ "verbs": {
69
+ "open": { "flags": ["--headed", "--url"] },
70
+ "click": { "flags": ["--ref"] },
71
+ "fill": { "flags": ["--ref", "--text", "--secret-stdin"] },
72
+ "snapshot": { "flags": ["--depth"] },
73
+ "press": { "flags": ["--key"] },
74
+ "select": { "flags": ["--ref", "--value", "--label", "--index"] },
75
+ "hover": { "flags": ["--ref"] },
76
+ "wait": { "flags": ["--selector", "--state", "--timeout"] },
77
+ "drag": { "flags": ["--src-ref", "--dst-ref"] },
78
+ "upload": { "flags": ["--ref", "--path"] },
79
+ "route": { "flags": ["--pattern", "--action"] },
80
+ "tab-list": { "flags": [] },
81
+ "tab-switch": { "flags": ["--by-index", "--by-url-pattern"] },
82
+ "tab-close": { "flags": ["--tab-id", "--by-url-pattern"] },
83
+ "inspect": { "flags": ["--capture-console", "--capture-network", "--screenshot"] },
84
+ "audit": { "flags": ["--lighthouse", "--perf-trace"] },
85
+ "extract": { "flags": ["--selector", "--eval"] },
86
+ "eval": { "flags": ["--expression"] }
87
+ }
88
+ }
89
+ EOF
90
+ }
91
+
92
+ tool_doctor_check() {
93
+ if ! command -v "${_BROWSER_TOOL_CHROME_DEVTOOLS_MCP_NODE_BIN}" >/dev/null 2>&1; then
94
+ cat <<EOF
95
+ { "ok": false, "binary": "${_BROWSER_TOOL_CHROME_DEVTOOLS_MCP_NODE_BIN}", "error": "node not on PATH",
96
+ "install_hint": "brew install node (>=20)" }
97
+ EOF
98
+ return 0
99
+ fi
100
+ if [ ! -f "${_BROWSER_TOOL_CHROME_DEVTOOLS_MCP_BRIDGE}" ]; then
101
+ printf '{"ok":false,"binary":"%s","error":"bridge missing","bridge_path":"%s"}\n' \
102
+ "${_BROWSER_TOOL_CHROME_DEVTOOLS_MCP_NODE_BIN}" "${_BROWSER_TOOL_CHROME_DEVTOOLS_MCP_BRIDGE}"
103
+ return 0
104
+ fi
105
+ local node_version
106
+ node_version="$("${_BROWSER_TOOL_CHROME_DEVTOOLS_MCP_NODE_BIN}" --version 2>/dev/null || printf 'unknown')"
107
+ printf '{"ok":true,"binary":"%s","node_version":"%s","mcp_server_bin":"%s","note":"real-mode MCP transport: 8/8 verbs (open/snapshot/eval/audit/inspect/extract one-shot or daemon; click/fill require daemon)"}\n' \
108
+ "${_BROWSER_TOOL_CHROME_DEVTOOLS_MCP_NODE_BIN}" "${node_version}" "${_BROWSER_TOOL_CHROME_DEVTOOLS_MCP_MCP_SERVER_BIN}"
109
+ }
110
+
111
+ # --- Verb-dispatch functions ---
112
+ # Argv translation: skill flags → bridge's `<verb> [args...]` surface. Bridge
113
+ # in stub mode hashes that surface (sha256 of args joined+terminated by NUL)
114
+ # and looks up the fixture. Bridge in real mode (part 1c) translates to
115
+ # MCP `tools/call` requests against the upstream chrome-devtools-mcp server.
116
+
117
+ _drive() {
118
+ "${_BROWSER_TOOL_CHROME_DEVTOOLS_MCP_NODE_BIN}" "${_BROWSER_TOOL_CHROME_DEVTOOLS_MCP_BRIDGE}" "$@"
119
+ }
120
+
121
+ tool_open() {
122
+ local url=""
123
+ local rest=()
124
+ while [ "$#" -gt 0 ]; do
125
+ case "$1" in
126
+ --url) url="$2"; shift 2 ;;
127
+ *) rest+=("$1"); shift ;;
128
+ esac
129
+ done
130
+ if [ -n "${url}" ]; then
131
+ _drive open "${url}" "${rest[@]}"
132
+ else
133
+ _drive open "${rest[@]}"
134
+ fi
135
+ }
136
+
137
+ tool_click() {
138
+ local target=""
139
+ local rest=()
140
+ while [ "$#" -gt 0 ]; do
141
+ case "$1" in
142
+ --ref|--selector) target="$2"; shift 2 ;;
143
+ *) rest+=("$1"); shift ;;
144
+ esac
145
+ done
146
+ [ -n "${target}" ] || return 41
147
+ _drive click "${target}" "${rest[@]}"
148
+ }
149
+
150
+ tool_fill() {
151
+ local target="" text="" use_stdin=0
152
+ local rest=()
153
+ while [ "$#" -gt 0 ]; do
154
+ case "$1" in
155
+ --ref|--selector) target="$2"; shift 2 ;;
156
+ --text) text="$2"; shift 2 ;;
157
+ --secret-stdin) use_stdin=1; shift ;;
158
+ *) rest+=("$1"); shift ;;
159
+ esac
160
+ done
161
+ [ -n "${target}" ] || return 41
162
+ if [ "${use_stdin}" = "1" ]; then
163
+ _drive fill "${target}" --secret-stdin "${rest[@]}"
164
+ return $?
165
+ fi
166
+ [ -n "${text}" ] || return 41
167
+ _drive fill "${target}" "${text}" "${rest[@]}"
168
+ }
169
+
170
+ tool_snapshot() {
171
+ _drive snapshot "$@"
172
+ }
173
+
174
+ tool_inspect() {
175
+ _drive inspect "$@"
176
+ }
177
+
178
+ tool_audit() {
179
+ _drive audit "$@"
180
+ }
181
+
182
+ tool_extract() {
183
+ _drive extract "$@"
184
+ }
185
+
186
+ tool_eval() {
187
+ local expression=""
188
+ local rest=()
189
+ while [ "$#" -gt 0 ]; do
190
+ case "$1" in
191
+ --expression) expression="$2"; shift 2 ;;
192
+ *) rest+=("$1"); shift ;;
193
+ esac
194
+ done
195
+ if [ -n "${expression}" ]; then
196
+ _drive eval "${expression}" "${rest[@]}"
197
+ else
198
+ _drive eval "${rest[@]}"
199
+ fi
200
+ }
201
+
202
+ tool_press() {
203
+ local key=""
204
+ local rest=()
205
+ while [ "$#" -gt 0 ]; do
206
+ case "$1" in
207
+ --key) key="$2"; shift 2 ;;
208
+ *) rest+=("$1"); shift ;;
209
+ esac
210
+ done
211
+ [ -n "${key}" ] || return 41
212
+ _drive press "${key}" "${rest[@]}"
213
+ }
214
+
215
+ tool_select() {
216
+ local ref=""
217
+ local mode_flag="" mode_val=""
218
+ local rest=()
219
+ while [ "$#" -gt 0 ]; do
220
+ case "$1" in
221
+ --ref|--selector) ref="$2"; shift 2 ;;
222
+ --value|--label|--index)
223
+ mode_flag="$1"; mode_val="$2"; shift 2 ;;
224
+ *) rest+=("$1"); shift ;;
225
+ esac
226
+ done
227
+ [ -n "${ref}" ] || return 41
228
+ [ -n "${mode_flag}" ] || return 41
229
+ _drive select "${ref}" "${mode_flag}" "${mode_val}" "${rest[@]}"
230
+ }
231
+
232
+ tool_hover() {
233
+ local target=""
234
+ local rest=()
235
+ while [ "$#" -gt 0 ]; do
236
+ case "$1" in
237
+ --ref|--selector) target="$2"; shift 2 ;;
238
+ *) rest+=("$1"); shift ;;
239
+ esac
240
+ done
241
+ [ -n "${target}" ] || return 41
242
+ _drive hover "${target}" "${rest[@]}"
243
+ }
244
+
245
+ tool_wait() {
246
+ local selector=""
247
+ local rest=()
248
+ while [ "$#" -gt 0 ]; do
249
+ case "$1" in
250
+ --selector) selector="$2"; shift 2 ;;
251
+ *) rest+=("$1"); shift ;;
252
+ esac
253
+ done
254
+ [ -n "${selector}" ] || return 41
255
+ _drive wait "${selector}" "${rest[@]}"
256
+ }
257
+
258
+ tool_drag() {
259
+ local src_ref="" dst_ref=""
260
+ local rest=()
261
+ while [ "$#" -gt 0 ]; do
262
+ case "$1" in
263
+ --src-ref) src_ref="$2"; shift 2 ;;
264
+ --dst-ref) dst_ref="$2"; shift 2 ;;
265
+ *) rest+=("$1"); shift ;;
266
+ esac
267
+ done
268
+ [ -n "${src_ref}" ] || return 41
269
+ [ -n "${dst_ref}" ] || return 41
270
+ _drive drag "${src_ref}" "${dst_ref}" "${rest[@]}"
271
+ }
272
+
273
+ tool_upload() {
274
+ local ref="" path=""
275
+ local rest=()
276
+ while [ "$#" -gt 0 ]; do
277
+ case "$1" in
278
+ --ref) ref="$2"; shift 2 ;;
279
+ --path) path="$2"; shift 2 ;;
280
+ *) rest+=("$1"); shift ;;
281
+ esac
282
+ done
283
+ [ -n "${ref}" ] || return 41
284
+ [ -n "${path}" ] || return 41
285
+ _drive upload "${ref}" "${path}" "${rest[@]}"
286
+ }
287
+
288
+ tool_route() {
289
+ local pattern="" action=""
290
+ local rest=()
291
+ while [ "$#" -gt 0 ]; do
292
+ case "$1" in
293
+ --pattern) pattern="$2"; shift 2 ;;
294
+ --action) action="$2"; shift 2 ;;
295
+ *) rest+=("$1"); shift ;;
296
+ esac
297
+ done
298
+ [ -n "${pattern}" ] || return 41
299
+ [ -n "${action}" ] || return 41
300
+ _drive route "${pattern}" "${action}" "${rest[@]}"
301
+ }
302
+
303
+ # Phase-6 part 8-i: read-only tab enumeration. No flags. Bridge dispatches
304
+ # to runTabListViaDaemon which caches the result in the daemon's `tabs` slot.
305
+ tool_tab-list() {
306
+ _drive tab-list "$@"
307
+ }
308
+
309
+ # Phase-6 part 8-ii: switch active tab via mutex selectors. Daemon-side
310
+ # resolves to a tab_id, calls MCP select_page, updates currentTab pointer.
311
+ tool_tab-switch() {
312
+ local by_index="" by_url_pattern=""
313
+ local rest=()
314
+ while [ "$#" -gt 0 ]; do
315
+ case "$1" in
316
+ --by-index) by_index="$2"; shift 2 ;;
317
+ --by-url-pattern) by_url_pattern="$2"; shift 2 ;;
318
+ *) rest+=("$1"); shift ;;
319
+ esac
320
+ done
321
+ if [ -n "${by_index}" ]; then
322
+ _drive tab-switch --by-index "${by_index}" "${rest[@]}"
323
+ elif [ -n "${by_url_pattern}" ]; then
324
+ _drive tab-switch --by-url-pattern "${by_url_pattern}" "${rest[@]}"
325
+ else
326
+ return 41
327
+ fi
328
+ }
329
+
330
+ # Phase-6 part 8-iii: close a tab. Splice + upstream close + null currentTab
331
+ # on match. Mutex on the two selectors (enforced bash-side; bridge re-checks).
332
+ tool_tab-close() {
333
+ local tab_id="" by_url_pattern=""
334
+ local rest=()
335
+ while [ "$#" -gt 0 ]; do
336
+ case "$1" in
337
+ --tab-id) tab_id="$2"; shift 2 ;;
338
+ --by-url-pattern) by_url_pattern="$2"; shift 2 ;;
339
+ *) rest+=("$1"); shift ;;
340
+ esac
341
+ done
342
+ if [ -n "${tab_id}" ]; then
343
+ _drive tab-close --tab-id "${tab_id}" "${rest[@]}"
344
+ elif [ -n "${by_url_pattern}" ]; then
345
+ _drive tab-close --by-url-pattern "${by_url_pattern}" "${rest[@]}"
346
+ else
347
+ return 41
348
+ fi
349
+ }
@@ -0,0 +1,249 @@
1
+ # scripts/lib/tool/obscura.sh — Obscura tool adapter (shell only; verb-dispatch
2
+ # stubs land real-mode in 8-1-ii / 8-1-iii).
3
+ #
4
+ # Implements the Tool Adapter Extension Model contract from
5
+ # docs/superpowers/specs/2026-04-30-tool-adapter-extension-model-design.md §2.
6
+ #
7
+ # Identity: tool_metadata, tool_capabilities, tool_doctor_check
8
+ # Verb dispatch: tool_open, tool_click, tool_fill, tool_snapshot, tool_inspect,
9
+ # tool_audit, tool_extract, tool_eval
10
+ #
11
+ # Obscura (https://github.com/h4ckf0r0day/obscura, Apache 2.0, Rust) ships in
12
+ # two modes:
13
+ # 1. Stateless one-shot CLI: `obscura fetch <url>` + `obscura scrape <urls...>`
14
+ # 2. CDP server daemon: `obscura serve --port 9222`
15
+ #
16
+ # This adapter targets ONLY mode 1 — the unique lane vs incumbents (parallel
17
+ # scrape + stealth + 30/70 MB footprint). Mode 2 overlaps with playwright-lib's
18
+ # CDP transport and will land there via a future --cdp-endpoint flag, NOT here.
19
+ #
20
+ # Reachable via --tool=obscura only in 8-1-i (Path A "ship-without-promotion"
21
+ # per spec 2026-04-30 §4.4). Router promotion to default for --scrape /
22
+ # --stealth lands in a follow-up PR (8-2-i, Path B).
23
+ #
24
+ # Adapters are LEAVES — never source another adapter. Shared logic factors into
25
+ # scripts/lib/<concern>.sh (sibling to lib/tool/).
26
+
27
+ [ -n "${_BROWSER_TOOL_OBSCURA_LOADED:-}" ] && return 0
28
+ readonly _BROWSER_TOOL_OBSCURA_LOADED=1
29
+
30
+ # Required by spec 2026-05-01-token-efficient-adapter-output-design §8: every
31
+ # adapter sources output.sh so verb-dispatch emits JSON via emit_summary /
32
+ # emit_event rather than hand-rolled printf. Lint tier 3 enforces this.
33
+ # shellcheck source=../output.sh
34
+ # shellcheck disable=SC1091
35
+ source "$(dirname "${BASH_SOURCE[0]}")/../output.sh"
36
+
37
+ readonly _BROWSER_TOOL_OBSCURA_BIN="${OBSCURA_BIN:-obscura}"
38
+
39
+ # --- Identity functions (called by framework once or for queries) ---
40
+
41
+ tool_metadata() {
42
+ cat <<'EOF'
43
+ {
44
+ "name": "obscura",
45
+ "abi_version": 1,
46
+ "version_pin": "0.x",
47
+ "cheatsheet_path": "references/obscura-cheatsheet.md",
48
+ "install_hint": "download release from https://github.com/h4ckf0r0day/obscura/releases (no Chrome/Node required); keep obscura + obscura-worker side-by-side"
49
+ }
50
+ EOF
51
+ }
52
+
53
+ tool_capabilities() {
54
+ # Only `extract` declared — obscura's unique lane is stateless fetch/scrape.
55
+ # Stateful navigation (open/click/fill/snapshot) belongs to playwright-cli /
56
+ # playwright-lib / chrome-devtools-mcp; declaring them here would let the
57
+ # router fall back to obscura for verbs it can't actually serve.
58
+ #
59
+ # Flags array is advisory in v1 (per spec 2026-04-30 §2.1) — `--scrape` and
60
+ # `--stealth` are listed for documentation; real flag plumbing lands in
61
+ # 8-1-ii / 8-1-iii.
62
+ cat <<'EOF'
63
+ {
64
+ "verbs": {
65
+ "extract": { "flags": ["--scrape", "--stealth", "--eval", "--selector"] }
66
+ }
67
+ }
68
+ EOF
69
+ }
70
+
71
+ tool_doctor_check() {
72
+ if ! command -v "${_BROWSER_TOOL_OBSCURA_BIN}" >/dev/null 2>&1; then
73
+ cat <<EOF
74
+ { "ok": false, "binary": "${_BROWSER_TOOL_OBSCURA_BIN}", "error": "not on PATH",
75
+ "install_hint": "download release from https://github.com/h4ckf0r0day/obscura/releases (no Chrome/Node required); keep obscura + obscura-worker side-by-side" }
76
+ EOF
77
+ return 0
78
+ fi
79
+ local version
80
+ version="$("${_BROWSER_TOOL_OBSCURA_BIN}" --version 2>/dev/null || printf 'unknown')"
81
+ printf '{"ok":true,"binary":"%s","version":"%s"}\n' \
82
+ "${_BROWSER_TOOL_OBSCURA_BIN}" "${version}"
83
+ }
84
+
85
+ # --- Verb-dispatch functions ---
86
+ # Each function:
87
+ # - Reads named flags from "$@".
88
+ # - Never accepts secrets in argv (uses --secret-stdin pattern).
89
+ # - Emits zero-or-more streaming JSON lines to stdout.
90
+ # - Returns 41 if it cannot handle the op (defensive — router shouldn't route
91
+ # here, but the guard is cheap).
92
+ #
93
+ # Phase 8 part 1-i: every verb returns 41. tool_extract becomes real-mode in
94
+ # 8-1-ii (--scrape; this PR) and 8-1-iii (--stealth). All other verbs stay 41
95
+ # forever — obscura is intentionally a one-shot extract-only adapter.
96
+
97
+ tool_open() { return 41; }
98
+ tool_click() { return 41; }
99
+ tool_fill() { return 41; }
100
+ tool_snapshot() { return 41; }
101
+ tool_inspect() { return 41; }
102
+ tool_audit() { return 41; }
103
+ tool_eval() { return 41; }
104
+
105
+ # tool_extract — Phase 8 part 1-ii (--scrape) + 1-iii (--stealth).
106
+ #
107
+ # Modes (router/verb selects via flags; mutually exclusive):
108
+ # --scrape <url1> <url2> ... [--eval EXPR] [--concurrency N]
109
+ # Wraps `obscura scrape u1 u2 ... --eval EXPR --format json`. Emits one
110
+ # `scrape_url` event per URL on stdout (success or error shape from
111
+ # obscura's per-result divergence in run_parallel_scrape).
112
+ # --stealth <url> --eval EXPR
113
+ # Wraps `obscura fetch <url> --stealth --eval EXPR`. Single URL.
114
+ # --eval REQUIRED (without it, obscura fetch dumps full HTML — too large
115
+ # for the streaming-event contract). Emits one `extract_stealth` event:
116
+ # {event, url, eval, time_ms}. Adapter times the call (fetch doesn't
117
+ # report time). `eval` always emitted as string (obscura fetch --eval
118
+ # prints raw, not wrapped JSON; typed parsing deferred).
119
+ # --selector / --eval (single URL, no --scrape / --stealth) — never supported
120
+ # here; routed to chrome-devtools-mcp / playwright-cli.
121
+ #
122
+ # Returns:
123
+ # 0 on successful adapter call (per-URL event stream may include errors).
124
+ # 2 on USAGE_ERROR (empty URL list with --scrape; missing URL or --eval
125
+ # with --stealth).
126
+ # 41 if no recognized mode OR mutually-exclusive modes selected.
127
+ tool_extract() {
128
+ local mode_scrape=0 mode_stealth=0 eval_expr="" concurrency=""
129
+ local urls=()
130
+ while [ "$#" -gt 0 ]; do
131
+ case "$1" in
132
+ --scrape) mode_scrape=1; shift ;;
133
+ --stealth) mode_stealth=1; shift ;;
134
+ --eval) eval_expr="$2"; shift 2 ;;
135
+ --concurrency) concurrency="$2"; shift 2 ;;
136
+ --selector|--site|--tool|--dry-run|--raw)
137
+ # Recognised skill flags not consumed by this adapter.
138
+ case "$1" in
139
+ --dry-run|--raw) shift ;;
140
+ *) shift 2 ;;
141
+ esac
142
+ ;;
143
+ --*)
144
+ # Unknown flag — passthrough to obscura would mask config drift; reject.
145
+ return 41
146
+ ;;
147
+ *) urls+=("$1"); shift ;;
148
+ esac
149
+ done
150
+
151
+ # Mutually-exclusive mode selection.
152
+ if [ "${mode_scrape}" = "1" ] && [ "${mode_stealth}" = "1" ]; then
153
+ return 41
154
+ fi
155
+
156
+ if [ "${mode_scrape}" = "1" ]; then
157
+ _tool_extract_scrape "${eval_expr}" "${concurrency}" "${urls[@]}"
158
+ return $?
159
+ fi
160
+
161
+ if [ "${mode_stealth}" = "1" ]; then
162
+ _tool_extract_stealth "${eval_expr}" "${urls[@]}"
163
+ return $?
164
+ fi
165
+
166
+ # No recognised mode. Other one-shot extract paths route elsewhere.
167
+ return 41
168
+ }
169
+
170
+ # _tool_extract_scrape EVAL_EXPR CONCURRENCY URLS...
171
+ # Internal helper — wraps `obscura scrape`.
172
+ _tool_extract_scrape() {
173
+ local eval_expr="$1" concurrency="$2"
174
+ shift 2
175
+ local urls=("$@")
176
+
177
+ if [ "${#urls[@]}" -eq 0 ]; then
178
+ return 2
179
+ fi
180
+
181
+ # Canonical argv (sha256-of-argv must be stable for fixture-based stub):
182
+ # scrape <urls...> [--eval EXPR] [--concurrency N] --format json
183
+ local args=("scrape" "${urls[@]}")
184
+ [ -n "${eval_expr}" ] && args+=(--eval "${eval_expr}")
185
+ [ -n "${concurrency}" ] && args+=(--concurrency "${concurrency}")
186
+ args+=(--format json)
187
+
188
+ local raw
189
+ if ! raw="$("${_BROWSER_TOOL_OBSCURA_BIN}" "${args[@]}" 2>/dev/null)"; then
190
+ return 41
191
+ fi
192
+
193
+ # Reshape obscura's per-URL .results[] into one streaming event line per URL.
194
+ # Direct jq pass-through preserves the eval field's JSON typing (string /
195
+ # number / array / null / object — emit_event can't carry arbitrary JSON
196
+ # values). Summary line built by browser-extract.sh via emit_summary.
197
+ printf '%s' "${raw}" | jq -c '
198
+ .results[] |
199
+ {event: "scrape_url"} +
200
+ if has("error") then
201
+ {url, error, time_ms}
202
+ else
203
+ {url, title, eval, time_ms}
204
+ end
205
+ ' || return 41
206
+
207
+ return 0
208
+ }
209
+
210
+ # _tool_extract_stealth EVAL_EXPR URLS...
211
+ # Internal helper — wraps `obscura fetch <url> --stealth --eval EXPR`.
212
+ # Single URL (rejects 0 or ≥2). --eval required.
213
+ _tool_extract_stealth() {
214
+ local eval_expr="$1"
215
+ shift
216
+ local urls=("$@")
217
+
218
+ if [ "${#urls[@]}" -ne 1 ]; then
219
+ return 2
220
+ fi
221
+ if [ -z "${eval_expr}" ]; then
222
+ return 2
223
+ fi
224
+ local url="${urls[0]}"
225
+
226
+ # Canonical argv: fetch <url> --stealth --eval EXPR
227
+ local args=("fetch" "${url}" --stealth --eval "${eval_expr}")
228
+
229
+ # No time_ms field (obscura fetch doesn't report timing; the verb-script's
230
+ # summary already carries end-to-end duration_ms via SUMMARY_T0). Adapters
231
+ # are leaves — don't source common.sh's now_ms; don't fabricate timing.
232
+ local raw
233
+ if ! raw="$("${_BROWSER_TOOL_OBSCURA_BIN}" "${args[@]}" 2>/dev/null)"; then
234
+ return 41
235
+ fi
236
+
237
+ # obscura fetch --eval prints raw evaluated result (string unquoted; other
238
+ # JSON-encoded). Strip trailing newline; emit as string. Typed parsing
239
+ # deferred — callers needing typed results should JSON.stringify in EXPR.
240
+ local eval_out
241
+ eval_out="${raw%$'\n'}"
242
+
243
+ jq -nc \
244
+ --arg url "${url}" \
245
+ --arg eval_val "${eval_out}" \
246
+ '{event: "extract_stealth", url: $url, eval: $eval_val}'
247
+
248
+ return 0
249
+ }