browser-debugger-cli 0.15.0 → 0.16.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (133) hide show
  1. package/.claude/skills/bdg/SKILL.md +2 -1
  2. package/dist/cdp/methodTarget.d.ts +92 -0
  3. package/dist/cdp/methodTarget.js +159 -0
  4. package/dist/cdp/protocol.d.ts +16 -1
  5. package/dist/cdp/protocol.js +21 -0
  6. package/dist/cdp/schema.d.ts +55 -1
  7. package/dist/cdp/schema.js +134 -25
  8. package/dist/cdp/types.d.ts +3 -1
  9. package/dist/commands/cdp.d.ts +38 -1
  10. package/dist/commands/cdp.js +200 -133
  11. package/dist/commands/cleanup.js +18 -4
  12. package/dist/commands/dom/formInteraction.js +8 -4
  13. package/dist/commands/dom/helpers/index.d.ts +4 -4
  14. package/dist/commands/dom/helpers/index.js +3 -3
  15. package/dist/commands/dom/helpers/query.d.ts +2 -2
  16. package/dist/commands/dom/helpers/query.js +2 -2
  17. package/dist/commands/dom/helpers/screenshot.d.ts +21 -26
  18. package/dist/commands/dom/helpers/screenshot.js +50 -668
  19. package/dist/commands/dom/screenshot.js +56 -36
  20. package/dist/commands/optionBehaviors.js +18 -8
  21. package/dist/commands/shared/CommandRunner.d.ts +5 -0
  22. package/dist/commands/shared/CommandRunner.js +18 -3
  23. package/dist/commands/shared/interrupt.d.ts +40 -0
  24. package/dist/commands/shared/interrupt.js +73 -0
  25. package/dist/commands/shared/optionTypes.d.ts +2 -0
  26. package/dist/commands/shared/startHelpers.d.ts +26 -3
  27. package/dist/commands/shared/startHelpers.js +145 -23
  28. package/dist/commands/types.d.ts +5 -0
  29. package/dist/connection/cdp.js +1 -16
  30. package/dist/connection/chromeIdentity.d.ts +24 -5
  31. package/dist/connection/chromeIdentity.js +53 -22
  32. package/dist/connection/launcher.d.ts +34 -1
  33. package/dist/connection/launcher.js +98 -10
  34. package/dist/connection/typed-cdp.d.ts +3 -2
  35. package/dist/constants.d.ts +1 -1
  36. package/dist/constants.js +1 -1
  37. package/dist/daemon/SessionController.d.ts +10 -5
  38. package/dist/daemon/SessionController.js +15 -8
  39. package/dist/daemon/ipcServer.js +1 -1
  40. package/dist/daemon/launcher.d.ts +5 -0
  41. package/dist/daemon/launcher.js +8 -1
  42. package/dist/daemon/session/Session.d.ts +5 -1
  43. package/dist/daemon/session/Session.js +9 -8
  44. package/dist/daemon/session/TelemetryStore.d.ts +5 -0
  45. package/dist/daemon/session/TelemetryStore.js +4 -0
  46. package/dist/daemon/session/captureGate.d.ts +59 -0
  47. package/dist/daemon/session/captureGate.js +96 -0
  48. package/dist/daemon/session/chromeConnection.d.ts +16 -1
  49. package/dist/daemon/session/chromeConnection.js +34 -4
  50. package/dist/daemon/session/collectors.d.ts +15 -0
  51. package/dist/daemon/session/collectors.js +39 -2
  52. package/dist/daemon/session/commandRegistry.d.ts +14 -1
  53. package/dist/daemon/session/commandRegistry.js +46 -11
  54. package/dist/daemon/session/downloads.d.ts +32 -0
  55. package/dist/daemon/session/downloads.js +96 -0
  56. package/dist/daemon/session/interactions.d.ts +3 -2
  57. package/dist/daemon/session/interactions.js +7 -2
  58. package/dist/daemon/session/plugins.js +6 -0
  59. package/dist/daemon.js +12843 -11482
  60. package/dist/errors/CommandError.d.ts +2 -0
  61. package/dist/errors/issues.d.ts +1 -1
  62. package/dist/errors/messages.d.ts +58 -0
  63. package/dist/errors/messages.js +112 -0
  64. package/dist/index.js +999 -1020
  65. package/dist/ipc/client.d.ts +14 -1
  66. package/dist/ipc/client.js +21 -4
  67. package/dist/ipc/protocol/commands.d.ts +32 -2
  68. package/dist/ipc/protocol/commands.js +1 -0
  69. package/dist/ipc/protocol/domTypes.d.ts +24 -1
  70. package/dist/ipc/session/queries.d.ts +3 -0
  71. package/dist/ipc/session/types.d.ts +5 -0
  72. package/dist/ipc/transport/IPCError.d.ts +9 -0
  73. package/dist/ipc/transport/IPCError.js +12 -0
  74. package/dist/ipc/transport/errors.d.ts +2 -1
  75. package/dist/ipc/transport/errors.js +4 -1
  76. package/dist/ipc/transport/index.d.ts +4 -2
  77. package/dist/ipc/transport/index.js +13 -3
  78. package/dist/runtime/dom/actionEffects.d.ts +48 -9
  79. package/dist/runtime/dom/actionEffects.js +269 -34
  80. package/dist/runtime/dom/actionEffectsScripts.d.ts +45 -0
  81. package/dist/runtime/dom/actionEffectsScripts.js +101 -2
  82. package/dist/runtime/dom/captureArea.d.ts +35 -0
  83. package/dist/runtime/dom/captureArea.js +203 -0
  84. package/dist/runtime/dom/elementInfo.d.ts +10 -8
  85. package/dist/runtime/dom/elementInfo.js +8 -6
  86. package/dist/runtime/dom/formDiscovery.d.ts +1 -1
  87. package/dist/runtime/page/bdgWorld.d.ts +9 -0
  88. package/dist/runtime/page/bdgWorld.js +11 -0
  89. package/dist/runtime/page/captureEmulation.d.ts +119 -0
  90. package/dist/runtime/page/captureEmulation.js +189 -0
  91. package/dist/runtime/page/captureScroll.d.ts +24 -0
  92. package/dist/runtime/page/captureScroll.js +124 -0
  93. package/dist/runtime/page/screenshot.d.ts +41 -0
  94. package/dist/runtime/page/screenshot.js +394 -0
  95. package/dist/session/paths.d.ts +14 -0
  96. package/dist/session/paths.js +25 -0
  97. package/dist/telemetry/downloads.d.ts +127 -0
  98. package/dist/telemetry/downloads.js +265 -0
  99. package/dist/telemetry/har/builder.js +22 -7
  100. package/dist/telemetry/har/sanitize.d.ts +7 -3
  101. package/dist/telemetry/har/sanitize.js +52 -6
  102. package/dist/telemetry/har/sanitizeBody.d.ts +47 -7
  103. package/dist/telemetry/har/sanitizeBody.js +429 -56
  104. package/dist/telemetry/har/types.d.ts +2 -0
  105. package/dist/telemetry/network.d.ts +4 -4
  106. package/dist/telemetry/network.js +38 -4
  107. package/dist/telemetry/networkRetention.d.ts +35 -14
  108. package/dist/telemetry/networkRetention.js +62 -26
  109. package/dist/types.d.ts +9 -14
  110. package/dist/ui/OutputBuilder.d.ts +3 -2
  111. package/dist/ui/OutputBuilder.js +4 -3
  112. package/dist/ui/formatters/cdp.d.ts +32 -9
  113. package/dist/ui/formatters/cdp.js +77 -6
  114. package/dist/ui/formatters/details.js +7 -15
  115. package/dist/ui/formatters/preview.d.ts +2 -0
  116. package/dist/ui/formatters/preview.js +7 -1
  117. package/dist/ui/formatters/status.js +6 -1
  118. package/dist/ui/formatting.d.ts +7 -0
  119. package/dist/ui/formatting.js +13 -0
  120. package/dist/ui/logging/logger.d.ts +1 -1
  121. package/dist/ui/messages/chrome.d.ts +13 -0
  122. package/dist/ui/messages/chrome.js +26 -0
  123. package/dist/ui/messages/commands.d.ts +71 -3
  124. package/dist/ui/messages/commands.js +98 -3
  125. package/dist/ui/messages/networkMessages.d.ts +24 -5
  126. package/dist/ui/messages/networkMessages.js +31 -8
  127. package/dist/utils/async.d.ts +3 -2
  128. package/dist/utils/async.js +16 -3
  129. package/dist/utils/http.d.ts +11 -4
  130. package/dist/utils/http.js +5 -3
  131. package/package.json +18 -4
  132. /package/dist/{commands/dom → runtime/page}/screenshotResize.d.ts +0 -0
  133. /package/dist/{commands/dom → runtime/page}/screenshotResize.js +0 -0
@@ -1,7 +1,7 @@
1
1
  import { type Command } from 'commander';
2
2
  import { type CommandResult } from './shared/CommandRunner.js';
3
3
  import type { CdpCommandOptions } from './shared/optionTypes.js';
4
- import { type CdpExecuteData } from '../ui/formatters/cdp.js';
4
+ import { type CdpDomainDescription, type CdpExecuteData, type CdpMethodDescription, type CdpTypeDescription } from '../ui/formatters/cdp.js';
5
5
  /**
6
6
  * Register CDP command with full introspection support.
7
7
  *
@@ -35,4 +35,41 @@ export declare function isBareDomain(method: string, options: CdpCommandOptions)
35
35
  * @returns Error result, or undefined when the result has no exception
36
36
  */
37
37
  export declare function pageExceptionResult(result: unknown): CommandResult<CdpExecuteData> | undefined;
38
+ /** A description `bdg cdp <name> --describe` gives */
39
+ type CdpDescription = CdpMethodDescription | CdpDomainDescription | CdpTypeDescription;
40
+ /**
41
+ * Handle describe mode: show a domain, a method's signature and parameters,
42
+ * or a protocol type's values or properties.
43
+ *
44
+ * @param name - Domain, `Domain.method` or `Domain.Type` (case-insensitive)
45
+ * @returns Success result with the description
46
+ */
47
+ export declare function handleDescribeMethod(name: string): CommandResult<CdpDescription>;
48
+ /**
49
+ * The method `bdg cdp <name>` sends: a bundled method with its casing, or a
50
+ * well-formed method the bundled protocol lacks, as typed with a warning.
51
+ *
52
+ * @param methodName - Method name as typed
53
+ * @param options - `sendAnyway` sends a close typo of bundled methods as typed
54
+ * @returns Method to send, and the warning for one the bundled protocol lacks
55
+ * @throws CommandError (exit 81) for a blocked method, a type, a close typo
56
+ * of bundled methods (without `sendAnyway`) or a name that is not `Domain.method`
57
+ */
58
+ export declare function methodToSend(methodName: string, options?: Pick<CdpCommandOptions, 'sendAnyway'>): {
59
+ method: string;
60
+ warning?: string;
61
+ };
62
+ /**
63
+ * The result of a CDP call: the page exception it reported (exit 91), or its
64
+ * result with the session's and the method's hints; either way with the
65
+ * warning for a method the bundled protocol lacks.
66
+ *
67
+ * @param method - Method called
68
+ * @param cdpResult - What Chrome returned
69
+ * @param warning - Warning for a method the bundled protocol lacks
70
+ * @param ipcHint - Hint the session gave (e.g. a repeated-call pattern)
71
+ * @returns Command result
72
+ */
73
+ export declare function cdpCallResult(method: string, cdpResult: unknown, warning?: string, ipcHint?: string): CommandResult<CdpExecuteData>;
74
+ export {};
38
75
  //# sourceMappingURL=cdp.d.ts.map
@@ -1,14 +1,16 @@
1
1
  import { Option } from 'commander';
2
- import { normalizeMethod } from '../cdp/protocol.js';
3
- import { getAllDomainSummaries, getDomainMethods, getProtocolCounts, getDomainSummary, getMethodSchema, } from '../cdp/schema.js';
2
+ import { resolveMethodTarget } from '../cdp/methodTarget.js';
3
+ import { getBundledProtocolVersion } from '../cdp/protocol.js';
4
+ import { getAllDomainSummaries, getDomainMethods, getProtocolCounts, getDomainSummary, getMethodSchema, getTypeSchema, } from '../cdp/schema.js';
4
5
  import { runCommand } from './shared/CommandRunner.js';
5
6
  import { jsonOption } from './shared/commonOptions.js';
6
7
  import { CommandError } from '../errors/index.js';
7
- import { emptyCdpSearchError, missingArgumentError, scriptExecutionError, } from '../errors/messages.js';
8
+ import { cdpMethodNotFoundError, cdpMethodNotInBundledProtocolError, cdpMethodTypoError, cdpTypeNotMethodError, emptyCdpSearchError, missingArgumentError, scriptExecutionError, } from '../errors/messages.js';
8
9
  import { callCDP } from '../ipc/client.js';
9
10
  import { validateIPCResponse } from '../ipc/index.js';
10
11
  import { describeException } from '../runtime/dom/evalHelpers.js';
11
12
  import { formatCdpDescription, formatCdpDomainMethods, formatCdpDomains, formatCdpResult, formatCdpSearch, isEmptyCdpResult, } from '../ui/formatters/cdp.js';
13
+ import { CDP_EXECUTION_HELP, cdpUnlistedMethodWarning } from '../ui/messages/commands.js';
12
14
  import { formatHint } from '../ui/messages/hints.js';
13
15
  import { sessionCommand } from '../ui/messages/sessionCommand.js';
14
16
  import { getErrorMessage } from '../utils/errors.js';
@@ -83,7 +85,7 @@ export function registerCdpCommand(program) {
83
85
  .summary('CDP protocol introspection and execution')
84
86
  .description('CDP protocol introspection and execution\n' +
85
87
  ' Discovery: --list, --search, --describe\n' +
86
- ' Execution: case-insensitive (network.getcookies works)')
88
+ CDP_EXECUTION_HELP)
87
89
  .argument('[method]', 'CDP method name (e.g., Network.getCookies, network.getcookies)')
88
90
  .addOption(new Option('--params <json>', 'Method parameters as JSON'))
89
91
  .addOption(new Option('--list', 'List all domains or methods in a domain').conflicts([
@@ -91,6 +93,7 @@ export function registerCdpCommand(program) {
91
93
  'params',
92
94
  ]))
93
95
  .addOption(new Option('--describe', 'Show method signature and parameters').conflicts('params'))
96
+ .addOption(new Option('--send-anyway', 'Send a Domain.method that looks like a typo of a bundled one as typed').conflicts(['list', 'describe', 'search']))
94
97
  .addOption(new Option('--search <query>', 'Search methods by keyword').conflicts([
95
98
  'list',
96
99
  'describe',
@@ -123,7 +126,7 @@ async function runCdpCommand(method, options) {
123
126
  return runCommand(async () => handleDescribeMethod(method), options, formatCdpDescription);
124
127
  }
125
128
  if (method) {
126
- return runCommand(async () => handleExecuteMethod(method, options.params), options, formatCdpResult);
129
+ return runCommand(async () => handleExecuteMethod(method, options), options, formatCdpResult);
127
130
  }
128
131
  return runCommand(async () => {
129
132
  const err = missingArgumentError(CDP_USAGE);
@@ -331,99 +334,116 @@ function handleListDomainMethods(domainName) {
331
334
  };
332
335
  }
333
336
  /**
334
- * Handle describe method mode: Show method signature and parameters.
337
+ * Handle describe mode: show a domain, a method's signature and parameters,
338
+ * or a protocol type's values or properties.
335
339
  *
336
- * @param methodName - Method name (case-insensitive, with or without domain)
337
- * @returns Success result with method schema
340
+ * @param name - Domain, `Domain.method` or `Domain.Type` (case-insensitive)
341
+ * @returns Success result with the description
338
342
  */
339
- function handleDescribeMethod(methodName) {
340
- const [domainName, method] = methodName.includes('.')
341
- ? methodName.split('.')
342
- : [methodName, undefined];
343
- if (!method) {
344
- const summary = getDomainSummary(domainName);
345
- if (!summary) {
346
- const similar = findSimilarMethods(methodName);
347
- const suggestions = ['Use: bdg cdp --list (to see all domains)'];
348
- if (similar.length > 0) {
349
- suggestions.push('');
350
- suggestions.push('Did you mean:');
351
- similar.forEach((name) => suggestions.push(` - ${name}`));
352
- }
353
- return {
354
- success: false,
355
- error: `Domain or method '${methodName}' not found`,
356
- exitCode: EXIT_CODES.INVALID_ARGUMENTS,
357
- errorContext: {
358
- suggestion: suggestions.join('\n'),
359
- },
360
- };
361
- }
362
- const domainNote = DOMAIN_NOTES[summary.name];
363
- return {
364
- success: true,
365
- data: {
366
- type: 'domain',
367
- domain: summary.name,
368
- description: summary.description,
369
- commands: summary.commandCount,
370
- events: summary.eventCount,
371
- experimental: summary.experimental,
372
- deprecated: summary.deprecated,
373
- note: domainNote,
374
- nextStep: `Use: bdg cdp ${summary.name} --list (to see all methods)`,
375
- },
376
- };
377
- }
378
- const schema = getMethodSchema(domainName, method);
379
- if (!schema) {
380
- const similar = findSimilarMethods(methodName, domainName);
381
- const suggestions = [`Use: bdg cdp ${domainName} --list (to see all ${domainName} methods)`];
382
- if (similar.length > 0) {
383
- suggestions.push('');
384
- suggestions.push('Did you mean:');
385
- similar.forEach((name) => suggestions.push(` - ${name}`));
386
- }
343
+ export function handleDescribeMethod(name) {
344
+ const [domainName = '', member] = name.includes('.') ? name.split('.') : [name, undefined];
345
+ if (!member)
346
+ return describeDomain(name);
347
+ const schema = getMethodSchema(domainName, member);
348
+ if (schema)
349
+ return { success: true, data: describeMethod(schema) };
350
+ const type = getTypeSchema(domainName, member);
351
+ if (type)
352
+ return { success: true, data: { type: 'type', ...type } };
353
+ const target = resolveMethodTarget(name);
354
+ const err = target.kind === 'unlisted'
355
+ ? cdpMethodNotInBundledProtocolError(target.method, getBundledProtocolVersion())
356
+ : cdpMethodNotFoundError(name, findSimilarMethods(name, domainName), `Use: bdg cdp ${domainName} --list (to see all ${domainName} methods)`);
357
+ return {
358
+ success: false,
359
+ error: err.message,
360
+ exitCode: EXIT_CODES.INVALID_ARGUMENTS,
361
+ errorContext: { suggestion: err.suggestion },
362
+ };
363
+ }
364
+ /**
365
+ * Describe a domain (`bdg cdp Network --describe`).
366
+ *
367
+ * @param domainName - Domain name (case-insensitive)
368
+ * @returns Domain description, or not found with similar names
369
+ */
370
+ function describeDomain(domainName) {
371
+ const summary = getDomainSummary(domainName);
372
+ if (!summary) {
373
+ const err = cdpMethodNotFoundError(domainName, findSimilarMethods(domainName), 'Use: bdg cdp --list (to see all domains)');
387
374
  return {
388
375
  success: false,
389
- error: `Method '${methodName}' not found`,
376
+ error: `Domain or method '${domainName}' not found`,
390
377
  exitCode: EXIT_CODES.INVALID_ARGUMENTS,
391
- errorContext: {
392
- suggestion: suggestions.join('\n'),
393
- },
378
+ errorContext: { suggestion: err.suggestion },
394
379
  };
395
380
  }
396
- const methodNote = METHOD_NOTES[schema.name] ?? DOMAIN_NOTES[schema.domain];
397
- const alternative = blockedAlternative(schema.name);
398
381
  return {
399
382
  success: true,
400
383
  data: {
401
- type: 'method',
402
- name: schema.name,
403
- domain: schema.domain,
404
- method: schema.method,
405
- description: schema.description,
406
- experimental: schema.experimental,
407
- deprecated: schema.deprecated,
408
- note: methodNote,
409
- parameters: schema.parameters.map((p) => ({
410
- name: p.name,
411
- type: p.type,
412
- required: p.required,
413
- description: p.description,
414
- enum: p.enum,
415
- items: p.items,
416
- deprecated: p.deprecated,
417
- })),
418
- returns: schema.returns.map((r) => ({
419
- name: r.name,
420
- type: r.type,
421
- optional: r.optional,
422
- description: r.description,
423
- items: r.items,
424
- })),
425
- example: alternative ? { command: alternative } : schema.example,
384
+ type: 'domain',
385
+ domain: summary.name,
386
+ description: summary.description,
387
+ commands: summary.commandCount,
388
+ events: summary.eventCount,
389
+ experimental: summary.experimental,
390
+ deprecated: summary.deprecated,
391
+ note: DOMAIN_NOTES[summary.name],
392
+ nextStep: `Use: bdg cdp ${summary.name} --list (to see all methods)`,
393
+ },
394
+ };
395
+ }
396
+ /**
397
+ * A parameter as `--describe` shows it.
398
+ *
399
+ * @param p - Parameter schema
400
+ * @returns Described parameter
401
+ */
402
+ function describeParameter(p) {
403
+ return {
404
+ name: p.name,
405
+ type: p.type,
406
+ required: p.required,
407
+ description: p.description,
408
+ enum: p.enum,
409
+ ref: p.ref,
410
+ refType: p.refType,
411
+ items: p.items,
412
+ experimental: p.experimental,
413
+ deprecated: p.deprecated,
414
+ };
415
+ }
416
+ /**
417
+ * Describe a method (`bdg cdp Network.getCookies --describe`).
418
+ *
419
+ * @param schema - Method schema
420
+ * @returns Method description with its redirect, note and example
421
+ */
422
+ function describeMethod(schema) {
423
+ const alternative = blockedAlternative(schema.name);
424
+ return {
425
+ type: 'method',
426
+ name: schema.name,
427
+ domain: schema.domain,
428
+ method: schema.method,
429
+ description: schema.description,
430
+ experimental: schema.experimental,
431
+ deprecated: schema.deprecated,
432
+ note: METHOD_NOTES[schema.name] ?? DOMAIN_NOTES[schema.domain],
433
+ parameters: schema.parameters.map(describeParameter),
434
+ returns: schema.returns.map((r) => ({
435
+ name: r.name,
436
+ type: r.type,
437
+ optional: r.optional,
438
+ description: r.description,
439
+ items: r.items,
440
+ })),
441
+ redirect: schema.redirect && {
442
+ method: schema.redirect.method,
443
+ resolved: schema.redirect.resolved,
444
+ parameters: schema.redirect.parameters.map(describeParameter),
426
445
  },
446
+ example: alternative ? { command: alternative } : schema.example,
427
447
  };
428
448
  }
429
449
  /**
@@ -445,44 +465,78 @@ const BLOCKED_CDP_METHODS = {
445
465
  },
446
466
  };
447
467
  /**
448
- * Handle execute method mode: Call CDP method.
468
+ * The method `bdg cdp <name>` sends: a bundled method with its casing, or a
469
+ * well-formed method the bundled protocol lacks, as typed with a warning.
449
470
  *
450
- * @param methodName - Method name (case-insensitive)
451
- * @param paramsJson - Parameters as JSON string
452
- * @returns Success result with method response
471
+ * @param methodName - Method name as typed
472
+ * @param options - `sendAnyway` sends a close typo of bundled methods as typed
473
+ * @returns Method to send, and the warning for one the bundled protocol lacks
474
+ * @throws CommandError (exit 81) for a blocked method, a type, a close typo
475
+ * of bundled methods (without `sendAnyway`) or a name that is not `Domain.method`
453
476
  */
454
- async function handleExecuteMethod(methodName, paramsJson) {
455
- const normalized = normalizeMethod(methodName);
456
- if (normalized && BLOCKED_CDP_METHODS[normalized]) {
457
- const blocked = BLOCKED_CDP_METHODS[normalized];
458
- return {
459
- success: false,
460
- error: `${normalized} is blocked via raw CDP: ${blocked.reason}`,
461
- exitCode: EXIT_CODES.INVALID_ARGUMENTS,
462
- errorContext: { suggestion: `Use: ${sessionCommand(blocked.alternative)}` },
463
- };
464
- }
465
- if (!normalized) {
466
- const similar = findSimilarMethods(methodName);
467
- const suggestions = ['Use: bdg cdp --search <keyword> (to search for methods)'];
468
- if (similar.length > 0) {
469
- suggestions.push('');
470
- suggestions.push('Did you mean:');
471
- similar.forEach((name) => suggestions.push(` - ${name}`));
477
+ export function methodToSend(methodName, options = {}) {
478
+ const target = resolveMethodTarget(methodName);
479
+ if (target.kind === 'known') {
480
+ const blocked = BLOCKED_CDP_METHODS[target.method];
481
+ if (blocked) {
482
+ throw new CommandError(`${target.method} is blocked via raw CDP: ${blocked.reason}`, { suggestion: `Use: ${sessionCommand(blocked.alternative)}` }, EXIT_CODES.INVALID_ARGUMENTS);
472
483
  }
484
+ return { method: target.method };
485
+ }
486
+ if (target.kind === 'unlisted' || (target.kind === 'typo' && options.sendAnyway)) {
473
487
  return {
474
- success: false,
475
- error: `Method '${methodName}' not found`,
476
- exitCode: EXIT_CODES.INVALID_ARGUMENTS,
477
- errorContext: {
478
- suggestion: suggestions.join('\n'),
479
- },
488
+ method: target.method,
489
+ warning: cdpUnlistedMethodWarning(target.method, getBundledProtocolVersion()),
480
490
  };
481
491
  }
492
+ const err = target.kind === 'type'
493
+ ? cdpTypeNotMethodError(target.name)
494
+ : unknownMethodError(methodName, target);
495
+ throw new CommandError(err.message, { suggestion: err.suggestion }, EXIT_CODES.INVALID_ARGUMENTS);
496
+ }
497
+ /**
498
+ * Not found, for a close typo of bundled methods or domains, or a name that
499
+ * is not `Domain.method`.
500
+ *
501
+ * @param methodName - Method name as typed
502
+ * @param target - Typo with its suggestions, or malformed
503
+ * @returns Message and suggestion
504
+ */
505
+ function unknownMethodError(methodName, target) {
506
+ if (target.kind === 'malformed') {
507
+ return cdpMethodNotFoundError(methodName, findSimilarMethods(methodName), 'Use: bdg cdp --search <keyword> (to search for methods)');
508
+ }
509
+ const [domainName = ''] = methodName.split('.');
510
+ return target.suggestions.length > 0
511
+ ? cdpMethodTypoError(methodName, target.suggestions.slice(0, 3), 'Use: bdg cdp --search <keyword> (to search for methods)')
512
+ : cdpMethodTypoError(methodName, [], domainSuggestion(domainName));
513
+ }
514
+ /**
515
+ * Add the warning about a method missing from the bundled protocol to the
516
+ * error Chrome gave for it, so a failed call still says it was sent as typed.
517
+ *
518
+ * @param error - Error of the call
519
+ * @param warning - Warning, when the method was not in the bundled protocol
520
+ * @returns The error, with the warning (top-level JSON `warning`) when there is one
521
+ */
522
+ function withWarning(error, warning) {
523
+ if (!warning || !(error instanceof CommandError))
524
+ return error;
525
+ return new CommandError(error.message, { ...error.metadata, warning }, error.exitCode);
526
+ }
527
+ /**
528
+ * Handle execute method mode: Call CDP method.
529
+ *
530
+ * @param methodName - Method name (case-insensitive for bundled methods)
531
+ * @param paramsJson - Parameters as JSON string
532
+ * @returns Success result with method response
533
+ */
534
+ async function handleExecuteMethod(methodName, options) {
535
+ const { method: normalized, warning } = methodToSend(methodName, options);
482
536
  let params;
483
- if (paramsJson) {
537
+ if (options.params) {
484
538
  try {
485
- params = JSON.parse(paramsJson);
539
+ params = JSON.parse(options.params);
486
540
  }
487
541
  catch (error) {
488
542
  return {
@@ -492,29 +546,42 @@ async function handleExecuteMethod(methodName, paramsJson) {
492
546
  errorContext: {
493
547
  suggestion: `Use: bdg cdp ${normalized} --describe (to see parameter schema)`,
494
548
  },
549
+ ...(warning && { warning }),
495
550
  };
496
551
  }
497
552
  }
498
553
  const response = await callCDP(normalized, params);
499
- validateIPCResponse(response);
500
- const cdpResult = response.data?.result;
554
+ try {
555
+ validateIPCResponse(response);
556
+ }
557
+ catch (error) {
558
+ throw withWarning(error, warning);
559
+ }
560
+ const ipcHint = response.data?.hint && formatHint(response.data.hint);
561
+ return cdpCallResult(normalized, response.data?.result, warning, ipcHint);
562
+ }
563
+ /**
564
+ * The result of a CDP call: the page exception it reported (exit 91), or its
565
+ * result with the session's and the method's hints; either way with the
566
+ * warning for a method the bundled protocol lacks.
567
+ *
568
+ * @param method - Method called
569
+ * @param cdpResult - What Chrome returned
570
+ * @param warning - Warning for a method the bundled protocol lacks
571
+ * @param ipcHint - Hint the session gave (e.g. a repeated-call pattern)
572
+ * @returns Command result
573
+ */
574
+ export function cdpCallResult(method, cdpResult, warning, ipcHint) {
575
+ const withCallWarning = warning ? { warning } : {};
501
576
  const exception = pageExceptionResult(cdpResult);
502
577
  if (exception)
503
- return exception;
504
- const result = {
578
+ return { ...exception, ...withCallWarning };
579
+ const hints = [ipcHint, getMethodHint(method, cdpResult)].filter(Boolean);
580
+ return {
505
581
  success: true,
506
- data: {
507
- method: normalized,
508
- result: cdpResult,
509
- },
582
+ data: { method, result: cdpResult },
583
+ ...withCallWarning,
584
+ ...(hints.length > 0 && { hint: hints.join('\n') }),
510
585
  };
511
- if (response.data?.hint) {
512
- result.hint = formatHint(response.data.hint);
513
- }
514
- const methodHint = getMethodHint(normalized, cdpResult);
515
- if (methodHint) {
516
- result.hint = result.hint ? `${result.hint}\n${methodHint}` : methodHint;
517
- }
518
- return result;
519
586
  }
520
587
  //# sourceMappingURL=cdp.js.map
@@ -5,10 +5,10 @@ import { purgeNeedsNamedSessionError, purgeRefusedError, sessionDirIsFileError,
5
5
  import { isSessionChrome } from '../session/cleanup/staleSession.js';
6
6
  import { performSessionCleanup } from '../session/cleanup/userCommands.js';
7
7
  import { isDaemonAlive } from '../session/daemonSocket.js';
8
- import { getSessionDir, getSessionFilePath, getSessionName } from '../session/paths.js';
8
+ import { getSessionDir, getSessionDownloadsDir, getSessionFilePath, getSessionName, } from '../session/paths.js';
9
9
  import { readDaemonPid, readPidFromFile } from '../session/pid.js';
10
10
  import { joinLines } from '../ui/formatting.js';
11
- import { sessionFilesCleanedMessage, sessionOutputRemovedMessage, sessionDirectoryCleanMessage, sessionDirectoryPurgedMessage, noSessionFilesMessage, sessionStillActiveError, sessionStillActiveSuggestion, warningMessage, } from '../ui/messages/commands.js';
11
+ import { downloadsKeptMessage, sessionFilesCleanedMessage, sessionOutputRemovedMessage, sessionDirectoryCleanMessage, sessionDirectoryPurgedMessage, noSessionFilesMessage, sessionStillActiveError, sessionStillActiveSuggestion, warningMessage, } from '../ui/messages/commands.js';
12
12
  import { delay } from '../utils/async.js';
13
13
  import { EXIT_CODES } from '../utils/exitCodes.js';
14
14
  import { isProcessAlive } from '../utils/process.js';
@@ -19,7 +19,7 @@ import { isProcessAlive } from '../utils/process.js';
19
19
  */
20
20
  function formatCleanup(data) {
21
21
  const { cleaned } = data;
22
- return joinLines(cleaned.session && sessionFilesCleanedMessage(), cleaned.output && sessionOutputRemovedMessage(), ...(data.warnings ?? []).map((warning) => warningMessage(warning)), '', data.message);
22
+ return joinLines(cleaned.session && sessionFilesCleanedMessage(), cleaned.output && sessionOutputRemovedMessage(), data.downloadsKept && downloadsKeptMessage(data.downloadsKept.dir, data.downloadsKept.files), ...(data.warnings ?? []).map((warning) => warningMessage(warning)), '', data.message);
23
23
  }
24
24
  /**
25
25
  * Delete the selected named session's directory (Chrome profile, logs, port).
@@ -162,11 +162,13 @@ async function cleanupSession(opts) {
162
162
  };
163
163
  }
164
164
  const didCleanup = Object.values(cleaned).some(Boolean) || purged !== undefined;
165
+ const downloadsKept = purged === undefined ? keptDownloads() : undefined;
165
166
  return {
166
167
  success: true,
167
168
  data: {
168
169
  cleaned,
169
170
  ...(purged !== undefined && { purged }),
171
+ ...(downloadsKept && { downloadsKept }),
170
172
  message: !didCleanup
171
173
  ? noSessionFilesMessage()
172
174
  : purged !== undefined
@@ -176,6 +178,18 @@ async function cleanupSession(opts) {
176
178
  },
177
179
  };
178
180
  }
181
+ /**
182
+ * Downloaded files of the session, which cleanup keeps.
183
+ *
184
+ * @returns Their directory and count, or undefined when there are none
185
+ */
186
+ function keptDownloads() {
187
+ const dir = getSessionDownloadsDir();
188
+ if (!fs.existsSync(dir) || !fs.statSync(dir).isDirectory())
189
+ return undefined;
190
+ const files = fs.readdirSync(dir).length;
191
+ return files > 0 ? { dir, files } : undefined;
192
+ }
179
193
  /**
180
194
  * Register cleanup command
181
195
  *
@@ -184,7 +198,7 @@ async function cleanupSession(opts) {
184
198
  export function registerCleanupCommand(program) {
185
199
  program
186
200
  .command('cleanup')
187
- .description('Clean up stale session files')
201
+ .description('Clean up stale session files (downloaded files are kept)')
188
202
  .option('-f, --force', 'Kill a running (possibly hung) session, then clean up', false)
189
203
  .option('--remove-output', 'Also remove session.json output file', false)
190
204
  .option('--aggressive', 'Alias for --force (kept for compatibility)', false)
@@ -18,7 +18,7 @@ import { callCDP, domClick, domFill, domPressKey, domScroll, domSubmit } from '.
18
18
  import { findUnknownModifiers } from '../../runtime/dom/keyMapping.js';
19
19
  import { formatTriggeredRequestLines, formatTriggeredRequestsTitle, } from '../../ui/formatters/triggeredRequests.js';
20
20
  import { OutputFormatter } from '../../ui/formatting.js';
21
- import { CLICK_RESULT_WAIT_HELP, HOVER_OFF_DONE, HOVER_USAGE, POINTER_ACTION_DONE, POINTER_ACTION_NOUN, pointerScrollText, actionStatusLine, dialogConsoleText, moreMessagesText, newMessageText, pageNavigationText, shownElementText, stillChangingNote, } from '../../ui/messages/commands.js';
21
+ import { CLICK_RESULT_WAIT_HELP, HOVER_OFF_DONE, HOVER_USAGE, POINTER_ACTION_DONE, POINTER_ACTION_NOUN, pointerScrollText, actionStatusLine, dialogConsoleText, downloadText, moreMessagesText, newMessageText, pageNavigationText, shownElementText, stillChangingNote, } from '../../ui/messages/commands.js';
22
22
  import { sessionCommand } from '../../ui/messages/sessionCommand.js';
23
23
  import { EXIT_CODES } from '../../utils/exitCodes.js';
24
24
  /** Help of `--strict` on click and hover */
@@ -313,9 +313,9 @@ async function runPointerCommand(selectorOrIndex, options, action) {
313
313
  * "⚠ Element Clicked (page still changing)" with what it was still working
314
314
  * on, or "⚠ Element Clicked (no visible effect: …)"), the details, what
315
315
  * changed on the page (`Page:` navigation, `New text:` messages, `Shown:`
316
- * elements), then the network requests it triggered and the dialogs it
317
- * caused. No request list is shown when there were none (JSON has an empty
318
- * `triggeredRequests` then).
316
+ * elements), then the network requests it triggered, the downloads it
317
+ * started and the dialogs it caused. No request list is shown when there
318
+ * were none (JSON has an empty `triggeredRequests` then).
319
319
  *
320
320
  * @param done - What was done, e.g. "Element Clicked"
321
321
  * @param details - Label/value rows
@@ -353,6 +353,10 @@ function formatActionOutput(done, details, result, options = {}) {
353
353
  .blank()
354
354
  .section(formatTriggeredRequestsTitle(result.triggeredRequests ?? [], omitted), requests);
355
355
  }
356
+ if (result.downloads?.length)
357
+ fmt.blank();
358
+ for (const download of result.downloads ?? [])
359
+ fmt.text(downloadText(download));
356
360
  for (const dialog of result.dialogs ?? []) {
357
361
  fmt.blank();
358
362
  fmt.text(`Dialog: ${dialogConsoleText(dialog)}`);
@@ -3,9 +3,9 @@
3
3
  * so callers can keep importing from `@/commands/dom/helpers.js`.
4
4
  *
5
5
  * - `query.ts` — selector → backend node ids (shadow roots, same-origin iframes), DOM.describeNode
6
- * - `screenshot.ts` — page / element capture, element bounds, scroll helpers
6
+ * - `screenshot.ts` — page / element capture through the daemon, writing the image
7
7
  */
8
- export { documentReadyState, noMatchesError, queryDOMElements, getDomContext, getDOMElements, resolveSelector, resolveBackendNodeIds, selectMatch, assertNodeAttached, pageDocumentId, } from './query.js';
9
- export { capturePageScreenshot, captureElementScreenshot, getElementBounds, } from './screenshot.js';
10
- export type { DomQueryResult, DomGetResult, ScreenshotResult, DomGetOptions, ScreenshotOptions, DomContext, ElementBounds, } from '../../../types.js';
8
+ export { noMatchesError, queryDOMElements, getDomContext, getDOMElements, resolveSelector, resolveBackendNodeIds, selectMatch, assertNodeAttached, pageDocumentId, } from './query.js';
9
+ export { captureScreenshot, screenshotInterrupted } from './screenshot.js';
10
+ export type { DomGetOptions, DomContext } from '../../../types.js';
11
11
  //# sourceMappingURL=index.d.ts.map
@@ -3,8 +3,8 @@
3
3
  * so callers can keep importing from `@/commands/dom/helpers.js`.
4
4
  *
5
5
  * - `query.ts` — selector → backend node ids (shadow roots, same-origin iframes), DOM.describeNode
6
- * - `screenshot.ts` — page / element capture, element bounds, scroll helpers
6
+ * - `screenshot.ts` — page / element capture through the daemon, writing the image
7
7
  */
8
- export { documentReadyState, noMatchesError, queryDOMElements, getDomContext, getDOMElements, resolveSelector, resolveBackendNodeIds, selectMatch, assertNodeAttached, pageDocumentId, } from './query.js';
9
- export { capturePageScreenshot, captureElementScreenshot, getElementBounds, } from './screenshot.js';
8
+ export { noMatchesError, queryDOMElements, getDomContext, getDOMElements, resolveSelector, resolveBackendNodeIds, selectMatch, assertNodeAttached, pageDocumentId, } from './query.js';
9
+ export { captureScreenshot, screenshotInterrupted } from './screenshot.js';
10
10
  //# sourceMappingURL=index.js.map
@@ -91,10 +91,10 @@ export declare function getDOMElements(options: DomGetOptions): Promise<DomGetRe
91
91
  * Resolve a selector to its first match.
92
92
  *
93
93
  * @param selector - CSS selector
94
- * @returns Reference to the first matching node (valid within this command)
94
+ * @returns Backend node id of the first match
95
95
  * @throws CommandError (83) when nothing matches
96
96
  */
97
- export declare function resolveSelector(selector: string): Promise<NodeRef>;
97
+ export declare function resolveSelector(selector: string): Promise<number>;
98
98
  /**
99
99
  * Resolve selectors to backend node ids (first match each).
100
100
  *
@@ -655,7 +655,7 @@ export async function getDOMElements(options) {
655
655
  * Resolve a selector to its first match.
656
656
  *
657
657
  * @param selector - CSS selector
658
- * @returns Reference to the first matching node (valid within this command)
658
+ * @returns Backend node id of the first match
659
659
  * @throws CommandError (83) when nothing matches
660
660
  */
661
661
  export async function resolveSelector(selector) {
@@ -664,7 +664,7 @@ export async function resolveSelector(selector) {
664
664
  const err = await noMatchesError(selector);
665
665
  throw new CommandError(err.message, { suggestion: err.suggestion }, EXIT_CODES.RESOURCE_NOT_FOUND);
666
666
  }
667
- return { backendNodeId };
667
+ return backendNodeId;
668
668
  }
669
669
  /**
670
670
  * Resolve selectors to backend node ids (first match each).