browser-debugger-cli 0.12.0 → 0.13.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 (180) hide show
  1. package/.claude/skills/bdg/SKILL.md +4 -4
  2. package/README.md +1 -0
  3. package/dist/commands/cdp.d.ts +22 -1
  4. package/dist/commands/cdp.js +100 -43
  5. package/dist/commands/console.d.ts +12 -0
  6. package/dist/commands/console.js +62 -12
  7. package/dist/commands/dom/DomElementResolver.d.ts +3 -1
  8. package/dist/commands/dom/DomElementResolver.js +10 -3
  9. package/dist/commands/dom/a11y.js +3 -2
  10. package/dist/commands/dom/eval.d.ts +3 -2
  11. package/dist/commands/dom/eval.js +11 -5
  12. package/dist/commands/dom/form.js +10 -9
  13. package/dist/commands/dom/formInteraction.js +8 -7
  14. package/dist/commands/dom/get.js +8 -8
  15. package/dist/commands/dom/helpers/index.d.ts +1 -1
  16. package/dist/commands/dom/helpers/index.js +1 -1
  17. package/dist/commands/dom/helpers/query.d.ts +27 -3
  18. package/dist/commands/dom/helpers/query.js +152 -64
  19. package/dist/commands/dom/helpers/screenshot.js +13 -13
  20. package/dist/commands/dom/index.js +4 -2
  21. package/dist/commands/dom/query.d.ts +19 -2
  22. package/dist/commands/dom/query.js +37 -6
  23. package/dist/commands/dom/screenshot.js +2 -1
  24. package/dist/commands/dom/semanticUtils.d.ts +3 -2
  25. package/dist/commands/dom/semanticUtils.js +40 -9
  26. package/dist/commands/helpJson.d.ts +82 -19
  27. package/dist/commands/helpJson.js +111 -40
  28. package/dist/commands/helpTopic.d.ts +16 -1
  29. package/dist/commands/helpTopic.js +59 -1
  30. package/dist/commands/installSkill.d.ts +15 -5
  31. package/dist/commands/installSkill.js +86 -16
  32. package/dist/commands/network/list.js +22 -12
  33. package/dist/commands/optionBehaviors.js +33 -11
  34. package/dist/commands/shared/daemonErrorHandler.d.ts +5 -2
  35. package/dist/commands/shared/daemonErrorHandler.js +20 -9
  36. package/dist/commands/shared/dataFetcher.d.ts +12 -4
  37. package/dist/commands/shared/dataFetcher.js +12 -4
  38. package/dist/commands/shared/followMode.d.ts +9 -1
  39. package/dist/commands/shared/followMode.js +22 -4
  40. package/dist/commands/shared/optionTypes.d.ts +4 -1
  41. package/dist/commands/shared/outputFile.js +6 -1
  42. package/dist/commands/start.d.ts +7 -5
  43. package/dist/commands/start.js +65 -21
  44. package/dist/commands/stop.d.ts +11 -0
  45. package/dist/commands/stop.js +24 -1
  46. package/dist/commands.js +1 -1
  47. package/dist/connection/cdp.d.ts +7 -0
  48. package/dist/connection/cdp.js +9 -0
  49. package/dist/connection/launcher.js +3 -2
  50. package/dist/daemon/SessionController.js +6 -1
  51. package/dist/daemon/launcher.d.ts +3 -2
  52. package/dist/daemon/launcher.js +47 -3
  53. package/dist/daemon/session/Session.d.ts +4 -1
  54. package/dist/daemon/session/Session.js +33 -2
  55. package/dist/daemon/session/TelemetryStore.d.ts +8 -1
  56. package/dist/daemon/session/TelemetryStore.js +13 -1
  57. package/dist/daemon/session/commandRegistry.js +29 -13
  58. package/dist/daemon/session/interactions.d.ts +2 -1
  59. package/dist/daemon/session/interactions.js +13 -1
  60. package/dist/daemon/session/plugins.js +16 -2
  61. package/dist/daemon/session/teardown.js +1 -1
  62. package/dist/daemon.js +1622 -748
  63. package/dist/errors/messages.d.ts +54 -11
  64. package/dist/errors/messages.js +109 -22
  65. package/dist/index.js +13733 -8796
  66. package/dist/ipc/client.d.ts +18 -2
  67. package/dist/ipc/client.js +26 -5
  68. package/dist/ipc/protocol/auditTypes.d.ts +8 -2
  69. package/dist/ipc/protocol/commands.d.ts +12 -0
  70. package/dist/ipc/protocol/domTypes.d.ts +12 -0
  71. package/dist/ipc/protocol/inspectTypes.d.ts +2 -0
  72. package/dist/ipc/session/types.d.ts +2 -0
  73. package/dist/runtime/dom/actionEffects.d.ts +5 -1
  74. package/dist/runtime/dom/actionEffects.js +26 -14
  75. package/dist/runtime/dom/audit.js +3 -2
  76. package/dist/runtime/dom/auditModel.js +6 -1
  77. package/dist/runtime/dom/auditScripts.d.ts +9 -3
  78. package/dist/runtime/dom/auditScripts.js +41 -5
  79. package/dist/runtime/dom/elementGeometry.d.ts +10 -3
  80. package/dist/runtime/dom/elementGeometry.js +27 -4
  81. package/dist/runtime/dom/elementInfo.d.ts +74 -18
  82. package/dist/runtime/dom/elementInfo.js +187 -40
  83. package/dist/runtime/dom/evalHelpers.d.ts +12 -2
  84. package/dist/runtime/dom/evalHelpers.js +67 -7
  85. package/dist/runtime/dom/formDiscovery.d.ts +6 -2
  86. package/dist/runtime/dom/formDiscovery.js +20 -3
  87. package/dist/runtime/dom/formFillHelpers/fill.js +7 -11
  88. package/dist/runtime/dom/formFillHelpers/pressKey.js +2 -2
  89. package/dist/runtime/dom/formFillHelpers/shared.d.ts +16 -10
  90. package/dist/runtime/dom/formFillHelpers/shared.js +19 -52
  91. package/dist/runtime/dom/formSubmitHelpers.js +4 -3
  92. package/dist/runtime/dom/frameLayout.js +1 -0
  93. package/dist/runtime/dom/inspect.js +5 -6
  94. package/dist/runtime/dom/inspectAllStyles.js +1 -0
  95. package/dist/runtime/dom/inspectHints.d.ts +1 -1
  96. package/dist/runtime/dom/inspectModel.d.ts +2 -1
  97. package/dist/runtime/dom/inspectModel.js +7 -3
  98. package/dist/runtime/dom/inspectPaintModel.d.ts +2 -0
  99. package/dist/runtime/dom/inspectPaintModel.js +3 -1
  100. package/dist/runtime/dom/inspectScripts.d.ts +29 -2
  101. package/dist/runtime/dom/inspectScripts.js +49 -10
  102. package/dist/runtime/dom/layout.js +9 -7
  103. package/dist/runtime/dom/reactEventHelpers.d.ts +14 -4
  104. package/dist/runtime/dom/reactEventHelpers.js +63 -27
  105. package/dist/runtime/dom/targetNode.d.ts +18 -5
  106. package/dist/runtime/dom/targetNode.js +268 -8
  107. package/dist/runtime/dom/wait.js +2 -1
  108. package/dist/runtime/page/bdgWorld.d.ts +57 -0
  109. package/dist/runtime/page/bdgWorld.js +180 -0
  110. package/dist/runtime/page/replacedBuiltins.d.ts +28 -0
  111. package/dist/runtime/page/replacedBuiltins.js +136 -0
  112. package/dist/session/QueryCacheManager.d.ts +4 -1
  113. package/dist/session/QueryCacheManager.js +5 -2
  114. package/dist/session/chrome.d.ts +4 -1
  115. package/dist/session/chrome.js +7 -1
  116. package/dist/session/cleanup/staleSession.d.ts +21 -4
  117. package/dist/session/cleanup/staleSession.js +79 -9
  118. package/dist/session/cleanup/userCommands.d.ts +4 -1
  119. package/dist/session/cleanup/userCommands.js +10 -5
  120. package/dist/session/daemonSocket.d.ts +10 -0
  121. package/dist/session/daemonSocket.js +22 -0
  122. package/dist/session/lastSession.d.ts +6 -3
  123. package/dist/session/lastSession.js +11 -5
  124. package/dist/session/paths.d.ts +3 -1
  125. package/dist/session/paths.js +5 -5
  126. package/dist/session/portClaims.js +4 -3
  127. package/dist/session/sessionList.d.ts +13 -5
  128. package/dist/session/sessionList.js +31 -7
  129. package/dist/telemetry/a11y.js +2 -2
  130. package/dist/telemetry/console.d.ts +2 -1
  131. package/dist/telemetry/console.js +30 -21
  132. package/dist/telemetry/pageCrash.d.ts +26 -0
  133. package/dist/telemetry/pageCrash.js +53 -0
  134. package/dist/types.d.ts +16 -0
  135. package/dist/ui/formatters/audit.js +14 -5
  136. package/dist/ui/formatters/cdp.d.ts +138 -0
  137. package/dist/ui/formatters/cdp.js +131 -0
  138. package/dist/ui/formatters/console/chronological.js +3 -1
  139. package/dist/ui/formatters/console/follow.d.ts +2 -1
  140. package/dist/ui/formatters/console/follow.js +2 -2
  141. package/dist/ui/formatters/console/json.d.ts +2 -2
  142. package/dist/ui/formatters/console/json.js +11 -5
  143. package/dist/ui/formatters/console/shared.d.ts +30 -0
  144. package/dist/ui/formatters/console/shared.js +16 -0
  145. package/dist/ui/formatters/console/summarize.d.ts +9 -2
  146. package/dist/ui/formatters/console/summarize.js +40 -9
  147. package/dist/ui/formatters/console.d.ts +2 -1
  148. package/dist/ui/formatters/console.js +7 -5
  149. package/dist/ui/formatters/details.js +3 -1
  150. package/dist/ui/formatters/dom.d.ts +1 -1
  151. package/dist/ui/formatters/dom.js +5 -6
  152. package/dist/ui/formatters/helpFormatters.js +1 -1
  153. package/dist/ui/formatters/inspect.js +9 -3
  154. package/dist/ui/formatters/installSkill.d.ts +9 -1
  155. package/dist/ui/formatters/installSkill.js +32 -6
  156. package/dist/ui/formatters/layout.js +2 -1
  157. package/dist/ui/formatters/networkList.d.ts +1 -1
  158. package/dist/ui/formatters/networkList.js +1 -2
  159. package/dist/ui/formatters/preview.d.ts +2 -0
  160. package/dist/ui/formatters/preview.js +17 -7
  161. package/dist/ui/formatters/sessions.d.ts +2 -2
  162. package/dist/ui/formatters/sessions.js +9 -2
  163. package/dist/ui/logging/logger.d.ts +1 -1
  164. package/dist/ui/messages/commands.d.ts +124 -4
  165. package/dist/ui/messages/commands.js +162 -7
  166. package/dist/ui/messages/consoleMessages.d.ts +24 -0
  167. package/dist/ui/messages/consoleMessages.js +32 -0
  168. package/dist/ui/messages/preview.d.ts +6 -0
  169. package/dist/ui/messages/preview.js +9 -1
  170. package/dist/ui/messages/session.d.ts +13 -2
  171. package/dist/ui/messages/session.js +22 -3
  172. package/dist/utils/directories.d.ts +34 -0
  173. package/dist/utils/directories.js +88 -0
  174. package/dist/utils/display.d.ts +16 -0
  175. package/dist/utils/display.js +42 -0
  176. package/dist/utils/exitCodes.d.ts +1 -0
  177. package/dist/utils/exitCodes.js +6 -0
  178. package/dist/utils/process.d.ts +12 -0
  179. package/dist/utils/process.js +25 -0
  180. package/package.json +1 -1
@@ -16,8 +16,8 @@ bdg stop # End session
16
16
  ## Session Management
17
17
 
18
18
  ```bash
19
- bdg <url> # Start session (1920x1080, headless if no display)
20
- bdg <url> --headless # Force headless mode
19
+ bdg <url> # Start session (a window on a Mac or Linux desktop; headless over SSH, with CI set, on servers)
20
+ bdg <url> --headless # Force headless mode (do this when running unattended)
21
21
  bdg <url> --no-headless # Force visible browser window
22
22
  bdg status # Check session status
23
23
  bdg peek # Preview collected telemetry
@@ -98,7 +98,7 @@ Selectors search open shadow roots and same-origin iframes, and accept `:has-tex
98
98
  ### Look Without a Screenshot
99
99
 
100
100
  ```bash
101
- bdg dom inspect "button.primary" # Box, layout, rendered font, colors + WCAG contrast, borders, state (~60-100 tokens)
101
+ bdg dom inspect "button.primary" # Box, layout, rendered font, colors + WCAG contrast, borders, state (~80-130 tokens; --no-hints drops the hints)
102
102
  bdg dom inspect ".card" --why color # Which CSS rule set a property, and what it overrode
103
103
  bdg dom layout ".card" # Positions/sizes of every match: above/below the fold, hidden, covered
104
104
  bdg dom listeners "#save" # Event listeners that run for an element (incl. delegated, React/Preact)
@@ -176,7 +176,7 @@ bdg cdp Runtime.evaluate --params '{
176
176
 
177
177
  ## JSON Output and Exit Codes
178
178
 
179
- Add `--json` (`-j`) to any command for `{ version, success, data }` (or `{ success: false, error, exitCode, suggestion }`). `bdg --help --json` lists every command, flag and exit code.
179
+ Add `--json` (`-j`) to any command for `{ version, success, data }` (or `{ success: false, error, exitCode, suggestion }`). `bdg --help --json` lists every command, flag and exit code; `bdg <command> --help --json` describes one command in full (option behaviors, defaults, examples).
180
180
 
181
181
  | Code | Meaning | Action |
182
182
  |------|---------|--------|
package/README.md CHANGED
@@ -81,6 +81,7 @@ No docs to paste into the prompt. The agent asks bdg:
81
81
 
82
82
  ```bash
83
83
  bdg --help --json # Every command, flag and exit code, plus "task → command" mappings
84
+ bdg dom query --help --json # One command in full: option behaviors, defaults, examples
84
85
  bdg cdp --search cookie # 13 matching methods across all CDP domains, each with an example call
85
86
  bdg cdp Network.getCookies --describe # Parameters, return types, an example
86
87
  ```
@@ -1,4 +1,7 @@
1
- import type { Command } from 'commander';
1
+ import { type Command } from 'commander';
2
+ import { type CommandResult } from './shared/CommandRunner.js';
3
+ import type { CdpCommandOptions } from './shared/optionTypes.js';
4
+ import { type CdpExecuteData } from '../ui/formatters/cdp.js';
2
5
  /**
3
6
  * Register CDP command with full introspection support.
4
7
  *
@@ -14,4 +17,22 @@ import type { Command } from 'commander';
14
17
  * @param program - Commander.js Command instance to register commands on
15
18
  */
16
19
  export declare function registerCdpCommand(program: Command): void;
20
+ /**
21
+ * Whether the argument names a domain without a method and nothing asks to
22
+ * run it (`bdg cdp Network`): its methods are listed, as with `--list`.
23
+ *
24
+ * @param method - Method or domain argument
25
+ * @param options - Command options
26
+ * @returns True for a bare domain name
27
+ */
28
+ export declare function isBareDomain(method: string, options: CdpCommandOptions): boolean;
29
+ /**
30
+ * The page exception a method reported in its result (`Runtime.evaluate`,
31
+ * `Runtime.callFunctionOn`, ... answer a script that threw with
32
+ * `exceptionDetails`), as an error result like `dom eval`'s (exit 91).
33
+ *
34
+ * @param result - Method result
35
+ * @returns Error result, or undefined when the result has no exception
36
+ */
37
+ export declare function pageExceptionResult(result: unknown): CommandResult<CdpExecuteData> | undefined;
17
38
  //# sourceMappingURL=cdp.d.ts.map
@@ -1,10 +1,14 @@
1
+ import { Option } from 'commander';
1
2
  import { normalizeMethod } from '../cdp/protocol.js';
2
3
  import { getAllDomainSummaries, getDomainMethods, getProtocolCounts, getDomainSummary, getMethodSchema, } from '../cdp/schema.js';
3
4
  import { runCommand } from './shared/CommandRunner.js';
4
5
  import { jsonOption } from './shared/commonOptions.js';
5
6
  import { CommandError } from '../errors/index.js';
7
+ import { emptyCdpSearchError, missingArgumentError, scriptExecutionError, } from '../errors/messages.js';
6
8
  import { callCDP } from '../ipc/client.js';
7
9
  import { validateIPCResponse } from '../ipc/index.js';
10
+ import { describeException } from '../runtime/dom/evalHelpers.js';
11
+ import { formatCdpDescription, formatCdpDomainMethods, formatCdpDomains, formatCdpResult, formatCdpSearch, isEmptyCdpResult, } from '../ui/formatters/cdp.js';
8
12
  import { formatHint } from '../ui/messages/hints.js';
9
13
  import { sessionCommand } from '../ui/messages/sessionCommand.js';
10
14
  import { getErrorMessage } from '../utils/errors.js';
@@ -26,6 +30,8 @@ const DOMAIN_NOTES = {
26
30
  Tracing: 'Performance tracing. Call Tracing.start, perform actions, then Tracing.end. ' +
27
31
  'Data arrives via Tracing.dataCollected events.',
28
32
  };
33
+ /** Usage of `bdg cdp`, suggested when it gets neither a method nor a flag */
34
+ const CDP_USAGE = 'Usage: bdg cdp [method] [--params <json>] [--list] [--describe] [--search <query>]';
29
35
  /**
30
36
  * Domain and method counts of the bundled protocol, for the help text.
31
37
  *
@@ -44,18 +50,6 @@ const METHOD_NOTES = {
44
50
  'Profiler.start': 'Starts CPU profiling. Returns empty. Call Profiler.stop to get the profile data.',
45
51
  'Tracing.start': 'Starts tracing. Returns empty. Data arrives via events after Tracing.end.',
46
52
  };
47
- /**
48
- * Check if a CDP result is empty (null, undefined, or empty object).
49
- */
50
- function isEmptyResult(result) {
51
- if (result === null || result === undefined) {
52
- return true;
53
- }
54
- if (typeof result === 'object' && Object.keys(result).length === 0) {
55
- return true;
56
- }
57
- return false;
58
- }
59
53
  /**
60
54
  * Get contextual hint for a method based on domain notes and result.
61
55
  */
@@ -64,7 +58,7 @@ function getMethodHint(methodName, result) {
64
58
  return METHOD_NOTES[methodName];
65
59
  }
66
60
  const domain = methodName.split('.')[0];
67
- if (domain && DOMAIN_NOTES[domain] && isEmptyResult(result)) {
61
+ if (domain && DOMAIN_NOTES[domain] && isEmptyCdpResult(result)) {
68
62
  return DOMAIN_NOTES[domain];
69
63
  }
70
64
  return undefined;
@@ -90,35 +84,87 @@ export function registerCdpCommand(program) {
90
84
  ' Discovery: --list, --search, --describe\n' +
91
85
  ' Execution: case-insensitive (network.getcookies works)')
92
86
  .argument('[method]', 'CDP method name (e.g., Network.getCookies, network.getcookies)')
93
- .option('--params <json>', 'Method parameters as JSON')
94
- .option('--list', 'List all domains or methods in a domain')
95
- .option('--describe', 'Show method signature and parameters')
96
- .option('--search <query>', 'Search methods by keyword')
97
- .addOption(jsonOption().hideHelp())
87
+ .addOption(new Option('--params <json>', 'Method parameters as JSON'))
88
+ .addOption(new Option('--list', 'List all domains or methods in a domain').conflicts([
89
+ 'describe',
90
+ 'params',
91
+ ]))
92
+ .addOption(new Option('--describe', 'Show method signature and parameters').conflicts('params'))
93
+ .addOption(new Option('--search <query>', 'Search methods by keyword').conflicts([
94
+ 'list',
95
+ 'describe',
96
+ 'params',
97
+ ]))
98
+ .addOption(jsonOption())
98
99
  .addHelpText('after', () => `\nBundled protocol: ${cdpCountsText()}`)
99
100
  .action(async (method, options) => {
100
- await runCommand(async (opts) => {
101
- if (opts.search) {
102
- return await handleSearch(opts.search, method);
103
- }
104
- if (opts.list && !method) {
105
- return handleListDomains();
106
- }
107
- if (opts.list && method) {
108
- return handleListDomainMethods(method);
109
- }
110
- if (opts.describe && method) {
111
- return handleDescribeMethod(method);
112
- }
113
- if (method) {
114
- return await handleExecuteMethod(method, opts.params);
115
- }
116
- throw new CommandError('Missing required argument or flag', {
117
- suggestion: 'Usage: bdg cdp [method] [--params <json>] [--list] [--describe] [--search <query>]',
118
- }, EXIT_CODES.INVALID_ARGUMENTS);
119
- }, { ...options, json: true });
101
+ await runCdpCommand(method, options);
120
102
  });
121
103
  }
104
+ /**
105
+ * Run the `bdg cdp` mode the options select, with its human-readable output.
106
+ *
107
+ * @param method - Method or domain argument
108
+ * @param options - Command options (exits 81 when neither a method nor a
109
+ * discovery flag is given)
110
+ */
111
+ async function runCdpCommand(method, options) {
112
+ if (options.search !== undefined) {
113
+ const query = options.search;
114
+ return runCommand(async () => handleSearch(query, method), options, formatCdpSearch);
115
+ }
116
+ if (method && (options.list || isBareDomain(method, options))) {
117
+ return runCommand(async () => handleListDomainMethods(method), options, formatCdpDomainMethods);
118
+ }
119
+ if (options.list)
120
+ return runCommand(async () => handleListDomains(), options, formatCdpDomains);
121
+ if (options.describe && method) {
122
+ return runCommand(async () => handleDescribeMethod(method), options, formatCdpDescription);
123
+ }
124
+ if (method) {
125
+ return runCommand(async () => handleExecuteMethod(method, options.params), options, formatCdpResult);
126
+ }
127
+ return runCommand(async () => {
128
+ const err = missingArgumentError(CDP_USAGE);
129
+ throw new CommandError(err.message, { suggestion: err.suggestion }, EXIT_CODES.INVALID_ARGUMENTS);
130
+ }, options);
131
+ }
132
+ /**
133
+ * Whether the argument names a domain without a method and nothing asks to
134
+ * run it (`bdg cdp Network`): its methods are listed, as with `--list`.
135
+ *
136
+ * @param method - Method or domain argument
137
+ * @param options - Command options
138
+ * @returns True for a bare domain name
139
+ */
140
+ export function isBareDomain(method, options) {
141
+ return (!method.includes('.') &&
142
+ options.params === undefined &&
143
+ !options.describe &&
144
+ getDomainSummary(method) !== undefined);
145
+ }
146
+ /**
147
+ * The page exception a method reported in its result (`Runtime.evaluate`,
148
+ * `Runtime.callFunctionOn`, ... answer a script that threw with
149
+ * `exceptionDetails`), as an error result like `dom eval`'s (exit 91).
150
+ *
151
+ * @param result - Method result
152
+ * @returns Error result, or undefined when the result has no exception
153
+ */
154
+ export function pageExceptionResult(result) {
155
+ if (typeof result !== 'object' || result === null || !('exceptionDetails' in result)) {
156
+ return undefined;
157
+ }
158
+ const details = result
159
+ .exceptionDetails;
160
+ const err = scriptExecutionError(describeException(details));
161
+ return {
162
+ success: false,
163
+ error: err.message,
164
+ exitCode: EXIT_CODES.SCRIPT_ERROR,
165
+ errorContext: { suggestion: err.suggestion },
166
+ };
167
+ }
122
168
  /**
123
169
  * Find similar methods to suggest when a method is not found.
124
170
  * Returns up to 3 closest matches based on edit distance.
@@ -177,6 +223,15 @@ function blockedAlternative(methodName) {
177
223
  * @returns Success result with matching methods
178
224
  */
179
225
  async function handleSearch(query, domain) {
226
+ if (!query.trim()) {
227
+ const err = emptyCdpSearchError();
228
+ return {
229
+ success: false,
230
+ error: err.message,
231
+ exitCode: EXIT_CODES.INVALID_ARGUMENTS,
232
+ errorContext: { suggestion: err.suggestion },
233
+ };
234
+ }
180
235
  if (domain !== undefined && !getDomainSummary(domain)) {
181
236
  return {
182
237
  success: false,
@@ -186,11 +241,11 @@ async function handleSearch(query, domain) {
186
241
  };
187
242
  }
188
243
  const { searchMethods } = await import('../cdp/schema.js');
189
- const results = searchMethods(query).filter((m) => domain === undefined || m.domain.toLowerCase() === domain.toLowerCase());
244
+ const results = searchMethods(query.trim()).filter((m) => domain === undefined || m.domain.toLowerCase() === domain.toLowerCase());
190
245
  return {
191
246
  success: true,
192
247
  data: {
193
- query,
248
+ query: query.trim(),
194
249
  count: results.length,
195
250
  methods: results.map((m) => ({
196
251
  name: m.name,
@@ -338,6 +393,7 @@ function handleDescribeMethod(methodName) {
338
393
  };
339
394
  }
340
395
  const methodNote = METHOD_NOTES[schema.name] ?? DOMAIN_NOTES[schema.domain];
396
+ const alternative = blockedAlternative(schema.name);
341
397
  return {
342
398
  success: true,
343
399
  data: {
@@ -365,9 +421,7 @@ function handleDescribeMethod(methodName) {
365
421
  description: r.description,
366
422
  items: r.items,
367
423
  })),
368
- example: blockedAlternative(schema.name)
369
- ? { command: blockedAlternative(schema.name) }
370
- : schema.example,
424
+ example: alternative ? { command: alternative } : schema.example,
371
425
  },
372
426
  };
373
427
  }
@@ -443,6 +497,9 @@ async function handleExecuteMethod(methodName, paramsJson) {
443
497
  const response = await callCDP(normalized, params);
444
498
  validateIPCResponse(response);
445
499
  const cdpResult = response.data?.result;
500
+ const exception = pageExceptionResult(cdpResult);
501
+ if (exception)
502
+ return exception;
446
503
  const result = {
447
504
  success: true,
448
505
  data: {
@@ -22,6 +22,18 @@ export declare function listsMessages(options: Pick<ConsoleCommandOptions, 'list
22
22
  * @returns Messages of the current page (empty if it logged nothing)
23
23
  */
24
24
  export declare function filterByCurrentNavigation(messages: ConsoleMessage[], currentNavigationId?: number): ConsoleMessage[];
25
+ /**
26
+ * Dropped messages that could have been in the view: all of them with
27
+ * `--history`; for the current page only while the oldest kept message is
28
+ * that page's (else every dropped one came from an earlier page).
29
+ *
30
+ * @param messages - All kept messages, oldest first
31
+ * @param dropped - Oldest messages the session dropped at its limit
32
+ * @param options - `--history`
33
+ * @param currentNavigationId - Navigation id of the current page, if known
34
+ * @returns Dropped count to warn about (0: none of the view's)
35
+ */
36
+ export declare function droppedInView(messages: ConsoleMessage[], dropped: number, options: Pick<ConsoleCommandOptions, 'history'>, currentNavigationId?: number): number;
25
37
  export declare function filterByLevel(messages: ConsoleMessage[], level: ConsoleLevel): ConsoleMessage[];
26
38
  /**
27
39
  * Messages the filters left out between the first and the last listed
@@ -6,11 +6,12 @@ import { runCommand } from './shared/CommandRunner.js';
6
6
  import { jsonOption } from './shared/commonOptions.js';
7
7
  import { noteFollowConnected } from './shared/daemonErrorHandler.js';
8
8
  import { fetchConsoleMessages, createErrorResult } from './shared/dataFetcher.js';
9
- import { followFetchFailure, setupFollowMode, } from './shared/followMode.js';
9
+ import { followFetchFailure, newPageCrashes, setupFollowMode, } from './shared/followMode.js';
10
10
  import { handleValidationError } from './shared/handleValidationError.js';
11
11
  import { consoleLevelOption, positiveIntRule } from './shared/validation.js';
12
12
  import { buildSuccessResponse } from '../ui/OutputBuilder.js';
13
13
  import { buildConsoleJsonOutput, formatConsole, formatConsoleFollowLines, LEVEL_MAP, lastMessages, } from '../ui/formatters/console.js';
14
+ import { pageCrashedNote } from '../ui/messages/commands.js';
14
15
  import { followingConsoleMessage, stoppedFollowingConsoleMessage, } from '../ui/messages/consoleMessages.js';
15
16
  const MIN_LAST = 0;
16
17
  const MAX_LAST = 10000;
@@ -37,9 +38,40 @@ export function listsMessages(options) {
37
38
  export function filterByCurrentNavigation(messages, currentNavigationId) {
38
39
  if (messages.length === 0)
39
40
  return messages;
40
- const navId = currentNavigationId ?? Math.max(...messages.map((m) => m.navigationId ?? 0));
41
+ const navId = shownNavigationId(messages, currentNavigationId);
41
42
  return messages.filter((m) => (m.navigationId ?? 0) === navId);
42
43
  }
44
+ /**
45
+ * Navigation id of the page whose messages are shown without `--history`.
46
+ *
47
+ * @param messages - All captured messages
48
+ * @param currentNavigationId - Navigation id of the current page, if known
49
+ * @returns That id, else the newest navigation id among the messages
50
+ */
51
+ function shownNavigationId(messages, currentNavigationId) {
52
+ return currentNavigationId ?? Math.max(...messages.map((m) => m.navigationId ?? 0));
53
+ }
54
+ /**
55
+ * Dropped messages that could have been in the view: all of them with
56
+ * `--history`; for the current page only while the oldest kept message is
57
+ * that page's (else every dropped one came from an earlier page).
58
+ *
59
+ * @param messages - All kept messages, oldest first
60
+ * @param dropped - Oldest messages the session dropped at its limit
61
+ * @param options - `--history`
62
+ * @param currentNavigationId - Navigation id of the current page, if known
63
+ * @returns Dropped count to warn about (0: none of the view's)
64
+ */
65
+ export function droppedInView(messages, dropped, options, currentNavigationId) {
66
+ if (dropped === 0 || options.history)
67
+ return dropped;
68
+ const oldest = messages[0];
69
+ if (!oldest)
70
+ return dropped;
71
+ return (oldest.navigationId ?? 0) === shownNavigationId(messages, currentNavigationId)
72
+ ? dropped
73
+ : 0;
74
+ }
43
75
  export function filterByLevel(messages, level) {
44
76
  return messages.filter((m) => LEVEL_MAP[m.type] === level);
45
77
  }
@@ -84,10 +116,15 @@ export function skippedMessages(all, listed) {
84
116
  * @param options - Command options
85
117
  * @param lastN - `--last` value
86
118
  * @param skipped - Messages the filters left out between the listed ones
119
+ * @param dropped - Oldest messages the session dropped at its limit
120
+ * @param pageCrashedAt - When the page crashed, while it is not loaded again
87
121
  * @returns Formatting options
88
122
  */
89
- function buildFormatOptions(options, lastN, skipped) {
123
+ function buildFormatOptions(options, lastN, skipped, dropped, pageCrashedAt) {
90
124
  return {
125
+ ...(options.last !== undefined && { groupLimit: lastN }),
126
+ ...(dropped && { dropped }),
127
+ ...(pageCrashedAt !== undefined && { pageCrashedAt }),
91
128
  json: options.json,
92
129
  list: listsMessages(options),
93
130
  follow: options.follow,
@@ -99,13 +136,15 @@ function buildFormatOptions(options, lastN, skipped) {
99
136
  }
100
137
  /**
101
138
  * Stream console messages: the last `lastN` at start, then each new message
102
- * once (like `tail -f`), with a separator when the page navigates.
139
+ * once (like `tail -f`), with a separator when the page navigates and a
140
+ * warning (JSON `pageCrashedAt`) once when the page crashes.
103
141
  *
104
142
  * @param options - Command options
105
143
  * @param lastN - Messages to show at start (0 = all)
106
144
  */
107
145
  async function runFollowMode(options, lastN) {
108
146
  const shown = new Set();
147
+ const newCrash = newPageCrashes();
109
148
  let navigationId;
110
149
  let started = false;
111
150
  const showConsole = async () => {
@@ -115,6 +154,7 @@ async function runFollowMode(options, lastN) {
115
154
  }
116
155
  noteFollowConnected();
117
156
  const { messages, currentNavigationId } = result.data;
157
+ const crashedAt = newCrash(result.data.pageCrashedAt);
118
158
  const matching = applyFilters(messages, options, currentNavigationId);
119
159
  const keys = messageKeys(matching);
120
160
  const fresh = matching.filter((_message, i) => !shown.has(keys[i]));
@@ -124,9 +164,13 @@ async function runFollowMode(options, lastN) {
124
164
  const navigated = started && navigationId !== currentNavigationId;
125
165
  navigationId = currentNavigationId;
126
166
  if (options.json) {
127
- if (!started || backlog.length > 0) {
128
- const data = buildConsoleJsonOutput(backlog, { list: true, last: 0 });
129
- console.log(JSON.stringify(buildSuccessResponse(data), null, 2));
167
+ if (!started || backlog.length > 0 || crashedAt !== undefined) {
168
+ const data = buildConsoleJsonOutput(backlog, {
169
+ list: true,
170
+ last: 0,
171
+ pageCrashedAt: crashedAt,
172
+ });
173
+ console.log(JSON.stringify(buildSuccessResponse(data)));
130
174
  }
131
175
  }
132
176
  else {
@@ -137,6 +181,8 @@ async function runFollowMode(options, lastN) {
137
181
  });
138
182
  if (text)
139
183
  console.log(text);
184
+ if (crashedAt !== undefined)
185
+ console.log(pageCrashedNote(crashedAt));
140
186
  }
141
187
  started = true;
142
188
  return undefined;
@@ -196,19 +242,23 @@ export function registerConsoleCommand(program) {
196
242
  if (!result.success) {
197
243
  return createErrorResult(result.error, result.exitCode, result.suggestion);
198
244
  }
199
- const { messages, currentNavigationId } = result.data;
245
+ const { messages, currentNavigationId, dropped, pageCrashedAt } = result.data;
200
246
  const filtered = applyFilters(messages, options, currentNavigationId);
201
247
  if (options.json) {
202
248
  return {
203
249
  success: true,
204
- data: buildConsoleJsonOutput(filtered, buildFormatOptions(options, lastN)),
250
+ data: buildConsoleJsonOutput(filtered, buildFormatOptions(options, lastN, undefined, dropped, pageCrashedAt)),
205
251
  };
206
252
  }
207
- return { success: true, data: { messages, filtered } };
253
+ const droppedShown = droppedInView(messages, dropped, options, currentNavigationId);
254
+ return {
255
+ success: true,
256
+ data: { messages, filtered, dropped: droppedShown, pageCrashedAt },
257
+ };
208
258
  }, options, (data) => {
209
- const { messages, filtered } = data;
259
+ const { messages, filtered, dropped, pageCrashedAt } = data;
210
260
  const skipped = skippedMessages(messages, lastMessages(filtered, lastN));
211
- return formatConsole(filtered, buildFormatOptions(options, lastN, skipped));
261
+ return formatConsole(filtered, buildFormatOptions(options, lastN, skipped, dropped, pageCrashedAt));
212
262
  });
213
263
  });
214
264
  }
@@ -109,7 +109,9 @@ export declare class DomElementResolver {
109
109
  *
110
110
  * @param index - Zero-based index
111
111
  * @returns Cached node and the query's selector
112
- * @throws CommandError (83) without a session, (81) without a usable cache, (87) for an index outside the cached results
112
+ * @throws CommandError (83) without a session, (81) without a usable cache,
113
+ * (87) for an index outside the cached results or of a page document
114
+ * that has since been replaced (another page's elements reuse its node ids)
113
115
  */
114
116
  private lookup;
115
117
  }
@@ -18,6 +18,7 @@
18
18
  * // { success: true, selector: 'button.submit' }
19
19
  * ```
20
20
  */
21
+ import { pageDocumentId } from './helpers/query.js';
21
22
  import { noActiveSessionError } from '../shared/CommandRunner.js';
22
23
  import { CommandError } from '../../errors/index.js';
23
24
  import { cachedIndexOutOfRangeError, indexWithIndexOptionError, staleNodeError, } from '../../errors/messages.js';
@@ -136,7 +137,9 @@ export class DomElementResolver {
136
137
  *
137
138
  * @param index - Zero-based index
138
139
  * @returns Cached node and the query's selector
139
- * @throws CommandError (83) without a session, (81) without a usable cache, (87) for an index outside the cached results
140
+ * @throws CommandError (83) without a session, (81) without a usable cache,
141
+ * (87) for an index outside the cached results or of a page document
142
+ * that has since been replaced (another page's elements reuse its node ids)
140
143
  */
141
144
  async lookup(index) {
142
145
  const validation = await this.cacheManager.validate();
@@ -145,11 +148,15 @@ export class DomElementResolver {
145
148
  throw noActiveSessionError();
146
149
  throw new CommandError(validation.error ?? 'No cached query results found', validation.suggestion ? { suggestion: validation.suggestion } : {}, EXIT_CODES.INVALID_ARGUMENTS);
147
150
  }
148
- const { nodes, selector } = validation.cache;
151
+ const { nodes, selector, count, document } = validation.cache;
149
152
  const source = indexSourceOf(index, selector);
153
+ if (document !== undefined && (await pageDocumentId()) !== document) {
154
+ const err = staleNodeError(index, source);
155
+ throw new CommandError(err.message, { suggestion: err.suggestion }, EXIT_CODES.STALE_CACHE);
156
+ }
150
157
  const node = nodes.find((n) => n.index === index);
151
158
  if (!node) {
152
- const err = cachedIndexOutOfRangeError(source, nodes.length);
159
+ const err = cachedIndexOutOfRangeError(source, nodes.length, count);
153
160
  throw new CommandError(err.message, { suggestion: err.suggestion }, EXIT_CODES.STALE_CACHE);
154
161
  }
155
162
  return { node, selector, source };
@@ -9,7 +9,7 @@
9
9
  * Uses IPC/callCDP pattern for consistency with other DOM commands.
10
10
  */
11
11
  import { DomElementResolver } from './DomElementResolver.js';
12
- import { getDomContext, resolveBackendNodeIds } from './helpers/index.js';
12
+ import { getDomContext, pageDocumentId, resolveBackendNodeIds, } from './helpers/index.js';
13
13
  import { withSecretMasked } from './semanticUtils.js';
14
14
  import { runCommand, runJsonCommand } from '../shared/CommandRunner.js';
15
15
  import { jsonOption } from '../shared/commonOptions.js';
@@ -85,6 +85,7 @@ async function handleA11yQuery(pattern, options) {
85
85
  const err = invalidQueryPatternError(pattern);
86
86
  throw new CommandError(err.message, { suggestion: err.suggestion }, EXIT_CODES.INVALID_ARGUMENTS);
87
87
  }
88
+ const document = await pageDocumentId();
88
89
  const tree = await collectA11yTree();
89
90
  const result = queryA11yTree(tree, queryPattern);
90
91
  if (result.count === 0) {
@@ -101,7 +102,7 @@ async function handleA11yQuery(pattern, options) {
101
102
  tag: node.role,
102
103
  ...(node.name && { preview: node.name }),
103
104
  })),
104
- });
105
+ }, document);
105
106
  return {
106
107
  success: true,
107
108
  data: limitMatches(indexed, options.limit ?? (options.json ? 0 : A11Y_QUERY_LIMIT)),
@@ -8,8 +8,9 @@
8
8
  import type { DomEvalCommandOptions } from '../shared/optionTypes.js';
9
9
  /**
10
10
  * Handle `bdg dom eval <script> [--frame <frame>]`. With `--frame`, the
11
- * frame the script ran in is a `Frame:` line on stderr (JSON: `frame`), so
12
- * stdout stays the bare value for pipes.
11
+ * frame the script ran in is a `Frame:` line on stderr (JSON: `frame`), and
12
+ * a warning about how the result was copied goes there too (JSON:
13
+ * `warning`), so stdout stays the bare value for pipes.
13
14
  */
14
15
  export declare function handleDomEval(script: string, options: DomEvalCommandOptions): Promise<void>;
15
16
  //# sourceMappingURL=eval.d.ts.map
@@ -10,12 +10,13 @@ import { runCommand } from '../shared/CommandRunner.js';
10
10
  import { emptyScriptError, withLoadingHint } from '../../errors/messages.js';
11
11
  import { domEval } from '../../ipc/client.js';
12
12
  import { formatDomEval } from '../../ui/formatters/dom.js';
13
- import { evalFrameLine } from '../../ui/messages/commands.js';
13
+ import { evalFrameLine, warningMessage } from '../../ui/messages/commands.js';
14
14
  import { EXIT_CODES } from '../../utils/exitCodes.js';
15
15
  /**
16
16
  * Handle `bdg dom eval <script> [--frame <frame>]`. With `--frame`, the
17
- * frame the script ran in is a `Frame:` line on stderr (JSON: `frame`), so
18
- * stdout stays the bare value for pipes.
17
+ * frame the script ran in is a `Frame:` line on stderr (JSON: `frame`), and
18
+ * a warning about how the result was copied goes there too (JSON:
19
+ * `warning`), so stdout stays the bare value for pipes.
19
20
  */
20
21
  export async function handleDomEval(script, options) {
21
22
  await runCommand(async () => {
@@ -38,7 +39,11 @@ export async function handleDomEval(script, options) {
38
39
  ...(suggestion && { errorContext: { suggestion } }),
39
40
  };
40
41
  }
41
- const { value, type, subtype, frame } = response.data;
42
+ const { value, type, subtype, frame, warning } = response.data;
43
+ const hint = [
44
+ ...(frame !== undefined ? [evalFrameLine(frame)] : []),
45
+ ...(warning ? [warningMessage(warning)] : []),
46
+ ].join('\n');
42
47
  return {
43
48
  success: true,
44
49
  data: {
@@ -46,8 +51,9 @@ export async function handleDomEval(script, options) {
46
51
  type,
47
52
  ...(subtype && { subtype }),
48
53
  ...(frame !== undefined && { frame }),
54
+ ...(warning && { warning }),
49
55
  },
50
- ...(frame !== undefined && !options.json && { hint: evalFrameLine(frame) }),
56
+ ...(hint && !options.json && { hint }),
51
57
  };
52
58
  }, options, formatDomEval);
53
59
  }
@@ -5,11 +5,12 @@
5
5
  * validation state, and suggested commands for agent consumption.
6
6
  */
7
7
  import { calculateSummary, orderForms, primaryButtonIndex } from './formSummary.js';
8
- import { resolveBackendNodeIds } from './helpers/index.js';
8
+ import { pageDocumentId, resolveBackendNodeIds } from './helpers/index.js';
9
9
  import { runCommand } from '../shared/CommandRunner.js';
10
10
  import { jsonOption } from '../shared/commonOptions.js';
11
11
  import { noFormsFoundError, formInIframeError } from '../../errors/messages.js';
12
12
  import { domFormDiscover } from '../../ipc/client.js';
13
+ import { MASKED_VALUE } from '../../runtime/dom/elementInfo.js';
13
14
  import { FORM_DISCOVERY_CACHE_SELECTOR, QueryCacheManager } from '../../session/QueryCacheManager.js';
14
15
  import { formatFormDiscovery } from '../../ui/formatters/form.js';
15
16
  import { createLogger } from '../../ui/logging/index.js';
@@ -84,16 +85,14 @@ function buildFieldState(raw) {
84
85
  return 'empty';
85
86
  }
86
87
  /**
87
- * Build masked value for password fields.
88
+ * The masked value of a sensitive field (the page script never sends its
89
+ * real value).
88
90
  *
89
91
  * @param raw - Raw field data
90
92
  * @returns Masked value string
91
93
  */
92
94
  function buildMaskedValue(raw) {
93
- if (raw.inputType === 'password' && typeof raw.value === 'string' && raw.value.length > 0) {
94
- return '•'.repeat(Math.min(raw.value.length, 8));
95
- }
96
- return undefined;
95
+ return raw.value === MASKED_VALUE ? MASKED_VALUE : undefined;
97
96
  }
98
97
  /**
99
98
  * Build interaction warning for non-native fields.
@@ -244,8 +243,9 @@ function transformForm(raw) {
244
243
  * the selector later.
245
244
  *
246
245
  * @param forms - Discovered forms
246
+ * @param document - Identity of the page document they were found in, read before discovery
247
247
  */
248
- async function cacheFormElements(forms) {
248
+ async function cacheFormElements(forms, document) {
249
249
  const elements = forms.flatMap((form) => [...form.fields, ...form.buttons]);
250
250
  const backendNodeIds = await resolveBackendNodeIds(elements.map((el) => el.selector)).catch((error) => {
251
251
  log.debug(`Form fields not cached by node: ${getErrorMessage(error)}`);
@@ -259,7 +259,7 @@ async function cacheFormElements(forms) {
259
259
  nodeId: backendNodeIds[i] ?? 0,
260
260
  selector: el.selector,
261
261
  })),
262
- });
262
+ }, document);
263
263
  log.debug(`Cached ${elements.length} form elements`);
264
264
  }
265
265
  /**
@@ -269,6 +269,7 @@ async function cacheFormElements(forms) {
269
269
  */
270
270
  async function handleFormCommand(options) {
271
271
  await runCommand(async () => {
272
+ const document = await pageDocumentId();
272
273
  const response = await domFormDiscover();
273
274
  if (response.status === 'error' || !response.data) {
274
275
  return {
@@ -311,7 +312,7 @@ async function handleFormCommand(options) {
311
312
  const allForms = orderForms(rawData.forms).map(transformForm);
312
313
  const forms = options.all ? allForms : [allForms[0]];
313
314
  // Cache ALL forms so global indices work with bdg dom fill/click
314
- await cacheFormElements(allForms);
315
+ await cacheFormElements(allForms, document);
315
316
  const result = {
316
317
  formCount: rawData.forms.length,
317
318
  selectedForm: 0,