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.
Files changed (117) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +144 -0
  3. package/SECURITY.md +39 -0
  4. package/SKILL.md +206 -0
  5. package/bin/cli.mjs +55 -0
  6. package/install.sh +143 -0
  7. package/package.json +54 -0
  8. package/references/adapter-candidates.md +40 -0
  9. package/references/browser-mcp-cheatsheet.md +132 -0
  10. package/references/browser-stats-cheatsheet.md +155 -0
  11. package/references/chrome-devtools-mcp-cheatsheet.md +232 -0
  12. package/references/midscene-integration.md +359 -0
  13. package/references/obscura-cheatsheet.md +103 -0
  14. package/references/playwright-cli-cheatsheet.md +64 -0
  15. package/references/playwright-lib-cheatsheet.md +90 -0
  16. package/references/recipes/add-a-tool-adapter.md +134 -0
  17. package/references/recipes/agent-workflows/README.md +37 -0
  18. package/references/recipes/agent-workflows/cache-driven-bulk-operation.md +110 -0
  19. package/references/recipes/agent-workflows/flow-record-and-replay.md +102 -0
  20. package/references/recipes/agent-workflows/incremental-pattern-discovery.md +125 -0
  21. package/references/recipes/agent-workflows/login-then-scrape.md +100 -0
  22. package/references/recipes/anti-patterns-tool-extension.md +182 -0
  23. package/references/recipes/body-bytes-not-body.md +139 -0
  24. package/references/recipes/cache-write-security.md +210 -0
  25. package/references/recipes/fingerprint-rescue.md +154 -0
  26. package/references/recipes/model-routing.md +143 -0
  27. package/references/recipes/path-security.md +138 -0
  28. package/references/recipes/privacy-canary.md +96 -0
  29. package/references/recipes/visual-rescue-hook.md +182 -0
  30. package/references/stats-prices.json +42 -0
  31. package/references/stats-schema.json +77 -0
  32. package/references/tool-versions.md +8 -0
  33. package/scripts/browser-add-site.sh +113 -0
  34. package/scripts/browser-assert.sh +106 -0
  35. package/scripts/browser-audit.sh +68 -0
  36. package/scripts/browser-baseline.sh +135 -0
  37. package/scripts/browser-click.sh +100 -0
  38. package/scripts/browser-creds-add.sh +254 -0
  39. package/scripts/browser-creds-list.sh +67 -0
  40. package/scripts/browser-creds-migrate.sh +122 -0
  41. package/scripts/browser-creds-remove.sh +69 -0
  42. package/scripts/browser-creds-rotate-totp.sh +109 -0
  43. package/scripts/browser-creds-show.sh +82 -0
  44. package/scripts/browser-creds-totp.sh +94 -0
  45. package/scripts/browser-do.sh +630 -0
  46. package/scripts/browser-doctor.sh +365 -0
  47. package/scripts/browser-drag.sh +90 -0
  48. package/scripts/browser-extract.sh +192 -0
  49. package/scripts/browser-fill.sh +142 -0
  50. package/scripts/browser-flow.sh +316 -0
  51. package/scripts/browser-history.sh +187 -0
  52. package/scripts/browser-hover.sh +92 -0
  53. package/scripts/browser-inspect.sh +188 -0
  54. package/scripts/browser-list-sessions.sh +78 -0
  55. package/scripts/browser-list-sites.sh +42 -0
  56. package/scripts/browser-login.sh +279 -0
  57. package/scripts/browser-mcp.sh +65 -0
  58. package/scripts/browser-migrate.sh +195 -0
  59. package/scripts/browser-open.sh +134 -0
  60. package/scripts/browser-press.sh +80 -0
  61. package/scripts/browser-remove-session.sh +72 -0
  62. package/scripts/browser-remove-site.sh +68 -0
  63. package/scripts/browser-replay.sh +206 -0
  64. package/scripts/browser-route.sh +174 -0
  65. package/scripts/browser-select.sh +122 -0
  66. package/scripts/browser-show-session.sh +57 -0
  67. package/scripts/browser-show-site.sh +37 -0
  68. package/scripts/browser-snapshot.sh +176 -0
  69. package/scripts/browser-stats.sh +522 -0
  70. package/scripts/browser-tab-close.sh +112 -0
  71. package/scripts/browser-tab-list.sh +70 -0
  72. package/scripts/browser-tab-switch.sh +111 -0
  73. package/scripts/browser-upload.sh +132 -0
  74. package/scripts/browser-use.sh +60 -0
  75. package/scripts/browser-vlm.sh +707 -0
  76. package/scripts/browser-wait.sh +97 -0
  77. package/scripts/install-git-hooks.sh +16 -0
  78. package/scripts/lib/capture.sh +356 -0
  79. package/scripts/lib/common.sh +262 -0
  80. package/scripts/lib/credential.sh +237 -0
  81. package/scripts/lib/fingerprint-rescue.js +123 -0
  82. package/scripts/lib/flow.sh +448 -0
  83. package/scripts/lib/flow_record.sh +210 -0
  84. package/scripts/lib/mask.sh +49 -0
  85. package/scripts/lib/memory.sh +427 -0
  86. package/scripts/lib/migrate.sh +390 -0
  87. package/scripts/lib/migrators/README.md +23 -0
  88. package/scripts/lib/migrators/memory/v1_to_v2.sh +15 -0
  89. package/scripts/lib/migrators/recent_urls/README.md +13 -0
  90. package/scripts/lib/migrators/stats/README.md +24 -0
  91. package/scripts/lib/node/chrome-devtools-bridge.mjs +1812 -0
  92. package/scripts/lib/node/mcp-server.mjs +531 -0
  93. package/scripts/lib/node/mcp-tools.json +68 -0
  94. package/scripts/lib/node/playwright-driver.mjs +1104 -0
  95. package/scripts/lib/node/totp-core.mjs +52 -0
  96. package/scripts/lib/node/totp.mjs +52 -0
  97. package/scripts/lib/node/url-pattern-cluster.mjs +102 -0
  98. package/scripts/lib/node/url-pattern-resolver.mjs +77 -0
  99. package/scripts/lib/output.sh +79 -0
  100. package/scripts/lib/router.sh +342 -0
  101. package/scripts/lib/sanitize.sh +107 -0
  102. package/scripts/lib/secret/keychain.sh +91 -0
  103. package/scripts/lib/secret/libsecret.sh +74 -0
  104. package/scripts/lib/secret/plaintext.sh +75 -0
  105. package/scripts/lib/secret_backend_select.sh +57 -0
  106. package/scripts/lib/session.sh +153 -0
  107. package/scripts/lib/site.sh +126 -0
  108. package/scripts/lib/stats.sh +419 -0
  109. package/scripts/lib/tool/.gitkeep +0 -0
  110. package/scripts/lib/tool/chrome-devtools-mcp.sh +349 -0
  111. package/scripts/lib/tool/obscura.sh +249 -0
  112. package/scripts/lib/tool/playwright-cli.sh +155 -0
  113. package/scripts/lib/tool/playwright-lib.sh +106 -0
  114. package/scripts/lib/verb_helpers.sh +222 -0
  115. package/scripts/lib/visual-rescue-default.sh +145 -0
  116. package/scripts/regenerate-docs.sh +99 -0
  117. 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
+ }