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.
- package/LICENSE +21 -0
- package/README.md +144 -0
- package/SECURITY.md +39 -0
- package/SKILL.md +206 -0
- package/bin/cli.mjs +55 -0
- package/install.sh +143 -0
- package/package.json +54 -0
- package/references/adapter-candidates.md +40 -0
- package/references/browser-mcp-cheatsheet.md +132 -0
- package/references/browser-stats-cheatsheet.md +155 -0
- package/references/chrome-devtools-mcp-cheatsheet.md +232 -0
- package/references/midscene-integration.md +359 -0
- package/references/obscura-cheatsheet.md +103 -0
- package/references/playwright-cli-cheatsheet.md +64 -0
- package/references/playwright-lib-cheatsheet.md +90 -0
- package/references/recipes/add-a-tool-adapter.md +134 -0
- package/references/recipes/agent-workflows/README.md +37 -0
- package/references/recipes/agent-workflows/cache-driven-bulk-operation.md +110 -0
- package/references/recipes/agent-workflows/flow-record-and-replay.md +102 -0
- package/references/recipes/agent-workflows/incremental-pattern-discovery.md +125 -0
- package/references/recipes/agent-workflows/login-then-scrape.md +100 -0
- package/references/recipes/anti-patterns-tool-extension.md +182 -0
- package/references/recipes/body-bytes-not-body.md +139 -0
- package/references/recipes/cache-write-security.md +210 -0
- package/references/recipes/fingerprint-rescue.md +154 -0
- package/references/recipes/model-routing.md +143 -0
- package/references/recipes/path-security.md +138 -0
- package/references/recipes/privacy-canary.md +96 -0
- package/references/recipes/visual-rescue-hook.md +182 -0
- package/references/stats-prices.json +42 -0
- package/references/stats-schema.json +77 -0
- package/references/tool-versions.md +8 -0
- package/scripts/browser-add-site.sh +113 -0
- package/scripts/browser-assert.sh +106 -0
- package/scripts/browser-audit.sh +68 -0
- package/scripts/browser-baseline.sh +135 -0
- package/scripts/browser-click.sh +100 -0
- package/scripts/browser-creds-add.sh +254 -0
- package/scripts/browser-creds-list.sh +67 -0
- package/scripts/browser-creds-migrate.sh +122 -0
- package/scripts/browser-creds-remove.sh +69 -0
- package/scripts/browser-creds-rotate-totp.sh +109 -0
- package/scripts/browser-creds-show.sh +82 -0
- package/scripts/browser-creds-totp.sh +94 -0
- package/scripts/browser-do.sh +630 -0
- package/scripts/browser-doctor.sh +365 -0
- package/scripts/browser-drag.sh +90 -0
- package/scripts/browser-extract.sh +192 -0
- package/scripts/browser-fill.sh +142 -0
- package/scripts/browser-flow.sh +316 -0
- package/scripts/browser-history.sh +187 -0
- package/scripts/browser-hover.sh +92 -0
- package/scripts/browser-inspect.sh +188 -0
- package/scripts/browser-list-sessions.sh +78 -0
- package/scripts/browser-list-sites.sh +42 -0
- package/scripts/browser-login.sh +279 -0
- package/scripts/browser-mcp.sh +65 -0
- package/scripts/browser-migrate.sh +195 -0
- package/scripts/browser-open.sh +134 -0
- package/scripts/browser-press.sh +80 -0
- package/scripts/browser-remove-session.sh +72 -0
- package/scripts/browser-remove-site.sh +68 -0
- package/scripts/browser-replay.sh +206 -0
- package/scripts/browser-route.sh +174 -0
- package/scripts/browser-select.sh +122 -0
- package/scripts/browser-show-session.sh +57 -0
- package/scripts/browser-show-site.sh +37 -0
- package/scripts/browser-snapshot.sh +176 -0
- package/scripts/browser-stats.sh +522 -0
- package/scripts/browser-tab-close.sh +112 -0
- package/scripts/browser-tab-list.sh +70 -0
- package/scripts/browser-tab-switch.sh +111 -0
- package/scripts/browser-upload.sh +132 -0
- package/scripts/browser-use.sh +60 -0
- package/scripts/browser-vlm.sh +707 -0
- package/scripts/browser-wait.sh +97 -0
- package/scripts/install-git-hooks.sh +16 -0
- package/scripts/lib/capture.sh +356 -0
- package/scripts/lib/common.sh +262 -0
- package/scripts/lib/credential.sh +237 -0
- package/scripts/lib/fingerprint-rescue.js +123 -0
- package/scripts/lib/flow.sh +448 -0
- package/scripts/lib/flow_record.sh +210 -0
- package/scripts/lib/mask.sh +49 -0
- package/scripts/lib/memory.sh +427 -0
- package/scripts/lib/migrate.sh +390 -0
- package/scripts/lib/migrators/README.md +23 -0
- package/scripts/lib/migrators/memory/v1_to_v2.sh +15 -0
- package/scripts/lib/migrators/recent_urls/README.md +13 -0
- package/scripts/lib/migrators/stats/README.md +24 -0
- package/scripts/lib/node/chrome-devtools-bridge.mjs +1812 -0
- package/scripts/lib/node/mcp-server.mjs +531 -0
- package/scripts/lib/node/mcp-tools.json +68 -0
- package/scripts/lib/node/playwright-driver.mjs +1104 -0
- package/scripts/lib/node/totp-core.mjs +52 -0
- package/scripts/lib/node/totp.mjs +52 -0
- package/scripts/lib/node/url-pattern-cluster.mjs +102 -0
- package/scripts/lib/node/url-pattern-resolver.mjs +77 -0
- package/scripts/lib/output.sh +79 -0
- package/scripts/lib/router.sh +342 -0
- package/scripts/lib/sanitize.sh +107 -0
- package/scripts/lib/secret/keychain.sh +91 -0
- package/scripts/lib/secret/libsecret.sh +74 -0
- package/scripts/lib/secret/plaintext.sh +75 -0
- package/scripts/lib/secret_backend_select.sh +57 -0
- package/scripts/lib/session.sh +153 -0
- package/scripts/lib/site.sh +126 -0
- package/scripts/lib/stats.sh +419 -0
- package/scripts/lib/tool/.gitkeep +0 -0
- package/scripts/lib/tool/chrome-devtools-mcp.sh +349 -0
- package/scripts/lib/tool/obscura.sh +249 -0
- package/scripts/lib/tool/playwright-cli.sh +155 -0
- package/scripts/lib/tool/playwright-lib.sh +106 -0
- package/scripts/lib/verb_helpers.sh +222 -0
- package/scripts/lib/visual-rescue-default.sh +145 -0
- package/scripts/regenerate-docs.sh +99 -0
- 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
|
+
}
|