browser-debugger-cli 0.12.0 → 0.14.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 (224) hide show
  1. package/.claude/skills/bdg/SKILL.md +100 -186
  2. package/README.md +5 -4
  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 +67 -13
  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.d.ts +1 -1
  10. package/dist/commands/dom/a11y.js +23 -22
  11. package/dist/commands/dom/eval.d.ts +4 -2
  12. package/dist/commands/dom/eval.js +31 -7
  13. package/dist/commands/dom/form.js +10 -9
  14. package/dist/commands/dom/formInteraction.js +9 -8
  15. package/dist/commands/dom/get.js +32 -14
  16. package/dist/commands/dom/helpers/index.d.ts +1 -1
  17. package/dist/commands/dom/helpers/index.js +1 -1
  18. package/dist/commands/dom/helpers/query.d.ts +27 -3
  19. package/dist/commands/dom/helpers/query.js +152 -64
  20. package/dist/commands/dom/helpers/screenshot.js +13 -13
  21. package/dist/commands/dom/index.js +10 -3
  22. package/dist/commands/dom/query.d.ts +20 -2
  23. package/dist/commands/dom/query.js +39 -6
  24. package/dist/commands/dom/screenshot.js +3 -1
  25. package/dist/commands/dom/semanticUtils.d.ts +3 -2
  26. package/dist/commands/dom/semanticUtils.js +40 -9
  27. package/dist/commands/helpJson.d.ts +82 -19
  28. package/dist/commands/helpJson.js +112 -41
  29. package/dist/commands/helpTopic.d.ts +16 -1
  30. package/dist/commands/helpTopic.js +59 -1
  31. package/dist/commands/installSkill.d.ts +15 -5
  32. package/dist/commands/installSkill.js +86 -16
  33. package/dist/commands/network/list.js +65 -12
  34. package/dist/commands/optionBehaviors.d.ts +25 -2
  35. package/dist/commands/optionBehaviors.js +81 -46
  36. package/dist/commands/peek.js +3 -0
  37. package/dist/commands/shared/CommandRunner.js +13 -13
  38. package/dist/commands/shared/daemonErrorHandler.d.ts +5 -2
  39. package/dist/commands/shared/daemonErrorHandler.js +21 -10
  40. package/dist/commands/shared/dataFetcher.d.ts +14 -4
  41. package/dist/commands/shared/dataFetcher.js +20 -4
  42. package/dist/commands/shared/followMode.d.ts +9 -1
  43. package/dist/commands/shared/followMode.js +22 -4
  44. package/dist/commands/shared/handleValidationError.js +3 -3
  45. package/dist/commands/shared/optionTypes.d.ts +17 -3
  46. package/dist/commands/shared/outputFile.js +6 -1
  47. package/dist/commands/shared/startHelpers.js +3 -3
  48. package/dist/commands/start.d.ts +7 -5
  49. package/dist/commands/start.js +65 -21
  50. package/dist/commands/stop.d.ts +11 -0
  51. package/dist/commands/stop.js +24 -1
  52. package/dist/commands.js +1 -1
  53. package/dist/connection/cdp.d.ts +7 -0
  54. package/dist/connection/cdp.js +9 -0
  55. package/dist/connection/chromeIdentity.d.ts +8 -2
  56. package/dist/connection/chromeIdentity.js +85 -13
  57. package/dist/connection/launcher.js +3 -2
  58. package/dist/constants.d.ts +29 -1
  59. package/dist/constants.js +35 -1
  60. package/dist/daemon/SessionController.js +8 -1
  61. package/dist/daemon/launcher.d.ts +3 -2
  62. package/dist/daemon/launcher.js +47 -3
  63. package/dist/daemon/session/Session.d.ts +5 -1
  64. package/dist/daemon/session/Session.js +42 -3
  65. package/dist/daemon/session/TelemetryStore.d.ts +15 -1
  66. package/dist/daemon/session/TelemetryStore.js +19 -1
  67. package/dist/daemon/session/commandRegistry.js +52 -18
  68. package/dist/daemon/session/interactions.d.ts +2 -1
  69. package/dist/daemon/session/interactions.js +13 -1
  70. package/dist/daemon/session/matchedStylesReset.d.ts +26 -0
  71. package/dist/daemon/session/matchedStylesReset.js +46 -0
  72. package/dist/daemon/session/plugins.js +17 -2
  73. package/dist/daemon/session/teardown.js +1 -1
  74. package/dist/daemon/session/triggeredRequests.d.ts +0 -5
  75. package/dist/daemon/session/triggeredRequests.js +13 -7
  76. package/dist/daemon.js +2385 -1229
  77. package/dist/errors/messages.d.ts +62 -11
  78. package/dist/errors/messages.js +119 -22
  79. package/dist/index.js +14995 -9866
  80. package/dist/ipc/client.d.ts +18 -2
  81. package/dist/ipc/client.js +26 -5
  82. package/dist/ipc/protocol/auditTypes.d.ts +8 -2
  83. package/dist/ipc/protocol/commands.d.ts +16 -0
  84. package/dist/ipc/protocol/domTypes.d.ts +12 -0
  85. package/dist/ipc/protocol/inspectTypes.d.ts +7 -2
  86. package/dist/ipc/session/types.d.ts +7 -1
  87. package/dist/program.d.ts +14 -0
  88. package/dist/program.js +53 -0
  89. package/dist/runtime/dom/actionEffects.d.ts +5 -1
  90. package/dist/runtime/dom/actionEffects.js +26 -14
  91. package/dist/runtime/dom/audit.js +3 -2
  92. package/dist/runtime/dom/auditModel.js +6 -1
  93. package/dist/runtime/dom/auditScripts.d.ts +9 -3
  94. package/dist/runtime/dom/auditScripts.js +41 -5
  95. package/dist/runtime/dom/elementGeometry.d.ts +33 -3
  96. package/dist/runtime/dom/elementGeometry.js +44 -19
  97. package/dist/runtime/dom/elementInfo.d.ts +76 -18
  98. package/dist/runtime/dom/elementInfo.js +190 -40
  99. package/dist/runtime/dom/evalHelpers.d.ts +12 -2
  100. package/dist/runtime/dom/evalHelpers.js +67 -7
  101. package/dist/runtime/dom/formDiscovery.d.ts +6 -2
  102. package/dist/runtime/dom/formDiscovery.js +20 -3
  103. package/dist/runtime/dom/formFillHelpers/fill.js +7 -11
  104. package/dist/runtime/dom/formFillHelpers/pressKey.js +2 -2
  105. package/dist/runtime/dom/formFillHelpers/shared.d.ts +16 -10
  106. package/dist/runtime/dom/formFillHelpers/shared.js +19 -52
  107. package/dist/runtime/dom/formSubmitHelpers.js +4 -3
  108. package/dist/runtime/dom/frameLayout.js +1 -0
  109. package/dist/runtime/dom/frameScopedConnection.d.ts +7 -0
  110. package/dist/runtime/dom/frameScopedConnection.js +2 -2
  111. package/dist/runtime/dom/inspect.d.ts +17 -3
  112. package/dist/runtime/dom/inspect.js +45 -32
  113. package/dist/runtime/dom/inspectAllStyles.js +1 -0
  114. package/dist/runtime/dom/inspectHints.d.ts +1 -1
  115. package/dist/runtime/dom/inspectModel.d.ts +5 -4
  116. package/dist/runtime/dom/inspectModel.js +7 -3
  117. package/dist/runtime/dom/inspectPaintModel.d.ts +2 -0
  118. package/dist/runtime/dom/inspectPaintModel.js +3 -1
  119. package/dist/runtime/dom/inspectRules.d.ts +29 -3
  120. package/dist/runtime/dom/inspectRules.js +205 -11
  121. package/dist/runtime/dom/inspectScripts.d.ts +29 -2
  122. package/dist/runtime/dom/inspectScripts.js +49 -10
  123. package/dist/runtime/dom/layout.d.ts +0 -2
  124. package/dist/runtime/dom/layout.js +10 -9
  125. package/dist/runtime/dom/reactEventHelpers.d.ts +17 -4
  126. package/dist/runtime/dom/reactEventHelpers.js +71 -28
  127. package/dist/runtime/dom/targetNode.d.ts +27 -10
  128. package/dist/runtime/dom/targetNode.js +283 -16
  129. package/dist/runtime/dom/wait.js +2 -1
  130. package/dist/runtime/page/bdgWorld.d.ts +57 -0
  131. package/dist/runtime/page/bdgWorld.js +180 -0
  132. package/dist/runtime/page/replacedBuiltins.d.ts +28 -0
  133. package/dist/runtime/page/replacedBuiltins.js +136 -0
  134. package/dist/session/QueryCacheManager.d.ts +4 -1
  135. package/dist/session/QueryCacheManager.js +5 -2
  136. package/dist/session/chrome.d.ts +4 -1
  137. package/dist/session/chrome.js +7 -1
  138. package/dist/session/cleanup/staleSession.d.ts +21 -4
  139. package/dist/session/cleanup/staleSession.js +79 -9
  140. package/dist/session/cleanup/userCommands.d.ts +4 -1
  141. package/dist/session/cleanup/userCommands.js +10 -5
  142. package/dist/session/daemonSocket.d.ts +10 -0
  143. package/dist/session/daemonSocket.js +22 -0
  144. package/dist/session/lastSession.d.ts +6 -3
  145. package/dist/session/lastSession.js +11 -5
  146. package/dist/session/paths.d.ts +3 -1
  147. package/dist/session/paths.js +5 -5
  148. package/dist/session/portClaims.js +4 -3
  149. package/dist/session/sessionList.d.ts +13 -5
  150. package/dist/session/sessionList.js +31 -7
  151. package/dist/telemetry/a11y.d.ts +15 -1
  152. package/dist/telemetry/a11y.js +85 -2
  153. package/dist/telemetry/console.d.ts +2 -1
  154. package/dist/telemetry/console.js +30 -21
  155. package/dist/telemetry/har/builder.js +1 -1
  156. package/dist/telemetry/network.d.ts +13 -16
  157. package/dist/telemetry/network.js +30 -52
  158. package/dist/telemetry/networkRetention.d.ts +83 -0
  159. package/dist/telemetry/networkRetention.js +117 -0
  160. package/dist/telemetry/pageCrash.d.ts +26 -0
  161. package/dist/telemetry/pageCrash.js +53 -0
  162. package/dist/types.d.ts +42 -0
  163. package/dist/ui/OutputBuilder.d.ts +10 -0
  164. package/dist/ui/OutputBuilder.js +12 -0
  165. package/dist/ui/formatters/a11y.d.ts +5 -7
  166. package/dist/ui/formatters/a11y.js +7 -61
  167. package/dist/ui/formatters/audit.js +14 -5
  168. package/dist/ui/formatters/cdp.d.ts +138 -0
  169. package/dist/ui/formatters/cdp.js +131 -0
  170. package/dist/ui/formatters/console/chronological.js +7 -5
  171. package/dist/ui/formatters/console/follow.d.ts +5 -2
  172. package/dist/ui/formatters/console/follow.js +7 -4
  173. package/dist/ui/formatters/console/json.d.ts +4 -7
  174. package/dist/ui/formatters/console/json.js +16 -14
  175. package/dist/ui/formatters/console/shared.d.ts +47 -2
  176. package/dist/ui/formatters/console/shared.js +33 -0
  177. package/dist/ui/formatters/console/summarize.d.ts +9 -2
  178. package/dist/ui/formatters/console/summarize.js +57 -11
  179. package/dist/ui/formatters/console.d.ts +3 -2
  180. package/dist/ui/formatters/console.js +8 -10
  181. package/dist/ui/formatters/details.js +4 -2
  182. package/dist/ui/formatters/dom.d.ts +14 -5
  183. package/dist/ui/formatters/dom.js +30 -13
  184. package/dist/ui/formatters/helpFormatters.js +1 -1
  185. package/dist/ui/formatters/inspect.js +9 -3
  186. package/dist/ui/formatters/installSkill.d.ts +9 -1
  187. package/dist/ui/formatters/installSkill.js +32 -6
  188. package/dist/ui/formatters/layout.js +4 -2
  189. package/dist/ui/formatters/longValues.d.ts +14 -0
  190. package/dist/ui/formatters/longValues.js +23 -0
  191. package/dist/ui/formatters/networkList.d.ts +8 -2
  192. package/dist/ui/formatters/networkList.js +11 -3
  193. package/dist/ui/formatters/preview.d.ts +6 -1
  194. package/dist/ui/formatters/preview.js +67 -15
  195. package/dist/ui/formatters/sessions.d.ts +2 -2
  196. package/dist/ui/formatters/sessions.js +9 -2
  197. package/dist/ui/formatters/status.js +7 -0
  198. package/dist/ui/formatters/triggeredRequests.js +2 -1
  199. package/dist/ui/logging/logger.d.ts +1 -1
  200. package/dist/ui/messages/chrome.d.ts +20 -1
  201. package/dist/ui/messages/chrome.js +29 -3
  202. package/dist/ui/messages/commands.d.ts +153 -12
  203. package/dist/ui/messages/commands.js +198 -15
  204. package/dist/ui/messages/consoleMessages.d.ts +24 -0
  205. package/dist/ui/messages/consoleMessages.js +32 -0
  206. package/dist/ui/messages/networkMessages.d.ts +24 -0
  207. package/dist/ui/messages/networkMessages.js +45 -0
  208. package/dist/ui/messages/preview.d.ts +6 -0
  209. package/dist/ui/messages/preview.js +9 -1
  210. package/dist/ui/messages/session.d.ts +13 -2
  211. package/dist/ui/messages/session.js +22 -3
  212. package/dist/utils/directories.d.ts +34 -0
  213. package/dist/utils/directories.js +88 -0
  214. package/dist/utils/display.d.ts +16 -0
  215. package/dist/utils/display.js +42 -0
  216. package/dist/utils/exitCodes.d.ts +1 -0
  217. package/dist/utils/exitCodes.js +6 -0
  218. package/dist/utils/http.d.ts +9 -2
  219. package/dist/utils/http.js +4 -3
  220. package/dist/utils/process.d.ts +12 -0
  221. package/dist/utils/process.js +25 -0
  222. package/dist/utils/strings.d.ts +19 -0
  223. package/dist/utils/strings.js +16 -0
  224. package/package.json +2 -2
@@ -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,13 @@ 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
+ import { MAX_CONSOLE_JSON_TEXT_LENGTH, MAX_CONSOLE_TEXT_LENGTH } from '../constants.js';
12
13
  import { buildSuccessResponse } from '../ui/OutputBuilder.js';
13
14
  import { buildConsoleJsonOutput, formatConsole, formatConsoleFollowLines, LEVEL_MAP, lastMessages, } from '../ui/formatters/console.js';
15
+ import { pageCrashedNote } from '../ui/messages/commands.js';
14
16
  import { followingConsoleMessage, stoppedFollowingConsoleMessage, } from '../ui/messages/consoleMessages.js';
15
17
  const MIN_LAST = 0;
16
18
  const MAX_LAST = 10000;
@@ -37,9 +39,40 @@ export function listsMessages(options) {
37
39
  export function filterByCurrentNavigation(messages, currentNavigationId) {
38
40
  if (messages.length === 0)
39
41
  return messages;
40
- const navId = currentNavigationId ?? Math.max(...messages.map((m) => m.navigationId ?? 0));
42
+ const navId = shownNavigationId(messages, currentNavigationId);
41
43
  return messages.filter((m) => (m.navigationId ?? 0) === navId);
42
44
  }
45
+ /**
46
+ * Navigation id of the page whose messages are shown without `--history`.
47
+ *
48
+ * @param messages - All captured messages
49
+ * @param currentNavigationId - Navigation id of the current page, if known
50
+ * @returns That id, else the newest navigation id among the messages
51
+ */
52
+ function shownNavigationId(messages, currentNavigationId) {
53
+ return currentNavigationId ?? Math.max(...messages.map((m) => m.navigationId ?? 0));
54
+ }
55
+ /**
56
+ * Dropped messages that could have been in the view: all of them with
57
+ * `--history`; for the current page only while the oldest kept message is
58
+ * that page's (else every dropped one came from an earlier page).
59
+ *
60
+ * @param messages - All kept messages, oldest first
61
+ * @param dropped - Oldest messages the session dropped at its limit
62
+ * @param options - `--history`
63
+ * @param currentNavigationId - Navigation id of the current page, if known
64
+ * @returns Dropped count to warn about (0: none of the view's)
65
+ */
66
+ export function droppedInView(messages, dropped, options, currentNavigationId) {
67
+ if (dropped === 0 || options.history)
68
+ return dropped;
69
+ const oldest = messages[0];
70
+ if (!oldest)
71
+ return dropped;
72
+ return (oldest.navigationId ?? 0) === shownNavigationId(messages, currentNavigationId)
73
+ ? dropped
74
+ : 0;
75
+ }
43
76
  export function filterByLevel(messages, level) {
44
77
  return messages.filter((m) => LEVEL_MAP[m.type] === level);
45
78
  }
@@ -84,28 +117,35 @@ export function skippedMessages(all, listed) {
84
117
  * @param options - Command options
85
118
  * @param lastN - `--last` value
86
119
  * @param skipped - Messages the filters left out between the listed ones
120
+ * @param dropped - Oldest messages the session dropped at its limit
121
+ * @param pageCrashedAt - When the page crashed, while it is not loaded again
87
122
  * @returns Formatting options
88
123
  */
89
- function buildFormatOptions(options, lastN, skipped) {
124
+ function buildFormatOptions(options, lastN, skipped, dropped, pageCrashedAt) {
90
125
  return {
91
- json: options.json,
126
+ ...(options.last !== undefined && { groupLimit: lastN }),
127
+ ...(dropped && { dropped }),
128
+ ...(pageCrashedAt !== undefined && { pageCrashedAt }),
92
129
  list: listsMessages(options),
93
130
  follow: options.follow,
94
131
  last: lastN,
95
132
  history: options.history,
96
133
  level: options.level,
134
+ full: options.full,
97
135
  skipped,
98
136
  };
99
137
  }
100
138
  /**
101
139
  * Stream console messages: the last `lastN` at start, then each new message
102
- * once (like `tail -f`), with a separator when the page navigates.
140
+ * once (like `tail -f`), with a separator when the page navigates and a
141
+ * warning (JSON `pageCrashedAt`) once when the page crashes.
103
142
  *
104
143
  * @param options - Command options
105
144
  * @param lastN - Messages to show at start (0 = all)
106
145
  */
107
146
  async function runFollowMode(options, lastN) {
108
147
  const shown = new Set();
148
+ const newCrash = newPageCrashes();
109
149
  let navigationId;
110
150
  let started = false;
111
151
  const showConsole = async () => {
@@ -115,6 +155,7 @@ async function runFollowMode(options, lastN) {
115
155
  }
116
156
  noteFollowConnected();
117
157
  const { messages, currentNavigationId } = result.data;
158
+ const crashedAt = newCrash(result.data.pageCrashedAt);
118
159
  const matching = applyFilters(messages, options, currentNavigationId);
119
160
  const keys = messageKeys(matching);
120
161
  const fresh = matching.filter((_message, i) => !shown.has(keys[i]));
@@ -124,19 +165,27 @@ async function runFollowMode(options, lastN) {
124
165
  const navigated = started && navigationId !== currentNavigationId;
125
166
  navigationId = currentNavigationId;
126
167
  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));
168
+ if (!started || backlog.length > 0 || crashedAt !== undefined) {
169
+ const data = buildConsoleJsonOutput(backlog, {
170
+ list: true,
171
+ last: 0,
172
+ pageCrashedAt: crashedAt,
173
+ full: options.full,
174
+ });
175
+ console.log(JSON.stringify(buildSuccessResponse(data)));
130
176
  }
131
177
  }
132
178
  else {
133
179
  const text = formatConsoleFollowLines(backlog, {
134
180
  header: !started,
181
+ full: options.full,
135
182
  ...(navigated &&
136
183
  currentNavigationId !== undefined && { navigationId: currentNavigationId }),
137
184
  });
138
185
  if (text)
139
186
  console.log(text);
187
+ if (crashedAt !== undefined)
188
+ console.log(pageCrashedNote(crashedAt));
140
189
  }
141
190
  started = true;
142
191
  return undefined;
@@ -173,6 +222,7 @@ export function registerConsoleCommand(program) {
173
222
  .addOption(new Option('-H, --history', 'Show messages from all page loads (default: current only)').default(false))
174
223
  .addOption(new Option('--level <level>', 'Filter by message level: error, warning, info (includes log), debug; any case').argParser(consoleLevelOption))
175
224
  .addOption(consoleLastOption)
225
+ .addOption(new Option('--full', `Print message texts whole (default: the first ${MAX_CONSOLE_TEXT_LENGTH} characters, ${MAX_CONSOLE_JSON_TEXT_LENGTH} in JSON)`).default(false))
176
226
  .addOption(jsonOption())
177
227
  .action(async (options) => {
178
228
  let lastN;
@@ -196,19 +246,23 @@ export function registerConsoleCommand(program) {
196
246
  if (!result.success) {
197
247
  return createErrorResult(result.error, result.exitCode, result.suggestion);
198
248
  }
199
- const { messages, currentNavigationId } = result.data;
249
+ const { messages, currentNavigationId, dropped, pageCrashedAt } = result.data;
200
250
  const filtered = applyFilters(messages, options, currentNavigationId);
201
251
  if (options.json) {
202
252
  return {
203
253
  success: true,
204
- data: buildConsoleJsonOutput(filtered, buildFormatOptions(options, lastN)),
254
+ data: buildConsoleJsonOutput(filtered, buildFormatOptions(options, lastN, undefined, dropped, pageCrashedAt)),
205
255
  };
206
256
  }
207
- return { success: true, data: { messages, filtered } };
257
+ const droppedShown = droppedInView(messages, dropped, options, currentNavigationId);
258
+ return {
259
+ success: true,
260
+ data: { messages, filtered, dropped: droppedShown, pageCrashedAt },
261
+ };
208
262
  }, options, (data) => {
209
- const { messages, filtered } = data;
263
+ const { messages, filtered, dropped, pageCrashedAt } = data;
210
264
  const skipped = skippedMessages(messages, lastMessages(filtered, lastN));
211
- return formatConsole(filtered, buildFormatOptions(options, lastN, skipped));
265
+ return formatConsole(filtered, buildFormatOptions(options, lastN, skipped, dropped, pageCrashedAt));
212
266
  });
213
267
  });
214
268
  }
@@ -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 };
@@ -2,7 +2,7 @@
2
2
  * Accessibility tree inspection commands for semantic element queries.
3
3
  *
4
4
  * Provides three core commands:
5
- * - tree: Dump full accessibility tree
5
+ * - tree: List the accessibility tree (bounded by --limit / --depth)
6
6
  * - query: Search by role/name/description patterns
7
7
  * - describe: Get A11y properties for CSS selector
8
8
  *
@@ -2,50 +2,48 @@
2
2
  * Accessibility tree inspection commands for semantic element queries.
3
3
  *
4
4
  * Provides three core commands:
5
- * - tree: Dump full accessibility tree
5
+ * - tree: List the accessibility tree (bounded by --limit / --depth)
6
6
  * - query: Search by role/name/description patterns
7
7
  * - describe: Get A11y properties for CSS selector
8
8
  *
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';
16
16
  import { integerOption } from '../shared/validation.js';
17
+ import { QUERY_JSON_LIST_LIMIT } from '../../constants.js';
17
18
  import { CommandError } from '../../errors/index.js';
18
19
  import { elementNotFoundError, invalidQueryPatternError, noA11yNodesFoundError, notInAccessibilityTreeError, } from '../../errors/messages.js';
19
20
  import { A11Y_CACHE_SELECTOR_PREFIX, QueryCacheManager } from '../../session/QueryCacheManager.js';
20
- import { a11yIgnoredReasons, collectA11yTree, queryA11yTree, parseQueryPattern, resolveA11yNode, } from '../../telemetry/a11y.js';
21
+ import { a11yIgnoredReasons, collectA11yTree, listA11yTree, queryA11yTree, parseQueryPattern, resolveA11yNode, } from '../../telemetry/a11y.js';
21
22
  import { formatA11yTree, formatA11yQueryResult, formatA11yNodeWithContext, } from '../../ui/formatters/a11y.js';
22
23
  import { EXIT_CODES } from '../../utils/exitCodes.js';
23
- /** Matches `dom a11y query` lists by default in human output (a page can have hundreds of links; JSON lists all) */
24
+ /** Matches `dom a11y query` lists by default in human output (a page can have hundreds of links) */
24
25
  const A11Y_QUERY_LIMIT = 50;
26
+ /** Nodes `dom a11y tree` lists by default, in human and JSON output */
27
+ const A11Y_TREE_LIMIT = 50;
25
28
  /**
26
29
  * Handle bdg dom a11y tree command
27
30
  *
28
- * Dumps the full accessibility tree for the current page via IPC.
29
- * Filters out ignored nodes for cleaner output.
31
+ * Lists the accessibility tree depth-first, the first `--limit` meaningful
32
+ * nodes (default {@link A11Y_TREE_LIMIT}, 0 = all) down to `--depth`, in
33
+ * human and JSON output; `count` is the whole tree, `omitted` the nodes cut
34
+ * and `skipped` the noise never listed.
30
35
  *
31
- * JSON output returns nodes as an array for natural jq filtering:
32
- * bdg dom a11y tree --json | jq '.data.nodes[] | select(.role == "checkbox")'
33
- * bdg dom a11y tree --json | jq '.data.nodes[0]'
36
+ * JSON output returns nodes as an array (each with its `depth`) for jq:
37
+ * bdg dom a11y tree --json --limit 0 | jq '.data.nodes[] | select(.role == "checkbox")'
34
38
  *
35
39
  * @param options - Command options
36
40
  */
37
41
  async function handleA11yTree(options) {
42
+ const listTree = async () => listA11yTree(await collectA11yTree(), options.limit ?? A11Y_TREE_LIMIT, options.depth);
38
43
  if (options.json) {
39
- await runJsonCommand(async () => {
40
- const tree = await collectA11yTree();
41
- return {
42
- root: tree.root,
43
- nodes: Array.from(tree.nodes.values()),
44
- count: tree.count,
45
- };
46
- });
44
+ await runJsonCommand(listTree);
47
45
  }
48
- await runCommand(async () => ({ success: true, data: await collectA11yTree() }), options, formatA11yTree);
46
+ await runCommand(async () => ({ success: true, data: await listTree() }), options, formatA11yTree);
49
47
  }
50
48
  /**
51
49
  * Quote a search text for a query pattern, keeping the quotes it contains
@@ -85,6 +83,7 @@ async function handleA11yQuery(pattern, options) {
85
83
  const err = invalidQueryPatternError(pattern);
86
84
  throw new CommandError(err.message, { suggestion: err.suggestion }, EXIT_CODES.INVALID_ARGUMENTS);
87
85
  }
86
+ const document = await pageDocumentId();
88
87
  const tree = await collectA11yTree();
89
88
  const result = queryA11yTree(tree, queryPattern);
90
89
  if (result.count === 0) {
@@ -101,10 +100,10 @@ async function handleA11yQuery(pattern, options) {
101
100
  tag: node.role,
102
101
  ...(node.name && { preview: node.name }),
103
102
  })),
104
- });
103
+ }, document);
105
104
  return {
106
105
  success: true,
107
- data: limitMatches(indexed, options.limit ?? (options.json ? 0 : A11Y_QUERY_LIMIT)),
106
+ data: limitMatches(indexed, options.limit ?? (options.json ? QUERY_JSON_LIST_LIMIT : A11Y_QUERY_LIMIT)),
108
107
  };
109
108
  }, options, formatA11yQueryResult);
110
109
  }
@@ -206,7 +205,9 @@ export function registerA11yCommands(domCmd) {
206
205
  });
207
206
  a11y
208
207
  .command('tree')
209
- .description('Dump full accessibility tree (filters ignored nodes)')
208
+ .description('List the accessibility tree depth-first (ignored nodes, text boxes, repeated text and nameless wrappers left out)')
209
+ .option('--limit <n>', `Nodes to list (default: ${A11Y_TREE_LIMIT}, also with --json; 0 = all listed nodes (text boxes and empty wrappers are always skipped))`, integerOption(0))
210
+ .option('--depth <n>', 'Levels below the root to list (0 = root only; default: all)', integerOption(0))
210
211
  .addOption(jsonOption())
211
212
  .action(async (options) => {
212
213
  await handleA11yTree(options);
@@ -215,7 +216,7 @@ export function registerA11yCommands(domCmd) {
215
216
  .command('query')
216
217
  .description('Query elements by accessibility properties (e.g., "role:button", "name:Submit")')
217
218
  .argument('<pattern>', "Fields role, name, description as key:value or key=value, separated by spaces or commas; * is a wildcard. A name or description runs to the next field or the end, so it may contain spaces and colons; quote the whole pattern (e.g. 'role=button name=Sign in', 'name=E-mail address:')")
218
- .option('--limit <n>', `Matches to list (default: ${A11Y_QUERY_LIMIT}, all with --json; 0 = all); all are indexed`, integerOption(0))
219
+ .option('--limit <n>', `Matches to list (default: ${A11Y_QUERY_LIMIT}, ${QUERY_JSON_LIST_LIMIT} with --json; 0 = all); all are indexed`, integerOption(0))
219
220
  .addOption(jsonOption())
220
221
  .action(async (pattern, options) => {
221
222
  await handleA11yQuery(pattern, options);
@@ -8,8 +8,10 @@
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. Long values are cut
14
+ * (a string result in JSON with `truncatedFrom`) unless `--full`.
13
15
  */
14
16
  export declare function handleDomEval(script: string, options: DomEvalCommandOptions): Promise<void>;
15
17
  //# sourceMappingURL=eval.d.ts.map
@@ -7,15 +7,19 @@
7
7
  */
8
8
  import { documentReadyState } from './helpers/query.js';
9
9
  import { runCommand } from '../shared/CommandRunner.js';
10
+ import { MAX_VALUE_LENGTH } from '../../constants.js';
10
11
  import { emptyScriptError, withLoadingHint } from '../../errors/messages.js';
11
12
  import { domEval } from '../../ipc/client.js';
12
13
  import { formatDomEval } from '../../ui/formatters/dom.js';
13
- import { evalFrameLine } from '../../ui/messages/commands.js';
14
+ import { evalFrameLine, warningMessage } from '../../ui/messages/commands.js';
14
15
  import { EXIT_CODES } from '../../utils/exitCodes.js';
16
+ import { capLength } from '../../utils/strings.js';
15
17
  /**
16
18
  * 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.
19
+ * frame the script ran in is a `Frame:` line on stderr (JSON: `frame`), and
20
+ * a warning about how the result was copied goes there too (JSON:
21
+ * `warning`), so stdout stays the bare value for pipes. Long values are cut
22
+ * (a string result in JSON with `truncatedFrom`) unless `--full`.
19
23
  */
20
24
  export async function handleDomEval(script, options) {
21
25
  await runCommand(async () => {
@@ -38,18 +42,38 @@ export async function handleDomEval(script, options) {
38
42
  ...(suggestion && { errorContext: { suggestion } }),
39
43
  };
40
44
  }
41
- const { value, type, subtype, frame } = response.data;
45
+ const { value, type, subtype, frame, warning } = response.data;
46
+ const hint = [
47
+ ...(frame !== undefined ? [evalFrameLine(frame)] : []),
48
+ ...(warning ? [warningMessage(warning)] : []),
49
+ ].join('\n');
42
50
  return {
43
51
  success: true,
44
52
  data: {
45
- result: value,
53
+ ...jsonResult(value, options),
46
54
  type,
47
55
  ...(subtype && { subtype }),
48
56
  ...(frame !== undefined && { frame }),
57
+ ...(warning && { warning }),
49
58
  },
50
- ...(frame !== undefined && !options.json && { hint: evalFrameLine(frame) }),
59
+ ...(hint && !options.json && { hint }),
51
60
  };
52
- }, options, formatDomEval);
61
+ }, options, (data) => formatDomEval(data, { full: options.full }));
62
+ }
63
+ /**
64
+ * The result field of the output: a string result in JSON cut to
65
+ * {@link MAX_VALUE_LENGTH} characters with `truncatedFrom`, unless `--full`
66
+ * (human output is cut when formatted).
67
+ *
68
+ * @param value - Evaluated value
69
+ * @param options - `--json`, `--full`
70
+ * @returns `result`, and `truncatedFrom` when cut
71
+ */
72
+ function jsonResult(value, options) {
73
+ if (!options.json || options.full || typeof value !== 'string')
74
+ return { result: value };
75
+ const { text, truncatedFrom } = capLength(value, MAX_VALUE_LENGTH);
76
+ return { result: text, ...(truncatedFrom !== undefined && { truncatedFrom }) };
53
77
  }
54
78
  /**
55
79
  * Suggestion of a failed eval; a frame not found (83) while the page is