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,448 @@
|
|
|
1
|
+
# scripts/lib/flow.sh — flow runner library (Phase 9 part 1-i).
|
|
2
|
+
#
|
|
3
|
+
# Three-fn API:
|
|
4
|
+
# flow_parse <flow-file> — parses YAML; sets FLOW_NAME, FLOW_SESSION,
|
|
5
|
+
# FLOW_VARS (assoc array); emits one
|
|
6
|
+
# {step_index, verb, args} JSON line per step
|
|
7
|
+
# on stdout.
|
|
8
|
+
# flow_apply_vars <step-json> — reads global FLOW_VARS; substitutes ${var}
|
|
9
|
+
# occurrences in step.args.* values; emits
|
|
10
|
+
# modified step JSON. ${refs.NAME} passes
|
|
11
|
+
# through literal (deferred to 9-1-ii).
|
|
12
|
+
# flow_dispatch <step-json> — translates step args → bash scripts/browser-
|
|
13
|
+
# <verb>.sh CLI invocation; runs it; wraps
|
|
14
|
+
# the verb's summary line into a step-event
|
|
15
|
+
# JSON line {step_index, verb, args, status,
|
|
16
|
+
# duration_ms, exit_code, summary} on stdout.
|
|
17
|
+
#
|
|
18
|
+
# YAML SUBSET (v1):
|
|
19
|
+
# - Top-level scalars: name, session (optional)
|
|
20
|
+
# - vars: block — flat key:value pairs, one per line, scalar values only
|
|
21
|
+
# - steps: list of single-key flow-style maps:
|
|
22
|
+
# - <verb>: { key: val, key: val }
|
|
23
|
+
# OR - <verb>: {}
|
|
24
|
+
# - Top-level keys other than {name, session, vars, steps} → warn + ignore
|
|
25
|
+
# - No nested maps, no list values in step bodies
|
|
26
|
+
# - No multi-line strings, no block scalars
|
|
27
|
+
# - ${var} substituted at parse-time via FLOW_VARS lookup
|
|
28
|
+
# - ${refs.NAME} left literal (resolution in 9-1-ii)
|
|
29
|
+
#
|
|
30
|
+
# Adapters / lib helpers are LEAVES — never source verb scripts. flow_dispatch
|
|
31
|
+
# shells out to browser-<verb>.sh as a subprocess; the verb script is a leaf
|
|
32
|
+
# from this library's POV.
|
|
33
|
+
|
|
34
|
+
[ -n "${BROWSER_SKILL_FLOW_LOADED:-}" ] && return 0
|
|
35
|
+
readonly BROWSER_SKILL_FLOW_LOADED=1
|
|
36
|
+
|
|
37
|
+
# Globals set by flow_parse:
|
|
38
|
+
# FLOW_NAME — string
|
|
39
|
+
# FLOW_SESSION — string (may be empty)
|
|
40
|
+
# FLOW_VARS — assoc array {key: value}
|
|
41
|
+
#
|
|
42
|
+
# Callers should `declare -gA FLOW_VARS=()` before calling flow_parse to
|
|
43
|
+
# reset state across multiple flows in the same shell.
|
|
44
|
+
|
|
45
|
+
flow_parse() {
|
|
46
|
+
local flow_file="$1"
|
|
47
|
+
[ -f "${flow_file}" ] || die "${EXIT_USAGE_ERROR}" "flow_parse: file not found: ${flow_file}"
|
|
48
|
+
|
|
49
|
+
# Reset globals (set in caller's shell — only useful when flow_parse is
|
|
50
|
+
# called WITHOUT command substitution; the `_meta` JSON line on stdout is
|
|
51
|
+
# the authoritative path for subshell-captured callers like browser-flow.sh).
|
|
52
|
+
FLOW_NAME=""
|
|
53
|
+
FLOW_SESSION=""
|
|
54
|
+
declare -gA FLOW_VARS=()
|
|
55
|
+
|
|
56
|
+
local in_vars=0 in_steps=0
|
|
57
|
+
local step_index=0
|
|
58
|
+
local line stripped
|
|
59
|
+
local steps_seen=0
|
|
60
|
+
# Build vars JSON object for the _meta line.
|
|
61
|
+
local vars_json='{}'
|
|
62
|
+
|
|
63
|
+
while IFS= read -r line || [ -n "${line}" ]; do
|
|
64
|
+
# Skip blank lines and full-line comments.
|
|
65
|
+
case "${line}" in
|
|
66
|
+
''|'#'*) continue ;;
|
|
67
|
+
esac
|
|
68
|
+
|
|
69
|
+
# Top-level field detection (no leading whitespace).
|
|
70
|
+
case "${line}" in
|
|
71
|
+
'name:'*)
|
|
72
|
+
FLOW_NAME="$(_flow_strip_value "${line#name:}")"
|
|
73
|
+
in_vars=0; in_steps=0
|
|
74
|
+
continue
|
|
75
|
+
;;
|
|
76
|
+
'session:'*)
|
|
77
|
+
FLOW_SESSION="$(_flow_strip_value "${line#session:}")"
|
|
78
|
+
in_vars=0; in_steps=0
|
|
79
|
+
continue
|
|
80
|
+
;;
|
|
81
|
+
'vars:'*)
|
|
82
|
+
in_vars=1; in_steps=0
|
|
83
|
+
continue
|
|
84
|
+
;;
|
|
85
|
+
'steps:'*)
|
|
86
|
+
in_vars=0; in_steps=1
|
|
87
|
+
steps_seen=1
|
|
88
|
+
# Emit _meta line BEFORE step lines so callers can read it first.
|
|
89
|
+
if [ -z "${FLOW_NAME}" ]; then
|
|
90
|
+
die "${EXIT_USAGE_ERROR}" "flow_parse: missing required field 'name'"
|
|
91
|
+
fi
|
|
92
|
+
jq -nc \
|
|
93
|
+
--arg name "${FLOW_NAME}" \
|
|
94
|
+
--arg session "${FLOW_SESSION}" \
|
|
95
|
+
--argjson vars "${vars_json}" \
|
|
96
|
+
'{_kind: "meta", name: $name, session: $session, vars: $vars}'
|
|
97
|
+
continue
|
|
98
|
+
;;
|
|
99
|
+
esac
|
|
100
|
+
|
|
101
|
+
if [ "${in_vars}" = "1" ]; then
|
|
102
|
+
# Flat key: value indented under vars:.
|
|
103
|
+
stripped="$(_flow_strip_indent "${line}")"
|
|
104
|
+
case "${stripped}" in
|
|
105
|
+
*': '*)
|
|
106
|
+
local k v
|
|
107
|
+
k="${stripped%%: *}"
|
|
108
|
+
v="${stripped#*: }"
|
|
109
|
+
v="$(_flow_strip_value "${v}")"
|
|
110
|
+
FLOW_VARS["${k}"]="${v}"
|
|
111
|
+
# Append into vars_json.
|
|
112
|
+
vars_json="$(printf '%s' "${vars_json}" | jq -c --arg k "${k}" --arg v "${v}" '. + {($k): $v}')"
|
|
113
|
+
;;
|
|
114
|
+
esac
|
|
115
|
+
continue
|
|
116
|
+
fi
|
|
117
|
+
|
|
118
|
+
if [ "${in_steps}" = "1" ]; then
|
|
119
|
+
stripped="$(_flow_strip_indent "${line}")"
|
|
120
|
+
case "${stripped}" in
|
|
121
|
+
'- '*)
|
|
122
|
+
# New step: `- <verb>: { ... }` OR `- <verb>: {}`
|
|
123
|
+
local body verb args_yaml
|
|
124
|
+
body="${stripped#- }"
|
|
125
|
+
verb="${body%%:*}"
|
|
126
|
+
args_yaml="${body#*:}"
|
|
127
|
+
args_yaml="$(_flow_strip_value "${args_yaml}")"
|
|
128
|
+
local args_json
|
|
129
|
+
args_json="$(_flow_inline_to_json "${args_yaml}")" \
|
|
130
|
+
|| die "${EXIT_USAGE_ERROR}" "flow_parse: bad step body at index ${step_index}: ${args_yaml}"
|
|
131
|
+
jq -nc \
|
|
132
|
+
--arg kind "step" \
|
|
133
|
+
--argjson step_index "${step_index}" \
|
|
134
|
+
--arg verb "${verb}" \
|
|
135
|
+
--argjson args "${args_json}" \
|
|
136
|
+
'{_kind: $kind, step_index: $step_index, verb: $verb, args: $args}'
|
|
137
|
+
step_index=$((step_index + 1))
|
|
138
|
+
;;
|
|
139
|
+
esac
|
|
140
|
+
continue
|
|
141
|
+
fi
|
|
142
|
+
|
|
143
|
+
# Unknown top-level key — warn but don't fail.
|
|
144
|
+
case "${line}" in
|
|
145
|
+
*': '*|*':')
|
|
146
|
+
printf 'flow_parse: warning: unknown top-level key in line: %s\n' "${line}" >&2
|
|
147
|
+
;;
|
|
148
|
+
esac
|
|
149
|
+
done < "${flow_file}"
|
|
150
|
+
|
|
151
|
+
[ -n "${FLOW_NAME}" ] || die "${EXIT_USAGE_ERROR}" "flow_parse: missing required field 'name'"
|
|
152
|
+
[ "${steps_seen}" = "1" ] || die "${EXIT_USAGE_ERROR}" "flow_parse: missing required field 'steps'"
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
# _flow_strip_value <raw> — strip leading space + trailing whitespace + quotes.
|
|
156
|
+
_flow_strip_value() {
|
|
157
|
+
local v="$1"
|
|
158
|
+
# Trim leading whitespace.
|
|
159
|
+
v="${v#"${v%%[![:space:]]*}"}"
|
|
160
|
+
# Trim trailing whitespace.
|
|
161
|
+
v="${v%"${v##*[![:space:]]}"}"
|
|
162
|
+
# Strip surrounding double or single quotes.
|
|
163
|
+
case "${v}" in
|
|
164
|
+
'"'*'"') v="${v#\"}"; v="${v%\"}" ;;
|
|
165
|
+
"'"*"'") v="${v#\'}"; v="${v%\'}" ;;
|
|
166
|
+
esac
|
|
167
|
+
printf '%s' "${v}"
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
# _flow_strip_indent <line> — strip leading whitespace.
|
|
171
|
+
_flow_strip_indent() {
|
|
172
|
+
local v="$1"
|
|
173
|
+
v="${v#"${v%%[![:space:]]*}"}"
|
|
174
|
+
printf '%s' "${v}"
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
# _flow_inline_to_json <yaml-flow-style> → JSON object on stdout.
|
|
178
|
+
# Handles {} (empty), { k: v }, { k1: v1, k2: v2 }.
|
|
179
|
+
# Values are coerced to JSON: "true"/"false"/"null"/numbers as JSON; else
|
|
180
|
+
# wrapped as strings.
|
|
181
|
+
_flow_inline_to_json() {
|
|
182
|
+
local raw="$1"
|
|
183
|
+
raw="$(_flow_strip_value "${raw}")"
|
|
184
|
+
case "${raw}" in
|
|
185
|
+
'{}'|'') printf '{}'; return 0 ;;
|
|
186
|
+
'{'*'}') ;;
|
|
187
|
+
*) return 1 ;;
|
|
188
|
+
esac
|
|
189
|
+
# Strip outer braces.
|
|
190
|
+
raw="${raw#\{}"
|
|
191
|
+
raw="${raw%\}}"
|
|
192
|
+
raw="$(_flow_strip_value "${raw}")"
|
|
193
|
+
[ -z "${raw}" ] && { printf '{}'; return 0; }
|
|
194
|
+
|
|
195
|
+
# Use indexed variable names ($_k0, $_k1, ...) so hyphenated YAML keys
|
|
196
|
+
# like `dry-run` don't break jq's tokenizer (jq vars must match
|
|
197
|
+
# [A-Za-z_][A-Za-z0-9_]*). Field names go through jq's bracket-string
|
|
198
|
+
# accessor `.["dry-run"]` for the same reason.
|
|
199
|
+
local jq_args=() jq_filter='. = {}'
|
|
200
|
+
local IFS_orig="${IFS}"
|
|
201
|
+
IFS=','
|
|
202
|
+
local pair
|
|
203
|
+
local idx=0
|
|
204
|
+
for pair in ${raw}; do
|
|
205
|
+
IFS="${IFS_orig}"
|
|
206
|
+
pair="$(_flow_strip_value "${pair}")"
|
|
207
|
+
[ -z "${pair}" ] && continue
|
|
208
|
+
case "${pair}" in
|
|
209
|
+
*': '*) ;;
|
|
210
|
+
*':'*) ;;
|
|
211
|
+
*) return 1 ;;
|
|
212
|
+
esac
|
|
213
|
+
local k v key_jstr
|
|
214
|
+
k="${pair%%:*}"
|
|
215
|
+
v="${pair#*:}"
|
|
216
|
+
k="$(_flow_strip_value "${k}")"
|
|
217
|
+
v="$(_flow_strip_value "${v}")"
|
|
218
|
+
# Coerce values to JSON: numbers / true / false / null pass as JSON; else
|
|
219
|
+
# wrapped as strings. ${var} placeholders stay as strings.
|
|
220
|
+
if [[ "${v}" =~ ^-?(0|[1-9][0-9]*)(\.[0-9]+)?$ ]]; then
|
|
221
|
+
jq_args+=(--argjson "_k${idx}" "${v}")
|
|
222
|
+
elif [ "${v}" = "true" ] || [ "${v}" = "false" ] || [ "${v}" = "null" ]; then
|
|
223
|
+
jq_args+=(--argjson "_k${idx}" "${v}")
|
|
224
|
+
else
|
|
225
|
+
jq_args+=(--arg "_k${idx}" "${v}")
|
|
226
|
+
fi
|
|
227
|
+
# Encode key as a JSON string for safe interpolation into the filter.
|
|
228
|
+
key_jstr="$(printf '%s' "${k}" | jq -Rs .)"
|
|
229
|
+
jq_filter="${jq_filter} | .[${key_jstr}] = \$_k${idx}"
|
|
230
|
+
idx=$((idx + 1))
|
|
231
|
+
IFS=','
|
|
232
|
+
done
|
|
233
|
+
IFS="${IFS_orig}"
|
|
234
|
+
jq -nc "${jq_args[@]}" "${jq_filter}"
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
# flow_apply_vars <step-json> [refs-mode] — substitutes ${var} via FLOW_VARS
|
|
238
|
+
# and ${refs.NAME} via FLOW_REFS in step.args.* string values; emits modified
|
|
239
|
+
# step JSON. Both globals are assoc arrays the caller MUST declare:
|
|
240
|
+
#
|
|
241
|
+
# declare -gA FLOW_VARS=( [key]=val ... ) # populated by flow_parse + --var
|
|
242
|
+
# declare -gA FLOW_REFS=( [name]=ref ... ) # populated by browser-flow.sh
|
|
243
|
+
# # after each snapshot step's
|
|
244
|
+
# # event line (latest-wins).
|
|
245
|
+
#
|
|
246
|
+
# refs-mode (default "strict"):
|
|
247
|
+
# strict — resolve ${refs.X} via FLOW_REFS or die EXIT_USAGE_ERROR (per
|
|
248
|
+
# design doc §3 F3 — fail loud).
|
|
249
|
+
# skip — leave ${refs.X} as literal pass-through. Used by --dry-run, where
|
|
250
|
+
# FLOW_REFS isn't populated (no snapshot has actually run).
|
|
251
|
+
#
|
|
252
|
+
# Missing FLOW_VARS[<name>] always dies EXIT_USAGE_ERROR (vars are static).
|
|
253
|
+
flow_apply_vars() {
|
|
254
|
+
local step="$1"
|
|
255
|
+
local refs_mode="${2:-strict}"
|
|
256
|
+
local arg_keys
|
|
257
|
+
arg_keys="$(printf '%s' "${step}" | jq -r '.args | keys[]?' 2>/dev/null || printf '')"
|
|
258
|
+
local key val
|
|
259
|
+
while IFS= read -r key; do
|
|
260
|
+
[ -z "${key}" ] && continue
|
|
261
|
+
val="$(printf '%s' "${step}" | jq -r --arg k "${key}" '.args[$k]')"
|
|
262
|
+
[ "${val}" = "null" ] && continue
|
|
263
|
+
# Walk all ${...} occurrences; substitute via FLOW_VARS or FLOW_REFS.
|
|
264
|
+
local rest="${val}"
|
|
265
|
+
local out=""
|
|
266
|
+
while [[ "${rest}" == *'${'*'}'* ]]; do
|
|
267
|
+
out="${out}${rest%%\$\{*}"
|
|
268
|
+
rest="${rest#*\$\{}"
|
|
269
|
+
local placeholder="${rest%%\}*}"
|
|
270
|
+
rest="${rest#*\}}"
|
|
271
|
+
case "${placeholder}" in
|
|
272
|
+
refs.*)
|
|
273
|
+
local ref_name="${placeholder#refs.}"
|
|
274
|
+
if [ "${refs_mode}" = "skip" ]; then
|
|
275
|
+
# Leave literal pass-through (dry-run mode — no snapshot has run).
|
|
276
|
+
out="${out}\${${placeholder}}"
|
|
277
|
+
elif [ -z "${FLOW_REFS[${ref_name}]+x}" ]; then
|
|
278
|
+
die "${EXIT_USAGE_ERROR}" \
|
|
279
|
+
"flow_apply_vars: undefined ref '\${${placeholder}}' in step ${key} (no snapshot has surfaced \"${ref_name}\" — add a snapshot step first OR check the accessible name)"
|
|
280
|
+
else
|
|
281
|
+
out="${out}${FLOW_REFS[${ref_name}]}"
|
|
282
|
+
fi
|
|
283
|
+
;;
|
|
284
|
+
*)
|
|
285
|
+
if [ -z "${FLOW_VARS[${placeholder}]+x}" ]; then
|
|
286
|
+
die "${EXIT_USAGE_ERROR}" "flow_apply_vars: undefined var '\${${placeholder}}' in step ${key}"
|
|
287
|
+
fi
|
|
288
|
+
out="${out}${FLOW_VARS[${placeholder}]}"
|
|
289
|
+
;;
|
|
290
|
+
esac
|
|
291
|
+
done
|
|
292
|
+
out="${out}${rest}"
|
|
293
|
+
step="$(printf '%s' "${step}" | jq -c --arg k "${key}" --arg v "${out}" '.args[$k] = $v')"
|
|
294
|
+
done <<< "${arg_keys}"
|
|
295
|
+
printf '%s\n' "${step}"
|
|
296
|
+
}
|
|
297
|
+
|
|
298
|
+
# flow_dispatch <step-json> — translates step args to a verb-script call;
|
|
299
|
+
# runs `bash scripts/browser-<verb>.sh --key val ...`; wraps the verb's
|
|
300
|
+
# summary into a step-event JSON line on stdout.
|
|
301
|
+
#
|
|
302
|
+
# For unknown verbs (no scripts/browser-<verb>.sh), emits an error step-event
|
|
303
|
+
# with exit_code=41 (UNSUPPORTED_OP) and status=error. flow_dispatch itself
|
|
304
|
+
# always returns 0 — failure surfaces in the step-event payload, NOT the
|
|
305
|
+
# return code (so flow execution can continue partially).
|
|
306
|
+
flow_dispatch() {
|
|
307
|
+
local step="$1"
|
|
308
|
+
local step_index verb args_obj
|
|
309
|
+
step_index="$(printf '%s' "${step}" | jq -r '.step_index')"
|
|
310
|
+
verb="$(printf '%s' "${step}" | jq -r '.verb')"
|
|
311
|
+
args_obj="$(printf '%s' "${step}" | jq -c '.args')"
|
|
312
|
+
|
|
313
|
+
local script="${SCRIPTS_DIR:-${REPO_ROOT:-.}/scripts}/browser-${verb}.sh"
|
|
314
|
+
if [ ! -f "${script}" ]; then
|
|
315
|
+
jq -nc \
|
|
316
|
+
--argjson step_index "${step_index}" \
|
|
317
|
+
--arg verb "${verb}" \
|
|
318
|
+
--argjson args "${args_obj}" \
|
|
319
|
+
--arg status "error" \
|
|
320
|
+
--argjson exit_code 41 \
|
|
321
|
+
--arg error "no verb script at ${script}" \
|
|
322
|
+
'{step_index: $step_index, verb: $verb, args: $args, status: $status, exit_code: $exit_code, error: $error}'
|
|
323
|
+
return 0
|
|
324
|
+
fi
|
|
325
|
+
|
|
326
|
+
# Translate args object → CLI flags.
|
|
327
|
+
local cli_flags=()
|
|
328
|
+
local key val
|
|
329
|
+
while IFS= read -r key; do
|
|
330
|
+
[ -z "${key}" ] && continue
|
|
331
|
+
val="$(printf '%s' "${args_obj}" | jq -r --arg k "${key}" '.[$k]')"
|
|
332
|
+
if [ "${val}" = "true" ]; then
|
|
333
|
+
# Boolean true → bare flag.
|
|
334
|
+
cli_flags+=("--${key}")
|
|
335
|
+
elif [ "${val}" = "false" ]; then
|
|
336
|
+
# Boolean false → omit (flag absence is the false state).
|
|
337
|
+
:
|
|
338
|
+
else
|
|
339
|
+
cli_flags+=("--${key}" "${val}")
|
|
340
|
+
fi
|
|
341
|
+
done <<< "$(printf '%s' "${args_obj}" | jq -r 'keys[]?')"
|
|
342
|
+
|
|
343
|
+
local started_at_ms
|
|
344
|
+
started_at_ms="$(now_ms)"
|
|
345
|
+
|
|
346
|
+
local verb_out verb_exit
|
|
347
|
+
set +e
|
|
348
|
+
verb_out="$(bash "${script}" "${cli_flags[@]}" 2>&1)"
|
|
349
|
+
verb_exit=$?
|
|
350
|
+
set -e
|
|
351
|
+
|
|
352
|
+
local duration_ms=$(( $(now_ms) - started_at_ms ))
|
|
353
|
+
|
|
354
|
+
local last_line summary_json status
|
|
355
|
+
last_line="$(printf '%s\n' "${verb_out}" | tail -1)"
|
|
356
|
+
if printf '%s' "${last_line}" | jq -e . >/dev/null 2>&1; then
|
|
357
|
+
summary_json="${last_line}"
|
|
358
|
+
else
|
|
359
|
+
summary_json="null"
|
|
360
|
+
fi
|
|
361
|
+
|
|
362
|
+
if [ "${verb_exit}" = "0" ]; then
|
|
363
|
+
status="ok"
|
|
364
|
+
else
|
|
365
|
+
status="error"
|
|
366
|
+
fi
|
|
367
|
+
|
|
368
|
+
# Phase 9 part 1-ii: snapshot verbs emit an `event:snapshot` line carrying
|
|
369
|
+
# `refs[]` (text → ref accessibility map). Extract it; attach as step.refs
|
|
370
|
+
# so browser-flow.sh's main loop can update the global FLOW_REFS map.
|
|
371
|
+
# Other verbs: refs stays null. Defensive jq -e for "no event line found".
|
|
372
|
+
local refs_json="null"
|
|
373
|
+
if [ "${verb}" = "snapshot" ]; then
|
|
374
|
+
refs_json="$(
|
|
375
|
+
printf '%s\n' "${verb_out}" \
|
|
376
|
+
| jq -c -s 'map(select(.event == "snapshot")) | .[0].refs // null' 2>/dev/null \
|
|
377
|
+
|| printf 'null'
|
|
378
|
+
)"
|
|
379
|
+
fi
|
|
380
|
+
|
|
381
|
+
jq -nc \
|
|
382
|
+
--argjson step_index "${step_index}" \
|
|
383
|
+
--arg verb "${verb}" \
|
|
384
|
+
--argjson args "${args_obj}" \
|
|
385
|
+
--arg status "${status}" \
|
|
386
|
+
--argjson duration_ms "${duration_ms}" \
|
|
387
|
+
--argjson exit_code "${verb_exit}" \
|
|
388
|
+
--argjson summary "${summary_json}" \
|
|
389
|
+
--argjson refs "${refs_json}" \
|
|
390
|
+
'{step_index: $step_index, verb: $verb, args: $args, status: $status, duration_ms: $duration_ms, exit_code: $exit_code, summary: $summary, refs: $refs}'
|
|
391
|
+
|
|
392
|
+
return 0
|
|
393
|
+
}
|
|
394
|
+
|
|
395
|
+
# flow_diff_steps <old-step-event-json> <new-step-event-json> — compares two
|
|
396
|
+
# step events (from steps.jsonl); emits one `event:replay_diff` JSON line on
|
|
397
|
+
# stdout. Returns 0 if both .status AND .summary match; 1 if either diverges.
|
|
398
|
+
#
|
|
399
|
+
# Used by browser-replay.sh's per-step diff loop. Per design doc §3 F5 + plan
|
|
400
|
+
# 2026-05-10-phase-09-part-1-iv-replay locked decisions D1 + D4.
|
|
401
|
+
flow_diff_steps() {
|
|
402
|
+
local old="$1"
|
|
403
|
+
local new="$2"
|
|
404
|
+
|
|
405
|
+
local step_index verb old_status new_status old_summary new_summary
|
|
406
|
+
step_index="$(printf '%s' "${new}" | jq -r '.step_index')"
|
|
407
|
+
verb="$(printf '%s' "${new}" | jq -r '.verb')"
|
|
408
|
+
old_status="$(printf '%s' "${old}" | jq -r '.status')"
|
|
409
|
+
new_status="$(printf '%s' "${new}" | jq -r '.status')"
|
|
410
|
+
# Strip timing-sensitive fields before output comparison — duration_ms
|
|
411
|
+
# always varies between runs and isn't a semantic difference. Per plan
|
|
412
|
+
# locked decision D4 (jq-equal on summary line; future iteration could
|
|
413
|
+
# add more granular per-field policies).
|
|
414
|
+
old_summary="$(printf '%s' "${old}" | jq -c '.summary | del(.duration_ms)')"
|
|
415
|
+
new_summary="$(printf '%s' "${new}" | jq -c '.summary | del(.duration_ms)')"
|
|
416
|
+
|
|
417
|
+
local status_match=true
|
|
418
|
+
[ "${old_status}" = "${new_status}" ] || status_match=false
|
|
419
|
+
|
|
420
|
+
local output_match=true
|
|
421
|
+
[ "${old_summary}" = "${new_summary}" ] || output_match=false
|
|
422
|
+
|
|
423
|
+
local output_diff_json='null'
|
|
424
|
+
if [ "${output_match}" = "false" ]; then
|
|
425
|
+
output_diff_json="$(jq -nc \
|
|
426
|
+
--argjson old "${old_summary}" \
|
|
427
|
+
--argjson new "${new_summary}" \
|
|
428
|
+
'{old: $old, new: $new}')"
|
|
429
|
+
fi
|
|
430
|
+
|
|
431
|
+
jq -nc \
|
|
432
|
+
--arg event "replay_diff" \
|
|
433
|
+
--argjson step_index "${step_index}" \
|
|
434
|
+
--arg verb "${verb}" \
|
|
435
|
+
--argjson status_match "${status_match}" \
|
|
436
|
+
--arg old_status "${old_status}" \
|
|
437
|
+
--arg new_status "${new_status}" \
|
|
438
|
+
--argjson output_match "${output_match}" \
|
|
439
|
+
--argjson output_diff "${output_diff_json}" \
|
|
440
|
+
'{event: $event, step_index: $step_index, verb: $verb,
|
|
441
|
+
status_match: $status_match, old_status: $old_status, new_status: $new_status,
|
|
442
|
+
output_match: $output_match, output_diff: $output_diff}'
|
|
443
|
+
|
|
444
|
+
if [ "${status_match}" = "true" ] && [ "${output_match}" = "true" ]; then
|
|
445
|
+
return 0
|
|
446
|
+
fi
|
|
447
|
+
return 1
|
|
448
|
+
}
|
|
@@ -0,0 +1,210 @@
|
|
|
1
|
+
# scripts/lib/flow_record.sh — flow recorder library (Phase 9 part 1-iii).
|
|
2
|
+
#
|
|
3
|
+
# Three-fn API:
|
|
4
|
+
# flow_record_detect_password <name>
|
|
5
|
+
# Returns 0 if <name> matches /password/i (case-insensitive substring).
|
|
6
|
+
# Per locked decision S1: any name containing "password" (any case) is
|
|
7
|
+
# a password field; recorded value is replaced with ${secrets.password}.
|
|
8
|
+
#
|
|
9
|
+
# flow_record_transform <out-name>
|
|
10
|
+
# Reads codegen JS on stdin; emits flow YAML on stdout. <out-name> is
|
|
11
|
+
# the value of the YAML's `name:` field. Detects password fields per
|
|
12
|
+
# flow_record_detect_password; writes ${secrets.password} placeholder
|
|
13
|
+
# in their place. Emits one stderr audit line per redaction.
|
|
14
|
+
#
|
|
15
|
+
# flow_record_emit_step <verb> <inline-args-yaml>
|
|
16
|
+
# Helper: prints ` - <verb>: { ... }` step line. Used by transformer.
|
|
17
|
+
#
|
|
18
|
+
# Codegen JS patterns supported (per locked decision F6-a):
|
|
19
|
+
# 1. await page.goto('URL')
|
|
20
|
+
# 2. await page.getByRole('textbox', { name: 'X' }).click()
|
|
21
|
+
# 3. await page.getByRole('textbox', { name: 'X' }).fill('V')
|
|
22
|
+
# 4. await page.getByRole('button', { name: 'X' }).click()
|
|
23
|
+
# 5. await page.locator('SELECTOR').click() # CSS selector only
|
|
24
|
+
# 6. await page.locator('SELECTOR').fill('V') # CSS selector only
|
|
25
|
+
#
|
|
26
|
+
# Out-of-scope codegen patterns (skipped with TODO comment):
|
|
27
|
+
# - xpath= selectors
|
|
28
|
+
# - waitForLoadState
|
|
29
|
+
# - storageState (codegen's session-save)
|
|
30
|
+
#
|
|
31
|
+
# Adapter authors: this lib is INTERNAL to the flow runner; verb scripts MUST
|
|
32
|
+
# NOT source it directly (composition through scripts/browser-flow.sh).
|
|
33
|
+
|
|
34
|
+
[ -n "${BROWSER_SKILL_FLOW_RECORD_LOADED:-}" ] && return 0
|
|
35
|
+
readonly BROWSER_SKILL_FLOW_RECORD_LOADED=1
|
|
36
|
+
|
|
37
|
+
# Globals set by flow_record_transform; readable by callers post-run.
|
|
38
|
+
FLOW_RECORD_PASSWORD_REDACTIONS=0
|
|
39
|
+
FLOW_RECORD_STEP_COUNT=0
|
|
40
|
+
|
|
41
|
+
# flow_record_detect_password <name> → exit 0 if name contains "password"
|
|
42
|
+
# (case-insensitive). Used by transformer to swap recorded values for the
|
|
43
|
+
# ${secrets.password} placeholder before persisting.
|
|
44
|
+
flow_record_detect_password() {
|
|
45
|
+
local name="$1"
|
|
46
|
+
local lower="${name,,}"
|
|
47
|
+
case "${lower}" in
|
|
48
|
+
*password*) return 0 ;;
|
|
49
|
+
*) return 1 ;;
|
|
50
|
+
esac
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
# flow_record_emit_step <verb> <inline-args-yaml> — prints one step line.
|
|
54
|
+
# Inline args are the exact YAML flow-style body (e.g. '{ url: /foo }' or '{}').
|
|
55
|
+
flow_record_emit_step() {
|
|
56
|
+
local verb="$1"
|
|
57
|
+
local args_yaml="$2"
|
|
58
|
+
printf ' - %s: %s\n' "${verb}" "${args_yaml}"
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
# _flow_record_emit_snapshot_if_needed
|
|
62
|
+
# Emits a snapshot step if the previous emitted step wasn't already a
|
|
63
|
+
# snapshot. Tracks via a closure-style bash global PREV_STEP_VERB.
|
|
64
|
+
_flow_record_emit_snapshot_if_needed() {
|
|
65
|
+
if [ "${PREV_STEP_VERB:-}" != "snapshot" ]; then
|
|
66
|
+
flow_record_emit_step "snapshot" "{}"
|
|
67
|
+
PREV_STEP_VERB="snapshot"
|
|
68
|
+
FLOW_RECORD_STEP_COUNT=$((FLOW_RECORD_STEP_COUNT + 1))
|
|
69
|
+
fi
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
# flow_record_transform <out-name> → reads codegen JS on stdin; emits flow
|
|
73
|
+
# YAML on stdout. Detects password fields per locked decision S1; writes
|
|
74
|
+
# audit line on stderr per redaction.
|
|
75
|
+
#
|
|
76
|
+
# Returns 0 on success; 2 on malformed JS input.
|
|
77
|
+
flow_record_transform() {
|
|
78
|
+
local out_name="${1:-recorded}"
|
|
79
|
+
FLOW_RECORD_PASSWORD_REDACTIONS=0
|
|
80
|
+
FLOW_RECORD_STEP_COUNT=0
|
|
81
|
+
PREV_STEP_VERB=""
|
|
82
|
+
|
|
83
|
+
# Header.
|
|
84
|
+
printf 'name: %s\n' "${out_name}"
|
|
85
|
+
printf 'steps:\n'
|
|
86
|
+
|
|
87
|
+
local line
|
|
88
|
+
while IFS= read -r line; do
|
|
89
|
+
case "${line}" in
|
|
90
|
+
*"page.goto("*)
|
|
91
|
+
# Pattern 1: await page.goto('URL')
|
|
92
|
+
local url
|
|
93
|
+
url="$(_flow_record_extract_arg "${line}" 'goto')"
|
|
94
|
+
[ -n "${url}" ] && {
|
|
95
|
+
flow_record_emit_step "open" "{ url: ${url} }"
|
|
96
|
+
PREV_STEP_VERB="open"
|
|
97
|
+
FLOW_RECORD_STEP_COUNT=$((FLOW_RECORD_STEP_COUNT + 1))
|
|
98
|
+
}
|
|
99
|
+
;;
|
|
100
|
+
*"page.getByRole("*".click()"*|*"page.getByLabel("*".click()"*)
|
|
101
|
+
# Patterns 2 + 4: getByRole(... name: 'X' ...).click()
|
|
102
|
+
# OR getByLabel('X').click()
|
|
103
|
+
local accessible_name
|
|
104
|
+
accessible_name="$(_flow_record_extract_role_name "${line}")"
|
|
105
|
+
[ -z "${accessible_name}" ] && accessible_name="$(_flow_record_extract_label "${line}")"
|
|
106
|
+
[ -n "${accessible_name}" ] && {
|
|
107
|
+
_flow_record_emit_snapshot_if_needed
|
|
108
|
+
flow_record_emit_step "click" "{ ref: \${refs.${accessible_name}} }"
|
|
109
|
+
PREV_STEP_VERB="click"
|
|
110
|
+
FLOW_RECORD_STEP_COUNT=$((FLOW_RECORD_STEP_COUNT + 1))
|
|
111
|
+
}
|
|
112
|
+
;;
|
|
113
|
+
*"page.getByRole("*".fill("*|*"page.getByLabel("*".fill("*)
|
|
114
|
+
# Pattern 3: getByRole(... name: 'X' ...).fill('V')
|
|
115
|
+
local accessible_name fill_value out_value
|
|
116
|
+
accessible_name="$(_flow_record_extract_role_name "${line}")"
|
|
117
|
+
[ -z "${accessible_name}" ] && accessible_name="$(_flow_record_extract_label "${line}")"
|
|
118
|
+
fill_value="$(_flow_record_extract_arg "${line}" 'fill')"
|
|
119
|
+
[ -n "${accessible_name}" ] && {
|
|
120
|
+
_flow_record_emit_snapshot_if_needed
|
|
121
|
+
if flow_record_detect_password "${accessible_name}"; then
|
|
122
|
+
out_value='${secrets.password}'
|
|
123
|
+
FLOW_RECORD_PASSWORD_REDACTIONS=$((FLOW_RECORD_PASSWORD_REDACTIONS + 1))
|
|
124
|
+
printf 'flow record: redacted password field "%s" → ${secrets.password} placeholder\n' \
|
|
125
|
+
"${accessible_name}" >&2
|
|
126
|
+
else
|
|
127
|
+
out_value="${fill_value}"
|
|
128
|
+
fi
|
|
129
|
+
flow_record_emit_step "fill" "{ ref: \${refs.${accessible_name}}, text: ${out_value} }"
|
|
130
|
+
PREV_STEP_VERB="fill"
|
|
131
|
+
FLOW_RECORD_STEP_COUNT=$((FLOW_RECORD_STEP_COUNT + 1))
|
|
132
|
+
}
|
|
133
|
+
;;
|
|
134
|
+
*"page.locator("*"xpath="*)
|
|
135
|
+
# XPath selectors not supported (per locked decision F6-a).
|
|
136
|
+
printf ' # TODO(flow record): unsupported xpath selector — %s\n' \
|
|
137
|
+
"$(_flow_record_strip_leading_ws "${line}")"
|
|
138
|
+
;;
|
|
139
|
+
*"page.locator("*".click()"*)
|
|
140
|
+
# Pattern 5: locator('CSS').click()
|
|
141
|
+
local selector
|
|
142
|
+
selector="$(_flow_record_extract_arg "${line}" 'locator')"
|
|
143
|
+
[ -n "${selector}" ] && {
|
|
144
|
+
flow_record_emit_step "click" "{ selector: ${selector} }"
|
|
145
|
+
PREV_STEP_VERB="click"
|
|
146
|
+
FLOW_RECORD_STEP_COUNT=$((FLOW_RECORD_STEP_COUNT + 1))
|
|
147
|
+
}
|
|
148
|
+
;;
|
|
149
|
+
*"page.locator("*".fill("*)
|
|
150
|
+
# Pattern 6: locator('CSS').fill('V')
|
|
151
|
+
local selector fill_value
|
|
152
|
+
selector="$(_flow_record_extract_arg "${line}" 'locator')"
|
|
153
|
+
fill_value="$(_flow_record_extract_arg "${line}" 'fill')"
|
|
154
|
+
[ -n "${selector}" ] && [ -n "${fill_value}" ] && {
|
|
155
|
+
flow_record_emit_step "fill" "{ selector: ${selector}, text: ${fill_value} }"
|
|
156
|
+
PREV_STEP_VERB="fill"
|
|
157
|
+
FLOW_RECORD_STEP_COUNT=$((FLOW_RECORD_STEP_COUNT + 1))
|
|
158
|
+
}
|
|
159
|
+
;;
|
|
160
|
+
esac
|
|
161
|
+
done
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
# Helper: strip leading whitespace.
|
|
165
|
+
_flow_record_strip_leading_ws() {
|
|
166
|
+
local v="$1"
|
|
167
|
+
v="${v#"${v%%[![:space:]]*}"}"
|
|
168
|
+
printf '%s' "${v}"
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
# Helper: extract single-quoted arg from `<fn>(...)` invocation in a line.
|
|
172
|
+
# E.g. _flow_record_extract_arg "page.goto('https://example.com')" "goto"
|
|
173
|
+
# → https://example.com
|
|
174
|
+
_flow_record_extract_arg() {
|
|
175
|
+
local line="$1"
|
|
176
|
+
local fn="$2"
|
|
177
|
+
# Find `<fn>('...')` portion; extract between the first pair of single quotes.
|
|
178
|
+
local rest="${line#*${fn}(}"
|
|
179
|
+
case "${rest}" in
|
|
180
|
+
"'"*)
|
|
181
|
+
rest="${rest#\'}"
|
|
182
|
+
printf '%s' "${rest%%\'*}"
|
|
183
|
+
;;
|
|
184
|
+
*) printf '' ;;
|
|
185
|
+
esac
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
# Helper: extract `name: 'X'` value from getByRole(...) call.
|
|
189
|
+
_flow_record_extract_role_name() {
|
|
190
|
+
local line="$1"
|
|
191
|
+
case "${line}" in
|
|
192
|
+
*"name: '"*)
|
|
193
|
+
local rest="${line#*name: \'}"
|
|
194
|
+
printf '%s' "${rest%%\'*}"
|
|
195
|
+
;;
|
|
196
|
+
*) printf '' ;;
|
|
197
|
+
esac
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
# Helper: extract `getByLabel('X')` value.
|
|
201
|
+
_flow_record_extract_label() {
|
|
202
|
+
local line="$1"
|
|
203
|
+
case "${line}" in
|
|
204
|
+
*"getByLabel('"*)
|
|
205
|
+
local rest="${line#*getByLabel(\'}"
|
|
206
|
+
printf '%s' "${rest%%\'*}"
|
|
207
|
+
;;
|
|
208
|
+
*) printf '' ;;
|
|
209
|
+
esac
|
|
210
|
+
}
|