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