browser-debugger-cli 0.14.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 (167) hide show
  1. package/.claude/skills/bdg/SKILL.md +3 -2
  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 +201 -133
  11. package/dist/commands/cleanup.js +21 -4
  12. package/dist/commands/dom/eval.d.ts +2 -1
  13. package/dist/commands/dom/eval.js +6 -21
  14. package/dist/commands/dom/formInteraction.js +8 -4
  15. package/dist/commands/dom/helpers/evalResult.d.ts +36 -0
  16. package/dist/commands/dom/helpers/evalResult.js +59 -0
  17. package/dist/commands/dom/helpers/index.d.ts +4 -4
  18. package/dist/commands/dom/helpers/index.js +3 -3
  19. package/dist/commands/dom/helpers/query.d.ts +2 -2
  20. package/dist/commands/dom/helpers/query.js +2 -2
  21. package/dist/commands/dom/helpers/screenshot.d.ts +21 -26
  22. package/dist/commands/dom/helpers/screenshot.js +50 -668
  23. package/dist/commands/dom/screenshot.js +56 -36
  24. package/dist/commands/helpJson.d.ts +1 -1
  25. package/dist/commands/helpJson.js +3 -3
  26. package/dist/commands/helpTopic.js +10 -4
  27. package/dist/commands/network/har.js +18 -14
  28. package/dist/commands/optionBehaviors.js +24 -9
  29. package/dist/commands/shared/CommandRunner.d.ts +5 -0
  30. package/dist/commands/shared/CommandRunner.js +18 -3
  31. package/dist/commands/shared/interrupt.d.ts +40 -0
  32. package/dist/commands/shared/interrupt.js +73 -0
  33. package/dist/commands/shared/optionTypes.d.ts +3 -0
  34. package/dist/commands/shared/outputFile.d.ts +2 -1
  35. package/dist/commands/shared/outputFile.js +7 -4
  36. package/dist/commands/shared/startHelpers.d.ts +26 -3
  37. package/dist/commands/shared/startHelpers.js +145 -23
  38. package/dist/commands/status.js +3 -1
  39. package/dist/commands/stop.js +2 -1
  40. package/dist/commands/types.d.ts +5 -0
  41. package/dist/connection/cdp.js +1 -16
  42. package/dist/connection/chromeIdentity.d.ts +24 -5
  43. package/dist/connection/chromeIdentity.js +53 -22
  44. package/dist/connection/launcher/flagsBuilder.d.ts +46 -0
  45. package/dist/connection/launcher/flagsBuilder.js +107 -23
  46. package/dist/connection/launcher.d.ts +35 -2
  47. package/dist/connection/launcher.js +99 -12
  48. package/dist/connection/typed-cdp.d.ts +3 -2
  49. package/dist/constants.d.ts +3 -5
  50. package/dist/constants.js +3 -5
  51. package/dist/daemon/SessionController.d.ts +10 -5
  52. package/dist/daemon/SessionController.js +15 -8
  53. package/dist/daemon/ipcServer.js +1 -1
  54. package/dist/daemon/launcher.d.ts +22 -3
  55. package/dist/daemon/launcher.js +45 -8
  56. package/dist/daemon/session/Session.d.ts +5 -1
  57. package/dist/daemon/session/Session.js +9 -8
  58. package/dist/daemon/session/TelemetryStore.d.ts +5 -0
  59. package/dist/daemon/session/TelemetryStore.js +4 -0
  60. package/dist/daemon/session/captureGate.d.ts +59 -0
  61. package/dist/daemon/session/captureGate.js +96 -0
  62. package/dist/daemon/session/chromeConnection.d.ts +16 -1
  63. package/dist/daemon/session/chromeConnection.js +34 -4
  64. package/dist/daemon/session/collectors.d.ts +15 -0
  65. package/dist/daemon/session/collectors.js +39 -2
  66. package/dist/daemon/session/commandRegistry.d.ts +14 -1
  67. package/dist/daemon/session/commandRegistry.js +48 -13
  68. package/dist/daemon/session/downloads.d.ts +32 -0
  69. package/dist/daemon/session/downloads.js +96 -0
  70. package/dist/daemon/session/interactions.d.ts +3 -2
  71. package/dist/daemon/session/interactions.js +7 -2
  72. package/dist/daemon/session/plugins.js +6 -0
  73. package/dist/daemon.js +18520 -17014
  74. package/dist/errors/CommandError.d.ts +2 -0
  75. package/dist/errors/issues.d.ts +1 -1
  76. package/dist/errors/messages.d.ts +81 -0
  77. package/dist/errors/messages.js +198 -6
  78. package/dist/index.js +1446 -1078
  79. package/dist/ipc/client.d.ts +20 -2
  80. package/dist/ipc/client.js +32 -6
  81. package/dist/ipc/protocol/commands.d.ts +36 -2
  82. package/dist/ipc/protocol/commands.js +1 -0
  83. package/dist/ipc/protocol/domTypes.d.ts +24 -1
  84. package/dist/ipc/session/queries.d.ts +3 -0
  85. package/dist/ipc/session/types.d.ts +5 -0
  86. package/dist/ipc/transport/IPCError.d.ts +9 -0
  87. package/dist/ipc/transport/IPCError.js +12 -0
  88. package/dist/ipc/transport/errors.d.ts +2 -1
  89. package/dist/ipc/transport/errors.js +4 -1
  90. package/dist/ipc/transport/index.d.ts +10 -2
  91. package/dist/ipc/transport/index.js +29 -4
  92. package/dist/runtime/dom/actionEffects.d.ts +48 -9
  93. package/dist/runtime/dom/actionEffects.js +269 -34
  94. package/dist/runtime/dom/actionEffectsScripts.d.ts +45 -0
  95. package/dist/runtime/dom/actionEffectsScripts.js +101 -2
  96. package/dist/runtime/dom/captureArea.d.ts +35 -0
  97. package/dist/runtime/dom/captureArea.js +203 -0
  98. package/dist/runtime/dom/elementInfo.d.ts +13 -4
  99. package/dist/runtime/dom/elementInfo.js +12 -3
  100. package/dist/runtime/dom/evalHelpers.d.ts +24 -4
  101. package/dist/runtime/dom/evalHelpers.js +40 -12
  102. package/dist/runtime/dom/formDiscovery.d.ts +1 -1
  103. package/dist/runtime/dom/frames.d.ts +2 -1
  104. package/dist/runtime/dom/frames.js +3 -1
  105. package/dist/runtime/page/bdgWorld.d.ts +9 -0
  106. package/dist/runtime/page/bdgWorld.js +11 -0
  107. package/dist/runtime/page/captureEmulation.d.ts +119 -0
  108. package/dist/runtime/page/captureEmulation.js +189 -0
  109. package/dist/runtime/page/captureScroll.d.ts +24 -0
  110. package/dist/runtime/page/captureScroll.js +124 -0
  111. package/dist/runtime/page/emulation.js +6 -5
  112. package/dist/runtime/page/screenshot.d.ts +41 -0
  113. package/dist/runtime/page/screenshot.js +394 -0
  114. package/dist/runtime/page/userAgent.d.ts +86 -2
  115. package/dist/runtime/page/userAgent.js +154 -33
  116. package/dist/session/paths.d.ts +52 -3
  117. package/dist/session/paths.js +179 -7
  118. package/dist/session/portClaims.d.ts +0 -8
  119. package/dist/session/portClaims.js +1 -22
  120. package/dist/session/sessionList.d.ts +5 -1
  121. package/dist/session/sessionList.js +5 -1
  122. package/dist/telemetry/downloads.d.ts +127 -0
  123. package/dist/telemetry/downloads.js +265 -0
  124. package/dist/telemetry/har/builder.d.ts +12 -1
  125. package/dist/telemetry/har/builder.js +32 -9
  126. package/dist/telemetry/har/sanitize.d.ts +28 -0
  127. package/dist/telemetry/har/sanitize.js +184 -0
  128. package/dist/telemetry/har/sanitizeBody.d.ts +78 -0
  129. package/dist/telemetry/har/sanitizeBody.js +541 -0
  130. package/dist/telemetry/har/types.d.ts +2 -0
  131. package/dist/telemetry/network.d.ts +4 -4
  132. package/dist/telemetry/network.js +38 -4
  133. package/dist/telemetry/networkRetention.d.ts +35 -14
  134. package/dist/telemetry/networkRetention.js +62 -26
  135. package/dist/types.d.ts +9 -14
  136. package/dist/ui/OutputBuilder.d.ts +3 -2
  137. package/dist/ui/OutputBuilder.js +4 -3
  138. package/dist/ui/formatters/cdp.d.ts +32 -9
  139. package/dist/ui/formatters/cdp.js +77 -6
  140. package/dist/ui/formatters/details.js +7 -15
  141. package/dist/ui/formatters/preview.d.ts +2 -0
  142. package/dist/ui/formatters/preview.js +7 -1
  143. package/dist/ui/formatters/sessions.d.ts +3 -2
  144. package/dist/ui/formatters/sessions.js +10 -3
  145. package/dist/ui/formatters/status.js +6 -1
  146. package/dist/ui/formatting.d.ts +7 -0
  147. package/dist/ui/formatting.js +13 -0
  148. package/dist/ui/logging/logger.d.ts +1 -1
  149. package/dist/ui/messages/chrome.d.ts +27 -6
  150. package/dist/ui/messages/chrome.js +78 -12
  151. package/dist/ui/messages/commands.d.ts +71 -3
  152. package/dist/ui/messages/commands.js +98 -3
  153. package/dist/ui/messages/networkMessages.d.ts +50 -5
  154. package/dist/ui/messages/networkMessages.js +50 -6
  155. package/dist/ui/messages/session.d.ts +8 -0
  156. package/dist/ui/messages/session.js +10 -0
  157. package/dist/utils/async.d.ts +3 -2
  158. package/dist/utils/async.js +16 -3
  159. package/dist/utils/atomicFile.d.ts +2 -1
  160. package/dist/utils/atomicFile.js +5 -2
  161. package/dist/utils/directories.d.ts +41 -0
  162. package/dist/utils/directories.js +48 -0
  163. package/dist/utils/http.d.ts +11 -4
  164. package/dist/utils/http.js +5 -3
  165. package/package.json +18 -4
  166. /package/dist/{commands/dom → runtime/page}/screenshotResize.d.ts +0 -0
  167. /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 = log34;
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 log34(...args) {
1131
+ function log33(...args) {
1099
1132
  return process.stderr.write(util.formatWithOptions(exports.inspectOpts, ...args) + "\n");
1100
1133
  }
1101
1134
  function save(namespaces) {
@@ -1287,14 +1320,14 @@ var require_is_docker = __commonJS({
1287
1320
  var require_is_wsl = __commonJS({
1288
1321
  "node_modules/is-wsl/index.js"(exports, module) {
1289
1322
  "use strict";
1290
- var os4 = __require("os");
1323
+ var os5 = __require("os");
1291
1324
  var fs19 = __require("fs");
1292
1325
  var isDocker = require_is_docker();
1293
1326
  var isWsl3 = () => {
1294
1327
  if (process.platform !== "linux") {
1295
1328
  return false;
1296
1329
  }
1297
- if (os4.release().toLowerCase().includes("microsoft")) {
1330
+ if (os5.release().toLowerCase().includes("microsoft")) {
1298
1331
  if (isDocker()) {
1299
1332
  return false;
1300
1333
  }
@@ -1327,7 +1360,7 @@ function resourceTypeFromName(name) {
1327
1360
  ([type, abbreviation]) => type.toLowerCase() === wanted || abbreviation.toLowerCase() === wanted
1328
1361
  )?.[0];
1329
1362
  }
1330
- var HTTP_LOCALHOST, MAX_NETWORK_REQUESTS, MAX_CONSOLE_MESSAGES, MAX_RESPONSE_SIZE, MAX_TOTAL_BODY_BYTES, MAX_VALUE_LENGTH, MAX_CONSOLE_TEXT_LENGTH, MAX_CONSOLE_JSON_TEXT_LENGTH, CHROME_NETWORK_BUFFER_TOTAL, CHROME_NETWORK_BUFFER_PER_RESOURCE, CHROME_POST_DATA_LIMIT, QUERY_JSON_LIST_LIMIT, LAYOUT_ELEMENT_LIMIT, MAX_TRIGGERED_REQUESTS, MAX_JSONL_BUFFER_SIZE, PORT_OPTION_DESCRIPTION, RESOURCE_TYPE_ABBREVIATIONS, MIME_TYPE_RULES;
1363
+ var HTTP_LOCALHOST, MAX_NETWORK_REQUESTS, MAX_CONSOLE_MESSAGES, MAX_RESPONSE_SIZE, MAX_TOTAL_BODY_BYTES, MAX_VALUE_LENGTH, MAX_CONSOLE_TEXT_LENGTH, MAX_CONSOLE_JSON_TEXT_LENGTH, CHROME_NETWORK_BUFFER_TOTAL, CHROME_NETWORK_BUFFER_PER_RESOURCE, CHROME_POST_DATA_LIMIT, QUERY_JSON_LIST_LIMIT, EVAL_JSON_ARRAY_LIMIT, LAYOUT_ELEMENT_LIMIT, MAX_TRIGGERED_REQUESTS, MAX_JSONL_BUFFER_SIZE, PORT_OPTION_DESCRIPTION, RESOURCE_TYPE_ABBREVIATIONS, MIME_TYPE_RULES;
1331
1364
  var init_constants = __esm({
1332
1365
  "src/constants.ts"() {
1333
1366
  "use strict";
@@ -1343,6 +1376,7 @@ var init_constants = __esm({
1343
1376
  CHROME_NETWORK_BUFFER_PER_RESOURCE = 10 * 1024 * 1024;
1344
1377
  CHROME_POST_DATA_LIMIT = 1 * 1024 * 1024;
1345
1378
  QUERY_JSON_LIST_LIMIT = 100;
1379
+ EVAL_JSON_ARRAY_LIMIT = 100;
1346
1380
  LAYOUT_ELEMENT_LIMIT = 100;
1347
1381
  MAX_TRIGGERED_REQUESTS = 50;
1348
1382
  MAX_JSONL_BUFFER_SIZE = 256 * 1024 * 1024;
@@ -1421,8 +1455,8 @@ function createLogger(context) {
1421
1455
  logger.debug = (message) => logMessage(message, "debug");
1422
1456
  return logger;
1423
1457
  }
1424
- function logDebugError(log34, context, error) {
1425
- log34.debug(`Failed to ${context}: ${getErrorMessage(error)}`);
1458
+ function logDebugError(log33, context, error) {
1459
+ log33.debug(`Failed to ${context}: ${getErrorMessage(error)}`);
1426
1460
  }
1427
1461
  var debugEnabled;
1428
1462
  var init_logger = __esm({
@@ -1489,11 +1523,30 @@ function makeDirectory(dir, mode) {
1489
1523
  }
1490
1524
  fs2.mkdirSync(dir, { recursive: true, ...mode !== void 0 && { mode } });
1491
1525
  }
1492
- var PSEUDO_FILESYSTEM;
1526
+ function dirTrustProblem(dir, options = {}) {
1527
+ const stat = fs2.lstatSync(dir);
1528
+ const problem = (reason, kind) => ({ reason, kind });
1529
+ if (stat.isSymbolicLink()) return problem("it is a symbolic link", "symlink");
1530
+ if (!stat.isDirectory()) return problem("not a directory", "not-directory");
1531
+ const uid = process.getuid?.();
1532
+ if (uid !== void 0 && stat.uid !== uid) return problem(`owned by uid ${stat.uid}`, "owner");
1533
+ if (process.platform === "win32") return null;
1534
+ const writeBits = options.allowGroupWrite ? OTHER_WRITE : GROUP_OTHER_WRITE;
1535
+ if ((stat.mode & writeBits) === 0) return null;
1536
+ const mode = (stat.mode & 4095).toString(8);
1537
+ if ((stat.mode & STICKY) !== 0 && (stat.mode & OTHER_WRITE) !== 0) {
1538
+ return problem(`a shared sticky directory (mode ${mode})`, "shared");
1539
+ }
1540
+ return problem(`writable by others (mode ${mode})`, "writable");
1541
+ }
1542
+ var PSEUDO_FILESYSTEM, GROUP_OTHER_WRITE, OTHER_WRITE, STICKY;
1493
1543
  var init_directories = __esm({
1494
1544
  "src/utils/directories.ts"() {
1495
1545
  "use strict";
1496
1546
  PSEUDO_FILESYSTEM = /^\/(proc|sys)(\/|$)/;
1547
+ GROUP_OTHER_WRITE = 18;
1548
+ OTHER_WRITE = 2;
1549
+ STICKY = 512;
1497
1550
  }
1498
1551
  });
1499
1552
 
@@ -1502,12 +1555,16 @@ import * as fs3 from "fs";
1502
1555
  import * as os2 from "os";
1503
1556
  import * as path3 from "path";
1504
1557
  function getSessionBaseDir() {
1505
- const override = process.env[SESSION_DIR_OVERRIDE_ENV];
1506
- if (override && override.trim().length > 0) {
1558
+ const override = sessionDirOverride();
1559
+ if (override !== null) {
1507
1560
  return path3.isAbsolute(override) ? override : path3.resolve(override);
1508
1561
  }
1509
1562
  return path3.join(os2.homedir(), ".bdg");
1510
1563
  }
1564
+ function sessionDirOverride() {
1565
+ const override = process.env[SESSION_DIR_OVERRIDE_ENV];
1566
+ return override && override.trim().length > 0 ? override : null;
1567
+ }
1511
1568
  function getSessionName() {
1512
1569
  const name = process.env[SESSION_NAME_ENV]?.trim();
1513
1570
  if (!name) return null;
@@ -1543,14 +1600,84 @@ function getDaemonSocketPath() {
1543
1600
  return getSessionFilePath("DAEMON_SOCKET");
1544
1601
  }
1545
1602
  function ensureSessionDir() {
1546
- makeDirectory(getSessionDir());
1603
+ makeDirectory(getSessionDir(), PRIVATE_DIR_MODE);
1547
1604
  }
1548
- 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;
1605
+ function getSessionDownloadsDir() {
1606
+ return path3.join(getSessionDir(), DOWNLOADS_DIR);
1607
+ }
1608
+ function sessionDirChain(dir) {
1609
+ const base = getSessionBaseDir();
1610
+ const relative2 = path3.relative(base, dir);
1611
+ if (relative2 === ".." || relative2.startsWith(`..${path3.sep}`) || path3.isAbsolute(relative2)) {
1612
+ return [{ dir, bdgOwned: false, isBase: false }];
1613
+ }
1614
+ const chain = [{ dir: base, bdgOwned: sessionDirOverride() === null, isBase: true }];
1615
+ let current = base;
1616
+ for (const part of relative2.split(path3.sep).filter(Boolean)) {
1617
+ current = path3.join(current, part);
1618
+ chain.push({ dir: current, bdgOwned: true, isBase: false });
1619
+ }
1620
+ return chain;
1621
+ }
1622
+ function tightenDir(dir) {
1623
+ if (process.platform === "win32") return;
1624
+ let fd;
1625
+ try {
1626
+ const { O_RDONLY, O_DIRECTORY, O_NOFOLLOW } = fs3.constants;
1627
+ fd = fs3.openSync(dir, O_RDONLY | O_DIRECTORY | O_NOFOLLOW);
1628
+ const stat = fs3.fstatSync(fd);
1629
+ if (stat.uid !== process.getuid?.() || (stat.mode & GROUP_OTHER_ACCESS) === 0) return;
1630
+ fs3.fchmodSync(fd, PRIVATE_DIR_MODE);
1631
+ log.debug(`Session directory ${dir} restricted to mode 700`);
1632
+ } catch (error) {
1633
+ log.debug(`Session directory ${dir} not restricted: ${getErrorMessage(error)}`);
1634
+ } finally {
1635
+ if (fd !== void 0) fs3.closeSync(fd);
1636
+ }
1637
+ }
1638
+ function chainDirProblem(entry) {
1639
+ const problem = dirTrustProblem(entry.dir, SESSION_DIR_TRUST);
1640
+ if (problem?.kind !== "symlink" || !entry.isBase) return problem;
1641
+ const target = fs3.realpathSync(entry.dir);
1642
+ const targetProblem = dirTrustProblem(target, SESSION_DIR_TRUST);
1643
+ return targetProblem && {
1644
+ ...targetProblem,
1645
+ reason: `it links to ${target}, which is ${targetProblem.reason}`
1646
+ };
1647
+ }
1648
+ function acceptTrustedDir(entry) {
1649
+ if (entry.bdgOwned) {
1650
+ tightenDir(entry.dir);
1651
+ return;
1652
+ }
1653
+ const mode = fs3.statSync(entry.dir, { throwIfNoEntry: false })?.mode ?? 0;
1654
+ if ((mode & GROUP_OTHER_ACCESS) === 0) return;
1655
+ log.debug(
1656
+ `Session directory ${entry.dir} is open to group or others; left as is (chosen with ${SESSION_DIR_OVERRIDE_ENV})`
1657
+ );
1658
+ }
1659
+ function secureSessionDir(dir = getSessionDir()) {
1660
+ for (const entry of sessionDirChain(dir)) {
1661
+ let problem;
1662
+ try {
1663
+ problem = chainDirProblem(entry);
1664
+ } catch (error) {
1665
+ const code = error.code;
1666
+ if (code === "ENOENT" || code === "ENOTDIR") continue;
1667
+ problem = { reason: getErrorMessage(error), kind: "not-directory" };
1668
+ }
1669
+ if (problem !== null) return { dir: entry.dir, ...problem, bdgOwned: entry.bdgOwned };
1670
+ acceptTrustedDir(entry);
1671
+ }
1672
+ return null;
1673
+ }
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;
1549
1675
  var init_paths = __esm({
1550
1676
  "src/session/paths.ts"() {
1551
1677
  "use strict";
1552
1678
  init_logging();
1553
1679
  init_directories();
1680
+ init_errors();
1554
1681
  log = createLogger("session");
1555
1682
  SESSION_FILES = {
1556
1683
  OUTPUT: "session.json",
@@ -1572,6 +1699,10 @@ var init_paths = __esm({
1572
1699
  "CHROME_PID",
1573
1700
  "METADATA"
1574
1701
  ];
1702
+ PRIVATE_DIR_MODE = 448;
1703
+ GROUP_OTHER_ACCESS = 63;
1704
+ DOWNLOADS_DIR = "downloads";
1705
+ SESSION_DIR_TRUST = { allowGroupWrite: true };
1575
1706
  }
1576
1707
  });
1577
1708
 
@@ -1653,14 +1784,17 @@ var init_atomicFile = __esm({
1653
1784
  *
1654
1785
  * @param filePath - Target file path
1655
1786
  * @param data - Data to write
1656
- * @param options - Write options
1787
+ * @param options - Write options (`mode`: permissions of the new file, less the umask)
1657
1788
  * @returns Promise that resolves when write completes
1658
1789
  * @throws Error if write operation fails
1659
1790
  */
1660
1791
  static async writeAsync(filePath, data, options = {}) {
1661
1792
  const tmpPath = this.getTempPath(filePath);
1662
1793
  try {
1663
- await fs4.promises.writeFile(tmpPath, data, { encoding: options.encoding ?? "utf-8" });
1794
+ await fs4.promises.writeFile(tmpPath, data, {
1795
+ encoding: options.encoding ?? "utf-8",
1796
+ ...options.mode !== void 0 && { mode: options.mode }
1797
+ });
1664
1798
  await fs4.promises.rename(tmpPath, filePath);
1665
1799
  } catch (error) {
1666
1800
  try {
@@ -1703,48 +1837,6 @@ var init_atomicFile = __esm({
1703
1837
  }
1704
1838
  });
1705
1839
 
1706
- // src/session/metadata.ts
1707
- import * as fs8 from "fs";
1708
- function handleCorruptedMetadata(metaPath, error, options) {
1709
- if (options?.warnOnCorruption) {
1710
- log6.info(`Session metadata corrupted (cannot read details): ${getErrorMessage(error)}`);
1711
- log6.info(`Troubleshooting: Run "${sessionCommand("bdg cleanup")}" to remove corrupted files`);
1712
- }
1713
- if (options?.selfHealOnCorruption) {
1714
- try {
1715
- fs8.rmSync(metaPath, { force: true });
1716
- log6.info("Removed corrupted session metadata file");
1717
- } catch (deleteError) {
1718
- log6.debug(`Failed to remove corrupted metadata: ${getErrorMessage(deleteError)}`);
1719
- }
1720
- }
1721
- }
1722
- function readSessionMetadata(options) {
1723
- const metaPath = getSessionFilePath("METADATA");
1724
- if (!fs8.existsSync(metaPath)) {
1725
- return null;
1726
- }
1727
- try {
1728
- const content = fs8.readFileSync(metaPath, "utf-8");
1729
- return JSON.parse(content);
1730
- } catch (error) {
1731
- handleCorruptedMetadata(metaPath, error, options);
1732
- return null;
1733
- }
1734
- }
1735
- var log6;
1736
- var init_metadata = __esm({
1737
- "src/session/metadata.ts"() {
1738
- "use strict";
1739
- init_logging();
1740
- init_sessionCommand();
1741
- init_atomicFile();
1742
- init_errors();
1743
- init_paths();
1744
- log6 = createLogger("session");
1745
- }
1746
- });
1747
-
1748
1840
  // src/utils/validation.ts
1749
1841
  function valid() {
1750
1842
  return { valid: true };
@@ -2041,6 +2133,10 @@ function formatDuration(ms) {
2041
2133
  }
2042
2134
  return `${ms}ms`;
2043
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
+ }
2044
2140
  var hintsHidden, OutputFormatter, MIN_URL_END_SHOWN, WEB_PROTOCOLS;
2045
2141
  var init_formatting = __esm({
2046
2142
  "src/ui/formatting.ts"() {
@@ -2192,13 +2288,22 @@ function chromeClosedMessage(pid) {
2192
2288
  function orphanedDaemonsCleanedMessage(count) {
2193
2289
  return `Cleaned up ${count} orphaned daemon process${count === 1 ? "" : "es"}`;
2194
2290
  }
2195
- function skillBackupMessage(path7) {
2196
- return `previous copy kept in ${path7}`;
2291
+ function skillBackupMessage(path8) {
2292
+ return `previous copy kept in ${path8}`;
2197
2293
  }
2198
2294
  function fieldLabelList(labels) {
2199
2295
  const shown = labels.slice(0, 5).map((label) => label.replace(/\s*:\s*$/, "")).join(", ");
2200
2296
  return labels.length > 5 ? `${shown} and ${labels.length - 5} more` : shown;
2201
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
+ }
2202
2307
  function requiredFieldsEmptyMessage(labels) {
2203
2308
  const fields = labels.length === 1 ? "field" : "fields";
2204
2309
  return `${labels.length} required ${fields} empty: ${fieldLabelList(labels)}`;
@@ -2235,6 +2340,19 @@ function pageNavigationText(navigation) {
2235
2340
  const status = navigation.status === void 0 ? "" : ` (${navigation.status})`;
2236
2341
  return `navigated to ${navigation.url}${status}`;
2237
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
+ }
2238
2356
  function moreMessagesText(count) {
2239
2357
  return `(+${count} more)`;
2240
2358
  }
@@ -2535,6 +2653,9 @@ function sessionFilesCleanedMessage() {
2535
2653
  function sessionOutputRemovedMessage() {
2536
2654
  return "Session output file removed";
2537
2655
  }
2656
+ function downloadsKeptMessage(dir, files) {
2657
+ return `Downloads kept: ${pluralize(files, "file")} in ${dir} (delete them yourself when done)`;
2658
+ }
2538
2659
  function sessionDirectoryCleanMessage() {
2539
2660
  return "Session directory is now clean";
2540
2661
  }
@@ -2598,7 +2719,7 @@ function helpJsonDetailsNote() {
2598
2719
  function moreCharsNote(count) {
2599
2720
  return `\u2026 ${count} more chars (use --full)`;
2600
2721
  }
2601
- 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;
2602
2723
  var init_commands = __esm({
2603
2724
  "src/ui/messages/commands.ts"() {
2604
2725
  "use strict";
@@ -2607,6 +2728,7 @@ var init_commands = __esm({
2607
2728
  init_sessionCommand();
2608
2729
  init_strings();
2609
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)";
2610
2732
  NO_VISIBLE_EFFECT = "no visible effect observed: no DOM change, requests or navigation within 300 ms";
2611
2733
  HOVER_OFF_DONE = "\u2713 Mouse moved off the page (hover styles and menus that close on mouseleave are gone)";
2612
2734
  HOVER_USAGE = "bdg dom hover <selector|index>, or bdg dom hover --off to move the mouse away";
@@ -2911,7 +3033,7 @@ var init_CommandError = __esm({
2911
3033
  });
2912
3034
 
2913
3035
  // src/ipc/transport/IPCError.ts
2914
- var IPCError, IPCConnectionError, IPCTimeoutError, IPCParseError, IPCEarlyCloseError;
3036
+ var IPCError, IPCConnectionError, IPCTimeoutError, IPCParseError, IPCEarlyCloseError, IPCCancelledError;
2915
3037
  var init_IPCError = __esm({
2916
3038
  "src/ipc/transport/IPCError.ts"() {
2917
3039
  "use strict";
@@ -2969,6 +3091,14 @@ var init_IPCError = __esm({
2969
3091
  this.requestName = requestName;
2970
3092
  }
2971
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
+ };
2972
3102
  }
2973
3103
  });
2974
3104
 
@@ -3120,6 +3250,7 @@ var init_shellDetection = __esm({
3120
3250
  });
3121
3251
 
3122
3252
  // src/errors/messages.ts
3253
+ import * as os3 from "os";
3123
3254
  import * as path4 from "path";
3124
3255
  function sessionAlreadyRunningError(pid, duration, targetUrl) {
3125
3256
  return joinLines(
@@ -3170,6 +3301,12 @@ function commandTimedOutError(seconds) {
3170
3301
  suggestion: `Check the session with: ${sessionCommand("bdg status")}; if the page stays frozen, end it with: ${sessionCommand("bdg cleanup --force")}`
3171
3302
  };
3172
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
+ }
3173
3310
  function sessionUnavailableSuggestion(exitCode) {
3174
3311
  return exitCode === 85 ? `Wait until "${sessionCommand("bdg <url>")}" returns, then retry` : startSessionSuggestion();
3175
3312
  }
@@ -3291,8 +3428,13 @@ function unknownHelpTopicError(topic, closest) {
3291
3428
  suggestion: closest ? `Did you mean: bdg help ${closest}?` : `Run "bdg ${parent ? `${parent} ` : ""}--help" for commands`
3292
3429
  };
3293
3430
  }
3294
- function usageHelpSuggestion(path7) {
3295
- return `Run "${path7} --help" for usage`;
3431
+ function didYouMeanSuggestion(candidates) {
3432
+ const last = candidates.at(-1) ?? "";
3433
+ const rest = candidates.slice(0, -1);
3434
+ return `Did you mean: ${rest.length > 0 ? `${rest.join(", ")} or ${last}` : last}?`;
3435
+ }
3436
+ function usageHelpSuggestion(path8) {
3437
+ return `Run "${path8} --help" for usage`;
3296
3438
  }
3297
3439
  function missingSubcommandMessage() {
3298
3440
  return "Missing subcommand";
@@ -3349,7 +3491,7 @@ function invalidSessionNameError(name, maxLength) {
3349
3491
  function sessionNameSocketTooLongError(name, socketPath, max) {
3350
3492
  return {
3351
3493
  message: `Session name "${name}" makes the daemon socket path too long (${Buffer.byteLength(socketPath)} bytes, at most ${max}): ${socketPath}`,
3352
- suggestion: "Use a shorter session name, or a shorter BDG_SESSION_DIR (e.g. /tmp/bdg)"
3494
+ suggestion: `Use a shorter session name, or a shorter BDG_SESSION_DIR (e.g. ${privateSessionDirExample()})`
3353
3495
  };
3354
3496
  }
3355
3497
  function purgeNeedsNamedSessionError() {
@@ -3364,19 +3506,52 @@ function purgeRefusedError(dir, reason) {
3364
3506
  suggestion: `End the session first (${sessionCommand("bdg cleanup --force")}), then retry: ${sessionCommand("bdg cleanup --purge")}`
3365
3507
  };
3366
3508
  }
3509
+ function privateSessionDirExample() {
3510
+ const runtimeDir = process.env["XDG_RUNTIME_DIR"]?.trim();
3511
+ if (runtimeDir) return path4.join(runtimeDir, "bdg");
3512
+ const uid = process.getuid?.();
3513
+ return path4.join(os3.tmpdir(), uid === void 0 ? "bdg" : `bdg-${uid}`);
3514
+ }
3515
+ function sessionDirSuggestion() {
3516
+ return `Set BDG_SESSION_DIR to a short directory only you can write to, e.g. BDG_SESSION_DIR=${privateSessionDirExample()}`;
3517
+ }
3367
3518
  function sessionDirIsFileError(dir) {
3368
- return { message: `Session directory ${dir} is a file`, suggestion: SESSION_DIR_SUGGESTION };
3519
+ return { message: `Session directory ${dir} is a file`, suggestion: sessionDirSuggestion() };
3369
3520
  }
3370
3521
  function sessionDirNotWritableError(dir, reason) {
3371
3522
  return {
3372
3523
  message: `Session directory ${dir} is not writable (${reason})`,
3373
- suggestion: SESSION_DIR_SUGGESTION
3524
+ suggestion: sessionDirSuggestion()
3525
+ };
3526
+ }
3527
+ function untrustedSessionDirError(untrusted) {
3528
+ return {
3529
+ message: `Session directory ${untrusted.dir} is not safe to use: ${untrusted.reason}`,
3530
+ suggestion: untrustedSessionDirSuggestion(untrusted)
3531
+ };
3532
+ }
3533
+ function untrustedSessionDirSuggestion(untrusted) {
3534
+ const { dir, kind, bdgOwned } = untrusted;
3535
+ const example = `BDG_SESSION_DIR=${privateSessionDirExample()}`;
3536
+ if (kind === "symlink") {
3537
+ return `Remove the link (rm ${dir}) or point BDG_SESSION_DIR at the real directory`;
3538
+ }
3539
+ if (kind === "shared") {
3540
+ return `${dir} is shared by all users; use a subdirectory only you can write to, e.g. ${example}`;
3541
+ }
3542
+ if (bdgOwned) return `Run chmod 700 ${dir} (or remove it if it is not yours), then retry`;
3543
+ return `Use a directory you own that others cannot write to, e.g. ${example} (or chmod 700 ${dir} if it is yours)`;
3544
+ }
3545
+ function daemonLogNotOpenedError(logPath, code) {
3546
+ return {
3547
+ message: `Cannot open the daemon log ${logPath} (${code === "ELOOP" ? "it is a symbolic link" : code})`,
3548
+ suggestion: `Remove ${logPath} if you did not create it, then retry`
3374
3549
  };
3375
3550
  }
3376
3551
  function socketPathTooLongError(socketPath, max) {
3377
3552
  return {
3378
3553
  message: `Session directory path is too long for the daemon socket (${Buffer.byteLength(socketPath)} bytes, at most ${max}): ${socketPath}`,
3379
- suggestion: SESSION_DIR_SUGGESTION
3554
+ suggestion: sessionDirSuggestion()
3380
3555
  };
3381
3556
  }
3382
3557
  function externalChromeUnreachableError(endpoint, secure) {
@@ -3575,18 +3750,6 @@ function waitTargetRequiredError() {
3575
3750
  suggestion: `e.g. ${sessionCommand("bdg dom wait '#result' --visible")}, ${sessionCommand("bdg dom wait body --text 'Welcome'")} or ${sessionCommand("bdg dom wait --load")}`
3576
3751
  };
3577
3752
  }
3578
- function elementNotVisibleError() {
3579
- return {
3580
- message: "Element is not rendered (e.g. display: none), so it has no area to capture",
3581
- suggestion: "Make it visible first, or capture a visible ancestor"
3582
- };
3583
- }
3584
- function elementZeroDimensionsError() {
3585
- return {
3586
- message: "Element has zero width or height, so there is nothing to capture",
3587
- suggestion: "It may be collapsed or empty; capture a visible ancestor instead"
3588
- };
3589
- }
3590
3753
  function hoverOffWithTargetError() {
3591
3754
  return {
3592
3755
  message: "--off moves the mouse off the page; it takes no element",
@@ -3709,16 +3872,44 @@ function formInIframeError(iframeUrl, crossOrigin) {
3709
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>'
3710
3873
  };
3711
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
+ }
3712
3903
  function skillSourceMissingError(source) {
3713
3904
  return {
3714
3905
  message: `The bdg skill is missing from the installed package (${source})`,
3715
3906
  suggestion: "Reinstall bdg: npm install -g browser-debugger-cli"
3716
3907
  };
3717
3908
  }
3718
- function skillWriteFailedError(path7, reason, pathIsFile, otherTargetFlag) {
3909
+ function skillWriteFailedError(path8, reason, pathIsFile, otherTargetFlag) {
3719
3910
  const fix = pathIsFile ? "A part of that path is a file, not a directory: move it away" : "Check the permissions of that directory";
3720
3911
  return {
3721
- message: `Could not write ${path7}: ${reason}`,
3912
+ message: `Could not write ${path8}: ${reason}`,
3722
3913
  suggestion: otherTargetFlag ? `${fix}, or install for the other agent only (${otherTargetFlag})` : fix
3723
3914
  };
3724
3915
  }
@@ -3731,7 +3922,7 @@ function intervalWithoutFollowError() {
3731
3922
  function unknownAuditCheckMessage(value, similar, checks) {
3732
3923
  return `Unknown check "${value}"${similar.length > 0 ? `; did you mean ${similar[0]}?` : ""} (checks: ${checks.join(", ")})`;
3733
3924
  }
3734
- var LAUNCHED_CHROME_DESCRIPTION, A11Y_NAME_QUERY_EXAMPLE, SCOPED_FILTER_EXAMPLES, PLAYWRIGHT_ONLY_SYNTAX, MALFORMED_SELECTOR_DETAILS, SESSION_DIR_SUGGESTION, SCROLL_PROBLEMS, VIA_LABEL_SUFFIX, NAME_QUERY_PLACEHOLDER, LABEL_WITHOUT_CONTROL, REDECLARED_PATTERN;
3925
+ var LAUNCHED_CHROME_DESCRIPTION, A11Y_NAME_QUERY_EXAMPLE, SCOPED_FILTER_EXAMPLES, PLAYWRIGHT_ONLY_SYNTAX, MALFORMED_SELECTOR_DETAILS, SCROLL_PROBLEMS, VIA_LABEL_SUFFIX, NAME_QUERY_PLACEHOLDER, LABEL_WITHOUT_CONTROL, REDECLARED_PATTERN;
3735
3926
  var init_messages = __esm({
3736
3927
  "src/errors/messages.ts"() {
3737
3928
  "use strict";
@@ -3752,7 +3943,6 @@ var init_messages = __esm({
3752
3943
  "empty-has": () => ":has() has an empty selector",
3753
3944
  "scope-in-has": () => ":has() with filters is already relative to the element; write :has(> a:visible) instead of :has(:scope > a:visible)"
3754
3945
  };
3755
- SESSION_DIR_SUGGESTION = "Set BDG_SESSION_DIR to a short, writable directory, e.g. BDG_SESSION_DIR=/tmp/bdg";
3756
3946
  SCROLL_PROBLEMS = {
3757
3947
  "index-without-selector": [
3758
3948
  "--index requires a selector",
@@ -3886,6 +4076,47 @@ var init_suggestions = __esm({
3886
4076
  }
3887
4077
  });
3888
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
+
3889
4120
  // src/ipc/transport/errors.ts
3890
4121
  function formatConnectionError(requestName, socketPath, error) {
3891
4122
  const code = error.code;
@@ -3907,6 +4138,9 @@ function formatTimeoutError(requestName, timeoutMs) {
3907
4138
  function formatEarlyCloseError(requestName) {
3908
4139
  return new IPCEarlyCloseError(requestName);
3909
4140
  }
4141
+ function formatCancelledError(requestName) {
4142
+ return new IPCCancelledError(requestName);
4143
+ }
3910
4144
  var init_errors4 = __esm({
3911
4145
  "src/ipc/transport/errors.ts"() {
3912
4146
  "use strict";
@@ -4075,13 +4309,30 @@ var init_validation2 = __esm({
4075
4309
  });
4076
4310
 
4077
4311
  // src/ipc/transport/index.ts
4078
- async function sendRequest(request, requestName, expectedType, timeoutMs = getIPCRequestTimeout(), socketPath = getDaemonSocketPath()) {
4312
+ import * as path5 from "path";
4313
+ async function sendRequest(request, requestName, expectedType, timeoutMs = getIPCRequestTimeout(), socketPath = getDaemonSocketPath(), signal) {
4314
+ const untrusted = secureSessionDir(path5.dirname(socketPath));
4315
+ if (untrusted) {
4316
+ const err = untrustedSessionDirError(untrusted);
4317
+ throw new CommandError(
4318
+ err.message,
4319
+ { suggestion: err.suggestion },
4320
+ EXIT_CODES.SESSION_FILE_ERROR
4321
+ );
4322
+ }
4079
4323
  return new Promise((resolve4, reject2) => {
4324
+ if (signal?.aborted) {
4325
+ reject2(formatCancelledError(requestName));
4326
+ return;
4327
+ }
4080
4328
  const buffer = new JSONLBuffer();
4081
4329
  let resolved = false;
4330
+ let onAbort = () => {
4331
+ };
4082
4332
  const resolveOnce = (cleanup2, error, response) => {
4083
4333
  if (resolved) return;
4084
4334
  resolved = true;
4335
+ signal?.removeEventListener("abort", onAbort);
4085
4336
  cleanup2();
4086
4337
  if (error) {
4087
4338
  reject2(error);
@@ -4127,6 +4378,8 @@ async function sendRequest(request, requestName, expectedType, timeoutMs = getIP
4127
4378
  }
4128
4379
  }
4129
4380
  );
4381
+ onAbort = () => resolveOnce(cleanup, formatCancelledError(requestName));
4382
+ signal?.addEventListener("abort", onAbort, { once: true });
4130
4383
  });
4131
4384
  }
4132
4385
  var log10;
@@ -4134,8 +4387,11 @@ var init_transport = __esm({
4134
4387
  "src/ipc/transport/index.ts"() {
4135
4388
  "use strict";
4136
4389
  init_constants();
4390
+ init_errors3();
4391
+ init_messages();
4137
4392
  init_paths();
4138
4393
  init_logging();
4394
+ init_exitCodes();
4139
4395
  init_errors4();
4140
4396
  init_jsonl();
4141
4397
  init_socket();
@@ -4231,7 +4487,7 @@ async function getHARData() {
4231
4487
  await assertResponsive();
4232
4488
  return sendRequest(request, "HAR data", "har_data_response");
4233
4489
  }
4234
- async function startSession(url, options) {
4490
+ async function startSession(url, options, signal) {
4235
4491
  const request = withSession({
4236
4492
  type: "start_session_request",
4237
4493
  url,
@@ -4253,7 +4509,10 @@ async function startSession(url, options) {
4253
4509
  return sendRequest(
4254
4510
  request,
4255
4511
  "start session",
4256
- "start_session_response"
4512
+ "start_session_response",
4513
+ void 0,
4514
+ void 0,
4515
+ signal
4257
4516
  );
4258
4517
  }
4259
4518
  async function stopSession() {
@@ -4265,7 +4524,7 @@ async function stopSession() {
4265
4524
  "stop_session_response"
4266
4525
  );
4267
4526
  }
4268
- async function sendCommand(commandName, params, timeoutMs) {
4527
+ async function sendCommand(commandName, params, timeoutMs, signal) {
4269
4528
  const request = {
4270
4529
  ...params,
4271
4530
  type: `${commandName}_request`,
@@ -4276,7 +4535,9 @@ async function sendCommand(commandName, params, timeoutMs) {
4276
4535
  request,
4277
4536
  commandName,
4278
4537
  `${commandName}_response`,
4279
- timeoutMs
4538
+ timeoutMs,
4539
+ void 0,
4540
+ signal
4280
4541
  );
4281
4542
  }
4282
4543
  async function getDetails(type, id) {
@@ -4311,8 +4572,12 @@ async function callCDP(method, params, options = {}) {
4311
4572
  function callBdgScript(method, params) {
4312
4573
  return callCDP(method, params, { isolated: true });
4313
4574
  }
4314
- async function domEval(script, frame) {
4315
- return sendCommand("dom_eval", { script, ...frame !== void 0 && { frame } });
4575
+ async function domEval(script, frame, full) {
4576
+ return sendCommand("dom_eval", {
4577
+ script,
4578
+ ...frame !== void 0 && { frame },
4579
+ ...full && { full }
4580
+ });
4316
4581
  }
4317
4582
  async function domFrames() {
4318
4583
  return sendCommand("dom_frames", {});
@@ -4356,6 +4621,9 @@ async function cssSearch(params) {
4356
4621
  async function domInspect(params) {
4357
4622
  return sendCommand("dom_inspect", params);
4358
4623
  }
4624
+ async function domScreenshot(params, signal) {
4625
+ return sendCommand("dom_screenshot", params, void 0, signal);
4626
+ }
4359
4627
  async function domWait(params) {
4360
4628
  return sendCommand(
4361
4629
  "dom_wait",
@@ -4376,36 +4644,6 @@ var init_client = __esm({
4376
4644
  }
4377
4645
  });
4378
4646
 
4379
- // src/connection/errors.ts
4380
- var ConnectionError, CDPConnectionError;
4381
- var init_errors5 = __esm({
4382
- "src/connection/errors.ts"() {
4383
- "use strict";
4384
- init_exitCodes();
4385
- ConnectionError = class extends Error {
4386
- issue;
4387
- constructor(message, options) {
4388
- super(message);
4389
- this.name = this.constructor.name;
4390
- if (options instanceof Error) {
4391
- this.cause = options;
4392
- return;
4393
- }
4394
- if (options?.cause) {
4395
- this.cause = options.cause;
4396
- }
4397
- if (options?.issue) {
4398
- this.issue = options.issue;
4399
- }
4400
- }
4401
- };
4402
- CDPConnectionError = class extends ConnectionError {
4403
- code = "CDP_CONNECTION_ERROR";
4404
- exitCode = EXIT_CODES.CDP_CONNECTION_FAILURE;
4405
- };
4406
- }
4407
- });
4408
-
4409
4647
  // node_modules/ws/lib/constants.js
4410
4648
  var require_constants = __commonJS({
4411
4649
  "node_modules/ws/lib/constants.js"(exports, module) {
@@ -8109,7 +8347,7 @@ function textPreview2(text, length = PREVIEW_LENGTH) {
8109
8347
  const characters = Array.from(collapsed);
8110
8348
  return characters.length > length ? characters.slice(0, length).join("") + "..." : collapsed;
8111
8349
  }
8112
- var CLASS_FRAGMENT, LABEL_CLASSES_JS, PREVIEW_LENGTH, ELEMENT_TEXT_LENGTH, WITHOUT_DECORATIONS_JS, COMPOSED_JS, FLAT_TEXT_JS, ELEMENT_TEXT_JS, MASKED_VALUE, SENSITIVE_FIELD_JS, ELEMENT_STATE_JS, ELEMENT_DESCRIPTION_JS, SIBLING_POSITION_JS, ELEMENT_IDENTITY_JS, ELEMENT_CONTEXT_JS;
8350
+ var CLASS_FRAGMENT, LABEL_CLASSES_JS, PREVIEW_LENGTH, ELEMENT_TEXT_LENGTH, WITHOUT_DECORATIONS_JS, COMPOSED_JS, FLAT_TEXT_JS, ELEMENT_TEXT_JS, MASKED_VALUE, SENSITIVE_NAME_SOURCE, SENSITIVE_FIELD_JS, ELEMENT_STATE_JS, ELEMENT_DESCRIPTION_JS, SIBLING_POSITION_JS, ELEMENT_IDENTITY_JS, ELEMENT_CONTEXT_JS;
8113
8351
  var init_elementInfo = __esm({
8114
8352
  "src/runtime/dom/elementInfo.ts"() {
8115
8353
  "use strict";
@@ -8200,12 +8438,13 @@ var init_elementInfo = __esm({
8200
8438
  return withoutDecorations(el, start);
8201
8439
  }`;
8202
8440
  MASKED_VALUE = "\u2022\u2022\u2022\u2022";
8441
+ SENSITIVE_NAME_SOURCE = "passw|passwd|pwd|passcode|passphrase|(^|[^a-z])(otp|pin|ssn)([^a-z]|$)|cvv|cvc|card[-_]?(num(ber)?|no)([^a-z]|$)";
8203
8442
  SENSITIVE_FIELD_JS = `(el) => {
8204
8443
  const autocomplete = el.getAttribute('autocomplete') || '';
8205
8444
  if (/(^|\\s)(cc-[a-z-]+|one-time-code|current-password|new-password)(\\s|$)/i.test(autocomplete)) return true;
8206
8445
  if (el.type === 'password' || /^password$/i.test(el.getAttribute('type') || '')) return true;
8207
8446
  const names = [el.getAttribute('name'), el.id, autocomplete].join(' ');
8208
- if (/passw|passwd|pwd|passcode|(^|[^a-z])otp([^a-z]|$)|cvv|cvc/i.test(names)) return true;
8447
+ if (/${SENSITIVE_NAME_SOURCE}/i.test(names)) return true;
8209
8448
  try {
8210
8449
  const security = el.ownerDocument.defaultView.getComputedStyle(el).getPropertyValue('-webkit-text-security');
8211
8450
  return Boolean(security) && security !== 'none';
@@ -8990,10 +9229,10 @@ function findHasFilters(text, selector) {
8990
9229
  }
8991
9230
  function parseHasArgument(argument, selector) {
8992
9231
  if (!FILTER_NAME_HINT.test(argument)) return null;
8993
- const selectors = splitSelectorList(argument).map((relative) => {
8994
- if (relative.trim() === "") reject(malformedSelectorError(selector, "empty-has"));
8995
- if (SCOPE_PSEUDO.test(relative)) reject(malformedSelectorError(selector, "scope-in-has"));
8996
- const { leading, steps } = parseChain(relative, selector);
9232
+ const selectors = splitSelectorList(argument).map((relative2) => {
9233
+ if (relative2.trim() === "") reject(malformedSelectorError(selector, "empty-has"));
9234
+ if (SCOPE_PSEUDO.test(relative2)) reject(malformedSelectorError(selector, "scope-in-has"));
9235
+ const { leading, steps } = parseChain(relative2, selector);
8997
9236
  if (leading && leading !== ">") reject(siblingInHasError(selector, leading));
8998
9237
  const [first, ...rest] = steps;
8999
9238
  return first ? [{ ...first, combinator: leading || " " }, ...rest] : [];
@@ -9673,7 +9912,7 @@ async function resolveSelector(selector) {
9673
9912
  EXIT_CODES.RESOURCE_NOT_FOUND
9674
9913
  );
9675
9914
  }
9676
- return { backendNodeId };
9915
+ return backendNodeId;
9677
9916
  }
9678
9917
  async function resolveBackendNodeIds(selectors) {
9679
9918
  return mapConcurrently(selectors, async (selector) => (await selectAll(selector))[0]);
@@ -9761,7 +10000,7 @@ var init_query = __esm({
9761
10000
 
9762
10001
  // src/commands/shared/outputFile.ts
9763
10002
  import * as fs15 from "fs";
9764
- import * as path5 from "path";
10003
+ import * as path6 from "path";
9765
10004
  function outputPathError(filePath, error, extension2) {
9766
10005
  const code = error.code ?? "";
9767
10006
  const problem = PATH_PROBLEMS[code];
@@ -9790,13 +10029,16 @@ function assertFilePath(filePath, extension2) {
9790
10029
  );
9791
10030
  }
9792
10031
  }
9793
- async function writeOutputFile(filePath, data, extension2) {
10032
+ async function writeOutputFile(filePath, data, extension2, mode) {
9794
10033
  assertFilePath(filePath, extension2);
9795
- const absolutePath = path5.resolve(filePath);
10034
+ const absolutePath = path6.resolve(filePath);
9796
10035
  try {
9797
- makeDirectory(path5.dirname(absolutePath));
9798
- if (typeof data === "string") await AtomicFileWriter.writeAsync(absolutePath, data);
9799
- else await AtomicFileWriter.writeBufferAsync(absolutePath, data);
10036
+ makeDirectory(path6.dirname(absolutePath));
10037
+ if (typeof data === "string") {
10038
+ await AtomicFileWriter.writeAsync(absolutePath, data, mode === void 0 ? {} : { mode });
10039
+ } else {
10040
+ await AtomicFileWriter.writeBufferAsync(absolutePath, data);
10041
+ }
9800
10042
  } catch (error) {
9801
10043
  throw outputPathError(filePath, error, extension2);
9802
10044
  }
@@ -9829,575 +10071,45 @@ var init_outputFile = __esm({
9829
10071
  }
9830
10072
  });
9831
10073
 
9832
- // src/runtime/page/userAgent.ts
9833
- var init_userAgent = __esm({
9834
- "src/runtime/page/userAgent.ts"() {
9835
- "use strict";
9836
- init_errors();
9837
- }
9838
- });
9839
-
9840
- // src/runtime/page/emulation.ts
9841
- function viewportOverride(viewport, deviceScaleFactor = 0) {
9842
- const mobile = viewport.mobile === true;
9843
- return {
9844
- width: viewport.width,
9845
- height: viewport.height,
9846
- deviceScaleFactor: deviceScaleFactor || (mobile ? MOBILE_PIXEL_RATIO : 0),
9847
- mobile
9848
- };
9849
- }
9850
- var log24, MOBILE_PIXEL_RATIO;
9851
- var init_emulation = __esm({
9852
- "src/runtime/page/emulation.ts"() {
9853
- "use strict";
9854
- init_elementGeometry();
9855
- init_userAgent();
9856
- init_logging();
9857
- init_errors();
9858
- log24 = createLogger("session");
9859
- MOBILE_PIXEL_RATIO = 3;
9860
- }
9861
- });
9862
-
9863
10074
  // src/commands/dom/helpers/screenshot.ts
9864
- async function waitForPostScrollStability() {
9865
- const deadline = Date.now() + POST_SCROLL_MAX_WAIT_MS;
9866
- await callBdgScript("Runtime.evaluate", {
9867
- expression: `
9868
- (() => {
9869
- window.__bdg_scrollStability = {
9870
- lastNetworkActivity: Date.now(),
9871
- lastDomMutation: Date.now(),
9872
- activeRequests: 0
9873
- };
9874
-
9875
- const state = window.__bdg_scrollStability;
9876
-
9877
- if (window.PerformanceObserver) {
9878
- const perfObserver = new PerformanceObserver((list) => {
9879
- for (const entry of list.getEntries()) {
9880
- if (entry.entryType === 'resource') {
9881
- state.lastNetworkActivity = Date.now();
9882
- }
9883
- }
9884
- });
9885
- try {
9886
- perfObserver.observe({ entryTypes: ['resource'] });
9887
- state.perfObserver = perfObserver;
9888
- } catch (e) {}
9889
- }
9890
-
9891
- const mutationObserver = new MutationObserver(() => {
9892
- state.lastDomMutation = Date.now();
9893
- });
9894
- mutationObserver.observe(document.body || document.documentElement, {
9895
- childList: true,
9896
- subtree: true,
9897
- attributes: true
9898
- });
9899
- state.mutationObserver = mutationObserver;
9900
- })()
9901
- `,
9902
- returnByValue: true
9903
- });
9904
- try {
9905
- while (Date.now() < deadline) {
9906
- const checkResult = await callBdgScript("Runtime.evaluate", {
9907
- expression: `
9908
- (() => {
9909
- const state = window.__bdg_scrollStability;
9910
- if (!state) return { networkIdle: 999, domIdle: 999 };
9911
- return {
9912
- networkIdle: Date.now() - state.lastNetworkActivity,
9913
- domIdle: Date.now() - state.lastDomMutation
9914
- };
9915
- })()
9916
- `,
9917
- returnByValue: true
9918
- });
9919
- const value = checkResult.data?.result?.result?.value;
9920
- const networkIdle = value?.networkIdle ?? 0;
9921
- const domIdle = value?.domIdle ?? 0;
9922
- if (networkIdle >= POST_SCROLL_NETWORK_IDLE_MS && domIdle >= POST_SCROLL_DOM_STABLE_MS) {
9923
- log25.debug(`Post-scroll stable: network ${networkIdle}ms, DOM ${domIdle}ms`);
9924
- return;
9925
- }
9926
- await new Promise((resolve4) => setTimeout(resolve4, STABILITY_CHECK_INTERVAL_MS));
9927
- }
9928
- log25.debug("Post-scroll stability timeout, proceeding anyway");
9929
- } finally {
9930
- await callBdgScript("Runtime.evaluate", {
9931
- expression: `
9932
- (() => {
9933
- const state = window.__bdg_scrollStability;
9934
- if (state) {
9935
- state.perfObserver?.disconnect();
9936
- state.mutationObserver?.disconnect();
9937
- delete window.__bdg_scrollStability;
9938
- }
9939
- })()
9940
- `,
9941
- returnByValue: true
9942
- });
9943
- }
9944
- }
9945
- async function scrollToElement(selector) {
9946
- const result = await callBdgScript("Runtime.evaluate", {
9947
- expression: `
9948
- (() => {
9949
- const el = (${DEEP_QUERY_JS})(${selectorArgsJS(selector)})[0];
9950
- if (!el) return { found: false };
9951
- const originalX = window.scrollX;
9952
- const originalY = window.scrollY;
9953
- el.scrollIntoView({ block: 'center', behavior: 'instant' });
9954
- return { found: true, originalX, originalY };
9955
- })()
9956
- `,
9957
- returnByValue: true
9958
- });
9959
- const value = result.data?.result?.result?.value;
9960
- if (!value?.found) {
9961
- const err = noNodesFoundError(selector);
9962
- throw new CommandError(
9963
- err.message,
9964
- { suggestion: err.suggestion },
9965
- EXIT_CODES.RESOURCE_NOT_FOUND
9966
- );
9967
- }
9968
- await waitForPostScrollStability();
9969
- return { x: value.originalX ?? 0, y: value.originalY ?? 0 };
9970
- }
9971
- async function restoreScrollPosition(position) {
9972
- await callBdgScript("Runtime.evaluate", {
9973
- expression: `window.scrollTo(${position.x}, ${position.y})`,
9974
- returnByValue: true
9975
- });
9976
- }
9977
- async function windowSize(viewport) {
9978
- const response = await callBdgScript("Runtime.evaluate", {
9979
- expression: "[window.innerWidth, window.innerHeight]",
9980
- returnByValue: true
9981
- });
9982
- const value = response.data?.result?.result?.value;
9983
- const [width, height] = Array.isArray(value) ? value : [];
9984
- return {
9985
- width: Math.round(width ?? viewport.clientWidth),
9986
- height: Math.round(height ?? viewport.clientHeight)
9987
- };
9988
- }
9989
- async function useUnitPixelRatio(devicePixelRatio, viewport) {
9990
- if (devicePixelRatio === 1) return () => Promise.resolve();
9991
- const sessionViewport = readSessionMetadata()?.viewport;
9992
- const size = sessionViewport ?? await windowSize(viewport);
9993
- await callCDP("Emulation.setDeviceMetricsOverride", viewportOverride(size, 1));
9994
- return restoreSessionMetrics;
9995
- }
9996
- async function restoreSessionMetrics() {
9997
- const sessionViewport = readSessionMetadata()?.viewport;
9998
- if (!sessionViewport) {
9999
- await callCDP("Emulation.clearDeviceMetricsOverride", {});
10000
- return;
10001
- }
10002
- await callCDP("Emulation.setDeviceMetricsOverride", viewportOverride(sessionViewport));
10003
- if (sessionViewport.mobile) {
10004
- await callCDP("Emulation.setTouchEmulationEnabled", { enabled: true, maxTouchPoints: 5 });
10005
- }
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;
10006
10085
  }
10007
- async function getElementBounds(ref) {
10008
- const response = await callCDP("DOM.getBoxModel", ref);
10009
- const boxModel = response.data?.result;
10010
- if (!boxModel?.model?.border) {
10011
- const err = elementNotVisibleError();
10012
- throw new CommandError(
10013
- err.message,
10014
- { suggestion: err.suggestion },
10015
- EXIT_CODES.INVALID_ARGUMENTS
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")}`
10016
10092
  );
10017
- }
10018
- const border = boxModel.model.border;
10019
- const x = border[0] ?? 0;
10020
- const y = border[1] ?? 0;
10021
- const width = (border[2] ?? 0) - x;
10022
- const height = (border[5] ?? 0) - y;
10023
- if (width <= 0 || height <= 0) {
10024
- const err = elementZeroDimensionsError();
10093
+ const suggestion = response.error === void 0 ? fallback.suggestion : response.suggestion;
10025
10094
  throw new CommandError(
10026
- err.message,
10027
- { suggestion: err.suggestion },
10028
- EXIT_CODES.INVALID_ARGUMENTS
10095
+ response.error ?? fallback.message,
10096
+ suggestion ? { suggestion } : {},
10097
+ response.exitCode ?? EXIT_CODES.CDP_CONNECTION_FAILURE
10029
10098
  );
10030
10099
  }
10031
- return { x, y, width, height };
10032
- }
10033
- async function visibleAreaOrigin() {
10034
- const response = await callCDP("Page.getLayoutMetrics", {});
10035
- const metrics = response.data?.result;
10036
- const viewport = metrics?.cssVisualViewport;
10037
- return { x: viewport?.pageX ?? 0, y: viewport?.pageY ?? 0 };
10100
+ const path8 = await writeOutputFile(outputPath, Buffer.from(response.data.image, "base64"));
10101
+ return { path: path8, ...response.data.screenshot };
10038
10102
  }
10039
- async function capturePageScreenshot(outputPath, options = {}) {
10040
- const format = options.format ?? "png";
10041
- const quality = format === "jpeg" ? options.quality ?? 90 : void 0;
10042
- const requestedFullPage = options.fullPage ?? true;
10043
- const noResize = options.noResize ?? false;
10044
- let originalScrollPosition;
10045
- if (options.scroll) {
10046
- originalScrollPosition = await scrollToElement(options.scroll);
10047
- }
10048
- const dprResponse = await callBdgScript("Runtime.evaluate", {
10049
- expression: "window.devicePixelRatio",
10050
- returnByValue: true
10051
- });
10052
- const devicePixelRatio = dprResponse.data?.result?.result?.value ?? 1;
10053
- const metricsResponse = await callCDP("Page.getLayoutMetrics", {});
10054
- const metricsResult = metricsResponse.data?.result;
10055
- const contentSize = metricsResult?.contentSize ?? { width: 0, height: 0 };
10056
- const viewport = metricsResult?.visualViewport ?? { clientWidth: 0, clientHeight: 0 };
10057
- const pageIsTooTall = !noResize && requestedFullPage && isTallPage(contentSize.width, contentSize.height);
10058
- const useScroll = options.scroll !== void 0;
10059
- const effectiveFullPage = useScroll ? false : pageIsTooTall ? false : requestedFullPage;
10060
- const captureWidth = effectiveFullPage ? contentSize.width : viewport.clientWidth;
10061
- const captureHeight = effectiveFullPage ? contentSize.height : viewport.clientHeight;
10062
- const resized = shouldResize(captureWidth, captureHeight, noResize);
10063
- const scale = resized ? calculateResizeScale(captureWidth, captureHeight) : 1;
10064
- const finalWidth = Math.round(captureWidth * scale);
10065
- const finalHeight = Math.round(captureHeight * scale);
10066
- const restoreMetrics = await useUnitPixelRatio(devicePixelRatio, viewport);
10067
- if (devicePixelRatio !== 1) {
10068
- if (options.scroll) {
10069
- await callBdgScript("Runtime.evaluate", {
10070
- expression: `(${DEEP_QUERY_JS})(${selectorArgsJS(options.scroll)})[0]?.scrollIntoView({ block: 'center', behavior: 'instant' })`,
10071
- returnByValue: true
10072
- });
10073
- }
10074
- }
10075
- const clipOrigin = effectiveFullPage ? { x: 0, y: 0 } : await visibleAreaOrigin();
10076
- let screenshotResult;
10077
- try {
10078
- const screenshotResponse = await callCDP("Page.captureScreenshot", {
10079
- format,
10080
- ...quality !== void 0 && { quality },
10081
- captureBeyondViewport: effectiveFullPage,
10082
- clip: {
10083
- x: clipOrigin.x,
10084
- y: clipOrigin.y,
10085
- width: captureWidth,
10086
- height: captureHeight,
10087
- scale
10088
- }
10089
- });
10090
- screenshotResult = screenshotResponse.data?.result;
10091
- } finally {
10092
- await restoreMetrics();
10093
- }
10094
- if (!screenshotResult?.data) {
10095
- throw new CDPConnectionError("No screenshot data returned", new Error("Empty response"));
10096
- }
10097
- const buffer = Buffer.from(screenshotResult.data, "base64");
10098
- const absolutePath = await writeOutputFile(outputPath, buffer);
10099
- const result = {
10100
- path: absolutePath,
10101
- format,
10102
- width: finalWidth,
10103
- height: finalHeight,
10104
- size: buffer.length,
10105
- fullPage: effectiveFullPage,
10106
- captureMode: effectiveFullPage ? "full_page" : "viewport",
10107
- finalTokens: calculateImageTokens(finalWidth, finalHeight)
10108
- };
10109
- if (quality !== void 0) {
10110
- result.quality = quality;
10111
- }
10112
- if (!effectiveFullPage) {
10113
- result.viewport = {
10114
- width: viewport.clientWidth,
10115
- height: viewport.clientHeight
10116
- };
10117
- }
10118
- if (resized) {
10119
- result.resized = true;
10120
- result.originalWidth = captureWidth;
10121
- result.originalHeight = captureHeight;
10122
- result.originalTokens = calculateImageTokens(captureWidth, captureHeight);
10123
- }
10124
- if (pageIsTooTall && !useScroll) {
10125
- const aspectRatio = Math.round(contentSize.height / contentSize.width * 10) / 10;
10126
- result.fullPageSkipped = {
10127
- reason: "page_too_tall",
10128
- originalHeight: contentSize.height,
10129
- aspectRatio
10130
- };
10131
- result.warning = `Full page capture skipped: page too tall (${aspectRatio}:1 aspect ratio). Only viewport captured.`;
10132
- }
10133
- if (useScroll && options.scroll) {
10134
- result.scrolledTo = options.scroll;
10135
- }
10136
- if (originalScrollPosition) {
10137
- await restoreScrollPosition(originalScrollPosition);
10138
- }
10139
- return result;
10140
- }
10141
- async function visibleViewport() {
10142
- const metrics = (await callCDP("Page.getLayoutMetrics", {})).data?.result;
10143
- const view = metrics?.cssVisualViewport ?? metrics?.visualViewport;
10144
- return { width: view?.clientWidth ?? 0, height: view?.clientHeight ?? 0 };
10145
- }
10146
- function insideView(area, view) {
10147
- return area.x >= 0 && area.y >= 0 && area.x + area.width <= view.width && area.y + area.height <= view.height;
10148
- }
10149
- async function measureInView(ref, padding) {
10150
- const view = await visibleViewport();
10151
- let box = await getElementBounds(ref);
10152
- let bounds = await captureArea(ref, box, padding);
10153
- const fits = bounds.width <= view.width && bounds.height <= view.height;
10154
- if (!fits) {
10155
- await keepLayoutWithoutScrollbars(view);
10156
- box = await getElementBounds(ref);
10157
- return { box, bounds: await captureArea(ref, box, padding), inView: false };
10158
- }
10159
- if (insideView(bounds, view)) return { box, bounds, inView: true };
10160
- const scrolledFrom = await scrollPosition();
10161
- const dx = bounds.x + bounds.width / 2 - view.width / 2;
10162
- const dy = bounds.y + bounds.height / 2 - view.height / 2;
10163
- await callBdgScript("Runtime.evaluate", { expression: `window.scrollBy(${dx}, ${dy})` });
10164
- try {
10165
- box = await getElementBounds(ref);
10166
- bounds = await captureArea(ref, box, padding);
10167
- } catch (error) {
10168
- await restoreScrollPosition(scrolledFrom);
10169
- throw error;
10170
- }
10171
- return { box, bounds, inView: insideView(bounds, view), scrolledFrom };
10172
- }
10173
- async function keepLayoutWithoutScrollbars(view) {
10174
- const phone = readSessionMetadata()?.viewport?.mobile;
10175
- await callCDP("Emulation.setScrollbarsHidden", { hidden: true });
10176
- await callCDP(
10177
- "Emulation.setDeviceMetricsOverride",
10178
- viewportOverride({ ...view, ...phone && { mobile: true } }, 1)
10179
- );
10180
- }
10181
- async function restoreViewport() {
10182
- await callCDP("Emulation.setScrollbarsHidden", { hidden: false });
10183
- await restoreSessionMetrics();
10184
- }
10185
- async function scrollPosition() {
10186
- const response = await callBdgScript("Runtime.evaluate", {
10187
- expression: "[window.scrollX, window.scrollY]",
10188
- returnByValue: true
10189
- });
10190
- const value = response.data?.result?.result?.value;
10191
- const [x, y] = Array.isArray(value) ? value : [];
10192
- return { x: x ?? 0, y: y ?? 0 };
10193
- }
10194
- async function captureArea(ref, bounds, padding) {
10195
- const area = await paintedArea(ref, bounds);
10196
- return padding > 0 ? {
10197
- x: area.x - padding,
10198
- y: area.y - padding,
10199
- width: area.width + 2 * padding,
10200
- height: area.height + 2 * padding
10201
- } : area;
10202
- }
10203
- async function paintedArea(ref, bounds) {
10204
- const objectGroup = `bdg-shot-${process.pid}`;
10205
- try {
10206
- const resolved = await callBdgScript("DOM.resolveNode", { ...ref, objectGroup });
10207
- const objectId = resolved.data?.result?.object.objectId;
10208
- if (!objectId) return bounds;
10209
- const response = await callCDP("Runtime.callFunctionOn", {
10210
- objectId,
10211
- functionDeclaration: CONTENT_OVERFLOW_JS,
10212
- returnByValue: true
10213
- });
10214
- const overflow = response.data?.result?.result?.value;
10215
- if (!overflow) return bounds;
10216
- const [left, top, right, bottom] = ["left", "top", "right", "bottom"].map(
10217
- (side) => Math.max(0, overflow[side] ?? 0)
10218
- );
10219
- if (Math.max(left, top, right, bottom) <= OVERFLOW_SLACK) return bounds;
10220
- return {
10221
- x: bounds.x - left,
10222
- y: bounds.y - top,
10223
- width: bounds.width + left + right,
10224
- height: bounds.height + top + bottom
10225
- };
10226
- } catch (error) {
10227
- log25.debug(`Could not measure overflowing content: ${String(error)}`);
10228
- return bounds;
10229
- } finally {
10230
- await callCDP("Runtime.releaseObjectGroup", { objectGroup }).catch(() => void 0);
10231
- }
10232
- }
10233
- async function captureElementScreenshot(outputPath, ref, options = {}) {
10234
- const format = options.format ?? "png";
10235
- const quality = format === "jpeg" ? options.quality ?? 90 : void 0;
10236
- const noResize = options.noResize ?? false;
10237
- const dprResponse = await callBdgScript("Runtime.evaluate", {
10238
- expression: "window.devicePixelRatio",
10239
- returnByValue: true
10240
- });
10241
- const devicePixelRatio = dprResponse.data?.result?.result?.value ?? 1;
10242
- const before = (await callCDP("Page.getLayoutMetrics", {})).data?.result;
10243
- const restoreMetrics = await useUnitPixelRatio(
10244
- devicePixelRatio,
10245
- before?.visualViewport ?? { clientWidth: 800, clientHeight: 600 }
10246
- );
10247
- const restore = async (wide, scrolledFrom2) => {
10248
- await (wide ? restoreViewport() : restoreMetrics());
10249
- if (scrolledFrom2) await restoreScrollPosition(scrolledFrom2);
10250
- };
10251
- let measured;
10252
- try {
10253
- measured = await measureInView(ref, options.padding ?? 0);
10254
- } catch (error) {
10255
- await restoreViewport();
10256
- throw error;
10257
- }
10258
- const { box, bounds, inView, scrolledFrom } = measured;
10259
- const originalWidth = bounds.width;
10260
- const originalHeight = bounds.height;
10261
- const resized = shouldResize(originalWidth, originalHeight, noResize);
10262
- const scale = resized ? calculateResizeScale(originalWidth, originalHeight) : 1;
10263
- const finalWidth = Math.round(originalWidth * scale);
10264
- const finalHeight = Math.round(originalHeight * scale);
10265
- const metricsResponse = await callCDP("Page.getLayoutMetrics", {});
10266
- const metricsResult = metricsResponse.data?.result;
10267
- const scroll = metricsResult?.cssLayoutViewport ?? { pageX: 0, pageY: 0 };
10268
- const onPage = (area) => ({
10269
- ...area,
10270
- x: area.x + scroll.pageX,
10271
- y: area.y + scroll.pageY
10272
- });
10273
- const clip = onPage(bounds);
10274
- let screenshotResult;
10275
- try {
10276
- const screenshotResponse = await callCDP("Page.captureScreenshot", {
10277
- format,
10278
- ...quality !== void 0 && { quality },
10279
- clip: { ...clip, scale },
10280
- captureBeyondViewport: !inView
10281
- });
10282
- screenshotResult = screenshotResponse.data?.result;
10283
- } finally {
10284
- await restore(!inView, scrolledFrom);
10285
- }
10286
- if (!screenshotResult?.data) {
10287
- throw new CDPConnectionError("No screenshot data returned", new Error("Empty response"));
10288
- }
10289
- const buffer = Buffer.from(screenshotResult.data, "base64");
10290
- const absolutePath = await writeOutputFile(outputPath, buffer);
10291
- const result = {
10292
- path: absolutePath,
10293
- format,
10294
- width: finalWidth,
10295
- height: finalHeight,
10296
- size: buffer.length,
10297
- fullPage: false,
10298
- finalTokens: calculateImageTokens(finalWidth, finalHeight),
10299
- element: {
10300
- bounds: roundBounds(onPage(box)),
10301
- ...bounds !== box && { captured: roundBounds(clip) },
10302
- ...options.padding && { padding: options.padding }
10303
- }
10304
- };
10305
- if (quality !== void 0) {
10306
- result.quality = quality;
10307
- }
10308
- if (resized) {
10309
- result.resized = true;
10310
- result.originalWidth = originalWidth;
10311
- result.originalHeight = originalHeight;
10312
- result.originalTokens = calculateImageTokens(originalWidth, originalHeight);
10313
- }
10314
- return result;
10315
- }
10316
- function roundBounds(bounds) {
10317
- return {
10318
- x: Math.round(bounds.x),
10319
- y: Math.round(bounds.y),
10320
- width: Math.round(bounds.width),
10321
- height: Math.round(bounds.height)
10322
- };
10323
- }
10324
- var log25, 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;
10325
10103
  var init_screenshot = __esm({
10326
10104
  "src/commands/dom/helpers/screenshot.ts"() {
10327
10105
  "use strict";
10328
- init_screenshotResize();
10106
+ init_interrupt();
10329
10107
  init_outputFile();
10330
- init_errors5();
10331
10108
  init_errors3();
10332
10109
  init_messages();
10333
10110
  init_client();
10334
- init_targetNode();
10335
- init_emulation();
10336
- init_metadata();
10337
- init_logging();
10111
+ init_sessionCommand();
10338
10112
  init_exitCodes();
10339
- log25 = createLogger("dom");
10340
- POST_SCROLL_NETWORK_IDLE_MS = 150;
10341
- POST_SCROLL_DOM_STABLE_MS = 200;
10342
- POST_SCROLL_MAX_WAIT_MS = 2e3;
10343
- STABILITY_CHECK_INTERVAL_MS = 50;
10344
- OVERFLOW_SCAN_LIMIT = 2e3;
10345
- CONTENT_OVERFLOW_JS = `function () {
10346
- const view = this.ownerDocument.defaultView;
10347
- const scroller = this.ownerDocument.scrollingElement || this.ownerDocument.documentElement;
10348
- const own = this.getBoundingClientRect();
10349
- const reach = { left: own.left, top: own.top, right: own.right, bottom: own.bottom };
10350
- const page = { left: -view.scrollX, top: -view.scrollY, right: scroller.scrollWidth - view.scrollX, bottom: scroller.scrollHeight - view.scrollY };
10351
- const clips = (style) => style.overflowX !== 'visible' || style.overflowY !== 'visible';
10352
- let budget = ${OVERFLOW_SCAN_LIMIT};
10353
- const walk = (el) => {
10354
- for (const child of el.children) {
10355
- if (--budget < 0) return;
10356
- const style = view.getComputedStyle(child);
10357
- if (style.display === 'none' || style.position === 'fixed') continue;
10358
- const r = child.getBoundingClientRect();
10359
- if (r.width > 0 && r.height > 0 && style.visibility === 'visible') {
10360
- reach.left = Math.min(reach.left, Math.max(r.left, page.left));
10361
- reach.top = Math.min(reach.top, Math.max(r.top, page.top));
10362
- reach.right = Math.max(reach.right, Math.min(r.right, page.right));
10363
- reach.bottom = Math.max(reach.bottom, Math.min(r.bottom, page.bottom));
10364
- }
10365
- if (!clips(style)) walk(child);
10366
- }
10367
- };
10368
- const ownStyle = view.getComputedStyle(this);
10369
- if (!clips(ownStyle)) {
10370
- walk(this);
10371
- const scaleX = this.offsetWidth ? own.width / this.offsetWidth : 1;
10372
- const scaleY = this.offsetHeight ? own.height / this.offsetHeight : 1;
10373
- const wider = Math.max(0, this.scrollWidth - this.clientWidth) * scaleX;
10374
- const taller = Math.max(0, this.scrollHeight - this.clientHeight) * scaleY;
10375
- if (ownStyle.direction === 'rtl') reach.left = Math.min(reach.left, own.left - wider);
10376
- else reach.right = Math.max(reach.right, own.right + wider);
10377
- reach.bottom = Math.max(reach.bottom, own.bottom + taller);
10378
- }
10379
- const ink = { left: 0, top: 0, right: 0, bottom: 0 };
10380
- const grow = (side, amount) => { ink[side] = Math.max(ink[side], amount); };
10381
- for (const layer of ownStyle.boxShadow === 'none' ? [] : ownStyle.boxShadow.split(/,(?![^(]*\\))/)) {
10382
- if (/\\binset\\b/.test(layer)) continue;
10383
- const [x = 0, y = 0, blur = 0, spread = 0] = (layer.replace(/(rgba?|hsla?|color|oklch|lab|lch)\\([^)]*\\)/g, '').match(/-?[\\d.]+px/g) || []).map(parseFloat);
10384
- grow('left', blur + spread - x);
10385
- grow('right', blur + spread + x);
10386
- grow('top', blur + spread - y);
10387
- grow('bottom', blur + spread + y);
10388
- }
10389
- if (ownStyle.outlineStyle !== 'none') {
10390
- const outline = parseFloat(ownStyle.outlineWidth) + parseFloat(ownStyle.outlineOffset);
10391
- ['left', 'top', 'right', 'bottom'].forEach((side) => grow(side, outline));
10392
- }
10393
- return {
10394
- left: Math.max(own.left - reach.left, ink.left),
10395
- top: Math.max(own.top - reach.top, ink.top),
10396
- right: Math.max(reach.right - own.right, ink.right),
10397
- bottom: Math.max(reach.bottom - own.bottom, ink.bottom)
10398
- };
10399
- }`;
10400
- OVERFLOW_SLACK = 1;
10401
10113
  }
10402
10114
  });
10403
10115
 
@@ -10405,17 +10117,15 @@ var init_screenshot = __esm({
10405
10117
  var helpers_exports = {};
10406
10118
  __export(helpers_exports, {
10407
10119
  assertNodeAttached: () => assertNodeAttached,
10408
- captureElementScreenshot: () => captureElementScreenshot,
10409
- capturePageScreenshot: () => capturePageScreenshot,
10410
- documentReadyState: () => documentReadyState,
10120
+ captureScreenshot: () => captureScreenshot,
10411
10121
  getDOMElements: () => getDOMElements,
10412
10122
  getDomContext: () => getDomContext,
10413
- getElementBounds: () => getElementBounds,
10414
10123
  noMatchesError: () => noMatchesError,
10415
10124
  pageDocumentId: () => pageDocumentId,
10416
10125
  queryDOMElements: () => queryDOMElements,
10417
10126
  resolveBackendNodeIds: () => resolveBackendNodeIds,
10418
10127
  resolveSelector: () => resolveSelector,
10128
+ screenshotInterrupted: () => screenshotInterrupted,
10419
10129
  selectMatch: () => selectMatch
10420
10130
  });
10421
10131
  var init_helpers = __esm({
@@ -13537,9 +13247,9 @@ Expecting one of '${allowedValues.join("', '")}'`);
13537
13247
  * @param {string} [path]
13538
13248
  * @return {(string|null|Command)}
13539
13249
  */
13540
- executableDir(path7) {
13541
- if (path7 === void 0) return this._executableDir;
13542
- this._executableDir = path7;
13250
+ executableDir(path8) {
13251
+ if (path8 === void 0) return this._executableDir;
13252
+ this._executableDir = path8;
13543
13253
  return this;
13544
13254
  }
13545
13255
  /**
@@ -13797,11 +13507,15 @@ var program = new Command();
13797
13507
  // src/commands/helpJson.ts
13798
13508
  init_schema();
13799
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
+
13800
13515
  // src/commands/optionBehaviors.ts
13801
- init_screenshotResize();
13802
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";
13803
- 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';
13804
- 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';
13805
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`;
13806
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)";
13807
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";
@@ -13882,9 +13596,9 @@ var OPTION_BEHAVIORS = {
13882
13596
  automaticBehavior: "The value is matched as: a 0-based index (bdg dom frames order: document order of the <iframe> elements, nested ones depth-first, main page not counted), else an exact name/id attribute, else a case-insensitive part of the name, id or URL. Several matches fail with 81 listing them; none fails with 83 listing all frames. Frames are looked up on every call (a reloaded iframe is found again). An index that names another frame than in the last bdg dom frames listing (iframes added, removed or moved, or the page navigated) fails with 87 STALE_CACHE: re-run bdg dom frames or pick the frame by name."
13883
13597
  },
13884
13598
  "eval:--full": {
13885
- default: 'Human output prints the first 20000 characters of the value followed by "\u2026 N more chars (use --full)"; in JSON a string result is cut to 20000 characters with truncatedFrom (the original length). Objects and arrays in JSON are not cut',
13886
- whenEnabled: "Prints the whole value, byte for byte",
13887
- tokenImpact: "dom eval document.documentElement.outerHTML on Wikipedia is about 3.6 MB with --full; select what you need in the expression instead"
13599
+ default: 'Human output prints the first 20000 characters of the value followed by "\u2026 N more chars (use --full)". In JSON a string result is cut to 20000 characters with truncatedFrom (the original length); an array result lists its first 100 elements with count (all of them) and omitted; an object or array whose JSON is still over 20000 characters becomes the first 20000 characters of its JSON text (a string; arrays keep count) with truncatedFrom (the length of the JSON text of the copy, which holds at most 1000 entries per list or object): a string result with truncatedFrom while type is object is such a JSON-text start',
13600
+ whenEnabled: "Prints the whole value, byte for byte; objects and arrays are copied with every entry (without --full at most 1000 per list or object)",
13601
+ tokenImpact: 'dom eval document.documentElement.outerHTML on Wikipedia is about 3.6 MB with --full; on a page with 20000 elements, [...document.querySelectorAll("*")].map(e => e.outerHTML) --json is 3 MB with --full and 23 KB without; select what you need in the expression instead'
13888
13602
  },
13889
13603
  "console:--full": {
13890
13604
  default: 'Message texts are cut: human output (summary, --list, --follow) at 200 characters followed by "\u2026 N more chars (use --full)"; JSON text at 10000 characters with truncatedFrom (the original length)',
@@ -14111,6 +13825,11 @@ var OPTION_BEHAVIORS = {
14111
13825
  whenEnabled: "Streams requests as they finish",
14112
13826
  automaticBehavior: FOLLOW_BEHAVIOR
14113
13827
  },
13828
+ "har:--include-sensitive": {
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`,
13830
+ whenEnabled: "Writes every captured value (JSON sanitized: false); human output warns that the file holds credentials",
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'
13832
+ },
14114
13833
  "peek:--verbose": {
14115
13834
  default: "Compact output (truncated URLs, no resource types)",
14116
13835
  whenEnabled: "Verbose output with full URLs and resource types"
@@ -14141,8 +13860,8 @@ var OPTION_BEHAVIORS = {
14141
13860
  whenEnabled: "Alias for --force, kept for compatibility"
14142
13861
  },
14143
13862
  "cleanup:--purge": {
14144
- default: "A named session's directory (Chrome profile, ~60 MB; logs; port.txt) is kept for its next start",
14145
- 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"
14146
13865
  },
14147
13866
  "bdg:--viewport": {
14148
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",
@@ -14162,12 +13881,22 @@ var OPTION_BEHAVIORS = {
14162
13881
  "bdg:--chrome-ws-url": {
14163
13882
  default: "bdg launches its own Chrome (closed on stop)",
14164
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)",
14165
- 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)"
14166
13885
  },
14167
13886
  "stop:--kill-chrome": {
14168
13887
  default: "Chrome launched by bdg is always closed on stop; an attached Chrome (--chrome-ws-url) is left running",
14169
13888
  whenEnabled: "No additional effect; kept for compatibility"
14170
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
+ },
14171
13900
  "status:--verbose": {
14172
13901
  default: "Basic session status (daemon running, session active, URL)",
14173
13902
  whenEnabled: "Includes Chrome diagnostics and CDP connection details"
@@ -14231,25 +13960,25 @@ var loggingBufferColumns = 25;
14231
13960
  var level_;
14232
13961
  var Log = class _Log {
14233
13962
  static _logToStdErr(title, argsArray) {
14234
- const log34 = _Log.loggerfn(title);
14235
- log34(...argsArray);
13963
+ const log33 = _Log.loggerfn(title);
13964
+ log33(...argsArray);
14236
13965
  }
14237
13966
  /**
14238
13967
  * @param {string} title
14239
13968
  */
14240
13969
  static loggerfn(title) {
14241
13970
  title = `LH:${title}`;
14242
- let log34 = loggersByTitle[title];
14243
- if (!log34) {
14244
- log34 = (0, import_debug.default)(title);
14245
- loggersByTitle[title] = log34;
13971
+ let log33 = loggersByTitle[title];
13972
+ if (!log33) {
13973
+ log33 = (0, import_debug.default)(title);
13974
+ loggersByTitle[title] = log33;
14246
13975
  if (title.endsWith("error")) {
14247
- log34.color = colors.red;
13976
+ log33.color = colors.red;
14248
13977
  } else if (title.endsWith("warn")) {
14249
- log34.color = colors.yellow;
13978
+ log33.color = colors.yellow;
14250
13979
  }
14251
13980
  }
14252
- return log34;
13981
+ return log33;
14253
13982
  }
14254
13983
  /**
14255
13984
  * @param {string} level
@@ -14825,14 +14554,61 @@ function isDaemonSocketGone(socketPath = getSessionFilePath("DAEMON_SOCKET"), ti
14825
14554
  });
14826
14555
  }
14827
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
+
14828
14593
  // src/session/cleanup/staleSession.ts
14829
- init_metadata();
14830
14594
  init_paths();
14831
14595
  init_logging();
14832
14596
 
14833
14597
  // src/utils/async.ts
14834
- function delay2(ms) {
14835
- 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
+ });
14836
14612
  }
14837
14613
  async function waitUntil(condition, timeoutMs, pollMs = 25) {
14838
14614
  const deadline = Date.now() + timeoutMs;
@@ -14846,12 +14622,12 @@ async function waitUntil(condition, timeoutMs, pollMs = 25) {
14846
14622
  // src/utils/file.ts
14847
14623
  init_logging();
14848
14624
  import * as fs9 from "fs";
14849
- function safeRemoveFile(path7, context, log34) {
14625
+ function safeRemoveFile(path8, context, log33) {
14850
14626
  try {
14851
- fs9.rmSync(path7, { force: true });
14627
+ fs9.rmSync(path8, { force: true });
14852
14628
  return true;
14853
14629
  } catch (error) {
14854
- logDebugError(log34, `remove ${context}`, error);
14630
+ logDebugError(log33, `remove ${context}`, error);
14855
14631
  return false;
14856
14632
  }
14857
14633
  }
@@ -15342,7 +15118,7 @@ function convertCompactCommand(command) {
15342
15118
  return {
15343
15119
  name: command.name(),
15344
15120
  ...aliases.length > 0 && { aliases },
15345
- description: command.description().split("\n")[0] ?? "",
15121
+ description: command.summary() || (command.description().split("\n")[0] ?? ""),
15346
15122
  ...args && { arguments: args },
15347
15123
  ...options.length > 0 && {
15348
15124
  options: Object.fromEntries(options.map((option) => [option.flags, option.description]))
@@ -15439,14 +15215,14 @@ init_errors3();
15439
15215
  init_messages();
15440
15216
  init_exitCodes();
15441
15217
  init_suggestions();
15442
- var COMMANDER_HINT = /\n?\(Did you mean (.+)\?\)\s*$/;
15218
+ var COMMANDER_HINT = /\n?\(Did you mean (?:one of )?(.+)\?\)\s*$/;
15443
15219
  function helpTopicPath(args) {
15444
15220
  const firstOption = args.findIndex((arg) => arg.startsWith("-"));
15445
15221
  return firstOption === -1 ? args : args.slice(0, firstOption);
15446
15222
  }
15447
15223
  function assertKnownHelpTopic(program2, topic) {
15448
15224
  let command = program2;
15449
- const path7 = [];
15225
+ const path8 = [];
15450
15226
  for (const word of topic) {
15451
15227
  if (command.commands.length === 0 || word === "help") return;
15452
15228
  const sub = command.commands.find((c) => c.name() === word || c.aliases().includes(word));
@@ -15456,8 +15232,8 @@ function assertKnownHelpTopic(program2, topic) {
15456
15232
  command.commands.map((c) => c.name())
15457
15233
  );
15458
15234
  const err = unknownHelpTopicError(
15459
- [...path7, word].join(" "),
15460
- closest && [...path7, closest].join(" ")
15235
+ [...path8, word].join(" "),
15236
+ closest && [...path8, closest].join(" ")
15461
15237
  );
15462
15238
  throw new CommandError(
15463
15239
  err.message,
@@ -15465,7 +15241,7 @@ function assertKnownHelpTopic(program2, topic) {
15465
15241
  EXIT_CODES.INVALID_ARGUMENTS
15466
15242
  );
15467
15243
  }
15468
- path7.push(sub.name());
15244
+ path8.push(sub.name());
15469
15245
  command = sub;
15470
15246
  }
15471
15247
  }
@@ -15473,7 +15249,10 @@ function splitCommanderHint(text) {
15473
15249
  const message = text.replace(/^error:\s*/i, "");
15474
15250
  const hint = COMMANDER_HINT.exec(message);
15475
15251
  if (!hint) return { message };
15476
- return { message: message.slice(0, hint.index).trim(), suggestion: `Did you mean: ${hint[1]}?` };
15252
+ return {
15253
+ message: message.slice(0, hint.index).trim(),
15254
+ suggestion: didYouMeanSuggestion((hint[1] ?? "").split(", "))
15255
+ };
15477
15256
  }
15478
15257
  var UNKNOWN_OPTION = /unknown option '([^'=]+)/;
15479
15258
  function closestOption(flag, command) {
@@ -15504,7 +15283,7 @@ function usageErrorDetails(error, command, argv = process.argv) {
15504
15283
 
15505
15284
  // src/commands/start.ts
15506
15285
  import fs11 from "node:fs";
15507
- import os3 from "node:os";
15286
+ import os4 from "node:os";
15508
15287
 
15509
15288
  // src/commands/shared/commonOptions.ts
15510
15289
  function jsonOption() {
@@ -15529,14 +15308,14 @@ init_messages();
15529
15308
 
15530
15309
  // src/utils/version.ts
15531
15310
  import { readFileSync as readFileSync6 } from "fs";
15532
- import { join as join5 } from "path";
15311
+ import { join as join6 } from "path";
15533
15312
  var cachedVersion = "";
15534
15313
  function getVersion() {
15535
15314
  if (cachedVersion) {
15536
15315
  return cachedVersion;
15537
15316
  }
15538
15317
  try {
15539
- const pkgPath = join5(PACKAGE_ROOT, "package.json");
15318
+ const pkgPath = join6(PACKAGE_ROOT, "package.json");
15540
15319
  const pkg = JSON.parse(readFileSync6(pkgPath, "utf-8"));
15541
15320
  cachedVersion = pkg.version ?? "0.0.0";
15542
15321
  } catch {
@@ -15547,8 +15326,8 @@ function getVersion() {
15547
15326
  var VERSION = getVersion();
15548
15327
 
15549
15328
  // src/ui/OutputBuilder.ts
15550
- function buildSuccessResponse(data) {
15551
- return { version: VERSION, success: true, data };
15329
+ function buildSuccessResponse(data, warning) {
15330
+ return { version: VERSION, success: true, data, ...warning && { warning } };
15552
15331
  }
15553
15332
  function stringifyEnvelope(envelope) {
15554
15333
  return process.stdout.isTTY ? JSON.stringify(envelope, null, 2) : JSON.stringify(envelope);
@@ -15659,6 +15438,9 @@ function sessionEndedError() {
15659
15438
  EXIT_CODES.RESOURCE_NOT_FOUND
15660
15439
  );
15661
15440
  }
15441
+ function printWarning(warning) {
15442
+ if (warning) console.error(escapeControlChars(`Warning: ${warning}`));
15443
+ }
15662
15444
  async function runCommand(handler, options, formatter) {
15663
15445
  try {
15664
15446
  const result = await handler(options);
@@ -15669,6 +15451,7 @@ async function runCommand(handler, options, formatter) {
15669
15451
  stringifyEnvelope(
15670
15452
  OutputBuilder.buildJsonError(result.error ?? "Unknown error", {
15671
15453
  ...result.errorContext,
15454
+ ...result.warning && { warning: result.warning },
15672
15455
  exitCode
15673
15456
  })
15674
15457
  )
@@ -15684,14 +15467,16 @@ async function runCommand(handler, options, formatter) {
15684
15467
  }
15685
15468
  }
15686
15469
  }
15470
+ printWarning(result.warning);
15687
15471
  }
15688
15472
  process.exit(exitCode);
15689
15473
  }
15474
+ if (!options.json) printWarning(result.warning);
15690
15475
  if (result.hint && !options.quiet) {
15691
15476
  console.error(escapeControlChars(result.hint));
15692
15477
  }
15693
15478
  if (options.json) {
15694
- console.log(stringifyEnvelope(buildSuccessResponse(result.data)));
15479
+ console.log(stringifyEnvelope(buildSuccessResponse(result.data, result.warning)));
15695
15480
  } else if (formatter) {
15696
15481
  const formattedOutput = formatter(result.data);
15697
15482
  console.log(escapeControlChars(formattedOutput));
@@ -15712,10 +15497,14 @@ async function runCommand(handler, options, formatter) {
15712
15497
  )
15713
15498
  );
15714
15499
  } else {
15500
+ const { warning, ...metadata } = error.metadata;
15715
15501
  console.error(genericError(error.message));
15716
- for (const value of Object.values(error.metadata)) {
15717
- 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
+ );
15718
15506
  }
15507
+ printWarning(warning);
15719
15508
  }
15720
15509
  process.exit(error.exitCode);
15721
15510
  }
@@ -15745,6 +15534,9 @@ async function runCommand(handler, options, formatter) {
15745
15534
  }
15746
15535
  }
15747
15536
 
15537
+ // src/commands/shared/startHelpers.ts
15538
+ init_interrupt();
15539
+
15748
15540
  // src/ui/messages/session.ts
15749
15541
  init_formatting();
15750
15542
  init_commands();
@@ -15792,6 +15584,9 @@ function lastSessionEndText(end) {
15792
15584
  function endedSessionText(label, end) {
15793
15585
  return `${label} ended ${sessionEndText(end)}`;
15794
15586
  }
15587
+ function untrustedSessionText(label, why) {
15588
+ return `${label}: ${why}`;
15589
+ }
15795
15590
  function sessionEndText(end) {
15796
15591
  const why = {
15797
15592
  crash: "Chrome crashed or was closed",
@@ -15849,7 +15644,7 @@ var SessionDirError = class extends DaemonError {
15849
15644
  // src/daemon/launcher.ts
15850
15645
  import { spawn } from "child_process";
15851
15646
  import fs10 from "fs";
15852
- import { join as join6 } from "path";
15647
+ import { join as join7 } from "path";
15853
15648
  init_messages();
15854
15649
  init_paths();
15855
15650
  init_logging();
@@ -15860,6 +15655,7 @@ var log8 = createLogger("launcher");
15860
15655
  var DAEMON_READY_TIMEOUT_MS = 5e3;
15861
15656
  var DAEMON_READY_POLL_MS = 20;
15862
15657
  async function launchDaemon() {
15658
+ assertUsableSessionDir();
15863
15659
  if (await isDaemonAlive()) {
15864
15660
  log8.debug("Daemon already running");
15865
15661
  return void 0;
@@ -15870,11 +15666,10 @@ async function launchDaemon() {
15870
15666
  "DAEMON_SCRIPT_NOT_FOUND"
15871
15667
  );
15872
15668
  }
15873
- assertUsableSessionDir();
15874
15669
  await stopUnreachableDaemon();
15875
- const logPath = join6(getSessionDir(), "daemon.log");
15670
+ const logPath = join7(getSessionDir(), "daemon.log");
15876
15671
  rotateLog(logPath);
15877
- const logFd = fs10.openSync(logPath, "a");
15672
+ const logFd = openDaemonLog(logPath);
15878
15673
  log8.debug(`Starting daemon: ${DAEMON_SCRIPT_PATH}`);
15879
15674
  const daemon = spawn(process.execPath, [DAEMON_SCRIPT_PATH], {
15880
15675
  detached: true,
@@ -15887,7 +15682,13 @@ async function launchDaemon() {
15887
15682
  });
15888
15683
  daemon.unref();
15889
15684
  await waitForDaemonReady(() => exited);
15890
- 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
+ };
15891
15692
  }
15892
15693
  var UNREACHABLE_DAEMON_EXIT_MS = 8e3;
15893
15694
  async function stopUnreachableDaemon() {
@@ -15930,6 +15731,8 @@ function assertUsableSessionDir() {
15930
15731
  problem.denied ? EXIT_CODES.PERMISSION_DENIED : EXIT_CODES.SESSION_FILE_ERROR
15931
15732
  );
15932
15733
  }
15734
+ const untrusted = secureSessionDir(dir);
15735
+ if (untrusted) fail(untrustedSessionDirError(untrusted));
15933
15736
  try {
15934
15737
  ensureSessionDir();
15935
15738
  fs10.accessSync(dir, fs10.constants.W_OK);
@@ -15942,6 +15745,18 @@ function assertUsableSessionDir() {
15942
15745
  );
15943
15746
  }
15944
15747
  }
15748
+ var DAEMON_LOG_FLAGS = fs10.constants.O_WRONLY | fs10.constants.O_APPEND | fs10.constants.O_CREAT | (fs10.constants.O_NOFOLLOW ?? 0);
15749
+ function openDaemonLog(logPath) {
15750
+ try {
15751
+ return fs10.openSync(logPath, DAEMON_LOG_FLAGS, 384);
15752
+ } catch (error) {
15753
+ const err = daemonLogNotOpenedError(
15754
+ logPath,
15755
+ error.code ?? getErrorMessage(error)
15756
+ );
15757
+ throw new SessionDirError(err.message, err.suggestion);
15758
+ }
15759
+ }
15945
15760
  var MAX_LOG_BYTES = 5 * 1024 * 1024;
15946
15761
  function rotateLog(logPath) {
15947
15762
  try {
@@ -15961,7 +15776,7 @@ async function waitForDaemonReady(hasExited) {
15961
15776
  }
15962
15777
  if (hasExited() && !await isDaemonAlive()) {
15963
15778
  throw new DaemonStartupError(
15964
- `Daemon exited during startup. See ${join6(getSessionDir(), "daemon.log")}`,
15779
+ `Daemon exited during startup. See ${join7(getSessionDir(), "daemon.log")}`,
15965
15780
  "DAEMON_EXITED"
15966
15781
  );
15967
15782
  }
@@ -16006,6 +15821,7 @@ var COMMANDS = {
16006
15821
  dom_audit: defineCommand(),
16007
15822
  css_search: defineCommand(),
16008
15823
  dom_inspect: defineCommand(),
15824
+ dom_screenshot: defineCommand(),
16009
15825
  dom_wait: defineCommand()
16010
15826
  };
16011
15827
 
@@ -16088,27 +15904,58 @@ var SPAWNED_DAEMON_EXIT_WAIT_MS = 3e3;
16088
15904
  var DEFAULT_START_DEPS = {
16089
15905
  launch: launchDaemon,
16090
15906
  send: startSession,
15907
+ stop: stopSession,
16091
15908
  exitWaitMs: SPAWNED_DAEMON_EXIT_WAIT_MS
16092
15909
  };
16093
15910
  async function startSessionViaDaemon(url, options, telemetry) {
16094
- process.once("SIGINT", () => reportStartOutcome(interruptedOutcome("SIGINT"), options));
16095
- process.once("SIGTERM", () => reportStartOutcome(interruptedOutcome("SIGTERM"), options));
16096
- 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
+ );
16097
15922
  }
16098
- async function attemptStart(url, options, telemetry, deps = DEFAULT_START_DEPS) {
15923
+ async function attemptStart(url, options, telemetry, deps = DEFAULT_START_DEPS, interrupt) {
16099
15924
  const spawned = [];
16100
15925
  const attempt = async () => {
16101
- const outcome2 = await requestSession(url, options, telemetry, deps);
15926
+ const outcome2 = await requestSession(url, options, telemetry, deps, interrupt);
16102
15927
  if (!outcome2.ok && outcome2.spawned) spawned.push(outcome2.spawned);
16103
15928
  return outcome2;
16104
15929
  };
16105
15930
  let outcome = await attempt();
16106
15931
  const deadline = Date.now() + SHUTDOWN_WAIT_MS;
16107
15932
  while (isShuttingDown(outcome) && Date.now() < deadline) {
16108
- await delay2(SHUTDOWN_POLL_MS);
15933
+ await delay2(SHUTDOWN_POLL_MS, interrupt);
16109
15934
  outcome = await attempt();
16110
15935
  }
16111
- 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)) };
16112
15959
  }
16113
15960
  async function afterSpawnedDaemonExit(outcome, spawned, waitMs) {
16114
15961
  if (outcome.ok) return outcome;
@@ -16136,15 +15983,18 @@ async function afterSpawnedDaemonExit(outcome, spawned, waitMs) {
16136
15983
  }
16137
15984
  function interruptedOutcome(signal) {
16138
15985
  const message = `Start cancelled (${signal === "SIGINT" ? "interrupted" : "terminated"})`;
16139
- const exitCode = signal === "SIGINT" ? EXIT_CODES.INTERRUPTED : EXIT_CODES.TERMINATED;
16140
- 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));
16141
15990
  }
16142
15991
  var SHUTDOWN_WAIT_MS = 15e3;
16143
15992
  var SHUTDOWN_POLL_MS = 200;
16144
15993
  function isShuttingDown(outcome) {
16145
15994
  return !outcome.ok && (outcome.retryable === true || outcome.errorCode === "SESSION_SHUTTING_DOWN" /* SESSION_SHUTTING_DOWN */);
16146
15995
  }
16147
- async function requestSession(url, options, telemetry, deps) {
15996
+ async function requestSession(url, options, telemetry, deps, interrupt) {
15997
+ if (interrupt?.aborted) return cancelledOutcome(interrupt);
16148
15998
  let spawned;
16149
15999
  try {
16150
16000
  spawned = await deps.launch();
@@ -16162,10 +16012,16 @@ async function requestSession(url, options, telemetry, deps) {
16162
16012
  const exitCode = error instanceof DaemonError ? error.exitCode : EXIT_CODES.SOFTWARE_ERROR;
16163
16013
  return { ok: false, error: message, human: genericError(message), exitCode };
16164
16014
  }
16165
- const outcome = await sendStart(url, options, telemetry, deps.send);
16166
- 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 };
16167
16023
  }
16168
- async function sendStart(url, options, telemetry, send) {
16024
+ async function sendStart(url, options, telemetry, send, interrupt) {
16169
16025
  try {
16170
16026
  log11.debug("Connecting to daemon...");
16171
16027
  const response = await send(
@@ -16182,7 +16038,8 @@ async function sendStart(url, options, telemetry, send) {
16182
16038
  chromeFlags: options.chromeFlags,
16183
16039
  viewport: options.viewport,
16184
16040
  colorScheme: options.colorScheme
16185
- })
16041
+ }),
16042
+ interrupt
16186
16043
  );
16187
16044
  if (response.status === "error") {
16188
16045
  return { ...describeStartFailure(response, options), daemonExiting: true };
@@ -16198,6 +16055,7 @@ async function sendStart(url, options, telemetry, send) {
16198
16055
  }
16199
16056
  return { ok: true, data: response.data };
16200
16057
  } catch (error) {
16058
+ if (interrupt?.aborted) return { ...cancelledOutcome(interrupt), daemonExiting: true };
16201
16059
  if (error instanceof IPCTimeoutError) {
16202
16060
  const timeout = timeoutError(error);
16203
16061
  return {
@@ -16547,9 +16405,11 @@ var CDP_HTTP_TIMEOUT_MS = 5e3;
16547
16405
  async function probeDevToolsEndpoint(port, logger, options) {
16548
16406
  const url = `${options?.secure ? "https" : "http"}://${options?.host ?? HTTP_LOCALHOST}:${port}/json/version`;
16549
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;
16550
16410
  let response;
16551
16411
  try {
16552
- response = await fetch(url, { signal: AbortSignal.timeout(timeoutMs) });
16412
+ response = await fetch(url, { signal });
16553
16413
  } catch (error) {
16554
16414
  logger?.debug(`Chrome version request failed: ${getErrorMessage(error)} (${url})`);
16555
16415
  return { kind: "unreachable" };
@@ -16568,7 +16428,7 @@ async function probeDevToolsEndpoint(port, logger, options) {
16568
16428
  init_suggestions();
16569
16429
  init_url();
16570
16430
  function expandHome(value) {
16571
- return value.startsWith("~/") ? value.replace(/^~/, os3.homedir()) : value;
16431
+ return value.startsWith("~/") ? value.replace(/^~/, os4.homedir()) : value;
16572
16432
  }
16573
16433
  function extractUserDataDirFromFlags(flags) {
16574
16434
  const rest = [];
@@ -16864,6 +16724,50 @@ function assertValidChromeWsUrl(url) {
16864
16724
  init_errors3();
16865
16725
  init_messages();
16866
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
+
16867
16771
  // src/commands/cdp.ts
16868
16772
  init_protocol();
16869
16773
  init_schema();
@@ -16871,8 +16775,10 @@ init_errors3();
16871
16775
  init_messages();
16872
16776
  init_client();
16873
16777
 
16778
+ // src/connection/errors.ts
16779
+ init_exitCodes();
16780
+
16874
16781
  // src/runtime/dom/evalHelpers.ts
16875
- init_errors5();
16876
16782
  init_errors3();
16877
16783
  init_messages();
16878
16784
 
@@ -16906,11 +16812,7 @@ var WEBSOCKET_CONFIG = {
16906
16812
  noPongClosure: 1001
16907
16813
  };
16908
16814
 
16909
- // src/connection/cdp.ts
16910
- init_errors5();
16911
-
16912
16815
  // src/daemon/session/cdpSetup.ts
16913
- init_errors5();
16914
16816
  init_constants();
16915
16817
  init_errors3();
16916
16818
  init_messages();
@@ -17104,6 +17006,7 @@ function describeException(details) {
17104
17006
 
17105
17007
  // src/ui/formatters/cdp.ts
17106
17008
  init_formatting();
17009
+ init_commands();
17107
17010
  function firstSentence(description) {
17108
17011
  const [line = ""] = (description ?? "").split("\n");
17109
17012
  const end = line.indexOf(". ");
@@ -17122,6 +17025,16 @@ function columns(rows) {
17122
17025
  function typeText(field) {
17123
17026
  return field.items ? `${field.type}<${field.items}>` : field.type;
17124
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
+ }
17125
17038
  function formatCdpSearch(data) {
17126
17039
  if (data.count === 0) {
17127
17040
  return joinLines(`No CDP method matches "${data.query}"`, "List the domains: bdg cdp --list");
@@ -17158,13 +17071,37 @@ function fieldSection(title, fields) {
17158
17071
  `${title}:`,
17159
17072
  ...columns(
17160
17073
  fields.map((f) => [
17161
- `${f.name}${f.optional ? "?" : ""}: ${typeText(f)}`,
17162
- (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)}`
17163
17076
  ])
17164
17077
  )
17165
17078
  ];
17166
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
+ }
17167
17103
  function formatCdpDescription(data) {
17104
+ if (data.type === "type") return formatCdpType(data);
17168
17105
  if (data.type === "domain") {
17169
17106
  return joinLines(
17170
17107
  `${data.domain}: ${pluralize(data.commands, "method")}, ${pluralize(data.events, "event")}${tags(data)}`,
@@ -17173,15 +17110,15 @@ function formatCdpDescription(data) {
17173
17110
  data.nextStep
17174
17111
  );
17175
17112
  }
17113
+ const redirected = data.redirect?.parameters ?? [];
17176
17114
  return joinLines(
17177
17115
  `${data.name}${tags(data)}`,
17178
17116
  data.description,
17179
- ...fieldSection(
17180
- "Parameters",
17181
- data.parameters.map((p) => ({ ...p, optional: !p.required }))
17182
- ),
17117
+ ...fieldSection("Parameters", parameterFields(data.parameters)),
17118
+ ...redirectSection(data.redirect),
17183
17119
  ...fieldSection("Returns", data.returns),
17184
17120
  data.note && `Note: ${data.note}`,
17121
+ typeHint([...data.parameters, ...redirected]),
17185
17122
  data.example && `Example: ${data.example.command}`
17186
17123
  );
17187
17124
  }
@@ -17192,6 +17129,9 @@ function formatCdpResult(data) {
17192
17129
  return isEmptyCdpResult(data.result) ? `${data.method}: done (no result data)` : JSON.stringify(data.result, null, 2);
17193
17130
  }
17194
17131
 
17132
+ // src/commands/cdp.ts
17133
+ init_commands();
17134
+
17195
17135
  // src/ui/messages/hints.ts
17196
17136
  init_formatting();
17197
17137
  init_sessionCommand();
@@ -17244,14 +17184,19 @@ function getMethodHint(methodName, result) {
17244
17184
  return void 0;
17245
17185
  }
17246
17186
  function registerCdpCommand(program2) {
17247
- program2.command("cdp").description(
17248
- "CDP protocol introspection and execution\n Discovery: --list, --search, --describe\n Execution: case-insensitive (network.getcookies works)"
17187
+ program2.command("cdp").summary("CDP protocol introspection and execution").description(
17188
+ "CDP protocol introspection and execution\n Discovery: --list, --search, --describe\n" + CDP_EXECUTION_HELP
17249
17189
  ).argument("[method]", "CDP method name (e.g., Network.getCookies, network.getcookies)").addOption(new Option("--params <json>", "Method parameters as JSON")).addOption(
17250
17190
  new Option("--list", "List all domains or methods in a domain").conflicts([
17251
17191
  "describe",
17252
17192
  "params"
17253
17193
  ])
17254
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(
17255
17200
  new Option("--search <query>", "Search methods by keyword").conflicts([
17256
17201
  "list",
17257
17202
  "describe",
@@ -17275,11 +17220,7 @@ async function runCdpCommand(method, options) {
17275
17220
  return runCommand(async () => handleDescribeMethod(method), options, formatCdpDescription);
17276
17221
  }
17277
17222
  if (method) {
17278
- return runCommand(
17279
- async () => handleExecuteMethod(method, options.params),
17280
- options,
17281
- formatCdpResult
17282
- );
17223
+ return runCommand(async () => handleExecuteMethod(method, options), options, formatCdpResult);
17283
17224
  }
17284
17225
  return runCommand(async () => {
17285
17226
  const err = missingArgumentError(CDP_USAGE);
@@ -17433,94 +17374,97 @@ function handleListDomainMethods(domainName) {
17433
17374
  }
17434
17375
  };
17435
17376
  }
17436
- function handleDescribeMethod(methodName) {
17437
- const [domainName, method] = methodName.includes(".") ? methodName.split(".") : [methodName, void 0];
17438
- if (!method) {
17439
- const summary = getDomainSummary(domainName);
17440
- if (!summary) {
17441
- const similar = findSimilarMethods(methodName);
17442
- const suggestions = ["Use: bdg cdp --list (to see all domains)"];
17443
- if (similar.length > 0) {
17444
- suggestions.push("");
17445
- suggestions.push("Did you mean:");
17446
- similar.forEach((name) => suggestions.push(` - ${name}`));
17447
- }
17448
- return {
17449
- success: false,
17450
- error: `Domain or method '${methodName}' not found`,
17451
- exitCode: EXIT_CODES.INVALID_ARGUMENTS,
17452
- errorContext: {
17453
- suggestion: suggestions.join("\n")
17454
- }
17455
- };
17456
- }
17457
- const domainNote = DOMAIN_NOTES[summary.name];
17458
- return {
17459
- success: true,
17460
- data: {
17461
- type: "domain",
17462
- domain: summary.name,
17463
- description: summary.description,
17464
- commands: summary.commandCount,
17465
- events: summary.eventCount,
17466
- experimental: summary.experimental,
17467
- deprecated: summary.deprecated,
17468
- note: domainNote,
17469
- nextStep: `Use: bdg cdp ${summary.name} --list (to see all methods)`
17470
- }
17471
- };
17472
- }
17473
- const schema = getMethodSchema(domainName, method);
17474
- if (!schema) {
17475
- const similar = findSimilarMethods(methodName, domainName);
17476
- const suggestions = [`Use: bdg cdp ${domainName} --list (to see all ${domainName} methods)`];
17477
- if (similar.length > 0) {
17478
- suggestions.push("");
17479
- suggestions.push("Did you mean:");
17480
- similar.forEach((name) => suggestions.push(` - ${name}`));
17481
- }
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
+ );
17482
17405
  return {
17483
17406
  success: false,
17484
- error: `Method '${methodName}' not found`,
17407
+ error: `Domain or method '${domainName}' not found`,
17485
17408
  exitCode: EXIT_CODES.INVALID_ARGUMENTS,
17486
- errorContext: {
17487
- suggestion: suggestions.join("\n")
17488
- }
17409
+ errorContext: { suggestion: err.suggestion }
17489
17410
  };
17490
17411
  }
17491
- const methodNote = METHOD_NOTES[schema.name] ?? DOMAIN_NOTES[schema.domain];
17492
- const alternative = blockedAlternative(schema.name);
17493
17412
  return {
17494
17413
  success: true,
17495
17414
  data: {
17496
- type: "method",
17497
- name: schema.name,
17498
- domain: schema.domain,
17499
- method: schema.method,
17500
- description: schema.description,
17501
- experimental: schema.experimental,
17502
- deprecated: schema.deprecated,
17503
- note: methodNote,
17504
- parameters: schema.parameters.map((p) => ({
17505
- name: p.name,
17506
- type: p.type,
17507
- required: p.required,
17508
- description: p.description,
17509
- enum: p.enum,
17510
- items: p.items,
17511
- deprecated: p.deprecated
17512
- })),
17513
- returns: schema.returns.map((r) => ({
17514
- name: r.name,
17515
- type: r.type,
17516
- optional: r.optional,
17517
- description: r.description,
17518
- items: r.items
17519
- })),
17520
- 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)`
17521
17424
  }
17522
17425
  };
17523
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
+ }
17524
17468
  var BLOCKED_CDP_METHODS = {
17525
17469
  "Page.captureScreenshot": {
17526
17470
  alternative: "bdg dom screenshot [path]",
@@ -17535,38 +17479,53 @@ var BLOCKED_CDP_METHODS = {
17535
17479
  reason: "Closes Chrome under the session, which then ends without saving its state"
17536
17480
  }
17537
17481
  };
17538
- async function handleExecuteMethod(methodName, paramsJson) {
17539
- const normalized = normalizeMethod(methodName);
17540
- if (normalized && BLOCKED_CDP_METHODS[normalized]) {
17541
- const blocked = BLOCKED_CDP_METHODS[normalized];
17542
- return {
17543
- success: false,
17544
- error: `${normalized} is blocked via raw CDP: ${blocked.reason}`,
17545
- exitCode: EXIT_CODES.INVALID_ARGUMENTS,
17546
- errorContext: { suggestion: `Use: ${sessionCommand(blocked.alternative)}` }
17547
- };
17548
- }
17549
- if (!normalized) {
17550
- const similar = findSimilarMethods(methodName);
17551
- const suggestions = ["Use: bdg cdp --search <keyword> (to search for methods)"];
17552
- if (similar.length > 0) {
17553
- suggestions.push("");
17554
- suggestions.push("Did you mean:");
17555
- 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
+ );
17556
17492
  }
17493
+ return { method: target.method };
17494
+ }
17495
+ if (target.kind === "unlisted" || target.kind === "typo" && options.sendAnyway) {
17557
17496
  return {
17558
- success: false,
17559
- error: `Method '${methodName}' not found`,
17560
- exitCode: EXIT_CODES.INVALID_ARGUMENTS,
17561
- errorContext: {
17562
- suggestion: suggestions.join("\n")
17563
- }
17497
+ method: target.method,
17498
+ warning: cdpUnlistedMethodWarning(target.method, getBundledProtocolVersion())
17564
17499
  };
17565
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);
17566
17525
  let params;
17567
- if (paramsJson) {
17526
+ if (options.params) {
17568
17527
  try {
17569
- params = JSON.parse(paramsJson);
17528
+ params = JSON.parse(options.params);
17570
17529
  } catch (error) {
17571
17530
  return {
17572
17531
  success: false,
@@ -17574,31 +17533,31 @@ async function handleExecuteMethod(methodName, paramsJson) {
17574
17533
  exitCode: EXIT_CODES.INVALID_ARGUMENTS,
17575
17534
  errorContext: {
17576
17535
  suggestion: `Use: bdg cdp ${normalized} --describe (to see parameter schema)`
17577
- }
17536
+ },
17537
+ ...warning && { warning }
17578
17538
  };
17579
17539
  }
17580
17540
  }
17581
17541
  const response = await callCDP(normalized, params);
17582
- validateIPCResponse(response);
17583
- 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 } : {};
17584
17552
  const exception = pageExceptionResult(cdpResult);
17585
- if (exception) return exception;
17586
- const result = {
17553
+ if (exception) return { ...exception, ...withCallWarning };
17554
+ const hints = [ipcHint, getMethodHint(method, cdpResult)].filter(Boolean);
17555
+ return {
17587
17556
  success: true,
17588
- data: {
17589
- method: normalized,
17590
- result: cdpResult
17591
- }
17557
+ data: { method, result: cdpResult },
17558
+ ...withCallWarning,
17559
+ ...hints.length > 0 && { hint: hints.join("\n") }
17592
17560
  };
17593
- if (response.data?.hint) {
17594
- result.hint = formatHint(response.data.hint);
17595
- }
17596
- const methodHint = getMethodHint(normalized, cdpResult);
17597
- if (methodHint) {
17598
- result.hint = result.hint ? `${result.hint}
17599
- ${methodHint}` : methodHint;
17600
- }
17601
- return result;
17602
17561
  }
17603
17562
 
17604
17563
  // src/commands/cleanup.ts
@@ -17701,6 +17660,7 @@ function formatCleanup(data) {
17701
17660
  return joinLines(
17702
17661
  cleaned.session && sessionFilesCleanedMessage(),
17703
17662
  cleaned.output && sessionOutputRemovedMessage(),
17663
+ data.downloadsKept && downloadsKeptMessage(data.downloadsKept.dir, data.downloadsKept.files),
17704
17664
  ...(data.warnings ?? []).map((warning) => warningMessage(warning)),
17705
17665
  "",
17706
17666
  data.message
@@ -17789,18 +17749,26 @@ async function cleanupSession(opts) {
17789
17749
  };
17790
17750
  }
17791
17751
  const didCleanup = Object.values(cleaned).some(Boolean) || purged !== void 0;
17752
+ const downloadsKept = purged === void 0 ? keptDownloads() : void 0;
17792
17753
  return {
17793
17754
  success: true,
17794
17755
  data: {
17795
17756
  cleaned,
17796
17757
  ...purged !== void 0 && { purged },
17758
+ ...downloadsKept && { downloadsKept },
17797
17759
  message: !didCleanup ? noSessionFilesMessage() : purged !== void 0 ? sessionDirectoryPurgedMessage(purged) : sessionDirectoryCleanMessage(),
17798
17760
  ...warnings.length > 0 && { warnings }
17799
17761
  }
17800
17762
  };
17801
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
+ }
17802
17770
  function registerCleanupCommand(program2) {
17803
- 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(
17804
17772
  "--purge",
17805
17773
  "Also delete a named session's directory (Chrome profile, logs, port); needs --session",
17806
17774
  false
@@ -18717,7 +18685,11 @@ function megabytes(bytes) {
18717
18685
  function networkEvictedNote(counts) {
18718
18686
  const { requestsDropped, bodiesEvicted } = counts;
18719
18687
  const requests = pluralize(requestsDropped, "older network request");
18720
- 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
+ );
18721
18693
  const budget = megabytes(MAX_TOTAL_BODY_BYTES);
18722
18694
  if (requestsDropped > 0 && bodiesEvicted > 0) {
18723
18695
  return `\u26A0 ${requests} dropped, ${bodies} evicted: bdg keeps the newest ${MAX_NETWORK_REQUESTS} requests and ${budget} of bodies`;
@@ -18742,6 +18714,15 @@ function headerRepeatedNote(count) {
18742
18714
  function localProxyNote() {
18743
18715
  return "(loopback; likely a local proxy)";
18744
18716
  }
18717
+ function harSanitizedComment() {
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";
18719
+ }
18720
+ function harExportedMessage(result) {
18721
+ const filterNote = result.filtered ? " (filtered)" : "";
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";
18723
+ return `\u2713 Exported ${result.entries} requests${filterNote} to ${result.file}
18724
+ ${note}`;
18725
+ }
18745
18726
 
18746
18727
  // src/telemetry/networkRetention.ts
18747
18728
  var SKIPPED_BODY_PATTERN = /^\[SKIPPED: (.*)\]$/s;
@@ -18854,10 +18835,6 @@ function addWebSocketMessages(fmt, webSocket) {
18854
18835
  webSocket.frames.forEach((frame) => fmt.text(formatWebSocketMessage(frame)));
18855
18836
  }
18856
18837
  var BODY_PREVIEW_LENGTH = 2e4;
18857
- function formatBytes(bytes) {
18858
- if (bytes < 1024) return `${bytes} B`;
18859
- return bytes < 1024 * 1024 ? `${(bytes / 1024).toFixed(1)} KB` : `${(bytes / 1024 / 1024).toFixed(1)} MB`;
18860
- }
18861
18838
  function isLoopback(ip) {
18862
18839
  const bare = ip.replace(/^\[|\]$/g, "").toLowerCase();
18863
18840
  return /^(::ffff:)?127\./.test(bare) || bare === "::1";
@@ -18933,7 +18910,11 @@ function formatNetworkDetails(request) {
18933
18910
  fmt.keyValueList(requestSummaryRows(request));
18934
18911
  fmt.blank();
18935
18912
  if (request.requestHeaders) addHeaders(fmt, "Request Headers:", request.requestHeaders);
18936
- 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) {
18937
18918
  fmt.text("Request Body:").separator("\u2501", 70);
18938
18919
  fmt.text(request.requestBody);
18939
18920
  fmt.blank();
@@ -19711,6 +19692,8 @@ function formatActionOutput(done, details, result, options = {}) {
19711
19692
  if (requests.length > 0) {
19712
19693
  fmt.blank().section(formatTriggeredRequestsTitle(result.triggeredRequests ?? [], omitted), requests);
19713
19694
  }
19695
+ if (result.downloads?.length) fmt.blank();
19696
+ for (const download of result.downloads ?? []) fmt.text(downloadText(download));
19714
19697
  for (const dialog of result.dialogs ?? []) {
19715
19698
  fmt.blank();
19716
19699
  fmt.text(`Dialog: ${dialogConsoleText(dialog)}`);
@@ -19912,9 +19895,9 @@ init_url();
19912
19895
  var MAX_VALUE_LENGTH2 = 40;
19913
19896
  function fileName(src) {
19914
19897
  if (/^(data|blob):/i.test(src)) return src;
19915
- const path7 = src.split(/[?#]/)[0] ?? src;
19916
- const name = path7.substring(path7.lastIndexOf("/") + 1);
19917
- return name && name !== path7 ? `\u2026/${name}` : src;
19898
+ const path8 = src.split(/[?#]/)[0] ?? src;
19899
+ const name = path8.substring(path8.lastIndexOf("/") + 1);
19900
+ return name && name !== path8 ? `\u2026/${name}` : src;
19918
19901
  }
19919
19902
  function hostOf(src) {
19920
19903
  const parsed = /^https?:/i.test(src) ? safeParseUrl(src) : null;
@@ -20043,7 +20026,7 @@ init_elementInfo();
20043
20026
  init_frameScopedConnection();
20044
20027
  init_logging();
20045
20028
  init_errors();
20046
- var log26 = createLogger("dom");
20029
+ var log24 = createLogger("dom");
20047
20030
  function childFrameIds(tree) {
20048
20031
  return (tree?.childFrames ?? []).flatMap((child) => [child.frame.id, ...childFrameIds(child)]);
20049
20032
  }
@@ -20765,9 +20748,34 @@ async function audit(checks, options) {
20765
20748
  return { success: true, data: response.data };
20766
20749
  }
20767
20750
 
20751
+ // src/commands/dom/helpers/evalResult.ts
20752
+ init_constants();
20753
+ init_strings();
20754
+ function boundEvalResult(value, length) {
20755
+ if (typeof value === "string") {
20756
+ const { text, truncatedFrom } = capLength(value, MAX_VALUE_LENGTH);
20757
+ return { result: text, ...truncatedFrom !== void 0 && { truncatedFrom } };
20758
+ }
20759
+ if (Array.isArray(value)) return boundArray(value, length ?? value.length);
20760
+ if (value === null || typeof value !== "object") return { result: value };
20761
+ const json = JSON.stringify(value);
20762
+ return json.length > MAX_VALUE_LENGTH ? jsonStart(json) : { result: value };
20763
+ }
20764
+ function boundArray(value, count) {
20765
+ const listed = value.slice(0, EVAL_JSON_ARRAY_LIMIT);
20766
+ const listedJson = JSON.stringify(listed);
20767
+ if (listedJson.length > MAX_VALUE_LENGTH) {
20768
+ const json = listed.length === value.length ? listedJson : JSON.stringify(value);
20769
+ return { ...jsonStart(json), count };
20770
+ }
20771
+ return listed.length < count ? { result: listed, count, omitted: count - listed.length } : { result: value };
20772
+ }
20773
+ function jsonStart(json) {
20774
+ return { result: capLength(json, MAX_VALUE_LENGTH).text, truncatedFrom: json.length };
20775
+ }
20776
+
20768
20777
  // src/commands/dom/eval.ts
20769
20778
  init_query();
20770
- init_constants();
20771
20779
  init_messages();
20772
20780
  init_client();
20773
20781
 
@@ -20861,7 +20869,6 @@ function formatDomScreenshot(data) {
20861
20869
  // src/commands/dom/eval.ts
20862
20870
  init_commands();
20863
20871
  init_exitCodes();
20864
- init_strings();
20865
20872
  async function handleDomEval(script, options) {
20866
20873
  await runCommand(
20867
20874
  async () => {
@@ -20874,7 +20881,7 @@ async function handleDomEval(script, options) {
20874
20881
  errorContext: { suggestion: err.suggestion }
20875
20882
  };
20876
20883
  }
20877
- const response = await domEval(script, options.frame);
20884
+ const response = await domEval(script, options.frame, options.full);
20878
20885
  if (response.status === "error" || !response.data) {
20879
20886
  const suggestion = await frameErrorSuggestion(response, options.frame);
20880
20887
  return {
@@ -20884,7 +20891,7 @@ async function handleDomEval(script, options) {
20884
20891
  ...suggestion && { errorContext: { suggestion } }
20885
20892
  };
20886
20893
  }
20887
- const { value, type, subtype, frame, warning } = response.data;
20894
+ const { value, type, subtype, length, frame, warning } = response.data;
20888
20895
  const hint = [
20889
20896
  ...frame !== void 0 ? [evalFrameLine(frame)] : [],
20890
20897
  ...warning ? [warningMessage(warning)] : []
@@ -20892,7 +20899,7 @@ async function handleDomEval(script, options) {
20892
20899
  return {
20893
20900
  success: true,
20894
20901
  data: {
20895
- ...jsonResult(value, options),
20902
+ ...options.json && !options.full ? boundEvalResult(value, length) : { result: value },
20896
20903
  type,
20897
20904
  ...subtype && { subtype },
20898
20905
  ...frame !== void 0 && { frame },
@@ -20905,11 +20912,6 @@ async function handleDomEval(script, options) {
20905
20912
  (data) => formatDomEval(data, { full: options.full })
20906
20913
  );
20907
20914
  }
20908
- function jsonResult(value, options) {
20909
- if (!options.json || options.full || typeof value !== "string") return { result: value };
20910
- const { text, truncatedFrom } = capLength(value, MAX_VALUE_LENGTH);
20911
- return { result: text, ...truncatedFrom !== void 0 && { truncatedFrom } };
20912
- }
20913
20915
  async function frameErrorSuggestion(response, frame) {
20914
20916
  if (frame === void 0 || response.exitCode !== EXIT_CODES.RESOURCE_NOT_FOUND) {
20915
20917
  return response.suggestion;
@@ -21227,7 +21229,7 @@ function formatFormDiscovery(result) {
21227
21229
  init_logging();
21228
21230
  init_errors();
21229
21231
  init_exitCodes();
21230
- var log27 = createLogger("dom");
21232
+ var log25 = createLogger("dom");
21231
21233
  function buildValidation(raw) {
21232
21234
  const hasNativeError = !raw.isValid && !raw.valueMissing && raw.validationMessage;
21233
21235
  const hasAriaError = raw.ariaInvalid;
@@ -21384,7 +21386,7 @@ async function cacheFormElements(forms, document2) {
21384
21386
  const elements = forms.flatMap((form) => [...form.fields, ...form.buttons]);
21385
21387
  const backendNodeIds = await resolveBackendNodeIds(elements.map((el) => el.selector)).catch(
21386
21388
  (error) => {
21387
- log27.debug(`Form fields not cached by node: ${getErrorMessage(error)}`);
21389
+ log25.debug(`Form fields not cached by node: ${getErrorMessage(error)}`);
21388
21390
  return [];
21389
21391
  }
21390
21392
  );
@@ -21400,7 +21402,7 @@ async function cacheFormElements(forms, document2) {
21400
21402
  },
21401
21403
  document2
21402
21404
  );
21403
- log27.debug(`Cached ${elements.length} form elements`);
21405
+ log25.debug(`Cached ${elements.length} form elements`);
21404
21406
  }
21405
21407
  async function handleFormCommand(options) {
21406
21408
  await runCommand(
@@ -22378,6 +22380,7 @@ function listedMatches(result, limit) {
22378
22380
  // src/commands/dom/screenshot.ts
22379
22381
  import { extname as extname2 } from "path";
22380
22382
  init_helpers();
22383
+ init_interrupt();
22381
22384
  init_outputFile();
22382
22385
  init_errors3();
22383
22386
  init_messages();
@@ -22386,7 +22389,7 @@ init_logging();
22386
22389
  init_directories();
22387
22390
  init_errors();
22388
22391
  init_exitCodes();
22389
- var log28 = createLogger("dom");
22392
+ var log26 = createLogger("dom");
22390
22393
  var EXTENSION_FORMATS = {
22391
22394
  ".png": "png",
22392
22395
  ".jpg": "jpeg",
@@ -22407,34 +22410,27 @@ function resolveImageFormat(outputPath, requested) {
22407
22410
  }
22408
22411
  return requested ?? fromExtension ?? "png";
22409
22412
  }
22410
- function buildPageScreenshotOptions(options) {
22411
- return filterDefined({
22412
- format: options.format,
22413
- quality: options.quality,
22414
- fullPage: options.fullPage,
22415
- noResize: options.resize === false,
22416
- scroll: options.scroll
22417
- });
22418
- }
22419
- function buildElementScreenshotOptions(options) {
22420
- return filterDefined({
22421
- format: options.format,
22422
- quality: options.quality,
22423
- noResize: options.resize === false,
22424
- padding: options.padding
22425
- });
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 }) };
22426
22423
  }
22427
22424
  function hasElementTarget(options) {
22428
22425
  return options.selector !== void 0 || options.index !== void 0;
22429
22426
  }
22430
22427
  async function resolveElementNodeId(options) {
22431
22428
  if (options.selector !== void 0 && options.index !== void 0) {
22432
- return { backendNodeId: await selectMatch(options.selector, options.index) };
22429
+ return selectMatch(options.selector, options.index);
22433
22430
  }
22434
22431
  if (options.index !== void 0) {
22435
- const resolver = DomElementResolver.getInstance();
22436
- const node = await resolver.getNodeIdForIndex(options.index);
22437
- return { backendNodeId: node.nodeId };
22432
+ const node = await DomElementResolver.getInstance().getNodeIdForIndex(options.index);
22433
+ return node.nodeId;
22438
22434
  }
22439
22435
  if (options.selector !== void 0) {
22440
22436
  return resolveSelector(options.selector);
@@ -22477,10 +22473,11 @@ function formatFrameFilename(frameNumber, format) {
22477
22473
  return `${String(frameNumber).padStart(3, "0")}.${format}`;
22478
22474
  }
22479
22475
  async function handlePageScreenshot(outputPath, options) {
22476
+ const interrupt = abortOnInterrupt();
22480
22477
  await runCommand(
22481
22478
  async () => {
22482
- const screenshotOptions = buildPageScreenshotOptions(options);
22483
- const result = await capturePageScreenshot(outputPath, screenshotOptions);
22479
+ const request = buildScreenshotRequest(options);
22480
+ const result = await captureScreenshot(outputPath, request, interrupt);
22484
22481
  return { success: true, data: result };
22485
22482
  },
22486
22483
  options,
@@ -22488,12 +22485,14 @@ async function handlePageScreenshot(outputPath, options) {
22488
22485
  );
22489
22486
  }
22490
22487
  async function handleElementScreenshot(outputPath, options) {
22488
+ const interrupt = abortOnInterrupt();
22491
22489
  await runCommand(
22492
22490
  async () => {
22493
- const nodeRef = await resolveElementNodeId(options);
22494
- const screenshotOptions = buildElementScreenshotOptions(options);
22495
- const result = await captureElementScreenshot(outputPath, nodeRef, screenshotOptions);
22496
- 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);
22497
22496
  return { success: true, data: elementResult };
22498
22497
  },
22499
22498
  options,
@@ -22501,19 +22500,13 @@ async function handleElementScreenshot(outputPath, options) {
22501
22500
  );
22502
22501
  }
22503
22502
  async function captureSequenceFrame(outputPath, options) {
22504
- if (hasElementTarget(options)) {
22505
- const nodeRef = await resolveElementNodeId(options);
22506
- const elementOptions = buildElementScreenshotOptions(options);
22507
- await captureElementScreenshot(outputPath, nodeRef, elementOptions);
22508
- } else {
22509
- const pageOptions = buildPageScreenshotOptions(options);
22510
- await capturePageScreenshot(outputPath, pageOptions);
22511
- }
22503
+ const backendNodeId = hasElementTarget(options) ? await resolveElementNodeId(options) : void 0;
22504
+ await captureScreenshot(outputPath, buildScreenshotRequest(options, backendNodeId));
22512
22505
  }
22513
22506
  async function handleSequenceCapture(outputDir, options) {
22514
22507
  const fs19 = await import("fs");
22515
- const path7 = await import("path");
22516
- const absoluteDir = path7.resolve(outputDir);
22508
+ const path8 = await import("path");
22509
+ const absoluteDir = path8.resolve(outputDir);
22517
22510
  ensureDirectory(absoluteDir, fs19);
22518
22511
  const intervalRule = positiveIntRule({ name: "--interval", min: 100, max: 6e4, default: 1e3 });
22519
22512
  const limitRule = positiveIntRule({ name: "--limit", min: 1, max: 1e4, required: false });
@@ -22529,7 +22522,7 @@ async function handleSequenceCapture(outputDir, options) {
22529
22522
  while (!stopping) {
22530
22523
  frameCount++;
22531
22524
  const filename = formatFrameFilename(frameCount, format);
22532
- const outputPath = path7.join(absoluteDir, filename);
22525
+ const outputPath = path8.join(absoluteDir, filename);
22533
22526
  try {
22534
22527
  await captureSequenceFrame(outputPath, options);
22535
22528
  } catch (error) {
@@ -22538,7 +22531,7 @@ async function handleSequenceCapture(outputDir, options) {
22538
22531
  if (options.json) {
22539
22532
  console.log(JSON.stringify(buildSuccessResponse({ frame: frameCount, path: outputPath })));
22540
22533
  } else {
22541
- log28.info(`Frame ${frameCount}: ${filename}`);
22534
+ log26.info(`Frame ${frameCount}: ${filename}`);
22542
22535
  }
22543
22536
  if (limit > 0 && frameCount >= limit) break;
22544
22537
  await delay2(interval);
@@ -22721,8 +22714,8 @@ function registerDomCommands(program2) {
22721
22714
  "Image format: png or jpeg/jpg (default: from the file extension, else png)",
22722
22715
  screenshotFormatOption
22723
22716
  ).option("--quality <number>", "JPEG quality 0-100 (default: 90)", integerOption(0, 100)).option("--no-full-page", "Capture viewport only (default: full page)").option("--no-resize", "Disable auto-resize (full resolution)").option("--scroll <selector>", "Scroll element into view before capture").option("-f, --follow", "Continuous capture mode to directory").option("--interval <ms>", "Capture interval for --follow (default: 1000)").option("--limit <count>", "Max frames for --follow").option("-j, --json", "Output as JSON").action(
22724
- async (path7, target, options) => {
22725
- await handleDomScreenshot(path7, target, options);
22717
+ async (path8, target, options) => {
22718
+ await handleDomScreenshot(path8, target, options);
22726
22719
  }
22727
22720
  );
22728
22721
  }
@@ -22730,7 +22723,7 @@ function registerDomCommands(program2) {
22730
22723
  // src/commands/installSkill.ts
22731
22724
  import { existsSync as existsSync11, mkdirSync as mkdirSync2, readFileSync as readFileSync8, renameSync as renameSync2, rmSync as rmSync6, writeFileSync as writeFileSync2 } from "fs";
22732
22725
  import { homedir as homedir3 } from "os";
22733
- import { dirname as dirname6, join as join7 } from "path";
22726
+ import { dirname as dirname7, join as join8 } from "path";
22734
22727
  init_errors3();
22735
22728
  init_messages();
22736
22729
 
@@ -22739,9 +22732,9 @@ init_formatting();
22739
22732
  init_commands();
22740
22733
  import { homedir as homedir2 } from "os";
22741
22734
  var PATH_INDENT = " ".repeat(2 + 6 + 2 + 9 + 2);
22742
- function tildePath(path7) {
22735
+ function tildePath(path8) {
22743
22736
  const home = homedir2();
22744
- return path7.startsWith(`${home}/`) ? `~${path7.slice(home.length)}` : path7;
22737
+ return path8.startsWith(`${home}/`) ? `~${path8.slice(home.length)}` : path8;
22745
22738
  }
22746
22739
  function addSkillTargets(fmt, skills) {
22747
22740
  fmt.text("bdg skill:");
@@ -22765,11 +22758,11 @@ function formatInstalledSkills(data) {
22765
22758
  init_logging();
22766
22759
  init_errors();
22767
22760
  init_exitCodes();
22768
- var log29 = createLogger("bdg");
22769
- var SKILL_SOURCE_PATH = join7(PACKAGE_ROOT, ".claude", "skills", "bdg", "SKILL.md");
22761
+ var log27 = createLogger("bdg");
22762
+ var SKILL_SOURCE_PATH = join8(PACKAGE_ROOT, ".claude", "skills", "bdg", "SKILL.md");
22770
22763
  var SKILL_ROOTS = {
22771
- claude: join7(".claude", "skills"),
22772
- agents: join7(".agents", "skills")
22764
+ claude: join8(".claude", "skills"),
22765
+ agents: join8(".agents", "skills")
22773
22766
  };
22774
22767
  function installSkill(targets, home = homedir3(), source = SKILL_SOURCE_PATH) {
22775
22768
  if (!existsSync11(source)) {
@@ -22784,7 +22777,7 @@ function installSkill(targets, home = homedir3(), source = SKILL_SOURCE_PATH) {
22784
22777
  const skills = [];
22785
22778
  const failures = [];
22786
22779
  for (const target of targets) {
22787
- const result = writeSkill(target, join7(home, SKILL_ROOTS[target], "bdg", "SKILL.md"), content);
22780
+ const result = writeSkill(target, join8(home, SKILL_ROOTS[target], "bdg", "SKILL.md"), content);
22788
22781
  if ("error" in result) failures.push(result);
22789
22782
  else skills.push(result);
22790
22783
  }
@@ -22793,10 +22786,10 @@ function installSkill(targets, home = homedir3(), source = SKILL_SOURCE_PATH) {
22793
22786
  }
22794
22787
  function skillFailureError(failures, targets) {
22795
22788
  const other = failures.length === 1 ? targets.find((t) => t !== failures[0]?.target) : void 0;
22796
- const errors = failures.map(({ path: path7, error }) => {
22789
+ const errors = failures.map(({ path: path8, error }) => {
22797
22790
  const code = error.code;
22798
22791
  return skillWriteFailedError(
22799
- path7,
22792
+ path8,
22800
22793
  getErrorMessage(error),
22801
22794
  code === "ENOTDIR" || code === "EEXIST",
22802
22795
  other && `--${other}`
@@ -22808,30 +22801,30 @@ function skillFailureError(failures, targets) {
22808
22801
  EXIT_CODES.PERMISSION_DENIED
22809
22802
  );
22810
22803
  }
22811
- function writeSkill(target, path7, content) {
22812
- const existing = existsSync11(path7) ? readFileSync8(path7, "utf-8") : void 0;
22804
+ function writeSkill(target, path8, content) {
22805
+ const existing = existsSync11(path8) ? readFileSync8(path8, "utf-8") : void 0;
22813
22806
  if (existing === content) {
22814
- return { target, path: path7, status: "unchanged" };
22807
+ return { target, path: path8, status: "unchanged" };
22815
22808
  }
22816
- const backup = existing === void 0 ? void 0 : `${path7}.bak`;
22817
- const temporary = `${path7}.tmp`;
22818
- let writing = path7;
22809
+ const backup = existing === void 0 ? void 0 : `${path8}.bak`;
22810
+ const temporary = `${path8}.tmp`;
22811
+ let writing = path8;
22819
22812
  try {
22820
- mkdirSync2(dirname6(path7), { recursive: true });
22813
+ mkdirSync2(dirname7(path8), { recursive: true });
22821
22814
  writeFileSync2(temporary, content);
22822
22815
  if (backup !== void 0) {
22823
22816
  writing = backup;
22824
22817
  rmSync6(backup, { force: true });
22825
22818
  writeFileSync2(backup, existing ?? "");
22826
22819
  }
22827
- writing = path7;
22828
- renameSync2(temporary, path7);
22820
+ writing = path8;
22821
+ renameSync2(temporary, path8);
22829
22822
  } catch (error) {
22830
- safeRemoveFile(temporary, "temporary skill copy", log29);
22823
+ safeRemoveFile(temporary, "temporary skill copy", log27);
22831
22824
  return { target, path: writing, error };
22832
22825
  }
22833
- if (backup === void 0) return { target, path: path7, status: "installed" };
22834
- return { target, path: path7, status: "updated", backup };
22826
+ if (backup === void 0) return { target, path: path8, status: "installed" };
22827
+ return { target, path: path8, status: "updated", backup };
22835
22828
  }
22836
22829
  function selectedTargets(options) {
22837
22830
  const picked = Object.keys(SKILL_ROOTS).filter((target) => options[target]);
@@ -22862,7 +22855,7 @@ function registerInstallSkillCommand(program2) {
22862
22855
 
22863
22856
  // src/commands/network/har.ts
22864
22857
  import * as fs16 from "fs";
22865
- import * as path6 from "path";
22858
+ import * as path7 from "path";
22866
22859
  init_outputFile();
22867
22860
  init_client();
22868
22861
  init_paths();
@@ -22982,7 +22975,7 @@ function filterByResourceType(requests, types) {
22982
22975
  init_logging();
22983
22976
  init_suggestions();
22984
22977
  init_url();
22985
- var log30 = createLogger("network");
22978
+ var log28 = createLogger("network");
22986
22979
  var SIZE_UNITS = {
22987
22980
  b: 1,
22988
22981
  kb: 1024,
@@ -23092,7 +23085,7 @@ function validateSizeValue(value) {
23092
23085
  parseSize(value);
23093
23086
  return null;
23094
23087
  } catch (error) {
23095
- log30.debug(
23088
+ log28.debug(
23096
23089
  `Size parse failed for "${value}": ${error instanceof Error ? error.message : String(error)}`
23097
23090
  );
23098
23091
  return createError(
@@ -23278,27 +23271,359 @@ Multiple filters:
23278
23271
 
23279
23272
  // src/telemetry/har/builder.ts
23280
23273
  import { createRequire as createRequire2 } from "node:module";
23274
+
23275
+ // src/telemetry/har/sanitizeBody.ts
23276
+ init_elementInfo();
23277
+ init_logging();
23278
+ init_errors();
23279
+ var log29 = createLogger("network");
23280
+ var REDACTED = "[redacted]";
23281
+ var SENSITIVE_FIELD = new RegExp(
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`,
23283
+ "i"
23284
+ );
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 });
23294
+ function isSensitiveField2(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("");
23321
+ }
23322
+ function redactPairs(text, isSensitive, replacement) {
23323
+ return text.split("&").map((pair) => {
23324
+ const eq = pair.indexOf("=");
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}`;
23330
+ }).join("&");
23331
+ }
23332
+ function redactBody(text, mimeType) {
23333
+ if (/multipart\/form-data/i.test(mimeType)) return redactMultipart(text, mimeType);
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;
23348
+ }
23349
+ const redacted = redactBody(decoded, mimeType);
23350
+ return redacted === decoded ? base64 : Buffer.from(redacted, "utf8").toString("base64");
23351
+ }
23352
+ function decodeName(name) {
23353
+ try {
23354
+ return decodeURIComponent(name.replace(/\+/g, " "));
23355
+ } catch (error) {
23356
+ log29.debug(`Field name not decodable: ${getErrorMessage(error)}`);
23357
+ return name;
23358
+ }
23359
+ }
23360
+ function redactMultipart(text, mimeType) {
23361
+ const match = /boundary=(?:"([^"]+)"|([^;\s]+))/i.exec(mimeType);
23362
+ const boundary = match?.[1] ?? match?.[2];
23363
+ if (!boundary) return text;
23364
+ const delimiter = `--${boundary}`;
23365
+ return text.split(delimiter).map(redactPart).join(delimiter);
23366
+ }
23367
+ function redactPart(part) {
23368
+ const headerEnd = part.indexOf("\r\n\r\n");
23369
+ if (headerEnd === -1) return part;
23370
+ const name = /;\s*name="([^"]*)"/i.exec(part.slice(0, headerEnd))?.[1];
23371
+ if (name === void 0 || !isSensitiveField2(name)) return redactJwts(part, REDACTED);
23372
+ const valueEnd = part.endsWith("\r\n") ? part.length - 2 : part.length;
23373
+ return `${part.slice(0, headerEnd + 4)}${REDACTED}${part.slice(valueEnd)}`;
23374
+ }
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;
23467
+ }
23468
+ if (!/[[{]/.test(scan2.text.slice(start + 1, end - 1))) return void 0;
23469
+ let decoded;
23470
+ try {
23471
+ decoded = JSON.parse(scan2.text.slice(start, end));
23472
+ } catch (error) {
23473
+ log29.debug(`String value not decodable: ${getErrorMessage(error)}`);
23474
+ return void 0;
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);
23479
+ }
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;
23517
+ }
23518
+ }
23519
+
23520
+ // src/telemetry/har/sanitize.ts
23521
+ var URL_REDACTED = encodeURIComponent(REDACTED);
23522
+ var SENSITIVE_HEADERS = /* @__PURE__ */ new Set([
23523
+ "authorization",
23524
+ "proxy-authorization",
23525
+ "authentication",
23526
+ "cookie",
23527
+ "set-cookie"
23528
+ ]);
23529
+ var SENSITIVE_CUSTOM_HEADER = /^x-.*(key|token|secret|auth)/i;
23530
+ var SENSITIVE_HEADER_SEGMENT = /(^|-)(api-?key|apikey|token|secret|jwt|subscription-key|session(-?id)?)(-|$)/i;
23531
+ var URL_HEADERS = /* @__PURE__ */ new Set(["location", "referer"]);
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;
23536
+ function sanitizeEntry(entry) {
23537
+ const { request, response, _webSocketMessages: messages } = entry;
23538
+ const postData = request.postData;
23539
+ return {
23540
+ ...entry,
23541
+ request: {
23542
+ ...request,
23543
+ url: redactUrl(request.url),
23544
+ cookies: request.cookies.map(redactCookie),
23545
+ headers: request.headers.map(redactHeader),
23546
+ queryString: request.queryString.map(redactQueryParam),
23547
+ ...postData?.text !== void 0 && {
23548
+ postData: {
23549
+ ...postData,
23550
+ text: redactOrReplaceWhole(postData.text, (text) => redactBody(text, postData.mimeType))
23551
+ }
23552
+ }
23553
+ },
23554
+ response: {
23555
+ ...response,
23556
+ cookies: response.cookies.map(redactCookie),
23557
+ headers: response.headers.map(redactHeader),
23558
+ redirectURL: redactUrl(response.redirectURL),
23559
+ content: redactContent(response.content)
23560
+ },
23561
+ ...messages && { _webSocketMessages: messages.map(redactWebSocketMessage) }
23562
+ };
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
+ }
23575
+ function isSensitiveHeader(name) {
23576
+ return SENSITIVE_HEADERS.has(name.toLowerCase()) || SENSITIVE_CUSTOM_HEADER.test(name) || SENSITIVE_HEADER_SEGMENT.test(name);
23577
+ }
23578
+ function redactHeader(header) {
23579
+ if (isSensitiveHeader(header.name)) return { ...header, value: REDACTED };
23580
+ if (!URL_HEADERS.has(header.name.toLowerCase())) return header;
23581
+ const value = redactUrl(header.value);
23582
+ return value === header.value ? header : { ...header, value };
23583
+ }
23584
+ function redactCookie(cookie) {
23585
+ return { ...cookie, value: REDACTED };
23586
+ }
23587
+ function isSensitiveParam(name) {
23588
+ return isSensitiveField2(name) || SENSITIVE_URL_PARAM.test(name);
23589
+ }
23590
+ function redactQueryParam(param) {
23591
+ return isSensitiveParam(param.name) ? { ...param, value: REDACTED } : param;
23592
+ }
23593
+ function redactUrl(url) {
23594
+ const hash = url.indexOf("#");
23595
+ const beforeHash = hash === -1 ? url : url.slice(0, hash);
23596
+ const fragment = hash === -1 ? "" : `#${redactPairs(url.slice(hash + 1), isSensitiveParam, URL_REDACTED)}`;
23597
+ const question = beforeHash.indexOf("?");
23598
+ if (question === -1) return beforeHash + fragment;
23599
+ const query = redactPairs(beforeHash.slice(question + 1), isSensitiveParam, URL_REDACTED);
23600
+ return `${beforeHash.slice(0, question)}?${query}${fragment}`;
23601
+ }
23602
+
23603
+ // src/telemetry/har/builder.ts
23281
23604
  var requireBuiltin = createRequire2(import.meta.url);
23282
23605
  var UNKNOWN_TIMING = -1;
23283
23606
  var DEFAULT_HTTP_VERSION = "HTTP/1.1";
23284
- function buildHAR(requests, metadata) {
23285
- const entries = [...requests].sort((a, b) => a.timestamp - b.timestamp).map(buildEntry);
23286
- const log34 = {
23607
+ function buildHAR(requests, metadata, options = {}) {
23608
+ const built = [...requests].sort((a, b) => a.timestamp - b.timestamp).map(buildEntry);
23609
+ const entries = options.includeSensitive ? built : built.map(sanitizeEntry);
23610
+ const log33 = {
23287
23611
  version: "1.2",
23288
23612
  creator: {
23289
23613
  name: "bdg",
23290
23614
  version: metadata.version,
23291
23615
  comment: "Browser Debugger CLI - https://github.com/szymdzum/browser-debugger-cli"
23292
23616
  },
23293
- entries
23617
+ entries,
23618
+ ...!options.includeSensitive && { comment: harSanitizedComment() }
23294
23619
  };
23295
23620
  if (metadata.chromeVersion) {
23296
- log34.browser = {
23621
+ log33.browser = {
23297
23622
  name: "Chrome",
23298
23623
  version: metadata.chromeVersion
23299
23624
  };
23300
23625
  }
23301
- return { log: log34 };
23626
+ return { log: log33 };
23302
23627
  }
23303
23628
  function buildEntry(req) {
23304
23629
  const timings = buildTimings(req);
@@ -23329,7 +23654,8 @@ function buildWebSocketMessage(frame) {
23329
23654
  type: frame.direction === "sent" ? "send" : "receive",
23330
23655
  time: frame.timestamp / 1e3,
23331
23656
  opcode: frame.opcode,
23332
- data: frame.payloadData
23657
+ data: frame.payloadData,
23658
+ ...frame.truncatedFrom !== void 0 && { _truncatedFrom: frame.truncatedFrom }
23333
23659
  };
23334
23660
  }
23335
23661
  function buildRequest(req) {
@@ -23342,7 +23668,7 @@ function buildRequest(req) {
23342
23668
  headers: convertHeaders(req.requestHeaders),
23343
23669
  queryString: extractQueryParams(url),
23344
23670
  headersSize: estimateRequestHeadersSize(req.method, req.url, req.requestHeaders),
23345
- bodySize: req.requestBody ? Buffer.byteLength(req.requestBody, "utf-8") : 0
23671
+ bodySize: requestBodySize(req)
23346
23672
  };
23347
23673
  const postData = buildPostData(req);
23348
23674
  if (postData) {
@@ -23390,13 +23716,17 @@ function contentLength(req) {
23390
23716
  const bytes = value === void 0 ? NaN : Number(value);
23391
23717
  return Number.isInteger(bytes) && bytes >= 0 ? bytes : void 0;
23392
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
+ }
23393
23724
  function buildPostData(req) {
23394
23725
  if (!req.requestBody) return void 0;
23395
- const contentType = getHeader(req.requestHeaders, "content-type") ?? "text/plain";
23396
- return {
23397
- mimeType: contentType,
23398
- text: req.requestBody
23399
- };
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 };
23400
23730
  }
23401
23731
  function span(start, end) {
23402
23732
  if (start === void 0 || end === void 0 || start < 0 || end < 0 || end < start) {
@@ -23509,8 +23839,8 @@ function extractRedirectURL(req) {
23509
23839
  }
23510
23840
  function estimateRequestHeadersSize(method, url, headers) {
23511
23841
  const parsedUrl = new URL(url);
23512
- const path7 = parsedUrl.pathname + parsedUrl.search;
23513
- const requestLine = `${method} ${path7} ${DEFAULT_HTTP_VERSION}\r
23842
+ const path8 = parsedUrl.pathname + parsedUrl.search;
23843
+ const requestLine = `${method} ${path8} ${DEFAULT_HTTP_VERSION}\r
23514
23844
  `;
23515
23845
  const headersString = headers ? Object.entries(headers).map(([name, value]) => `${name}: ${value}\r
23516
23846
  `).join("") : "";
@@ -23642,6 +23972,13 @@ function formatSessionStatus(metadata, pid, activity, pageState, verbose = false
23642
23972
  if (activity.lastConsoleMessageAt) {
23643
23973
  fmt.keyValue(" Last Message", formatTimeAgo(activity.lastConsoleMessageAt), 18);
23644
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
+ );
23645
23982
  }
23646
23983
  fmt.blank().text("Collectors").separator("\u2501", 50);
23647
23984
  const activeTelemetry = metadata.activeTelemetry ?? ["network", "console", "dom"];
@@ -23778,6 +24115,7 @@ function buildPreviewJsonData(output, options) {
23778
24115
  ...output.partial !== void 0 && { partial: output.partial },
23779
24116
  ...output.totals && { totals: output.totals },
23780
24117
  ...output.pageCrashedAt !== void 0 && { pageCrashedAt: output.pageCrashedAt },
24118
+ ...output.downloads && { downloads: output.downloads },
23781
24119
  ...pick("network") && output.data.network && { network: last(output.data.network) },
23782
24120
  ...pick("console") && output.data.console && {
23783
24121
  console: last(output.data.console)?.map((message) => capMessageText(message, options.full))
@@ -23862,6 +24200,8 @@ function formatPreviewCompact(output, options) {
23862
24200
  fmt.blank();
23863
24201
  }
23864
24202
  }
24203
+ if (output.downloads?.length)
24204
+ fmt.text(`Downloads: ${downloadsSummary(output.downloads)}`).blank();
23865
24205
  if (!options.follow) {
23866
24206
  fmt.tip(compactTipsMessage());
23867
24207
  }
@@ -23950,6 +24290,9 @@ function formatPreviewVerbose(output, options) {
23950
24290
  fmt.blank();
23951
24291
  }
23952
24292
  }
24293
+ if (output.downloads?.length) {
24294
+ fmt.keyValue("Downloads", downloadsSummary(output.downloads), 18).blank();
24295
+ }
23953
24296
  if (!options.follow) {
23954
24297
  fmt.tip(verboseCommandsMessage());
23955
24298
  }
@@ -24073,7 +24416,7 @@ function registerDocumentCommand(networkCmd) {
24073
24416
  }
24074
24417
 
24075
24418
  // src/commands/network/har.ts
24076
- var log31 = createLogger("network");
24419
+ var log30 = createLogger("network");
24077
24420
  function generateHARFilename() {
24078
24421
  const now = /* @__PURE__ */ new Date();
24079
24422
  const year = now.getFullYear();
@@ -24083,10 +24426,10 @@ function generateHARFilename() {
24083
24426
  const minutes = String(now.getMinutes()).padStart(2, "0");
24084
24427
  const seconds = String(now.getSeconds()).padStart(2, "0");
24085
24428
  const base = `capture-${year}-${month}-${day}-${hours}${minutes}${seconds}`;
24086
- const sessionDir = path6.dirname(getSessionFilePath("OUTPUT"));
24087
- let candidate = path6.join(sessionDir, `${base}.har`);
24429
+ const sessionDir = path7.dirname(getSessionFilePath("OUTPUT"));
24430
+ let candidate = path7.join(sessionDir, `${base}.har`);
24088
24431
  for (let n = 2; fs16.existsSync(candidate); n++) {
24089
- candidate = path6.join(sessionDir, `${base}-${n}.har`);
24432
+ candidate = path7.join(sessionDir, `${base}-${n}.har`);
24090
24433
  }
24091
24434
  return candidate;
24092
24435
  }
@@ -24096,22 +24439,26 @@ async function getChromeVersion() {
24096
24439
  const product = response.data?.result?.product;
24097
24440
  return product?.split("/")[1];
24098
24441
  } catch (error) {
24099
- log31.debug(`Could not read Chrome version: ${getErrorMessage(error)}`);
24442
+ log30.debug(`Could not read Chrome version: ${getErrorMessage(error)}`);
24100
24443
  return void 0;
24101
24444
  }
24102
24445
  }
24103
24446
  function formatHARExport(data) {
24104
24447
  if ("log" in data) return JSON.stringify(data, null, 2);
24105
- const filterNote = data.filtered ? " (filtered)" : "";
24106
- return `\u2713 Exported ${data.entries} requests${filterNote} to ${data.file}`;
24448
+ return harExportedMessage(data);
24107
24449
  }
24450
+ var HAR_FILE_MODE = 384;
24108
24451
  var STDOUT_PATH = "-";
24109
24452
  var filterDslOption = new Option(
24110
24453
  "--filter <dsl>",
24111
24454
  'Filter requests using DevTools DSL (e.g., "status-code:>=400")'
24112
24455
  );
24456
+ var includeSensitiveOption = new Option(
24457
+ "--include-sensitive",
24458
+ "Keep credentials (auth/cookie/API key headers, cookie values, URL tokens, password and token body fields); redacted by default"
24459
+ );
24113
24460
  function registerHarCommand(networkCmd) {
24114
- networkCmd.command("har [output-file]").addHelpText("after", "\nUse - as the output file to write the HAR to stdout.").description("Export network data as HAR 1.2 format").addOption(jsonOption()).addOption(filterDslOption).action(async (outputFile, options) => {
24461
+ networkCmd.command("har [output-file]").addHelpText("after", "\nUse - as the output file to write the HAR to stdout.").description("Export network data as HAR 1.2 format").addOption(jsonOption()).addOption(filterDslOption).addOption(includeSensitiveOption).action(async (outputFile, options) => {
24115
24462
  await runCommand(
24116
24463
  async () => {
24117
24464
  let requests = await getNetworkRequests();
@@ -24126,20 +24473,26 @@ function registerHarCommand(networkCmd) {
24126
24473
  const outputPath = outputFile ?? generateHARFilename();
24127
24474
  if (outputPath !== STDOUT_PATH) assertFilePath(outputPath, ".har");
24128
24475
  const chromeVersion = await getChromeVersion();
24129
- const har = buildHAR(requests, {
24130
- version: VERSION,
24131
- ...chromeVersion && { chromeVersion }
24132
- });
24476
+ const includeSensitive = options.includeSensitive === true;
24477
+ const har = buildHAR(
24478
+ requests,
24479
+ { version: VERSION, ...chromeVersion && { chromeVersion } },
24480
+ { includeSensitive }
24481
+ );
24133
24482
  if (outputPath === STDOUT_PATH) return { success: true, data: har };
24134
- const file = await writeOutputFile(outputPath, JSON.stringify(har, null, 2), ".har");
24135
- return {
24136
- success: true,
24137
- data: {
24138
- file,
24139
- entries: har.log.entries.length,
24140
- filtered
24141
- }
24483
+ const file = await writeOutputFile(
24484
+ outputPath,
24485
+ JSON.stringify(har, null, 2),
24486
+ ".har",
24487
+ HAR_FILE_MODE
24488
+ );
24489
+ const data = {
24490
+ file,
24491
+ entries: har.log.entries.length,
24492
+ filtered,
24493
+ sanitized: !includeSensitive
24142
24494
  };
24495
+ return { success: true, data };
24143
24496
  },
24144
24497
  options,
24145
24498
  formatHARExport
@@ -24898,13 +25251,13 @@ import * as fs17 from "fs";
24898
25251
  init_paths();
24899
25252
  init_logging();
24900
25253
  init_directories();
24901
- var log32 = createLogger("session");
25254
+ var log31 = createLogger("session");
24902
25255
  function readPortFile(portPath) {
24903
25256
  try {
24904
25257
  const port = parseInt(fs17.readFileSync(portPath, "utf-8").trim(), 10);
24905
25258
  return Number.isInteger(port) && port >= 1 && port <= 65535 ? port : null;
24906
25259
  } catch (error) {
24907
- logDebugError(log32, `read ${portPath}`, error);
25260
+ logDebugError(log31, `read ${portPath}`, error);
24908
25261
  return null;
24909
25262
  }
24910
25263
  }
@@ -24956,7 +25309,7 @@ function selectSession(optionValue) {
24956
25309
  // src/session/sessionList.ts
24957
25310
  init_logging();
24958
25311
  init_sessionCommand();
24959
- var log33 = createLogger("session");
25312
+ var log32 = createLogger("session");
24960
25313
  function toRunningSession(name, data) {
24961
25314
  const meta = data.sessionMetadata;
24962
25315
  const state = meta ? "active" : data.ending ? "ending" : "starting";
@@ -24975,11 +25328,15 @@ async function describeSession(entry) {
24975
25328
  const socketPath = sessionFilePathIn(dir, "DAEMON_SOCKET");
24976
25329
  const probe = await probeDaemonSocket(socketPath);
24977
25330
  if (probe !== "alive") return describeWithoutSocket(entry, probe);
25331
+ const untrusted = secureSessionDir(dir);
25332
+ if (untrusted) {
25333
+ return { name, state: "untrusted", untrusted: `${untrusted.dir}: ${untrusted.reason}` };
25334
+ }
24978
25335
  try {
24979
25336
  const response = await getStatus(socketPath);
24980
25337
  if (response.status === "ok" && response.data) return toRunningSession(name, response.data);
24981
25338
  } catch (error) {
24982
- logDebugError(log33, `get status of ${dir}`, error);
25339
+ logDebugError(log32, `get status of ${dir}`, error);
24983
25340
  }
24984
25341
  return { name, state: "unresponsive" };
24985
25342
  }
@@ -25018,7 +25375,7 @@ function leftoverPort(dir) {
25018
25375
  const meta = JSON.parse(fs18.readFileSync(sessionFilePathIn(dir, "METADATA"), "utf8"));
25019
25376
  if (typeof meta.port === "number") return meta.port;
25020
25377
  } catch (error) {
25021
- logDebugError(log33, `read the metadata in ${dir}`, error);
25378
+ logDebugError(log32, `read the metadata in ${dir}`, error);
25022
25379
  }
25023
25380
  return readPortFile(sessionFilePathIn(dir, "PORT"));
25024
25381
  }
@@ -25031,7 +25388,7 @@ function isSameDir(a, b) {
25031
25388
  const second = fs18.statSync(b);
25032
25389
  return first.ino === second.ino && first.dev === second.dev;
25033
25390
  } catch (error) {
25034
- logDebugError(log33, `compare ${a} with ${b}`, error);
25391
+ logDebugError(log32, `compare ${a} with ${b}`, error);
25035
25392
  return false;
25036
25393
  }
25037
25394
  }
@@ -25090,6 +25447,15 @@ function formatSessionList(data) {
25090
25447
  if (ended.length > 0) {
25091
25448
  fmt.blank().section("Ended without bdg stop:", ended);
25092
25449
  }
25450
+ const untrusted = data.sessions.flatMap(
25451
+ ({ name, untrusted: why }) => why ? [untrustedSessionText(name ?? DEFAULT_SESSION_LABEL, why)] : []
25452
+ );
25453
+ if (untrusted.length > 0) {
25454
+ fmt.blank().section(
25455
+ "Directory not safe to use (not asked; bdg status --session <name> says how to fix it):",
25456
+ untrusted
25457
+ );
25458
+ }
25093
25459
  const cleanups = data.sessions.flatMap((session) => session.cleanup ? [session.cleanup] : []);
25094
25460
  if (cleanups.length > 0) {
25095
25461
  fmt.hints("Crashed or stale sessions; clean up with:", cleanups);
@@ -25207,6 +25573,7 @@ function registerStatusCommand(program2) {
25207
25573
  }
25208
25574
  return { success: true, data: withSessionName(jsonOutput) };
25209
25575
  } catch (error) {
25576
+ if (error instanceof CommandError) throw error;
25210
25577
  const errorMessage = getErrorMessage(error);
25211
25578
  if (error instanceof IPCTimeoutError) {
25212
25579
  const err = sessionNotRespondingError(error.timeoutMs / 1e3);
@@ -25255,6 +25622,7 @@ function registerStatusCommand(program2) {
25255
25622
  }
25256
25623
 
25257
25624
  // src/commands/stop.ts
25625
+ init_errors3();
25258
25626
  init_client();
25259
25627
  init_transport();
25260
25628
  init_formatting();
@@ -25325,7 +25693,7 @@ function registerStopCommand(program2) {
25325
25693
  };
25326
25694
  }
25327
25695
  } catch (error) {
25328
- if (error instanceof IPCTimeoutError) throw error;
25696
+ if (error instanceof IPCTimeoutError || error instanceof CommandError) throw error;
25329
25697
  const errorMessage = getErrorMessage(error);
25330
25698
  if (isDaemonNotRunningError(errorMessage)) {
25331
25699
  return {