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
package/dist/index.js CHANGED
@@ -54,6 +54,12 @@ var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: tru
54
54
  import { readFileSync } from "fs";
55
55
  import { createRequire } from "module";
56
56
  import { dirname, join } from "path";
57
+ function getBundledProtocolVersion() {
58
+ const packageJson = JSON.parse(
59
+ readFileSync(require2.resolve("devtools-protocol/package.json"), "utf-8")
60
+ );
61
+ return packageJson.version;
62
+ }
57
63
  function loadProtocol() {
58
64
  if (cachedProtocol) {
59
65
  return cachedProtocol;
@@ -83,6 +89,10 @@ function findCommand(domainName, commandName) {
83
89
  const normalized = commandName.toLowerCase();
84
90
  return domain.commands.find((c) => c.name.toLowerCase() === normalized);
85
91
  }
92
+ function findType(domainName, typeName) {
93
+ const normalized = typeName.toLowerCase();
94
+ return findDomain(domainName)?.types?.find((t) => t.id.toLowerCase() === normalized);
95
+ }
86
96
  function normalizeMethod(input) {
87
97
  const [domainName, commandName] = input.split(".");
88
98
  if (!domainName || !commandName) {
@@ -115,6 +125,7 @@ __export(schema_exports, {
115
125
  getDomainSummary: () => getDomainSummary,
116
126
  getMethodSchema: () => getMethodSchema,
117
127
  getProtocolCounts: () => getProtocolCounts,
128
+ getTypeSchema: () => getTypeSchema,
118
129
  searchMethods: () => searchMethods
119
130
  });
120
131
  function getMethodSchema(domainName, methodName) {
@@ -129,47 +140,86 @@ function getMethodSchema(domainName, methodName) {
129
140
  return buildMethodSchema(domain.domain, command);
130
141
  }
131
142
  function buildMethodSchema(domainName, command) {
132
- const parameters = command.parameters?.map(paramToSchema) ?? [];
143
+ const parameters = command.parameters?.map((p) => paramToSchema(domainName, p)) ?? [];
133
144
  const returns = command.returns?.map(returnToSchema) ?? [];
134
- const example = {
135
- command: `bdg cdp ${domainName}.${command.name}`
136
- };
137
- if (parameters.length > 0) {
138
- const exampleParams = {};
139
- parameters.forEach((p) => {
140
- if (!p.required) return;
141
- exampleParams[p.name] = getExampleValue(p);
142
- });
143
- if (Object.keys(exampleParams).length > 0) {
144
- example.params = exampleParams;
145
- example.command += ` --params '${JSON.stringify(exampleParams)}'`;
146
- }
147
- }
145
+ const redirect = buildRedirect(command);
146
+ const exampleSource = parameters.length > 0 ? parameters : redirect?.parameters ?? [];
148
147
  const schema = {
149
148
  name: `${domainName}.${command.name}`,
150
149
  domain: domainName,
151
150
  method: command.name,
152
151
  parameters,
153
152
  returns,
154
- example
153
+ example: buildExample(`${domainName}.${command.name}`, exampleSource)
155
154
  };
155
+ if (redirect) schema.redirect = redirect;
156
156
  if (command.description) schema.description = command.description;
157
157
  if (command.experimental) schema.experimental = command.experimental;
158
158
  if (command.deprecated) schema.deprecated = command.deprecated;
159
159
  return schema;
160
160
  }
161
- function paramToSchema(param) {
161
+ function buildRedirect(command) {
162
+ if (!command.redirect) return void 0;
163
+ const target = findCommand(command.redirect, command.name);
164
+ return {
165
+ method: `${command.redirect}.${command.name}`,
166
+ resolved: target !== void 0,
167
+ parameters: target?.parameters?.map((p) => paramToSchema(command.redirect ?? "", p)) ?? []
168
+ };
169
+ }
170
+ function buildExample(methodName, parameters) {
171
+ const example = { command: `bdg cdp ${methodName}` };
172
+ const required = parameters.filter((p) => p.required);
173
+ if (required.length === 0) return example;
174
+ const exampleParams = Object.fromEntries(required.map((p) => [p.name, getExampleValue(p)]));
175
+ example.params = exampleParams;
176
+ example.command += ` --params '${JSON.stringify(exampleParams)}'`;
177
+ return example;
178
+ }
179
+ function lookupRef(domainName, ref) {
180
+ const [refDomain, id] = ref.includes(".") ? ref.split(".") : [domainName, ref];
181
+ const type = refDomain && id ? findType(refDomain, id) : void 0;
182
+ return type && { name: `${refDomain}.${type.id}`, type };
183
+ }
184
+ function paramToSchema(domainName, param) {
162
185
  const schema = {
163
186
  name: param.name,
164
187
  type: resolveType(param),
165
188
  required: !param.optional
166
189
  };
190
+ const referenced = param.$ref ? lookupRef(domainName, param.$ref) : void 0;
191
+ const values = param.enum ?? referenced?.type.enum;
167
192
  if (param.description) schema.description = param.description;
168
193
  if (param.deprecated) schema.deprecated = param.deprecated;
169
- if (param.enum) schema.enum = param.enum;
194
+ if (param.experimental) schema.experimental = param.experimental;
195
+ if (values) schema.enum = values;
196
+ if (referenced) {
197
+ schema.ref = referenced.name;
198
+ schema.refType = referenced.type.type;
199
+ }
170
200
  if (param.items) schema.items = resolveType(param.items);
171
201
  return schema;
172
202
  }
203
+ function getTypeSchema(domainName, typeName) {
204
+ const domain = findDomain(domainName);
205
+ const type = domain && findType(domain.domain, typeName);
206
+ if (!domain || !type) return void 0;
207
+ const schema = {
208
+ name: `${domain.domain}.${type.id}`,
209
+ domain: domain.domain,
210
+ id: type.id,
211
+ baseType: type.type
212
+ };
213
+ if (type.description) schema.description = type.description;
214
+ if (type.experimental) schema.experimental = type.experimental;
215
+ if (type.deprecated) schema.deprecated = type.deprecated;
216
+ if (type.enum) schema.enum = type.enum;
217
+ if (type.items) schema.items = resolveType(type.items);
218
+ if (type.properties) {
219
+ schema.properties = type.properties.map((p) => paramToSchema(domain.domain, p));
220
+ }
221
+ return schema;
222
+ }
173
223
  function returnToSchema(ret) {
174
224
  const schema = {
175
225
  name: ret.name,
@@ -190,12 +240,16 @@ function getExampleValue(param) {
190
240
  if (param.enum && param.enum.length > 0) {
191
241
  return param.enum[0];
192
242
  }
193
- switch (param.type) {
243
+ const type = param.refType ?? param.type;
244
+ const named = Object.hasOwn(EXAMPLE_VALUES, param.name) ? EXAMPLE_VALUES[param.name] : void 0;
245
+ const namedType = typeof named === "number" ? ["integer", "number"] : ["string"];
246
+ if (named !== void 0 && namedType.includes(type)) return named;
247
+ switch (type) {
194
248
  case "string":
195
249
  return "example";
196
250
  case "integer":
197
251
  case "number":
198
- return 0;
252
+ return 1;
199
253
  case "boolean":
200
254
  return true;
201
255
  case "array":
@@ -265,11 +319,22 @@ function searchMethods(query) {
265
319
  });
266
320
  return [...named, ...described];
267
321
  }
268
- var SEARCH_KEYWORDS;
322
+ var EXAMPLE_VALUES, SEARCH_KEYWORDS;
269
323
  var init_schema = __esm({
270
324
  "src/cdp/schema.ts"() {
271
325
  "use strict";
272
326
  init_protocol();
327
+ EXAMPLE_VALUES = {
328
+ width: 1280,
329
+ height: 800,
330
+ x: 100,
331
+ y: 100,
332
+ deviceScaleFactor: 1,
333
+ scale: 1,
334
+ timeout: 5e3,
335
+ responseCode: 200,
336
+ url: "https://example.com"
337
+ };
273
338
  SEARCH_KEYWORDS = {
274
339
  "Emulation.setDeviceMetricsOverride": [
275
340
  "viewport",
@@ -292,38 +357,6 @@ var init_schema = __esm({
292
357
  }
293
358
  });
294
359
 
295
- // src/commands/dom/screenshotResize.ts
296
- function calculateImageTokens(width, height) {
297
- return Math.ceil(width * height / PIXELS_PER_TOKEN);
298
- }
299
- function calculateResizeScale(width, height) {
300
- const longestEdge = Math.max(width, height);
301
- if (longestEdge <= MAX_EDGE_PX) {
302
- return 1;
303
- }
304
- return MAX_EDGE_PX / longestEdge;
305
- }
306
- function isTallPage(width, height) {
307
- if (width === 0) return false;
308
- return height / width > TALL_PAGE_THRESHOLD;
309
- }
310
- function shouldResize(width, height, noResize) {
311
- if (noResize) {
312
- return false;
313
- }
314
- const longestEdge = Math.max(width, height);
315
- return longestEdge > MAX_EDGE_PX;
316
- }
317
- var MAX_EDGE_PX, PIXELS_PER_TOKEN, TALL_PAGE_THRESHOLD;
318
- var init_screenshotResize = __esm({
319
- "src/commands/dom/screenshotResize.ts"() {
320
- "use strict";
321
- MAX_EDGE_PX = 1568;
322
- PIXELS_PER_TOKEN = 750;
323
- TALL_PAGE_THRESHOLD = 3;
324
- }
325
- });
326
-
327
360
  // node_modules/escape-string-regexp/index.js
328
361
  var require_escape_string_regexp = __commonJS({
329
362
  "node_modules/escape-string-regexp/index.js"(exports, module) {
@@ -960,7 +993,7 @@ var require_node = __commonJS({
960
993
  var tty2 = __require("tty");
961
994
  var util = __require("util");
962
995
  exports.init = init;
963
- exports.log = log36;
996
+ exports.log = log33;
964
997
  exports.formatArgs = formatArgs;
965
998
  exports.save = save;
966
999
  exports.load = load;
@@ -1095,7 +1128,7 @@ var require_node = __commonJS({
1095
1128
  }
1096
1129
  return (/* @__PURE__ */ new Date()).toISOString() + " ";
1097
1130
  }
1098
- function log36(...args) {
1131
+ function log33(...args) {
1099
1132
  return process.stderr.write(util.formatWithOptions(exports.inspectOpts, ...args) + "\n");
1100
1133
  }
1101
1134
  function save(namespaces) {
@@ -1422,8 +1455,8 @@ function createLogger(context) {
1422
1455
  logger.debug = (message) => logMessage(message, "debug");
1423
1456
  return logger;
1424
1457
  }
1425
- function logDebugError(log36, context, error) {
1426
- log36.debug(`Failed to ${context}: ${getErrorMessage(error)}`);
1458
+ function logDebugError(log33, context, error) {
1459
+ log33.debug(`Failed to ${context}: ${getErrorMessage(error)}`);
1427
1460
  }
1428
1461
  var debugEnabled;
1429
1462
  var init_logger = __esm({
@@ -1569,6 +1602,9 @@ function getDaemonSocketPath() {
1569
1602
  function ensureSessionDir() {
1570
1603
  makeDirectory(getSessionDir(), PRIVATE_DIR_MODE);
1571
1604
  }
1605
+ function getSessionDownloadsDir() {
1606
+ return path3.join(getSessionDir(), DOWNLOADS_DIR);
1607
+ }
1572
1608
  function sessionDirChain(dir) {
1573
1609
  const base = getSessionBaseDir();
1574
1610
  const relative2 = path3.relative(base, dir);
@@ -1635,7 +1671,7 @@ function secureSessionDir(dir = getSessionDir()) {
1635
1671
  }
1636
1672
  return null;
1637
1673
  }
1638
- var log, SESSION_FILES, SESSION_DIR_OVERRIDE_ENV, SESSION_NAME_ENV, NAMED_SESSIONS_DIR, MAX_SOCKET_PATH_BYTES, MAX_DAEMON_SOCKET_PATH_BYTES, SESSION_STATE_FILES, PRIVATE_DIR_MODE, GROUP_OTHER_ACCESS, SESSION_DIR_TRUST;
1674
+ var log, SESSION_FILES, SESSION_DIR_OVERRIDE_ENV, SESSION_NAME_ENV, NAMED_SESSIONS_DIR, MAX_SOCKET_PATH_BYTES, MAX_DAEMON_SOCKET_PATH_BYTES, SESSION_STATE_FILES, PRIVATE_DIR_MODE, GROUP_OTHER_ACCESS, DOWNLOADS_DIR, SESSION_DIR_TRUST;
1639
1675
  var init_paths = __esm({
1640
1676
  "src/session/paths.ts"() {
1641
1677
  "use strict";
@@ -1665,6 +1701,7 @@ var init_paths = __esm({
1665
1701
  ];
1666
1702
  PRIVATE_DIR_MODE = 448;
1667
1703
  GROUP_OTHER_ACCESS = 63;
1704
+ DOWNLOADS_DIR = "downloads";
1668
1705
  SESSION_DIR_TRUST = { allowGroupWrite: true };
1669
1706
  }
1670
1707
  });
@@ -1800,48 +1837,6 @@ var init_atomicFile = __esm({
1800
1837
  }
1801
1838
  });
1802
1839
 
1803
- // src/session/metadata.ts
1804
- import * as fs8 from "fs";
1805
- function handleCorruptedMetadata(metaPath, error, options) {
1806
- if (options?.warnOnCorruption) {
1807
- log6.info(`Session metadata corrupted (cannot read details): ${getErrorMessage(error)}`);
1808
- log6.info(`Troubleshooting: Run "${sessionCommand("bdg cleanup")}" to remove corrupted files`);
1809
- }
1810
- if (options?.selfHealOnCorruption) {
1811
- try {
1812
- fs8.rmSync(metaPath, { force: true });
1813
- log6.info("Removed corrupted session metadata file");
1814
- } catch (deleteError) {
1815
- log6.debug(`Failed to remove corrupted metadata: ${getErrorMessage(deleteError)}`);
1816
- }
1817
- }
1818
- }
1819
- function readSessionMetadata(options) {
1820
- const metaPath = getSessionFilePath("METADATA");
1821
- if (!fs8.existsSync(metaPath)) {
1822
- return null;
1823
- }
1824
- try {
1825
- const content = fs8.readFileSync(metaPath, "utf-8");
1826
- return JSON.parse(content);
1827
- } catch (error) {
1828
- handleCorruptedMetadata(metaPath, error, options);
1829
- return null;
1830
- }
1831
- }
1832
- var log6;
1833
- var init_metadata = __esm({
1834
- "src/session/metadata.ts"() {
1835
- "use strict";
1836
- init_logging();
1837
- init_sessionCommand();
1838
- init_atomicFile();
1839
- init_errors();
1840
- init_paths();
1841
- log6 = createLogger("session");
1842
- }
1843
- });
1844
-
1845
1840
  // src/utils/validation.ts
1846
1841
  function valid() {
1847
1842
  return { valid: true };
@@ -2138,6 +2133,10 @@ function formatDuration(ms) {
2138
2133
  }
2139
2134
  return `${ms}ms`;
2140
2135
  }
2136
+ function formatBytes(bytes) {
2137
+ if (bytes < 1024) return `${bytes} B`;
2138
+ return bytes < 1024 * 1024 ? `${(bytes / 1024).toFixed(1)} KB` : `${(bytes / 1024 / 1024).toFixed(1)} MB`;
2139
+ }
2141
2140
  var hintsHidden, OutputFormatter, MIN_URL_END_SHOWN, WEB_PROTOCOLS;
2142
2141
  var init_formatting = __esm({
2143
2142
  "src/ui/formatting.ts"() {
@@ -2296,6 +2295,15 @@ function fieldLabelList(labels) {
2296
2295
  const shown = labels.slice(0, 5).map((label) => label.replace(/\s*:\s*$/, "")).join(", ");
2297
2296
  return labels.length > 5 ? `${shown} and ${labels.length - 5} more` : shown;
2298
2297
  }
2298
+ function cdpUnlistedMethodWarning(method, protocolVersion) {
2299
+ return `${method} is not in the bundled protocol (devtools-protocol ${protocolVersion}); sending it to Chrome as is`;
2300
+ }
2301
+ function cdpUnresolvedRedirectLine(method) {
2302
+ return `Redirect target ${method} is not in the protocol (unresolved redirect)`;
2303
+ }
2304
+ function cdpRedirectTitle(method) {
2305
+ return `Implemented by ${method} (redirect)`;
2306
+ }
2299
2307
  function requiredFieldsEmptyMessage(labels) {
2300
2308
  const fields = labels.length === 1 ? "field" : "fields";
2301
2309
  return `${labels.length} required ${fields} empty: ${fieldLabelList(labels)}`;
@@ -2332,6 +2340,19 @@ function pageNavigationText(navigation) {
2332
2340
  const status = navigation.status === void 0 ? "" : ` (${navigation.status})`;
2333
2341
  return `navigated to ${navigation.url}${status}`;
2334
2342
  }
2343
+ function downloadText(download) {
2344
+ const where = download.path === void 0 ? "" : ` \u2192 ${download.path}`;
2345
+ const running = download.state === "inProgress";
2346
+ const bytes = download.bytes === void 0 || running && download.bytes === 0 ? "" : `, ${formatBytes(download.bytes)}${running ? " so far" : ""}`;
2347
+ const reason = download.reason === void 0 ? "" : `: ${download.reason}`;
2348
+ return `Download: ${download.suggestedFilename}${where} (${download.state}${bytes}${reason})`;
2349
+ }
2350
+ function downloadsSummary(downloads) {
2351
+ const last = downloads[downloads.length - 1];
2352
+ if (!last) return "0";
2353
+ const where = last.path === void 0 ? "" : ` \u2192 ${last.path}`;
2354
+ return `${downloads.length} (last: ${last.suggestedFilename}${where}, ${last.state})`;
2355
+ }
2335
2356
  function moreMessagesText(count) {
2336
2357
  return `(+${count} more)`;
2337
2358
  }
@@ -2632,6 +2653,9 @@ function sessionFilesCleanedMessage() {
2632
2653
  function sessionOutputRemovedMessage() {
2633
2654
  return "Session output file removed";
2634
2655
  }
2656
+ function downloadsKeptMessage(dir, files) {
2657
+ return `Downloads kept: ${pluralize(files, "file")} in ${dir} (delete them yourself when done)`;
2658
+ }
2635
2659
  function sessionDirectoryCleanMessage() {
2636
2660
  return "Session directory is now clean";
2637
2661
  }
@@ -2695,7 +2719,7 @@ function helpJsonDetailsNote() {
2695
2719
  function moreCharsNote(count) {
2696
2720
  return `\u2026 ${count} more chars (use --full)`;
2697
2721
  }
2698
- var REQUIRED_FIELD_EMPTY_REASON, NO_VISIBLE_EFFECT, HOVER_OFF_DONE, HOVER_USAGE, LAYOUT_REASONS, SCROLL_LOCKED_PREFIX, VIEWPORT_POSITION_HINTS, INSPECT_OUTPUT_LEGEND, COLLAPSED_LISTENERS_HEADING, NO_LISTENERS_HINT, NOTE_TYPES_SHOWN, POINTER_ACTION_DONE, POINTER_ACTION_NOUN, PAGE_ACTION_DONE, PAGE_INFO_DESCRIPTION, PAGE_EMULATE_DESCRIPTION, PAGE_ACTION_DESCRIPTIONS, CLICK_RESULT_WAIT_HELP, WAIT_HELP_EXAMPLES, FRAME_URL_MAX_LENGTH, AUDIT_HELP_EXAMPLES, CSS_SEARCH_HELP_EXAMPLES;
2722
+ var REQUIRED_FIELD_EMPTY_REASON, CDP_EXECUTION_HELP, NO_VISIBLE_EFFECT, HOVER_OFF_DONE, HOVER_USAGE, LAYOUT_REASONS, SCROLL_LOCKED_PREFIX, VIEWPORT_POSITION_HINTS, INSPECT_OUTPUT_LEGEND, COLLAPSED_LISTENERS_HEADING, NO_LISTENERS_HINT, NOTE_TYPES_SHOWN, POINTER_ACTION_DONE, POINTER_ACTION_NOUN, PAGE_ACTION_DONE, PAGE_INFO_DESCRIPTION, PAGE_EMULATE_DESCRIPTION, PAGE_ACTION_DESCRIPTIONS, CLICK_RESULT_WAIT_HELP, WAIT_HELP_EXAMPLES, FRAME_URL_MAX_LENGTH, AUDIT_HELP_EXAMPLES, CSS_SEARCH_HELP_EXAMPLES;
2699
2723
  var init_commands = __esm({
2700
2724
  "src/ui/messages/commands.ts"() {
2701
2725
  "use strict";
@@ -2704,6 +2728,7 @@ var init_commands = __esm({
2704
2728
  init_sessionCommand();
2705
2729
  init_strings();
2706
2730
  REQUIRED_FIELD_EMPTY_REASON = "Required field is empty";
2731
+ CDP_EXECUTION_HELP = " Execution: bundled methods are case-insensitive (network.getcookies works)\n Other methods are sent as typed (case-sensitive)";
2707
2732
  NO_VISIBLE_EFFECT = "no visible effect observed: no DOM change, requests or navigation within 300 ms";
2708
2733
  HOVER_OFF_DONE = "\u2713 Mouse moved off the page (hover styles and menus that close on mouseleave are gone)";
2709
2734
  HOVER_USAGE = "bdg dom hover <selector|index>, or bdg dom hover --off to move the mouse away";
@@ -3008,7 +3033,7 @@ var init_CommandError = __esm({
3008
3033
  });
3009
3034
 
3010
3035
  // src/ipc/transport/IPCError.ts
3011
- var IPCError, IPCConnectionError, IPCTimeoutError, IPCParseError, IPCEarlyCloseError;
3036
+ var IPCError, IPCConnectionError, IPCTimeoutError, IPCParseError, IPCEarlyCloseError, IPCCancelledError;
3012
3037
  var init_IPCError = __esm({
3013
3038
  "src/ipc/transport/IPCError.ts"() {
3014
3039
  "use strict";
@@ -3066,6 +3091,14 @@ var init_IPCError = __esm({
3066
3091
  this.requestName = requestName;
3067
3092
  }
3068
3093
  };
3094
+ IPCCancelledError = class extends IPCError {
3095
+ name = "IPCCancelledError";
3096
+ requestName;
3097
+ constructor(requestName) {
3098
+ super(`${requestName} request cancelled`, EXIT_CODES.SOFTWARE_ERROR);
3099
+ this.requestName = requestName;
3100
+ }
3101
+ };
3069
3102
  }
3070
3103
  });
3071
3104
 
@@ -3268,6 +3301,12 @@ function commandTimedOutError(seconds) {
3268
3301
  suggestion: `Check the session with: ${sessionCommand("bdg status")}; if the page stays frozen, end it with: ${sessionCommand("bdg cleanup --force")}`
3269
3302
  };
3270
3303
  }
3304
+ function screenshotInterruptedError(signal) {
3305
+ return {
3306
+ message: `Screenshot cancelled (${signal === "SIGINT" ? "interrupted" : "terminated"})`,
3307
+ suggestion: "No file was written; run it again to capture (the next command sees the page's own emulation)"
3308
+ };
3309
+ }
3271
3310
  function sessionUnavailableSuggestion(exitCode) {
3272
3311
  return exitCode === 85 ? `Wait until "${sessionCommand("bdg <url>")}" returns, then retry` : startSessionSuggestion();
3273
3312
  }
@@ -3711,18 +3750,6 @@ function waitTargetRequiredError() {
3711
3750
  suggestion: `e.g. ${sessionCommand("bdg dom wait '#result' --visible")}, ${sessionCommand("bdg dom wait body --text 'Welcome'")} or ${sessionCommand("bdg dom wait --load")}`
3712
3751
  };
3713
3752
  }
3714
- function elementNotVisibleError() {
3715
- return {
3716
- message: "Element is not rendered (e.g. display: none), so it has no area to capture",
3717
- suggestion: "Make it visible first, or capture a visible ancestor"
3718
- };
3719
- }
3720
- function elementZeroDimensionsError() {
3721
- return {
3722
- message: "Element has zero width or height, so there is nothing to capture",
3723
- suggestion: "It may be collapsed or empty; capture a visible ancestor instead"
3724
- };
3725
- }
3726
3753
  function hoverOffWithTargetError() {
3727
3754
  return {
3728
3755
  message: "--off moves the mouse off the page; it takes no element",
@@ -3845,6 +3872,34 @@ function formInIframeError(iframeUrl, crossOrigin) {
3845
3872
  suggestion: crossOrigin ? "Cross-origin iframes cannot be read or controlled; open the iframe URL directly: bdg <iframe url>" : 'dom form lists forms of the main document only; its fields are reachable directly: bdg dom query "input, select, textarea", then bdg dom fill <index> <value>'
3846
3873
  };
3847
3874
  }
3875
+ function cdpTypeNotMethodError(name) {
3876
+ return {
3877
+ message: `${name} is a protocol type, not a method`,
3878
+ suggestion: `Use: bdg cdp ${name} --describe (to see its values or properties)`
3879
+ };
3880
+ }
3881
+ function cdpMethodNotFoundError(input, similar, listHint) {
3882
+ const didYouMean = similar.length > 0 ? ["", "Did you mean:", ...similar.map((name) => ` - ${name}`)] : [];
3883
+ return {
3884
+ message: `Method '${input}' not found`,
3885
+ suggestion: [listHint, ...didYouMean].join("\n")
3886
+ };
3887
+ }
3888
+ function cdpMethodTypoError(input, similar, listHint) {
3889
+ const err = cdpMethodNotFoundError(input, similar, listHint);
3890
+ return {
3891
+ message: err.message,
3892
+ suggestion: `${err.suggestion}
3893
+
3894
+ To send it as typed (a method newer than bdg's protocol): ${sessionCommand(`bdg cdp ${input} --send-anyway`)}`
3895
+ };
3896
+ }
3897
+ function cdpMethodNotInBundledProtocolError(method, protocolVersion) {
3898
+ return {
3899
+ message: `Method '${method}' is not in the bundled protocol (devtools-protocol ${protocolVersion})`,
3900
+ suggestion: `bdg cdp ${method} sends it to Chrome as is, which may still have it. Methods bdg knows: bdg cdp --search <keyword>`
3901
+ };
3902
+ }
3848
3903
  function skillSourceMissingError(source) {
3849
3904
  return {
3850
3905
  message: `The bdg skill is missing from the installed package (${source})`,
@@ -4021,6 +4076,47 @@ var init_suggestions = __esm({
4021
4076
  }
4022
4077
  });
4023
4078
 
4079
+ // src/commands/shared/interrupt.ts
4080
+ function interruptExitCode(signal) {
4081
+ return signal === "SIGINT" ? EXIT_CODES.INTERRUPTED : EXIT_CODES.TERMINATED;
4082
+ }
4083
+ function interruptSignal(interrupt) {
4084
+ return interrupt.reason === "SIGTERM" ? "SIGTERM" : "SIGINT";
4085
+ }
4086
+ function abortOnInterrupt() {
4087
+ const interrupt = new AbortController();
4088
+ for (const signal of ["SIGINT", "SIGTERM"]) {
4089
+ process.on(signal, () => {
4090
+ if (interrupt.signal.aborted) process.exit(interruptExitCode(signal));
4091
+ interrupt.abort(signal);
4092
+ });
4093
+ }
4094
+ return interrupt.signal;
4095
+ }
4096
+ async function unlessInterrupted(work, interrupt, interruptedError) {
4097
+ const fail = () => interruptedError(interruptSignal(interrupt));
4098
+ if (interrupt.aborted) throw fail();
4099
+ let onAbort = () => void 0;
4100
+ const interrupted = new Promise((_, reject2) => {
4101
+ onAbort = () => reject2(fail());
4102
+ interrupt.addEventListener("abort", onAbort, { once: true });
4103
+ });
4104
+ try {
4105
+ return await Promise.race([work, interrupted]);
4106
+ } catch (error) {
4107
+ if (interrupt.aborted) throw fail();
4108
+ throw error;
4109
+ } finally {
4110
+ interrupt.removeEventListener("abort", onAbort);
4111
+ }
4112
+ }
4113
+ var init_interrupt = __esm({
4114
+ "src/commands/shared/interrupt.ts"() {
4115
+ "use strict";
4116
+ init_exitCodes();
4117
+ }
4118
+ });
4119
+
4024
4120
  // src/ipc/transport/errors.ts
4025
4121
  function formatConnectionError(requestName, socketPath, error) {
4026
4122
  const code = error.code;
@@ -4042,6 +4138,9 @@ function formatTimeoutError(requestName, timeoutMs) {
4042
4138
  function formatEarlyCloseError(requestName) {
4043
4139
  return new IPCEarlyCloseError(requestName);
4044
4140
  }
4141
+ function formatCancelledError(requestName) {
4142
+ return new IPCCancelledError(requestName);
4143
+ }
4045
4144
  var init_errors4 = __esm({
4046
4145
  "src/ipc/transport/errors.ts"() {
4047
4146
  "use strict";
@@ -4211,7 +4310,7 @@ var init_validation2 = __esm({
4211
4310
 
4212
4311
  // src/ipc/transport/index.ts
4213
4312
  import * as path5 from "path";
4214
- async function sendRequest(request, requestName, expectedType, timeoutMs = getIPCRequestTimeout(), socketPath = getDaemonSocketPath()) {
4313
+ async function sendRequest(request, requestName, expectedType, timeoutMs = getIPCRequestTimeout(), socketPath = getDaemonSocketPath(), signal) {
4215
4314
  const untrusted = secureSessionDir(path5.dirname(socketPath));
4216
4315
  if (untrusted) {
4217
4316
  const err = untrustedSessionDirError(untrusted);
@@ -4222,11 +4321,18 @@ async function sendRequest(request, requestName, expectedType, timeoutMs = getIP
4222
4321
  );
4223
4322
  }
4224
4323
  return new Promise((resolve4, reject2) => {
4324
+ if (signal?.aborted) {
4325
+ reject2(formatCancelledError(requestName));
4326
+ return;
4327
+ }
4225
4328
  const buffer = new JSONLBuffer();
4226
4329
  let resolved = false;
4330
+ let onAbort = () => {
4331
+ };
4227
4332
  const resolveOnce = (cleanup2, error, response) => {
4228
4333
  if (resolved) return;
4229
4334
  resolved = true;
4335
+ signal?.removeEventListener("abort", onAbort);
4230
4336
  cleanup2();
4231
4337
  if (error) {
4232
4338
  reject2(error);
@@ -4272,6 +4378,8 @@ async function sendRequest(request, requestName, expectedType, timeoutMs = getIP
4272
4378
  }
4273
4379
  }
4274
4380
  );
4381
+ onAbort = () => resolveOnce(cleanup, formatCancelledError(requestName));
4382
+ signal?.addEventListener("abort", onAbort, { once: true });
4275
4383
  });
4276
4384
  }
4277
4385
  var log10;
@@ -4379,7 +4487,7 @@ async function getHARData() {
4379
4487
  await assertResponsive();
4380
4488
  return sendRequest(request, "HAR data", "har_data_response");
4381
4489
  }
4382
- async function startSession(url, options) {
4490
+ async function startSession(url, options, signal) {
4383
4491
  const request = withSession({
4384
4492
  type: "start_session_request",
4385
4493
  url,
@@ -4401,7 +4509,10 @@ async function startSession(url, options) {
4401
4509
  return sendRequest(
4402
4510
  request,
4403
4511
  "start session",
4404
- "start_session_response"
4512
+ "start_session_response",
4513
+ void 0,
4514
+ void 0,
4515
+ signal
4405
4516
  );
4406
4517
  }
4407
4518
  async function stopSession() {
@@ -4413,7 +4524,7 @@ async function stopSession() {
4413
4524
  "stop_session_response"
4414
4525
  );
4415
4526
  }
4416
- async function sendCommand(commandName, params, timeoutMs) {
4527
+ async function sendCommand(commandName, params, timeoutMs, signal) {
4417
4528
  const request = {
4418
4529
  ...params,
4419
4530
  type: `${commandName}_request`,
@@ -4424,7 +4535,9 @@ async function sendCommand(commandName, params, timeoutMs) {
4424
4535
  request,
4425
4536
  commandName,
4426
4537
  `${commandName}_response`,
4427
- timeoutMs
4538
+ timeoutMs,
4539
+ void 0,
4540
+ signal
4428
4541
  );
4429
4542
  }
4430
4543
  async function getDetails(type, id) {
@@ -4508,6 +4621,9 @@ async function cssSearch(params) {
4508
4621
  async function domInspect(params) {
4509
4622
  return sendCommand("dom_inspect", params);
4510
4623
  }
4624
+ async function domScreenshot(params, signal) {
4625
+ return sendCommand("dom_screenshot", params, void 0, signal);
4626
+ }
4511
4627
  async function domWait(params) {
4512
4628
  return sendCommand(
4513
4629
  "dom_wait",
@@ -4528,36 +4644,6 @@ var init_client = __esm({
4528
4644
  }
4529
4645
  });
4530
4646
 
4531
- // src/connection/errors.ts
4532
- var ConnectionError, CDPConnectionError;
4533
- var init_errors5 = __esm({
4534
- "src/connection/errors.ts"() {
4535
- "use strict";
4536
- init_exitCodes();
4537
- ConnectionError = class extends Error {
4538
- issue;
4539
- constructor(message, options) {
4540
- super(message);
4541
- this.name = this.constructor.name;
4542
- if (options instanceof Error) {
4543
- this.cause = options;
4544
- return;
4545
- }
4546
- if (options?.cause) {
4547
- this.cause = options.cause;
4548
- }
4549
- if (options?.issue) {
4550
- this.issue = options.issue;
4551
- }
4552
- }
4553
- };
4554
- CDPConnectionError = class extends ConnectionError {
4555
- code = "CDP_CONNECTION_ERROR";
4556
- exitCode = EXIT_CODES.CDP_CONNECTION_FAILURE;
4557
- };
4558
- }
4559
- });
4560
-
4561
4647
  // node_modules/ws/lib/constants.js
4562
4648
  var require_constants = __commonJS({
4563
4649
  "node_modules/ws/lib/constants.js"(exports, module) {
@@ -8352,7 +8438,7 @@ var init_elementInfo = __esm({
8352
8438
  return withoutDecorations(el, start);
8353
8439
  }`;
8354
8440
  MASKED_VALUE = "\u2022\u2022\u2022\u2022";
8355
- SENSITIVE_NAME_SOURCE = "passw|passwd|pwd|passcode|(^|[^a-z])otp([^a-z]|$)|cvv|cvc";
8441
+ SENSITIVE_NAME_SOURCE = "passw|passwd|pwd|passcode|passphrase|(^|[^a-z])(otp|pin|ssn)([^a-z]|$)|cvv|cvc|card[-_]?(num(ber)?|no)([^a-z]|$)";
8356
8442
  SENSITIVE_FIELD_JS = `(el) => {
8357
8443
  const autocomplete = el.getAttribute('autocomplete') || '';
8358
8444
  if (/(^|\\s)(cc-[a-z-]+|one-time-code|current-password|new-password)(\\s|$)/i.test(autocomplete)) return true;
@@ -9826,7 +9912,7 @@ async function resolveSelector(selector) {
9826
9912
  EXIT_CODES.RESOURCE_NOT_FOUND
9827
9913
  );
9828
9914
  }
9829
- return { backendNodeId };
9915
+ return backendNodeId;
9830
9916
  }
9831
9917
  async function resolveBackendNodeIds(selectors) {
9832
9918
  return mapConcurrently(selectors, async (selector) => (await selectAll(selector))[0]);
@@ -9974,589 +10060,56 @@ var init_outputFile = __esm({
9974
10060
  EISDIR: { reason: "it is a directory", exitCode: EXIT_CODES.INVALID_ARGUMENTS },
9975
10061
  ENOTDIR: { reason: "a part of the path is a file", exitCode: EXIT_CODES.INVALID_ARGUMENTS },
9976
10062
  EEXIST: { reason: "a part of the path is a file", exitCode: EXIT_CODES.INVALID_ARGUMENTS },
9977
- ENAMETOOLONG: { reason: "the name is too long", exitCode: EXIT_CODES.INVALID_ARGUMENTS },
9978
- ENOENT: { reason: "the directory cannot be created", exitCode: EXIT_CODES.INVALID_ARGUMENTS },
9979
- ENOSPC: { reason: "no space left on the device", exitCode: EXIT_CODES.SESSION_FILE_ERROR },
9980
- EPSEUDOFS: {
9981
- reason: "it is on a pseudo-filesystem (/proc, /sys)",
9982
- exitCode: EXIT_CODES.INVALID_ARGUMENTS
9983
- }
9984
- };
9985
- }
9986
- });
9987
-
9988
- // src/runtime/page/userAgent.ts
9989
- var log24;
9990
- var init_userAgent = __esm({
9991
- "src/runtime/page/userAgent.ts"() {
9992
- "use strict";
9993
- init_logging();
9994
- init_errors();
9995
- log24 = createLogger("session");
9996
- }
9997
- });
9998
-
9999
- // src/runtime/page/emulation.ts
10000
- function viewportOverride(viewport, deviceScaleFactor = 0) {
10001
- const mobile = viewport.mobile === true;
10002
- return {
10003
- width: viewport.width,
10004
- height: viewport.height,
10005
- deviceScaleFactor: deviceScaleFactor || (mobile ? MOBILE_PIXEL_RATIO : 0),
10006
- mobile
10007
- };
10008
- }
10009
- var log25, MOBILE_PIXEL_RATIO;
10010
- var init_emulation = __esm({
10011
- "src/runtime/page/emulation.ts"() {
10012
- "use strict";
10013
- init_elementGeometry();
10014
- init_userAgent();
10015
- init_logging();
10016
- init_errors();
10017
- log25 = createLogger("session");
10018
- MOBILE_PIXEL_RATIO = 3;
10019
- }
10020
- });
10021
-
10022
- // src/commands/dom/helpers/screenshot.ts
10023
- async function waitForPostScrollStability() {
10024
- const deadline = Date.now() + POST_SCROLL_MAX_WAIT_MS;
10025
- await callBdgScript("Runtime.evaluate", {
10026
- expression: `
10027
- (() => {
10028
- window.__bdg_scrollStability = {
10029
- lastNetworkActivity: Date.now(),
10030
- lastDomMutation: Date.now(),
10031
- activeRequests: 0
10032
- };
10033
-
10034
- const state = window.__bdg_scrollStability;
10035
-
10036
- if (window.PerformanceObserver) {
10037
- const perfObserver = new PerformanceObserver((list) => {
10038
- for (const entry of list.getEntries()) {
10039
- if (entry.entryType === 'resource') {
10040
- state.lastNetworkActivity = Date.now();
10041
- }
10042
- }
10043
- });
10044
- try {
10045
- perfObserver.observe({ entryTypes: ['resource'] });
10046
- state.perfObserver = perfObserver;
10047
- } catch (e) {}
10048
- }
10049
-
10050
- const mutationObserver = new MutationObserver(() => {
10051
- state.lastDomMutation = Date.now();
10052
- });
10053
- mutationObserver.observe(document.body || document.documentElement, {
10054
- childList: true,
10055
- subtree: true,
10056
- attributes: true
10057
- });
10058
- state.mutationObserver = mutationObserver;
10059
- })()
10060
- `,
10061
- returnByValue: true
10062
- });
10063
- try {
10064
- while (Date.now() < deadline) {
10065
- const checkResult = await callBdgScript("Runtime.evaluate", {
10066
- expression: `
10067
- (() => {
10068
- const state = window.__bdg_scrollStability;
10069
- if (!state) return { networkIdle: 999, domIdle: 999 };
10070
- return {
10071
- networkIdle: Date.now() - state.lastNetworkActivity,
10072
- domIdle: Date.now() - state.lastDomMutation
10073
- };
10074
- })()
10075
- `,
10076
- returnByValue: true
10077
- });
10078
- const value = checkResult.data?.result?.result?.value;
10079
- const networkIdle = value?.networkIdle ?? 0;
10080
- const domIdle = value?.domIdle ?? 0;
10081
- if (networkIdle >= POST_SCROLL_NETWORK_IDLE_MS && domIdle >= POST_SCROLL_DOM_STABLE_MS) {
10082
- log26.debug(`Post-scroll stable: network ${networkIdle}ms, DOM ${domIdle}ms`);
10083
- return;
10084
- }
10085
- await new Promise((resolve4) => setTimeout(resolve4, STABILITY_CHECK_INTERVAL_MS));
10086
- }
10087
- log26.debug("Post-scroll stability timeout, proceeding anyway");
10088
- } finally {
10089
- await callBdgScript("Runtime.evaluate", {
10090
- expression: `
10091
- (() => {
10092
- const state = window.__bdg_scrollStability;
10093
- if (state) {
10094
- state.perfObserver?.disconnect();
10095
- state.mutationObserver?.disconnect();
10096
- delete window.__bdg_scrollStability;
10097
- }
10098
- })()
10099
- `,
10100
- returnByValue: true
10101
- });
10102
- }
10103
- }
10104
- async function scrollToElement(selector) {
10105
- const result = await callBdgScript("Runtime.evaluate", {
10106
- expression: `
10107
- (() => {
10108
- const el = (${DEEP_QUERY_JS})(${selectorArgsJS(selector)})[0];
10109
- if (!el) return { found: false };
10110
- const originalX = window.scrollX;
10111
- const originalY = window.scrollY;
10112
- el.scrollIntoView({ block: 'center', behavior: 'instant' });
10113
- return { found: true, originalX, originalY };
10114
- })()
10115
- `,
10116
- returnByValue: true
10117
- });
10118
- const value = result.data?.result?.result?.value;
10119
- if (!value?.found) {
10120
- const err = noNodesFoundError(selector);
10121
- throw new CommandError(
10122
- err.message,
10123
- { suggestion: err.suggestion },
10124
- EXIT_CODES.RESOURCE_NOT_FOUND
10125
- );
10126
- }
10127
- await waitForPostScrollStability();
10128
- return { x: value.originalX ?? 0, y: value.originalY ?? 0 };
10129
- }
10130
- async function restoreScrollPosition(position) {
10131
- await callBdgScript("Runtime.evaluate", {
10132
- expression: `window.scrollTo(${position.x}, ${position.y})`,
10133
- returnByValue: true
10134
- });
10135
- }
10136
- async function windowSize(viewport) {
10137
- const response = await callBdgScript("Runtime.evaluate", {
10138
- expression: "[window.innerWidth, window.innerHeight]",
10139
- returnByValue: true
10140
- });
10141
- const value = response.data?.result?.result?.value;
10142
- const [width, height] = Array.isArray(value) ? value : [];
10143
- return {
10144
- width: Math.round(width ?? viewport.clientWidth),
10145
- height: Math.round(height ?? viewport.clientHeight)
10146
- };
10147
- }
10148
- async function useUnitPixelRatio(devicePixelRatio, viewport) {
10149
- if (devicePixelRatio === 1) return () => Promise.resolve();
10150
- const sessionViewport = readSessionMetadata()?.viewport;
10151
- const size = sessionViewport ?? await windowSize(viewport);
10152
- await callCDP("Emulation.setDeviceMetricsOverride", viewportOverride(size, 1));
10153
- return restoreSessionMetrics;
10154
- }
10155
- async function restoreSessionMetrics() {
10156
- const sessionViewport = readSessionMetadata()?.viewport;
10157
- if (!sessionViewport) {
10158
- await callCDP("Emulation.clearDeviceMetricsOverride", {});
10159
- return;
10160
- }
10161
- await callCDP("Emulation.setDeviceMetricsOverride", viewportOverride(sessionViewport));
10162
- if (sessionViewport.mobile) {
10163
- await callCDP("Emulation.setTouchEmulationEnabled", { enabled: true, maxTouchPoints: 5 });
10164
- }
10165
- }
10166
- async function getElementBounds(ref) {
10167
- const response = await callCDP("DOM.getBoxModel", ref);
10168
- const boxModel = response.data?.result;
10169
- if (!boxModel?.model?.border) {
10170
- const err = elementNotVisibleError();
10171
- throw new CommandError(
10172
- err.message,
10173
- { suggestion: err.suggestion },
10174
- EXIT_CODES.INVALID_ARGUMENTS
10175
- );
10176
- }
10177
- const border = boxModel.model.border;
10178
- const x = border[0] ?? 0;
10179
- const y = border[1] ?? 0;
10180
- const width = (border[2] ?? 0) - x;
10181
- const height = (border[5] ?? 0) - y;
10182
- if (width <= 0 || height <= 0) {
10183
- const err = elementZeroDimensionsError();
10184
- throw new CommandError(
10185
- err.message,
10186
- { suggestion: err.suggestion },
10187
- EXIT_CODES.INVALID_ARGUMENTS
10188
- );
10189
- }
10190
- return { x, y, width, height };
10191
- }
10192
- async function visibleAreaOrigin() {
10193
- const response = await callCDP("Page.getLayoutMetrics", {});
10194
- const metrics = response.data?.result;
10195
- const viewport = metrics?.cssVisualViewport;
10196
- return { x: viewport?.pageX ?? 0, y: viewport?.pageY ?? 0 };
10197
- }
10198
- async function capturePageScreenshot(outputPath, options = {}) {
10199
- const format = options.format ?? "png";
10200
- const quality = format === "jpeg" ? options.quality ?? 90 : void 0;
10201
- const requestedFullPage = options.fullPage ?? true;
10202
- const noResize = options.noResize ?? false;
10203
- let originalScrollPosition;
10204
- if (options.scroll) {
10205
- originalScrollPosition = await scrollToElement(options.scroll);
10206
- }
10207
- const dprResponse = await callBdgScript("Runtime.evaluate", {
10208
- expression: "window.devicePixelRatio",
10209
- returnByValue: true
10210
- });
10211
- const devicePixelRatio = dprResponse.data?.result?.result?.value ?? 1;
10212
- const metricsResponse = await callCDP("Page.getLayoutMetrics", {});
10213
- const metricsResult = metricsResponse.data?.result;
10214
- const contentSize = metricsResult?.contentSize ?? { width: 0, height: 0 };
10215
- const viewport = metricsResult?.visualViewport ?? { clientWidth: 0, clientHeight: 0 };
10216
- const pageIsTooTall = !noResize && requestedFullPage && isTallPage(contentSize.width, contentSize.height);
10217
- const useScroll = options.scroll !== void 0;
10218
- const effectiveFullPage = useScroll ? false : pageIsTooTall ? false : requestedFullPage;
10219
- const captureWidth = effectiveFullPage ? contentSize.width : viewport.clientWidth;
10220
- const captureHeight = effectiveFullPage ? contentSize.height : viewport.clientHeight;
10221
- const resized = shouldResize(captureWidth, captureHeight, noResize);
10222
- const scale = resized ? calculateResizeScale(captureWidth, captureHeight) : 1;
10223
- const finalWidth = Math.round(captureWidth * scale);
10224
- const finalHeight = Math.round(captureHeight * scale);
10225
- const restoreMetrics = await useUnitPixelRatio(devicePixelRatio, viewport);
10226
- if (devicePixelRatio !== 1) {
10227
- if (options.scroll) {
10228
- await callBdgScript("Runtime.evaluate", {
10229
- expression: `(${DEEP_QUERY_JS})(${selectorArgsJS(options.scroll)})[0]?.scrollIntoView({ block: 'center', behavior: 'instant' })`,
10230
- returnByValue: true
10231
- });
10232
- }
10233
- }
10234
- const clipOrigin = effectiveFullPage ? { x: 0, y: 0 } : await visibleAreaOrigin();
10235
- let screenshotResult;
10236
- try {
10237
- const screenshotResponse = await callCDP("Page.captureScreenshot", {
10238
- format,
10239
- ...quality !== void 0 && { quality },
10240
- captureBeyondViewport: effectiveFullPage,
10241
- clip: {
10242
- x: clipOrigin.x,
10243
- y: clipOrigin.y,
10244
- width: captureWidth,
10245
- height: captureHeight,
10246
- scale
10247
- }
10248
- });
10249
- screenshotResult = screenshotResponse.data?.result;
10250
- } finally {
10251
- await restoreMetrics();
10252
- }
10253
- if (!screenshotResult?.data) {
10254
- throw new CDPConnectionError("No screenshot data returned", new Error("Empty response"));
10255
- }
10256
- const buffer = Buffer.from(screenshotResult.data, "base64");
10257
- const absolutePath = await writeOutputFile(outputPath, buffer);
10258
- const result = {
10259
- path: absolutePath,
10260
- format,
10261
- width: finalWidth,
10262
- height: finalHeight,
10263
- size: buffer.length,
10264
- fullPage: effectiveFullPage,
10265
- captureMode: effectiveFullPage ? "full_page" : "viewport",
10266
- finalTokens: calculateImageTokens(finalWidth, finalHeight)
10267
- };
10268
- if (quality !== void 0) {
10269
- result.quality = quality;
10270
- }
10271
- if (!effectiveFullPage) {
10272
- result.viewport = {
10273
- width: viewport.clientWidth,
10274
- height: viewport.clientHeight
10275
- };
10276
- }
10277
- if (resized) {
10278
- result.resized = true;
10279
- result.originalWidth = captureWidth;
10280
- result.originalHeight = captureHeight;
10281
- result.originalTokens = calculateImageTokens(captureWidth, captureHeight);
10282
- }
10283
- if (pageIsTooTall && !useScroll) {
10284
- const aspectRatio = Math.round(contentSize.height / contentSize.width * 10) / 10;
10285
- result.fullPageSkipped = {
10286
- reason: "page_too_tall",
10287
- originalHeight: contentSize.height,
10288
- aspectRatio
10289
- };
10290
- result.warning = `Full page capture skipped: page too tall (${aspectRatio}:1 aspect ratio). Only viewport captured.`;
10291
- }
10292
- if (useScroll && options.scroll) {
10293
- result.scrolledTo = options.scroll;
10294
- }
10295
- if (originalScrollPosition) {
10296
- await restoreScrollPosition(originalScrollPosition);
10297
- }
10298
- return result;
10299
- }
10300
- async function visibleViewport() {
10301
- const metrics = (await callCDP("Page.getLayoutMetrics", {})).data?.result;
10302
- const view = metrics?.cssVisualViewport ?? metrics?.visualViewport;
10303
- return { width: view?.clientWidth ?? 0, height: view?.clientHeight ?? 0 };
10304
- }
10305
- function insideView(area, view) {
10306
- return area.x >= 0 && area.y >= 0 && area.x + area.width <= view.width && area.y + area.height <= view.height;
10307
- }
10308
- async function measureInView(ref, padding) {
10309
- const view = await visibleViewport();
10310
- let box = await getElementBounds(ref);
10311
- let bounds = await captureArea(ref, box, padding);
10312
- const fits = bounds.width <= view.width && bounds.height <= view.height;
10313
- if (!fits) {
10314
- await keepLayoutWithoutScrollbars(view);
10315
- box = await getElementBounds(ref);
10316
- return { box, bounds: await captureArea(ref, box, padding), inView: false };
10317
- }
10318
- if (insideView(bounds, view)) return { box, bounds, inView: true };
10319
- const scrolledFrom = await scrollPosition();
10320
- const dx = bounds.x + bounds.width / 2 - view.width / 2;
10321
- const dy = bounds.y + bounds.height / 2 - view.height / 2;
10322
- await callBdgScript("Runtime.evaluate", { expression: `window.scrollBy(${dx}, ${dy})` });
10323
- try {
10324
- box = await getElementBounds(ref);
10325
- bounds = await captureArea(ref, box, padding);
10326
- } catch (error) {
10327
- await restoreScrollPosition(scrolledFrom);
10328
- throw error;
10329
- }
10330
- return { box, bounds, inView: insideView(bounds, view), scrolledFrom };
10331
- }
10332
- async function keepLayoutWithoutScrollbars(view) {
10333
- const phone = readSessionMetadata()?.viewport?.mobile;
10334
- await callCDP("Emulation.setScrollbarsHidden", { hidden: true });
10335
- await callCDP(
10336
- "Emulation.setDeviceMetricsOverride",
10337
- viewportOverride({ ...view, ...phone && { mobile: true } }, 1)
10338
- );
10339
- }
10340
- async function restoreViewport() {
10341
- await callCDP("Emulation.setScrollbarsHidden", { hidden: false });
10342
- await restoreSessionMetrics();
10343
- }
10344
- async function scrollPosition() {
10345
- const response = await callBdgScript("Runtime.evaluate", {
10346
- expression: "[window.scrollX, window.scrollY]",
10347
- returnByValue: true
10348
- });
10349
- const value = response.data?.result?.result?.value;
10350
- const [x, y] = Array.isArray(value) ? value : [];
10351
- return { x: x ?? 0, y: y ?? 0 };
10352
- }
10353
- async function captureArea(ref, bounds, padding) {
10354
- const area = await paintedArea(ref, bounds);
10355
- return padding > 0 ? {
10356
- x: area.x - padding,
10357
- y: area.y - padding,
10358
- width: area.width + 2 * padding,
10359
- height: area.height + 2 * padding
10360
- } : area;
10361
- }
10362
- async function paintedArea(ref, bounds) {
10363
- const objectGroup = `bdg-shot-${process.pid}`;
10364
- try {
10365
- const resolved = await callBdgScript("DOM.resolveNode", { ...ref, objectGroup });
10366
- const objectId = resolved.data?.result?.object.objectId;
10367
- if (!objectId) return bounds;
10368
- const response = await callCDP("Runtime.callFunctionOn", {
10369
- objectId,
10370
- functionDeclaration: CONTENT_OVERFLOW_JS,
10371
- returnByValue: true
10372
- });
10373
- const overflow = response.data?.result?.result?.value;
10374
- if (!overflow) return bounds;
10375
- const [left, top, right, bottom] = ["left", "top", "right", "bottom"].map(
10376
- (side) => Math.max(0, overflow[side] ?? 0)
10377
- );
10378
- if (Math.max(left, top, right, bottom) <= OVERFLOW_SLACK) return bounds;
10379
- return {
10380
- x: bounds.x - left,
10381
- y: bounds.y - top,
10382
- width: bounds.width + left + right,
10383
- height: bounds.height + top + bottom
10384
- };
10385
- } catch (error) {
10386
- log26.debug(`Could not measure overflowing content: ${String(error)}`);
10387
- return bounds;
10388
- } finally {
10389
- await callCDP("Runtime.releaseObjectGroup", { objectGroup }).catch(() => void 0);
10390
- }
10391
- }
10392
- async function captureElementScreenshot(outputPath, ref, options = {}) {
10393
- const format = options.format ?? "png";
10394
- const quality = format === "jpeg" ? options.quality ?? 90 : void 0;
10395
- const noResize = options.noResize ?? false;
10396
- const dprResponse = await callBdgScript("Runtime.evaluate", {
10397
- expression: "window.devicePixelRatio",
10398
- returnByValue: true
10399
- });
10400
- const devicePixelRatio = dprResponse.data?.result?.result?.value ?? 1;
10401
- const before = (await callCDP("Page.getLayoutMetrics", {})).data?.result;
10402
- const restoreMetrics = await useUnitPixelRatio(
10403
- devicePixelRatio,
10404
- before?.visualViewport ?? { clientWidth: 800, clientHeight: 600 }
10405
- );
10406
- const restore = async (wide, scrolledFrom2) => {
10407
- await (wide ? restoreViewport() : restoreMetrics());
10408
- if (scrolledFrom2) await restoreScrollPosition(scrolledFrom2);
10409
- };
10410
- let measured;
10411
- try {
10412
- measured = await measureInView(ref, options.padding ?? 0);
10413
- } catch (error) {
10414
- await restoreViewport();
10415
- throw error;
10416
- }
10417
- const { box, bounds, inView, scrolledFrom } = measured;
10418
- const originalWidth = bounds.width;
10419
- const originalHeight = bounds.height;
10420
- const resized = shouldResize(originalWidth, originalHeight, noResize);
10421
- const scale = resized ? calculateResizeScale(originalWidth, originalHeight) : 1;
10422
- const finalWidth = Math.round(originalWidth * scale);
10423
- const finalHeight = Math.round(originalHeight * scale);
10424
- const metricsResponse = await callCDP("Page.getLayoutMetrics", {});
10425
- const metricsResult = metricsResponse.data?.result;
10426
- const scroll = metricsResult?.cssLayoutViewport ?? { pageX: 0, pageY: 0 };
10427
- const onPage = (area) => ({
10428
- ...area,
10429
- x: area.x + scroll.pageX,
10430
- y: area.y + scroll.pageY
10431
- });
10432
- const clip = onPage(bounds);
10433
- let screenshotResult;
10434
- try {
10435
- const screenshotResponse = await callCDP("Page.captureScreenshot", {
10436
- format,
10437
- ...quality !== void 0 && { quality },
10438
- clip: { ...clip, scale },
10439
- captureBeyondViewport: !inView
10440
- });
10441
- screenshotResult = screenshotResponse.data?.result;
10442
- } finally {
10443
- await restore(!inView, scrolledFrom);
10444
- }
10445
- if (!screenshotResult?.data) {
10446
- throw new CDPConnectionError("No screenshot data returned", new Error("Empty response"));
10447
- }
10448
- const buffer = Buffer.from(screenshotResult.data, "base64");
10449
- const absolutePath = await writeOutputFile(outputPath, buffer);
10450
- const result = {
10451
- path: absolutePath,
10452
- format,
10453
- width: finalWidth,
10454
- height: finalHeight,
10455
- size: buffer.length,
10456
- fullPage: false,
10457
- finalTokens: calculateImageTokens(finalWidth, finalHeight),
10458
- element: {
10459
- bounds: roundBounds(onPage(box)),
10460
- ...bounds !== box && { captured: roundBounds(clip) },
10461
- ...options.padding && { padding: options.padding }
10462
- }
10463
- };
10464
- if (quality !== void 0) {
10465
- result.quality = quality;
10466
- }
10467
- if (resized) {
10468
- result.resized = true;
10469
- result.originalWidth = originalWidth;
10470
- result.originalHeight = originalHeight;
10471
- result.originalTokens = calculateImageTokens(originalWidth, originalHeight);
10063
+ ENAMETOOLONG: { reason: "the name is too long", exitCode: EXIT_CODES.INVALID_ARGUMENTS },
10064
+ ENOENT: { reason: "the directory cannot be created", exitCode: EXIT_CODES.INVALID_ARGUMENTS },
10065
+ ENOSPC: { reason: "no space left on the device", exitCode: EXIT_CODES.SESSION_FILE_ERROR },
10066
+ EPSEUDOFS: {
10067
+ reason: "it is on a pseudo-filesystem (/proc, /sys)",
10068
+ exitCode: EXIT_CODES.INVALID_ARGUMENTS
10069
+ }
10070
+ };
10472
10071
  }
10473
- return result;
10072
+ });
10073
+
10074
+ // src/commands/dom/helpers/screenshot.ts
10075
+ function screenshotInterrupted(signal) {
10076
+ const err = screenshotInterruptedError(signal);
10077
+ return new CommandError(err.message, { suggestion: err.suggestion }, interruptExitCode(signal));
10078
+ }
10079
+ async function requestCapture(request, interrupt) {
10080
+ if (!interrupt) return domScreenshot(request);
10081
+ const capture = domScreenshot(request, interrupt);
10082
+ const response = await unlessInterrupted(capture, interrupt, screenshotInterrupted);
10083
+ if (interrupt.aborted) throw screenshotInterrupted(interruptSignal(interrupt));
10084
+ return response;
10474
10085
  }
10475
- function roundBounds(bounds) {
10476
- return {
10477
- x: Math.round(bounds.x),
10478
- y: Math.round(bounds.y),
10479
- width: Math.round(bounds.width),
10480
- height: Math.round(bounds.height)
10481
- };
10086
+ async function captureScreenshot(outputPath, request, interrupt) {
10087
+ const response = await requestCapture(request, interrupt);
10088
+ if (response.status === "error" || !response.data) {
10089
+ const fallback = operationFailedError(
10090
+ "take the screenshot",
10091
+ `Check the session: ${sessionCommand("bdg status")}`
10092
+ );
10093
+ const suggestion = response.error === void 0 ? fallback.suggestion : response.suggestion;
10094
+ throw new CommandError(
10095
+ response.error ?? fallback.message,
10096
+ suggestion ? { suggestion } : {},
10097
+ response.exitCode ?? EXIT_CODES.CDP_CONNECTION_FAILURE
10098
+ );
10099
+ }
10100
+ const path8 = await writeOutputFile(outputPath, Buffer.from(response.data.image, "base64"));
10101
+ return { path: path8, ...response.data.screenshot };
10482
10102
  }
10483
- var log26, POST_SCROLL_NETWORK_IDLE_MS, POST_SCROLL_DOM_STABLE_MS, POST_SCROLL_MAX_WAIT_MS, STABILITY_CHECK_INTERVAL_MS, OVERFLOW_SCAN_LIMIT, CONTENT_OVERFLOW_JS, OVERFLOW_SLACK;
10484
10103
  var init_screenshot = __esm({
10485
10104
  "src/commands/dom/helpers/screenshot.ts"() {
10486
10105
  "use strict";
10487
- init_screenshotResize();
10106
+ init_interrupt();
10488
10107
  init_outputFile();
10489
- init_errors5();
10490
10108
  init_errors3();
10491
10109
  init_messages();
10492
10110
  init_client();
10493
- init_targetNode();
10494
- init_emulation();
10495
- init_metadata();
10496
- init_logging();
10111
+ init_sessionCommand();
10497
10112
  init_exitCodes();
10498
- log26 = createLogger("dom");
10499
- POST_SCROLL_NETWORK_IDLE_MS = 150;
10500
- POST_SCROLL_DOM_STABLE_MS = 200;
10501
- POST_SCROLL_MAX_WAIT_MS = 2e3;
10502
- STABILITY_CHECK_INTERVAL_MS = 50;
10503
- OVERFLOW_SCAN_LIMIT = 2e3;
10504
- CONTENT_OVERFLOW_JS = `function () {
10505
- const view = this.ownerDocument.defaultView;
10506
- const scroller = this.ownerDocument.scrollingElement || this.ownerDocument.documentElement;
10507
- const own = this.getBoundingClientRect();
10508
- const reach = { left: own.left, top: own.top, right: own.right, bottom: own.bottom };
10509
- const page = { left: -view.scrollX, top: -view.scrollY, right: scroller.scrollWidth - view.scrollX, bottom: scroller.scrollHeight - view.scrollY };
10510
- const clips = (style) => style.overflowX !== 'visible' || style.overflowY !== 'visible';
10511
- let budget = ${OVERFLOW_SCAN_LIMIT};
10512
- const walk = (el) => {
10513
- for (const child of el.children) {
10514
- if (--budget < 0) return;
10515
- const style = view.getComputedStyle(child);
10516
- if (style.display === 'none' || style.position === 'fixed') continue;
10517
- const r = child.getBoundingClientRect();
10518
- if (r.width > 0 && r.height > 0 && style.visibility === 'visible') {
10519
- reach.left = Math.min(reach.left, Math.max(r.left, page.left));
10520
- reach.top = Math.min(reach.top, Math.max(r.top, page.top));
10521
- reach.right = Math.max(reach.right, Math.min(r.right, page.right));
10522
- reach.bottom = Math.max(reach.bottom, Math.min(r.bottom, page.bottom));
10523
- }
10524
- if (!clips(style)) walk(child);
10525
- }
10526
- };
10527
- const ownStyle = view.getComputedStyle(this);
10528
- if (!clips(ownStyle)) {
10529
- walk(this);
10530
- const scaleX = this.offsetWidth ? own.width / this.offsetWidth : 1;
10531
- const scaleY = this.offsetHeight ? own.height / this.offsetHeight : 1;
10532
- const wider = Math.max(0, this.scrollWidth - this.clientWidth) * scaleX;
10533
- const taller = Math.max(0, this.scrollHeight - this.clientHeight) * scaleY;
10534
- if (ownStyle.direction === 'rtl') reach.left = Math.min(reach.left, own.left - wider);
10535
- else reach.right = Math.max(reach.right, own.right + wider);
10536
- reach.bottom = Math.max(reach.bottom, own.bottom + taller);
10537
- }
10538
- const ink = { left: 0, top: 0, right: 0, bottom: 0 };
10539
- const grow = (side, amount) => { ink[side] = Math.max(ink[side], amount); };
10540
- for (const layer of ownStyle.boxShadow === 'none' ? [] : ownStyle.boxShadow.split(/,(?![^(]*\\))/)) {
10541
- if (/\\binset\\b/.test(layer)) continue;
10542
- const [x = 0, y = 0, blur = 0, spread = 0] = (layer.replace(/(rgba?|hsla?|color|oklch|lab|lch)\\([^)]*\\)/g, '').match(/-?[\\d.]+px/g) || []).map(parseFloat);
10543
- grow('left', blur + spread - x);
10544
- grow('right', blur + spread + x);
10545
- grow('top', blur + spread - y);
10546
- grow('bottom', blur + spread + y);
10547
- }
10548
- if (ownStyle.outlineStyle !== 'none') {
10549
- const outline = parseFloat(ownStyle.outlineWidth) + parseFloat(ownStyle.outlineOffset);
10550
- ['left', 'top', 'right', 'bottom'].forEach((side) => grow(side, outline));
10551
- }
10552
- return {
10553
- left: Math.max(own.left - reach.left, ink.left),
10554
- top: Math.max(own.top - reach.top, ink.top),
10555
- right: Math.max(reach.right - own.right, ink.right),
10556
- bottom: Math.max(reach.bottom - own.bottom, ink.bottom)
10557
- };
10558
- }`;
10559
- OVERFLOW_SLACK = 1;
10560
10113
  }
10561
10114
  });
10562
10115
 
@@ -10564,17 +10117,15 @@ var init_screenshot = __esm({
10564
10117
  var helpers_exports = {};
10565
10118
  __export(helpers_exports, {
10566
10119
  assertNodeAttached: () => assertNodeAttached,
10567
- captureElementScreenshot: () => captureElementScreenshot,
10568
- capturePageScreenshot: () => capturePageScreenshot,
10569
- documentReadyState: () => documentReadyState,
10120
+ captureScreenshot: () => captureScreenshot,
10570
10121
  getDOMElements: () => getDOMElements,
10571
10122
  getDomContext: () => getDomContext,
10572
- getElementBounds: () => getElementBounds,
10573
10123
  noMatchesError: () => noMatchesError,
10574
10124
  pageDocumentId: () => pageDocumentId,
10575
10125
  queryDOMElements: () => queryDOMElements,
10576
10126
  resolveBackendNodeIds: () => resolveBackendNodeIds,
10577
10127
  resolveSelector: () => resolveSelector,
10128
+ screenshotInterrupted: () => screenshotInterrupted,
10578
10129
  selectMatch: () => selectMatch
10579
10130
  });
10580
10131
  var init_helpers = __esm({
@@ -13956,11 +13507,15 @@ var program = new Command();
13956
13507
  // src/commands/helpJson.ts
13957
13508
  init_schema();
13958
13509
 
13510
+ // src/runtime/page/screenshotResize.ts
13511
+ var MAX_EDGE_PX = 1568;
13512
+ var PIXELS_PER_TOKEN = 750;
13513
+ var TALL_PAGE_THRESHOLD = 3;
13514
+
13959
13515
  // src/commands/optionBehaviors.ts
13960
- init_screenshotResize();
13961
13516
  var 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";
13962
- var 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: "\u2026" (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';
13963
- var 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';
13517
+ var 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: "\u2026" (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> \u2192 <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)\u2026 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: \u2026)); 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';
13518
+ var 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';
13964
13519
  var 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`;
13965
13520
  var NO_WAIT_TRIGGERED_REQUESTS = "Returns immediately without waiting for network; triggeredRequests lists only requests bdg saw start before returning (often none yet; check bdg network list later)";
13966
13521
  var FOLLOW_BEHAVIOR = "Stops with exit 83 when the session it follows ends, 130 on Ctrl-C, 143 on SIGTERM; with --json prints one compact object per line (NDJSON). Other failures (a busy page, a timeout) are retried: reported once in text, as one error line per refresh in JSON";
@@ -14271,9 +13826,9 @@ var OPTION_BEHAVIORS = {
14271
13826
  automaticBehavior: FOLLOW_BEHAVIOR
14272
13827
  },
14273
13828
  "har:--include-sensitive": {
14274
- 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',
13829
+ 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\u2026) 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`,
14275
13830
  whenEnabled: "Writes every captured value (JSON sanitized: false); human output warns that the file holds credentials",
14276
- 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"
13831
+ 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'
14277
13832
  },
14278
13833
  "peek:--verbose": {
14279
13834
  default: "Compact output (truncated URLs, no resource types)",
@@ -14305,8 +13860,8 @@ var OPTION_BEHAVIORS = {
14305
13860
  whenEnabled: "Alias for --force, kept for compatibility"
14306
13861
  },
14307
13862
  "cleanup:--purge": {
14308
- default: "A named session's directory (Chrome profile, ~60 MB; logs; port.txt) is kept for its next start",
14309
- 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"
13863
+ default: "A named session's directory (Chrome profile, ~60 MB; logs; port.txt; downloads/) is kept for its next start",
13864
+ 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"
14310
13865
  },
14311
13866
  "bdg:--viewport": {
14312
13867
  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",
@@ -14326,12 +13881,22 @@ var OPTION_BEHAVIORS = {
14326
13881
  "bdg:--chrome-ws-url": {
14327
13882
  default: "bdg launches its own Chrome (closed on stop)",
14328
13883
  whenEnabled: "Attaches to a running Chrome instead; it keeps running after stop. --port, -u and --[no-]headless cannot be combined with it (exit 81)",
14329
- 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)"
13884
+ 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)"
14330
13885
  },
14331
13886
  "stop:--kill-chrome": {
14332
13887
  default: "Chrome launched by bdg is always closed on stop; an attached Chrome (--chrome-ws-url) is left running",
14333
13888
  whenEnabled: "No additional effect; kept for compatibility"
14334
13889
  },
13890
+ "cdp:--send-anyway": {
13891
+ 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",
13892
+ 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",
13893
+ 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"
13894
+ },
13895
+ "cdp:--describe": {
13896
+ 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)",
13897
+ whenEnabled: "Describes a domain, a method (parameters with ? for optional, returns, example) or a protocol type (Domain.Type: enum values or object properties)",
13898
+ 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 \u2192 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)"
13899
+ },
14335
13900
  "status:--verbose": {
14336
13901
  default: "Basic session status (daemon running, session active, URL)",
14337
13902
  whenEnabled: "Includes Chrome diagnostics and CDP connection details"
@@ -14395,25 +13960,25 @@ var loggingBufferColumns = 25;
14395
13960
  var level_;
14396
13961
  var Log = class _Log {
14397
13962
  static _logToStdErr(title, argsArray) {
14398
- const log36 = _Log.loggerfn(title);
14399
- log36(...argsArray);
13963
+ const log33 = _Log.loggerfn(title);
13964
+ log33(...argsArray);
14400
13965
  }
14401
13966
  /**
14402
13967
  * @param {string} title
14403
13968
  */
14404
13969
  static loggerfn(title) {
14405
13970
  title = `LH:${title}`;
14406
- let log36 = loggersByTitle[title];
14407
- if (!log36) {
14408
- log36 = (0, import_debug.default)(title);
14409
- loggersByTitle[title] = log36;
13971
+ let log33 = loggersByTitle[title];
13972
+ if (!log33) {
13973
+ log33 = (0, import_debug.default)(title);
13974
+ loggersByTitle[title] = log33;
14410
13975
  if (title.endsWith("error")) {
14411
- log36.color = colors.red;
13976
+ log33.color = colors.red;
14412
13977
  } else if (title.endsWith("warn")) {
14413
- log36.color = colors.yellow;
13978
+ log33.color = colors.yellow;
14414
13979
  }
14415
13980
  }
14416
- return log36;
13981
+ return log33;
14417
13982
  }
14418
13983
  /**
14419
13984
  * @param {string} level
@@ -14989,14 +14554,61 @@ function isDaemonSocketGone(socketPath = getSessionFilePath("DAEMON_SOCKET"), ti
14989
14554
  });
14990
14555
  }
14991
14556
 
14557
+ // src/session/metadata.ts
14558
+ init_logging();
14559
+ init_sessionCommand();
14560
+ init_atomicFile();
14561
+ init_errors();
14562
+ init_paths();
14563
+ import * as fs8 from "fs";
14564
+ var log6 = createLogger("session");
14565
+ function handleCorruptedMetadata(metaPath, error, options) {
14566
+ if (options?.warnOnCorruption) {
14567
+ log6.info(`Session metadata corrupted (cannot read details): ${getErrorMessage(error)}`);
14568
+ log6.info(`Troubleshooting: Run "${sessionCommand("bdg cleanup")}" to remove corrupted files`);
14569
+ }
14570
+ if (options?.selfHealOnCorruption) {
14571
+ try {
14572
+ fs8.rmSync(metaPath, { force: true });
14573
+ log6.info("Removed corrupted session metadata file");
14574
+ } catch (deleteError) {
14575
+ log6.debug(`Failed to remove corrupted metadata: ${getErrorMessage(deleteError)}`);
14576
+ }
14577
+ }
14578
+ }
14579
+ function readSessionMetadata(options) {
14580
+ const metaPath = getSessionFilePath("METADATA");
14581
+ if (!fs8.existsSync(metaPath)) {
14582
+ return null;
14583
+ }
14584
+ try {
14585
+ const content = fs8.readFileSync(metaPath, "utf-8");
14586
+ return JSON.parse(content);
14587
+ } catch (error) {
14588
+ handleCorruptedMetadata(metaPath, error, options);
14589
+ return null;
14590
+ }
14591
+ }
14592
+
14992
14593
  // src/session/cleanup/staleSession.ts
14993
- init_metadata();
14994
14594
  init_paths();
14995
14595
  init_logging();
14996
14596
 
14997
14597
  // src/utils/async.ts
14998
- function delay2(ms) {
14999
- return new Promise((resolve4) => setTimeout(resolve4, ms));
14598
+ function delay2(ms, signal) {
14599
+ return new Promise((resolve4) => {
14600
+ if (signal?.aborted) {
14601
+ resolve4();
14602
+ return;
14603
+ }
14604
+ const done = () => {
14605
+ clearTimeout(timer);
14606
+ signal?.removeEventListener("abort", done);
14607
+ resolve4();
14608
+ };
14609
+ const timer = setTimeout(done, ms);
14610
+ signal?.addEventListener("abort", done, { once: true });
14611
+ });
15000
14612
  }
15001
14613
  async function waitUntil(condition, timeoutMs, pollMs = 25) {
15002
14614
  const deadline = Date.now() + timeoutMs;
@@ -15010,12 +14622,12 @@ async function waitUntil(condition, timeoutMs, pollMs = 25) {
15010
14622
  // src/utils/file.ts
15011
14623
  init_logging();
15012
14624
  import * as fs9 from "fs";
15013
- function safeRemoveFile(path8, context, log36) {
14625
+ function safeRemoveFile(path8, context, log33) {
15014
14626
  try {
15015
14627
  fs9.rmSync(path8, { force: true });
15016
14628
  return true;
15017
14629
  } catch (error) {
15018
- logDebugError(log36, `remove ${context}`, error);
14630
+ logDebugError(log33, `remove ${context}`, error);
15019
14631
  return false;
15020
14632
  }
15021
14633
  }
@@ -15714,8 +15326,8 @@ function getVersion() {
15714
15326
  var VERSION = getVersion();
15715
15327
 
15716
15328
  // src/ui/OutputBuilder.ts
15717
- function buildSuccessResponse(data) {
15718
- return { version: VERSION, success: true, data };
15329
+ function buildSuccessResponse(data, warning) {
15330
+ return { version: VERSION, success: true, data, ...warning && { warning } };
15719
15331
  }
15720
15332
  function stringifyEnvelope(envelope) {
15721
15333
  return process.stdout.isTTY ? JSON.stringify(envelope, null, 2) : JSON.stringify(envelope);
@@ -15826,6 +15438,9 @@ function sessionEndedError() {
15826
15438
  EXIT_CODES.RESOURCE_NOT_FOUND
15827
15439
  );
15828
15440
  }
15441
+ function printWarning(warning) {
15442
+ if (warning) console.error(escapeControlChars(`Warning: ${warning}`));
15443
+ }
15829
15444
  async function runCommand(handler, options, formatter) {
15830
15445
  try {
15831
15446
  const result = await handler(options);
@@ -15836,6 +15451,7 @@ async function runCommand(handler, options, formatter) {
15836
15451
  stringifyEnvelope(
15837
15452
  OutputBuilder.buildJsonError(result.error ?? "Unknown error", {
15838
15453
  ...result.errorContext,
15454
+ ...result.warning && { warning: result.warning },
15839
15455
  exitCode
15840
15456
  })
15841
15457
  )
@@ -15851,14 +15467,16 @@ async function runCommand(handler, options, formatter) {
15851
15467
  }
15852
15468
  }
15853
15469
  }
15470
+ printWarning(result.warning);
15854
15471
  }
15855
15472
  process.exit(exitCode);
15856
15473
  }
15474
+ if (!options.json) printWarning(result.warning);
15857
15475
  if (result.hint && !options.quiet) {
15858
15476
  console.error(escapeControlChars(result.hint));
15859
15477
  }
15860
15478
  if (options.json) {
15861
- console.log(stringifyEnvelope(buildSuccessResponse(result.data)));
15479
+ console.log(stringifyEnvelope(buildSuccessResponse(result.data, result.warning)));
15862
15480
  } else if (formatter) {
15863
15481
  const formattedOutput = formatter(result.data);
15864
15482
  console.log(escapeControlChars(formattedOutput));
@@ -15879,10 +15497,14 @@ async function runCommand(handler, options, formatter) {
15879
15497
  )
15880
15498
  );
15881
15499
  } else {
15500
+ const { warning, ...metadata } = error.metadata;
15882
15501
  console.error(genericError(error.message));
15883
- for (const value of Object.values(error.metadata)) {
15884
- console.error(escapeControlChars(String(value)));
15502
+ for (const value of Object.values(metadata)) {
15503
+ console.error(
15504
+ escapeControlChars(typeof value === "string" ? value : JSON.stringify(value))
15505
+ );
15885
15506
  }
15507
+ printWarning(warning);
15886
15508
  }
15887
15509
  process.exit(error.exitCode);
15888
15510
  }
@@ -15912,6 +15534,9 @@ async function runCommand(handler, options, formatter) {
15912
15534
  }
15913
15535
  }
15914
15536
 
15537
+ // src/commands/shared/startHelpers.ts
15538
+ init_interrupt();
15539
+
15915
15540
  // src/ui/messages/session.ts
15916
15541
  init_formatting();
15917
15542
  init_commands();
@@ -16057,7 +15682,13 @@ async function launchDaemon() {
16057
15682
  });
16058
15683
  daemon.unref();
16059
15684
  await waitForDaemonReady(() => exited);
16060
- return { pid: daemon.pid, hasExited: () => exited };
15685
+ return {
15686
+ pid: daemon.pid,
15687
+ hasExited: () => exited,
15688
+ stop: () => {
15689
+ if (!exited) daemon.kill("SIGTERM");
15690
+ }
15691
+ };
16061
15692
  }
16062
15693
  var UNREACHABLE_DAEMON_EXIT_MS = 8e3;
16063
15694
  async function stopUnreachableDaemon() {
@@ -16190,6 +15821,7 @@ var COMMANDS = {
16190
15821
  dom_audit: defineCommand(),
16191
15822
  css_search: defineCommand(),
16192
15823
  dom_inspect: defineCommand(),
15824
+ dom_screenshot: defineCommand(),
16193
15825
  dom_wait: defineCommand()
16194
15826
  };
16195
15827
 
@@ -16272,27 +15904,58 @@ var SPAWNED_DAEMON_EXIT_WAIT_MS = 3e3;
16272
15904
  var DEFAULT_START_DEPS = {
16273
15905
  launch: launchDaemon,
16274
15906
  send: startSession,
15907
+ stop: stopSession,
16275
15908
  exitWaitMs: SPAWNED_DAEMON_EXIT_WAIT_MS
16276
15909
  };
16277
15910
  async function startSessionViaDaemon(url, options, telemetry) {
16278
- process.once("SIGINT", () => reportStartOutcome(interruptedOutcome("SIGINT"), options));
16279
- process.once("SIGTERM", () => reportStartOutcome(interruptedOutcome("SIGTERM"), options));
16280
- reportStartOutcome(await attemptStart(url, options, telemetry), options);
15911
+ const interrupt = new AbortController();
15912
+ for (const signal of ["SIGINT", "SIGTERM"]) {
15913
+ process.on(signal, () => {
15914
+ if (interrupt.signal.aborted) reportStartOutcome(interruptedOutcome(signal), options);
15915
+ interrupt.abort(signal);
15916
+ });
15917
+ }
15918
+ reportStartOutcome(
15919
+ await attemptStart(url, options, telemetry, DEFAULT_START_DEPS, interrupt.signal),
15920
+ options
15921
+ );
16281
15922
  }
16282
- async function attemptStart(url, options, telemetry, deps = DEFAULT_START_DEPS) {
15923
+ async function attemptStart(url, options, telemetry, deps = DEFAULT_START_DEPS, interrupt) {
16283
15924
  const spawned = [];
16284
15925
  const attempt = async () => {
16285
- const outcome2 = await requestSession(url, options, telemetry, deps);
15926
+ const outcome2 = await requestSession(url, options, telemetry, deps, interrupt);
16286
15927
  if (!outcome2.ok && outcome2.spawned) spawned.push(outcome2.spawned);
16287
15928
  return outcome2;
16288
15929
  };
16289
15930
  let outcome = await attempt();
16290
15931
  const deadline = Date.now() + SHUTDOWN_WAIT_MS;
16291
15932
  while (isShuttingDown(outcome) && Date.now() < deadline) {
16292
- await delay2(SHUTDOWN_POLL_MS);
15933
+ await delay2(SHUTDOWN_POLL_MS, interrupt);
16293
15934
  outcome = await attempt();
16294
15935
  }
16295
- return afterSpawnedDaemonExit(outcome, spawned, deps.exitWaitMs);
15936
+ return settleOutcome(outcome, spawned, deps, interrupt);
15937
+ }
15938
+ async function settleOutcome(outcome, spawned, deps, interrupt) {
15939
+ if (outcome.ok) {
15940
+ const { spawned: daemon, ...started } = outcome;
15941
+ return interrupt?.aborted ? cancelStartedSession(started.data, daemon, deps, interrupt) : started;
15942
+ }
15943
+ const failure = await afterSpawnedDaemonExit(outcome, spawned, deps.exitWaitMs);
15944
+ return interrupt?.aborted ? withInterruptExitCode(failure, interrupt) : failure;
15945
+ }
15946
+ function cancelStartedSession(data, spawned, deps, interrupt) {
15947
+ log11.debug("Interrupted as the session started; stopping it");
15948
+ deps.stop().catch((error) => {
15949
+ log11.debug(`Stopping the interrupted session failed: ${getErrorMessage(error)}`);
15950
+ });
15951
+ const daemon = spawned ?? {
15952
+ pid: data.daemonPid,
15953
+ hasExited: () => !isProcessAlive(data.daemonPid)
15954
+ };
15955
+ return afterSpawnedDaemonExit(cancelledOutcome(interrupt), [daemon], deps.exitWaitMs);
15956
+ }
15957
+ function withInterruptExitCode(outcome, interrupt) {
15958
+ return outcome.ok ? outcome : { ...outcome, exitCode: interruptExitCode(interruptSignal(interrupt)) };
16296
15959
  }
16297
15960
  async function afterSpawnedDaemonExit(outcome, spawned, waitMs) {
16298
15961
  if (outcome.ok) return outcome;
@@ -16320,15 +15983,18 @@ async function afterSpawnedDaemonExit(outcome, spawned, waitMs) {
16320
15983
  }
16321
15984
  function interruptedOutcome(signal) {
16322
15985
  const message = `Start cancelled (${signal === "SIGINT" ? "interrupted" : "terminated"})`;
16323
- const exitCode = signal === "SIGINT" ? EXIT_CODES.INTERRUPTED : EXIT_CODES.TERMINATED;
16324
- return { ok: false, error: message, human: message, exitCode };
15986
+ return { ok: false, error: message, human: message, exitCode: interruptExitCode(signal) };
15987
+ }
15988
+ function cancelledOutcome(interrupt) {
15989
+ return interruptedOutcome(interruptSignal(interrupt));
16325
15990
  }
16326
15991
  var SHUTDOWN_WAIT_MS = 15e3;
16327
15992
  var SHUTDOWN_POLL_MS = 200;
16328
15993
  function isShuttingDown(outcome) {
16329
15994
  return !outcome.ok && (outcome.retryable === true || outcome.errorCode === "SESSION_SHUTTING_DOWN" /* SESSION_SHUTTING_DOWN */);
16330
15995
  }
16331
- async function requestSession(url, options, telemetry, deps) {
15996
+ async function requestSession(url, options, telemetry, deps, interrupt) {
15997
+ if (interrupt?.aborted) return cancelledOutcome(interrupt);
16332
15998
  let spawned;
16333
15999
  try {
16334
16000
  spawned = await deps.launch();
@@ -16346,10 +16012,16 @@ async function requestSession(url, options, telemetry, deps) {
16346
16012
  const exitCode = error instanceof DaemonError ? error.exitCode : EXIT_CODES.SOFTWARE_ERROR;
16347
16013
  return { ok: false, error: message, human: genericError(message), exitCode };
16348
16014
  }
16349
- const outcome = await sendStart(url, options, telemetry, deps.send);
16350
- return !outcome.ok && outcome.daemonExiting && spawned ? { ...outcome, spawned } : outcome;
16015
+ if (interrupt?.aborted) return cancelBeforeRequest(spawned, interrupt);
16016
+ const outcome = await sendStart(url, options, telemetry, deps.send, interrupt);
16017
+ return spawned && (outcome.ok || outcome.daemonExiting) ? { ...outcome, spawned } : outcome;
16018
+ }
16019
+ function cancelBeforeRequest(spawned, interrupt) {
16020
+ if (!spawned) return cancelledOutcome(interrupt);
16021
+ spawned.stop();
16022
+ return { ...cancelledOutcome(interrupt), daemonExiting: true, spawned };
16351
16023
  }
16352
- async function sendStart(url, options, telemetry, send) {
16024
+ async function sendStart(url, options, telemetry, send, interrupt) {
16353
16025
  try {
16354
16026
  log11.debug("Connecting to daemon...");
16355
16027
  const response = await send(
@@ -16366,7 +16038,8 @@ async function sendStart(url, options, telemetry, send) {
16366
16038
  chromeFlags: options.chromeFlags,
16367
16039
  viewport: options.viewport,
16368
16040
  colorScheme: options.colorScheme
16369
- })
16041
+ }),
16042
+ interrupt
16370
16043
  );
16371
16044
  if (response.status === "error") {
16372
16045
  return { ...describeStartFailure(response, options), daemonExiting: true };
@@ -16382,6 +16055,7 @@ async function sendStart(url, options, telemetry, send) {
16382
16055
  }
16383
16056
  return { ok: true, data: response.data };
16384
16057
  } catch (error) {
16058
+ if (interrupt?.aborted) return { ...cancelledOutcome(interrupt), daemonExiting: true };
16385
16059
  if (error instanceof IPCTimeoutError) {
16386
16060
  const timeout = timeoutError(error);
16387
16061
  return {
@@ -16731,9 +16405,11 @@ var CDP_HTTP_TIMEOUT_MS = 5e3;
16731
16405
  async function probeDevToolsEndpoint(port, logger, options) {
16732
16406
  const url = `${options?.secure ? "https" : "http"}://${options?.host ?? HTTP_LOCALHOST}:${port}/json/version`;
16733
16407
  const timeoutMs = options?.timeoutMs ?? CDP_HTTP_TIMEOUT_MS;
16408
+ const timeout = AbortSignal.timeout(timeoutMs);
16409
+ const signal = options?.signal ? AbortSignal.any([timeout, options.signal]) : timeout;
16734
16410
  let response;
16735
16411
  try {
16736
- response = await fetch(url, { signal: AbortSignal.timeout(timeoutMs) });
16412
+ response = await fetch(url, { signal });
16737
16413
  } catch (error) {
16738
16414
  logger?.debug(`Chrome version request failed: ${getErrorMessage(error)} (${url})`);
16739
16415
  return { kind: "unreachable" };
@@ -17048,6 +16724,50 @@ function assertValidChromeWsUrl(url) {
17048
16724
  init_errors3();
17049
16725
  init_messages();
17050
16726
 
16727
+ // src/cdp/methodTarget.ts
16728
+ init_protocol();
16729
+ init_levenshtein();
16730
+ init_suggestions();
16731
+ var METHOD_NAME = /^[A-Za-z][A-Za-z0-9]*\.[A-Za-z][A-Za-z0-9]*$/;
16732
+ function typoLimit(name) {
16733
+ return name.length <= 5 ? 1 : 2;
16734
+ }
16735
+ function closeNames(input, candidates) {
16736
+ const lower = input.toLowerCase();
16737
+ return candidates.map((name) => ({ name, distance: levenshteinDistance(lower, name.toLowerCase()) })).filter(({ distance }) => distance > 0 && distance <= typoLimit(input)).sort((a, b) => a.distance - b.distance).map(({ name }) => name);
16738
+ }
16739
+ function closeMethods(domainName, methodName) {
16740
+ const commands = findDomain(domainName)?.commands ?? [];
16741
+ const exact = commands.find((c) => c.name.toLowerCase() === methodName.toLowerCase());
16742
+ const names = exact ? [exact.name] : closeNames(
16743
+ methodName,
16744
+ commands.map((c) => c.name)
16745
+ );
16746
+ return names.map((name) => `${domainName}.${name}`);
16747
+ }
16748
+ function resolveUnknownDomain(domainName, methodName) {
16749
+ const domains = closeNames(
16750
+ domainName,
16751
+ loadProtocol().domains.map((d) => d.domain)
16752
+ );
16753
+ const method = `${domainName}.${methodName}`;
16754
+ if (domains.length === 0) return { kind: "unlisted", method };
16755
+ return { kind: "typo", method, suggestions: domains.flatMap((d) => closeMethods(d, methodName)) };
16756
+ }
16757
+ function resolveMethodTarget(input) {
16758
+ if (!METHOD_NAME.test(input)) return { kind: "malformed" };
16759
+ const known = normalizeMethod(input);
16760
+ if (known) return { kind: "known", method: known };
16761
+ const [domainName = "", methodName = ""] = input.split(".");
16762
+ const domain = findDomain(domainName);
16763
+ if (!domain) return resolveUnknownDomain(domainName, methodName);
16764
+ const type = findType(domain.domain, methodName);
16765
+ if (type) return { kind: "type", name: `${domain.domain}.${type.id}` };
16766
+ const method = `${domain.domain}.${methodName}`;
16767
+ const suggestions = closeMethods(domain.domain, methodName);
16768
+ return suggestions.length > 0 ? { kind: "typo", method, suggestions } : { kind: "unlisted", method };
16769
+ }
16770
+
17051
16771
  // src/commands/cdp.ts
17052
16772
  init_protocol();
17053
16773
  init_schema();
@@ -17055,8 +16775,10 @@ init_errors3();
17055
16775
  init_messages();
17056
16776
  init_client();
17057
16777
 
16778
+ // src/connection/errors.ts
16779
+ init_exitCodes();
16780
+
17058
16781
  // src/runtime/dom/evalHelpers.ts
17059
- init_errors5();
17060
16782
  init_errors3();
17061
16783
  init_messages();
17062
16784
 
@@ -17090,11 +16812,7 @@ var WEBSOCKET_CONFIG = {
17090
16812
  noPongClosure: 1001
17091
16813
  };
17092
16814
 
17093
- // src/connection/cdp.ts
17094
- init_errors5();
17095
-
17096
16815
  // src/daemon/session/cdpSetup.ts
17097
- init_errors5();
17098
16816
  init_constants();
17099
16817
  init_errors3();
17100
16818
  init_messages();
@@ -17288,6 +17006,7 @@ function describeException(details) {
17288
17006
 
17289
17007
  // src/ui/formatters/cdp.ts
17290
17008
  init_formatting();
17009
+ init_commands();
17291
17010
  function firstSentence(description) {
17292
17011
  const [line = ""] = (description ?? "").split("\n");
17293
17012
  const end = line.indexOf(". ");
@@ -17306,6 +17025,16 @@ function columns(rows) {
17306
17025
  function typeText(field) {
17307
17026
  return field.items ? `${field.type}<${field.items}>` : field.type;
17308
17027
  }
17028
+ var MAX_ENUM_VALUES = 8;
17029
+ function expansionText(field) {
17030
+ const values = field.enum;
17031
+ if (!values || values.length === 0) {
17032
+ return field.refType && field.refType !== "object" ? ` (${field.refType})` : "";
17033
+ }
17034
+ const shown = values.slice(0, MAX_ENUM_VALUES);
17035
+ const more = values.length - shown.length;
17036
+ return ` (${[...shown, ...more > 0 ? [`\u2026 ${more} more`] : []].join("|")})`;
17037
+ }
17309
17038
  function formatCdpSearch(data) {
17310
17039
  if (data.count === 0) {
17311
17040
  return joinLines(`No CDP method matches "${data.query}"`, "List the domains: bdg cdp --list");
@@ -17342,13 +17071,37 @@ function fieldSection(title, fields) {
17342
17071
  `${title}:`,
17343
17072
  ...columns(
17344
17073
  fields.map((f) => [
17345
- `${f.name}${f.optional ? "?" : ""}: ${typeText(f)}`,
17346
- (f.description ?? "").replace(/\s*\n\s*/g, " ")
17074
+ `${f.name}${f.optional ? "?" : ""}: ${typeText(f)}${expansionText(f)}`,
17075
+ `${(f.description ?? "").replace(/\s*\n\s*/g, " ")}${tags(f)}`
17347
17076
  ])
17348
17077
  )
17349
17078
  ];
17350
17079
  }
17080
+ function parameterFields(parameters) {
17081
+ return parameters.map((p) => ({ ...p, optional: !p.required }));
17082
+ }
17083
+ function typeHint(parameters) {
17084
+ const ref = parameters.find((p) => p.refType === "object")?.ref;
17085
+ return ref && `Describe a type: bdg cdp ${ref} --describe`;
17086
+ }
17087
+ function redirectSection(redirect) {
17088
+ if (!redirect) return [];
17089
+ if (!redirect.resolved) return [cdpUnresolvedRedirectLine(redirect.method)];
17090
+ const title = cdpRedirectTitle(redirect.method);
17091
+ return redirect.parameters.length === 0 ? [title] : fieldSection(`${title}, with these parameters`, parameterFields(redirect.parameters));
17092
+ }
17093
+ function formatCdpType(data) {
17094
+ const properties = data.properties ?? [];
17095
+ return joinLines(
17096
+ `${data.name}: ${typeText({ name: data.id, type: data.baseType, items: data.items })}${tags(data)}`,
17097
+ data.description,
17098
+ data.enum && `Values: ${data.enum.join(", ")}`,
17099
+ ...fieldSection("Properties", parameterFields(properties)),
17100
+ typeHint(properties)
17101
+ );
17102
+ }
17351
17103
  function formatCdpDescription(data) {
17104
+ if (data.type === "type") return formatCdpType(data);
17352
17105
  if (data.type === "domain") {
17353
17106
  return joinLines(
17354
17107
  `${data.domain}: ${pluralize(data.commands, "method")}, ${pluralize(data.events, "event")}${tags(data)}`,
@@ -17357,15 +17110,15 @@ function formatCdpDescription(data) {
17357
17110
  data.nextStep
17358
17111
  );
17359
17112
  }
17113
+ const redirected = data.redirect?.parameters ?? [];
17360
17114
  return joinLines(
17361
17115
  `${data.name}${tags(data)}`,
17362
17116
  data.description,
17363
- ...fieldSection(
17364
- "Parameters",
17365
- data.parameters.map((p) => ({ ...p, optional: !p.required }))
17366
- ),
17117
+ ...fieldSection("Parameters", parameterFields(data.parameters)),
17118
+ ...redirectSection(data.redirect),
17367
17119
  ...fieldSection("Returns", data.returns),
17368
17120
  data.note && `Note: ${data.note}`,
17121
+ typeHint([...data.parameters, ...redirected]),
17369
17122
  data.example && `Example: ${data.example.command}`
17370
17123
  );
17371
17124
  }
@@ -17376,6 +17129,9 @@ function formatCdpResult(data) {
17376
17129
  return isEmptyCdpResult(data.result) ? `${data.method}: done (no result data)` : JSON.stringify(data.result, null, 2);
17377
17130
  }
17378
17131
 
17132
+ // src/commands/cdp.ts
17133
+ init_commands();
17134
+
17379
17135
  // src/ui/messages/hints.ts
17380
17136
  init_formatting();
17381
17137
  init_sessionCommand();
@@ -17429,13 +17185,18 @@ function getMethodHint(methodName, result) {
17429
17185
  }
17430
17186
  function registerCdpCommand(program2) {
17431
17187
  program2.command("cdp").summary("CDP protocol introspection and execution").description(
17432
- "CDP protocol introspection and execution\n Discovery: --list, --search, --describe\n Execution: case-insensitive (network.getcookies works)"
17188
+ "CDP protocol introspection and execution\n Discovery: --list, --search, --describe\n" + CDP_EXECUTION_HELP
17433
17189
  ).argument("[method]", "CDP method name (e.g., Network.getCookies, network.getcookies)").addOption(new Option("--params <json>", "Method parameters as JSON")).addOption(
17434
17190
  new Option("--list", "List all domains or methods in a domain").conflicts([
17435
17191
  "describe",
17436
17192
  "params"
17437
17193
  ])
17438
17194
  ).addOption(new Option("--describe", "Show method signature and parameters").conflicts("params")).addOption(
17195
+ new Option(
17196
+ "--send-anyway",
17197
+ "Send a Domain.method that looks like a typo of a bundled one as typed"
17198
+ ).conflicts(["list", "describe", "search"])
17199
+ ).addOption(
17439
17200
  new Option("--search <query>", "Search methods by keyword").conflicts([
17440
17201
  "list",
17441
17202
  "describe",
@@ -17459,11 +17220,7 @@ async function runCdpCommand(method, options) {
17459
17220
  return runCommand(async () => handleDescribeMethod(method), options, formatCdpDescription);
17460
17221
  }
17461
17222
  if (method) {
17462
- return runCommand(
17463
- async () => handleExecuteMethod(method, options.params),
17464
- options,
17465
- formatCdpResult
17466
- );
17223
+ return runCommand(async () => handleExecuteMethod(method, options), options, formatCdpResult);
17467
17224
  }
17468
17225
  return runCommand(async () => {
17469
17226
  const err = missingArgumentError(CDP_USAGE);
@@ -17617,94 +17374,97 @@ function handleListDomainMethods(domainName) {
17617
17374
  }
17618
17375
  };
17619
17376
  }
17620
- function handleDescribeMethod(methodName) {
17621
- const [domainName, method] = methodName.includes(".") ? methodName.split(".") : [methodName, void 0];
17622
- if (!method) {
17623
- const summary = getDomainSummary(domainName);
17624
- if (!summary) {
17625
- const similar = findSimilarMethods(methodName);
17626
- const suggestions = ["Use: bdg cdp --list (to see all domains)"];
17627
- if (similar.length > 0) {
17628
- suggestions.push("");
17629
- suggestions.push("Did you mean:");
17630
- similar.forEach((name) => suggestions.push(` - ${name}`));
17631
- }
17632
- return {
17633
- success: false,
17634
- error: `Domain or method '${methodName}' not found`,
17635
- exitCode: EXIT_CODES.INVALID_ARGUMENTS,
17636
- errorContext: {
17637
- suggestion: suggestions.join("\n")
17638
- }
17639
- };
17640
- }
17641
- const domainNote = DOMAIN_NOTES[summary.name];
17642
- return {
17643
- success: true,
17644
- data: {
17645
- type: "domain",
17646
- domain: summary.name,
17647
- description: summary.description,
17648
- commands: summary.commandCount,
17649
- events: summary.eventCount,
17650
- experimental: summary.experimental,
17651
- deprecated: summary.deprecated,
17652
- note: domainNote,
17653
- nextStep: `Use: bdg cdp ${summary.name} --list (to see all methods)`
17654
- }
17655
- };
17656
- }
17657
- const schema = getMethodSchema(domainName, method);
17658
- if (!schema) {
17659
- const similar = findSimilarMethods(methodName, domainName);
17660
- const suggestions = [`Use: bdg cdp ${domainName} --list (to see all ${domainName} methods)`];
17661
- if (similar.length > 0) {
17662
- suggestions.push("");
17663
- suggestions.push("Did you mean:");
17664
- similar.forEach((name) => suggestions.push(` - ${name}`));
17665
- }
17377
+ function handleDescribeMethod(name) {
17378
+ const [domainName = "", member] = name.includes(".") ? name.split(".") : [name, void 0];
17379
+ if (!member) return describeDomain(name);
17380
+ const schema = getMethodSchema(domainName, member);
17381
+ if (schema) return { success: true, data: describeMethod(schema) };
17382
+ const type = getTypeSchema(domainName, member);
17383
+ if (type) return { success: true, data: { type: "type", ...type } };
17384
+ const target = resolveMethodTarget(name);
17385
+ const err = target.kind === "unlisted" ? cdpMethodNotInBundledProtocolError(target.method, getBundledProtocolVersion()) : cdpMethodNotFoundError(
17386
+ name,
17387
+ findSimilarMethods(name, domainName),
17388
+ `Use: bdg cdp ${domainName} --list (to see all ${domainName} methods)`
17389
+ );
17390
+ return {
17391
+ success: false,
17392
+ error: err.message,
17393
+ exitCode: EXIT_CODES.INVALID_ARGUMENTS,
17394
+ errorContext: { suggestion: err.suggestion }
17395
+ };
17396
+ }
17397
+ function describeDomain(domainName) {
17398
+ const summary = getDomainSummary(domainName);
17399
+ if (!summary) {
17400
+ const err = cdpMethodNotFoundError(
17401
+ domainName,
17402
+ findSimilarMethods(domainName),
17403
+ "Use: bdg cdp --list (to see all domains)"
17404
+ );
17666
17405
  return {
17667
17406
  success: false,
17668
- error: `Method '${methodName}' not found`,
17407
+ error: `Domain or method '${domainName}' not found`,
17669
17408
  exitCode: EXIT_CODES.INVALID_ARGUMENTS,
17670
- errorContext: {
17671
- suggestion: suggestions.join("\n")
17672
- }
17409
+ errorContext: { suggestion: err.suggestion }
17673
17410
  };
17674
17411
  }
17675
- const methodNote = METHOD_NOTES[schema.name] ?? DOMAIN_NOTES[schema.domain];
17676
- const alternative = blockedAlternative(schema.name);
17677
17412
  return {
17678
17413
  success: true,
17679
17414
  data: {
17680
- type: "method",
17681
- name: schema.name,
17682
- domain: schema.domain,
17683
- method: schema.method,
17684
- description: schema.description,
17685
- experimental: schema.experimental,
17686
- deprecated: schema.deprecated,
17687
- note: methodNote,
17688
- parameters: schema.parameters.map((p) => ({
17689
- name: p.name,
17690
- type: p.type,
17691
- required: p.required,
17692
- description: p.description,
17693
- enum: p.enum,
17694
- items: p.items,
17695
- deprecated: p.deprecated
17696
- })),
17697
- returns: schema.returns.map((r) => ({
17698
- name: r.name,
17699
- type: r.type,
17700
- optional: r.optional,
17701
- description: r.description,
17702
- items: r.items
17703
- })),
17704
- example: alternative ? { command: alternative } : schema.example
17415
+ type: "domain",
17416
+ domain: summary.name,
17417
+ description: summary.description,
17418
+ commands: summary.commandCount,
17419
+ events: summary.eventCount,
17420
+ experimental: summary.experimental,
17421
+ deprecated: summary.deprecated,
17422
+ note: DOMAIN_NOTES[summary.name],
17423
+ nextStep: `Use: bdg cdp ${summary.name} --list (to see all methods)`
17705
17424
  }
17706
17425
  };
17707
17426
  }
17427
+ function describeParameter(p) {
17428
+ return {
17429
+ name: p.name,
17430
+ type: p.type,
17431
+ required: p.required,
17432
+ description: p.description,
17433
+ enum: p.enum,
17434
+ ref: p.ref,
17435
+ refType: p.refType,
17436
+ items: p.items,
17437
+ experimental: p.experimental,
17438
+ deprecated: p.deprecated
17439
+ };
17440
+ }
17441
+ function describeMethod(schema) {
17442
+ const alternative = blockedAlternative(schema.name);
17443
+ return {
17444
+ type: "method",
17445
+ name: schema.name,
17446
+ domain: schema.domain,
17447
+ method: schema.method,
17448
+ description: schema.description,
17449
+ experimental: schema.experimental,
17450
+ deprecated: schema.deprecated,
17451
+ note: METHOD_NOTES[schema.name] ?? DOMAIN_NOTES[schema.domain],
17452
+ parameters: schema.parameters.map(describeParameter),
17453
+ returns: schema.returns.map((r) => ({
17454
+ name: r.name,
17455
+ type: r.type,
17456
+ optional: r.optional,
17457
+ description: r.description,
17458
+ items: r.items
17459
+ })),
17460
+ redirect: schema.redirect && {
17461
+ method: schema.redirect.method,
17462
+ resolved: schema.redirect.resolved,
17463
+ parameters: schema.redirect.parameters.map(describeParameter)
17464
+ },
17465
+ example: alternative ? { command: alternative } : schema.example
17466
+ };
17467
+ }
17708
17468
  var BLOCKED_CDP_METHODS = {
17709
17469
  "Page.captureScreenshot": {
17710
17470
  alternative: "bdg dom screenshot [path]",
@@ -17719,38 +17479,53 @@ var BLOCKED_CDP_METHODS = {
17719
17479
  reason: "Closes Chrome under the session, which then ends without saving its state"
17720
17480
  }
17721
17481
  };
17722
- async function handleExecuteMethod(methodName, paramsJson) {
17723
- const normalized = normalizeMethod(methodName);
17724
- if (normalized && BLOCKED_CDP_METHODS[normalized]) {
17725
- const blocked = BLOCKED_CDP_METHODS[normalized];
17726
- return {
17727
- success: false,
17728
- error: `${normalized} is blocked via raw CDP: ${blocked.reason}`,
17729
- exitCode: EXIT_CODES.INVALID_ARGUMENTS,
17730
- errorContext: { suggestion: `Use: ${sessionCommand(blocked.alternative)}` }
17731
- };
17732
- }
17733
- if (!normalized) {
17734
- const similar = findSimilarMethods(methodName);
17735
- const suggestions = ["Use: bdg cdp --search <keyword> (to search for methods)"];
17736
- if (similar.length > 0) {
17737
- suggestions.push("");
17738
- suggestions.push("Did you mean:");
17739
- similar.forEach((name) => suggestions.push(` - ${name}`));
17482
+ function methodToSend(methodName, options = {}) {
17483
+ const target = resolveMethodTarget(methodName);
17484
+ if (target.kind === "known") {
17485
+ const blocked = BLOCKED_CDP_METHODS[target.method];
17486
+ if (blocked) {
17487
+ throw new CommandError(
17488
+ `${target.method} is blocked via raw CDP: ${blocked.reason}`,
17489
+ { suggestion: `Use: ${sessionCommand(blocked.alternative)}` },
17490
+ EXIT_CODES.INVALID_ARGUMENTS
17491
+ );
17740
17492
  }
17493
+ return { method: target.method };
17494
+ }
17495
+ if (target.kind === "unlisted" || target.kind === "typo" && options.sendAnyway) {
17741
17496
  return {
17742
- success: false,
17743
- error: `Method '${methodName}' not found`,
17744
- exitCode: EXIT_CODES.INVALID_ARGUMENTS,
17745
- errorContext: {
17746
- suggestion: suggestions.join("\n")
17747
- }
17497
+ method: target.method,
17498
+ warning: cdpUnlistedMethodWarning(target.method, getBundledProtocolVersion())
17748
17499
  };
17749
17500
  }
17501
+ const err = target.kind === "type" ? cdpTypeNotMethodError(target.name) : unknownMethodError(methodName, target);
17502
+ throw new CommandError(err.message, { suggestion: err.suggestion }, EXIT_CODES.INVALID_ARGUMENTS);
17503
+ }
17504
+ function unknownMethodError(methodName, target) {
17505
+ if (target.kind === "malformed") {
17506
+ return cdpMethodNotFoundError(
17507
+ methodName,
17508
+ findSimilarMethods(methodName),
17509
+ "Use: bdg cdp --search <keyword> (to search for methods)"
17510
+ );
17511
+ }
17512
+ const [domainName = ""] = methodName.split(".");
17513
+ return target.suggestions.length > 0 ? cdpMethodTypoError(
17514
+ methodName,
17515
+ target.suggestions.slice(0, 3),
17516
+ "Use: bdg cdp --search <keyword> (to search for methods)"
17517
+ ) : cdpMethodTypoError(methodName, [], domainSuggestion(domainName));
17518
+ }
17519
+ function withWarning(error, warning) {
17520
+ if (!warning || !(error instanceof CommandError)) return error;
17521
+ return new CommandError(error.message, { ...error.metadata, warning }, error.exitCode);
17522
+ }
17523
+ async function handleExecuteMethod(methodName, options) {
17524
+ const { method: normalized, warning } = methodToSend(methodName, options);
17750
17525
  let params;
17751
- if (paramsJson) {
17526
+ if (options.params) {
17752
17527
  try {
17753
- params = JSON.parse(paramsJson);
17528
+ params = JSON.parse(options.params);
17754
17529
  } catch (error) {
17755
17530
  return {
17756
17531
  success: false,
@@ -17758,31 +17533,31 @@ async function handleExecuteMethod(methodName, paramsJson) {
17758
17533
  exitCode: EXIT_CODES.INVALID_ARGUMENTS,
17759
17534
  errorContext: {
17760
17535
  suggestion: `Use: bdg cdp ${normalized} --describe (to see parameter schema)`
17761
- }
17536
+ },
17537
+ ...warning && { warning }
17762
17538
  };
17763
17539
  }
17764
17540
  }
17765
17541
  const response = await callCDP(normalized, params);
17766
- validateIPCResponse(response);
17767
- const cdpResult = response.data?.result;
17542
+ try {
17543
+ validateIPCResponse(response);
17544
+ } catch (error) {
17545
+ throw withWarning(error, warning);
17546
+ }
17547
+ const ipcHint = response.data?.hint && formatHint(response.data.hint);
17548
+ return cdpCallResult(normalized, response.data?.result, warning, ipcHint);
17549
+ }
17550
+ function cdpCallResult(method, cdpResult, warning, ipcHint) {
17551
+ const withCallWarning = warning ? { warning } : {};
17768
17552
  const exception = pageExceptionResult(cdpResult);
17769
- if (exception) return exception;
17770
- const result = {
17553
+ if (exception) return { ...exception, ...withCallWarning };
17554
+ const hints = [ipcHint, getMethodHint(method, cdpResult)].filter(Boolean);
17555
+ return {
17771
17556
  success: true,
17772
- data: {
17773
- method: normalized,
17774
- result: cdpResult
17775
- }
17557
+ data: { method, result: cdpResult },
17558
+ ...withCallWarning,
17559
+ ...hints.length > 0 && { hint: hints.join("\n") }
17776
17560
  };
17777
- if (response.data?.hint) {
17778
- result.hint = formatHint(response.data.hint);
17779
- }
17780
- const methodHint = getMethodHint(normalized, cdpResult);
17781
- if (methodHint) {
17782
- result.hint = result.hint ? `${result.hint}
17783
- ${methodHint}` : methodHint;
17784
- }
17785
- return result;
17786
17561
  }
17787
17562
 
17788
17563
  // src/commands/cleanup.ts
@@ -17885,6 +17660,7 @@ function formatCleanup(data) {
17885
17660
  return joinLines(
17886
17661
  cleaned.session && sessionFilesCleanedMessage(),
17887
17662
  cleaned.output && sessionOutputRemovedMessage(),
17663
+ data.downloadsKept && downloadsKeptMessage(data.downloadsKept.dir, data.downloadsKept.files),
17888
17664
  ...(data.warnings ?? []).map((warning) => warningMessage(warning)),
17889
17665
  "",
17890
17666
  data.message
@@ -17973,18 +17749,26 @@ async function cleanupSession(opts) {
17973
17749
  };
17974
17750
  }
17975
17751
  const didCleanup = Object.values(cleaned).some(Boolean) || purged !== void 0;
17752
+ const downloadsKept = purged === void 0 ? keptDownloads() : void 0;
17976
17753
  return {
17977
17754
  success: true,
17978
17755
  data: {
17979
17756
  cleaned,
17980
17757
  ...purged !== void 0 && { purged },
17758
+ ...downloadsKept && { downloadsKept },
17981
17759
  message: !didCleanup ? noSessionFilesMessage() : purged !== void 0 ? sessionDirectoryPurgedMessage(purged) : sessionDirectoryCleanMessage(),
17982
17760
  ...warnings.length > 0 && { warnings }
17983
17761
  }
17984
17762
  };
17985
17763
  }
17764
+ function keptDownloads() {
17765
+ const dir = getSessionDownloadsDir();
17766
+ if (!fs14.existsSync(dir) || !fs14.statSync(dir).isDirectory()) return void 0;
17767
+ const files = fs14.readdirSync(dir).length;
17768
+ return files > 0 ? { dir, files } : void 0;
17769
+ }
17986
17770
  function registerCleanupCommand(program2) {
17987
- program2.command("cleanup").description("Clean up stale session files").option("-f, --force", "Kill a running (possibly hung) session, then clean up", false).option("--remove-output", "Also remove session.json output file", false).option("--aggressive", "Alias for --force (kept for compatibility)", false).option(
17771
+ program2.command("cleanup").description("Clean up stale session files (downloaded files are kept)").option("-f, --force", "Kill a running (possibly hung) session, then clean up", false).option("--remove-output", "Also remove session.json output file", false).option("--aggressive", "Alias for --force (kept for compatibility)", false).option(
17988
17772
  "--purge",
17989
17773
  "Also delete a named session's directory (Chrome profile, logs, port); needs --session",
17990
17774
  false
@@ -18901,7 +18685,11 @@ function megabytes(bytes) {
18901
18685
  function networkEvictedNote(counts) {
18902
18686
  const { requestsDropped, bodiesEvicted } = counts;
18903
18687
  const requests = pluralize(requestsDropped, "older network request");
18904
- const bodies = pluralize(bodiesEvicted, "older response body", "older response bodies");
18688
+ const bodies = pluralize(
18689
+ bodiesEvicted,
18690
+ "older request/response body",
18691
+ "older request/response bodies"
18692
+ );
18905
18693
  const budget = megabytes(MAX_TOTAL_BODY_BYTES);
18906
18694
  if (requestsDropped > 0 && bodiesEvicted > 0) {
18907
18695
  return `\u26A0 ${requests} dropped, ${bodies} evicted: bdg keeps the newest ${MAX_NETWORK_REQUESTS} requests and ${budget} of bodies`;
@@ -18927,11 +18715,11 @@ function localProxyNote() {
18927
18715
  return "(loopback; likely a local proxy)";
18928
18716
  }
18929
18717
  function harSanitizedComment() {
18930
- 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";
18718
+ 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";
18931
18719
  }
18932
18720
  function harExportedMessage(result) {
18933
18721
  const filterNote = result.filtered ? " (filtered)" : "";
18934
- const note = result.sanitized ? "Credentials sanitized (auth/cookie/API key/token headers, cookies, URL tokens, password and token body fields are [redacted]); --include-sensitive keeps them" : "\u26A0 Includes credentials (--include-sensitive): share this file with care";
18722
+ const note = result.sanitized ? "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" : "\u26A0 Includes credentials (--include-sensitive): share this file with care";
18935
18723
  return `\u2713 Exported ${result.entries} requests${filterNote} to ${result.file}
18936
18724
  ${note}`;
18937
18725
  }
@@ -19047,10 +18835,6 @@ function addWebSocketMessages(fmt, webSocket) {
19047
18835
  webSocket.frames.forEach((frame) => fmt.text(formatWebSocketMessage(frame)));
19048
18836
  }
19049
18837
  var BODY_PREVIEW_LENGTH = 2e4;
19050
- function formatBytes(bytes) {
19051
- if (bytes < 1024) return `${bytes} B`;
19052
- return bytes < 1024 * 1024 ? `${(bytes / 1024).toFixed(1)} KB` : `${(bytes / 1024 / 1024).toFixed(1)} MB`;
19053
- }
19054
18838
  function isLoopback(ip) {
19055
18839
  const bare = ip.replace(/^\[|\]$/g, "").toLowerCase();
19056
18840
  return /^(::ffff:)?127\./.test(bare) || bare === "::1";
@@ -19126,7 +18910,11 @@ function formatNetworkDetails(request) {
19126
18910
  fmt.keyValueList(requestSummaryRows(request));
19127
18911
  fmt.blank();
19128
18912
  if (request.requestHeaders) addHeaders(fmt, "Request Headers:", request.requestHeaders);
19129
- if (request.requestBody) {
18913
+ if (request.requestBodyNotCaptured) {
18914
+ fmt.text("Request Body:").separator("\u2501", 70);
18915
+ fmt.text(`(not captured: ${request.requestBodyNotCaptured})`);
18916
+ fmt.blank();
18917
+ } else if (request.requestBody) {
19130
18918
  fmt.text("Request Body:").separator("\u2501", 70);
19131
18919
  fmt.text(request.requestBody);
19132
18920
  fmt.blank();
@@ -19904,6 +19692,8 @@ function formatActionOutput(done, details, result, options = {}) {
19904
19692
  if (requests.length > 0) {
19905
19693
  fmt.blank().section(formatTriggeredRequestsTitle(result.triggeredRequests ?? [], omitted), requests);
19906
19694
  }
19695
+ if (result.downloads?.length) fmt.blank();
19696
+ for (const download of result.downloads ?? []) fmt.text(downloadText(download));
19907
19697
  for (const dialog of result.dialogs ?? []) {
19908
19698
  fmt.blank();
19909
19699
  fmt.text(`Dialog: ${dialogConsoleText(dialog)}`);
@@ -20236,7 +20026,7 @@ init_elementInfo();
20236
20026
  init_frameScopedConnection();
20237
20027
  init_logging();
20238
20028
  init_errors();
20239
- var log27 = createLogger("dom");
20029
+ var log24 = createLogger("dom");
20240
20030
  function childFrameIds(tree) {
20241
20031
  return (tree?.childFrames ?? []).flatMap((child) => [child.frame.id, ...childFrameIds(child)]);
20242
20032
  }
@@ -21439,7 +21229,7 @@ function formatFormDiscovery(result) {
21439
21229
  init_logging();
21440
21230
  init_errors();
21441
21231
  init_exitCodes();
21442
- var log28 = createLogger("dom");
21232
+ var log25 = createLogger("dom");
21443
21233
  function buildValidation(raw) {
21444
21234
  const hasNativeError = !raw.isValid && !raw.valueMissing && raw.validationMessage;
21445
21235
  const hasAriaError = raw.ariaInvalid;
@@ -21596,7 +21386,7 @@ async function cacheFormElements(forms, document2) {
21596
21386
  const elements = forms.flatMap((form) => [...form.fields, ...form.buttons]);
21597
21387
  const backendNodeIds = await resolveBackendNodeIds(elements.map((el) => el.selector)).catch(
21598
21388
  (error) => {
21599
- log28.debug(`Form fields not cached by node: ${getErrorMessage(error)}`);
21389
+ log25.debug(`Form fields not cached by node: ${getErrorMessage(error)}`);
21600
21390
  return [];
21601
21391
  }
21602
21392
  );
@@ -21612,7 +21402,7 @@ async function cacheFormElements(forms, document2) {
21612
21402
  },
21613
21403
  document2
21614
21404
  );
21615
- log28.debug(`Cached ${elements.length} form elements`);
21405
+ log25.debug(`Cached ${elements.length} form elements`);
21616
21406
  }
21617
21407
  async function handleFormCommand(options) {
21618
21408
  await runCommand(
@@ -22590,6 +22380,7 @@ function listedMatches(result, limit) {
22590
22380
  // src/commands/dom/screenshot.ts
22591
22381
  import { extname as extname2 } from "path";
22592
22382
  init_helpers();
22383
+ init_interrupt();
22593
22384
  init_outputFile();
22594
22385
  init_errors3();
22595
22386
  init_messages();
@@ -22598,7 +22389,7 @@ init_logging();
22598
22389
  init_directories();
22599
22390
  init_errors();
22600
22391
  init_exitCodes();
22601
- var log29 = createLogger("dom");
22392
+ var log26 = createLogger("dom");
22602
22393
  var EXTENSION_FORMATS = {
22603
22394
  ".png": "png",
22604
22395
  ".jpg": "jpeg",
@@ -22619,34 +22410,27 @@ function resolveImageFormat(outputPath, requested) {
22619
22410
  }
22620
22411
  return requested ?? fromExtension ?? "png";
22621
22412
  }
22622
- function buildPageScreenshotOptions(options) {
22623
- return filterDefined({
22624
- format: options.format,
22625
- quality: options.quality,
22626
- fullPage: options.fullPage,
22627
- noResize: options.resize === false,
22628
- scroll: options.scroll
22629
- });
22630
- }
22631
- function buildElementScreenshotOptions(options) {
22632
- return filterDefined({
22633
- format: options.format,
22634
- quality: options.quality,
22635
- noResize: options.resize === false,
22636
- padding: options.padding
22637
- });
22413
+ function buildScreenshotRequest(options, backendNodeId) {
22414
+ const shared = {
22415
+ format: options.format ?? "png",
22416
+ ...filterDefined({ quality: options.quality }),
22417
+ ...options.resize === false && { noResize: true }
22418
+ };
22419
+ if (backendNodeId !== void 0) {
22420
+ return { ...shared, backendNodeId, ...filterDefined({ padding: options.padding }) };
22421
+ }
22422
+ return { ...shared, ...filterDefined({ fullPage: options.fullPage, scroll: options.scroll }) };
22638
22423
  }
22639
22424
  function hasElementTarget(options) {
22640
22425
  return options.selector !== void 0 || options.index !== void 0;
22641
22426
  }
22642
22427
  async function resolveElementNodeId(options) {
22643
22428
  if (options.selector !== void 0 && options.index !== void 0) {
22644
- return { backendNodeId: await selectMatch(options.selector, options.index) };
22429
+ return selectMatch(options.selector, options.index);
22645
22430
  }
22646
22431
  if (options.index !== void 0) {
22647
- const resolver = DomElementResolver.getInstance();
22648
- const node = await resolver.getNodeIdForIndex(options.index);
22649
- return { backendNodeId: node.nodeId };
22432
+ const node = await DomElementResolver.getInstance().getNodeIdForIndex(options.index);
22433
+ return node.nodeId;
22650
22434
  }
22651
22435
  if (options.selector !== void 0) {
22652
22436
  return resolveSelector(options.selector);
@@ -22689,10 +22473,11 @@ function formatFrameFilename(frameNumber, format) {
22689
22473
  return `${String(frameNumber).padStart(3, "0")}.${format}`;
22690
22474
  }
22691
22475
  async function handlePageScreenshot(outputPath, options) {
22476
+ const interrupt = abortOnInterrupt();
22692
22477
  await runCommand(
22693
22478
  async () => {
22694
- const screenshotOptions = buildPageScreenshotOptions(options);
22695
- const result = await capturePageScreenshot(outputPath, screenshotOptions);
22479
+ const request = buildScreenshotRequest(options);
22480
+ const result = await captureScreenshot(outputPath, request, interrupt);
22696
22481
  return { success: true, data: result };
22697
22482
  },
22698
22483
  options,
@@ -22700,12 +22485,14 @@ async function handlePageScreenshot(outputPath, options) {
22700
22485
  );
22701
22486
  }
22702
22487
  async function handleElementScreenshot(outputPath, options) {
22488
+ const interrupt = abortOnInterrupt();
22703
22489
  await runCommand(
22704
22490
  async () => {
22705
- const nodeRef = await resolveElementNodeId(options);
22706
- const screenshotOptions = buildElementScreenshotOptions(options);
22707
- const result = await captureElementScreenshot(outputPath, nodeRef, screenshotOptions);
22708
- const elementResult = addElementInfo(result, options);
22491
+ const lookup = resolveElementNodeId(options);
22492
+ const backendNodeId = await unlessInterrupted(lookup, interrupt, screenshotInterrupted);
22493
+ const request = buildScreenshotRequest(options, backendNodeId);
22494
+ const shot = await captureScreenshot(outputPath, request, interrupt);
22495
+ const elementResult = addElementInfo(shot, options);
22709
22496
  return { success: true, data: elementResult };
22710
22497
  },
22711
22498
  options,
@@ -22713,14 +22500,8 @@ async function handleElementScreenshot(outputPath, options) {
22713
22500
  );
22714
22501
  }
22715
22502
  async function captureSequenceFrame(outputPath, options) {
22716
- if (hasElementTarget(options)) {
22717
- const nodeRef = await resolveElementNodeId(options);
22718
- const elementOptions = buildElementScreenshotOptions(options);
22719
- await captureElementScreenshot(outputPath, nodeRef, elementOptions);
22720
- } else {
22721
- const pageOptions = buildPageScreenshotOptions(options);
22722
- await capturePageScreenshot(outputPath, pageOptions);
22723
- }
22503
+ const backendNodeId = hasElementTarget(options) ? await resolveElementNodeId(options) : void 0;
22504
+ await captureScreenshot(outputPath, buildScreenshotRequest(options, backendNodeId));
22724
22505
  }
22725
22506
  async function handleSequenceCapture(outputDir, options) {
22726
22507
  const fs19 = await import("fs");
@@ -22750,7 +22531,7 @@ async function handleSequenceCapture(outputDir, options) {
22750
22531
  if (options.json) {
22751
22532
  console.log(JSON.stringify(buildSuccessResponse({ frame: frameCount, path: outputPath })));
22752
22533
  } else {
22753
- log29.info(`Frame ${frameCount}: ${filename}`);
22534
+ log26.info(`Frame ${frameCount}: ${filename}`);
22754
22535
  }
22755
22536
  if (limit > 0 && frameCount >= limit) break;
22756
22537
  await delay2(interval);
@@ -22977,7 +22758,7 @@ function formatInstalledSkills(data) {
22977
22758
  init_logging();
22978
22759
  init_errors();
22979
22760
  init_exitCodes();
22980
- var log30 = createLogger("bdg");
22761
+ var log27 = createLogger("bdg");
22981
22762
  var SKILL_SOURCE_PATH = join8(PACKAGE_ROOT, ".claude", "skills", "bdg", "SKILL.md");
22982
22763
  var SKILL_ROOTS = {
22983
22764
  claude: join8(".claude", "skills"),
@@ -23039,7 +22820,7 @@ function writeSkill(target, path8, content) {
23039
22820
  writing = path8;
23040
22821
  renameSync2(temporary, path8);
23041
22822
  } catch (error) {
23042
- safeRemoveFile(temporary, "temporary skill copy", log30);
22823
+ safeRemoveFile(temporary, "temporary skill copy", log27);
23043
22824
  return { target, path: writing, error };
23044
22825
  }
23045
22826
  if (backup === void 0) return { target, path: path8, status: "installed" };
@@ -23194,7 +22975,7 @@ function filterByResourceType(requests, types) {
23194
22975
  init_logging();
23195
22976
  init_suggestions();
23196
22977
  init_url();
23197
- var log31 = createLogger("network");
22978
+ var log28 = createLogger("network");
23198
22979
  var SIZE_UNITS = {
23199
22980
  b: 1,
23200
22981
  kb: 1024,
@@ -23304,7 +23085,7 @@ function validateSizeValue(value) {
23304
23085
  parseSize(value);
23305
23086
  return null;
23306
23087
  } catch (error) {
23307
- log31.debug(
23088
+ log28.debug(
23308
23089
  `Size parse failed for "${value}": ${error instanceof Error ? error.message : String(error)}`
23309
23090
  );
23310
23091
  return createError(
@@ -23495,35 +23276,84 @@ import { createRequire as createRequire2 } from "node:module";
23495
23276
  init_elementInfo();
23496
23277
  init_logging();
23497
23278
  init_errors();
23498
- var log32 = createLogger("network");
23279
+ var log29 = createLogger("network");
23499
23280
  var REDACTED = "[redacted]";
23500
23281
  var SENSITIVE_FIELD = new RegExp(
23501
- `${SENSITIVE_NAME_SOURCE}|token|secret|api[-_]?key|authoriz|credential|jwt|private[-_]?key|access[-_]?key|session|signature`,
23282
+ `${SENSITIVE_NAME_SOURCE}|token|secret|api[-_]?key|authoriz|credential|jwt|private[-_]?key|access[-_]?key|session|signature|bearer|cookie|csrf|xsrf|refresh|oauth|(^|[^a-z])(auth|sid)([^a-z]|$)|auth[-_]?code|code[-_]?verifier`,
23502
23283
  "i"
23503
23284
  );
23504
- var FORM_BODY = /^[^\s=&{["]+=[^\s&]*(?:&[^\s=&]+=[^\s&]*)*$/;
23285
+ var FORM_BODY = /^(?!\[)[^\s=&{"]+=[^\s&]*(?:&[^\s=&]+=[^\s&]*)*$/;
23286
+ var JSON_LIKE_START = /^\uFEFF?(?:\)\]\}',?)?\s*(?:(?:\d+:)?(?:\d*-?(?:\/[^,]*,)?|[ac])[[{]|(?:data|event|id|retry):)/;
23287
+ var JSON_MIME = /json|event-stream/i;
23288
+ var DECODABLE_MIME = /^(?:application\/octet-stream|binary\/octet-stream|application\/x-www-form-urlencoded|text\/event-stream)?$|json/i;
23289
+ var BARE = /[^\s,:[\]{}"]+/y;
23290
+ var DOT = 46;
23291
+ var MAX_ENCODED_DEPTH = 3;
23292
+ var REDACTED_STRING = JSON.stringify(REDACTED);
23293
+ var UTF8 = new TextDecoder("utf-8", { fatal: true, ignoreBOM: true });
23505
23294
  function isSensitiveField2(name) {
23506
- return SENSITIVE_FIELD.test(name);
23295
+ return SENSITIVE_FIELD.test(name.replace(/([a-z])([A-Z])/g, "$1_$2"));
23296
+ }
23297
+ function redactOrReplaceWhole(text, redact, whole = REDACTED) {
23298
+ try {
23299
+ return redact(text);
23300
+ } catch (error) {
23301
+ log29.debug(`Body replaced whole, sanitizing failed: ${getErrorMessage(error)}`);
23302
+ return whole;
23303
+ }
23304
+ }
23305
+ function redactJwts(text, replacement) {
23306
+ if (!text.includes("eyJ")) return text;
23307
+ const parts = [];
23308
+ let copied = 0;
23309
+ let at = text.indexOf("eyJ");
23310
+ while (at !== -1) {
23311
+ const end = isJwtBoundary(text, at - 1) ? jwtEnd(text, at) : void 0;
23312
+ if (end !== void 0 && isJwtBoundary(text, end)) {
23313
+ parts.push(text.slice(copied, at), replacement);
23314
+ copied = end;
23315
+ }
23316
+ at = text.indexOf("eyJ", Math.max(at + 3, copied));
23317
+ }
23318
+ if (parts.length === 0) return text;
23319
+ parts.push(text.slice(copied));
23320
+ return parts.join("");
23507
23321
  }
23508
23322
  function redactPairs(text, isSensitive, replacement) {
23509
23323
  return text.split("&").map((pair) => {
23510
23324
  const eq = pair.indexOf("=");
23511
- if (eq === -1 || !isSensitive(decodeName(pair.slice(0, eq)))) return pair;
23512
- return `${pair.slice(0, eq)}=${replacement}`;
23325
+ if (eq === -1) return pair;
23326
+ if (isSensitive(decodeName(pair.slice(0, eq)))) return `${pair.slice(0, eq)}=${replacement}`;
23327
+ const value = pair.slice(eq + 1);
23328
+ const redacted = redactJwts(value, replacement);
23329
+ return redacted === value ? pair : `${pair.slice(0, eq + 1)}${redacted}`;
23513
23330
  }).join("&");
23514
23331
  }
23515
- function redactRequestBody(text, mimeType) {
23332
+ function redactBody(text, mimeType) {
23516
23333
  if (/multipart\/form-data/i.test(mimeType)) return redactMultipart(text, mimeType);
23517
- if (/x-www-form-urlencoded/i.test(mimeType) || FORM_BODY.test(text)) {
23518
- return redactPairs(text, isSensitiveField2, REDACTED);
23334
+ if (/x-www-form-urlencoded/i.test(mimeType)) return redactPairs(text, isSensitiveField2, REDACTED);
23335
+ if (JSON_LIKE_START.test(text)) return redactJsonText(text, 0);
23336
+ if (FORM_BODY.test(text)) return redactPairs(text, isSensitiveField2, REDACTED);
23337
+ if (JSON_MIME.test(mimeType)) return redactJsonText(text, 0);
23338
+ return redactJwts(text, REDACTED);
23339
+ }
23340
+ function redactBase64Body(base64, mimeType) {
23341
+ if (!DECODABLE_MIME.test(mimeType.split(";")[0]?.trim() ?? "")) return base64;
23342
+ let decoded;
23343
+ try {
23344
+ decoded = UTF8.decode(Buffer.from(base64, "base64"));
23345
+ } catch (error) {
23346
+ log29.debug(`Base64 body is not UTF-8: ${getErrorMessage(error)}`);
23347
+ return base64;
23519
23348
  }
23520
- return redactJsonBody(text);
23349
+ const redacted = redactBody(decoded, mimeType);
23350
+ return redacted === decoded ? base64 : Buffer.from(redacted, "utf8").toString("base64");
23521
23351
  }
23522
23352
  function decodeName(name) {
23523
23353
  try {
23524
23354
  return decodeURIComponent(name.replace(/\+/g, " "));
23525
23355
  } catch (error) {
23526
- log32.debug(`Field name not decodable: ${getErrorMessage(error)}`);
23356
+ log29.debug(`Field name not decodable: ${getErrorMessage(error)}`);
23527
23357
  return name;
23528
23358
  }
23529
23359
  }
@@ -23538,41 +23368,153 @@ function redactPart(part) {
23538
23368
  const headerEnd = part.indexOf("\r\n\r\n");
23539
23369
  if (headerEnd === -1) return part;
23540
23370
  const name = /;\s*name="([^"]*)"/i.exec(part.slice(0, headerEnd))?.[1];
23541
- if (name === void 0 || !isSensitiveField2(name)) return part;
23371
+ if (name === void 0 || !isSensitiveField2(name)) return redactJwts(part, REDACTED);
23542
23372
  const valueEnd = part.endsWith("\r\n") ? part.length - 2 : part.length;
23543
23373
  return `${part.slice(0, headerEnd + 4)}${REDACTED}${part.slice(valueEnd)}`;
23544
23374
  }
23545
- function redactJsonBody(text) {
23546
- if (!/^\s*[[{]/.test(text)) return text;
23547
- let parsed;
23548
- try {
23549
- parsed = JSON.parse(text);
23550
- } catch (error) {
23551
- log32.debug(`Request body not JSON: ${getErrorMessage(error)}`);
23552
- return error instanceof SyntaxError ? text : REDACTED;
23375
+ function redactJsonText(text, depth) {
23376
+ const scan2 = {
23377
+ text,
23378
+ parts: [],
23379
+ copied: 0,
23380
+ containers: [false],
23381
+ key: void 0,
23382
+ lineEnd: -1,
23383
+ depth
23384
+ };
23385
+ let index = 0;
23386
+ while (index < text.length) index = scanToken(scan2, index);
23387
+ if (scan2.parts.length === 0) return text;
23388
+ scan2.parts.push(text.slice(scan2.copied));
23389
+ return scan2.parts.join("");
23390
+ }
23391
+ function scanToken(scan2, index) {
23392
+ const char = scan2.text[index];
23393
+ if (char === '"') return scanString(scan2, index);
23394
+ if (char === "{" || char === "[") {
23395
+ scan2.containers.push(takeValue(scan2));
23396
+ } else if (char === "}" || char === "]") {
23397
+ if (scan2.containers.length > 1) scan2.containers.pop();
23398
+ scan2.key = void 0;
23399
+ } else if (char === ",") {
23400
+ scan2.key = void 0;
23401
+ } else {
23402
+ return scanBare(scan2, index);
23403
+ }
23404
+ return index + 1;
23405
+ }
23406
+ function scanString(scan2, start) {
23407
+ const end = stringEnd(scan2, start);
23408
+ if (isFollowedByColon(scan2.text, end)) {
23409
+ scan2.key = isSensitiveField2(keyName(scan2.text, start, end));
23410
+ } else if (takeValue(scan2)) {
23411
+ replaceValue(scan2, start, end, REDACTED_STRING);
23412
+ } else {
23413
+ const encoded = redactEncodedJson(scan2, start, end);
23414
+ if (encoded === void 0) redactStringJwts(scan2, start, end);
23415
+ else replaceValue(scan2, start, end, encoded);
23416
+ }
23417
+ return end;
23418
+ }
23419
+ function redactStringJwts(scan2, start, end) {
23420
+ const contentEnd = end - 1 > start && scan2.text[end - 1] === '"' ? end - 1 : end;
23421
+ const content = scan2.text.slice(start + 1, contentEnd);
23422
+ const redacted = redactJwts(content, REDACTED);
23423
+ if (redacted !== content) replaceValue(scan2, start + 1, contentEnd, redacted);
23424
+ }
23425
+ function scanBare(scan2, index) {
23426
+ BARE.lastIndex = index;
23427
+ const bare = BARE.exec(scan2.text)?.[0];
23428
+ if (bare === void 0) return index + 1;
23429
+ const end = index + bare.length;
23430
+ const isLiteral = /^[-\d]/.test(bare) || bare === "true" || bare === "false" || bare === "null";
23431
+ if (!isLiteral && scan2.key !== true) return end;
23432
+ if (takeValue(scan2) && bare !== "null") replaceValue(scan2, index, end, REDACTED_STRING);
23433
+ return end;
23434
+ }
23435
+ function takeValue(scan2) {
23436
+ const sensitive = scan2.key === true || scan2.containers[scan2.containers.length - 1] === true;
23437
+ scan2.key = void 0;
23438
+ return sensitive;
23439
+ }
23440
+ function replaceValue(scan2, start, end, replacement) {
23441
+ scan2.parts.push(scan2.text.slice(scan2.copied, start), replacement);
23442
+ scan2.copied = end;
23443
+ }
23444
+ function isJwtBoundary(text, index) {
23445
+ if (index < 0 || index >= text.length) return true;
23446
+ const code = text.charCodeAt(index);
23447
+ return !isBase64UrlCode(code) && code !== DOT;
23448
+ }
23449
+ function jwtEnd(text, start) {
23450
+ const first = segmentEnd(text, start + 3);
23451
+ if (first - start < 8 || text[first] !== ".") return void 0;
23452
+ const second = segmentEnd(text, first + 1);
23453
+ if (second - first - 1 < 5 || text[second] !== ".") return void 0;
23454
+ return segmentEnd(text, second + 1);
23455
+ }
23456
+ function segmentEnd(text, from) {
23457
+ let index = from;
23458
+ while (index < text.length && isBase64UrlCode(text.charCodeAt(index))) index++;
23459
+ return index;
23460
+ }
23461
+ function isBase64UrlCode(code) {
23462
+ return code >= 48 && code <= 57 || code >= 65 && code <= 90 || code >= 97 && code <= 122 || code === 95 || code === 45;
23463
+ }
23464
+ function redactEncodedJson(scan2, start, end) {
23465
+ if (scan2.depth >= MAX_ENCODED_DEPTH || end - start < 2 || scan2.text[end - 1] !== '"') {
23466
+ return void 0;
23553
23467
  }
23468
+ if (!/[[{]/.test(scan2.text.slice(start + 1, end - 1))) return void 0;
23469
+ let decoded;
23554
23470
  try {
23555
- const hits = { count: 0 };
23556
- const redacted = redactJson(parsed, false, hits);
23557
- return hits.count > 0 ? JSON.stringify(redacted) : text;
23471
+ decoded = JSON.parse(scan2.text.slice(start, end));
23558
23472
  } catch (error) {
23559
- log32.debug(`Request body redacted whole: ${getErrorMessage(error)}`);
23560
- return REDACTED;
23473
+ log29.debug(`String value not decodable: ${getErrorMessage(error)}`);
23474
+ return void 0;
23561
23475
  }
23476
+ if (!JSON_LIKE_START.test(decoded)) return void 0;
23477
+ const redacted = redactJsonText(decoded, scan2.depth + 1);
23478
+ return redacted === decoded ? void 0 : JSON.stringify(redacted);
23562
23479
  }
23563
- function redactJson(value, sensitive, hits) {
23564
- if (Array.isArray(value)) return value.map((item) => redactJson(item, sensitive, hits));
23565
- if (value !== null && typeof value === "object") {
23566
- return Object.fromEntries(
23567
- Object.entries(value).map(([key, field]) => [
23568
- key,
23569
- redactJson(field, sensitive || isSensitiveField2(key), hits)
23570
- ])
23571
- );
23480
+ function stringEnd(scan2, start) {
23481
+ const { text } = scan2;
23482
+ let from = start + 1;
23483
+ for (; ; ) {
23484
+ const quote = text.indexOf('"', from);
23485
+ const lineEnd = nextLineEnd(scan2, from);
23486
+ if (quote === -1 || lineEnd < quote) {
23487
+ return lineEnd > from && text[lineEnd - 1] === "\r" ? lineEnd - 1 : lineEnd;
23488
+ }
23489
+ let backslashes = 0;
23490
+ while (text[quote - 1 - backslashes] === "\\" && quote - 1 - backslashes > start) {
23491
+ backslashes++;
23492
+ }
23493
+ if (backslashes % 2 === 0) return quote + 1;
23494
+ from = quote + 1;
23495
+ }
23496
+ }
23497
+ function nextLineEnd(scan2, from) {
23498
+ if (scan2.lineEnd < from) {
23499
+ const lineEnd = scan2.text.indexOf("\n", from);
23500
+ scan2.lineEnd = lineEnd === -1 ? scan2.text.length : lineEnd;
23501
+ }
23502
+ return scan2.lineEnd;
23503
+ }
23504
+ function isFollowedByColon(text, index) {
23505
+ let next = index;
23506
+ while (next < text.length && " \n\r".includes(text.charAt(next))) next++;
23507
+ return text[next] === ":";
23508
+ }
23509
+ function keyName(text, start, end) {
23510
+ const raw = text.slice(start + 1, end - 1);
23511
+ if (!raw.includes("\\")) return raw;
23512
+ try {
23513
+ return JSON.parse(text.slice(start, end));
23514
+ } catch (error) {
23515
+ log29.debug(`Key not decodable: ${getErrorMessage(error)}`);
23516
+ return raw;
23572
23517
  }
23573
- if (!sensitive || !["string", "number", "boolean"].includes(typeof value)) return value;
23574
- hits.count++;
23575
- return REDACTED;
23576
23518
  }
23577
23519
 
23578
23520
  // src/telemetry/har/sanitize.ts
@@ -23588,8 +23530,11 @@ var SENSITIVE_CUSTOM_HEADER = /^x-.*(key|token|secret|auth)/i;
23588
23530
  var SENSITIVE_HEADER_SEGMENT = /(^|-)(api-?key|apikey|token|secret|jwt|subscription-key|session(-?id)?)(-|$)/i;
23589
23531
  var URL_HEADERS = /* @__PURE__ */ new Set(["location", "referer"]);
23590
23532
  var SENSITIVE_URL_PARAM = /^(code|sig|key)$/i;
23533
+ var BASE64_REDACTED = Buffer.from(REDACTED).toString("base64");
23534
+ var TEXT_OPCODE = 1;
23535
+ var BINARY_OPCODE2 = 2;
23591
23536
  function sanitizeEntry(entry) {
23592
- const { request, response } = entry;
23537
+ const { request, response, _webSocketMessages: messages } = entry;
23593
23538
  const postData = request.postData;
23594
23539
  return {
23595
23540
  ...entry,
@@ -23600,17 +23545,33 @@ function sanitizeEntry(entry) {
23600
23545
  headers: request.headers.map(redactHeader),
23601
23546
  queryString: request.queryString.map(redactQueryParam),
23602
23547
  ...postData?.text !== void 0 && {
23603
- postData: { ...postData, text: redactRequestBody(postData.text, postData.mimeType) }
23548
+ postData: {
23549
+ ...postData,
23550
+ text: redactOrReplaceWhole(postData.text, (text) => redactBody(text, postData.mimeType))
23551
+ }
23604
23552
  }
23605
23553
  },
23606
23554
  response: {
23607
23555
  ...response,
23608
23556
  cookies: response.cookies.map(redactCookie),
23609
23557
  headers: response.headers.map(redactHeader),
23610
- redirectURL: redactUrl(response.redirectURL)
23611
- }
23558
+ redirectURL: redactUrl(response.redirectURL),
23559
+ content: redactContent(response.content)
23560
+ },
23561
+ ...messages && { _webSocketMessages: messages.map(redactWebSocketMessage) }
23612
23562
  };
23613
23563
  }
23564
+ function redactContent(content) {
23565
+ const { text, mimeType } = content;
23566
+ if (text === void 0) return content;
23567
+ const redacted = content.encoding === "base64" ? redactOrReplaceWhole(text, (body) => redactBase64Body(body, mimeType), BASE64_REDACTED) : redactOrReplaceWhole(text, (body) => redactBody(body, mimeType));
23568
+ return redacted === text ? content : { ...content, text: redacted };
23569
+ }
23570
+ function redactWebSocketMessage(message) {
23571
+ if (message.opcode !== TEXT_OPCODE && message.opcode !== BINARY_OPCODE2) return message;
23572
+ const data = message.opcode === TEXT_OPCODE ? redactOrReplaceWhole(message.data, (text) => redactBody(text, "")) : redactOrReplaceWhole(message.data, (text) => redactBase64Body(text, ""), BASE64_REDACTED);
23573
+ return data === message.data ? message : { ...message, data };
23574
+ }
23614
23575
  function isSensitiveHeader(name) {
23615
23576
  return SENSITIVE_HEADERS.has(name.toLowerCase()) || SENSITIVE_CUSTOM_HEADER.test(name) || SENSITIVE_HEADER_SEGMENT.test(name);
23616
23577
  }
@@ -23646,7 +23607,7 @@ var DEFAULT_HTTP_VERSION = "HTTP/1.1";
23646
23607
  function buildHAR(requests, metadata, options = {}) {
23647
23608
  const built = [...requests].sort((a, b) => a.timestamp - b.timestamp).map(buildEntry);
23648
23609
  const entries = options.includeSensitive ? built : built.map(sanitizeEntry);
23649
- const log36 = {
23610
+ const log33 = {
23650
23611
  version: "1.2",
23651
23612
  creator: {
23652
23613
  name: "bdg",
@@ -23657,12 +23618,12 @@ function buildHAR(requests, metadata, options = {}) {
23657
23618
  ...!options.includeSensitive && { comment: harSanitizedComment() }
23658
23619
  };
23659
23620
  if (metadata.chromeVersion) {
23660
- log36.browser = {
23621
+ log33.browser = {
23661
23622
  name: "Chrome",
23662
23623
  version: metadata.chromeVersion
23663
23624
  };
23664
23625
  }
23665
- return { log: log36 };
23626
+ return { log: log33 };
23666
23627
  }
23667
23628
  function buildEntry(req) {
23668
23629
  const timings = buildTimings(req);
@@ -23693,7 +23654,8 @@ function buildWebSocketMessage(frame) {
23693
23654
  type: frame.direction === "sent" ? "send" : "receive",
23694
23655
  time: frame.timestamp / 1e3,
23695
23656
  opcode: frame.opcode,
23696
- data: frame.payloadData
23657
+ data: frame.payloadData,
23658
+ ...frame.truncatedFrom !== void 0 && { _truncatedFrom: frame.truncatedFrom }
23697
23659
  };
23698
23660
  }
23699
23661
  function buildRequest(req) {
@@ -23706,7 +23668,7 @@ function buildRequest(req) {
23706
23668
  headers: convertHeaders(req.requestHeaders),
23707
23669
  queryString: extractQueryParams(url),
23708
23670
  headersSize: estimateRequestHeadersSize(req.method, req.url, req.requestHeaders),
23709
- bodySize: req.requestBody ? Buffer.byteLength(req.requestBody, "utf-8") : 0
23671
+ bodySize: requestBodySize(req)
23710
23672
  };
23711
23673
  const postData = buildPostData(req);
23712
23674
  if (postData) {
@@ -23754,13 +23716,17 @@ function contentLength(req) {
23754
23716
  const bytes = value === void 0 ? NaN : Number(value);
23755
23717
  return Number.isInteger(bytes) && bytes >= 0 ? bytes : void 0;
23756
23718
  }
23719
+ function requestBodySize(req) {
23720
+ if (!req.requestBody) return 0;
23721
+ if (skippedBodyReason(req.requestBody) !== void 0) return -1;
23722
+ return Buffer.byteLength(req.requestBody, "utf-8");
23723
+ }
23757
23724
  function buildPostData(req) {
23758
23725
  if (!req.requestBody) return void 0;
23759
- const contentType = getHeader(req.requestHeaders, "content-type") ?? "text/plain";
23760
- return {
23761
- mimeType: contentType,
23762
- text: req.requestBody
23763
- };
23726
+ const mimeType = getHeader(req.requestHeaders, "content-type") ?? "text/plain";
23727
+ const skipped = skippedBodyReason(req.requestBody);
23728
+ if (skipped !== void 0) return { mimeType, comment: `Body not captured: ${skipped}` };
23729
+ return { mimeType, text: req.requestBody };
23764
23730
  }
23765
23731
  function span(start, end) {
23766
23732
  if (start === void 0 || end === void 0 || start < 0 || end < 0 || end < start) {
@@ -24006,6 +23972,13 @@ function formatSessionStatus(metadata, pid, activity, pageState, verbose = false
24006
23972
  if (activity.lastConsoleMessageAt) {
24007
23973
  fmt.keyValue(" Last Message", formatTimeAgo(activity.lastConsoleMessageAt), 18);
24008
23974
  }
23975
+ const downloadRows = [
23976
+ ...activity.downloads?.length ? [downloadsSummary(activity.downloads)] : [],
23977
+ ...activity.downloadsWarning ? [`\u26A0 ${activity.downloadsWarning}`] : []
23978
+ ];
23979
+ downloadRows.forEach(
23980
+ (row, index) => index === 0 ? fmt.keyValue("Downloads", row, 18) : fmt.text(`${" ".repeat(18)}${row}`)
23981
+ );
24009
23982
  }
24010
23983
  fmt.blank().text("Collectors").separator("\u2501", 50);
24011
23984
  const activeTelemetry = metadata.activeTelemetry ?? ["network", "console", "dom"];
@@ -24142,6 +24115,7 @@ function buildPreviewJsonData(output, options) {
24142
24115
  ...output.partial !== void 0 && { partial: output.partial },
24143
24116
  ...output.totals && { totals: output.totals },
24144
24117
  ...output.pageCrashedAt !== void 0 && { pageCrashedAt: output.pageCrashedAt },
24118
+ ...output.downloads && { downloads: output.downloads },
24145
24119
  ...pick("network") && output.data.network && { network: last(output.data.network) },
24146
24120
  ...pick("console") && output.data.console && {
24147
24121
  console: last(output.data.console)?.map((message) => capMessageText(message, options.full))
@@ -24226,6 +24200,8 @@ function formatPreviewCompact(output, options) {
24226
24200
  fmt.blank();
24227
24201
  }
24228
24202
  }
24203
+ if (output.downloads?.length)
24204
+ fmt.text(`Downloads: ${downloadsSummary(output.downloads)}`).blank();
24229
24205
  if (!options.follow) {
24230
24206
  fmt.tip(compactTipsMessage());
24231
24207
  }
@@ -24314,6 +24290,9 @@ function formatPreviewVerbose(output, options) {
24314
24290
  fmt.blank();
24315
24291
  }
24316
24292
  }
24293
+ if (output.downloads?.length) {
24294
+ fmt.keyValue("Downloads", downloadsSummary(output.downloads), 18).blank();
24295
+ }
24317
24296
  if (!options.follow) {
24318
24297
  fmt.tip(verboseCommandsMessage());
24319
24298
  }
@@ -24437,7 +24416,7 @@ function registerDocumentCommand(networkCmd) {
24437
24416
  }
24438
24417
 
24439
24418
  // src/commands/network/har.ts
24440
- var log33 = createLogger("network");
24419
+ var log30 = createLogger("network");
24441
24420
  function generateHARFilename() {
24442
24421
  const now = /* @__PURE__ */ new Date();
24443
24422
  const year = now.getFullYear();
@@ -24460,7 +24439,7 @@ async function getChromeVersion() {
24460
24439
  const product = response.data?.result?.product;
24461
24440
  return product?.split("/")[1];
24462
24441
  } catch (error) {
24463
- log33.debug(`Could not read Chrome version: ${getErrorMessage(error)}`);
24442
+ log30.debug(`Could not read Chrome version: ${getErrorMessage(error)}`);
24464
24443
  return void 0;
24465
24444
  }
24466
24445
  }
@@ -25272,13 +25251,13 @@ import * as fs17 from "fs";
25272
25251
  init_paths();
25273
25252
  init_logging();
25274
25253
  init_directories();
25275
- var log34 = createLogger("session");
25254
+ var log31 = createLogger("session");
25276
25255
  function readPortFile(portPath) {
25277
25256
  try {
25278
25257
  const port = parseInt(fs17.readFileSync(portPath, "utf-8").trim(), 10);
25279
25258
  return Number.isInteger(port) && port >= 1 && port <= 65535 ? port : null;
25280
25259
  } catch (error) {
25281
- logDebugError(log34, `read ${portPath}`, error);
25260
+ logDebugError(log31, `read ${portPath}`, error);
25282
25261
  return null;
25283
25262
  }
25284
25263
  }
@@ -25330,7 +25309,7 @@ function selectSession(optionValue) {
25330
25309
  // src/session/sessionList.ts
25331
25310
  init_logging();
25332
25311
  init_sessionCommand();
25333
- var log35 = createLogger("session");
25312
+ var log32 = createLogger("session");
25334
25313
  function toRunningSession(name, data) {
25335
25314
  const meta = data.sessionMetadata;
25336
25315
  const state = meta ? "active" : data.ending ? "ending" : "starting";
@@ -25357,7 +25336,7 @@ async function describeSession(entry) {
25357
25336
  const response = await getStatus(socketPath);
25358
25337
  if (response.status === "ok" && response.data) return toRunningSession(name, response.data);
25359
25338
  } catch (error) {
25360
- logDebugError(log35, `get status of ${dir}`, error);
25339
+ logDebugError(log32, `get status of ${dir}`, error);
25361
25340
  }
25362
25341
  return { name, state: "unresponsive" };
25363
25342
  }
@@ -25396,7 +25375,7 @@ function leftoverPort(dir) {
25396
25375
  const meta = JSON.parse(fs18.readFileSync(sessionFilePathIn(dir, "METADATA"), "utf8"));
25397
25376
  if (typeof meta.port === "number") return meta.port;
25398
25377
  } catch (error) {
25399
- logDebugError(log35, `read the metadata in ${dir}`, error);
25378
+ logDebugError(log32, `read the metadata in ${dir}`, error);
25400
25379
  }
25401
25380
  return readPortFile(sessionFilePathIn(dir, "PORT"));
25402
25381
  }
@@ -25409,7 +25388,7 @@ function isSameDir(a, b) {
25409
25388
  const second = fs18.statSync(b);
25410
25389
  return first.ino === second.ino && first.dev === second.dev;
25411
25390
  } catch (error) {
25412
- logDebugError(log35, `compare ${a} with ${b}`, error);
25391
+ logDebugError(log32, `compare ${a} with ${b}`, error);
25413
25392
  return false;
25414
25393
  }
25415
25394
  }