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,57 @@
1
+ # scripts/lib/secret_backend_select.sh
2
+ # Smart per-OS credentials backend auto-detection per parent spec §1.
3
+ #
4
+ # detect_backend → echoes one of: keychain | libsecret | plaintext
5
+ #
6
+ # Resolution order:
7
+ # 1. ${BROWSER_SKILL_FORCE_BACKEND} ∈ {keychain, libsecret, plaintext} →
8
+ # echoed verbatim. (Test override; also a user knob if auto-detect picks
9
+ # wrong on their box — e.g. Linux without a running D-Bus session.)
10
+ # 2. uname -s gates per-OS:
11
+ # - Darwin + ${KEYCHAIN_SECURITY_BIN:-security} on PATH → keychain
12
+ # - Linux + ${LIBSECRET_TOOL_BIN:-secret-tool} on PATH → libsecret
13
+ # - Anything else → plaintext
14
+ # 3. We do NOT probe D-Bus reachability for libsecret. That probe is brittle
15
+ # (no clean way to tell "no agent" from "no item matching"), and the user
16
+ # can override via BROWSER_SKILL_FORCE_BACKEND=plaintext when needed.
17
+ #
18
+ # Source from any verb that needs to choose a backend (creds-add, future
19
+ # creds-migrate, etc). Requires lib/common.sh sourced first (assert_safe_name,
20
+ # EXIT_*).
21
+
22
+ [ -n "${BROWSER_SKILL_SECRET_BACKEND_SELECT_LOADED:-}" ] && return 0
23
+ readonly BROWSER_SKILL_SECRET_BACKEND_SELECT_LOADED=1
24
+
25
+ readonly _BACKEND_VALID_SET="keychain libsecret plaintext"
26
+
27
+ detect_backend() {
28
+ if [ -n "${BROWSER_SKILL_FORCE_BACKEND:-}" ]; then
29
+ case " ${_BACKEND_VALID_SET} " in
30
+ *" ${BROWSER_SKILL_FORCE_BACKEND} "*)
31
+ printf '%s\n' "${BROWSER_SKILL_FORCE_BACKEND}"
32
+ return 0
33
+ ;;
34
+ *)
35
+ # Invalid override — fall through to auto-detect (don't fail; the
36
+ # user typo'd a backend name but we still want a working default).
37
+ ;;
38
+ esac
39
+ fi
40
+
41
+ case "$(uname -s)" in
42
+ Darwin)
43
+ if command -v "${KEYCHAIN_SECURITY_BIN:-security}" >/dev/null 2>&1; then
44
+ printf 'keychain\n'
45
+ return 0
46
+ fi
47
+ ;;
48
+ Linux)
49
+ if command -v "${LIBSECRET_TOOL_BIN:-secret-tool}" >/dev/null 2>&1; then
50
+ printf 'libsecret\n'
51
+ return 0
52
+ fi
53
+ ;;
54
+ esac
55
+
56
+ printf 'plaintext\n'
57
+ }
@@ -0,0 +1,153 @@
1
+ # scripts/lib/session.sh
2
+ # Read/write helpers for Playwright storageState files plus their meta sidecar.
3
+ # Source from any verb that needs to load or save a session.
4
+ # Requires lib/common.sh sourced first (init_paths must have run).
5
+
6
+ [ -n "${BROWSER_SKILL_SESSION_LOADED:-}" ] && return 0
7
+ readonly BROWSER_SKILL_SESSION_LOADED=1
8
+
9
+ _session_path() { printf '%s/%s.json' "${SESSIONS_DIR}" "$1"; }
10
+ _session_meta_path() { printf '%s/%s.meta.json' "${SESSIONS_DIR}" "$1"; }
11
+
12
+ session_exists() {
13
+ [ -f "$(_session_path "$1")" ]
14
+ }
15
+
16
+ # session_save NAME STORAGE_STATE_JSON META_JSON
17
+ # Validates that storageState has top-level `cookies` and `origins` arrays
18
+ # (Playwright shape), then writes both files atomically at mode 0600.
19
+ session_save() {
20
+ local name="$1" ss_json="$2" meta_json="$3"
21
+ assert_safe_name "${name}" "session-name"
22
+
23
+ if ! printf '%s' "${ss_json}" | jq -e . >/dev/null 2>&1; then
24
+ die "${EXIT_USAGE_ERROR}" "session_save: storageState JSON is not valid"
25
+ fi
26
+ if ! printf '%s' "${ss_json}" | jq -e '.cookies | type == "array"' >/dev/null 2>&1; then
27
+ die "${EXIT_USAGE_ERROR}" "session_save: storageState missing cookies array"
28
+ fi
29
+ if ! printf '%s' "${ss_json}" | jq -e '.origins | type == "array"' >/dev/null 2>&1; then
30
+ die "${EXIT_USAGE_ERROR}" "session_save: storageState missing origins array"
31
+ fi
32
+ # Playwright's context.addInitScript() / .storageState() round-trip requires
33
+ # each origin to declare a localStorage array (may be empty). Hand-edited
34
+ # storageState files trip on this at browser launch — surface it here.
35
+ if ! printf '%s' "${ss_json}" | jq -e 'all(.origins[]; .localStorage | type == "array")' >/dev/null 2>&1; then
36
+ die "${EXIT_USAGE_ERROR}" "session_save: storageState origins[*].localStorage must be an array (use [] for empty); see Playwright storageState shape"
37
+ fi
38
+ if ! printf '%s' "${meta_json}" | jq -e . >/dev/null 2>&1; then
39
+ die "${EXIT_USAGE_ERROR}" "session_save: meta JSON is not valid"
40
+ fi
41
+
42
+ mkdir -p "${SESSIONS_DIR}"
43
+ chmod 700 "${SESSIONS_DIR}"
44
+
45
+ local ss_path meta_path ss_tmp meta_tmp
46
+ ss_path="$(_session_path "${name}")"
47
+ meta_path="$(_session_meta_path "${name}")"
48
+ ss_tmp="${ss_path}.tmp.$$"
49
+ meta_tmp="${meta_path}.tmp.$$"
50
+
51
+ (
52
+ umask 077
53
+ printf '%s\n' "${ss_json}" | jq . > "${ss_tmp}"
54
+ printf '%s\n' "${meta_json}" | jq . > "${meta_tmp}"
55
+ )
56
+ chmod 600 "${ss_tmp}" "${meta_tmp}"
57
+ mv "${ss_tmp}" "${ss_path}"
58
+ mv "${meta_tmp}" "${meta_path}"
59
+ }
60
+
61
+ # session_load NAME → echoes the storageState JSON (Playwright shape).
62
+ # Missing session → exit 22 (SESSION_EXPIRED) per spec §5.5; the caller
63
+ # can decide whether to relogin (Phase 5) or surface to the user.
64
+ session_load() {
65
+ local name="$1"
66
+ local path
67
+ path="$(_session_path "${name}")"
68
+ if [ ! -f "${path}" ]; then
69
+ die "${EXIT_SESSION_EXPIRED}" "session not found: ${name}"
70
+ fi
71
+ cat "${path}"
72
+ }
73
+
74
+ # session_delete NAME — remove session storageState + meta files.
75
+ # Idempotent: succeeds even if files are already gone (dangling references
76
+ # can come from a cascading site removal that didn't clean sessions).
77
+ # Does NOT clear site.default_session pointers — Phase 5 may add that
78
+ # cascade once the credential lifecycle is more developed.
79
+ session_delete() {
80
+ local name="$1"
81
+ assert_safe_name "${name}" "session-name"
82
+ local ss_path meta_path
83
+ ss_path="$(_session_path "${name}")"
84
+ meta_path="$(_session_meta_path "${name}")"
85
+ rm -f "${ss_path}" "${meta_path}"
86
+ }
87
+
88
+ session_meta_load() {
89
+ local name="$1"
90
+ local path
91
+ path="$(_session_meta_path "${name}")"
92
+ if [ ! -f "${path}" ]; then
93
+ die "${EXIT_SESSION_EXPIRED}" "session meta not found: ${name}"
94
+ fi
95
+ cat "${path}"
96
+ }
97
+
98
+ # url_origin URL → echoes scheme://host[:port] from a URL string.
99
+ # Bash-only, no python3 dep. Examples:
100
+ # https://app.example.com/x -> https://app.example.com
101
+ # https://app.example.com:8443/ -> https://app.example.com:8443
102
+ # http://localhost -> http://localhost
103
+ url_origin() {
104
+ local url="$1"
105
+ case "${url}" in
106
+ http://*) ;;
107
+ https://*) ;;
108
+ *) die "${EXIT_USAGE_ERROR}" "url must start with http:// or https:// (got: ${url})" ;;
109
+ esac
110
+ # Strip the path/query/fragment after the host[:port].
111
+ printf '%s' "${url}" | awk '
112
+ {
113
+ n = index($0, "://")
114
+ scheme = substr($0, 1, n + 2)
115
+ rest = substr($0, n + 3)
116
+ slash = index(rest, "/")
117
+ if (slash > 0) rest = substr(rest, 1, slash - 1)
118
+ q = index(rest, "?")
119
+ if (q > 0) rest = substr(rest, 1, q - 1)
120
+ h = index(rest, "#")
121
+ if (h > 0) rest = substr(rest, 1, h - 1)
122
+ printf "%s%s", scheme, rest
123
+ }'
124
+ }
125
+
126
+ # session_origin_check NAME TARGET_URL
127
+ # Compares the session's stored origin (from meta sidecar) against the URL's
128
+ # origin. Exits EXIT_SESSION_EXPIRED on mismatch (spec §5.5).
129
+ session_origin_check() {
130
+ local name="$1" target_url="$2"
131
+ local meta_origin target_origin
132
+ meta_origin="$(session_meta_load "${name}" | jq -r .origin)"
133
+ target_origin="$(url_origin "${target_url}")"
134
+ if [ "${meta_origin}" != "${target_origin}" ]; then
135
+ die "${EXIT_SESSION_EXPIRED}" \
136
+ "origin mismatch: session origin=${meta_origin}, target origin=${target_origin}"
137
+ fi
138
+ }
139
+
140
+ # session_expiry_summary NAME → emits a single-line JSON object:
141
+ # {"session": NAME, "captured_at": ..., "expires_in_hours": ..., "origin": ...}
142
+ # Used by login + later phases' relogin / doctor to surface session staleness.
143
+ session_expiry_summary() {
144
+ local name="$1"
145
+ local meta
146
+ meta="$(session_meta_load "${name}")"
147
+ jq -c --arg n "${name}" '{
148
+ session: $n,
149
+ origin: (.origin // null),
150
+ captured_at: (.captured_at // null),
151
+ expires_in_hours: (.expires_in_hours // null)
152
+ }' <<< "${meta}"
153
+ }
@@ -0,0 +1,126 @@
1
+ # scripts/lib/site.sh
2
+ # Site profile read/write/list/delete + `current` file helpers.
3
+ # Source from any verb that needs to read or write a site profile.
4
+ # Requires lib/common.sh to be sourced first (init_paths must have run).
5
+
6
+ [ -n "${BROWSER_SKILL_SITE_LOADED:-}" ] && return 0
7
+ readonly BROWSER_SKILL_SITE_LOADED=1
8
+
9
+ # Internal: path of <name>'s profile JSON inside SITES_DIR.
10
+ _site_path() {
11
+ printf '%s/%s.json' "${SITES_DIR}" "$1"
12
+ }
13
+
14
+ # Internal: path of <name>'s meta sidecar.
15
+ _site_meta_path() {
16
+ printf '%s/%s.meta.json' "${SITES_DIR}" "$1"
17
+ }
18
+
19
+ # True iff a site profile JSON file exists for the given name.
20
+ site_exists() {
21
+ [ -f "$(_site_path "$1")" ]
22
+ }
23
+
24
+ # site_save NAME PROFILE_JSON META_JSON
25
+ # Validates both JSON blobs, writes atomically (tmp + mv), mode 0600.
26
+ # Caller is responsible for shape — site.sh only validates "is it JSON".
27
+ site_save() {
28
+ local name="$1" profile_json="$2" meta_json="$3"
29
+ assert_safe_name "${name}" "site-name"
30
+
31
+ if ! printf '%s' "${profile_json}" | jq -e . >/dev/null 2>&1; then
32
+ die "${EXIT_USAGE_ERROR}" "site_save: profile JSON is not valid"
33
+ fi
34
+ if ! printf '%s' "${meta_json}" | jq -e . >/dev/null 2>&1; then
35
+ die "${EXIT_USAGE_ERROR}" "site_save: meta JSON is not valid"
36
+ fi
37
+
38
+ mkdir -p "${SITES_DIR}"
39
+ chmod 700 "${SITES_DIR}"
40
+
41
+ local profile_path meta_path profile_tmp meta_tmp
42
+ profile_path="$(_site_path "${name}")"
43
+ meta_path="$(_site_meta_path "${name}")"
44
+ profile_tmp="${profile_path}.tmp.$$"
45
+ meta_tmp="${meta_path}.tmp.$$"
46
+
47
+ (
48
+ umask 077
49
+ printf '%s\n' "${profile_json}" | jq . > "${profile_tmp}"
50
+ printf '%s\n' "${meta_json}" | jq . > "${meta_tmp}"
51
+ )
52
+ chmod 600 "${profile_tmp}" "${meta_tmp}"
53
+ mv "${profile_tmp}" "${profile_path}"
54
+ mv "${meta_tmp}" "${meta_path}"
55
+ }
56
+
57
+ # site_load NAME → echoes the profile JSON (un-jq'd, exactly as on disk).
58
+ site_load() {
59
+ local name="$1"
60
+ local path
61
+ path="$(_site_path "${name}")"
62
+ if [ ! -f "${path}" ]; then
63
+ die "${EXIT_SITE_NOT_FOUND}" "site not found: ${name}"
64
+ fi
65
+ cat "${path}"
66
+ }
67
+
68
+ # site_meta_load NAME → echoes the meta JSON.
69
+ site_meta_load() {
70
+ local name="$1"
71
+ local path
72
+ path="$(_site_meta_path "${name}")"
73
+ if [ ! -f "${path}" ]; then
74
+ die "${EXIT_SITE_NOT_FOUND}" "site meta not found: ${name}"
75
+ fi
76
+ cat "${path}"
77
+ }
78
+
79
+ # site_list_names → echoes each registered site name on its own line, sorted.
80
+ # Excludes *.meta.json files; an empty SITES_DIR (or missing) prints nothing.
81
+ site_list_names() {
82
+ if [ ! -d "${SITES_DIR}" ]; then
83
+ return 0
84
+ fi
85
+ find "${SITES_DIR}" -maxdepth 1 -type f -name '*.json' ! -name '*.meta.json' \
86
+ -exec basename {} .json \; 2>/dev/null | sort
87
+ }
88
+
89
+ # site_delete NAME → rm -f the profile + meta. Idempotent.
90
+ # If CURRENT_FILE points at this site, clear it (orphan reference fix).
91
+ site_delete() {
92
+ local name="$1"
93
+ rm -f "$(_site_path "${name}")" "$(_site_meta_path "${name}")"
94
+
95
+ if [ -f "${CURRENT_FILE}" ]; then
96
+ local current
97
+ current="$(tr -d '[:space:]' < "${CURRENT_FILE}" 2>/dev/null || true)"
98
+ if [ "${current}" = "${name}" ]; then
99
+ rm -f "${CURRENT_FILE}"
100
+ fi
101
+ fi
102
+ }
103
+
104
+ # current_get → echo current site name (empty string if unset).
105
+ current_get() {
106
+ if [ -f "${CURRENT_FILE}" ]; then
107
+ tr -d '[:space:]' < "${CURRENT_FILE}"
108
+ fi
109
+ }
110
+
111
+ # current_set NAME → set CURRENT_FILE to NAME (must be a registered site).
112
+ current_set() {
113
+ local name="$1"
114
+ assert_safe_name "${name}" "site-name"
115
+ if ! site_exists "${name}"; then
116
+ die "${EXIT_SITE_NOT_FOUND}" "cannot set current: site not found: ${name}"
117
+ fi
118
+ mkdir -p "${BROWSER_SKILL_HOME}"
119
+ ( umask 077; printf '%s\n' "${name}" > "${CURRENT_FILE}" )
120
+ chmod 600 "${CURRENT_FILE}"
121
+ }
122
+
123
+ # current_clear → rm -f CURRENT_FILE (idempotent).
124
+ current_clear() {
125
+ rm -f "${CURRENT_FILE}"
126
+ }