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,69 @@
1
+ #!/usr/bin/env bash
2
+ # creds-remove — typed-name confirmation, then delete metadata + secret via
3
+ # backend. Mirrors remove-session UX exactly: --yes-i-know skips prompt,
4
+ # --dry-run reports without writing.
5
+
6
+ set -euo pipefail
7
+ IFS=$'\n\t'
8
+ umask 077
9
+
10
+ SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
11
+ # shellcheck source=lib/common.sh
12
+ # shellcheck disable=SC1091
13
+ source "${SCRIPT_DIR}/lib/common.sh"
14
+ # shellcheck source=lib/credential.sh
15
+ # shellcheck disable=SC1091
16
+ source "${SCRIPT_DIR}/lib/credential.sh"
17
+ init_paths
18
+
19
+ name=""; yes=0; dry_run=0
20
+ usage() {
21
+ cat <<'USAGE'
22
+ Usage: creds-remove --as CRED_NAME [--yes-i-know] [--dry-run]
23
+
24
+ --as CRED_NAME credential to remove (required)
25
+ --yes-i-know skip the typed-name confirmation
26
+ --dry-run print planned action; remove nothing
27
+ USAGE
28
+ }
29
+ while [ $# -gt 0 ]; do
30
+ case "$1" in
31
+ --as) name="$2"; shift 2 ;;
32
+ --yes-i-know) yes=1; shift ;;
33
+ --dry-run) dry_run=1; shift ;;
34
+ -h|--help) usage; exit 0 ;;
35
+ *) die "${EXIT_USAGE_ERROR}" "unknown flag: $1" ;;
36
+ esac
37
+ done
38
+ [ -n "${name}" ] || { usage; die "${EXIT_USAGE_ERROR}" "--as is required"; }
39
+ assert_safe_name "${name}" "credential-name"
40
+
41
+ started_at_ms="$(now_ms)"
42
+
43
+ if ! credential_exists "${name}"; then
44
+ die "${EXIT_SITE_NOT_FOUND}" "credential not found: ${name}"
45
+ fi
46
+
47
+ if [ "${dry_run}" -eq 1 ]; then
48
+ ok "dry-run: would remove credential ${name}"
49
+ duration_ms=$(( $(now_ms) - started_at_ms ))
50
+ summary_json verb=creds-remove tool=none why=dry-run status=ok would_run=true \
51
+ credential="${name}" duration_ms="${duration_ms}"
52
+ exit "${EXIT_OK}"
53
+ fi
54
+
55
+ if [ "${yes}" -ne 1 ]; then
56
+ printf 'Type the credential name (%s) to confirm removal: ' "${name}" >&2
57
+ answer=""
58
+ IFS= read -r answer || true
59
+ if [ "${answer}" != "${name}" ]; then
60
+ die "${EXIT_USAGE_ERROR}" "removal aborted (confirmation mismatch)"
61
+ fi
62
+ fi
63
+
64
+ credential_delete "${name}"
65
+ ok "credential removed: ${name}"
66
+
67
+ duration_ms=$(( $(now_ms) - started_at_ms ))
68
+ summary_json verb=creds-remove tool=none why=delete status=ok \
69
+ credential="${name}" duration_ms="${duration_ms}"
@@ -0,0 +1,109 @@
1
+ #!/usr/bin/env bash
2
+ # creds-rotate-totp — re-enroll the TOTP shared secret for an existing
3
+ # credential. Use case: service forces a new TOTP secret (re-issued QR code
4
+ # during account recovery, security-incident rotation, etc.). Replaces the
5
+ # `<name>__totp` backend slot with a new value; metadata.totp_enabled stays
6
+ # true; password slot untouched.
7
+ #
8
+ # Usage: bash scripts/browser-creds-rotate-totp.sh \
9
+ # --as CRED_NAME --totp-secret-stdin [--yes-i-know] [--dry-run]
10
+ #
11
+ # Phase-5 part 4-iv. Mirrors creds-migrate's typed-phrase confirmation.
12
+ # AP-7: TOTP secret comes via stdin only — never argv.
13
+
14
+ set -euo pipefail
15
+ IFS=$'\n\t'
16
+ umask 077
17
+
18
+ SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
19
+ # shellcheck source=lib/common.sh
20
+ # shellcheck disable=SC1091
21
+ source "${SCRIPT_DIR}/lib/common.sh"
22
+ # shellcheck source=lib/credential.sh
23
+ # shellcheck disable=SC1091
24
+ source "${SCRIPT_DIR}/lib/credential.sh"
25
+ init_paths
26
+
27
+ name=""; yes=0; dry_run=0; read_stdin=0
28
+
29
+ usage() {
30
+ cat <<'USAGE'
31
+ Usage: creds-rotate-totp --as CRED_NAME --totp-secret-stdin [options]
32
+
33
+ --as CRED_NAME credential to rotate (must be totp_enabled).
34
+ --totp-secret-stdin REQUIRED — read NEW base32 TOTP secret from stdin
35
+ (one chunk; no NUL needed). AP-7: never argv.
36
+ --yes-i-know skip the typed-name confirmation prompt.
37
+ --dry-run print planned action; backend unchanged.
38
+ -h, --help this message
39
+
40
+ Behavior:
41
+ 1. Validates cred exists + is totp_enabled.
42
+ 2. Reads new TOTP secret from stdin.
43
+ 3. Typed-phrase confirmation (unless --yes-i-know).
44
+ 4. Overwrites <name>__totp backend slot.
45
+ 5. Metadata + password slot UNCHANGED.
46
+
47
+ Privacy: summary JSON NEVER includes the new TOTP secret value.
48
+ USAGE
49
+ }
50
+
51
+ while [ $# -gt 0 ]; do
52
+ case "$1" in
53
+ --as) name="$2"; shift 2 ;;
54
+ --totp-secret-stdin) read_stdin=1; shift ;;
55
+ --yes-i-know) yes=1; shift ;;
56
+ --dry-run) dry_run=1; shift ;;
57
+ -h|--help) usage; exit 0 ;;
58
+ *) die "${EXIT_USAGE_ERROR}" "unknown flag: $1" ;;
59
+ esac
60
+ done
61
+
62
+ [ -n "${name}" ] || { usage; die "${EXIT_USAGE_ERROR}" "--as is required"; }
63
+ [ "${read_stdin}" -eq 1 ] || { usage; die "${EXIT_USAGE_ERROR}" "--totp-secret-stdin is required (AP-7: secrets via stdin only)"; }
64
+ assert_safe_name "${name}" "credential-name"
65
+
66
+ if ! credential_exists "${name}"; then
67
+ die "${EXIT_SITE_NOT_FOUND}" "credential not found: ${name}"
68
+ fi
69
+
70
+ cred_meta="$(credential_load "${name}")"
71
+ totp_enabled="$(printf '%s' "${cred_meta}" | jq -r '.totp_enabled // false')"
72
+ if [ "${totp_enabled}" != "true" ]; then
73
+ die "${EXIT_USAGE_ERROR}" "credential ${name} is not totp_enabled (use creds-add --enable-totp instead)"
74
+ fi
75
+
76
+ # Read new TOTP secret from stdin. Single chunk — no NUL splitting needed.
77
+ new_totp="$(cat)"
78
+ if [ -z "${new_totp}" ]; then
79
+ die "${EXIT_USAGE_ERROR}" "--totp-secret-stdin: empty secret on stdin"
80
+ fi
81
+
82
+ started_at_ms="$(now_ms)"
83
+
84
+ if [ "${dry_run}" -eq 1 ]; then
85
+ ok "dry-run: would rotate TOTP secret for ${name} (${#new_totp} chars on stdin)"
86
+ duration_ms=$(( $(now_ms) - started_at_ms ))
87
+ summary_json verb=creds-rotate-totp tool=none why=dry-run status=ok would_run=true \
88
+ credential="${name}" duration_ms="${duration_ms}"
89
+ exit "${EXIT_OK}"
90
+ fi
91
+
92
+ if [ "${yes}" -ne 1 ]; then
93
+ printf 'Type the credential name (%s) to confirm TOTP rotation: ' "${name}" >&2
94
+ answer=""
95
+ IFS= read -r answer || true
96
+ if [ "${answer}" != "${name}" ]; then
97
+ die "${EXIT_USAGE_ERROR}" "rotation aborted (confirmation mismatch)"
98
+ fi
99
+ fi
100
+
101
+ # Overwrite the <name>__totp backend slot. credential_set_totp_secret reads
102
+ # stdin — pipe the new secret in.
103
+ printf '%s' "${new_totp}" | credential_set_totp_secret "${name}"
104
+
105
+ ok "TOTP secret rotated: ${name}"
106
+
107
+ duration_ms=$(( $(now_ms) - started_at_ms ))
108
+ summary_json verb=creds-rotate-totp tool=none why=rotate-totp status=ok \
109
+ credential="${name}" duration_ms="${duration_ms}"
@@ -0,0 +1,82 @@
1
+ #!/usr/bin/env bash
2
+ # creds-show — emit credential metadata. Optional --reveal exposes the secret
3
+ # value behind a typed-phrase confirmation gate.
4
+ #
5
+ # CRITICAL SECURITY INVARIANT (default mode): this verb NEVER emits the secret
6
+ # payload. Only the metadata sidecar (site, account, backend, auto_relogin,
7
+ # totp_enabled, created_at) is surfaced. The agent has no business seeing raw
8
+ # secret material; downstream auth flows pass the payload via stdin pipes.
9
+ #
10
+ # --reveal flow: typed-phrase confirmation (mirror remove-session UX). User
11
+ # must type the credential name back via stdin (single line). On match, the
12
+ # secret is fetched (via credential_get_secret) and emitted alongside its
13
+ # masked preview (via mask_string). On mismatch, the verb dies with a
14
+ # self-healing hint. The masked preview lets the user confirm visually they
15
+ # revealed the right thing without re-leaking the value.
16
+
17
+ set -euo pipefail
18
+ IFS=$'\n\t'
19
+ umask 077
20
+
21
+ SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
22
+ # shellcheck source=lib/common.sh
23
+ # shellcheck disable=SC1091
24
+ source "${SCRIPT_DIR}/lib/common.sh"
25
+ # shellcheck source=lib/credential.sh
26
+ # shellcheck disable=SC1091
27
+ source "${SCRIPT_DIR}/lib/credential.sh"
28
+ # shellcheck source=lib/mask.sh
29
+ # shellcheck disable=SC1091
30
+ source "${SCRIPT_DIR}/lib/mask.sh"
31
+ init_paths
32
+
33
+ name=""; reveal=0
34
+ usage() {
35
+ cat <<'USAGE'
36
+ Usage: creds-show --as CRED_NAME [--reveal]
37
+
38
+ --as CRED_NAME credential to show (required)
39
+ --reveal after typed-phrase confirmation, include secret value
40
+ + masked preview in the output JSON
41
+ USAGE
42
+ }
43
+ while [ $# -gt 0 ]; do
44
+ case "$1" in
45
+ --as) name="$2"; shift 2 ;;
46
+ --reveal) reveal=1; shift ;;
47
+ -h|--help) usage; exit 0 ;;
48
+ *) die "${EXIT_USAGE_ERROR}" "unknown flag: $1" ;;
49
+ esac
50
+ done
51
+ [ -n "${name}" ] || { usage; die "${EXIT_USAGE_ERROR}" "--as is required"; }
52
+ assert_safe_name "${name}" "credential-name"
53
+
54
+ started_at_ms="$(now_ms)"
55
+
56
+ if ! credential_exists "${name}"; then
57
+ die "${EXIT_SITE_NOT_FOUND}" "credential not found: ${name}"
58
+ fi
59
+
60
+ meta="$(credential_load "${name}")"
61
+
62
+ if [ "${reveal}" -eq 1 ]; then
63
+ printf 'Type the credential name (%s) to confirm reveal: ' "${name}" >&2
64
+ answer=""
65
+ IFS= read -r answer || true
66
+ if [ "${answer}" != "${name}" ]; then
67
+ die "${EXIT_USAGE_ERROR}" "reveal aborted (confirmation mismatch)"
68
+ fi
69
+ secret="$(credential_get_secret "${name}")"
70
+ secret_masked="$(mask_string "${secret}")"
71
+ duration_ms=$(( $(now_ms) - started_at_ms ))
72
+ jq -cn --arg n "${name}" --argjson m "${meta}" --arg s "${secret}" --arg sm "${secret_masked}" --argjson d "${duration_ms}" \
73
+ '{verb: "creds-show", tool: "none", why: "reveal", status: "ok",
74
+ credential: $n, meta: $m, secret_masked: $sm, secret: $s,
75
+ duration_ms: $d}'
76
+ exit "${EXIT_OK}"
77
+ fi
78
+
79
+ duration_ms=$(( $(now_ms) - started_at_ms ))
80
+ jq -cn --arg n "${name}" --argjson m "${meta}" --argjson d "${duration_ms}" \
81
+ '{verb: "creds-show", tool: "none", why: "show", status: "ok",
82
+ credential: $n, meta: $m, duration_ms: $d}'
@@ -0,0 +1,94 @@
1
+ #!/usr/bin/env bash
2
+ # scripts/browser-creds-totp.sh — produce the current TOTP code for a stored
3
+ # credential. Reads the base32 shared secret from the backend (`<name>:totp`
4
+ # slot stored at `creds-add --enable-totp --totp-secret-stdin` time), generates
5
+ # the RFC 6238 6-digit code via scripts/lib/node/totp.mjs, prints to stdout.
6
+ #
7
+ # Usage: bash scripts/browser-creds-totp.sh --as CRED_NAME [--dry-run]
8
+ #
9
+ # Phase-5 part 4-ii. login --auto auto-replay of TOTP codes is part 4-iii.
10
+
11
+ set -euo pipefail
12
+ IFS=$'\n\t'
13
+
14
+ SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
15
+ # shellcheck source=lib/common.sh
16
+ # shellcheck disable=SC1091
17
+ source "${SCRIPT_DIR}/lib/common.sh"
18
+ # shellcheck source=lib/output.sh
19
+ # shellcheck disable=SC1091
20
+ source "${SCRIPT_DIR}/lib/output.sh"
21
+ # shellcheck source=lib/credential.sh
22
+ # shellcheck disable=SC1091
23
+ source "${SCRIPT_DIR}/lib/credential.sh"
24
+
25
+ init_paths
26
+
27
+ SUMMARY_T0="$(now_ms)"; export SUMMARY_T0
28
+
29
+ as=""
30
+ dry_run=0
31
+
32
+ usage() {
33
+ cat <<'USAGE'
34
+ Usage: creds-totp --as CRED_NAME [--dry-run]
35
+
36
+ --as CRED_NAME credential name (must be totp_enabled, must have a
37
+ stored TOTP secret).
38
+ --dry-run report planned action; emit nothing on stdout.
39
+ -h, --help this message
40
+
41
+ Stdout: 6-digit code (or empty on dry-run).
42
+ Stderr: human-friendly status / errors.
43
+ USAGE
44
+ }
45
+
46
+ while [ $# -gt 0 ]; do
47
+ case "$1" in
48
+ --as) as="$2"; shift 2 ;;
49
+ --dry-run) dry_run=1; shift ;;
50
+ -h|--help) usage; exit 0 ;;
51
+ *) die "${EXIT_USAGE_ERROR}" "unknown flag: $1" ;;
52
+ esac
53
+ done
54
+
55
+ [ -n "${as}" ] || { usage; die "${EXIT_USAGE_ERROR}" "--as is required"; }
56
+ assert_safe_name "${as}" "credential-name"
57
+
58
+ if ! credential_exists "${as}"; then
59
+ die "${EXIT_SITE_NOT_FOUND}" "credential not found: ${as}"
60
+ fi
61
+
62
+ cred_meta="$(credential_load "${as}")"
63
+ totp_enabled="$(printf '%s' "${cred_meta}" | jq -r '.totp_enabled // false')"
64
+ if [ "${totp_enabled}" != "true" ]; then
65
+ die "${EXIT_USAGE_ERROR}" "credential ${as} is not totp_enabled (re-add with --enable-totp --totp-secret-stdin)"
66
+ fi
67
+
68
+ if [ "${dry_run}" -eq 1 ]; then
69
+ ok "dry-run: would generate TOTP code for ${as}"
70
+ duration_ms=$(( $(now_ms) - $(printf '%s' "${SUMMARY_T0}") ))
71
+ summary_json verb=creds-totp tool=node why=dry-run status=ok would_run=true \
72
+ credential="${as}" duration_ms="${duration_ms}"
73
+ exit "${EXIT_OK}"
74
+ fi
75
+
76
+ # Pipe the TOTP secret to the node generator via stdin (AP-7: never argv).
77
+ node_bin="${BROWSER_SKILL_NODE_BIN:-node}"
78
+ totp_script="${SCRIPT_DIR}/lib/node/totp.mjs"
79
+
80
+ set +e
81
+ code="$(credential_get_totp_secret "${as}" | "${node_bin}" "${totp_script}")"
82
+ gen_rc=$?
83
+ set -e
84
+
85
+ if [ "${gen_rc}" -ne 0 ]; then
86
+ die "${EXIT_GENERIC_ERROR}" "TOTP code generation failed (rc=${gen_rc})"
87
+ fi
88
+
89
+ # Emit code on stdout — agent reads it then types into browser.
90
+ printf '%s\n' "${code}"
91
+
92
+ duration_ms=$(( $(now_ms) - $(printf '%s' "${SUMMARY_T0}") ))
93
+ summary_json verb=creds-totp tool=node why=generate-totp-code status=ok \
94
+ credential="${as}" duration_ms="${duration_ms}"