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,49 @@
|
|
|
1
|
+
# scripts/lib/mask.sh
|
|
2
|
+
# Reusable masking helper for rendering sensitive values safely.
|
|
3
|
+
#
|
|
4
|
+
# mask_string VAL [SHOW_FIRST=1] [SHOW_LAST=1]
|
|
5
|
+
# "password123" → "p*********3"
|
|
6
|
+
# "abc" → "a*c"
|
|
7
|
+
# "ab" / "x" / "" → "**" / "*" / "" (no leak; full mask)
|
|
8
|
+
# long (200 chars) → first + (cap 80 stars) + last
|
|
9
|
+
#
|
|
10
|
+
# Used by `creds show --reveal` to display a masked preview alongside the
|
|
11
|
+
# unmasked value, and reusable for any future verb that needs to render a
|
|
12
|
+
# sensitive value safely (e.g. show-credential's --masked default mode if it
|
|
13
|
+
# ever lands).
|
|
14
|
+
#
|
|
15
|
+
# Source from any verb / lib that needs to mask a string.
|
|
16
|
+
# Requires lib/common.sh sourced first (just for consistency; no deps used).
|
|
17
|
+
|
|
18
|
+
[ -n "${BROWSER_SKILL_MASK_LOADED:-}" ] && return 0
|
|
19
|
+
readonly BROWSER_SKILL_MASK_LOADED=1
|
|
20
|
+
|
|
21
|
+
readonly _MASK_MIDDLE_CAP=80
|
|
22
|
+
|
|
23
|
+
mask_string() {
|
|
24
|
+
local val="$1"
|
|
25
|
+
local show_first="${2:-1}"
|
|
26
|
+
local show_last="${3:-1}"
|
|
27
|
+
local len=${#val}
|
|
28
|
+
|
|
29
|
+
# Empty → empty.
|
|
30
|
+
if [ "${len}" -eq 0 ]; then
|
|
31
|
+
printf ''
|
|
32
|
+
return 0
|
|
33
|
+
fi
|
|
34
|
+
|
|
35
|
+
# If reveal-budget covers the whole string, refuse to leak any chars.
|
|
36
|
+
if [ "${len}" -le "$((show_first + show_last))" ]; then
|
|
37
|
+
printf '%*s' "${len}" '' | tr ' ' '*'
|
|
38
|
+
return 0
|
|
39
|
+
fi
|
|
40
|
+
|
|
41
|
+
local middle=$((len - show_first - show_last))
|
|
42
|
+
if [ "${middle}" -gt "${_MASK_MIDDLE_CAP}" ]; then
|
|
43
|
+
middle="${_MASK_MIDDLE_CAP}"
|
|
44
|
+
fi
|
|
45
|
+
|
|
46
|
+
local stars
|
|
47
|
+
stars="$(printf '%*s' "${middle}" '' | tr ' ' '*')"
|
|
48
|
+
printf '%s%s%s' "${val:0:${show_first}}" "${stars}" "${val: -${show_last}}"
|
|
49
|
+
}
|
|
@@ -0,0 +1,427 @@
|
|
|
1
|
+
# shellcheck shell=bash
|
|
2
|
+
# scripts/lib/memory.sh
|
|
3
|
+
# Phase 11 part 1-i — per-archetype selector/action cache I/O foundation.
|
|
4
|
+
# Pure read/write API; no verb integration (deferred to 11-1-ii browser-do).
|
|
5
|
+
# Storage shape per design doc 2026-05-08-phase-11-memory-design.md §4.
|
|
6
|
+
#
|
|
7
|
+
# Requires lib/common.sh sourced first (init_paths must have run; uses
|
|
8
|
+
# BROWSER_SKILL_HOME, file_mode, now_iso, assert_safe_name, die, EXIT_*).
|
|
9
|
+
|
|
10
|
+
[ -n "${BROWSER_SKILL_MEMORY_LOADED:-}" ] && return 0
|
|
11
|
+
readonly BROWSER_SKILL_MEMORY_LOADED=1
|
|
12
|
+
|
|
13
|
+
# --- Internal path helpers (memory-scoped; not exported to common.sh) ---
|
|
14
|
+
|
|
15
|
+
_memory_dir() {
|
|
16
|
+
printf '%s/memory' "${BROWSER_SKILL_HOME}"
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
_memory_site_dir() {
|
|
20
|
+
printf '%s/%s' "$(_memory_dir)" "$1"
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
_memory_patterns_path() {
|
|
24
|
+
printf '%s/patterns.json' "$(_memory_site_dir "$1")"
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
_memory_archetype_path() {
|
|
28
|
+
printf '%s/archetypes/%s.json' "$(_memory_site_dir "$1")" "$2"
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
_memory_node_resolver_path() {
|
|
32
|
+
local lib_dir
|
|
33
|
+
lib_dir="$(dirname "${BASH_SOURCE[0]}")"
|
|
34
|
+
printf '%s/node/url-pattern-resolver.mjs' "${lib_dir}"
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
# --- Init ---
|
|
38
|
+
|
|
39
|
+
# memory_init_dir
|
|
40
|
+
# mkdir -p ${BROWSER_SKILL_HOME}/memory + chmod 700. Idempotent.
|
|
41
|
+
# Lazy-creation pattern (mirror Phase 7 captures/, Phase 9-1-v baselines.json).
|
|
42
|
+
memory_init_dir() {
|
|
43
|
+
local dir
|
|
44
|
+
dir="$(_memory_dir)"
|
|
45
|
+
mkdir -p "${dir}"
|
|
46
|
+
chmod 700 "${dir}"
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
# Internal: ensure per-site dir + archetypes/ subdir exist with mode 0700.
|
|
50
|
+
_memory_ensure_site_dir() {
|
|
51
|
+
local site="$1"
|
|
52
|
+
memory_init_dir
|
|
53
|
+
local site_dir
|
|
54
|
+
site_dir="$(_memory_site_dir "${site}")"
|
|
55
|
+
mkdir -p "${site_dir}/archetypes"
|
|
56
|
+
chmod 700 "${site_dir}" "${site_dir}/archetypes"
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
# Internal: atomic JSON write at PATH with mode 0600.
|
|
60
|
+
_memory_write_json() {
|
|
61
|
+
local path="$1" json="$2"
|
|
62
|
+
local tmp="${path}.tmp.$$"
|
|
63
|
+
( umask 077; printf '%s\n' "${json}" | jq . > "${tmp}" )
|
|
64
|
+
chmod 600 "${tmp}"
|
|
65
|
+
mv "${tmp}" "${path}"
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
# --- Archetype I/O ---
|
|
69
|
+
|
|
70
|
+
# memory_load_archetype SITE ARCHETYPE_ID
|
|
71
|
+
# Echoes the archetype JSON exactly as on disk, or empty string if missing.
|
|
72
|
+
memory_load_archetype() {
|
|
73
|
+
local path
|
|
74
|
+
path="$(_memory_archetype_path "$1" "$2")"
|
|
75
|
+
if [ -f "${path}" ]; then
|
|
76
|
+
cat "${path}"
|
|
77
|
+
fi
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
# memory_save_archetype SITE ARCHETYPE_ID JSON
|
|
81
|
+
# Validates JSON, atomic-writes mode 0600. Caller is responsible for shape;
|
|
82
|
+
# this lib only validates "is it valid JSON".
|
|
83
|
+
memory_save_archetype() {
|
|
84
|
+
local site="$1" id="$2" json="$3"
|
|
85
|
+
assert_safe_name "${site}" "site-name"
|
|
86
|
+
assert_safe_name "${id}" "archetype-id"
|
|
87
|
+
if ! printf '%s' "${json}" | jq -e . >/dev/null 2>&1; then
|
|
88
|
+
die "${EXIT_USAGE_ERROR}" "memory_save_archetype: invalid JSON"
|
|
89
|
+
fi
|
|
90
|
+
_memory_ensure_site_dir "${site}"
|
|
91
|
+
_memory_write_json "$(_memory_archetype_path "${site}" "${id}")" "${json}"
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
# --- Lookup ---
|
|
95
|
+
|
|
96
|
+
# memory_lookup SITE ARCHETYPE_ID INTENT
|
|
97
|
+
# Echoes the cached selector for (site, archetype, intent), or empty on miss.
|
|
98
|
+
# Disabled interactions (self-heal) are skipped — they're effectively absent
|
|
99
|
+
# from the cache until 11-1-iii's loop re-resolves and overwrites.
|
|
100
|
+
memory_lookup() {
|
|
101
|
+
local path
|
|
102
|
+
path="$(_memory_archetype_path "$1" "$2")"
|
|
103
|
+
[ -f "${path}" ] || return 0
|
|
104
|
+
jq -r --arg intent "$3" '
|
|
105
|
+
(.interactions // [])
|
|
106
|
+
| map(select(.intent == $intent and (.disabled // false) == false))
|
|
107
|
+
| if length == 0 then "" else .[0].selector end
|
|
108
|
+
' "${path}"
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
# --- Recording ---
|
|
112
|
+
|
|
113
|
+
# memory_record SITE ARCHETYPE_ID INTENT SELECTOR
|
|
114
|
+
# Upserts an interaction. New intent → first_used+last_used set, success_count:1,
|
|
115
|
+
# fail_count:0, disabled:false, self_heal_history:[]. Existing intent →
|
|
116
|
+
# selector overwritten, last_used advances, success_count++; first_used preserved.
|
|
117
|
+
# Bumps archetype's use_count + last_seen.
|
|
118
|
+
memory_record() {
|
|
119
|
+
local site="$1" id="$2" intent="$3" selector="$4"
|
|
120
|
+
local path now updated
|
|
121
|
+
path="$(_memory_archetype_path "${site}" "${id}")"
|
|
122
|
+
if [ ! -f "${path}" ]; then
|
|
123
|
+
die "${EXIT_USAGE_ERROR}" "memory_record: archetype not found: ${site}/${id}"
|
|
124
|
+
fi
|
|
125
|
+
now="$(now_iso)"
|
|
126
|
+
updated="$(jq --arg intent "${intent}" --arg sel "${selector}" --arg now "${now}" '
|
|
127
|
+
.interactions = (
|
|
128
|
+
if ((.interactions // []) | map(select(.intent == $intent)) | length) > 0 then
|
|
129
|
+
(.interactions | map(
|
|
130
|
+
if .intent == $intent then
|
|
131
|
+
.selector = $sel
|
|
132
|
+
| .last_used = $now
|
|
133
|
+
| .success_count = (.success_count + 1)
|
|
134
|
+
# Self-heal (Phase 11 1-iii D2): a successful re-record clears
|
|
135
|
+
# any prior failure state. This is what "agent re-resolved →
|
|
136
|
+
# cache heals" means at the storage layer.
|
|
137
|
+
#
|
|
138
|
+
# Pick A5: log the disabled→enabled transition. Check BEFORE
|
|
139
|
+
# resetting so the entry can capture the pre-reset fail_count.
|
|
140
|
+
| (if (.disabled // false) == true then
|
|
141
|
+
.self_heal_history = ((.self_heal_history // []) + [{
|
|
142
|
+
ts: $now, event: "healed",
|
|
143
|
+
fail_count: (.fail_count // 0),
|
|
144
|
+
selector_at_time: $sel
|
|
145
|
+
}])
|
|
146
|
+
else . end)
|
|
147
|
+
| .fail_count = 0
|
|
148
|
+
| .disabled = false
|
|
149
|
+
else . end
|
|
150
|
+
))
|
|
151
|
+
else
|
|
152
|
+
((.interactions // []) + [{
|
|
153
|
+
intent: $intent, selector: $sel,
|
|
154
|
+
first_used: $now, last_used: $now,
|
|
155
|
+
success_count: 1, fail_count: 0,
|
|
156
|
+
disabled: false, self_heal_history: []
|
|
157
|
+
}])
|
|
158
|
+
end
|
|
159
|
+
)
|
|
160
|
+
| .last_seen = $now
|
|
161
|
+
| .use_count = ((.use_count // 0) + 1)
|
|
162
|
+
' "${path}")"
|
|
163
|
+
memory_save_archetype "${site}" "${id}" "${updated}"
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
# memory_record_failure SITE ARCHETYPE_ID INTENT
|
|
167
|
+
# Increments fail_count on the matching interaction. Sets disabled:true once
|
|
168
|
+
# fail_count > 3 (H1 threshold from design doc §3). No-op if intent absent —
|
|
169
|
+
# callers should only invoke after a lookup hit.
|
|
170
|
+
memory_record_failure() {
|
|
171
|
+
local site="$1" id="$2" intent="$3"
|
|
172
|
+
local path now updated
|
|
173
|
+
path="$(_memory_archetype_path "${site}" "${id}")"
|
|
174
|
+
if [ ! -f "${path}" ]; then
|
|
175
|
+
die "${EXIT_USAGE_ERROR}" "memory_record_failure: archetype not found: ${site}/${id}"
|
|
176
|
+
fi
|
|
177
|
+
now="$(now_iso)"
|
|
178
|
+
updated="$(jq --arg intent "${intent}" --arg now "${now}" '
|
|
179
|
+
.interactions = ((.interactions // []) | map(
|
|
180
|
+
if .intent == $intent then
|
|
181
|
+
.fail_count = ((.fail_count // 0) + 1)
|
|
182
|
+
| .last_used = $now
|
|
183
|
+
# Pick A5: log the enabled→disabled transition (single-shot). Append
|
|
184
|
+
# ONLY when the new fail_count crosses the threshold AND the prior
|
|
185
|
+
# .disabled was not already true. Subsequent failures past the
|
|
186
|
+
# threshold do not double-log.
|
|
187
|
+
| (if (.fail_count > 3) and ((.disabled // false) == false) then
|
|
188
|
+
.self_heal_history = ((.self_heal_history // []) + [{
|
|
189
|
+
ts: $now, event: "disabled",
|
|
190
|
+
fail_count: .fail_count,
|
|
191
|
+
selector_at_time: .selector
|
|
192
|
+
}])
|
|
193
|
+
else . end)
|
|
194
|
+
| .disabled = (.fail_count > 3)
|
|
195
|
+
else . end
|
|
196
|
+
))
|
|
197
|
+
' "${path}")"
|
|
198
|
+
memory_save_archetype "${site}" "${id}" "${updated}"
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
# --- Pattern I/O ---
|
|
202
|
+
|
|
203
|
+
# memory_record_pattern SITE URL_PATTERN ARCHETYPE_ID
|
|
204
|
+
# Upserts a (url_pattern, archetype_id) pair into <site>/patterns.json.
|
|
205
|
+
# Idempotent: same canonical url_pattern → bumps last_seen + hit_count.
|
|
206
|
+
#
|
|
207
|
+
# Pick A4 — pattern-equivalence canonicalization: `:NAME` segments differ
|
|
208
|
+
# in name (`/devices/:id` vs `/devices/:itemId`) but describe the SAME URL
|
|
209
|
+
# family. Idempotency uses the CANONICAL form (`:NAME` → `:_` collapse)
|
|
210
|
+
# for compare; the original url_pattern + archetype_id are preserved in
|
|
211
|
+
# storage (first-write wins on canonical match — subsequent records bump
|
|
212
|
+
# hit_count, archetype_id unchanged even if new record specified a
|
|
213
|
+
# different one).
|
|
214
|
+
memory_record_pattern() {
|
|
215
|
+
local site="$1" url_pattern="$2" arch_id="$3"
|
|
216
|
+
assert_safe_name "${site}" "site-name"
|
|
217
|
+
assert_safe_name "${arch_id}" "archetype-id"
|
|
218
|
+
_memory_ensure_site_dir "${site}"
|
|
219
|
+
|
|
220
|
+
local patterns_path now current updated
|
|
221
|
+
patterns_path="$(_memory_patterns_path "${site}")"
|
|
222
|
+
now="$(now_iso)"
|
|
223
|
+
|
|
224
|
+
if [ -f "${patterns_path}" ]; then
|
|
225
|
+
current="$(cat "${patterns_path}")"
|
|
226
|
+
else
|
|
227
|
+
current='{"schema_version":1,"patterns":[]}'
|
|
228
|
+
fi
|
|
229
|
+
|
|
230
|
+
updated="$(printf '%s' "${current}" | jq \
|
|
231
|
+
--arg p "${url_pattern}" --arg arch "${arch_id}" --arg now "${now}" '
|
|
232
|
+
# Pick A4: canonical-pattern helper. Collapses all `:NAME` segments to
|
|
233
|
+
# `:_` so /devices/:id and /devices/:itemId match on compare. The regex
|
|
234
|
+
# mirrors the JS resolver helper (`/:[A-Za-z_][\w$]*/g`).
|
|
235
|
+
def _canonical: gsub(":[A-Za-z_][A-Za-z0-9_]*"; ":_");
|
|
236
|
+
($p | _canonical) as $pc
|
|
237
|
+
| if ((.patterns // []) | map(select((.url_pattern | _canonical) == $pc)) | length) > 0 then
|
|
238
|
+
.patterns |= map(
|
|
239
|
+
if (.url_pattern | _canonical) == $pc then
|
|
240
|
+
.last_seen = $now | .hit_count = ((.hit_count // 0) + 1)
|
|
241
|
+
else . end
|
|
242
|
+
)
|
|
243
|
+
else
|
|
244
|
+
.patterns = ((.patterns // []) + [{
|
|
245
|
+
url_pattern: $p, archetype_id: $arch,
|
|
246
|
+
first_seen: $now, last_seen: $now, hit_count: 1
|
|
247
|
+
}])
|
|
248
|
+
end
|
|
249
|
+
')"
|
|
250
|
+
|
|
251
|
+
_memory_write_json "${patterns_path}" "${updated}"
|
|
252
|
+
}
|
|
253
|
+
|
|
254
|
+
# --- Pick A6: passive navigation observation log (recent_urls.jsonl) ---
|
|
255
|
+
|
|
256
|
+
# memory_record_recent_url SITE URL VERB
|
|
257
|
+
# Append one observation row to ${BROWSER_SKILL_HOME}/memory/recent_urls.jsonl
|
|
258
|
+
# (mode 0600 in mode 0700 memory/). Shape: {ts, url, verb, site, schema_version:1}.
|
|
259
|
+
# Best-effort writer — failure emits warn: and continues; never taints caller
|
|
260
|
+
# exit code. Same convention as browser-do.sh::_record_event (PR #115 events.jsonl).
|
|
261
|
+
#
|
|
262
|
+
# Schema starts at v1 from inception; no migrator needed until shape changes.
|
|
263
|
+
# (lib/migrators/recent_urls/ stays empty until a future bump.)
|
|
264
|
+
memory_record_recent_url() {
|
|
265
|
+
local site="$1" url="$2" verb="$3"
|
|
266
|
+
local events_dir events_file ts line
|
|
267
|
+
events_dir="$(_memory_dir)"
|
|
268
|
+
events_file="${events_dir}/recent_urls.jsonl"
|
|
269
|
+
ts="$(now_iso)"
|
|
270
|
+
|
|
271
|
+
if ! mkdir -p "${events_dir}" 2>/dev/null; then
|
|
272
|
+
warn "memory_record_recent_url: mkdir failed (best-effort; navigation unaffected)"
|
|
273
|
+
return 0
|
|
274
|
+
fi
|
|
275
|
+
chmod 700 "${events_dir}" 2>/dev/null || true
|
|
276
|
+
|
|
277
|
+
if ! line="$(jq -nc \
|
|
278
|
+
--arg ts "${ts}" --arg url "${url}" --arg verb "${verb}" --arg site "${site}" \
|
|
279
|
+
'{ts:$ts, url:$url, verb:$verb, site:$site, schema_version:1}' 2>/dev/null)"; then
|
|
280
|
+
warn "memory_record_recent_url: encode failed (best-effort; navigation unaffected)"
|
|
281
|
+
return 0
|
|
282
|
+
fi
|
|
283
|
+
|
|
284
|
+
# O_APPEND atomicity for short JSON lines (well below PIPE_BUF 4KB).
|
|
285
|
+
if ! printf '%s\n' "${line}" >> "${events_file}" 2>/dev/null; then
|
|
286
|
+
warn "memory_record_recent_url: append failed (best-effort; navigation unaffected)"
|
|
287
|
+
return 0
|
|
288
|
+
fi
|
|
289
|
+
chmod 600 "${events_file}" 2>/dev/null || true
|
|
290
|
+
}
|
|
291
|
+
|
|
292
|
+
# --- Phase 13: weak-fingerprint selector rescue --------------------------
|
|
293
|
+
|
|
294
|
+
# _memory_parse_selector_to_fp SELECTOR
|
|
295
|
+
# Echo a JSON fingerprint {tag, classes, attrs} derived from a CSS selector
|
|
296
|
+
# string. "Weak" — handles the common shapes recorded by browser-do record:
|
|
297
|
+
# `tag`, `tag.class`, `tag.class1.class2`, `#id`, `[name=value]`, mixed.
|
|
298
|
+
# Combinators (`>`, `+`, `~`), pseudo-classes (`:hover`), attribute operators
|
|
299
|
+
# (`^=`, `*=`, etc.) are not parsed — the resulting fingerprint will simply be
|
|
300
|
+
# weaker, and the JS scorer falls back gracefully (score < threshold = miss).
|
|
301
|
+
_memory_parse_selector_to_fp() {
|
|
302
|
+
local sel="$1"
|
|
303
|
+
local tag="*" id="" classes=()
|
|
304
|
+
if [[ "${sel}" =~ ^([a-zA-Z][a-zA-Z0-9]*) ]]; then
|
|
305
|
+
tag="${BASH_REMATCH[1]^^}"
|
|
306
|
+
fi
|
|
307
|
+
local rest="${sel}"
|
|
308
|
+
while [[ "${rest}" =~ \.([A-Za-z][A-Za-z0-9_-]*) ]]; do
|
|
309
|
+
classes+=("${BASH_REMATCH[1]}")
|
|
310
|
+
rest="${rest/${BASH_REMATCH[0]}/}"
|
|
311
|
+
done
|
|
312
|
+
if [[ "${sel}" =~ \#([A-Za-z][A-Za-z0-9_-]*) ]]; then
|
|
313
|
+
id="${BASH_REMATCH[1]}"
|
|
314
|
+
fi
|
|
315
|
+
local classes_json="[]"
|
|
316
|
+
if [ "${#classes[@]}" -gt 0 ]; then
|
|
317
|
+
classes_json="$(printf '%s\n' "${classes[@]}" | jq -R . | jq -sc .)"
|
|
318
|
+
fi
|
|
319
|
+
jq -nc --arg tag "${tag}" --arg id "${id}" --argjson cls "${classes_json}" '
|
|
320
|
+
{tag: $tag,
|
|
321
|
+
classes: $cls,
|
|
322
|
+
attrs: (if $id != "" then {id: $id} else {} end)}'
|
|
323
|
+
}
|
|
324
|
+
|
|
325
|
+
# memory_fingerprint_rescue SITE ARCHETYPE_ID INTENT CACHED_SELECTOR
|
|
326
|
+
# Echo the rescued selector on hit, empty on miss. Best-effort — failures emit
|
|
327
|
+
# warn: and return 0 without rescuing.
|
|
328
|
+
# Threshold defaults to 0.70; override per-session via BROWSER_DO_RESCUE_THRESHOLD.
|
|
329
|
+
# Algorithm + selector synthesis lives in scripts/lib/fingerprint-rescue.js.
|
|
330
|
+
# SITE/ARCHETYPE/INTENT are reserved for future strong-fingerprint mode that
|
|
331
|
+
# reads archetype-stored fingerprint dimensions; the weak v1 only needs the
|
|
332
|
+
# cached selector itself.
|
|
333
|
+
memory_fingerprint_rescue() {
|
|
334
|
+
local site="$1" archetype="$2" intent="$3" cached_selector="$4"
|
|
335
|
+
: "${site}" "${archetype}" "${intent}" # silence SC2034 — strong-fp v2 will use
|
|
336
|
+
local threshold="${BROWSER_DO_RESCUE_THRESHOLD:-0.70}"
|
|
337
|
+
local script_dir lib_dir js_template js_payload
|
|
338
|
+
lib_dir="$(dirname "${BASH_SOURCE[0]}")"
|
|
339
|
+
script_dir="$(cd "${lib_dir}/.." && pwd)"
|
|
340
|
+
js_template="${lib_dir}/fingerprint-rescue.js"
|
|
341
|
+
[ -f "${js_template}" ] || { warn "memory_fingerprint_rescue: missing ${js_template}"; return 0; }
|
|
342
|
+
|
|
343
|
+
local fp_json
|
|
344
|
+
fp_json="$(_memory_parse_selector_to_fp "${cached_selector}" 2>/dev/null || printf '')"
|
|
345
|
+
[ -z "${fp_json}" ] && return 0
|
|
346
|
+
|
|
347
|
+
# Prepend the two constants the JS payload reads.
|
|
348
|
+
js_payload="const __FP=${fp_json};const __TH=${threshold};$(cat "${js_template}")"
|
|
349
|
+
|
|
350
|
+
# Invoke browser-extract --eval; capture stdout. Best-effort — adapter
|
|
351
|
+
# failure / no daemon / no current page → empty rescue, fall through to
|
|
352
|
+
# the existing fail_count path.
|
|
353
|
+
local out
|
|
354
|
+
if ! out="$(bash "${script_dir}/browser-extract.sh" --eval "${js_payload}" 2>/dev/null)"; then
|
|
355
|
+
return 0
|
|
356
|
+
fi
|
|
357
|
+
|
|
358
|
+
# Streaming output: pick the line carrying {rescued_selector:...}. Tolerant
|
|
359
|
+
# of multiple lines (events + summary); jq returns empty if the field is null.
|
|
360
|
+
local rescued
|
|
361
|
+
rescued="$(printf '%s\n' "${out}" \
|
|
362
|
+
| jq -rs '
|
|
363
|
+
map(select(type=="object" and has("rescued_selector"))) as $hits
|
|
364
|
+
| if ($hits | length) == 0 then ""
|
|
365
|
+
else
|
|
366
|
+
($hits | last) as $h
|
|
367
|
+
| if ($h.rescued_selector == null) then ""
|
|
368
|
+
else $h.rescued_selector end
|
|
369
|
+
end' 2>/dev/null || printf '')"
|
|
370
|
+
printf '%s' "${rescued}"
|
|
371
|
+
}
|
|
372
|
+
|
|
373
|
+
# memory_record_heal SITE ARCHETYPE_ID INTENT FROM_SELECTOR TO_SELECTOR
|
|
374
|
+
# Overwrite an interaction's cached selector after a successful fingerprint
|
|
375
|
+
# rescue + retry. Resets fail_count, bumps success_count, appends a "rescued"
|
|
376
|
+
# entry to self_heal_history. Distinct from memory_record so the audit trail
|
|
377
|
+
# can tell LLM-resolution (memory_record) from pre-LLM fingerprint rescue
|
|
378
|
+
# (memory_record_heal).
|
|
379
|
+
memory_record_heal() {
|
|
380
|
+
local site="$1" id="$2" intent="$3" from_sel="$4" to_sel="$5"
|
|
381
|
+
local path now updated
|
|
382
|
+
path="$(_memory_archetype_path "${site}" "${id}")"
|
|
383
|
+
if [ ! -f "${path}" ]; then
|
|
384
|
+
die "${EXIT_USAGE_ERROR}" "memory_record_heal: archetype not found: ${site}/${id}"
|
|
385
|
+
fi
|
|
386
|
+
now="$(now_iso)"
|
|
387
|
+
updated="$(jq --arg intent "${intent}" --arg from "${from_sel}" \
|
|
388
|
+
--arg to "${to_sel}" --arg now "${now}" '
|
|
389
|
+
.interactions = ((.interactions // []) | map(
|
|
390
|
+
if .intent == $intent then
|
|
391
|
+
.selector = $to
|
|
392
|
+
| .last_used = $now
|
|
393
|
+
| .success_count = ((.success_count // 0) + 1)
|
|
394
|
+
| .fail_count = 0
|
|
395
|
+
| .disabled = false
|
|
396
|
+
| .self_heal_history = ((.self_heal_history // []) + [{
|
|
397
|
+
ts: $now,
|
|
398
|
+
event: "rescued",
|
|
399
|
+
from_selector: $from,
|
|
400
|
+
to_selector: $to
|
|
401
|
+
}])
|
|
402
|
+
else . end
|
|
403
|
+
))
|
|
404
|
+
| .last_seen = $now
|
|
405
|
+
| .use_count = ((.use_count // 0) + 1)
|
|
406
|
+
' "${path}")"
|
|
407
|
+
memory_save_archetype "${site}" "${id}" "${updated}"
|
|
408
|
+
}
|
|
409
|
+
|
|
410
|
+
# memory_resolve_archetype SITE URL
|
|
411
|
+
# Echoes the archetype_id for the first matching url_pattern in <site>/patterns.json.
|
|
412
|
+
# Empty on miss (or if patterns.json is absent). URLPattern resolution is
|
|
413
|
+
# delegated to scripts/lib/node/url-pattern-resolver.mjs (Node 20+ web standard).
|
|
414
|
+
memory_resolve_archetype() {
|
|
415
|
+
local site="$1" url="$2"
|
|
416
|
+
local patterns_path
|
|
417
|
+
patterns_path="$(_memory_patterns_path "${site}")"
|
|
418
|
+
[ -f "${patterns_path}" ] || return 0
|
|
419
|
+
|
|
420
|
+
local input result
|
|
421
|
+
input="$(jq -nc --slurpfile p "${patterns_path}" --arg url "${url}" \
|
|
422
|
+
'{patterns: ($p[0].patterns // []), url: $url}')"
|
|
423
|
+
result="$(printf '%s' "${input}" | node "$(_memory_node_resolver_path)")"
|
|
424
|
+
if [ -n "${result}" ] && [ "${result}" != "null" ]; then
|
|
425
|
+
printf '%s' "${result}" | jq -r '.archetype_id // empty'
|
|
426
|
+
fi
|
|
427
|
+
}
|