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,390 @@
1
+ # shellcheck shell=bash
2
+ # scripts/lib/migrate.sh
3
+ # Phase 10 part 1-i — schema migration foundation.
4
+ # Per-schema versions in versions.json; per-schema atomic-swap with backup;
5
+ # manual rollback. Pure read/write API; no verb integration (10-1-ii) and no
6
+ # real migrators registered (10-1-iii).
7
+ #
8
+ # Per design doc 2026-05-11-phase-10-schema-migration-design.md:
9
+ # MIG1 — per-schema versions (not global)
10
+ # MIG2 — registry directory: scripts/lib/migrators/<schema>/v<from>_to_<to>.sh
11
+ # BROWSER_SKILL_MIGRATORS_DIR env override = test-only seam
12
+ # MIG3 — atomic write + backup; jq -e validation; refuse swap on bad output
13
+ # MIG5 — pure bash + jq; no Node
14
+ #
15
+ # Requires lib/common.sh sourced first (uses BROWSER_SKILL_HOME, file_mode,
16
+ # now_iso, assert_safe_name, die, EXIT_*).
17
+
18
+ [ -n "${BROWSER_SKILL_MIGRATE_LOADED:-}" ] && return 0
19
+ readonly BROWSER_SKILL_MIGRATE_LOADED=1
20
+
21
+ # --- Constants ---
22
+
23
+ # Known schemas; versions.json is initialized with all of these at v1 by
24
+ # migrate_init when no prior state exists. Adding a new schema = append here +
25
+ # add the corresponding entry to versions.json on next migrate_init call.
26
+ # Phase 12 part 2: `stats` schema registered for per-action telemetry log
27
+ # (memory/stats.jsonl + stats.db). v1 only; v2+ migrators land at
28
+ # scripts/lib/migrators/stats/v<from>_to_<to>.sh when shape evolves.
29
+ readonly _MIGRATE_KNOWN_SCHEMAS=(sites sessions credentials captures baselines memory config stats)
30
+
31
+ # Default backup retention.
32
+ readonly _MIGRATE_DEFAULT_KEEP=5
33
+
34
+ # --- Internal path helpers ---
35
+
36
+ _migrate_versions_path() {
37
+ printf '%s/versions.json' "${BROWSER_SKILL_HOME}"
38
+ }
39
+
40
+ _migrate_legacy_version_path() {
41
+ printf '%s/version' "${BROWSER_SKILL_HOME}"
42
+ }
43
+
44
+ _migrate_backups_dir() {
45
+ printf '%s/backups' "${BROWSER_SKILL_HOME}"
46
+ }
47
+
48
+ _migrate_schema_backups_dir() {
49
+ printf '%s/%s' "$(_migrate_backups_dir)" "$1"
50
+ }
51
+
52
+ _migrate_migrators_dir() {
53
+ if [ -n "${BROWSER_SKILL_MIGRATORS_DIR:-}" ]; then
54
+ printf '%s' "${BROWSER_SKILL_MIGRATORS_DIR}"
55
+ return 0
56
+ fi
57
+ local lib_dir
58
+ lib_dir="$(dirname "${BASH_SOURCE[0]}")"
59
+ printf '%s/migrators' "${lib_dir}"
60
+ }
61
+
62
+ # --- Init ---
63
+
64
+ # migrate_init
65
+ # Lazy-creates versions.json (mode 0600) + backups/ (mode 0700) at the
66
+ # canonical state-home paths. Idempotent. If a legacy `version` file exists
67
+ # (single integer), reads it and seeds the new versions.json with all known
68
+ # schemas at that version (typically 1).
69
+ migrate_init() {
70
+ local backups_dir versions_path
71
+ backups_dir="$(_migrate_backups_dir)"
72
+ versions_path="$(_migrate_versions_path)"
73
+
74
+ mkdir -p "${backups_dir}"
75
+ chmod 700 "${backups_dir}"
76
+
77
+ if [ ! -f "${versions_path}" ]; then
78
+ local default_v=1
79
+ local legacy="$(_migrate_legacy_version_path)"
80
+ if [ -f "${legacy}" ]; then
81
+ default_v="$(tr -d '[:space:]' < "${legacy}" 2>/dev/null || printf '1')"
82
+ [[ "${default_v}" =~ ^[0-9]+$ ]] || default_v=1
83
+ fi
84
+ local schemas_obj
85
+ schemas_obj="$(_migrate_build_default_schemas_object "${default_v}")"
86
+ _migrate_atomic_versions_write "$(jq -nc \
87
+ --argjson v "${default_v}" \
88
+ --argjson schemas "${schemas_obj}" \
89
+ --arg sk "v0.56.0" \
90
+ '{schema_version:1, schema_versions:$schemas, skill_version:$sk}')"
91
+ fi
92
+ }
93
+
94
+ # Internal: build a JSON object {sites:N, sessions:N, ...} for the known schemas.
95
+ _migrate_build_default_schemas_object() {
96
+ local v="$1"
97
+ local args=()
98
+ local s
99
+ for s in "${_MIGRATE_KNOWN_SCHEMAS[@]}"; do
100
+ args+=(--argjson "${s}" "${v}")
101
+ done
102
+ local filter='. = {}'
103
+ for s in "${_MIGRATE_KNOWN_SCHEMAS[@]}"; do
104
+ filter="${filter} | .${s} = \$${s}"
105
+ done
106
+ jq -nc "${args[@]}" "${filter}"
107
+ }
108
+
109
+ # --- Versions read/write ---
110
+
111
+ # migrate_get_version SCHEMA
112
+ # Echoes current schema_version for SCHEMA. Returns 1 (default) if SCHEMA is
113
+ # absent from versions.json or if versions.json itself is missing.
114
+ migrate_get_version() {
115
+ local schema="$1"
116
+ assert_safe_name "${schema}" "schema-name"
117
+ local versions_path
118
+ versions_path="$(_migrate_versions_path)"
119
+ if [ ! -f "${versions_path}" ]; then
120
+ printf '1\n'
121
+ return 0
122
+ fi
123
+ jq -r --arg s "${schema}" '.schema_versions[$s] // 1' "${versions_path}"
124
+ }
125
+
126
+ # migrate_set_version SCHEMA N
127
+ # Atomic-write SCHEMA → N in versions.json. Auto-init if versions.json missing.
128
+ migrate_set_version() {
129
+ local schema="$1" n="$2"
130
+ assert_safe_name "${schema}" "schema-name"
131
+ [[ "${n}" =~ ^[0-9]+$ ]] || die "${EXIT_USAGE_ERROR}" "migrate_set_version: N must be integer (got: ${n})"
132
+ migrate_init
133
+ local versions_path current updated
134
+ versions_path="$(_migrate_versions_path)"
135
+ current="$(cat "${versions_path}")"
136
+ updated="$(printf '%s' "${current}" | jq -c --arg s "${schema}" --argjson n "${n}" \
137
+ '.schema_versions[$s] = $n')"
138
+ _migrate_atomic_versions_write "${updated}"
139
+ }
140
+
141
+ # Internal: atomic-write versions.json. Validates JSON before swap.
142
+ _migrate_atomic_versions_write() {
143
+ local json="$1"
144
+ if ! printf '%s' "${json}" | jq -e . >/dev/null 2>&1; then
145
+ die "${EXIT_GENERIC_ERROR}" "migrate: refused to write invalid versions.json"
146
+ fi
147
+ local path tmp
148
+ path="$(_migrate_versions_path)"
149
+ tmp="${path}.tmp.$$"
150
+ ( umask 077; printf '%s\n' "${json}" | jq . > "${tmp}" )
151
+ chmod 600 "${tmp}"
152
+ mv "${tmp}" "${path}"
153
+ }
154
+
155
+ # --- Registry ---
156
+
157
+ # Internal in-process registry table. Keys: "<schema>:<from>:<to>". Values: fn name.
158
+ declare -gA _MIGRATE_REGISTRY=()
159
+
160
+ # _migrate_register SCHEMA FROM TO FN
161
+ # Adds FN as the migrator for SCHEMA going from version FROM to version TO.
162
+ _migrate_register() {
163
+ local schema="$1" from="$2" to="$3" fn="$4"
164
+ _MIGRATE_REGISTRY["${schema}:${from}:${to}"]="${fn}"
165
+ }
166
+
167
+ # _migrate_load_registry
168
+ # Sources every scripts/lib/migrators/*/v*_to_v*.sh under the configured
169
+ # migrators dir. Each loaded file is expected to call _migrate_register itself
170
+ # OR to define a fn that follows the convention `migrate_<schema>_v<F>_to_v<T>`
171
+ # which we auto-register based on the file path.
172
+ _migrate_load_registry() {
173
+ local migrators_dir
174
+ migrators_dir="$(_migrate_migrators_dir)"
175
+ [ -d "${migrators_dir}" ] || return 0
176
+ local schema_dir schema migrator_file from to fn
177
+ for schema_dir in "${migrators_dir}"/*; do
178
+ [ -d "${schema_dir}" ] || continue
179
+ schema="$(basename "${schema_dir}")"
180
+ for migrator_file in "${schema_dir}"/v*_to_v*.sh; do
181
+ [ -f "${migrator_file}" ] || continue
182
+ # Parse "v<F>_to_v<T>.sh" out of the basename.
183
+ local base="$(basename "${migrator_file}" .sh)"
184
+ if [[ "${base}" =~ ^v([0-9]+)_to_v([0-9]+)$ ]]; then
185
+ from="${BASH_REMATCH[1]}"
186
+ to="${BASH_REMATCH[2]}"
187
+ # shellcheck disable=SC1090
188
+ source "${migrator_file}"
189
+ fn="migrate_${schema}_v${from}_to_v${to}"
190
+ _migrate_register "${schema}" "${from}" "${to}" "${fn}"
191
+ fi
192
+ done
193
+ done
194
+ }
195
+
196
+ # --- Check ---
197
+
198
+ # migrate_check
199
+ # Walks the registry; for each (schema, from, to) entry where current schema
200
+ # version == from, emits a `_kind:migration_needed` event. Always exits 0.
201
+ migrate_check() {
202
+ local t0=$(now_ms)
203
+ migrate_init
204
+ _migrate_load_registry
205
+
206
+ local pending=0
207
+ local key
208
+ for key in "${!_MIGRATE_REGISTRY[@]}"; do
209
+ local schema="${key%%:*}"
210
+ local rest="${key#*:}"
211
+ local from="${rest%:*}"
212
+ local to="${rest#*:}"
213
+ local current
214
+ current="$(migrate_get_version "${schema}")"
215
+ if [ "${current}" = "${from}" ]; then
216
+ printf '%s\n' "$(jq -nc \
217
+ --arg schema "${schema}" --argjson from "${from}" --argjson to "${to}" \
218
+ '{_kind:"migration_needed", schema:$schema, from:$from, to:$to}')"
219
+ pending=$(( pending + 1 ))
220
+ fi
221
+ done
222
+
223
+ local duration_ms=$(( $(now_ms) - t0 ))
224
+ summary_json verb=migrate mode=check pending="${pending}" \
225
+ duration_ms="${duration_ms}" status=ok
226
+ }
227
+
228
+ # --- Run ---
229
+
230
+ # migrate_run [SCHEMA]
231
+ # For each registered migrator (optionally filtered to SCHEMA), if the current
232
+ # version matches the migrator's `from`, run it against every file under
233
+ # ${BROWSER_SKILL_HOME}/<schema>/ that exists. Validates output via jq -e;
234
+ # refuses to atomic-swap on validation failure (version stays at `from`).
235
+ # Backs up each file to backups/<schema>/<basename>.bak.v<from> before swap.
236
+ migrate_run() {
237
+ local t0=$(now_ms)
238
+ local filter_schema="${1:-}"
239
+ migrate_init
240
+ _migrate_load_registry
241
+
242
+ local migrated=0 failed=0
243
+ local key
244
+ for key in "${!_MIGRATE_REGISTRY[@]}"; do
245
+ local schema="${key%%:*}"
246
+ [ -n "${filter_schema}" ] && [ "${schema}" != "${filter_schema}" ] && continue
247
+ local rest="${key#*:}"
248
+ local from="${rest%:*}"
249
+ local to="${rest#*:}"
250
+ local current
251
+ current="$(migrate_get_version "${schema}")"
252
+ [ "${current}" = "${from}" ] || continue
253
+
254
+ local fn="${_MIGRATE_REGISTRY[$key]}"
255
+ local schema_state_dir="${BROWSER_SKILL_HOME}/${schema}"
256
+ [ -d "${schema_state_dir}" ] || continue
257
+
258
+ # Run migrator against every file in the schema's state dir.
259
+ local file ok=1
260
+ while IFS= read -r -d '' file; do
261
+ _migrate_backup "${schema}" "${file}"
262
+ "${fn}" "${file}" || { ok=0; break; }
263
+ # Validate post-migration JSON.
264
+ if ! jq -e . "${file}" >/dev/null 2>&1; then
265
+ warn "migrate_run: ${schema} migrator produced invalid JSON for ${file}; rolling back this file"
266
+ local bk="$(_migrate_schema_backups_dir "${schema}")/$(basename "${file}").bak.v${from}"
267
+ if [ -f "${bk}" ]; then
268
+ cp "${bk}" "${file}"
269
+ fi
270
+ ok=0
271
+ break
272
+ fi
273
+ done < <(find "${schema_state_dir}" -maxdepth 4 -type f -name '*.json' -print0 2>/dev/null)
274
+
275
+ if [ "${ok}" = "1" ]; then
276
+ migrate_set_version "${schema}" "${to}"
277
+ migrated=$(( migrated + 1 ))
278
+ printf '%s\n' "$(jq -nc \
279
+ --arg schema "${schema}" --argjson from "${from}" --argjson to "${to}" \
280
+ '{_kind:"migration_applied", schema:$schema, from:$from, to:$to}')"
281
+ else
282
+ failed=$(( failed + 1 ))
283
+ fi
284
+ done
285
+
286
+ local duration_ms=$(( $(now_ms) - t0 ))
287
+ summary_json verb=migrate mode=run migrated="${migrated}" failed="${failed}" \
288
+ duration_ms="${duration_ms}" status=ok
289
+ }
290
+
291
+ # Internal: backup FILE under backups/<schema>/<basename>.bak.v<current_version>.
292
+ _migrate_backup() {
293
+ local schema="$1" file="$2"
294
+ local current bk_dir bk
295
+ current="$(migrate_get_version "${schema}")"
296
+ bk_dir="$(_migrate_schema_backups_dir "${schema}")"
297
+ mkdir -p "${bk_dir}"
298
+ chmod 700 "${bk_dir}"
299
+ bk="${bk_dir}/$(basename "${file}").bak.v${current}"
300
+ cp "${file}" "${bk}"
301
+ chmod 600 "${bk}"
302
+ }
303
+
304
+ # --- Rollback ---
305
+
306
+ # migrate_rollback SCHEMA
307
+ # Restores the latest backup for SCHEMA's state-dir files. Single-step only;
308
+ # multi-version chains require multiple invocations.
309
+ migrate_rollback() {
310
+ local schema="$1"
311
+ assert_safe_name "${schema}" "schema-name"
312
+ migrate_init
313
+
314
+ local current bk_dir
315
+ current="$(migrate_get_version "${schema}")"
316
+ bk_dir="$(_migrate_schema_backups_dir "${schema}")"
317
+ [ -d "${bk_dir}" ] || die "${EXIT_USAGE_ERROR}" "migrate_rollback: no backups for schema '${schema}'"
318
+
319
+ # Find latest backup version present.
320
+ local latest=0 file v
321
+ while IFS= read -r file; do
322
+ base="$(basename "${file}")"
323
+ v="${base##*.v}"
324
+ [[ "${v}" =~ ^[0-9]+$ ]] || continue
325
+ [ "${v}" -gt "${latest}" ] && latest="${v}"
326
+ done < <(find "${bk_dir}" -maxdepth 1 -type f -name '*.bak.v*' 2>/dev/null)
327
+
328
+ [ "${latest}" -gt 0 ] || die "${EXIT_USAGE_ERROR}" "migrate_rollback: no v* backups found for '${schema}'"
329
+
330
+ # Restore every file at that version.
331
+ local schema_state_dir="${BROWSER_SKILL_HOME}/${schema}"
332
+ while IFS= read -r file; do
333
+ local target_basename="${file##*/}"
334
+ target_basename="${target_basename%.bak.v*}"
335
+ cp "${file}" "${schema_state_dir}/${target_basename}"
336
+ chmod 600 "${schema_state_dir}/${target_basename}"
337
+ done < <(find "${bk_dir}" -maxdepth 1 -type f -name "*.bak.v${latest}" 2>/dev/null)
338
+
339
+ migrate_set_version "${schema}" "${latest}"
340
+ }
341
+
342
+ # --- Status ---
343
+
344
+ # migrate_status
345
+ # Echoes versions.json (after init).
346
+ migrate_status() {
347
+ migrate_init
348
+ cat "$(_migrate_versions_path)"
349
+ }
350
+
351
+ # --- Clean backups ---
352
+
353
+ # migrate_clean_backups [N]
354
+ # Discards backups beyond the newest N versions per schema. Default N=5.
355
+ #
356
+ # Implementation note: uses newline-separated find pipeline (rather than
357
+ # space-joined values in an associative array) because callers may run with
358
+ # IFS=$'\n\t' set globally — that breaks word-splitting on space inside
359
+ # `printf '%s\n' ${array_value}`. Newlines are robust under all IFS settings.
360
+ migrate_clean_backups() {
361
+ local keep="${1:-${_MIGRATE_DEFAULT_KEEP}}"
362
+ [[ "${keep}" =~ ^[0-9]+$ ]] || die "${EXIT_USAGE_ERROR}" "migrate_clean_backups: N must be integer (got: ${keep})"
363
+ local backups_dir
364
+ backups_dir="$(_migrate_backups_dir)"
365
+ [ -d "${backups_dir}" ] || return 0
366
+
367
+ local schema_dir
368
+ for schema_dir in "${backups_dir}"/*; do
369
+ [ -d "${schema_dir}" ] || continue
370
+
371
+ # Discover unique basename-without-version keys via find + sed + sort -u.
372
+ local basenames
373
+ basenames="$(find "${schema_dir}" -maxdepth 1 -type f -name '*.bak.v*' -exec basename {} \; 2>/dev/null \
374
+ | sed -E 's/\.bak\.v[0-9]+$//' | sort -u)"
375
+ [ -z "${basenames}" ] && continue
376
+
377
+ local basename_key versions v
378
+ while IFS= read -r basename_key; do
379
+ [ -z "${basename_key}" ] && continue
380
+ # Per-basename: list version numbers desc; drop top N; rm the rest.
381
+ versions="$(find "${schema_dir}" -maxdepth 1 -type f -name "${basename_key}.bak.v*" \
382
+ -exec basename {} \; 2>/dev/null \
383
+ | sed -E "s/^.*\.bak\.v//" | sort -rn | tail -n +$(( keep + 1 )))"
384
+ while IFS= read -r v; do
385
+ [ -z "${v}" ] && continue
386
+ rm -f "${schema_dir}/${basename_key}.bak.v${v}"
387
+ done <<<"${versions}"
388
+ done <<<"${basenames}"
389
+ done
390
+ }
@@ -0,0 +1,23 @@
1
+ # Phase 10 — Schema migrators
2
+
3
+ This directory is intentionally empty in 10-1-i (foundation only).
4
+
5
+ Real migrators land in 10-1-iii (no-op `v1_to_v2` for memory archetype JSONs) and case-by-case PRs thereafter.
6
+
7
+ ## Convention
8
+
9
+ Each migrator is a bash file at `<schema>/v<from>_to_<to>.sh`. It must define a function named `migrate_<schema>_v<from>_to_v<to>` that takes a file path argument and mutates the file in-place (atomic-swap via tmp+mv).
10
+
11
+ Example (post-10-1-iii):
12
+
13
+ ```
14
+ scripts/lib/migrators/
15
+ └── memory/
16
+ └── v1_to_v2.sh # defines: migrate_memory_v1_to_v2()
17
+ ```
18
+
19
+ The registry auto-loader (`_migrate_load_registry` in `lib/migrate.sh`) sources every `v*_to_v*.sh` file under this dir, parses the version pair from the filename, and registers the fn.
20
+
21
+ ## Test-only seam
22
+
23
+ Tests override via `BROWSER_SKILL_MIGRATORS_DIR=/tmp/fixture-migrators` env var. Production code never sets this.
@@ -0,0 +1,15 @@
1
+ # scripts/lib/migrators/memory/v1_to_v2.sh
2
+ # Phase 10 part 1-iii: first real migrator. No-op identity for memory schema.
3
+ # Validates the registry + dispatch end-to-end without risking data corruption.
4
+ #
5
+ # Migrates every JSON file under ${BROWSER_SKILL_HOME}/memory/ (both
6
+ # patterns.json + archetype JSONs). The change is purely the schema_version
7
+ # bump from 1 to 2; data shape is unchanged.
8
+ #
9
+ # Future migrators (v2_to_v3 and beyond) ship per actual schema-shape changes.
10
+
11
+ migrate_memory_v1_to_v2() {
12
+ local file_path="$1"
13
+ jq '.schema_version = 2' "${file_path}" > "${file_path}.tmp"
14
+ mv "${file_path}.tmp" "${file_path}"
15
+ }
@@ -0,0 +1,13 @@
1
+ # lib/migrators/recent_urls/
2
+
3
+ Schema migrator directory for the `recent_urls.jsonl` observation log
4
+ (Phase 11 v2 Pick A6, PR introducing schema_version:1).
5
+
6
+ **Currently empty.** The log ships at `schema_version:1` from inception; no
7
+ migrator is needed until the line shape changes. Future shape bumps land
8
+ a `v1_to_v2.sh` here following the precedent in `lib/migrators/memory/v1_to_v2.sh`
9
+ (PR #111).
10
+
11
+ The directory exists so the migrate-registry mechanism (`lib/migrate.sh`)
12
+ walks the `recent_urls` schema namespace alongside other schemas — even with
13
+ no migrators registered, the schema's `versions.json` slot is tracked.
@@ -0,0 +1,24 @@
1
+ # Migrators — `stats` schema (Phase 12 part 2)
2
+
3
+ Per-action telemetry log under `${BROWSER_SKILL_HOME}/memory/`:
4
+ - `stats.jsonl` — append-only JSONL, one event per adapter call (source of truth)
5
+ - `stats.db` — lazy-built SQLite mirror, rebuilt from JSONL on `browser-stats rebuild`
6
+
7
+ Schema starts at v1. **No migrator needed until shape changes** (mirrors the
8
+ `recent_urls/` precedent — registry tolerates a schema with no `v*_to_v*.sh`
9
+ file as long as `versions.json` keeps it pinned at v1).
10
+
11
+ ## When a v2 ships
12
+
13
+ 1. Drop `v1_to_v2.sh` here defining `migrate_stats_v1_to_v2(file_path)`.
14
+ 2. Bump the JSONL writer's `STATS_SCHEMA_VERSION` constant in
15
+ `scripts/lib/stats.sh`.
16
+ 3. SQLite mirror rebuilds itself on next `browser-stats rebuild` from the
17
+ (now-migrated) JSONL — no SQL DDL migration needed because
18
+ `audit_events.raw_json` carries the source event verbatim.
19
+
20
+ ## Event schema reference
21
+
22
+ Canonical event shape: [`references/stats-schema.json`](../../../../references/stats-schema.json).
23
+ Field naming follows OpenInference + OTel GenAI v1.40 conventions
24
+ (snake_case dot-name flattening).