browser-debugger-cli 0.15.0 → 0.16.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 (133) hide show
  1. package/.claude/skills/bdg/SKILL.md +2 -1
  2. package/dist/cdp/methodTarget.d.ts +92 -0
  3. package/dist/cdp/methodTarget.js +159 -0
  4. package/dist/cdp/protocol.d.ts +16 -1
  5. package/dist/cdp/protocol.js +21 -0
  6. package/dist/cdp/schema.d.ts +55 -1
  7. package/dist/cdp/schema.js +134 -25
  8. package/dist/cdp/types.d.ts +3 -1
  9. package/dist/commands/cdp.d.ts +38 -1
  10. package/dist/commands/cdp.js +200 -133
  11. package/dist/commands/cleanup.js +18 -4
  12. package/dist/commands/dom/formInteraction.js +8 -4
  13. package/dist/commands/dom/helpers/index.d.ts +4 -4
  14. package/dist/commands/dom/helpers/index.js +3 -3
  15. package/dist/commands/dom/helpers/query.d.ts +2 -2
  16. package/dist/commands/dom/helpers/query.js +2 -2
  17. package/dist/commands/dom/helpers/screenshot.d.ts +21 -26
  18. package/dist/commands/dom/helpers/screenshot.js +50 -668
  19. package/dist/commands/dom/screenshot.js +56 -36
  20. package/dist/commands/optionBehaviors.js +18 -8
  21. package/dist/commands/shared/CommandRunner.d.ts +5 -0
  22. package/dist/commands/shared/CommandRunner.js +18 -3
  23. package/dist/commands/shared/interrupt.d.ts +40 -0
  24. package/dist/commands/shared/interrupt.js +73 -0
  25. package/dist/commands/shared/optionTypes.d.ts +2 -0
  26. package/dist/commands/shared/startHelpers.d.ts +26 -3
  27. package/dist/commands/shared/startHelpers.js +145 -23
  28. package/dist/commands/types.d.ts +5 -0
  29. package/dist/connection/cdp.js +1 -16
  30. package/dist/connection/chromeIdentity.d.ts +24 -5
  31. package/dist/connection/chromeIdentity.js +53 -22
  32. package/dist/connection/launcher.d.ts +34 -1
  33. package/dist/connection/launcher.js +98 -10
  34. package/dist/connection/typed-cdp.d.ts +3 -2
  35. package/dist/constants.d.ts +1 -1
  36. package/dist/constants.js +1 -1
  37. package/dist/daemon/SessionController.d.ts +10 -5
  38. package/dist/daemon/SessionController.js +15 -8
  39. package/dist/daemon/ipcServer.js +1 -1
  40. package/dist/daemon/launcher.d.ts +5 -0
  41. package/dist/daemon/launcher.js +8 -1
  42. package/dist/daemon/session/Session.d.ts +5 -1
  43. package/dist/daemon/session/Session.js +9 -8
  44. package/dist/daemon/session/TelemetryStore.d.ts +5 -0
  45. package/dist/daemon/session/TelemetryStore.js +4 -0
  46. package/dist/daemon/session/captureGate.d.ts +59 -0
  47. package/dist/daemon/session/captureGate.js +96 -0
  48. package/dist/daemon/session/chromeConnection.d.ts +16 -1
  49. package/dist/daemon/session/chromeConnection.js +34 -4
  50. package/dist/daemon/session/collectors.d.ts +15 -0
  51. package/dist/daemon/session/collectors.js +39 -2
  52. package/dist/daemon/session/commandRegistry.d.ts +14 -1
  53. package/dist/daemon/session/commandRegistry.js +46 -11
  54. package/dist/daemon/session/downloads.d.ts +32 -0
  55. package/dist/daemon/session/downloads.js +96 -0
  56. package/dist/daemon/session/interactions.d.ts +3 -2
  57. package/dist/daemon/session/interactions.js +7 -2
  58. package/dist/daemon/session/plugins.js +6 -0
  59. package/dist/daemon.js +12843 -11482
  60. package/dist/errors/CommandError.d.ts +2 -0
  61. package/dist/errors/issues.d.ts +1 -1
  62. package/dist/errors/messages.d.ts +58 -0
  63. package/dist/errors/messages.js +112 -0
  64. package/dist/index.js +999 -1020
  65. package/dist/ipc/client.d.ts +14 -1
  66. package/dist/ipc/client.js +21 -4
  67. package/dist/ipc/protocol/commands.d.ts +32 -2
  68. package/dist/ipc/protocol/commands.js +1 -0
  69. package/dist/ipc/protocol/domTypes.d.ts +24 -1
  70. package/dist/ipc/session/queries.d.ts +3 -0
  71. package/dist/ipc/session/types.d.ts +5 -0
  72. package/dist/ipc/transport/IPCError.d.ts +9 -0
  73. package/dist/ipc/transport/IPCError.js +12 -0
  74. package/dist/ipc/transport/errors.d.ts +2 -1
  75. package/dist/ipc/transport/errors.js +4 -1
  76. package/dist/ipc/transport/index.d.ts +4 -2
  77. package/dist/ipc/transport/index.js +13 -3
  78. package/dist/runtime/dom/actionEffects.d.ts +48 -9
  79. package/dist/runtime/dom/actionEffects.js +269 -34
  80. package/dist/runtime/dom/actionEffectsScripts.d.ts +45 -0
  81. package/dist/runtime/dom/actionEffectsScripts.js +101 -2
  82. package/dist/runtime/dom/captureArea.d.ts +35 -0
  83. package/dist/runtime/dom/captureArea.js +203 -0
  84. package/dist/runtime/dom/elementInfo.d.ts +10 -8
  85. package/dist/runtime/dom/elementInfo.js +8 -6
  86. package/dist/runtime/dom/formDiscovery.d.ts +1 -1
  87. package/dist/runtime/page/bdgWorld.d.ts +9 -0
  88. package/dist/runtime/page/bdgWorld.js +11 -0
  89. package/dist/runtime/page/captureEmulation.d.ts +119 -0
  90. package/dist/runtime/page/captureEmulation.js +189 -0
  91. package/dist/runtime/page/captureScroll.d.ts +24 -0
  92. package/dist/runtime/page/captureScroll.js +124 -0
  93. package/dist/runtime/page/screenshot.d.ts +41 -0
  94. package/dist/runtime/page/screenshot.js +394 -0
  95. package/dist/session/paths.d.ts +14 -0
  96. package/dist/session/paths.js +25 -0
  97. package/dist/telemetry/downloads.d.ts +127 -0
  98. package/dist/telemetry/downloads.js +265 -0
  99. package/dist/telemetry/har/builder.js +22 -7
  100. package/dist/telemetry/har/sanitize.d.ts +7 -3
  101. package/dist/telemetry/har/sanitize.js +52 -6
  102. package/dist/telemetry/har/sanitizeBody.d.ts +47 -7
  103. package/dist/telemetry/har/sanitizeBody.js +429 -56
  104. package/dist/telemetry/har/types.d.ts +2 -0
  105. package/dist/telemetry/network.d.ts +4 -4
  106. package/dist/telemetry/network.js +38 -4
  107. package/dist/telemetry/networkRetention.d.ts +35 -14
  108. package/dist/telemetry/networkRetention.js +62 -26
  109. package/dist/types.d.ts +9 -14
  110. package/dist/ui/OutputBuilder.d.ts +3 -2
  111. package/dist/ui/OutputBuilder.js +4 -3
  112. package/dist/ui/formatters/cdp.d.ts +32 -9
  113. package/dist/ui/formatters/cdp.js +77 -6
  114. package/dist/ui/formatters/details.js +7 -15
  115. package/dist/ui/formatters/preview.d.ts +2 -0
  116. package/dist/ui/formatters/preview.js +7 -1
  117. package/dist/ui/formatters/status.js +6 -1
  118. package/dist/ui/formatting.d.ts +7 -0
  119. package/dist/ui/formatting.js +13 -0
  120. package/dist/ui/logging/logger.d.ts +1 -1
  121. package/dist/ui/messages/chrome.d.ts +13 -0
  122. package/dist/ui/messages/chrome.js +26 -0
  123. package/dist/ui/messages/commands.d.ts +71 -3
  124. package/dist/ui/messages/commands.js +98 -3
  125. package/dist/ui/messages/networkMessages.d.ts +24 -5
  126. package/dist/ui/messages/networkMessages.js +31 -8
  127. package/dist/utils/async.d.ts +3 -2
  128. package/dist/utils/async.js +16 -3
  129. package/dist/utils/http.d.ts +11 -4
  130. package/dist/utils/http.js +5 -3
  131. package/package.json +18 -4
  132. /package/dist/{commands/dom → runtime/page}/screenshotResize.d.ts +0 -0
  133. /package/dist/{commands/dom → runtime/page}/screenshotResize.js +0 -0
@@ -3,8 +3,9 @@
3
3
  */
4
4
  import { extname } from 'path';
5
5
  import { DomElementResolver } from './DomElementResolver.js';
6
- import { capturePageScreenshot, captureElementScreenshot, resolveSelector, selectMatch, } from './helpers/index.js';
6
+ import { captureScreenshot, resolveSelector, screenshotInterrupted, selectMatch, } from './helpers/index.js';
7
7
  import { runCommand } from '../shared/CommandRunner.js';
8
+ import { abortOnInterrupt, unlessInterrupted } from '../shared/interrupt.js';
8
9
  import { assertFilePath, outputPathError } from '../shared/outputFile.js';
9
10
  import { positiveIntRule } from '../shared/validation.js';
10
11
  import { CommandError } from '../../errors/index.js';
@@ -47,34 +48,43 @@ export function resolveImageFormat(outputPath, requested) {
47
48
  }
48
49
  return requested ?? fromExtension ?? 'png';
49
50
  }
50
- function buildPageScreenshotOptions(options) {
51
- return filterDefined({
52
- format: options.format,
53
- quality: options.quality,
54
- fullPage: options.fullPage,
55
- noResize: options.resize === false,
56
- scroll: options.scroll,
57
- });
58
- }
59
- function buildElementScreenshotOptions(options) {
60
- return filterDefined({
61
- format: options.format,
62
- quality: options.quality,
63
- noResize: options.resize === false,
64
- padding: options.padding,
65
- });
51
+ /**
52
+ * What to capture, from the command's options: the element `backendNodeId`
53
+ * names (with its `--padding`), else the page (`--full-page`, `--scroll`).
54
+ *
55
+ * @param options - Command options
56
+ * @param backendNodeId - Element to capture, if any
57
+ * @returns Daemon request
58
+ */
59
+ function buildScreenshotRequest(options, backendNodeId) {
60
+ const shared = {
61
+ format: options.format ?? 'png',
62
+ ...filterDefined({ quality: options.quality }),
63
+ ...(options.resize === false && { noResize: true }),
64
+ };
65
+ if (backendNodeId !== undefined) {
66
+ return { ...shared, backendNodeId, ...filterDefined({ padding: options.padding }) };
67
+ }
68
+ return { ...shared, ...filterDefined({ fullPage: options.fullPage, scroll: options.scroll }) };
66
69
  }
67
70
  function hasElementTarget(options) {
68
71
  return options.selector !== undefined || options.index !== undefined;
69
72
  }
73
+ /**
74
+ * The element to capture: the selector's match (`--index` picks one), or the
75
+ * cached query result at `--index`.
76
+ *
77
+ * @param options - Command options
78
+ * @returns Its backend node id
79
+ * @throws CommandError (81) when neither is given
80
+ */
70
81
  async function resolveElementNodeId(options) {
71
82
  if (options.selector !== undefined && options.index !== undefined) {
72
- return { backendNodeId: await selectMatch(options.selector, options.index) };
83
+ return selectMatch(options.selector, options.index);
73
84
  }
74
85
  if (options.index !== undefined) {
75
- const resolver = DomElementResolver.getInstance();
76
- const node = await resolver.getNodeIdForIndex(options.index);
77
- return { backendNodeId: node.nodeId };
86
+ const node = await DomElementResolver.getInstance().getNodeIdForIndex(options.index);
87
+ return node.nodeId;
78
88
  }
79
89
  if (options.selector !== undefined) {
80
90
  return resolveSelector(options.selector);
@@ -120,32 +130,42 @@ function ensureDirectory(dirPath, fs) {
120
130
  function formatFrameFilename(frameNumber, format) {
121
131
  return `${String(frameNumber).padStart(3, '0')}.${format}`;
122
132
  }
133
+ /**
134
+ * Capture the page. Ctrl-C (or SIGTERM) cancels the capture and exits 130
135
+ * (143), with the error envelope under `--json`.
136
+ *
137
+ * @param outputPath - File to write
138
+ * @param options - Command options
139
+ */
123
140
  async function handlePageScreenshot(outputPath, options) {
141
+ const interrupt = abortOnInterrupt();
124
142
  await runCommand(async () => {
125
- const screenshotOptions = buildPageScreenshotOptions(options);
126
- const result = await capturePageScreenshot(outputPath, screenshotOptions);
143
+ const request = buildScreenshotRequest(options);
144
+ const result = await captureScreenshot(outputPath, request, interrupt);
127
145
  return { success: true, data: result };
128
146
  }, options, formatDomScreenshot);
129
147
  }
148
+ /**
149
+ * Capture one element; interrupted like {@link handlePageScreenshot}, also
150
+ * while the element is looked up.
151
+ *
152
+ * @param outputPath - File to write
153
+ * @param options - Command options
154
+ */
130
155
  async function handleElementScreenshot(outputPath, options) {
156
+ const interrupt = abortOnInterrupt();
131
157
  await runCommand(async () => {
132
- const nodeRef = await resolveElementNodeId(options);
133
- const screenshotOptions = buildElementScreenshotOptions(options);
134
- const result = await captureElementScreenshot(outputPath, nodeRef, screenshotOptions);
135
- const elementResult = addElementInfo(result, options);
158
+ const lookup = resolveElementNodeId(options);
159
+ const backendNodeId = await unlessInterrupted(lookup, interrupt, screenshotInterrupted);
160
+ const request = buildScreenshotRequest(options, backendNodeId);
161
+ const shot = await captureScreenshot(outputPath, request, interrupt);
162
+ const elementResult = addElementInfo(shot, options);
136
163
  return { success: true, data: elementResult };
137
164
  }, options, formatDomScreenshot);
138
165
  }
139
166
  async function captureSequenceFrame(outputPath, options) {
140
- if (hasElementTarget(options)) {
141
- const nodeRef = await resolveElementNodeId(options);
142
- const elementOptions = buildElementScreenshotOptions(options);
143
- await captureElementScreenshot(outputPath, nodeRef, elementOptions);
144
- }
145
- else {
146
- const pageOptions = buildPageScreenshotOptions(options);
147
- await capturePageScreenshot(outputPath, pageOptions);
148
- }
167
+ const backendNodeId = hasElementTarget(options) ? await resolveElementNodeId(options) : undefined;
168
+ await captureScreenshot(outputPath, buildScreenshotRequest(options, backendNodeId));
149
169
  }
150
170
  async function handleSequenceCapture(outputDir, options) {
151
171
  const fs = await import('fs');
@@ -6,13 +6,13 @@
6
6
  *
7
7
  * @see docs/principles/SELF_DOCUMENTING_SYSTEMS.md
8
8
  */
9
- import { MAX_EDGE_PX, PIXELS_PER_TOKEN, TALL_PAGE_THRESHOLD, } from './dom/screenshotResize.js';
9
+ import { MAX_EDGE_PX, PIXELS_PER_TOKEN, TALL_PAGE_THRESHOLD, } from '../runtime/page/screenshotResize.js';
10
10
  /** What DOM actions report about the network requests they triggered */
11
11
  const TRIGGERED_REQUESTS_BEHAVIOR = 'Requests (and WebSocket connections) that start after the action begins are returned as triggeredRequests (method, url, status, durationMs; pending when still running at return, loading when the response arrived but its body is still streaming; with resourceType; human output lists documents, XHR/fetch and WebSockets first (up to 10) and counts static assets on one line; absent when network telemetry is off). Attribution is by time: requests a page timer or poller starts meanwhile are listed too, whether or not the action caused them';
12
12
  /** What every DOM action reports about the page besides its requests */
13
- const ACTION_EFFECTS_BEHAVIOR = 'The result also says what changed on the page: a navigation (Page: navigated to <url> (status), or URL changed to <url> (same document); JSON navigation { url, sameDocument, status }), and messages that appeared or changed in alert/status/aria-live elements or flash/error/toast-like classes (New text: "…" (element); JSON messages [{ text, element }], at most 3, with "(+N more)" and moreMessages for the rest; after a navigation every message on the new page counts; texts of only digits and time units, such as clocks and counters, are left out, but other text that changes on its own, such as a rotating banner, can show up). Both are absent when nothing changed. Cost: one page script sent before the action without waiting for it and one read after it, a few ms; when the page does not answer (a pending navigation) bdg waits at most 200 ms for the snapshot and 250 ms per read, and the navigation is still reported from CDP events';
13
+ const ACTION_EFFECTS_BEHAVIOR = 'The result also says what changed on the page: a navigation (Page: navigated to <url> (status), or URL changed to <url> (same document); JSON navigation { url, sameDocument, status }), and messages that appeared or changed in alert/status/aria-live elements or flash/error/toast-like classes (New text: "…" (element); JSON messages [{ text, element }], at most 3, with "(+N more)" and moreMessages for the rest; after a navigation every message on the new page counts; texts of only digits and time units, such as clocks and counters, are left out, but other text that changes on its own, such as a rotating banner, can show up). Both are absent when nothing changed. Downloads that began meanwhile are listed (Download: <name> → <path> (<state>, <size>); JSON downloads [{ url, suggestedFilename, path, state: inProgress|completed|canceled, bytes }]); a Chrome bdg launched saves them into <session dir>/downloads (never ~/Downloads), named as suggested with (1), (2)… when taken, and one still running when the action returns is inProgress and appears at its path once complete; downloads of tabs the page opens (target=_blank, window.open) count too; when that directory cannot be created they are refused (canceled, with reason) instead of going to ~/Downloads, and if Chrome refuses the download behavior bdg status warns that downloads are not redirected; ones that begin after the action returned are listed by bdg status and bdg peek (Downloads: N (last: …)); files stay after stop and cleanup; with --chrome-ws-url (the exception: not a browser bdg owns) downloads go to the download folder of that Chrome (usually ~/Downloads), only downloads of the session page are tracked (not those of tabs or popups it opens), and bdg sets behavior default with events, which replaces one another CDP client set and which Chrome drops when the session connection closes. Cost: one page script sent before the action without waiting for it and one read after it, a few ms; when the page does not answer (a pending navigation) bdg waits at most 200 ms for the snapshot and 250 ms per read, and the navigation is still reported from CDP events';
14
14
  /** What click and pressKey report when the page was still changing as they returned */
15
- const STILL_CHANGING_BEHAVIOR = 'When the page was still changing as the action returned, the status line says (page still changing), a note below it says what was pending and suggests bdg dom wait <selector>, and JSON has settled: false with pending { requests (document, fetch/XHR and script requests still running), navigation (a new page still loading), loading (a loading indicator that appeared, e.g. "div#loading"), domChanging (DOM changes kept coming in bursts over a second look 250 ms later; a single render, ticking text and style animations do not count), busy (the page did not answer within 250 ms: a long script) }; absent when the page looked settled (exit code stays 0). A result a timer renders later, with no DOM change, request or loading indicator before it, is not detected. Cost: nothing extra, except 250 ms plus one read when the DOM looked busy. Not checked with --no-wait';
15
+ const STILL_CHANGING_BEHAVIOR = 'When the page was still changing as the action returned, the status line says (page still changing), a note below it says what was pending and suggests bdg dom wait <selector>, and JSON has settled: false with pending { requests (document, fetch/XHR and script requests still running), navigation (a new page still loading), loading (a loading indicator that appeared, e.g. "div#loading"), domChanging (DOM changes came in bursts and, at a second look 250 ms later, again, with no quiet gap over 150 ms; time the page could not run its tasks, a long task or timers a starved renderer runs late, is not quiet, and a single burst more than 150 ms old with at most 75 ms of quiet time since also gets the second look (only that 250 ms; domChanging still needs a fresh change at it); a single render, ticking text and style animations do not count, and changes more than about 150 ms apart while the page could run look settled), busy (the page did not answer within 250 ms and ran a long task since the action began, or did not answer a check within 250 ms more: a long script; a page that answered late without a long task, a starved renderer, gets its answer waited for instead; a renderer descheduled in the middle of a task can still record a long task and is then busy; the check runs at most once per action, adding at most 250 ms, so an endless script is reported busy about 500 ms after the action) }; absent when the page looked settled (exit code stays 0). A result a timer renders later, with no DOM change, request or loading indicator before it, is not detected. Cost: nothing extra, except 250 ms plus one read when the DOM looked busy. Not checked with --no-wait';
16
16
  /** What hover and pressKey report about elements they showed */
17
17
  const SHOWN_BEHAVIOR = 'Elements the action showed are listed (Shown: <element> "<text>"; JSON shown [{ text, element }], at most 3, outermost first): elements with visible text added inside the target\'s form, search box, dialog or combobox (else its grandparent, or its parent when that is the body), and popups and messages added anywhere (tooltip, menu, listbox, dialog, alert, status roles, popover, aria-live, message-like classes); widgets elsewhere on the page and re-rendered elements whose text was there before do not count';
18
18
  /** What `--no-wait` does to a DOM action's triggered requests */
@@ -331,9 +331,9 @@ const OPTION_BEHAVIORS = {
331
331
  automaticBehavior: FOLLOW_BEHAVIOR,
332
332
  },
333
333
  'har:--include-sensitive': {
334
- default: 'The HAR is sanitized: values become "[redacted]" for Authorization, Proxy-Authorization, Authentication, Cookie and Set-Cookie headers, X-*key/token/secret/auth headers and headers with an api-key/apikey/token/secret/jwt/subscription-key/session(-id) segment (www-authenticate is kept); every cookie value; query and fragment parameters named like credentials (plus code, sig, key) in the request URL, queryString, redirectURL and Location/Referer headers ("%5Bredacted%5D" in URLs); and password/token/secret/key/session/signature fields of JSON (primitives at any depth under such a name), form-urlencoded (also sniffed when the Content-Type says otherwise) and multipart request bodies. A JSON body too deep to walk becomes "[redacted]" whole. Header, cookie and parameter names, cookie attributes, headersSize and bodySize stay; log.comment and JSON sanitized: true say so',
334
+ default: 'The HAR is sanitized: values become "[redacted]" for Authorization, Proxy-Authorization, Authentication, Cookie and Set-Cookie headers, X-*key/token/secret/auth headers and headers with an api-key/apikey/token/secret/jwt/subscription-key/session(-id) segment (www-authenticate is kept); every cookie value; query and fragment parameters named like credentials (plus code, sig, key) in the request URL, queryString, redirectURL and Location/Referer headers ("%5Bredacted%5D" in URLs); and password/token/secret/key/session/signature/bearer/cookie/csrf/refresh/auth/sid/pin/ssn/card-number fields of JSON (primitives at any depth under such a name), form-urlencoded (also sniffed when the Content-Type says otherwise, user[password] names too) and multipart request and response bodies (an access_token/refresh_token/id_token login response too) and of WebSocket messages, also in truncated JSON, JSON encoded in string values (3 levels), socket.io and SockJS packets, server-sent events, NDJSON, base64 bodies with no, a generic, JSON, form or event-stream type and binary WebSocket messages that are UTF-8 text; any whole JWT (eyJ…) in a JSON string, form or URL value, multipart part or text body (only the JWT is replaced). camelCase names count as words (userPin). JSON is edited in place, not re-serialized: everything but the replaced values (64-bit numbers, formatting, duplicate keys, a BOM or )]}\' prefix) stays byte for byte. Header, cookie and parameter names, cookie attributes, headersSize, bodySize and content.size stay; log.comment and JSON sanitized: true say so',
335
335
  whenEnabled: 'Writes every captured value (JSON sanitized: false); human output warns that the file holds credentials',
336
- automaticBehavior: 'Matching is by name, so it over-redacts: harmless values under credential-looking names (tokenCount: 5, sessionLength) are replaced too, and credentials under other names are kept. Unlike Chrome DevTools, which drops these headers and empties cookies, names are kept so the HAR still shows a request was authenticated. Response bodies and WebSocket messages are not redacted. HAR files are written readable by their owner only (0600). network headers and network getCookies always show real values',
336
+ automaticBehavior: 'Matching is by name, so it over-redacts: harmless values under credential-looking names (tokenCount: 5, sessionLength, refreshInterval, cookieConsent) are replaced too, and credentials under other names are kept. Only JSON syntax is understood: single-quoted strings, unquoted keys, JSONP, STOMP passcode: header lines and bare values with spaces ({"token":abc def}, a non-JWT token after Bearer in text) are not (fully) redacted. A body the sanitizer fails on is replaced whole by [redacted]. Unlike Chrome DevTools, which drops these headers and empties cookies, names are kept so the HAR still shows a request was authenticated. Kept as captured: other binary (base64) bodies, binary WebSocket messages that are not UTF-8, and text that is not JSON or a form (apart from JWTs); a WebSocket message cut at 100 KB has _truncatedFrom. HAR files are written readable by their owner only (0600). network headers and network getCookies always show real values',
337
337
  },
338
338
  'peek:--verbose': {
339
339
  default: 'Compact output (truncated URLs, no resource types)',
@@ -365,8 +365,8 @@ const OPTION_BEHAVIORS = {
365
365
  whenEnabled: 'Alias for --force, kept for compatibility',
366
366
  },
367
367
  'cleanup:--purge': {
368
- default: "A named session's directory (Chrome profile, ~60 MB; logs; port.txt) is kept for its next start",
369
- whenEnabled: 'After cleaning up, deletes the directory of the session named by --session (exit 81 without --session); a running session is refused unless --force is given, and the directory is kept (exit 90) if the daemon still answers, cleanup reported a problem, or its Chrome has not exited',
368
+ default: "A named session's directory (Chrome profile, ~60 MB; logs; port.txt; downloads/) is kept for its next start",
369
+ whenEnabled: 'After cleaning up, deletes the directory of the session named by --session, downloaded files included (exit 81 without --session); a running session is refused unless --force is given, and the directory is kept (exit 90) if the daemon still answers, cleanup reported a problem, or its Chrome has not exited',
370
370
  },
371
371
  'bdg:--viewport': {
372
372
  default: 'A launched Chrome opens a 1920x1080 window (the viewport is smaller by the scrollbar, and in a visible window by the browser UI); an attached Chrome keeps its window',
@@ -386,12 +386,22 @@ const OPTION_BEHAVIORS = {
386
386
  'bdg:--chrome-ws-url': {
387
387
  default: 'bdg launches its own Chrome (closed on stop)',
388
388
  whenEnabled: 'Attaches to a running Chrome instead; it keeps running after stop. --port, -u and --[no-]headless cannot be combined with it (exit 81)',
389
- automaticBehavior: 'A port (9222), host:port or http://host:port is turned into the browser WebSocket URL via /json/version; a browser URL uses the first open tab. Refused with exit 90 when another running bdg session launched that Chrome or drives that tab (sessions of this BDG_SESSION_DIR, and of others that claimed a port)',
389
+ automaticBehavior: 'A port (9222), host:port or http://host:port is turned into the browser WebSocket URL via /json/version; a browser URL uses the first open tab. Refused with exit 90 when another running bdg session launched that Chrome or drives that tab (sessions of this BDG_SESSION_DIR, and of others that claimed a port). Downloads are not redirected: they go to the download folder of that Chrome (usually ~/Downloads), and only downloads of the session page are reported (not those of tabs or popups it opens)',
390
390
  },
391
391
  'stop:--kill-chrome': {
392
392
  default: 'Chrome launched by bdg is always closed on stop; an attached Chrome (--chrome-ws-url) is left running',
393
393
  whenEnabled: 'No additional effect; kept for compatibility',
394
394
  },
395
+ 'cdp:--send-anyway': {
396
+ default: 'A Domain.method 1 edit from a bundled method or domain (2 for names over 5 letters, case ignored) is taken for a typo: exit 81 with Did you mean',
397
+ whenEnabled: 'Sends such a name to Chrome as typed (for a method newer than the bundled protocol, e.g. next to a bundled sibling like getWindowBounds/setWindowBounds), with the not-in-bundled-protocol warning',
398
+ automaticBehavior: 'Blocked methods (Page.captureScreenshot, Page.close, Browser.close), type names and names that are not Domain.method are still refused; a well-formed name far from every bundled one is sent without the flag',
399
+ },
400
+ 'cdp:--describe': {
401
+ default: 'Without --describe, a Domain.method is called (one missing from the bundled protocol is sent as typed, with a warning: only bundled methods are matched case-insensitively)',
402
+ whenEnabled: 'Describes a domain, a method (parameters with ? for optional, returns, example) or a protocol type (Domain.Type: enum values or object properties)',
403
+ automaticBehavior: 'Parameters referring to an enum type list its values inline (JSON enum, ref, refType); a redirected method (DOM.highlightNode) also shows the method implementing it and its parameters, which Chrome checks (JSON redirect, resolved: true); a redirect to a method the protocol lacks (Page.deleteCookie → Network.deleteCookie) is shown as unresolved (resolved: false). Example values work as typed: width 1280, height 800, x/y 100, deviceScaleFactor/scale 1, timeout 5000, url https://example.com, other numbers 1 (never 0, which often means off)',
404
+ },
395
405
  'status:--verbose': {
396
406
  default: 'Basic session status (daemon running, session active, URL)',
397
407
  whenEnabled: 'Includes Chrome diagnostics and CDP connection details',
@@ -44,6 +44,11 @@ export interface CommandResult<T = unknown> {
44
44
  errorContext?: Record<string, unknown>;
45
45
  /** Optional hint message to display on stderr (for successful commands with guidance) */
46
46
  hint?: string;
47
+ /**
48
+ * Warning about how the command ran (success or failure): top-level
49
+ * `warning` in the JSON envelope, `Warning: …` on stderr otherwise
50
+ */
51
+ warning?: string;
47
52
  }
48
53
  /**
49
54
  * Handler function type.
@@ -80,6 +80,15 @@ function sessionEndedError() {
80
80
  const err = sessionEndedDuringCommandError();
81
81
  return new CommandError(err.message, { suggestion: err.suggestion }, EXIT_CODES.RESOURCE_NOT_FOUND);
82
82
  }
83
+ /**
84
+ * Print a command's warning on stderr (text output; `--json` has it in the envelope).
85
+ *
86
+ * @param warning - Warning, if any
87
+ */
88
+ function printWarning(warning) {
89
+ if (warning)
90
+ console.error(escapeControlChars(`Warning: ${warning}`));
91
+ }
83
92
  /**
84
93
  * Run a command with consistent error handling, output formatting, and exit codes.
85
94
  * Eliminates boilerplate try-catch and JSON output logic from command handlers.
@@ -114,6 +123,7 @@ export async function runCommand(handler, options, formatter) {
114
123
  if (options.json) {
115
124
  console.log(stringifyEnvelope(OutputBuilder.buildJsonError(result.error ?? 'Unknown error', {
116
125
  ...result.errorContext,
126
+ ...(result.warning && { warning: result.warning }),
117
127
  exitCode,
118
128
  })));
119
129
  }
@@ -126,14 +136,17 @@ export async function runCommand(handler, options, formatter) {
126
136
  }
127
137
  }
128
138
  }
139
+ printWarning(result.warning);
129
140
  }
130
141
  process.exit(exitCode);
131
142
  }
143
+ if (!options.json)
144
+ printWarning(result.warning);
132
145
  if (result.hint && !options.quiet) {
133
146
  console.error(escapeControlChars(result.hint));
134
147
  }
135
148
  if (options.json) {
136
- console.log(stringifyEnvelope(buildSuccessResponse(result.data)));
149
+ console.log(stringifyEnvelope(buildSuccessResponse(result.data, result.warning)));
137
150
  }
138
151
  else if (formatter) {
139
152
  const formattedOutput = formatter(result.data);
@@ -158,10 +171,12 @@ export async function runCommand(handler, options, formatter) {
158
171
  })));
159
172
  }
160
173
  else {
174
+ const { warning, ...metadata } = error.metadata;
161
175
  console.error(genericError(error.message));
162
- for (const value of Object.values(error.metadata)) {
163
- console.error(escapeControlChars(String(value)));
176
+ for (const value of Object.values(metadata)) {
177
+ console.error(escapeControlChars(typeof value === 'string' ? value : JSON.stringify(value)));
164
178
  }
179
+ printWarning(warning);
165
180
  }
166
181
  process.exit(error.exitCode);
167
182
  }
@@ -0,0 +1,40 @@
1
+ /**
2
+ * Ctrl-C and SIGTERM for a command that cancels its daemon request instead
3
+ * of dying mid-request, so it can still report (the `--json` envelope) and
4
+ * exit with the shell's code.
5
+ */
6
+ /** Signals that interrupt a command */
7
+ export type InterruptSignal = 'SIGINT' | 'SIGTERM';
8
+ /**
9
+ * The exit code of an interrupted command, as shells expect.
10
+ *
11
+ * @param signal - The signal
12
+ * @returns 130 for SIGINT, 143 for SIGTERM
13
+ */
14
+ export declare function interruptExitCode(signal: InterruptSignal): number;
15
+ /**
16
+ * The signal that aborted an interrupt, from its reason.
17
+ *
18
+ * @param interrupt - Aborted interrupt (reason: the signal name)
19
+ * @returns The signal, SIGINT unless it was SIGTERM
20
+ */
21
+ export declare function interruptSignal(interrupt: AbortSignal): InterruptSignal;
22
+ /**
23
+ * Abort on Ctrl-C or SIGTERM (reason: the signal name). A second signal
24
+ * exits at once.
25
+ *
26
+ * @returns Aborted on the first signal
27
+ */
28
+ export declare function abortOnInterrupt(): AbortSignal;
29
+ /**
30
+ * Wait for work unless interrupted first: the first signal ends the wait at
31
+ * once with the interrupted error (the work is left to the exiting process).
32
+ *
33
+ * @param work - Work to wait for
34
+ * @param interrupt - Aborted on Ctrl-C or SIGTERM (reason: the signal)
35
+ * @param interruptedError - The error for an interrupt by a signal
36
+ * @returns The work's value
37
+ * @throws The interrupted error once interrupted, else the work's error
38
+ */
39
+ export declare function unlessInterrupted<T>(work: Promise<T>, interrupt: AbortSignal, interruptedError: (signal: InterruptSignal) => Error): Promise<T>;
40
+ //# sourceMappingURL=interrupt.d.ts.map
@@ -0,0 +1,73 @@
1
+ /**
2
+ * Ctrl-C and SIGTERM for a command that cancels its daemon request instead
3
+ * of dying mid-request, so it can still report (the `--json` envelope) and
4
+ * exit with the shell's code.
5
+ */
6
+ import { EXIT_CODES } from '../../utils/exitCodes.js';
7
+ /**
8
+ * The exit code of an interrupted command, as shells expect.
9
+ *
10
+ * @param signal - The signal
11
+ * @returns 130 for SIGINT, 143 for SIGTERM
12
+ */
13
+ export function interruptExitCode(signal) {
14
+ return signal === 'SIGINT' ? EXIT_CODES.INTERRUPTED : EXIT_CODES.TERMINATED;
15
+ }
16
+ /**
17
+ * The signal that aborted an interrupt, from its reason.
18
+ *
19
+ * @param interrupt - Aborted interrupt (reason: the signal name)
20
+ * @returns The signal, SIGINT unless it was SIGTERM
21
+ */
22
+ export function interruptSignal(interrupt) {
23
+ return interrupt.reason === 'SIGTERM' ? 'SIGTERM' : 'SIGINT';
24
+ }
25
+ /**
26
+ * Abort on Ctrl-C or SIGTERM (reason: the signal name). A second signal
27
+ * exits at once.
28
+ *
29
+ * @returns Aborted on the first signal
30
+ */
31
+ export function abortOnInterrupt() {
32
+ const interrupt = new AbortController();
33
+ for (const signal of ['SIGINT', 'SIGTERM']) {
34
+ process.on(signal, () => {
35
+ if (interrupt.signal.aborted)
36
+ process.exit(interruptExitCode(signal));
37
+ interrupt.abort(signal);
38
+ });
39
+ }
40
+ return interrupt.signal;
41
+ }
42
+ /**
43
+ * Wait for work unless interrupted first: the first signal ends the wait at
44
+ * once with the interrupted error (the work is left to the exiting process).
45
+ *
46
+ * @param work - Work to wait for
47
+ * @param interrupt - Aborted on Ctrl-C or SIGTERM (reason: the signal)
48
+ * @param interruptedError - The error for an interrupt by a signal
49
+ * @returns The work's value
50
+ * @throws The interrupted error once interrupted, else the work's error
51
+ */
52
+ export async function unlessInterrupted(work, interrupt, interruptedError) {
53
+ const fail = () => interruptedError(interruptSignal(interrupt));
54
+ if (interrupt.aborted)
55
+ throw fail();
56
+ let onAbort = () => undefined;
57
+ const interrupted = new Promise((_, reject) => {
58
+ onAbort = () => reject(fail());
59
+ interrupt.addEventListener('abort', onAbort, { once: true });
60
+ });
61
+ try {
62
+ return await Promise.race([work, interrupted]);
63
+ }
64
+ catch (error) {
65
+ if (interrupt.aborted)
66
+ throw fail();
67
+ throw error;
68
+ }
69
+ finally {
70
+ interrupt.removeEventListener('abort', onAbort);
71
+ }
72
+ }
73
+ //# sourceMappingURL=interrupt.js.map
@@ -129,6 +129,8 @@ export interface CdpMethodOptions {
129
129
  describe?: boolean;
130
130
  /** Search for methods */
131
131
  search?: string;
132
+ /** Send a method that looks like a typo of a bundled one as typed */
133
+ sendAnyway?: boolean;
132
134
  }
133
135
  /** Options for stop command */
134
136
  export type StopCommandOptions = BaseOptions & ChromeOptions;
@@ -15,6 +15,8 @@ import type { TelemetryType } from '../../types.js';
15
15
  export type StartOutcome = {
16
16
  ok: true;
17
17
  data: StartSessionResponseData;
18
+ /** The daemon this attempt spawned (internal: stopped if the start is interrupted) */
19
+ spawned?: SpawnedDaemon;
18
20
  } | {
19
21
  ok: false;
20
22
  /** Message for `--json` (no "Error:" prefix) */
@@ -44,6 +46,8 @@ export interface StartDeps {
44
46
  launch: () => Promise<SpawnedDaemon | undefined>;
45
47
  /** Sends `start_session_request` */
46
48
  send: typeof sendStartSessionRequest;
49
+ /** Sends `stop_session_request` (stops a session whose start was interrupted) */
50
+ stop: () => Promise<unknown>;
47
51
  /** How long a failed start waits for the daemons it spawned to exit */
48
52
  exitWaitMs: number;
49
53
  }
@@ -53,6 +57,19 @@ export interface StartDeps {
53
57
  * Spawns the daemon if needed, sends `start_session_request`, then prints the
54
58
  * result as a JSON envelope (`--json`) or human-readable text, and exits.
55
59
  *
60
+ * Ctrl-C (or SIGTERM) cancels the start at any point until the outcome is
61
+ * printed: the connection is closed, so the daemon abandons the session (a
62
+ * daemon spawned but not yet asked is told to shut down, a session that has
63
+ * just started is stopped), and the command exits with 130 (143) once the
64
+ * daemon it spawned has exited (bounded, like a failed start), so a command
65
+ * run right after sees no session. A second signal exits at once.
66
+ *
67
+ * Race-free: {@link attemptStart} checks the interrupt last, synchronously,
68
+ * and only microtasks run between that check and the exit in
69
+ * {@link reportStartOutcome}; a signal handler runs as a macrotask, so a
70
+ * signal either lands before the check (the start is cancelled) or after the
71
+ * outcome has been printed.
72
+ *
56
73
  * @param url - Target URL to navigate to
57
74
  * @param options - Session configuration options
58
75
  * @param telemetry - Array of telemetry types to enable
@@ -63,13 +80,19 @@ export declare function startSessionViaDaemon(url: string, options: SessionStart
63
80
  * failure the daemon reported (or a dropped connection), it waits for every
64
81
  * daemon the attempts spawned to exit ({@link afterSpawnedDaemonExit}).
65
82
  *
83
+ * An interrupt cancels the start whenever it arrives: a start that succeeded
84
+ * meanwhile is stopped ({@link cancelStartedSession}), and a failure keeps its
85
+ * message but exits 130 (143), also when the signal came during the wait.
86
+ *
66
87
  * @param url - Target URL
67
88
  * @param options - Session options
68
89
  * @param telemetry - Telemetry types
69
90
  * @param deps - How to reach the daemon (tests replace it)
70
- * @returns Start outcome, ready to report
91
+ * @param interrupt - Aborted (reason: the signal) on Ctrl-C or SIGTERM: the
92
+ * start is cancelled and the daemon it spawned waited for like after a failure
93
+ * @returns Start outcome, ready to report (the interrupt is checked last)
71
94
  */
72
- export declare function attemptStart(url: string, options: SessionStartOptions, telemetry: TelemetryType[], deps?: StartDeps): Promise<StartOutcome>;
95
+ export declare function attemptStart(url: string, options: SessionStartOptions, telemetry: TelemetryType[], deps?: StartDeps, interrupt?: AbortSignal): Promise<StartOutcome>;
73
96
  /**
74
97
  * Let the daemons a failed start spawned finish exiting before the error is
75
98
  * reported: a daemon removes its session files on the way out, and a command
@@ -82,5 +105,5 @@ export declare function attemptStart(url: string, options: SessionStartOptions,
82
105
  * @param waitMs - Milliseconds to wait at most for all of them
83
106
  * @returns The failure (without internal fields), with a hint when a daemon did not exit in time
84
107
  */
85
- export declare function afterSpawnedDaemonExit(outcome: StartOutcome, spawned: SpawnedDaemon[], waitMs: number): Promise<StartOutcome>;
108
+ export declare function afterSpawnedDaemonExit(outcome: StartOutcome, spawned: Pick<SpawnedDaemon, 'pid' | 'hasExited'>[], waitMs: number): Promise<StartOutcome>;
86
109
  //# sourceMappingURL=startHelpers.d.ts.map