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,92 @@
1
+ #!/usr/bin/env bash
2
+ # scripts/browser-hover.sh — pointer hover an element by --ref eN or --selector CSS.
3
+ # Usage: bash scripts/browser-hover.sh [--site NAME] [--tool NAME] [--dry-run]
4
+ # [--raw] (--ref eN | --selector CSS)
5
+ #
6
+ # Routes to chrome-devtools-mcp by default (Phase 6 part 3). Stateful —
7
+ # requires running daemon (refMap precondition; mirrors click/select).
8
+ # --selector path enables Phase 11 cache dispatch (cache stores selectors,
9
+ # not snapshot-relative refs). Mirrors browser-click.sh + browser-fill.sh
10
+ # precedent.
11
+
12
+ set -euo pipefail
13
+ IFS=$'\n\t'
14
+
15
+ SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
16
+ # shellcheck source=lib/common.sh
17
+ # shellcheck disable=SC1091
18
+ source "${SCRIPT_DIR}/lib/common.sh"
19
+ # shellcheck source=lib/output.sh
20
+ # shellcheck disable=SC1091
21
+ source "${SCRIPT_DIR}/lib/output.sh"
22
+ # shellcheck source=lib/router.sh
23
+ # shellcheck disable=SC1091
24
+ source "${SCRIPT_DIR}/lib/router.sh"
25
+ # shellcheck source=lib/verb_helpers.sh
26
+ # shellcheck disable=SC1091
27
+ source "${SCRIPT_DIR}/lib/verb_helpers.sh"
28
+
29
+ init_paths
30
+
31
+ SUMMARY_T0="$(now_ms)"; export SUMMARY_T0
32
+
33
+ parse_verb_globals "$@"
34
+
35
+ resolve_session_storage_state
36
+
37
+ ref="" selector=""
38
+ verb_argv=()
39
+ i=0
40
+ while [ "${i}" -lt "${#REMAINING_ARGV[@]}" ]; do
41
+ case "${REMAINING_ARGV[i]}" in
42
+ --ref)
43
+ ref="${REMAINING_ARGV[i+1]:-}"
44
+ [ -n "${ref}" ] || die "${EXIT_USAGE_ERROR}" "--ref requires a value"
45
+ verb_argv+=(--ref "${ref}")
46
+ i=$((i + 2))
47
+ ;;
48
+ --selector)
49
+ selector="${REMAINING_ARGV[i+1]:-}"
50
+ [ -n "${selector}" ] || die "${EXIT_USAGE_ERROR}" "--selector requires a value"
51
+ verb_argv+=(--selector "${selector}")
52
+ i=$((i + 2))
53
+ ;;
54
+ *)
55
+ verb_argv+=("${REMAINING_ARGV[i]}")
56
+ i=$((i + 1))
57
+ ;;
58
+ esac
59
+ done
60
+
61
+ if [ -n "${ref}" ] && [ -n "${selector}" ]; then
62
+ die "${EXIT_USAGE_ERROR}" "--ref and --selector are mutually exclusive"
63
+ fi
64
+ if [ -z "${ref}" ] && [ -z "${selector}" ]; then
65
+ die "${EXIT_USAGE_ERROR}" "hover requires --ref eN or --selector CSS"
66
+ fi
67
+
68
+ if [ "${ARG_DRY_RUN:-0}" = "1" ]; then
69
+ ok "dry-run: would hover ${ref:-${selector}}"
70
+ emit_summary verb=hover tool=none why=dry-run status=ok ref="${ref}" selector="${selector}" dry_run=true
71
+ exit 0
72
+ fi
73
+
74
+ picked="$(pick_tool hover "${verb_argv[@]}")"
75
+ tool_name="${picked%%$'\t'*}"
76
+ why="${picked#*$'\t'}"
77
+
78
+ source_picked_adapter "${tool_name}"
79
+
80
+ set +e
81
+ adapter_out="$(invoke_with_retry hover "${verb_argv[@]}")"
82
+ adapter_rc=$?
83
+ set -e
84
+
85
+ [ -n "${adapter_out}" ] && printf '%s\n' "${adapter_out}"
86
+
87
+ if [ "${adapter_rc}" -eq 0 ]; then
88
+ emit_summary verb=hover tool="${tool_name}" why="${why}" status=ok ref="${ref}" selector="${selector}"
89
+ exit 0
90
+ fi
91
+ emit_summary verb=hover tool="${tool_name}" why="${why}" status=error ref="${ref}" selector="${selector}"
92
+ exit "${adapter_rc}"
@@ -0,0 +1,188 @@
1
+ #!/usr/bin/env bash
2
+ # scripts/browser-inspect.sh — inspect a page (console, network, screenshot,
3
+ # or selector text). Usage:
4
+ # bash scripts/browser-inspect.sh [--site NAME] [--tool NAME] [--dry-run]
5
+ # [--raw]
6
+ # (--capture-console | --capture-network
7
+ # | --screenshot | --selector CSS)
8
+ # [--capture]
9
+ #
10
+ # Routes to chrome-devtools-mcp by default (post-1d router promotion — only
11
+ # adapter with dedicated console + network MCP tools per parent spec
12
+ # Appendix B). At least one of --capture-* / --screenshot / --selector is
13
+ # required so the adapter has something to do.
14
+ #
15
+ # Phase 7 part 1-iii: --capture writes adapter output to ${CAPTURES_DIR}/NNN/
16
+ # as console.json + network.har, sanitized via lib/sanitize.sh. Stdout output
17
+ # is ALSO sanitized (defense in depth) — single transformation, both sinks.
18
+
19
+ set -euo pipefail
20
+ IFS=$'\n\t'
21
+
22
+ SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
23
+ # shellcheck source=lib/common.sh
24
+ # shellcheck disable=SC1091
25
+ source "${SCRIPT_DIR}/lib/common.sh"
26
+ # shellcheck source=lib/output.sh
27
+ # shellcheck disable=SC1091
28
+ source "${SCRIPT_DIR}/lib/output.sh"
29
+ # shellcheck source=lib/router.sh
30
+ # shellcheck disable=SC1091
31
+ source "${SCRIPT_DIR}/lib/router.sh"
32
+ # shellcheck source=lib/verb_helpers.sh
33
+ # shellcheck disable=SC1091
34
+ source "${SCRIPT_DIR}/lib/verb_helpers.sh"
35
+ # shellcheck source=lib/capture.sh
36
+ # shellcheck disable=SC1091
37
+ source "${SCRIPT_DIR}/lib/capture.sh"
38
+ # shellcheck source=lib/sanitize.sh
39
+ # shellcheck disable=SC1091
40
+ source "${SCRIPT_DIR}/lib/sanitize.sh"
41
+
42
+ init_paths
43
+
44
+ SUMMARY_T0="$(now_ms)"; export SUMMARY_T0
45
+
46
+ parse_verb_globals "$@"
47
+
48
+ resolve_session_storage_state
49
+
50
+ selector="" capture_console=0 capture_network=0 screenshot=0 do_capture=0 unsanitized=0
51
+ verb_argv=()
52
+ i=0
53
+ while [ "${i}" -lt "${#REMAINING_ARGV[@]}" ]; do
54
+ case "${REMAINING_ARGV[i]}" in
55
+ --selector)
56
+ selector="${REMAINING_ARGV[i+1]:-}"
57
+ [ -n "${selector}" ] || die "${EXIT_USAGE_ERROR}" "--selector requires a value"
58
+ verb_argv+=(--selector "${selector}")
59
+ i=$((i + 2))
60
+ ;;
61
+ --capture-console)
62
+ capture_console=1
63
+ verb_argv+=(--capture-console)
64
+ i=$((i + 1))
65
+ ;;
66
+ --capture-network)
67
+ capture_network=1
68
+ verb_argv+=(--capture-network)
69
+ i=$((i + 1))
70
+ ;;
71
+ --screenshot)
72
+ screenshot=1
73
+ verb_argv+=(--screenshot)
74
+ i=$((i + 1))
75
+ ;;
76
+ --capture)
77
+ do_capture=1
78
+ i=$((i + 1))
79
+ ;;
80
+ --unsanitized)
81
+ unsanitized=1
82
+ i=$((i + 1))
83
+ ;;
84
+ *)
85
+ verb_argv+=("${REMAINING_ARGV[i]}")
86
+ i=$((i + 1))
87
+ ;;
88
+ esac
89
+ done
90
+
91
+ # Phase 7 part 1-iv: --unsanitized requires typed-phrase confirmation.
92
+ # Strict equality (no whitespace strip) — friction-by-design. Mirrors
93
+ # scripts/browser-creds-show.sh::--reveal precedent. Phrase verbatim per
94
+ # parent spec §8.3. Scripted use: pipe phrase via stdin.
95
+ if [ "${unsanitized}" = "1" ]; then
96
+ printf 'Type the unsanitized confirmation phrase to confirm: ' >&2
97
+ IFS= read -r unsanitized_answer || true
98
+ if [ "${unsanitized_answer}" != "I want raw network/console data including auth tokens" ]; then
99
+ die "${EXIT_USAGE_ERROR}" "unsanitized aborted (confirmation mismatch)"
100
+ fi
101
+ fi
102
+
103
+ if [ -z "${selector}" ] && [ "${capture_console}" = 0 ] \
104
+ && [ "${capture_network}" = 0 ] && [ "${screenshot}" = 0 ]; then
105
+ die "${EXIT_USAGE_ERROR}" \
106
+ "inspect requires one of --capture-console / --capture-network / --screenshot / --selector CSS"
107
+ fi
108
+
109
+ if [ "${ARG_DRY_RUN:-0}" = "1" ]; then
110
+ ok "dry-run: would inspect (${selector:-<no-selector>})"
111
+ if [ "${do_capture}" = "1" ]; then
112
+ emit_summary verb=inspect tool=none why=dry-run status=ok selector="${selector}" dry_run=true capture=true
113
+ else
114
+ emit_summary verb=inspect tool=none why=dry-run status=ok selector="${selector}" dry_run=true
115
+ fi
116
+ exit 0
117
+ fi
118
+
119
+ picked="$(pick_tool inspect "${verb_argv[@]}")"
120
+ tool_name="${picked%%$'\t'*}"
121
+ why="${picked#*$'\t'}"
122
+
123
+ source_picked_adapter "${tool_name}"
124
+
125
+ if [ "${do_capture}" = "1" ]; then
126
+ capture_start inspect
127
+ fi
128
+
129
+ set +e
130
+ adapter_out="$(invoke_with_retry inspect "${verb_argv[@]}")"
131
+ adapter_rc=$?
132
+ set -e
133
+
134
+ if [ "${do_capture}" = "1" ] && [ -n "${adapter_out}" ]; then
135
+ # Single (maybe-)sanitize, both sinks: stdout + per-aspect files. Either
136
+ # path produces the same emit-twice contract; only the transformation
137
+ # differs. --unsanitized skips sanitize_inspect_reply.
138
+ if [ "${unsanitized}" = "1" ]; then
139
+ out_for_emit="${adapter_out}"
140
+ sanitized_flag=false
141
+ else
142
+ out_for_emit="$(printf '%s' "${adapter_out}" | sanitize_inspect_reply)"
143
+ sanitized_flag=true
144
+ fi
145
+
146
+ # console.json — extract .console_messages array.
147
+ if [ "${capture_console}" = "1" ]; then
148
+ if printf '%s' "${out_for_emit}" | jq -e 'has("console_messages")' >/dev/null 2>&1; then
149
+ printf '%s' "${out_for_emit}" | jq '.console_messages // []' > "${CAPTURE_DIR}/console.json"
150
+ chmod 600 "${CAPTURE_DIR}/console.json"
151
+ fi
152
+ fi
153
+
154
+ # network.har — wrap .network_requests in HAR envelope and persist.
155
+ if [ "${capture_network}" = "1" ]; then
156
+ if printf '%s' "${out_for_emit}" | jq -e 'has("network_requests")' >/dev/null 2>&1; then
157
+ printf '%s' "${out_for_emit}" \
158
+ | jq '{log: {version: "1.2", entries: (.network_requests // [])}}' \
159
+ > "${CAPTURE_DIR}/network.har"
160
+ chmod 600 "${CAPTURE_DIR}/network.har"
161
+ fi
162
+ fi
163
+
164
+ printf '%s\n' "${out_for_emit}"
165
+
166
+ if [ "${adapter_rc}" -eq 0 ]; then
167
+ capture_finish ok "${sanitized_flag}"
168
+ else
169
+ capture_finish error "${sanitized_flag}"
170
+ fi
171
+ else
172
+ [ -n "${adapter_out}" ] && printf '%s\n' "${adapter_out}"
173
+ fi
174
+
175
+ if [ "${adapter_rc}" -eq 0 ]; then
176
+ if [ "${do_capture}" = "1" ]; then
177
+ emit_summary verb=inspect tool="${tool_name}" why="${why}" status=ok selector="${selector}" capture_id="${CAPTURE_ID}"
178
+ else
179
+ emit_summary verb=inspect tool="${tool_name}" why="${why}" status=ok selector="${selector}"
180
+ fi
181
+ exit 0
182
+ fi
183
+ if [ "${do_capture}" = "1" ]; then
184
+ emit_summary verb=inspect tool="${tool_name}" why="${why}" status=error selector="${selector}" capture_id="${CAPTURE_ID}"
185
+ else
186
+ emit_summary verb=inspect tool="${tool_name}" why="${why}" status=error selector="${selector}"
187
+ fi
188
+ exit "${adapter_rc}"
@@ -0,0 +1,78 @@
1
+ #!/usr/bin/env bash
2
+ # list-sessions — list captured Playwright sessions (storageState files).
3
+ #
4
+ # Sessions are tied to sites via meta.site; pass --site NAME to filter.
5
+ # A site may have many sessions — this verb is the discoverability surface
6
+ # for the 1-many credential model (e.g. prod--admin, prod--readonly, prod--ci).
7
+ #
8
+ # Storage state itself is sensitive and stays at mode 0600 — this verb only
9
+ # emits metadata (origin, captured_at, expires_in_hours), never cookie/token
10
+ # values.
11
+
12
+ set -euo pipefail
13
+ IFS=$'\n\t'
14
+ umask 077
15
+
16
+ SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
17
+ # shellcheck source=lib/common.sh
18
+ # shellcheck disable=SC1091
19
+ source "${SCRIPT_DIR}/lib/common.sh"
20
+ # shellcheck source=lib/session.sh
21
+ # shellcheck disable=SC1091
22
+ source "${SCRIPT_DIR}/lib/session.sh"
23
+ init_paths
24
+
25
+ site_filter=""
26
+ while [ $# -gt 0 ]; do
27
+ case "$1" in
28
+ --site) site_filter="$2"; shift 2 ;;
29
+ -h|--help)
30
+ cat <<'USAGE'
31
+ Usage: list-sessions [--site NAME]
32
+
33
+ --site NAME show only sessions bound to this site
34
+ USAGE
35
+ exit 0
36
+ ;;
37
+ *) die "${EXIT_USAGE_ERROR}" "unknown flag: $1" ;;
38
+ esac
39
+ done
40
+
41
+ started_at_ms="$(now_ms)"
42
+
43
+ rows='[]'
44
+ count=0
45
+ if [ -d "${SESSIONS_DIR}" ]; then
46
+ shopt -s nullglob
47
+ for f in "${SESSIONS_DIR}"/*.json; do
48
+ base="$(basename "${f}" .json)"
49
+ case "${base}" in
50
+ *.meta) continue ;;
51
+ *interactive-tmp*) continue ;;
52
+ esac
53
+ [ -f "${SESSIONS_DIR}/${base}.meta.json" ] || continue
54
+ meta="$(session_meta_load "${base}" 2>/dev/null)" || continue
55
+ sess_site="$(printf '%s' "${meta}" | jq -r '.site // ""')"
56
+ if [ -n "${site_filter}" ] && [ "${sess_site}" != "${site_filter}" ]; then
57
+ continue
58
+ fi
59
+ rows="$(jq --arg n "${base}" --argjson m "${meta}" '
60
+ . + [{
61
+ session: $n,
62
+ site: ($m.site // null),
63
+ origin: ($m.origin // null),
64
+ captured_at: ($m.captured_at // null),
65
+ expires_in_hours: ($m.expires_in_hours // null)
66
+ }]' <<< "${rows}")"
67
+ count=$((count + 1))
68
+ done
69
+ shopt -u nullglob
70
+ fi
71
+
72
+ duration_ms=$(( $(now_ms) - started_at_ms ))
73
+ jq -cn --argjson r "${rows}" --argjson c "${count}" --argjson d "${duration_ms}" \
74
+ --arg sf "${site_filter}" \
75
+ '{verb: "list-sessions", tool: "none", why: (if $sf == "" then "list-all" else "list-by-site" end),
76
+ status: (if $c == 0 then "empty" else "ok" end),
77
+ site_filter: ($sf | if . == "" then null else . end),
78
+ count: $c, sessions: $r, duration_ms: $d}'
@@ -0,0 +1,42 @@
1
+ #!/usr/bin/env bash
2
+ # list-sites — list registered site profiles (no creds; sites are non-secret).
3
+ set -euo pipefail
4
+ IFS=$'\n\t'
5
+ umask 077
6
+
7
+ SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
8
+ # shellcheck source=lib/common.sh
9
+ # shellcheck disable=SC1091
10
+ source "${SCRIPT_DIR}/lib/common.sh"
11
+ # shellcheck source=lib/site.sh
12
+ # shellcheck disable=SC1091
13
+ source "${SCRIPT_DIR}/lib/site.sh"
14
+ init_paths
15
+
16
+ started_at_ms="$(now_ms)"
17
+
18
+ names="$(site_list_names)"
19
+ rows='[]'
20
+ count=0
21
+ if [ -n "${names}" ]; then
22
+ while IFS= read -r n; do
23
+ [ -z "${n}" ] && continue
24
+ profile="$(site_load "${n}")"
25
+ meta="$(site_meta_load "${n}")"
26
+ rows="$(jq --argjson p "${profile}" --argjson m "${meta}" '
27
+ . + [{
28
+ name: $p.name,
29
+ url: $p.url,
30
+ label: ($p.label // ""),
31
+ default_session:$p.default_session,
32
+ default_tool: $p.default_tool,
33
+ last_used_at: ($m.last_used_at // null)
34
+ }]' <<< "${rows}")"
35
+ count=$((count + 1))
36
+ done <<< "${names}"
37
+ fi
38
+
39
+ duration_ms=$(( $(now_ms) - started_at_ms ))
40
+ jq -cn --argjson r "${rows}" --argjson c "${count}" --argjson d "${duration_ms}" \
41
+ '{verb: "list-sites", tool: "none", why: "list", status: "ok",
42
+ count: $c, sites: $r, duration_ms: $d}'
@@ -0,0 +1,279 @@
1
+ #!/usr/bin/env bash
2
+ # login — capture a Playwright storageState into sessions/<name>.json.
3
+ #
4
+ # Three modes:
5
+ # --interactive headed Chromium; user logs in, presses Enter
6
+ # --storage-state-file import a hand-edited storageState file
7
+ # --auto phase-5 part 3 — programmatic headless login
8
+ # using the stored credential (creds-add). Reads
9
+ # username + password (NUL-separated) from
10
+ # credential, sends to driver via stdin per AP-7.
11
+ set -euo pipefail
12
+ IFS=$'\n\t'
13
+ umask 077
14
+
15
+ SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
16
+ # shellcheck source=lib/common.sh
17
+ # shellcheck disable=SC1091
18
+ source "${SCRIPT_DIR}/lib/common.sh"
19
+ # shellcheck source=lib/site.sh
20
+ # shellcheck disable=SC1091
21
+ source "${SCRIPT_DIR}/lib/site.sh"
22
+ # shellcheck source=lib/session.sh
23
+ # shellcheck disable=SC1091
24
+ source "${SCRIPT_DIR}/lib/session.sh"
25
+ # shellcheck source=lib/credential.sh
26
+ # shellcheck disable=SC1091
27
+ source "${SCRIPT_DIR}/lib/credential.sh"
28
+ init_paths
29
+
30
+ site=""; as=""; ss_file=""; dry_run=0; auto=0; headed=1; interactive=0
31
+
32
+ usage() {
33
+ cat <<'USAGE'
34
+ Usage: login --site NAME --as SESSION (--storage-state-file PATH | --interactive) [--dry-run]
35
+
36
+ Capture a Playwright storageState into sessions/<SESSION>.json. Two modes:
37
+
38
+ --interactive Launch a headed Chromium via the playwright-lib
39
+ driver. User logs in interactively; press Enter
40
+ in this terminal to capture and save the session.
41
+ --storage-state-file PATH Skip the browser launch; consume an already-
42
+ captured storageState file (legacy hand-edit
43
+ path, useful for CI / non-interactive imports).
44
+ --auto Programmatic headless login using the stored
45
+ credential (set via creds-add). Requires
46
+ --site + --as; the credential's auto_relogin
47
+ flag must be true. Username + password reach
48
+ the driver via stdin only (AP-7).
49
+
50
+ --site NAME site profile to bind the session to (required)
51
+ --as SESSION session name (required; falls back to site.default_session)
52
+ --dry-run validate inputs; write nothing
53
+ --headed accepted (interactive mode is always headed)
54
+ -h, --help
55
+
56
+ A site may have many sessions: pass different --as names to capture per-role
57
+ or per-account credentials (e.g. prod--admin, prod--readonly, prod--ci).
58
+ USAGE
59
+ }
60
+
61
+ while [ $# -gt 0 ]; do
62
+ case "$1" in
63
+ --site) site="$2"; shift 2 ;;
64
+ --as) as="$2"; shift 2 ;;
65
+ --storage-state-file) ss_file="$2"; shift 2 ;;
66
+ --interactive) interactive=1; shift ;;
67
+ --dry-run) dry_run=1; shift ;;
68
+ --headed) headed=1; shift ;;
69
+ --auto) auto=1; shift ;;
70
+ -h|--help) usage; exit 0 ;;
71
+ *) die "${EXIT_USAGE_ERROR}" "unknown flag: $1" ;;
72
+ esac
73
+ done
74
+
75
+ if [ "${auto}" -eq 1 ] && [ "${interactive}" -eq 1 ]; then
76
+ die "${EXIT_USAGE_ERROR}" "--auto and --interactive are mutually exclusive"
77
+ fi
78
+ if [ "${auto}" -eq 1 ] && [ -n "${ss_file}" ]; then
79
+ die "${EXIT_USAGE_ERROR}" "--auto and --storage-state-file are mutually exclusive"
80
+ fi
81
+ if [ "${interactive}" -eq 1 ] && [ -n "${ss_file}" ]; then
82
+ die "${EXIT_USAGE_ERROR}" "--interactive and --storage-state-file are mutually exclusive"
83
+ fi
84
+ [ -n "${site}" ] || { usage; die "${EXIT_USAGE_ERROR}" "--site is required"; }
85
+ # --as defaults to site.default_session if the site sets one.
86
+ if [ -z "${as}" ]; then
87
+ default_session_from_site="$(site_load "${site}" | jq -r '.default_session // ""')"
88
+ if [ -n "${default_session_from_site}" ]; then
89
+ as="${default_session_from_site}"
90
+ else
91
+ die "${EXIT_USAGE_ERROR}" "--as is required (site ${site} has no default_session set)"
92
+ fi
93
+ fi
94
+ assert_safe_name "${as}" "session-name"
95
+ if [ "${interactive}" -eq 0 ] && [ "${auto}" -eq 0 ] && [ -z "${ss_file}" ]; then
96
+ usage
97
+ die "${EXIT_USAGE_ERROR}" "--interactive, --auto, or --storage-state-file is required"
98
+ fi
99
+ if [ "${interactive}" -eq 0 ] && [ "${auto}" -eq 0 ]; then
100
+ [ -f "${ss_file}" ] || die "${EXIT_USAGE_ERROR}" "storage-state-file not found: ${ss_file}"
101
+ fi
102
+
103
+ started_at_ms="$(now_ms)"
104
+
105
+ # Site must exist; load its URL → derive origin for binding.
106
+ profile_json="$(site_load "${site}")" # exits 23 if missing
107
+ site_url="$(printf '%s' "${profile_json}" | jq -r .url)"
108
+ site_origin="$(url_origin "${site_url}")"
109
+
110
+ # --auto: programmatic headless login using stored credential. Validates the
111
+ # credential exists + is bound to this site + has auto_relogin=true. Loads
112
+ # the secret via credential_get_secret (dispatches to whichever backend the
113
+ # cred uses — plaintext / keychain / libsecret). Sends username\0password
114
+ # to the driver via stdin (AP-7 — secret never on argv).
115
+ if [ "${auto}" -eq 1 ]; then
116
+ if ! credential_exists "${as}"; then
117
+ die "${EXIT_SITE_NOT_FOUND}" "credential not found: ${as} (run: creds-add --site ${site} --as ${as} --password-stdin)"
118
+ fi
119
+
120
+ cred_meta="$(credential_load "${as}")"
121
+ cred_site="$(printf '%s' "${cred_meta}" | jq -r .site)"
122
+ cred_account="$(printf '%s' "${cred_meta}" | jq -r .account)"
123
+ cred_auto="$(printf '%s' "${cred_meta}" | jq -r .auto_relogin)"
124
+ cred_auth_flow="$(printf '%s' "${cred_meta}" | jq -r '.auth_flow // "single-step-username-password"')"
125
+ cred_totp_enabled="$(printf '%s' "${cred_meta}" | jq -r '.totp_enabled // false')"
126
+
127
+ if [ "${cred_site}" != "${site}" ]; then
128
+ die "${EXIT_USAGE_ERROR}" "credential ${as} is bound to site '${cred_site}', not '${site}'"
129
+ fi
130
+ if [ "${cred_auto}" != "true" ]; then
131
+ die "${EXIT_USAGE_ERROR}" "credential ${as} has auto_relogin=false; cannot --auto (re-add the credential or use --interactive)"
132
+ fi
133
+ if [ -z "${cred_account}" ] || [ "${cred_account}" = "null" ]; then
134
+ die "${EXIT_USAGE_ERROR}" "credential ${as} has empty account; cannot --auto"
135
+ fi
136
+ # Phase-5 part 3-iii: only single-step-username-password is supported by
137
+ # the playwright-driver auto-relogin path. Other auth_flow values were
138
+ # persisted at creds-add time for documentation; relogin requires user
139
+ # interaction.
140
+ if [ "${cred_auth_flow}" != "single-step-username-password" ]; then
141
+ die "${EXIT_USAGE_ERROR}" "credential ${as} has auth_flow=${cred_auth_flow}; --auto only supports single-step-username-password (use --interactive)"
142
+ fi
143
+
144
+ if [ "${dry_run}" -eq 1 ]; then
145
+ ok "dry-run: would auto-relogin ${as} (site=${site}, account=${cred_account})"
146
+ duration_ms=$(( $(now_ms) - started_at_ms ))
147
+ summary_json verb=login tool=playwright-lib why=auto-relogin-dry-run status=ok would_run=true \
148
+ site="${site}" session="${as}" account="${cred_account}" \
149
+ duration_ms="${duration_ms}"
150
+ exit "${EXIT_OK}"
151
+ fi
152
+
153
+ mkdir -p "${SESSIONS_DIR}"
154
+ chmod 700 "${SESSIONS_DIR}"
155
+ ss_file="${SESSIONS_DIR}/${as}.auto-tmp.$$"
156
+ ok "auto-relogin: launching headless Chromium at ${site_url} as ${cred_account}"
157
+
158
+ # Pipe `account\0password` to driver stdin. AP-7: secret never on argv.
159
+ # Phase-5 part 4-iii: when cred is totp_enabled, append `\0totp_secret` so
160
+ # the driver can replay TOTP automatically after detect2FA fires.
161
+ set +e
162
+ if [ "${cred_totp_enabled}" = "true" ]; then
163
+ {
164
+ printf '%s\0' "${cred_account}"
165
+ credential_get_secret "${as}"
166
+ printf '\0'
167
+ credential_get_totp_secret "${as}"
168
+ } | node "${SCRIPT_DIR}/lib/node/playwright-driver.mjs" auto-relogin \
169
+ --url "${site_url}" --output-path "${ss_file}"
170
+ else
171
+ { printf '%s\0' "${cred_account}"; credential_get_secret "${as}"; } | \
172
+ node "${SCRIPT_DIR}/lib/node/playwright-driver.mjs" auto-relogin \
173
+ --url "${site_url}" --output-path "${ss_file}"
174
+ fi
175
+ driver_rc=${PIPESTATUS[1]}
176
+ set -e
177
+ if [ "${driver_rc}" = "${EXIT_AUTH_INTERACTIVE_REQUIRED}" ]; then
178
+ # Phase-5 part 3-iv: driver detected a 2FA challenge that it couldn't
179
+ # auto-replay (no totp_enabled cred OR replay failed). Tell the user to
180
+ # either store a TOTP secret (creds-add --enable-totp) or fall back to
181
+ # --interactive.
182
+ rm -f "${ss_file}"
183
+ die "${EXIT_AUTH_INTERACTIVE_REQUIRED}" \
184
+ "site requires 2FA / interactive challenge — re-run with --interactive (or store a TOTP secret with creds-add --enable-totp --totp-secret-stdin)"
185
+ fi
186
+ if [ "${driver_rc}" -ne 0 ]; then
187
+ rm -f "${ss_file}"
188
+ die "${EXIT_TOOL_CRASHED}" "auto-relogin failed (driver returned ${driver_rc})"
189
+ fi
190
+ fi
191
+
192
+ # Interactive mode: launch the driver, which opens a headed browser, waits
193
+ # for the user to press Enter, and writes the captured storageState to a
194
+ # temp file. Then we validate + save through the same pipeline as the
195
+ # storage-state-file path.
196
+ if [ "${interactive}" -eq 1 ]; then
197
+ if [ "${dry_run}" -eq 1 ]; then
198
+ ok "dry-run: would launch headed browser to ${site_url}, capture session ${as}"
199
+ duration_ms=$(( $(now_ms) - started_at_ms ))
200
+ summary_json verb=login tool=playwright-lib why=interactive-dry-run status=ok would_run=true \
201
+ site="${site}" session="${as}" duration_ms="${duration_ms}"
202
+ exit "${EXIT_OK}"
203
+ fi
204
+ # Tempfile under SESSIONS_DIR (mode 0600 inherited from 0700 dir + driver chmod).
205
+ mkdir -p "${SESSIONS_DIR}"
206
+ chmod 700 "${SESSIONS_DIR}"
207
+ ss_file="${SESSIONS_DIR}/${as}.interactive-tmp.$$"
208
+ ok "launching headed Chromium at ${site_url}; press Enter when done logging in"
209
+ if ! node "${SCRIPT_DIR}/lib/node/playwright-driver.mjs" login \
210
+ --url "${site_url}" --output-path "${ss_file}"; then
211
+ rm -f "${ss_file}"
212
+ die "${EXIT_TOOL_CRASHED}" "interactive login failed (driver returned non-zero)"
213
+ fi
214
+ fi
215
+
216
+ # Read & validate the storageState file.
217
+ if ! ss_json="$(jq -c . "${ss_file}" 2>/dev/null)"; then
218
+ if [ "${interactive}" -eq 1 ] || [ "${auto}" -eq 1 ]; then
219
+ rm -f "${ss_file}"
220
+ fi
221
+ die "${EXIT_USAGE_ERROR}" "storage-state-file is not valid JSON: ${ss_file}"
222
+ fi
223
+
224
+ # Origin-binding (spec §5.5): every storageState.origins[] must match site_origin.
225
+ # Empty origins[] is allowed (storageState may carry only cookies).
226
+ mismatched="$(printf '%s' "${ss_json}" | jq -r --arg target "${site_origin}" '
227
+ [.origins[]? | select(.origin != $target) | .origin] | join(",")')"
228
+ if [ -n "${mismatched}" ]; then
229
+ die "${EXIT_SESSION_EXPIRED}" \
230
+ "origin mismatch: storage-state-file origins=[${mismatched}], site origin=${site_origin}"
231
+ fi
232
+
233
+ ok "site=${site} session=${as} origin=${site_origin}"
234
+
235
+ if [ "${dry_run}" -eq 1 ]; then
236
+ ok "dry-run: would write ${SESSIONS_DIR}/${as}.json"
237
+ duration_ms=$(( $(now_ms) - started_at_ms ))
238
+ summary_json verb=login tool=playwright-lib why=dry-run status=ok would_run=true \
239
+ site="${site}" session="${as}" duration_ms="${duration_ms}"
240
+ exit "${EXIT_OK}"
241
+ fi
242
+
243
+ # Build meta sidecar.
244
+ captured_at="$(now_iso)"
245
+ if [ "${auto}" -eq 1 ]; then
246
+ ua_tag='browser-skill playwright-lib auto-relogin'
247
+ elif [ "${interactive}" -eq 1 ]; then
248
+ ua_tag='browser-skill playwright-lib interactive capture'
249
+ else
250
+ ua_tag='browser-skill storageState-file import'
251
+ fi
252
+ meta_json="$(jq -nc \
253
+ --arg n "${as}" \
254
+ --arg s "${site}" \
255
+ --arg o "${site_origin}" \
256
+ --arg c "${captured_at}" \
257
+ --arg ua "${ua_tag}" \
258
+ '{
259
+ name: $n, site: $s, origin: $o, captured_at: $c,
260
+ source_user_agent: $ua, expires_in_hours: 168, schema_version: 1
261
+ }')"
262
+
263
+ session_save "${as}" "${ss_json}" "${meta_json}"
264
+ if [ "${interactive}" -eq 1 ] || [ "${auto}" -eq 1 ]; then
265
+ rm -f "${ss_file}"
266
+ fi
267
+ ok "session captured: ${as}"
268
+
269
+ duration_ms=$(( $(now_ms) - started_at_ms ))
270
+ if [ "${auto}" -eq 1 ]; then
271
+ why_tag="auto-relogin"
272
+ elif [ "${interactive}" -eq 1 ]; then
273
+ why_tag="interactive-headed-capture"
274
+ else
275
+ why_tag="storageState-file-import"
276
+ fi
277
+ summary_json verb=login tool=playwright-lib why="${why_tag}" status=ok \
278
+ site="${site}" session="${as}" origin="${site_origin}" \
279
+ expires_in_hours=168 duration_ms="${duration_ms}"