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,522 @@
|
|
|
1
|
+
#!/usr/bin/env bash
|
|
2
|
+
# scripts/browser-stats.sh — Phase 12 part 1: telemetry surface verb.
|
|
3
|
+
# Usage:
|
|
4
|
+
# browser-stats event ... # internal — emit one event from CLI
|
|
5
|
+
# browser-stats rebuild # rebuild SQLite mirror from JSONL
|
|
6
|
+
# browser-stats report [--days N] [--route ROUTE] [--verb VERB] [--pareto]
|
|
7
|
+
# browser-stats mark <span_id> <verdict>[:<reason>]
|
|
8
|
+
# browser-stats tune [--days N] [--route ROUTE]
|
|
9
|
+
#
|
|
10
|
+
# Verdict ∈ {success, fail}. Reason optional ("fail:popup_intercept" etc).
|
|
11
|
+
#
|
|
12
|
+
# All subcommands respect $BROWSER_SKILL_HOME. Writes are mode 0600 in the
|
|
13
|
+
# memory/ dir (mode 0700). No network. SQLite is built lazily on `rebuild`;
|
|
14
|
+
# JSONL is the source of truth.
|
|
15
|
+
|
|
16
|
+
set -Eeuo pipefail
|
|
17
|
+
# Inherit errexit into command substitutions (bash 4.4+). Without this,
|
|
18
|
+
# `local x=$(jq ...)` silently swallows jq's non-zero exit (shellcheck SC2311
|
|
19
|
+
# trap). With it, the script aborts as expected.
|
|
20
|
+
shopt -s inherit_errexit
|
|
21
|
+
IFS=$'\n\t'
|
|
22
|
+
|
|
23
|
+
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
|
24
|
+
# shellcheck source=lib/common.sh
|
|
25
|
+
# shellcheck disable=SC1091
|
|
26
|
+
source "${SCRIPT_DIR}/lib/common.sh"
|
|
27
|
+
# shellcheck source=lib/output.sh
|
|
28
|
+
# shellcheck disable=SC1091
|
|
29
|
+
source "${SCRIPT_DIR}/lib/output.sh"
|
|
30
|
+
# shellcheck source=lib/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
|
+
STATS_JSONL="${BROWSER_SKILL_HOME}/memory/stats.jsonl"
|
|
39
|
+
STATS_DB="${BROWSER_SKILL_HOME}/memory/stats.db"
|
|
40
|
+
PRICES_FILE="${BROWSER_STATS_PRICES_FILE:-${SCRIPT_DIR}/../references/stats-prices.json}"
|
|
41
|
+
|
|
42
|
+
subcmd="${1:-}"
|
|
43
|
+
[ -n "${subcmd}" ] || die "${EXIT_USAGE_ERROR}" "browser-stats: subcommand required (event|rebuild|report|mark|tune)"
|
|
44
|
+
shift
|
|
45
|
+
|
|
46
|
+
# --- helpers ----------------------------------------------------------------
|
|
47
|
+
|
|
48
|
+
require_sqlite3() {
|
|
49
|
+
command -v sqlite3 >/dev/null 2>&1 \
|
|
50
|
+
|| die "${EXIT_PREFLIGHT_FAILED}" "browser-stats: sqlite3 not installed"
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
# stats_db_init — create schema if absent (idempotent).
|
|
54
|
+
stats_db_init() {
|
|
55
|
+
require_sqlite3
|
|
56
|
+
stats_init_dir
|
|
57
|
+
sqlite3 "${STATS_DB}" <<'SQL'
|
|
58
|
+
CREATE TABLE IF NOT EXISTS stats_events (
|
|
59
|
+
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
60
|
+
schema_version INTEGER NOT NULL,
|
|
61
|
+
ts TEXT NOT NULL,
|
|
62
|
+
trace_id TEXT NOT NULL,
|
|
63
|
+
span_id TEXT NOT NULL UNIQUE,
|
|
64
|
+
parent_span_id TEXT,
|
|
65
|
+
session_id TEXT,
|
|
66
|
+
gen_ai_tool_name TEXT,
|
|
67
|
+
verb TEXT NOT NULL,
|
|
68
|
+
adapter_route TEXT NOT NULL,
|
|
69
|
+
site TEXT,
|
|
70
|
+
selector_kind TEXT,
|
|
71
|
+
selector_value TEXT,
|
|
72
|
+
duration_ms INTEGER,
|
|
73
|
+
argv_bytes INTEGER DEFAULT 0,
|
|
74
|
+
stdout_bytes INTEGER DEFAULT 0,
|
|
75
|
+
stderr_bytes INTEGER DEFAULT 0,
|
|
76
|
+
rc INTEGER,
|
|
77
|
+
outcome TEXT NOT NULL,
|
|
78
|
+
failure_mode TEXT,
|
|
79
|
+
model TEXT,
|
|
80
|
+
service_tier TEXT,
|
|
81
|
+
input_tokens INTEGER,
|
|
82
|
+
output_tokens INTEGER,
|
|
83
|
+
cache_read_tokens INTEGER,
|
|
84
|
+
cache_create_tokens INTEGER,
|
|
85
|
+
post_condition_target_type TEXT,
|
|
86
|
+
post_condition_matcher TEXT,
|
|
87
|
+
post_condition_hit INTEGER,
|
|
88
|
+
post_condition_expected TEXT,
|
|
89
|
+
post_condition_observed TEXT,
|
|
90
|
+
raw_json TEXT NOT NULL
|
|
91
|
+
);
|
|
92
|
+
CREATE INDEX IF NOT EXISTS ix_stats_ts ON stats_events(ts DESC);
|
|
93
|
+
CREATE INDEX IF NOT EXISTS ix_stats_verb_route ON stats_events(verb, adapter_route);
|
|
94
|
+
CREATE INDEX IF NOT EXISTS ix_stats_outcome ON stats_events(outcome);
|
|
95
|
+
CREATE INDEX IF NOT EXISTS ix_stats_site ON stats_events(site);
|
|
96
|
+
CREATE INDEX IF NOT EXISTS ix_stats_failure_mode ON stats_events(failure_mode);
|
|
97
|
+
|
|
98
|
+
CREATE TABLE IF NOT EXISTS stats_cursor (
|
|
99
|
+
source TEXT PRIMARY KEY,
|
|
100
|
+
last_line INTEGER NOT NULL DEFAULT 0,
|
|
101
|
+
last_ts TEXT
|
|
102
|
+
);
|
|
103
|
+
CREATE TABLE IF NOT EXISTS stats_overrides (
|
|
104
|
+
span_id TEXT PRIMARY KEY,
|
|
105
|
+
verdict TEXT NOT NULL,
|
|
106
|
+
reason TEXT,
|
|
107
|
+
marked_at TEXT NOT NULL
|
|
108
|
+
);
|
|
109
|
+
PRAGMA user_version = 1;
|
|
110
|
+
SQL
|
|
111
|
+
chmod 600 "${STATS_DB}" 2>/dev/null || true
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
# stats_rebuild — tail JSONL from last cursor, upsert into SQLite.
|
|
115
|
+
stats_rebuild() {
|
|
116
|
+
stats_db_init
|
|
117
|
+
[ -f "${STATS_JSONL}" ] || { ok "stats: no JSONL yet — nothing to rebuild"; return 0; }
|
|
118
|
+
|
|
119
|
+
local last_line cur new_lines processed=0
|
|
120
|
+
last_line=$(sqlite3 "${STATS_DB}" \
|
|
121
|
+
"SELECT COALESCE(last_line, 0) FROM stats_cursor WHERE source='stats.jsonl';" 2>/dev/null \
|
|
122
|
+
|| printf '0')
|
|
123
|
+
last_line="${last_line:-0}"
|
|
124
|
+
cur=$(wc -l < "${STATS_JSONL}" | tr -d ' ')
|
|
125
|
+
|
|
126
|
+
if [ "${cur}" -le "${last_line}" ]; then
|
|
127
|
+
ok "stats: SQLite up to date (${cur} lines indexed)"
|
|
128
|
+
return 0
|
|
129
|
+
fi
|
|
130
|
+
|
|
131
|
+
# Stream new lines into SQL inserts via jq → sqlite3 stdin (single transaction).
|
|
132
|
+
new_lines=$(( cur - last_line ))
|
|
133
|
+
{
|
|
134
|
+
printf 'BEGIN;\n'
|
|
135
|
+
tail -n "${new_lines}" "${STATS_JSONL}" | jq -r '
|
|
136
|
+
def q: tostring | gsub("'"'"'"; "'"'"''"'"'");
|
|
137
|
+
[
|
|
138
|
+
(.schema_version // 1 | tostring),
|
|
139
|
+
("'"'"'" + (.ts // "" | q) + "'"'"'"),
|
|
140
|
+
("'"'"'" + (.trace_id // "" | q) + "'"'"'"),
|
|
141
|
+
("'"'"'" + (.span_id // "" | q) + "'"'"'"),
|
|
142
|
+
(if .parent_span_id then ("'"'"'" + (.parent_span_id | q) + "'"'"'") else "NULL" end),
|
|
143
|
+
(if .session_id then ("'"'"'" + (.session_id | q) + "'"'"'") else "NULL" end),
|
|
144
|
+
(if .gen_ai_tool_name then ("'"'"'" + (.gen_ai_tool_name | q) + "'"'"'") else "NULL" end),
|
|
145
|
+
("'"'"'" + (.verb // "" | q) + "'"'"'"),
|
|
146
|
+
("'"'"'" + (.adapter_route // "" | q) + "'"'"'"),
|
|
147
|
+
(if .site then ("'"'"'" + (.site | q) + "'"'"'") else "NULL" end),
|
|
148
|
+
(if .selector_kind then ("'"'"'" + (.selector_kind | q) + "'"'"'") else "NULL" end),
|
|
149
|
+
(if .selector_value then ("'"'"'" + (.selector_value | q) + "'"'"'") else "NULL" end),
|
|
150
|
+
(.duration_ms // 0 | tostring),
|
|
151
|
+
(.argv_bytes // 0 | tostring),
|
|
152
|
+
(.stdout_bytes // 0 | tostring),
|
|
153
|
+
(.stderr_bytes // 0 | tostring),
|
|
154
|
+
(.rc // 0 | tostring),
|
|
155
|
+
("'"'"'" + (.outcome // "" | q) + "'"'"'"),
|
|
156
|
+
(if .failure_mode then ("'"'"'" + (.failure_mode | q) + "'"'"'") else "NULL" end),
|
|
157
|
+
(if .model then ("'"'"'" + (.model | q) + "'"'"'") else "NULL" end),
|
|
158
|
+
(if .service_tier then ("'"'"'" + (.service_tier | q) + "'"'"'") else "NULL" end),
|
|
159
|
+
(if .gen_ai_usage_input_tokens then (.gen_ai_usage_input_tokens | tostring) else "NULL" end),
|
|
160
|
+
(if .gen_ai_usage_output_tokens then (.gen_ai_usage_output_tokens | tostring) else "NULL" end),
|
|
161
|
+
(if .gen_ai_usage_cache_read_input_tokens then (.gen_ai_usage_cache_read_input_tokens | tostring) else "NULL" end),
|
|
162
|
+
(if .gen_ai_usage_cache_creation_input_tokens then (.gen_ai_usage_cache_creation_input_tokens | tostring) else "NULL" end),
|
|
163
|
+
(if .post_condition_target_type then ("'"'"'" + (.post_condition_target_type | q) + "'"'"'") else "NULL" end),
|
|
164
|
+
(if .post_condition_matcher then ("'"'"'" + (.post_condition_matcher | q) + "'"'"'") else "NULL" end),
|
|
165
|
+
(if .post_condition_hit == true then "1" elif .post_condition_hit == false then "0" else "NULL" end),
|
|
166
|
+
(if .post_condition_expected then ("'"'"'" + (.post_condition_expected | q) + "'"'"'") else "NULL" end),
|
|
167
|
+
(if .post_condition_observed then ("'"'"'" + (.post_condition_observed | q) + "'"'"'") else "NULL" end),
|
|
168
|
+
("'"'"'" + (. | tostring | q) + "'"'"'")
|
|
169
|
+
] | "INSERT OR IGNORE INTO stats_events VALUES (NULL," + join(",") + ");"
|
|
170
|
+
'
|
|
171
|
+
printf "INSERT INTO stats_cursor(source,last_line,last_ts) VALUES('stats.jsonl',%d,datetime('now')) ON CONFLICT(source) DO UPDATE SET last_line=excluded.last_line,last_ts=excluded.last_ts;\n" "${cur}"
|
|
172
|
+
printf 'COMMIT;\n'
|
|
173
|
+
} | sqlite3 "${STATS_DB}"
|
|
174
|
+
processed="${new_lines}"
|
|
175
|
+
ok "stats: rebuilt — indexed ${processed} new event(s) (total lines: ${cur})"
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
# stats_load_prices — sources the JSON price table; sets globals PRICE_<MODEL>_<KIND>.
|
|
179
|
+
# Falls back silently if file missing — cost columns become null in report.
|
|
180
|
+
stats_load_prices() {
|
|
181
|
+
PRICES_AVAILABLE=0
|
|
182
|
+
[ -f "${PRICES_FILE}" ] || return 0
|
|
183
|
+
PRICES_AVAILABLE=1
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
# stats_report — print summary tables.
|
|
187
|
+
stats_report() {
|
|
188
|
+
require_sqlite3
|
|
189
|
+
stats_rebuild >/dev/null 2>&1 || true
|
|
190
|
+
[ -f "${STATS_DB}" ] || { ok "stats: no events yet"; emit_summary verb=stats tool=none why=report status=empty events=0; return 0; }
|
|
191
|
+
|
|
192
|
+
local days=7 route_filter="" verb_filter="" pareto=0
|
|
193
|
+
while [ "$#" -gt 0 ]; do
|
|
194
|
+
case "$1" in
|
|
195
|
+
--days) days="$2"; shift 2 ;;
|
|
196
|
+
--route) route_filter="$2"; shift 2 ;;
|
|
197
|
+
--verb) verb_filter="$2"; shift 2 ;;
|
|
198
|
+
--pareto) pareto=1; shift ;;
|
|
199
|
+
*) die "${EXIT_USAGE_ERROR}" "report: unknown flag '$1'" ;;
|
|
200
|
+
esac
|
|
201
|
+
done
|
|
202
|
+
|
|
203
|
+
local where="WHERE ts >= datetime('now', '-${days} days')"
|
|
204
|
+
[ -n "${route_filter}" ] && where="${where} AND adapter_route='${route_filter}'"
|
|
205
|
+
[ -n "${verb_filter}" ] && where="${where} AND verb='${verb_filter}'"
|
|
206
|
+
|
|
207
|
+
stats_load_prices
|
|
208
|
+
|
|
209
|
+
printf '\n=== browser-stats report (last %s day(s)) ===\n\n' "${days}" >&2
|
|
210
|
+
|
|
211
|
+
# Headline: events / outcomes.
|
|
212
|
+
local total
|
|
213
|
+
total=$(sqlite3 "${STATS_DB}" "SELECT COUNT(*) FROM stats_events ${where};")
|
|
214
|
+
[ "${total}" = "0" ] && { ok "stats: no events in window"; emit_summary verb=stats tool=none why=report status=empty events=0; return 0; }
|
|
215
|
+
|
|
216
|
+
printf 'Events: %s\n' "${total}" >&2
|
|
217
|
+
sqlite3 -separator $'\t' "${STATS_DB}" "
|
|
218
|
+
SELECT outcome, COUNT(*) AS n,
|
|
219
|
+
ROUND(100.0*COUNT(*)/${total}, 1) AS pct
|
|
220
|
+
FROM stats_events ${where}
|
|
221
|
+
GROUP BY outcome ORDER BY n DESC;" \
|
|
222
|
+
| awk -F'\t' 'BEGIN{printf "\n %-10s %8s %8s\n", "outcome","count","pct"}
|
|
223
|
+
{printf " %-10s %8s %7s%%\n",$1,$2,$3}' >&2
|
|
224
|
+
|
|
225
|
+
# Route × verb table.
|
|
226
|
+
printf '\nRoute × verb:\n' >&2
|
|
227
|
+
sqlite3 -separator $'\t' "${STATS_DB}" "
|
|
228
|
+
SELECT adapter_route, verb, COUNT(*) AS n,
|
|
229
|
+
SUM(CASE WHEN outcome='success' THEN 1 ELSE 0 END) AS ok,
|
|
230
|
+
CAST(AVG(duration_ms) AS INTEGER) AS avg_ms,
|
|
231
|
+
CAST(AVG(stdout_bytes) AS INTEGER) AS avg_out
|
|
232
|
+
FROM stats_events ${where}
|
|
233
|
+
GROUP BY adapter_route, verb
|
|
234
|
+
ORDER BY n DESC LIMIT 20;" \
|
|
235
|
+
| awk -F'\t' 'BEGIN{printf " %-22s %-12s %6s %6s %8s %10s\n","route","verb","n","ok","avg_ms","avg_out_b"}
|
|
236
|
+
{printf " %-22s %-12s %6s %6s %8s %10s\n",$1,$2,$3,$4,$5,$6}' >&2
|
|
237
|
+
|
|
238
|
+
# Failure modes.
|
|
239
|
+
printf '\nFailure modes:\n' >&2
|
|
240
|
+
sqlite3 -separator $'\t' "${STATS_DB}" "
|
|
241
|
+
SELECT COALESCE(failure_mode,'(unclassified)') AS fm, COUNT(*) AS n
|
|
242
|
+
FROM stats_events ${where} AND outcome != 'success'
|
|
243
|
+
GROUP BY failure_mode ORDER BY n DESC LIMIT 15;" 2>/dev/null \
|
|
244
|
+
| awk -F'\t' '{printf " %-24s %6s\n",$1,$2}' >&2
|
|
245
|
+
|
|
246
|
+
# Post-condition assertion rate.
|
|
247
|
+
printf '\nPost-condition assertions:\n' >&2
|
|
248
|
+
sqlite3 -separator $'\t' "${STATS_DB}" "
|
|
249
|
+
SELECT
|
|
250
|
+
SUM(CASE WHEN post_condition_hit=1 THEN 1 ELSE 0 END) AS hit,
|
|
251
|
+
SUM(CASE WHEN post_condition_hit=0 THEN 1 ELSE 0 END) AS miss,
|
|
252
|
+
SUM(CASE WHEN post_condition_hit IS NULL THEN 1 ELSE 0 END) AS none
|
|
253
|
+
FROM stats_events ${where};" \
|
|
254
|
+
| awk -F'\t' '{printf " hit:%s miss:%s not-asserted:%s\n",$1,$2,$3}' >&2
|
|
255
|
+
|
|
256
|
+
# Oblivious-success — the killer signal.
|
|
257
|
+
local obliv
|
|
258
|
+
obliv=$(sqlite3 "${STATS_DB}" "
|
|
259
|
+
SELECT COUNT(*) FROM stats_events ${where}
|
|
260
|
+
AND failure_mode='oblivious_success';")
|
|
261
|
+
printf '\n ⚠ oblivious_success: %s (adapter said ok but post-condition failed)\n' "${obliv}" >&2
|
|
262
|
+
|
|
263
|
+
# Token + cost rollup if prices available.
|
|
264
|
+
if [ "${PRICES_AVAILABLE}" = "1" ]; then
|
|
265
|
+
printf '\nToken / cost (when injected via CLAUDE_USAGE_* env):\n' >&2
|
|
266
|
+
sqlite3 -separator $'\t' "${STATS_DB}" "
|
|
267
|
+
SELECT
|
|
268
|
+
COALESCE(model,'(no model)') AS m,
|
|
269
|
+
COUNT(*) AS n,
|
|
270
|
+
COALESCE(SUM(input_tokens),0) AS in_tok,
|
|
271
|
+
COALESCE(SUM(output_tokens),0) AS out_tok,
|
|
272
|
+
COALESCE(SUM(cache_read_tokens),0) AS cr,
|
|
273
|
+
COALESCE(SUM(cache_create_tokens),0) AS cc
|
|
274
|
+
FROM stats_events ${where}
|
|
275
|
+
GROUP BY model ORDER BY n DESC;" \
|
|
276
|
+
| awk -F'\t' 'BEGIN{printf " %-22s %6s %10s %10s %10s %10s\n","model","n","input","output","cache_r","cache_w"}
|
|
277
|
+
{printf " %-22s %6s %10s %10s %10s %10s\n",$1,$2,$3,$4,$5,$6}' >&2
|
|
278
|
+
|
|
279
|
+
# Read prices, compute $ per model. Per-model row produced by SQLite;
|
|
280
|
+
# jq does the dollar math (centi-USD rounded to keep the report readable).
|
|
281
|
+
local rows_json
|
|
282
|
+
rows_json="$(sqlite3 "${STATS_DB}" -json "
|
|
283
|
+
SELECT
|
|
284
|
+
COALESCE(model,'(none)') AS model,
|
|
285
|
+
COALESCE(SUM(input_tokens),0) AS i,
|
|
286
|
+
COALESCE(SUM(output_tokens),0) AS o,
|
|
287
|
+
COALESCE(SUM(cache_read_tokens),0) AS cr,
|
|
288
|
+
COALESCE(SUM(cache_create_tokens),0) AS cc
|
|
289
|
+
FROM stats_events ${where}
|
|
290
|
+
GROUP BY model;")"
|
|
291
|
+
[ -z "${rows_json}" ] && rows_json='[]'
|
|
292
|
+
local cost_json
|
|
293
|
+
cost_json=$(
|
|
294
|
+
jq -nc \
|
|
295
|
+
--slurpfile prices "${PRICES_FILE}" \
|
|
296
|
+
--argjson rows "${rows_json}" '
|
|
297
|
+
($prices[0].models // {}) as $p
|
|
298
|
+
| [ $rows[]
|
|
299
|
+
| . as $r
|
|
300
|
+
| ($p[$r.model]) as $price
|
|
301
|
+
| if ($price == null) then
|
|
302
|
+
{model: $r.model, cost_usd: null}
|
|
303
|
+
else
|
|
304
|
+
( ($r.i * ($price.input // 0)) +
|
|
305
|
+
($r.o * ($price.output // 0)) +
|
|
306
|
+
($r.cr * ($price.cache_read // 0)) +
|
|
307
|
+
($r.cc * ($price.cache_create // 0))
|
|
308
|
+
) as $raw
|
|
309
|
+
| (($raw * 100 | round) / 100) as $cents
|
|
310
|
+
| {model: $r.model, cost_usd: $cents}
|
|
311
|
+
end
|
|
312
|
+
]'
|
|
313
|
+
)
|
|
314
|
+
printf '\n cost (USD, computed from references/stats-prices.json):\n' >&2
|
|
315
|
+
printf '%s\n' "${cost_json}" | jq -r '.[] | " \(.model): $\(.cost_usd // "n/a")"' >&2
|
|
316
|
+
fi
|
|
317
|
+
|
|
318
|
+
# Pareto frontier — composite efficiency score.
|
|
319
|
+
if [ "${pareto}" = "1" ]; then
|
|
320
|
+
printf '\nRoute Pareto (success_rate × 1/(1+log10(1+avg_kb))):\n' >&2
|
|
321
|
+
sqlite3 -separator $'\t' "${STATS_DB}" "
|
|
322
|
+
SELECT adapter_route,
|
|
323
|
+
ROUND(1.0*SUM(CASE WHEN outcome='success' THEN 1 ELSE 0 END)/COUNT(*),3) AS sr,
|
|
324
|
+
CAST(AVG(stdout_bytes)/1024.0 AS REAL) AS kb,
|
|
325
|
+
ROUND(
|
|
326
|
+
(1.0*SUM(CASE WHEN outcome='success' THEN 1 ELSE 0 END)/COUNT(*))
|
|
327
|
+
/ (1.0 + 0.4343*LN(1.0 + AVG(stdout_bytes)/1024.0))
|
|
328
|
+
,3) AS efficiency
|
|
329
|
+
FROM stats_events ${where}
|
|
330
|
+
GROUP BY adapter_route ORDER BY efficiency DESC;" \
|
|
331
|
+
| awk -F'\t' 'BEGIN{printf " %-22s %8s %10s %12s\n","route","sr","avg_kb","efficiency"}
|
|
332
|
+
{printf " %-22s %8s %10.2f %12s\n",$1,$2,$3,$4}' >&2
|
|
333
|
+
fi
|
|
334
|
+
|
|
335
|
+
emit_summary verb=stats tool=none why=report status=ok events="${total}" days="${days}"
|
|
336
|
+
}
|
|
337
|
+
|
|
338
|
+
# stats_mark — record a user override for one span_id.
|
|
339
|
+
stats_mark() {
|
|
340
|
+
stats_db_init
|
|
341
|
+
local span="${1:-}"; local verdict_full="${2:-}"
|
|
342
|
+
[ -n "${span}" ] && [ -n "${verdict_full}" ] \
|
|
343
|
+
|| die "${EXIT_USAGE_ERROR}" "mark: usage: browser-stats mark <span_id> <success|fail[:reason]>"
|
|
344
|
+
local verdict reason
|
|
345
|
+
verdict="${verdict_full%%:*}"
|
|
346
|
+
reason="${verdict_full#*:}"
|
|
347
|
+
[ "${reason}" = "${verdict}" ] && reason=""
|
|
348
|
+
case "${verdict}" in
|
|
349
|
+
success|fail) ;;
|
|
350
|
+
*) die "${EXIT_USAGE_ERROR}" "mark: verdict must be 'success' or 'fail' (got '${verdict}')" ;;
|
|
351
|
+
esac
|
|
352
|
+
# Confirm span exists.
|
|
353
|
+
local found
|
|
354
|
+
found=$(sqlite3 "${STATS_DB}" "SELECT COUNT(*) FROM stats_events WHERE span_id='${span//\'/\'\'}';")
|
|
355
|
+
if [ "${found}" = "0" ]; then
|
|
356
|
+
warn "mark: span_id '${span}' not found in stats_events (override recorded anyway; will apply on rebuild)"
|
|
357
|
+
fi
|
|
358
|
+
sqlite3 "${STATS_DB}" "
|
|
359
|
+
INSERT INTO stats_overrides(span_id, verdict, reason, marked_at)
|
|
360
|
+
VALUES('${span//\'/\'\'}', '${verdict}', '${reason//\'/\'\'}', datetime('now'))
|
|
361
|
+
ON CONFLICT(span_id) DO UPDATE SET verdict=excluded.verdict, reason=excluded.reason, marked_at=excluded.marked_at;"
|
|
362
|
+
ok "stats: override recorded — span=${span} verdict=${verdict} reason=${reason:-(none)}"
|
|
363
|
+
emit_summary verb=stats tool=none why=mark status=ok span_id="${span}" verdict="${verdict}"
|
|
364
|
+
}
|
|
365
|
+
|
|
366
|
+
# stats_tune — surface a candidate verb for /autoresearch handoff.
|
|
367
|
+
stats_tune() {
|
|
368
|
+
require_sqlite3
|
|
369
|
+
stats_rebuild >/dev/null 2>&1 || true
|
|
370
|
+
local days=30 route_filter=""
|
|
371
|
+
while [ "$#" -gt 0 ]; do
|
|
372
|
+
case "$1" in
|
|
373
|
+
--days) days="$2"; shift 2 ;;
|
|
374
|
+
--route) route_filter="$2"; shift 2 ;;
|
|
375
|
+
*) die "${EXIT_USAGE_ERROR}" "tune: unknown flag '$1'" ;;
|
|
376
|
+
esac
|
|
377
|
+
done
|
|
378
|
+
local where="WHERE ts >= datetime('now', '-${days} days')"
|
|
379
|
+
[ -n "${route_filter}" ] && where="${where} AND adapter_route='${route_filter}'"
|
|
380
|
+
|
|
381
|
+
printf '\n=== browser-stats tune (last %s day(s)) ===\n\n' "${days}" >&2
|
|
382
|
+
printf 'Worst-performing (verb,route) by success rate (min 10 events):\n' >&2
|
|
383
|
+
sqlite3 -separator $'\t' "${STATS_DB}" "
|
|
384
|
+
SELECT verb, adapter_route,
|
|
385
|
+
COUNT(*) AS n,
|
|
386
|
+
ROUND(1.0*SUM(CASE WHEN outcome='success' THEN 1 ELSE 0 END)/COUNT(*),3) AS sr,
|
|
387
|
+
CAST(AVG(duration_ms) AS INTEGER) AS avg_ms
|
|
388
|
+
FROM stats_events ${where}
|
|
389
|
+
GROUP BY verb, adapter_route
|
|
390
|
+
HAVING n >= 10
|
|
391
|
+
ORDER BY sr ASC, avg_ms DESC LIMIT 5;" \
|
|
392
|
+
| awk -F'\t' 'BEGIN{printf " %-12s %-22s %6s %8s %10s\n","verb","route","n","sr","avg_ms"}
|
|
393
|
+
{printf " %-12s %-22s %6s %8s %10s\n",$1,$2,$3,$4,$5}' >&2
|
|
394
|
+
|
|
395
|
+
printf '\nHand-off recipe (human-in-loop):\n' >&2
|
|
396
|
+
printf ' 1. Pick a (verb,route) row above with low success rate.\n' >&2
|
|
397
|
+
printf ' 2. Invoke /autoresearch with that verb as the optimization target.\n' >&2
|
|
398
|
+
printf ' 3. autoresearch reads stats_events to derive eval cases automatically.\n' >&2
|
|
399
|
+
printf ' 4. Review proposed mutation; apply by hand. No auto-merge.\n' >&2
|
|
400
|
+
|
|
401
|
+
emit_summary verb=stats tool=none why=tune status=ok days="${days}"
|
|
402
|
+
}
|
|
403
|
+
|
|
404
|
+
# stats_prune — close the telemetry feedback loop (Phase 14+).
|
|
405
|
+
#
|
|
406
|
+
# Find (site, selector) tuples with ≥THRESHOLD oblivious_success events in
|
|
407
|
+
# the last --days days. Each such tuple = "cache lied" repeatedly: adapter
|
|
408
|
+
# said ok but post-condition failed. The interaction's cached selector is
|
|
409
|
+
# semantically broken (pointing at the wrong element, or page redesigned in
|
|
410
|
+
# a way Phase-13 + Path 3 can't rescue).
|
|
411
|
+
#
|
|
412
|
+
# Modes:
|
|
413
|
+
# default (advisory): list candidates as NDJSON _kind:prune_candidate
|
|
414
|
+
# lines; summary reports count. No mutation.
|
|
415
|
+
# --apply : mark each candidate interaction .disabled=true in
|
|
416
|
+
# its archetype JSON; emits _kind:prune_applied. The
|
|
417
|
+
# disabled marker is the same one Phase 11 self-heal
|
|
418
|
+
# sets after 4 plain failures; cache lookups skip
|
|
419
|
+
# disabled interactions, so the cloud-LLM path takes
|
|
420
|
+
# over on the next call.
|
|
421
|
+
#
|
|
422
|
+
# Why pruning matters: without this, cache pollution accumulates silently
|
|
423
|
+
# over time. Phase 12 telemetry (oblivious_success) was the read side;
|
|
424
|
+
# this is the write side that closes the loop.
|
|
425
|
+
stats_prune() {
|
|
426
|
+
require_sqlite3
|
|
427
|
+
stats_rebuild >/dev/null 2>&1 || true
|
|
428
|
+
local days=7 threshold=3 apply=0 site_filter=""
|
|
429
|
+
while [ "$#" -gt 0 ]; do
|
|
430
|
+
case "$1" in
|
|
431
|
+
--days) days="$2"; shift 2 ;;
|
|
432
|
+
--threshold) threshold="$2"; shift 2 ;;
|
|
433
|
+
--apply) apply=1; shift ;;
|
|
434
|
+
--site) site_filter="$2"; shift 2 ;;
|
|
435
|
+
-h|--help)
|
|
436
|
+
cat <<'PRUNEUSAGE' >&2
|
|
437
|
+
browser-stats prune [--days N] [--threshold N] [--apply] [--site NAME]
|
|
438
|
+
|
|
439
|
+
Find cache archetype interactions where the cached selector has caused
|
|
440
|
+
≥THRESHOLD oblivious_success events in the last --days days (default
|
|
441
|
+
--days 7, --threshold 3). Adapter said ok but post-condition failed
|
|
442
|
+
— a strong "cache is wrong" signal that Phase-13 + Path 3 couldn't
|
|
443
|
+
heal. Dry-run by default: emits _kind:prune_candidate lines. With
|
|
444
|
+
--apply, marks each matching interaction .disabled=true in its
|
|
445
|
+
archetype JSON (lookups skip disabled → cloud-LLM path runs instead).
|
|
446
|
+
PRUNEUSAGE
|
|
447
|
+
return 0
|
|
448
|
+
;;
|
|
449
|
+
*) die "${EXIT_USAGE_ERROR}" "prune: unknown flag '$1'" ;;
|
|
450
|
+
esac
|
|
451
|
+
done
|
|
452
|
+
local where="WHERE failure_mode='oblivious_success' AND ts >= datetime('now', '-${days} days') AND site IS NOT NULL AND selector_value IS NOT NULL"
|
|
453
|
+
[ -n "${site_filter}" ] && where="${where} AND site='${site_filter}'"
|
|
454
|
+
|
|
455
|
+
local candidates
|
|
456
|
+
candidates="$(sqlite3 -separator $'\t' "${STATS_DB}" "
|
|
457
|
+
SELECT site, selector_value, COUNT(*) AS n
|
|
458
|
+
FROM stats_events ${where}
|
|
459
|
+
GROUP BY site, selector_value
|
|
460
|
+
HAVING n >= ${threshold}
|
|
461
|
+
ORDER BY n DESC;" 2>/dev/null)"
|
|
462
|
+
|
|
463
|
+
local candidate_count=0 applied_count=0
|
|
464
|
+
while IFS=$'\t' read -r site sel n; do
|
|
465
|
+
[ -z "${site}" ] && continue
|
|
466
|
+
[ -z "${sel}" ] && continue
|
|
467
|
+
local arch_dir="${BROWSER_SKILL_HOME}/memory/${site}/archetypes"
|
|
468
|
+
[ -d "${arch_dir}" ] || continue
|
|
469
|
+
local arch_file
|
|
470
|
+
for arch_file in "${arch_dir}"/*.json; do
|
|
471
|
+
[ -f "${arch_file}" ] || continue
|
|
472
|
+
local arch_id intent
|
|
473
|
+
arch_id="$(basename "${arch_file}" .json)"
|
|
474
|
+
intent="$(jq -r --arg sel "${sel}" \
|
|
475
|
+
'.interactions[]? | select(.selector == $sel) | .intent' \
|
|
476
|
+
"${arch_file}" 2>/dev/null | head -1)"
|
|
477
|
+
if [ -n "${intent}" ]; then
|
|
478
|
+
candidate_count=$((candidate_count + 1))
|
|
479
|
+
jq -nc \
|
|
480
|
+
--arg site "${site}" --arg sel "${sel}" \
|
|
481
|
+
--arg arch_id "${arch_id}" --arg intent "${intent}" \
|
|
482
|
+
--argjson n "${n}" \
|
|
483
|
+
'{_kind:"prune_candidate", site:$site, selector:$sel,
|
|
484
|
+
oblivious_success_count:$n, archetype_id:$arch_id, intent:$intent}'
|
|
485
|
+
if [ "${apply}" = "1" ]; then
|
|
486
|
+
local tmp
|
|
487
|
+
tmp="$(mktemp)"
|
|
488
|
+
jq --arg sel "${sel}" \
|
|
489
|
+
'(.interactions[] | select(.selector == $sel)).disabled = true' \
|
|
490
|
+
"${arch_file}" > "${tmp}" \
|
|
491
|
+
&& mv "${tmp}" "${arch_file}" \
|
|
492
|
+
&& chmod 600 "${arch_file}" \
|
|
493
|
+
&& applied_count=$((applied_count + 1))
|
|
494
|
+
jq -nc \
|
|
495
|
+
--arg site "${site}" --arg sel "${sel}" \
|
|
496
|
+
--arg arch_id "${arch_id}" --arg intent "${intent}" \
|
|
497
|
+
'{_kind:"prune_applied", site:$site, selector:$sel,
|
|
498
|
+
archetype_id:$arch_id, intent:$intent}'
|
|
499
|
+
fi
|
|
500
|
+
break
|
|
501
|
+
fi
|
|
502
|
+
done
|
|
503
|
+
done <<< "${candidates}"
|
|
504
|
+
|
|
505
|
+
emit_summary verb=stats tool=none why=prune status=ok \
|
|
506
|
+
days="${days}" threshold="${threshold}" \
|
|
507
|
+
candidates="${candidate_count}" applied="${applied_count}"
|
|
508
|
+
}
|
|
509
|
+
|
|
510
|
+
case "${subcmd}" in
|
|
511
|
+
rebuild) stats_rebuild "$@" ;;
|
|
512
|
+
report) stats_report "$@" ;;
|
|
513
|
+
mark) stats_mark "$@" ;;
|
|
514
|
+
tune) stats_tune "$@" ;;
|
|
515
|
+
prune) stats_prune "$@" ;;
|
|
516
|
+
event)
|
|
517
|
+
# Internal use — adapters normally call lib/stats.sh helpers directly.
|
|
518
|
+
# Exposed via CLI for debugging and for tests.
|
|
519
|
+
die "${EXIT_USAGE_ERROR}" "browser-stats event: reserved for in-process callers (use lib/stats.sh::stats_run_adapter_emit)"
|
|
520
|
+
;;
|
|
521
|
+
*) die "${EXIT_USAGE_ERROR}" "browser-stats: unknown subcommand '${subcmd}' (expected: rebuild|report|mark|tune|prune)" ;;
|
|
522
|
+
esac
|
|
@@ -0,0 +1,112 @@
|
|
|
1
|
+
#!/usr/bin/env bash
|
|
2
|
+
# scripts/browser-tab-close.sh — close a tab in the daemon-held browser.
|
|
3
|
+
# Usage: bash scripts/browser-tab-close.sh [--site NAME] [--tool NAME]
|
|
4
|
+
# [--dry-run] [--raw]
|
|
5
|
+
# (--tab-id N | --by-url-pattern STR)
|
|
6
|
+
#
|
|
7
|
+
# Routes to chrome-devtools-mcp by default (Phase 6 part 8-iii). Daemon-required.
|
|
8
|
+
# Splices the matching entry from the daemon's tabs[] cache + asks upstream MCP
|
|
9
|
+
# to close the page. If the closed tab matches `currentTab`, the pointer is
|
|
10
|
+
# nulled (`current_tab_id: null` in subsequent tab-list output).
|
|
11
|
+
#
|
|
12
|
+
# Mutex: exactly one of --tab-id (canonical 1-based id from tab-list output) or
|
|
13
|
+
# --by-url-pattern (substring-contains, first-match-wins, mirrors tab-switch).
|
|
14
|
+
#
|
|
15
|
+
# Why --tab-id instead of --by-index: by the time agents reach tab-close, they
|
|
16
|
+
# already hold a tab_id from tab-list. Positional indexing would drift across
|
|
17
|
+
# successive closes; canonical id is unambiguous.
|
|
18
|
+
|
|
19
|
+
set -euo pipefail
|
|
20
|
+
IFS=$'\n\t'
|
|
21
|
+
|
|
22
|
+
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
|
23
|
+
# shellcheck source=lib/common.sh
|
|
24
|
+
# shellcheck disable=SC1091
|
|
25
|
+
source "${SCRIPT_DIR}/lib/common.sh"
|
|
26
|
+
# shellcheck source=lib/output.sh
|
|
27
|
+
# shellcheck disable=SC1091
|
|
28
|
+
source "${SCRIPT_DIR}/lib/output.sh"
|
|
29
|
+
# shellcheck source=lib/router.sh
|
|
30
|
+
# shellcheck disable=SC1091
|
|
31
|
+
source "${SCRIPT_DIR}/lib/router.sh"
|
|
32
|
+
# shellcheck source=lib/verb_helpers.sh
|
|
33
|
+
# shellcheck disable=SC1091
|
|
34
|
+
source "${SCRIPT_DIR}/lib/verb_helpers.sh"
|
|
35
|
+
|
|
36
|
+
init_paths
|
|
37
|
+
|
|
38
|
+
SUMMARY_T0="$(now_ms)"; export SUMMARY_T0
|
|
39
|
+
|
|
40
|
+
parse_verb_globals "$@"
|
|
41
|
+
|
|
42
|
+
resolve_session_storage_state
|
|
43
|
+
|
|
44
|
+
tab_id="" by_url_pattern=""
|
|
45
|
+
verb_argv=()
|
|
46
|
+
i=0
|
|
47
|
+
while [ "${i}" -lt "${#REMAINING_ARGV[@]}" ]; do
|
|
48
|
+
case "${REMAINING_ARGV[i]}" in
|
|
49
|
+
--tab-id)
|
|
50
|
+
tab_id="${REMAINING_ARGV[i+1]:-}"
|
|
51
|
+
[ -n "${tab_id}" ] || die "${EXIT_USAGE_ERROR}" "--tab-id requires a value"
|
|
52
|
+
verb_argv+=(--tab-id "${tab_id}")
|
|
53
|
+
i=$((i + 2))
|
|
54
|
+
;;
|
|
55
|
+
--by-url-pattern)
|
|
56
|
+
by_url_pattern="${REMAINING_ARGV[i+1]:-}"
|
|
57
|
+
[ -n "${by_url_pattern}" ] || die "${EXIT_USAGE_ERROR}" "--by-url-pattern requires a value"
|
|
58
|
+
verb_argv+=(--by-url-pattern "${by_url_pattern}")
|
|
59
|
+
i=$((i + 2))
|
|
60
|
+
;;
|
|
61
|
+
*)
|
|
62
|
+
verb_argv+=("${REMAINING_ARGV[i]}")
|
|
63
|
+
i=$((i + 1))
|
|
64
|
+
;;
|
|
65
|
+
esac
|
|
66
|
+
done
|
|
67
|
+
|
|
68
|
+
# Mutex + presence: exactly one selector required.
|
|
69
|
+
if [ -n "${tab_id}" ] && [ -n "${by_url_pattern}" ]; then
|
|
70
|
+
die "${EXIT_USAGE_ERROR}" "--tab-id and --by-url-pattern are mutually exclusive"
|
|
71
|
+
fi
|
|
72
|
+
if [ -z "${tab_id}" ] && [ -z "${by_url_pattern}" ]; then
|
|
73
|
+
die "${EXIT_USAGE_ERROR}" "tab-close requires exactly one of --tab-id N or --by-url-pattern STR"
|
|
74
|
+
fi
|
|
75
|
+
|
|
76
|
+
# Validate tab_id is a positive integer (1-based).
|
|
77
|
+
if [ -n "${tab_id}" ]; then
|
|
78
|
+
if ! printf '%s' "${tab_id}" | grep -Eq '^[1-9][0-9]*$'; then
|
|
79
|
+
die "${EXIT_USAGE_ERROR}" "--tab-id must be a positive integer (1-based); got: ${tab_id}"
|
|
80
|
+
fi
|
|
81
|
+
fi
|
|
82
|
+
|
|
83
|
+
if [ "${ARG_DRY_RUN:-0}" = "1" ]; then
|
|
84
|
+
if [ -n "${tab_id}" ]; then
|
|
85
|
+
ok "dry-run: would close tab #${tab_id}"
|
|
86
|
+
emit_summary verb=tab-close tool=none why=dry-run status=ok tab_id="${tab_id}" dry_run=true
|
|
87
|
+
else
|
|
88
|
+
ok "dry-run: would close first tab matching ${by_url_pattern}"
|
|
89
|
+
emit_summary verb=tab-close tool=none why=dry-run status=ok by_url_pattern="${by_url_pattern}" dry_run=true
|
|
90
|
+
fi
|
|
91
|
+
exit 0
|
|
92
|
+
fi
|
|
93
|
+
|
|
94
|
+
picked="$(pick_tool tab-close "${verb_argv[@]}")"
|
|
95
|
+
tool_name="${picked%%$'\t'*}"
|
|
96
|
+
why="${picked#*$'\t'}"
|
|
97
|
+
|
|
98
|
+
source_picked_adapter "${tool_name}"
|
|
99
|
+
|
|
100
|
+
set +e
|
|
101
|
+
adapter_out="$(invoke_with_retry tab-close "${verb_argv[@]}")"
|
|
102
|
+
adapter_rc=$?
|
|
103
|
+
set -e
|
|
104
|
+
|
|
105
|
+
[ -n "${adapter_out}" ] && printf '%s\n' "${adapter_out}"
|
|
106
|
+
|
|
107
|
+
if [ "${adapter_rc}" -eq 0 ]; then
|
|
108
|
+
emit_summary verb=tab-close tool="${tool_name}" why="${why}" status=ok
|
|
109
|
+
exit 0
|
|
110
|
+
fi
|
|
111
|
+
emit_summary verb=tab-close tool="${tool_name}" why="${why}" status=error
|
|
112
|
+
exit "${adapter_rc}"
|