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
@@ -83,4 +83,11 @@ export declare function truncateUrl(url: string, maxLength?: number): string;
83
83
  export declare function cutMiddle(text: string, maxLength: number): string;
84
84
  export declare function pluralize(count: number, singular: string, plural?: string): string;
85
85
  export declare function formatDuration(ms: number): string;
86
+ /**
87
+ * Format a byte count for humans.
88
+ *
89
+ * @param bytes - Byte count
90
+ * @returns e.g. "512 B", "12.3 KB"
91
+ */
92
+ export declare function formatBytes(bytes: number): string;
86
93
  //# sourceMappingURL=formatting.d.ts.map
@@ -216,4 +216,17 @@ export function formatDuration(ms) {
216
216
  }
217
217
  return `${ms}ms`;
218
218
  }
219
+ /**
220
+ * Format a byte count for humans.
221
+ *
222
+ * @param bytes - Byte count
223
+ * @returns e.g. "512 B", "12.3 KB"
224
+ */
225
+ export function formatBytes(bytes) {
226
+ if (bytes < 1024)
227
+ return `${bytes} B`;
228
+ return bytes < 1024 * 1024
229
+ ? `${(bytes / 1024).toFixed(1)} KB`
230
+ : `${(bytes / 1024 / 1024).toFixed(1)} MB`;
231
+ }
219
232
  //# sourceMappingURL=formatting.js.map
@@ -29,7 +29,7 @@ export type LogLevel = 'info' | 'debug';
29
29
  * Log contexts for different components.
30
30
  * Used to prefix log messages with component name.
31
31
  */
32
- export type LogContext = 'bdg' | 'launcher' | 'daemon' | 'client' | 'cleanup' | 'session' | 'chrome' | 'cdp' | 'ipc' | 'http' | 'dialogs' | 'navigation' | 'page-crash' | 'console' | 'dom' | 'network' | 'diagnostics' | 'readiness' | 'atomic-file' | 'object-expander' | 'fetcher' | 'targets';
32
+ export type LogContext = 'bdg' | 'launcher' | 'daemon' | 'client' | 'cleanup' | 'session' | 'chrome' | 'cdp' | 'ipc' | 'http' | 'dialogs' | 'downloads' | 'navigation' | 'page-crash' | 'console' | 'dom' | 'network' | 'diagnostics' | 'readiness' | 'atomic-file' | 'object-expander' | 'fetcher' | 'targets';
33
33
  /**
34
34
  * Logger instance with support for different log levels.
35
35
  */
@@ -26,6 +26,8 @@ export declare function formatChromeNotice(notice: NoticeDetails<ChromeNoticeCod
26
26
  * @returns User-facing message
27
27
  */
28
28
  export declare function formatChromeIssue(issue: IssueDetails, diagnostics?: () => ChromeDiagnostics): string;
29
+ /** Chrome's launch ended because the session was stopped meanwhile */
30
+ export declare const CHROME_LAUNCH_ABORTED_MESSAGE = "Chrome launch aborted: the session was stopped";
29
31
  /**
30
32
  * Chrome exited before its debugging port opened.
31
33
  *
@@ -188,6 +190,17 @@ export declare function portTakenByReason(answeredBy: 'browser' | 'process' | 'n
188
190
  * @returns Reason for the CHROME_LAUNCH_FAILED issue
189
191
  */
190
192
  export declare function chromeNotAnsweringReason(port: number, waitedMs: number): string;
193
+ /**
194
+ * Chrome is running but did not open its debugging port within
195
+ * chrome-launcher's readiness budget (a slow start, not a crash or a port
196
+ * conflict: nothing answered on the port, which was free before the launch).
197
+ *
198
+ * @param pid - Chrome's PID
199
+ * @param port - Debugging port
200
+ * @param waitedMs - The readiness budget
201
+ * @returns Error message
202
+ */
203
+ export declare function chromePortNotOpenedMessage(pid: number, port: number, waitedMs: number): string;
191
204
  /**
192
205
  * Warning when bdg's Chrome preferences (password manager and leak check
193
206
  * off, etc.) could not be written into the profile.
@@ -62,6 +62,8 @@ export function formatChromeIssue(issue, diagnostics = getChromeDiagnostics) {
62
62
  return joinLines(header, '', ...diagnosticLines);
63
63
  return joinLines(header, '', 'Possible causes:', ` - Port ${port} conflict (check: lsof -ti:${port})`, ` - Chrome binary not found`, ` - Insufficient permissions`, ` - Chrome crashed on startup`, '', ...diagnosticLines, '', 'Try:', ` - ${sessionCommand('bdg cleanup')}`, ` - See what uses the port: lsof -i :${port}`, ` - Use different port: ${sessionCommand(`bdg <url> --port ${port + 1}`)}`, ` - In a container where Chrome's sandbox fails: BDG_NO_SANDBOX=1 ${sessionCommand('bdg <url>')}`);
64
64
  }
65
+ case 'CHROME_PORT_NOT_OPENED':
66
+ return joinLines(chromePortNotOpenedMessage(ctx['pid'], ctx['port'], ctx['waitedMs']), chromePortNotOpenedSuggestion());
65
67
  case 'CHROME_EXITED_DURING_STARTUP':
66
68
  return chromeExitedDuringStartupError(ctx['exitCode'], ctx['output'] ?? [], ctx['userDataDir'], ctx['profileInUse'] === true);
67
69
  case 'NO_PAGE_TARGET_FOUND':
@@ -86,6 +88,8 @@ export function formatChromeIssue(issue, diagnostics = getChromeDiagnostics) {
86
88
  return `Chrome preferences must be JSON-serializable: ${ctx['reason'] ?? 'unknown error'}`;
87
89
  }
88
90
  }
91
+ /** Chrome's launch ended because the session was stopped meanwhile */
92
+ export const CHROME_LAUNCH_ABORTED_MESSAGE = 'Chrome launch aborted: the session was stopped';
89
93
  /**
90
94
  * Chrome exited before its debugging port opened.
91
95
  *
@@ -345,6 +349,28 @@ export function portTakenByReason(answeredBy, chromeHost) {
345
349
  export function chromeNotAnsweringReason(port, waitedMs) {
346
350
  return `Chrome announced port ${port} but did not answer on 127.0.0.1 within ${(waitedMs / 1000).toFixed(1)}s (slow start)`;
347
351
  }
352
+ /**
353
+ * Chrome is running but did not open its debugging port within
354
+ * chrome-launcher's readiness budget (a slow start, not a crash or a port
355
+ * conflict: nothing answered on the port, which was free before the launch).
356
+ *
357
+ * @param pid - Chrome's PID
358
+ * @param port - Debugging port
359
+ * @param waitedMs - The readiness budget
360
+ * @returns Error message
361
+ */
362
+ export function chromePortNotOpenedMessage(pid, port, waitedMs) {
363
+ return `Chrome started (pid ${pid}) but did not open its debugging port ${port} within ${Number((waitedMs / 1000).toFixed(1))} s`;
364
+ }
365
+ /**
366
+ * What to do when Chrome started but did not open its debugging port in time.
367
+ *
368
+ * @returns Suggestion
369
+ */
370
+ function chromePortNotOpenedSuggestion() {
371
+ return (`Retry: ${sessionCommand('bdg <url>')} (a first start on a cold machine can be slow). ` +
372
+ `If it keeps failing: ${sessionCommand('bdg cleanup')} && ${sessionCommand('bdg <url>')}`);
373
+ }
348
374
  /**
349
375
  * Warning when bdg's Chrome preferences (password manager and leak check
350
376
  * off, etc.) could not be written into the profile.
@@ -5,7 +5,7 @@
5
5
  * cleaning up stale files, and validating command arguments.
6
6
  */
7
7
  import type { DomFrame, PageLoadingState, PendingRequestInfo } from '../../ipc/protocol/commands.js';
8
- import type { ElementLayout, FillValueMismatch, LayoutSize, NewMessage, PageLayout, PageNavigation, PendingChanges, ShownElement } from '../../ipc/protocol/domTypes.js';
8
+ import type { DownloadInfo, ElementLayout, FillValueMismatch, LayoutSize, NewMessage, PageLayout, PageNavigation, PendingChanges, ShownElement } from '../../ipc/protocol/domTypes.js';
9
9
  import type { InspectVisibility } from '../../ipc/protocol/inspectTypes.js';
10
10
  import type { DelegationNote } from '../../runtime/dom/listenerSummary.js';
11
11
  import type { WaitCondition, WaitSnapshot } from '../../runtime/dom/waitCondition.js';
@@ -40,6 +40,34 @@ export declare function skillBackupMessage(path: string): string;
40
40
  export declare function domClickFallbackWarning(reason: string | null | undefined): string;
41
41
  /** Reason of a `bdg dom form` blocker for a required field left empty */
42
42
  export declare const REQUIRED_FIELD_EMPTY_REASON = "Required field is empty";
43
+ /**
44
+ * Warning for a `bdg cdp` method the bundled protocol lacks, sent anyway.
45
+ *
46
+ * @param method - Method sent
47
+ * @param protocolVersion - Bundled devtools-protocol version
48
+ * @returns Warning text
49
+ */
50
+ export declare function cdpUnlistedMethodWarning(method: string, protocolVersion: string): string;
51
+ /**
52
+ * `bdg cdp --describe` line for a redirect to a method the protocol lacks
53
+ * (e.g. Page.deleteCookie names Network.deleteCookie).
54
+ *
55
+ * @param method - Redirect target
56
+ * @returns Line text
57
+ */
58
+ export declare function cdpUnresolvedRedirectLine(method: string): string;
59
+ /**
60
+ * `bdg cdp --describe` title of a redirect to a method the protocol has.
61
+ *
62
+ * @param method - Redirect target
63
+ * @returns Title text
64
+ */
65
+ export declare function cdpRedirectTitle(method: string): string;
66
+ /**
67
+ * Lines of `bdg cdp --help` saying how names are matched (each short
68
+ * enough not to be wrapped).
69
+ */
70
+ export declare const CDP_EXECUTION_HELP: string;
43
71
  /**
44
72
  * Part of the `bdg dom form` summary naming the required fields left empty.
45
73
  *
@@ -101,6 +129,38 @@ export declare function shownElementText(element: ShownElement): string;
101
129
  * "URL changed to https://example.com/#/active (same document)"
102
130
  */
103
131
  export declare function pageNavigationText(navigation: PageNavigation): string;
132
+ /**
133
+ * A download an action started, as its output line reads.
134
+ *
135
+ * @param download - Download
136
+ * @returns e.g. `Download: report.txt → /Users/me/.bdg/downloads/report.txt (completed, 15 B)`,
137
+ * `Download: big.zip → … (inProgress, 9.8 KB so far)` (no size before the first
138
+ * bytes), `Download: report.txt (canceled)`
139
+ */
140
+ export declare function downloadText(download: DownloadInfo): string;
141
+ /**
142
+ * The session's downloads in one line: how many, and the last one.
143
+ *
144
+ * @param downloads - Downloads, oldest first (at least one)
145
+ * @returns e.g. `2 (last: report.txt → /Users/me/.bdg/downloads/report.txt, completed)`
146
+ */
147
+ export declare function downloadsSummary(downloads: DownloadInfo[]): string;
148
+ /**
149
+ * Warning while Chrome did not take bdg's download behavior, so downloads
150
+ * are not saved in the session directory.
151
+ *
152
+ * @param detail - Chrome's error
153
+ * @returns Warning shown by `bdg status`
154
+ */
155
+ export declare function downloadsNotRedirectedWarning(detail: string): string;
156
+ /**
157
+ * Why downloads are refused when the session's downloads directory could not
158
+ * be created.
159
+ *
160
+ * @param detail - What went wrong, e.g. `/home/me/.bdg/downloads is a file`
161
+ * @returns Reason recorded on each refused download
162
+ */
163
+ export declare function downloadsDirUnavailableReason(detail: string): string;
104
164
  /**
105
165
  * Last `New text:` row when an action made more messages appear than are listed.
106
166
  *
@@ -748,9 +808,17 @@ export declare function sessionFilesCleanedMessage(): string;
748
808
  */
749
809
  export declare function sessionOutputRemovedMessage(): string;
750
810
  /**
751
- * Generate session directory clean message.
811
+ * Cleanup left the session's downloaded files in place.
752
812
  *
753
- * @returns Formatted success message
813
+ * @param dir - Downloads directory
814
+ * @param files - Files in it
815
+ * @returns e.g. `Downloads kept: 2 files in /Users/me/.bdg/downloads (delete them yourself when done)`
816
+ */
817
+ export declare function downloadsKeptMessage(dir: string, files: number): string;
818
+ /**
819
+ * Cleanup finished and left nothing behind.
820
+ *
821
+ * @returns Message
754
822
  */
755
823
  export declare function sessionDirectoryCleanMessage(): string;
756
824
  /**
@@ -5,7 +5,7 @@
5
5
  * cleaning up stale files, and validating command arguments.
6
6
  */
7
7
  import { buildAgentDiscoveryHelp, buildCommonTaskExamples, buildUrlExamples, buildSessionManagementReminder, } from '../formatters/helpFormatters.js';
8
- import { formatDuration, joinLines, pluralize, truncateUrl } from '../formatting.js';
8
+ import { formatBytes, formatDuration, joinLines, pluralize, truncateUrl } from '../formatting.js';
9
9
  import { sessionCommand } from './sessionCommand.js';
10
10
  import { truncateByLength } from '../../utils/strings.js';
11
11
  /**
@@ -60,6 +60,41 @@ function fieldLabelList(labels) {
60
60
  .join(', ');
61
61
  return labels.length > 5 ? `${shown} and ${labels.length - 5} more` : shown;
62
62
  }
63
+ /**
64
+ * Warning for a `bdg cdp` method the bundled protocol lacks, sent anyway.
65
+ *
66
+ * @param method - Method sent
67
+ * @param protocolVersion - Bundled devtools-protocol version
68
+ * @returns Warning text
69
+ */
70
+ export function cdpUnlistedMethodWarning(method, protocolVersion) {
71
+ return `${method} is not in the bundled protocol (devtools-protocol ${protocolVersion}); sending it to Chrome as is`;
72
+ }
73
+ /**
74
+ * `bdg cdp --describe` line for a redirect to a method the protocol lacks
75
+ * (e.g. Page.deleteCookie names Network.deleteCookie).
76
+ *
77
+ * @param method - Redirect target
78
+ * @returns Line text
79
+ */
80
+ export function cdpUnresolvedRedirectLine(method) {
81
+ return `Redirect target ${method} is not in the protocol (unresolved redirect)`;
82
+ }
83
+ /**
84
+ * `bdg cdp --describe` title of a redirect to a method the protocol has.
85
+ *
86
+ * @param method - Redirect target
87
+ * @returns Title text
88
+ */
89
+ export function cdpRedirectTitle(method) {
90
+ return `Implemented by ${method} (redirect)`;
91
+ }
92
+ /**
93
+ * Lines of `bdg cdp --help` saying how names are matched (each short
94
+ * enough not to be wrapped).
95
+ */
96
+ export const CDP_EXECUTION_HELP = ' Execution: bundled methods are case-insensitive (network.getcookies works)\n' +
97
+ ' Other methods are sent as typed (case-sensitive)';
63
98
  /**
64
99
  * Part of the `bdg dom form` summary naming the required fields left empty.
65
100
  *
@@ -145,6 +180,56 @@ export function pageNavigationText(navigation) {
145
180
  const status = navigation.status === undefined ? '' : ` (${navigation.status})`;
146
181
  return `navigated to ${navigation.url}${status}`;
147
182
  }
183
+ /**
184
+ * A download an action started, as its output line reads.
185
+ *
186
+ * @param download - Download
187
+ * @returns e.g. `Download: report.txt → /Users/me/.bdg/downloads/report.txt (completed, 15 B)`,
188
+ * `Download: big.zip → … (inProgress, 9.8 KB so far)` (no size before the first
189
+ * bytes), `Download: report.txt (canceled)`
190
+ */
191
+ export function downloadText(download) {
192
+ const where = download.path === undefined ? '' : ` → ${download.path}`;
193
+ const running = download.state === 'inProgress';
194
+ const bytes = download.bytes === undefined || (running && download.bytes === 0)
195
+ ? ''
196
+ : `, ${formatBytes(download.bytes)}${running ? ' so far' : ''}`;
197
+ const reason = download.reason === undefined ? '' : `: ${download.reason}`;
198
+ return `Download: ${download.suggestedFilename}${where} (${download.state}${bytes}${reason})`;
199
+ }
200
+ /**
201
+ * The session's downloads in one line: how many, and the last one.
202
+ *
203
+ * @param downloads - Downloads, oldest first (at least one)
204
+ * @returns e.g. `2 (last: report.txt → /Users/me/.bdg/downloads/report.txt, completed)`
205
+ */
206
+ export function downloadsSummary(downloads) {
207
+ const last = downloads[downloads.length - 1];
208
+ if (!last)
209
+ return '0';
210
+ const where = last.path === undefined ? '' : ` → ${last.path}`;
211
+ return `${downloads.length} (last: ${last.suggestedFilename}${where}, ${last.state})`;
212
+ }
213
+ /**
214
+ * Warning while Chrome did not take bdg's download behavior, so downloads
215
+ * are not saved in the session directory.
216
+ *
217
+ * @param detail - Chrome's error
218
+ * @returns Warning shown by `bdg status`
219
+ */
220
+ export function downloadsNotRedirectedWarning(detail) {
221
+ return `downloads are not redirected to the session directory (Chrome refused the download behavior: ${detail}); Chrome saves them in its default folder, usually ~/Downloads`;
222
+ }
223
+ /**
224
+ * Why downloads are refused when the session's downloads directory could not
225
+ * be created.
226
+ *
227
+ * @param detail - What went wrong, e.g. `/home/me/.bdg/downloads is a file`
228
+ * @returns Reason recorded on each refused download
229
+ */
230
+ export function downloadsDirUnavailableReason(detail) {
231
+ return `bdg's downloads directory could not be created (${detail}), so downloads are refused`;
232
+ }
148
233
  /**
149
234
  * Last `New text:` row when an action made more messages appear than are listed.
150
235
  *
@@ -1161,9 +1246,19 @@ export function sessionOutputRemovedMessage() {
1161
1246
  return 'Session output file removed';
1162
1247
  }
1163
1248
  /**
1164
- * Generate session directory clean message.
1249
+ * Cleanup left the session's downloaded files in place.
1165
1250
  *
1166
- * @returns Formatted success message
1251
+ * @param dir - Downloads directory
1252
+ * @param files - Files in it
1253
+ * @returns e.g. `Downloads kept: 2 files in /Users/me/.bdg/downloads (delete them yourself when done)`
1254
+ */
1255
+ export function downloadsKeptMessage(dir, files) {
1256
+ return `Downloads kept: ${pluralize(files, 'file')} in ${dir} (delete them yourself when done)`;
1257
+ }
1258
+ /**
1259
+ * Cleanup finished and left nothing behind.
1260
+ *
1261
+ * @returns Message
1167
1262
  */
1168
1263
  export function sessionDirectoryCleanMessage() {
1169
1264
  return 'Session directory is now clean';
@@ -4,23 +4,42 @@
4
4
  * User-facing messages for the network list command output and formatting.
5
5
  */
6
6
  /**
7
- * Why a stored response body was replaced by a placeholder (shown by
8
- * `bdg details network <id>` as `bodyNotCaptured`).
7
+ * Why a stored request or response body was replaced by a placeholder
8
+ * (shown by `bdg details network <id>` as `requestBodyNotCaptured` or
9
+ * `bodyNotCaptured`).
9
10
  *
10
11
  * @param budgetBytes - Total body budget of the session
11
- * @returns e.g. `evicted: total body budget (bdg keeps the newest 100 MB of response bodies)`
12
+ * @returns e.g. `evicted: total body budget (bdg keeps the newest 100 MB of request and response bodies)`
12
13
  */
13
14
  export declare function bodyEvictedReason(budgetBytes: number): string;
15
+ /**
16
+ * Why a response body is missing when Chrome no longer had it
17
+ * (`Network.getResponseBody` failed with "No resource with given identifier
18
+ * found" or "No data found for resource with given identifier"), shown by
19
+ * `bdg details network <id>` as `bodyNotCaptured` and in the HAR as the
20
+ * content comment.
21
+ *
22
+ * @returns Reason text
23
+ */
24
+ export declare function bodyGoneReason(): string;
25
+ /**
26
+ * Why a response body is missing when `Network.getResponseBody` failed for
27
+ * another reason (a CDP timeout, the connection closing mid-fetch).
28
+ *
29
+ * @param errorMessage - The error Chrome or the connection gave
30
+ * @returns e.g. `Chrome did not return the body: CDP command timeout`
31
+ */
32
+ export declare function bodyFetchFailedReason(errorMessage: string): string;
14
33
  /** What a session's network capture let go at its limits */
15
34
  export interface NetworkEvictionCounts {
16
35
  /** Oldest finished requests dropped at the request cap */
17
36
  requestsDropped: number;
18
- /** Oldest response bodies evicted at the body budget */
37
+ /** Oldest request and response bodies evicted at the body budget */
19
38
  bodiesEvicted: number;
20
39
  }
21
40
  /**
22
41
  * Note that the session dropped its oldest requests or evicted its oldest
23
- * response bodies at its limits.
42
+ * request and response bodies at its limits.
24
43
  *
25
44
  * @param counts - Requests dropped and bodies evicted
26
45
  * @returns e.g. `⚠ 2000 older network requests were dropped: bdg keeps the newest 10000`;
@@ -15,18 +15,41 @@ function megabytes(bytes) {
15
15
  return `${Math.round(bytes / (1024 * 1024))} MB`;
16
16
  }
17
17
  /**
18
- * Why a stored response body was replaced by a placeholder (shown by
19
- * `bdg details network <id>` as `bodyNotCaptured`).
18
+ * Why a stored request or response body was replaced by a placeholder
19
+ * (shown by `bdg details network <id>` as `requestBodyNotCaptured` or
20
+ * `bodyNotCaptured`).
20
21
  *
21
22
  * @param budgetBytes - Total body budget of the session
22
- * @returns e.g. `evicted: total body budget (bdg keeps the newest 100 MB of response bodies)`
23
+ * @returns e.g. `evicted: total body budget (bdg keeps the newest 100 MB of request and response bodies)`
23
24
  */
24
25
  export function bodyEvictedReason(budgetBytes) {
25
- return `evicted: total body budget (bdg keeps the newest ${megabytes(budgetBytes)} of response bodies)`;
26
+ return `evicted: total body budget (bdg keeps the newest ${megabytes(budgetBytes)} of request and response bodies)`;
27
+ }
28
+ /**
29
+ * Why a response body is missing when Chrome no longer had it
30
+ * (`Network.getResponseBody` failed with "No resource with given identifier
31
+ * found" or "No data found for resource with given identifier"), shown by
32
+ * `bdg details network <id>` as `bodyNotCaptured` and in the HAR as the
33
+ * content comment.
34
+ *
35
+ * @returns Reason text
36
+ */
37
+ export function bodyGoneReason() {
38
+ return 'Chrome no longer had the body (its network buffer evicted it, or the request was cancelled)';
39
+ }
40
+ /**
41
+ * Why a response body is missing when `Network.getResponseBody` failed for
42
+ * another reason (a CDP timeout, the connection closing mid-fetch).
43
+ *
44
+ * @param errorMessage - The error Chrome or the connection gave
45
+ * @returns e.g. `Chrome did not return the body: CDP command timeout`
46
+ */
47
+ export function bodyFetchFailedReason(errorMessage) {
48
+ return `Chrome did not return the body: ${errorMessage}`;
26
49
  }
27
50
  /**
28
51
  * Note that the session dropped its oldest requests or evicted its oldest
29
- * response bodies at its limits.
52
+ * request and response bodies at its limits.
30
53
  *
31
54
  * @param counts - Requests dropped and bodies evicted
32
55
  * @returns e.g. `⚠ 2000 older network requests were dropped: bdg keeps the newest 10000`;
@@ -35,7 +58,7 @@ export function bodyEvictedReason(budgetBytes) {
35
58
  export function networkEvictedNote(counts) {
36
59
  const { requestsDropped, bodiesEvicted } = counts;
37
60
  const requests = pluralize(requestsDropped, 'older network request');
38
- const bodies = pluralize(bodiesEvicted, 'older response body', 'older response bodies');
61
+ const bodies = pluralize(bodiesEvicted, 'older request/response body', 'older request/response bodies');
39
62
  const budget = megabytes(MAX_TOTAL_BODY_BYTES);
40
63
  if (requestsDropped > 0 && bodiesEvicted > 0) {
41
64
  return `⚠ ${requests} dropped, ${bodies} evicted: bdg keeps the newest ${MAX_NETWORK_REQUESTS} requests and ${budget} of bodies`;
@@ -88,7 +111,7 @@ export function localProxyNote() {
88
111
  * @returns Comment naming what was redacted and the flag that keeps it
89
112
  */
90
113
  export function harSanitizedComment() {
91
- return 'Sanitized by bdg: values of auth, cookie, API key, token and session headers, cookies, credential query parameters in URLs, and password/token fields of request bodies are [redacted] (by name, so some harmless values are too); response bodies and WebSocket messages are not sanitized. Export with --include-sensitive to keep everything';
114
+ return 'Sanitized by bdg: values of auth, cookie, API key, token and session headers, cookies, credential query parameters in URLs, password/token/secret fields of JSON and form request and response bodies and WebSocket messages (also truncated JSON, JSON encoded in strings, socket.io, SockJS, server-sent events, NDJSON, and base64 bodies and binary messages that are UTF-8 text), and any JWT (also inside longer strings and form values) are [redacted] in place (by name, so some harmless values are too); everything else stays byte for byte. Not sanitized: other binary data, text that is not JSON or a form, and non-JSON syntax (single quotes, unquoted keys, JSONP, bare values with spaces); a body that could not be sanitized is [redacted] whole. Export with --include-sensitive to keep everything';
92
115
  }
93
116
  /**
94
117
  * Success message of `bdg network har`.
@@ -99,7 +122,7 @@ export function harSanitizedComment() {
99
122
  export function harExportedMessage(result) {
100
123
  const filterNote = result.filtered ? ' (filtered)' : '';
101
124
  const note = result.sanitized
102
- ? 'Credentials sanitized (auth/cookie/API key/token headers, cookies, URL tokens, password and token body fields are [redacted]); --include-sensitive keeps them'
125
+ ? 'Credentials sanitized (auth/cookie/API key/token headers, cookies, URL tokens, password and token fields of bodies and WebSocket messages are [redacted]); --include-sensitive keeps them'
103
126
  : '⚠ Includes credentials (--include-sensitive): share this file with care';
104
127
  return `✓ Exported ${result.entries} requests${filterNote} to ${result.file}\n ${note}`;
105
128
  }
@@ -2,11 +2,12 @@
2
2
  * Async utilities for common patterns.
3
3
  */
4
4
  /**
5
- * Delay execution for a specified duration.
5
+ * Delay execution for a specified duration, ending early when `signal` aborts.
6
6
  *
7
7
  * @param ms - Milliseconds to delay
8
+ * @param signal - Optional abort signal (the delay resolves, not rejects, on abort)
8
9
  */
9
- export declare function delay(ms: number): Promise<void>;
10
+ export declare function delay(ms: number, signal?: AbortSignal): Promise<void>;
10
11
  /**
11
12
  * Wait for a promise, giving up after a time; the timer is cleared either way.
12
13
  *
@@ -2,12 +2,25 @@
2
2
  * Async utilities for common patterns.
3
3
  */
4
4
  /**
5
- * Delay execution for a specified duration.
5
+ * Delay execution for a specified duration, ending early when `signal` aborts.
6
6
  *
7
7
  * @param ms - Milliseconds to delay
8
+ * @param signal - Optional abort signal (the delay resolves, not rejects, on abort)
8
9
  */
9
- export function delay(ms) {
10
- return new Promise((resolve) => setTimeout(resolve, ms));
10
+ export function delay(ms, signal) {
11
+ return new Promise((resolve) => {
12
+ if (signal?.aborted) {
13
+ resolve();
14
+ return;
15
+ }
16
+ const done = () => {
17
+ clearTimeout(timer);
18
+ signal?.removeEventListener('abort', done);
19
+ resolve();
20
+ };
21
+ const timer = setTimeout(done, ms);
22
+ signal?.addEventListener('abort', done, { once: true });
23
+ });
11
24
  }
12
25
  /**
13
26
  * Wait for a promise, giving up after a time; the timer is cleared either way.
@@ -37,6 +37,13 @@ export interface FetchCDPTargetsOptions {
37
37
  * - Network errors are logged to help diagnose Chrome connectivity issues
38
38
  */
39
39
  export declare function fetchCDPTargets(port?: number, logger?: Logger, options?: FetchCDPTargetsOptions): Promise<CDPTarget[]>;
40
+ /**
41
+ * Options for a DevTools endpoint probe.
42
+ */
43
+ export interface DevToolsProbeOptions extends FetchCDPTargetsOptions {
44
+ /** Ends a pending request early (the probe then reports `unreachable`) */
45
+ signal?: AbortSignal | undefined;
46
+ }
40
47
  /**
41
48
  * What answers on a DevTools HTTP endpoint: a Chrome (with its browser-level
42
49
  * WebSocket URL), an HTTP server that is not DevTools, or nothing.
@@ -54,19 +61,19 @@ export type DevToolsProbe = {
54
61
  *
55
62
  * @param port - Chrome debugging port
56
63
  * @param logger - Optional logger for debug output
57
- * @param options - Host, HTTPS and request timeout
64
+ * @param options - Host, HTTPS, request timeout and abort signal
58
65
  * @returns What answered
59
66
  */
60
- export declare function probeDevToolsEndpoint(port: number, logger?: Logger, options?: FetchCDPTargetsOptions): Promise<DevToolsProbe>;
67
+ export declare function probeDevToolsEndpoint(port: number, logger?: Logger, options?: DevToolsProbeOptions): Promise<DevToolsProbe>;
61
68
  /**
62
69
  * The browser-level DevTools WebSocket URL of a Chrome (`/json/version`).
63
70
  *
64
71
  * @param port - Chrome debugging port
65
72
  * @param logger - Optional logger for debug output
66
- * @param options - Host and HTTPS
73
+ * @param options - Host, HTTPS and abort signal
67
74
  * @returns The URL, or null if no Chrome answered
68
75
  */
69
- export declare function fetchBrowserWsUrl(port: number, logger?: Logger, options?: Pick<FetchCDPTargetsOptions, 'host' | 'secure'>): Promise<string | null>;
76
+ export declare function fetchBrowserWsUrl(port: number, logger?: Logger, options?: Pick<DevToolsProbeOptions, 'host' | 'secure' | 'signal'>): Promise<string | null>;
70
77
  /**
71
78
  * Fetch specific CDP target by ID from Chrome's HTTP API.
72
79
  *
@@ -67,15 +67,17 @@ export async function fetchCDPTargets(port = DEFAULT_CDP_PORT, logger, options)
67
67
  *
68
68
  * @param port - Chrome debugging port
69
69
  * @param logger - Optional logger for debug output
70
- * @param options - Host, HTTPS and request timeout
70
+ * @param options - Host, HTTPS, request timeout and abort signal
71
71
  * @returns What answered
72
72
  */
73
73
  export async function probeDevToolsEndpoint(port, logger, options) {
74
74
  const url = `${options?.secure ? 'https' : 'http'}://${options?.host ?? HTTP_LOCALHOST}:${port}/json/version`;
75
75
  const timeoutMs = options?.timeoutMs ?? CDP_HTTP_TIMEOUT_MS;
76
+ const timeout = AbortSignal.timeout(timeoutMs);
77
+ const signal = options?.signal ? AbortSignal.any([timeout, options.signal]) : timeout;
76
78
  let response;
77
79
  try {
78
- response = await fetch(url, { signal: AbortSignal.timeout(timeoutMs) });
80
+ response = await fetch(url, { signal });
79
81
  }
80
82
  catch (error) {
81
83
  logger?.debug(`Chrome version request failed: ${getErrorMessage(error)} (${url})`);
@@ -97,7 +99,7 @@ export async function probeDevToolsEndpoint(port, logger, options) {
97
99
  *
98
100
  * @param port - Chrome debugging port
99
101
  * @param logger - Optional logger for debug output
100
- * @param options - Host and HTTPS
102
+ * @param options - Host, HTTPS and abort signal
101
103
  * @returns The URL, or null if no Chrome answered
102
104
  */
103
105
  export async function fetchBrowserWsUrl(port, logger, options) {
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "browser-debugger-cli",
3
- "version": "0.15.0",
4
- "description": "DevTools telemetry in your terminal. For humans and agents. Direct WebSocket to Chrome's debugging port.",
3
+ "version": "0.16.0",
4
+ "description": "Let Claude Code and other coding agents drive and debug Chrome from the shell. DOM, network, console and raw CDP as short commands, no screenshots and no MCP needed.",
5
5
  "type": "module",
6
6
  "bin": {
7
7
  "bdg": "dist/index.js"
@@ -37,10 +37,24 @@
37
37
  "keywords": [
38
38
  "cdp",
39
39
  "chrome-devtools-protocol",
40
+ "chrome",
41
+ "devtools",
42
+ "debugging",
40
43
  "browser",
41
- "telemetry",
44
+ "browser-automation",
45
+ "headless-chrome",
42
46
  "cli",
43
- "devtools"
47
+ "claude",
48
+ "claude-code",
49
+ "coding-agent",
50
+ "ai-agent",
51
+ "agent-skill",
52
+ "codex",
53
+ "css",
54
+ "figma",
55
+ "telemetry",
56
+ "puppeteer-alternative",
57
+ "playwright-alternative"
44
58
  ],
45
59
  "license": "MIT",
46
60
  "dependencies": {