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,142 @@
|
|
|
1
|
+
#!/usr/bin/env bash
|
|
2
|
+
# scripts/browser-fill.sh — fill an input by --ref eN or --selector CSS with --text or --secret-stdin.
|
|
3
|
+
# Usage: bash scripts/browser-fill.sh [--site NAME] [--tool NAME] [--dry-run]
|
|
4
|
+
# [--raw] (--ref eN | --selector CSS)
|
|
5
|
+
# (--text VALUE | --secret-stdin)
|
|
6
|
+
#
|
|
7
|
+
# CRITICAL: --secret-stdin reads the secret from this script's stdin and pipes
|
|
8
|
+
# it to the adapter; the secret never appears on argv (anti-pattern AP-7).
|
|
9
|
+
# Test: tests/browser-fill.bats::secret-not-in-argv.
|
|
10
|
+
#
|
|
11
|
+
# --selector path enables Phase 11 cache dispatch (cache stores selectors,
|
|
12
|
+
# not snapshot-relative refs). Mirrors browser-click.sh's --ref/--selector
|
|
13
|
+
# precedent.
|
|
14
|
+
|
|
15
|
+
set -euo pipefail
|
|
16
|
+
IFS=$'\n\t'
|
|
17
|
+
|
|
18
|
+
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
|
19
|
+
# shellcheck source=lib/common.sh
|
|
20
|
+
# shellcheck disable=SC1091
|
|
21
|
+
source "${SCRIPT_DIR}/lib/common.sh"
|
|
22
|
+
# shellcheck source=lib/output.sh
|
|
23
|
+
# shellcheck disable=SC1091
|
|
24
|
+
source "${SCRIPT_DIR}/lib/output.sh"
|
|
25
|
+
# shellcheck source=lib/router.sh
|
|
26
|
+
# shellcheck disable=SC1091
|
|
27
|
+
source "${SCRIPT_DIR}/lib/router.sh"
|
|
28
|
+
# shellcheck source=lib/verb_helpers.sh
|
|
29
|
+
# shellcheck disable=SC1091
|
|
30
|
+
source "${SCRIPT_DIR}/lib/verb_helpers.sh"
|
|
31
|
+
# shellcheck source=lib/stats.sh
|
|
32
|
+
# shellcheck disable=SC1091
|
|
33
|
+
source "${SCRIPT_DIR}/lib/stats.sh"
|
|
34
|
+
|
|
35
|
+
init_paths
|
|
36
|
+
|
|
37
|
+
SUMMARY_T0="$(now_ms)"; export SUMMARY_T0
|
|
38
|
+
|
|
39
|
+
parse_verb_globals "$@"
|
|
40
|
+
|
|
41
|
+
# Resolve site/session → BROWSER_SKILL_STORAGE_STATE (no-op if neither set).
|
|
42
|
+
# Router's rule_session_required reads the env var to prefer playwright-lib
|
|
43
|
+
# (which natively supports --secret-stdin via stdin-pipe to driver).
|
|
44
|
+
resolve_session_storage_state
|
|
45
|
+
|
|
46
|
+
ref="" selector="" text="" use_stdin=0
|
|
47
|
+
verb_argv=()
|
|
48
|
+
i=0
|
|
49
|
+
while [ "${i}" -lt "${#REMAINING_ARGV[@]}" ]; do
|
|
50
|
+
case "${REMAINING_ARGV[i]}" in
|
|
51
|
+
--ref)
|
|
52
|
+
ref="${REMAINING_ARGV[i+1]:-}"
|
|
53
|
+
[ -n "${ref}" ] || die "${EXIT_USAGE_ERROR}" "--ref requires a value"
|
|
54
|
+
verb_argv+=(--ref "${ref}")
|
|
55
|
+
i=$((i + 2))
|
|
56
|
+
;;
|
|
57
|
+
--selector)
|
|
58
|
+
selector="${REMAINING_ARGV[i+1]:-}"
|
|
59
|
+
[ -n "${selector}" ] || die "${EXIT_USAGE_ERROR}" "--selector requires a value"
|
|
60
|
+
verb_argv+=(--selector "${selector}")
|
|
61
|
+
i=$((i + 2))
|
|
62
|
+
;;
|
|
63
|
+
--text)
|
|
64
|
+
text="${REMAINING_ARGV[i+1]:-}"
|
|
65
|
+
[ -n "${text}" ] || die "${EXIT_USAGE_ERROR}" "--text requires a value"
|
|
66
|
+
verb_argv+=(--text "${text}")
|
|
67
|
+
i=$((i + 2))
|
|
68
|
+
;;
|
|
69
|
+
--secret-stdin)
|
|
70
|
+
use_stdin=1
|
|
71
|
+
verb_argv+=(--secret-stdin)
|
|
72
|
+
i=$((i + 1))
|
|
73
|
+
;;
|
|
74
|
+
*)
|
|
75
|
+
verb_argv+=("${REMAINING_ARGV[i]}")
|
|
76
|
+
i=$((i + 1))
|
|
77
|
+
;;
|
|
78
|
+
esac
|
|
79
|
+
done
|
|
80
|
+
|
|
81
|
+
if [ -n "${ref}" ] && [ -n "${selector}" ]; then
|
|
82
|
+
die "${EXIT_USAGE_ERROR}" "--ref and --selector are mutually exclusive"
|
|
83
|
+
fi
|
|
84
|
+
if [ -z "${ref}" ] && [ -z "${selector}" ]; then
|
|
85
|
+
die "${EXIT_USAGE_ERROR}" "fill requires --ref eN or --selector CSS"
|
|
86
|
+
fi
|
|
87
|
+
if [ -n "${text}" ] && [ "${use_stdin}" = "1" ]; then
|
|
88
|
+
die "${EXIT_USAGE_ERROR}" "--text and --secret-stdin are mutually exclusive"
|
|
89
|
+
fi
|
|
90
|
+
if [ -z "${text}" ] && [ "${use_stdin}" = "0" ]; then
|
|
91
|
+
die "${EXIT_USAGE_ERROR}" "fill requires --text VALUE or --secret-stdin"
|
|
92
|
+
fi
|
|
93
|
+
|
|
94
|
+
if [ "${ARG_DRY_RUN:-0}" = "1" ]; then
|
|
95
|
+
ok "dry-run: would fill ${ref:-${selector}}"
|
|
96
|
+
emit_summary verb=fill tool=none why=dry-run status=ok ref="${ref}" selector="${selector}" dry_run=true
|
|
97
|
+
exit 0
|
|
98
|
+
fi
|
|
99
|
+
|
|
100
|
+
picked="$(pick_tool fill "${verb_argv[@]}")"
|
|
101
|
+
tool_name="${picked%%$'\t'*}"
|
|
102
|
+
why="${picked#*$'\t'}"
|
|
103
|
+
|
|
104
|
+
source_picked_adapter "${tool_name}"
|
|
105
|
+
|
|
106
|
+
# stdin (if --secret-stdin) flows through to tool_fill -> adapter binary.
|
|
107
|
+
# Capture stdout in subshell; stdin inherits naturally.
|
|
108
|
+
stats_t0="$(now_ms)"
|
|
109
|
+
set +e
|
|
110
|
+
adapter_out="$(invoke_with_retry fill "${verb_argv[@]}")"
|
|
111
|
+
adapter_rc=$?
|
|
112
|
+
set -e
|
|
113
|
+
|
|
114
|
+
# Phase 12 part 1 + Phase 14 (Bundle #2): per-action telemetry. CRITICAL — when
|
|
115
|
+
# --secret-stdin was used, NEVER auto-derive EXPECT_VALUE from the secret
|
|
116
|
+
# (would leak the secret into stats.jsonl per AP-7). Auto-derive only fires when
|
|
117
|
+
# (a) --text was used (no secrets) AND (b) BROWSER_SKILL_STRICT_POSTCOND=1
|
|
118
|
+
# opts in. Opt-in default: many fill adapters don't echo the typed value back,
|
|
119
|
+
# so a blanket auto-check would generate false oblivious_success events.
|
|
120
|
+
# Future: compose with a follow-up snapshot to read the actual element value.
|
|
121
|
+
if [ "${BROWSER_SKILL_STRICT_POSTCOND:-0}" = "1" ] \
|
|
122
|
+
&& [ "${use_stdin}" = "0" ] && [ -n "${text}" ] \
|
|
123
|
+
&& [ "${adapter_rc}" -eq 0 ]; then
|
|
124
|
+
: "${BROWSER_STATS_EXPECT_TYPE:=element_value}"
|
|
125
|
+
: "${BROWSER_STATS_EXPECT_MATCH:=include}"
|
|
126
|
+
: "${BROWSER_STATS_EXPECT_VALUE:=${text}}"
|
|
127
|
+
fi
|
|
128
|
+
: "${BROWSER_STATS_OBSERVED:=${adapter_out}}"
|
|
129
|
+
export BROWSER_STATS_EXPECT_TYPE BROWSER_STATS_EXPECT_MATCH BROWSER_STATS_EXPECT_VALUE BROWSER_STATS_OBSERVED
|
|
130
|
+
|
|
131
|
+
stats_run_adapter_emit \
|
|
132
|
+
"fill" "${tool_name}" "${stats_t0}" "${adapter_rc}" "${adapter_out}" "" \
|
|
133
|
+
-- "${verb_argv[@]}" || true
|
|
134
|
+
|
|
135
|
+
[ -n "${adapter_out}" ] && printf '%s\n' "${adapter_out}"
|
|
136
|
+
|
|
137
|
+
if [ "${adapter_rc}" -eq 0 ]; then
|
|
138
|
+
emit_summary verb=fill tool="${tool_name}" why="${why}" status=ok ref="${ref}" selector="${selector}"
|
|
139
|
+
exit 0
|
|
140
|
+
fi
|
|
141
|
+
emit_summary verb=fill tool="${tool_name}" why="${why}" status=error ref="${ref}" selector="${selector}"
|
|
142
|
+
exit "${adapter_rc}"
|
|
@@ -0,0 +1,316 @@
|
|
|
1
|
+
#!/usr/bin/env bash
|
|
2
|
+
# scripts/browser-flow.sh — flow runner verb (Phase 9 part 1-i).
|
|
3
|
+
#
|
|
4
|
+
# Usage:
|
|
5
|
+
# bash scripts/browser-flow.sh run <flow-file> [--var key=val ...] [--dry-run]
|
|
6
|
+
#
|
|
7
|
+
# Sub-modes (current):
|
|
8
|
+
# run — execute a .flow.yaml file end-to-end (this PR)
|
|
9
|
+
# Sub-modes (planned):
|
|
10
|
+
# record — wrap `playwright codegen` (9-1-iii)
|
|
11
|
+
#
|
|
12
|
+
# Capture composition (per design doc 2026-05-10-phase-09-flow-runner-design §3 F4):
|
|
13
|
+
# one capture per flow run; per-step events streamed to ${CAPTURE_DIR}/steps.jsonl;
|
|
14
|
+
# meta.json carries verb=flow + flow_name + step_count + successful_steps +
|
|
15
|
+
# failed_steps + status (ok / partial / error).
|
|
16
|
+
|
|
17
|
+
set -euo pipefail
|
|
18
|
+
IFS=$'\n\t'
|
|
19
|
+
|
|
20
|
+
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
|
21
|
+
SCRIPTS_DIR="${SCRIPT_DIR}"
|
|
22
|
+
export SCRIPTS_DIR
|
|
23
|
+
|
|
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/capture.sh
|
|
31
|
+
# shellcheck disable=SC1091
|
|
32
|
+
source "${SCRIPT_DIR}/lib/capture.sh"
|
|
33
|
+
# shellcheck source=lib/flow.sh
|
|
34
|
+
# shellcheck disable=SC1091
|
|
35
|
+
source "${SCRIPT_DIR}/lib/flow.sh"
|
|
36
|
+
|
|
37
|
+
init_paths
|
|
38
|
+
|
|
39
|
+
SUMMARY_T0="$(now_ms)"; export SUMMARY_T0
|
|
40
|
+
|
|
41
|
+
sub_mode="${1:-}"
|
|
42
|
+
[ -n "${sub_mode}" ] || die "${EXIT_USAGE_ERROR}" "browser-flow: missing sub-mode (use 'run')"
|
|
43
|
+
shift
|
|
44
|
+
|
|
45
|
+
case "${sub_mode}" in
|
|
46
|
+
run) ;;
|
|
47
|
+
record)
|
|
48
|
+
# Phase 9 part 1-iii: wraps `playwright codegen <url>`; transforms emitted
|
|
49
|
+
# JS → flow YAML; writes ${OUT} mode 0600. Privacy canary on recorder
|
|
50
|
+
# write side: passwords detected via /password/i name match, replaced
|
|
51
|
+
# with ${secrets.password} placeholder.
|
|
52
|
+
# shellcheck source=lib/flow_record.sh
|
|
53
|
+
# shellcheck disable=SC1091
|
|
54
|
+
source "${SCRIPT_DIR}/lib/flow_record.sh"
|
|
55
|
+
|
|
56
|
+
record_url=""
|
|
57
|
+
record_out=""
|
|
58
|
+
record_name=""
|
|
59
|
+
record_tool=""
|
|
60
|
+
while [ "$#" -gt 0 ]; do
|
|
61
|
+
case "$1" in
|
|
62
|
+
--url) record_url="$2"; shift 2 ;;
|
|
63
|
+
--out) record_out="$2"; shift 2 ;;
|
|
64
|
+
--name) record_name="$2"; shift 2 ;;
|
|
65
|
+
--tool) record_tool="$2"; shift 2 ;;
|
|
66
|
+
--site) shift 2 ;; # accepted; site resolution deferred
|
|
67
|
+
*) die "${EXIT_USAGE_ERROR}" "browser-flow record: unknown flag '$1'" ;;
|
|
68
|
+
esac
|
|
69
|
+
done
|
|
70
|
+
|
|
71
|
+
# Per locked decision W1: codegen targets Playwright/Chrome; obscura's
|
|
72
|
+
# stateless one-shot model has no interactive recording surface.
|
|
73
|
+
if [ "${record_tool}" = "obscura" ]; then
|
|
74
|
+
die "${EXIT_USAGE_ERROR}" "browser-flow record: recorder does not support obscura (codegen targets Playwright; obscura is stateless one-shot — no interactive recording surface)"
|
|
75
|
+
fi
|
|
76
|
+
|
|
77
|
+
# Per locked decision O1: --out is REQUIRED.
|
|
78
|
+
[ -n "${record_out}" ] || die "${EXIT_USAGE_ERROR}" "browser-flow record: --out FILE is required"
|
|
79
|
+
[ -n "${record_url}" ] || die "${EXIT_USAGE_ERROR}" "browser-flow record: --url URL is required (or --site NAME — deferred to follow-up)"
|
|
80
|
+
|
|
81
|
+
# Path security: realpath canonicalize + sensitive-pattern reject. Mirror
|
|
82
|
+
# references/recipes/path-security.md.
|
|
83
|
+
record_out_dir="$(dirname "${record_out}")"
|
|
84
|
+
[ -d "${record_out_dir}" ] || mkdir -p "${record_out_dir}"
|
|
85
|
+
record_out_abs="$(cd "${record_out_dir}" && pwd)/$(basename "${record_out}")"
|
|
86
|
+
case "${record_out_abs}" in
|
|
87
|
+
*/.ssh/*|*/.aws/*|*/.gnupg/*|*/.netrc*|*/private_key*|*/id_rsa*|*/id_ed25519*)
|
|
88
|
+
die "${EXIT_USAGE_ERROR}" "browser-flow record: --out path matches sensitive pattern (refusing): ${record_out_abs}"
|
|
89
|
+
;;
|
|
90
|
+
esac
|
|
91
|
+
|
|
92
|
+
# Default flow name = basename of --out (sans .flow.yaml).
|
|
93
|
+
[ -z "${record_name}" ] && record_name="$(basename "${record_out}" .flow.yaml)"
|
|
94
|
+
|
|
95
|
+
# Spawn codegen (or mock via env-var override).
|
|
96
|
+
codegen_bin="${PLAYWRIGHT_CODEGEN_BIN:-}"
|
|
97
|
+
if [ -z "${codegen_bin}" ]; then
|
|
98
|
+
codegen_bin="$(command -v playwright || true)"
|
|
99
|
+
[ -z "${codegen_bin}" ] && die "${EXIT_PREFLIGHT_FAILED}" "playwright not found on PATH (set PLAYWRIGHT_CODEGEN_BIN to override)"
|
|
100
|
+
codegen_args=(codegen --target javascript "${record_url}")
|
|
101
|
+
else
|
|
102
|
+
codegen_args=()
|
|
103
|
+
fi
|
|
104
|
+
|
|
105
|
+
# Capture codegen stdout. Real codegen blocks until user closes the headed
|
|
106
|
+
# window; mock exits immediately.
|
|
107
|
+
set +e
|
|
108
|
+
codegen_js="$("${codegen_bin}" "${codegen_args[@]}" 2>/dev/null)"
|
|
109
|
+
codegen_rc=$?
|
|
110
|
+
set -e
|
|
111
|
+
if [ "${codegen_rc}" -ne 0 ]; then
|
|
112
|
+
die "${EXIT_TOOL_CRASHED}" "playwright codegen failed (rc=${codegen_rc})"
|
|
113
|
+
fi
|
|
114
|
+
|
|
115
|
+
# Transform JS → YAML. flow_record_transform sets globals
|
|
116
|
+
# FLOW_RECORD_PASSWORD_REDACTIONS + FLOW_RECORD_STEP_COUNT.
|
|
117
|
+
yaml_out="$(printf '%s' "${codegen_js}" | flow_record_transform "${record_name}" 2>/tmp/flow-record-stderr-$$.log)"
|
|
118
|
+
redaction_msgs="$(cat /tmp/flow-record-stderr-$$.log 2>/dev/null || true)"
|
|
119
|
+
rm -f /tmp/flow-record-stderr-$$.log
|
|
120
|
+
[ -n "${redaction_msgs}" ] && printf '%s\n' "${redaction_msgs}" >&2
|
|
121
|
+
|
|
122
|
+
# Write to --out, mode 0600.
|
|
123
|
+
tmp="${record_out_abs}.tmp.$$"
|
|
124
|
+
printf '%s' "${yaml_out}" > "${tmp}"
|
|
125
|
+
chmod 600 "${tmp}"
|
|
126
|
+
mv "${tmp}" "${record_out_abs}"
|
|
127
|
+
|
|
128
|
+
emit_summary verb=flow tool=playwright-cli why=record status=ok mode=record \
|
|
129
|
+
flow_name="${record_name}" out_file="${record_out_abs}" \
|
|
130
|
+
step_count="${FLOW_RECORD_STEP_COUNT}" \
|
|
131
|
+
password_redactions="${FLOW_RECORD_PASSWORD_REDACTIONS}"
|
|
132
|
+
exit 0
|
|
133
|
+
;;
|
|
134
|
+
*) die "${EXIT_USAGE_ERROR}" "browser-flow: unknown sub-mode '${sub_mode}'" ;;
|
|
135
|
+
esac
|
|
136
|
+
|
|
137
|
+
flow_file="${1:-}"
|
|
138
|
+
[ -n "${flow_file}" ] || die "${EXIT_USAGE_ERROR}" "browser-flow run: missing <flow-file>"
|
|
139
|
+
shift
|
|
140
|
+
|
|
141
|
+
# Path security: realpath canonicalize. Reject sensitive patterns. Per recipe
|
|
142
|
+
# references/recipes/path-security.md.
|
|
143
|
+
if [ ! -f "${flow_file}" ]; then
|
|
144
|
+
alt="${BROWSER_SKILL_HOME}/flows/${flow_file}"
|
|
145
|
+
if [ -f "${alt}" ]; then
|
|
146
|
+
flow_file="${alt}"
|
|
147
|
+
else
|
|
148
|
+
die "${EXIT_USAGE_ERROR}" "flow file not found: ${flow_file}"
|
|
149
|
+
fi
|
|
150
|
+
fi
|
|
151
|
+
flow_file_abs="$(cd "$(dirname "${flow_file}")" && pwd)/$(basename "${flow_file}")"
|
|
152
|
+
case "${flow_file_abs}" in
|
|
153
|
+
*/.ssh/*|*/.aws/*|*/.gnupg/*|*/.netrc*|*/private_key*|*/id_rsa*|*/id_ed25519*)
|
|
154
|
+
die "${EXIT_USAGE_ERROR}" "flow file path matches sensitive pattern (refusing): ${flow_file_abs}"
|
|
155
|
+
;;
|
|
156
|
+
esac
|
|
157
|
+
|
|
158
|
+
cli_var_overrides=()
|
|
159
|
+
dry_run=0
|
|
160
|
+
while [ "$#" -gt 0 ]; do
|
|
161
|
+
case "$1" in
|
|
162
|
+
--var)
|
|
163
|
+
[ -n "${2:-}" ] || die "${EXIT_USAGE_ERROR}" "--var requires key=val"
|
|
164
|
+
cli_var_overrides+=("$2")
|
|
165
|
+
shift 2
|
|
166
|
+
;;
|
|
167
|
+
--dry-run)
|
|
168
|
+
dry_run=1
|
|
169
|
+
shift
|
|
170
|
+
;;
|
|
171
|
+
*)
|
|
172
|
+
die "${EXIT_USAGE_ERROR}" "browser-flow run: unknown flag '$1'"
|
|
173
|
+
;;
|
|
174
|
+
esac
|
|
175
|
+
done
|
|
176
|
+
|
|
177
|
+
# Reset flow state in this shell.
|
|
178
|
+
declare -gA FLOW_VARS=()
|
|
179
|
+
declare -gA FLOW_REFS=()
|
|
180
|
+
FLOW_NAME=""
|
|
181
|
+
FLOW_SESSION=""
|
|
182
|
+
|
|
183
|
+
# Parse the flow file → captures _meta line + per-step lines on stdout.
|
|
184
|
+
parsed="$(flow_parse "${flow_file_abs}")"
|
|
185
|
+
|
|
186
|
+
# Extract _meta line (first one with _kind=="meta").
|
|
187
|
+
meta_line="$(printf '%s\n' "${parsed}" | jq -c -s 'map(select(._kind=="meta")) | .[0]' 2>/dev/null || printf 'null')"
|
|
188
|
+
[ "${meta_line}" = "null" ] && die "${EXIT_GENERIC_ERROR}" "flow_parse: missing _meta line in output"
|
|
189
|
+
FLOW_NAME="$(printf '%s' "${meta_line}" | jq -r '.name')"
|
|
190
|
+
FLOW_SESSION="$(printf '%s' "${meta_line}" | jq -r '.session // ""')"
|
|
191
|
+
|
|
192
|
+
# Hydrate FLOW_VARS from _meta.vars (file-defined defaults).
|
|
193
|
+
while IFS=$'\t' read -r k v; do
|
|
194
|
+
[ -z "${k}" ] && continue
|
|
195
|
+
FLOW_VARS["${k}"]="${v}"
|
|
196
|
+
done <<< "$(printf '%s' "${meta_line}" | jq -r '.vars | to_entries[]? | "\(.key)\t\(.value)"')"
|
|
197
|
+
|
|
198
|
+
# Apply CLI --var overrides (after parse, so they win over file vars:).
|
|
199
|
+
for ov in "${cli_var_overrides[@]}"; do
|
|
200
|
+
case "${ov}" in
|
|
201
|
+
*=*) FLOW_VARS["${ov%%=*}"]="${ov#*=}" ;;
|
|
202
|
+
*) die "${EXIT_USAGE_ERROR}" "--var requires key=val (got: ${ov})" ;;
|
|
203
|
+
esac
|
|
204
|
+
done
|
|
205
|
+
|
|
206
|
+
# Extract step lines.
|
|
207
|
+
steps_jsonl="$(printf '%s\n' "${parsed}" | jq -c 'select(._kind=="step")')"
|
|
208
|
+
step_count=$(printf '%s\n' "${steps_jsonl}" | grep -c '^.' || printf '0')
|
|
209
|
+
|
|
210
|
+
if [ "${dry_run}" = "1" ]; then
|
|
211
|
+
# Dry-run pre-pass: substitute vars (with refs-mode=skip since no snapshot
|
|
212
|
+
# has actually run); print the planned step list. Per Phase 9 part 1-ii:
|
|
213
|
+
# ${refs.NAME} stays literal in dry-run output (FLOW_REFS would be empty
|
|
214
|
+
# anyway).
|
|
215
|
+
while IFS= read -r step_line; do
|
|
216
|
+
[ -z "${step_line}" ] && continue
|
|
217
|
+
flow_apply_vars "${step_line}" skip
|
|
218
|
+
done <<< "${steps_jsonl}"
|
|
219
|
+
emit_summary verb=flow tool=none why=dry-run status=ok mode=run \
|
|
220
|
+
flow_name="${FLOW_NAME}" step_count="${step_count}" dry_run=true
|
|
221
|
+
exit 0
|
|
222
|
+
fi
|
|
223
|
+
|
|
224
|
+
# Real run: capture pipeline + per-step dispatch with mid-flow ref resolution.
|
|
225
|
+
capture_start "flow"
|
|
226
|
+
# Append flow_name into meta.json (additive; no schema bump per design F4).
|
|
227
|
+
meta="${CAPTURE_DIR}/meta.json"
|
|
228
|
+
tmp="${meta}.tmp.$$"
|
|
229
|
+
jq --arg n "${FLOW_NAME}" '.flow_name = $n' "${meta}" > "${tmp}"
|
|
230
|
+
chmod 600 "${tmp}"
|
|
231
|
+
mv "${tmp}" "${meta}"
|
|
232
|
+
|
|
233
|
+
steps_log="${CAPTURE_DIR}/steps.jsonl"
|
|
234
|
+
: > "${steps_log}"
|
|
235
|
+
chmod 600 "${steps_log}"
|
|
236
|
+
|
|
237
|
+
successful_steps=0
|
|
238
|
+
failed_steps=0
|
|
239
|
+
last_exit=0
|
|
240
|
+
while IFS= read -r step_line; do
|
|
241
|
+
[ -z "${step_line}" ] && continue
|
|
242
|
+
# Per-step substitution AT EXECUTION TIME — FLOW_REFS may have just been
|
|
243
|
+
# populated by the prior snapshot step. flow_apply_vars defaults to
|
|
244
|
+
# refs-mode=strict (fail loud on missing ref).
|
|
245
|
+
set +e
|
|
246
|
+
substituted_step="$(flow_apply_vars "${step_line}")"
|
|
247
|
+
apply_rc=$?
|
|
248
|
+
set -e
|
|
249
|
+
if [ "${apply_rc}" -ne 0 ]; then
|
|
250
|
+
# flow_apply_vars already emitted the error message via die. Surface
|
|
251
|
+
# the failure as a step-event + abort the flow.
|
|
252
|
+
evt="$(jq -nc \
|
|
253
|
+
--argjson step_index "$(printf '%s' "${step_line}" | jq '.step_index')" \
|
|
254
|
+
--arg verb "$(printf '%s' "${step_line}" | jq -r '.verb')" \
|
|
255
|
+
--argjson exit_code "${apply_rc}" \
|
|
256
|
+
--arg status "error" \
|
|
257
|
+
--arg error "var/ref substitution failed" \
|
|
258
|
+
'{step_index: $step_index, verb: $verb, status: $status, exit_code: $exit_code, error: $error}')"
|
|
259
|
+
printf '%s\n' "${evt}" >> "${steps_log}"
|
|
260
|
+
failed_steps=$((failed_steps + 1))
|
|
261
|
+
last_exit="${apply_rc}"
|
|
262
|
+
break
|
|
263
|
+
fi
|
|
264
|
+
|
|
265
|
+
evt="$(flow_dispatch "${substituted_step}")"
|
|
266
|
+
printf '%s\n' "${evt}" >> "${steps_log}"
|
|
267
|
+
status="$(printf '%s' "${evt}" | jq -r '.status')"
|
|
268
|
+
if [ "${status}" = "ok" ]; then
|
|
269
|
+
successful_steps=$((successful_steps + 1))
|
|
270
|
+
else
|
|
271
|
+
failed_steps=$((failed_steps + 1))
|
|
272
|
+
last_exit="$(printf '%s' "${evt}" | jq -r '.exit_code')"
|
|
273
|
+
fi
|
|
274
|
+
|
|
275
|
+
# Phase 9 part 1-ii: harvest step.refs into FLOW_REFS (latest-wins).
|
|
276
|
+
refs_for_step="$(printf '%s' "${evt}" | jq -c '.refs // null')"
|
|
277
|
+
if [ "${refs_for_step}" != "null" ]; then
|
|
278
|
+
# Reset FLOW_REFS wholesale (latest-snapshot-wins).
|
|
279
|
+
FLOW_REFS=()
|
|
280
|
+
while IFS=$'\t' read -r ref_text ref_id; do
|
|
281
|
+
[ -z "${ref_text}" ] && continue
|
|
282
|
+
FLOW_REFS["${ref_text}"]="${ref_id}"
|
|
283
|
+
done <<< "$(printf '%s' "${refs_for_step}" | jq -r '.[] | "\(.text)\t\(.ref)"')"
|
|
284
|
+
fi
|
|
285
|
+
done <<< "${steps_jsonl}"
|
|
286
|
+
|
|
287
|
+
# Determine overall flow status.
|
|
288
|
+
if [ "${failed_steps}" = "0" ]; then
|
|
289
|
+
flow_status="ok"
|
|
290
|
+
elif [ "${successful_steps}" = "0" ]; then
|
|
291
|
+
flow_status="error"
|
|
292
|
+
else
|
|
293
|
+
flow_status="partial"
|
|
294
|
+
fi
|
|
295
|
+
|
|
296
|
+
# Append per-flow counts to meta.json.
|
|
297
|
+
tmp="${meta}.tmp.$$"
|
|
298
|
+
jq \
|
|
299
|
+
--argjson sc "${step_count}" \
|
|
300
|
+
--argjson ss "${successful_steps}" \
|
|
301
|
+
--argjson fs "${failed_steps}" \
|
|
302
|
+
'. + {step_count: $sc, successful_steps: $ss, failed_steps: $fs}' \
|
|
303
|
+
"${meta}" > "${tmp}"
|
|
304
|
+
chmod 600 "${tmp}"
|
|
305
|
+
mv "${tmp}" "${meta}"
|
|
306
|
+
|
|
307
|
+
capture_finish "${flow_status}" true
|
|
308
|
+
|
|
309
|
+
emit_summary verb=flow tool=none why=run status="${flow_status}" mode=run \
|
|
310
|
+
flow_name="${FLOW_NAME}" capture_id="${CAPTURE_ID}" \
|
|
311
|
+
step_count="${step_count}" successful_steps="${successful_steps}" failed_steps="${failed_steps}"
|
|
312
|
+
|
|
313
|
+
if [ "${flow_status}" = "ok" ]; then
|
|
314
|
+
exit 0
|
|
315
|
+
fi
|
|
316
|
+
exit "${last_exit}"
|
|
@@ -0,0 +1,187 @@
|
|
|
1
|
+
#!/usr/bin/env bash
|
|
2
|
+
# scripts/browser-history.sh — read-side ops over the captures pipeline +
|
|
3
|
+
# manual-trigger prune. Phase 9 part 1-v (CLOSES Phase 9).
|
|
4
|
+
#
|
|
5
|
+
# Sub-modes:
|
|
6
|
+
# list [--limit N] — enumerate captures (newest first)
|
|
7
|
+
# show <capture-id> — print meta.json + steps.jsonl
|
|
8
|
+
# diff <id1> <id2> — per-step replay_diff via flow_diff_steps
|
|
9
|
+
# clear [--keep N] [--days D] — manual prune (composes Phase 7's
|
|
10
|
+
# [--not-baseline] capture_prune; respects is_baseline
|
|
11
|
+
# skip-rule by default)
|
|
12
|
+
|
|
13
|
+
set -euo pipefail
|
|
14
|
+
IFS=$'\n\t'
|
|
15
|
+
|
|
16
|
+
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
|
17
|
+
SCRIPTS_DIR="${SCRIPT_DIR}"
|
|
18
|
+
export SCRIPTS_DIR
|
|
19
|
+
|
|
20
|
+
# shellcheck source=lib/common.sh
|
|
21
|
+
# shellcheck disable=SC1091
|
|
22
|
+
source "${SCRIPT_DIR}/lib/common.sh"
|
|
23
|
+
# shellcheck source=lib/output.sh
|
|
24
|
+
# shellcheck disable=SC1091
|
|
25
|
+
source "${SCRIPT_DIR}/lib/output.sh"
|
|
26
|
+
# shellcheck source=lib/capture.sh
|
|
27
|
+
# shellcheck disable=SC1091
|
|
28
|
+
source "${SCRIPT_DIR}/lib/capture.sh"
|
|
29
|
+
# shellcheck source=lib/flow.sh
|
|
30
|
+
# shellcheck disable=SC1091
|
|
31
|
+
source "${SCRIPT_DIR}/lib/flow.sh"
|
|
32
|
+
|
|
33
|
+
init_paths
|
|
34
|
+
|
|
35
|
+
SUMMARY_T0="$(now_ms)"; export SUMMARY_T0
|
|
36
|
+
|
|
37
|
+
sub_mode="${1:-}"
|
|
38
|
+
[ -n "${sub_mode}" ] || die "${EXIT_USAGE_ERROR}" "browser-history: missing sub-mode (list / show / diff / clear)"
|
|
39
|
+
shift
|
|
40
|
+
|
|
41
|
+
case "${sub_mode}" in
|
|
42
|
+
list)
|
|
43
|
+
limit=""
|
|
44
|
+
while [ "$#" -gt 0 ]; do
|
|
45
|
+
case "$1" in
|
|
46
|
+
--limit) limit="$2"; shift 2 ;;
|
|
47
|
+
--since) shift 2 ;; # accepted; deferred (out-of-scope filter)
|
|
48
|
+
*) die "${EXIT_USAGE_ERROR}" "history list: unknown flag '$1'" ;;
|
|
49
|
+
esac
|
|
50
|
+
done
|
|
51
|
+
total=0
|
|
52
|
+
if [ -d "${CAPTURES_DIR}" ]; then
|
|
53
|
+
shopt -s nullglob
|
|
54
|
+
# Sort by capture_id (which is monotonic per Phase 7); newest first.
|
|
55
|
+
ids=()
|
|
56
|
+
for d in "${CAPTURES_DIR}"/[0-9]*/; do
|
|
57
|
+
ids+=("$(basename "${d}")")
|
|
58
|
+
done
|
|
59
|
+
shopt -u nullglob
|
|
60
|
+
# Reverse-sort to put newest first.
|
|
61
|
+
mapfile -t sorted < <(printf '%s\n' "${ids[@]}" | sort -r)
|
|
62
|
+
for id in "${sorted[@]}"; do
|
|
63
|
+
meta="${CAPTURES_DIR}/${id}/meta.json"
|
|
64
|
+
[ -f "${meta}" ] || continue
|
|
65
|
+
if [ -n "${limit}" ] && [ "${total}" -ge "${limit}" ]; then
|
|
66
|
+
break
|
|
67
|
+
fi
|
|
68
|
+
# Emit per-capture row event.
|
|
69
|
+
jq -c --arg event "history_row" \
|
|
70
|
+
'. + {event: $event}' "${meta}"
|
|
71
|
+
total=$((total + 1))
|
|
72
|
+
done
|
|
73
|
+
fi
|
|
74
|
+
emit_summary verb=history tool=none why=list status=ok mode=list total="${total}"
|
|
75
|
+
exit 0
|
|
76
|
+
;;
|
|
77
|
+
|
|
78
|
+
show)
|
|
79
|
+
show_id="${1:-}"
|
|
80
|
+
[ -n "${show_id}" ] || die "${EXIT_USAGE_ERROR}" "history show: missing <capture-id>"
|
|
81
|
+
meta="${CAPTURES_DIR}/${show_id}/meta.json"
|
|
82
|
+
[ -f "${meta}" ] || die "${EXIT_USAGE_ERROR}" "history show: no such capture '${show_id}'"
|
|
83
|
+
# Compact meta.json onto a single line so callers can parse the first
|
|
84
|
+
# line as a complete JSON object (capture_start writes pretty-printed
|
|
85
|
+
# multi-line; jq -c re-flattens).
|
|
86
|
+
jq -c . "${meta}"
|
|
87
|
+
steps_log="${CAPTURES_DIR}/${show_id}/steps.jsonl"
|
|
88
|
+
[ -f "${steps_log}" ] && cat "${steps_log}"
|
|
89
|
+
emit_summary verb=history tool=none why=show status=ok mode=show capture_id="${show_id}"
|
|
90
|
+
exit 0
|
|
91
|
+
;;
|
|
92
|
+
|
|
93
|
+
diff)
|
|
94
|
+
id1="${1:-}"
|
|
95
|
+
id2="${2:-}"
|
|
96
|
+
[ -n "${id1}" ] && [ -n "${id2}" ] || die "${EXIT_USAGE_ERROR}" "history diff: requires <id1> <id2>"
|
|
97
|
+
log1="${CAPTURES_DIR}/${id1}/steps.jsonl"
|
|
98
|
+
log2="${CAPTURES_DIR}/${id2}/steps.jsonl"
|
|
99
|
+
[ -f "${log1}" ] || die "${EXIT_USAGE_ERROR}" "history diff: no steps.jsonl at captures/${id1}/"
|
|
100
|
+
[ -f "${log2}" ] || die "${EXIT_USAGE_ERROR}" "history diff: no steps.jsonl at captures/${id2}/"
|
|
101
|
+
# Iterate paired step events; emit replay_diff per pair.
|
|
102
|
+
matched=0
|
|
103
|
+
diverged=0
|
|
104
|
+
paste -d $'\t' "${log1}" "${log2}" | while IFS=$'\t' read -r old new; do
|
|
105
|
+
[ -z "${old}" ] || [ -z "${new}" ] && continue
|
|
106
|
+
flow_diff_steps "${old}" "${new}" || true
|
|
107
|
+
done
|
|
108
|
+
# Aggregate counts via separate read pass.
|
|
109
|
+
total_steps="$(wc -l < "${log1}" | tr -d ' ')"
|
|
110
|
+
emit_summary verb=history tool=none why=diff status=ok mode=diff \
|
|
111
|
+
capture_id_old="${id1}" capture_id_new="${id2}" total_steps="${total_steps}"
|
|
112
|
+
exit 0
|
|
113
|
+
;;
|
|
114
|
+
|
|
115
|
+
clear)
|
|
116
|
+
keep=""
|
|
117
|
+
days=""
|
|
118
|
+
not_baseline=0
|
|
119
|
+
while [ "$#" -gt 0 ]; do
|
|
120
|
+
case "$1" in
|
|
121
|
+
--keep) keep="$2"; shift 2 ;;
|
|
122
|
+
--days) days="$2"; shift 2 ;;
|
|
123
|
+
--not-baseline) not_baseline=1; shift ;;
|
|
124
|
+
*) die "${EXIT_USAGE_ERROR}" "history clear: unknown flag '$1'" ;;
|
|
125
|
+
esac
|
|
126
|
+
done
|
|
127
|
+
|
|
128
|
+
pruned=0
|
|
129
|
+
if [ -d "${CAPTURES_DIR}" ]; then
|
|
130
|
+
shopt -s nullglob
|
|
131
|
+
ids=()
|
|
132
|
+
for d in "${CAPTURES_DIR}"/[0-9]*/; do
|
|
133
|
+
ids+=("$(basename "${d}")")
|
|
134
|
+
done
|
|
135
|
+
shopt -u nullglob
|
|
136
|
+
# Newest-first sort; keep first N if --keep.
|
|
137
|
+
mapfile -t sorted < <(printf '%s\n' "${ids[@]}" | sort -r)
|
|
138
|
+
idx=0
|
|
139
|
+
now_epoch="$(date -u +%s)"
|
|
140
|
+
for id in "${sorted[@]}"; do
|
|
141
|
+
meta="${CAPTURES_DIR}/${id}/meta.json"
|
|
142
|
+
[ -f "${meta}" ] || continue
|
|
143
|
+
is_baseline="$(jq -r '.is_baseline // false' "${meta}" 2>/dev/null || printf 'false')"
|
|
144
|
+
# is_baseline:true is ALWAYS skipped (per Phase 7 prune contract +
|
|
145
|
+
# locked decision H3).
|
|
146
|
+
if [ "${is_baseline}" = "true" ]; then
|
|
147
|
+
idx=$((idx + 1))
|
|
148
|
+
continue
|
|
149
|
+
fi
|
|
150
|
+
# --keep N: keep the newest N (skip pruning the first N).
|
|
151
|
+
if [ -n "${keep}" ] && [ "${idx}" -lt "${keep}" ]; then
|
|
152
|
+
idx=$((idx + 1))
|
|
153
|
+
continue
|
|
154
|
+
fi
|
|
155
|
+
# --days D: keep captures younger than D days.
|
|
156
|
+
if [ -n "${days}" ]; then
|
|
157
|
+
finished_at="$(jq -r '.finished_at // .started_at // ""' "${meta}" 2>/dev/null || printf '')"
|
|
158
|
+
if [ -n "${finished_at}" ]; then
|
|
159
|
+
cap_epoch="$(date -d "${finished_at}" +%s 2>/dev/null || date -j -f '%Y-%m-%dT%H:%M:%SZ' "${finished_at}" +%s 2>/dev/null || printf '0')"
|
|
160
|
+
age_days=$(( (now_epoch - cap_epoch) / 86400 ))
|
|
161
|
+
if [ "${age_days}" -lt "${days}" ]; then
|
|
162
|
+
idx=$((idx + 1))
|
|
163
|
+
continue
|
|
164
|
+
fi
|
|
165
|
+
fi
|
|
166
|
+
fi
|
|
167
|
+
# If --not-baseline alone (no --keep / --days), prune everything
|
|
168
|
+
# non-baseline. If --not-baseline + --keep / --days, the above
|
|
169
|
+
# checks already excluded baselines + applied limits.
|
|
170
|
+
if [ -z "${keep}" ] && [ -z "${days}" ] && [ "${not_baseline}" = "0" ]; then
|
|
171
|
+
# No flags at all → no-op (use config defaults via auto-prune).
|
|
172
|
+
idx=$((idx + 1))
|
|
173
|
+
continue
|
|
174
|
+
fi
|
|
175
|
+
rm -rf "${CAPTURES_DIR}/${id}"
|
|
176
|
+
pruned=$((pruned + 1))
|
|
177
|
+
idx=$((idx + 1))
|
|
178
|
+
done
|
|
179
|
+
fi
|
|
180
|
+
emit_summary verb=history tool=none why=clear status=ok mode=clear pruned="${pruned}"
|
|
181
|
+
exit 0
|
|
182
|
+
;;
|
|
183
|
+
|
|
184
|
+
*)
|
|
185
|
+
die "${EXIT_USAGE_ERROR}" "browser-history: unknown sub-mode '${sub_mode}' (use list / show / diff / clear)"
|
|
186
|
+
;;
|
|
187
|
+
esac
|