opera-devtools-mcp 0.6.1 → 0.8.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 (96) hide show
  1. package/README.md +1 -1
  2. package/build/src/McpContext.js +78 -41
  3. package/build/src/McpPage.js +5 -1
  4. package/build/src/ToolHandler.js +54 -76
  5. package/build/src/bin/chrome-devtools-mcp-main.js +4 -4
  6. package/build/src/bin/chrome-devtools.js +56 -124
  7. package/build/src/bin/opera-browser-cli.js +102 -0
  8. package/build/src/bin/opera-devtools-cli-options.js +1 -1
  9. package/build/src/bin/opera-devtools-mcp-cli-options.js +1 -1
  10. package/build/src/bin/opera-devtools-mcp.js +20 -1
  11. package/build/src/browser.js +23 -25
  12. package/build/src/config/browser-options.js +126 -0
  13. package/build/src/config/category-options.js +81 -0
  14. package/build/src/{bin/chrome-devtools-cli-options.js → config/cli-options.js} +368 -26
  15. package/build/src/{bin/chrome-devtools-mcp-cli-options.js → config/mcp-options.js} +143 -164
  16. package/build/src/daemon/client.js +55 -40
  17. package/build/src/daemon/daemon.js +62 -39
  18. package/build/src/daemon/utils.js +6 -0
  19. package/build/src/devtools/DevtoolsUtils.js +27 -21
  20. package/build/src/formatters/NetworkFormatter.js +5 -2
  21. package/build/src/index.js +166 -100
  22. package/build/src/opera/branding.js +4 -2
  23. package/build/src/opera/browserActivity.js +62 -0
  24. package/build/src/opera/browserCleanup.js +123 -0
  25. package/build/src/opera/browserErrors.js +66 -0
  26. package/build/src/opera/browserFlags.js +184 -38
  27. package/build/src/opera/browserTarget.js +513 -0
  28. package/build/src/opera/cdpErrors.js +391 -0
  29. package/build/src/opera/cliCommands.js +378 -0
  30. package/build/src/opera/cliOutput.js +284 -0
  31. package/build/src/opera/compactSnapshot.js +525 -0
  32. package/build/src/opera/config.js +166 -0
  33. package/build/src/opera/daemonLifecycle.js +257 -0
  34. package/build/src/opera/daemonLog.js +103 -0
  35. package/build/src/opera/daemonPidFile.js +83 -0
  36. package/build/src/opera/daemonShutdown.js +66 -0
  37. package/build/src/opera/daemonSocket.js +87 -0
  38. package/build/src/opera/daemonStreaming.js +130 -0
  39. package/build/src/opera/daemonToolCall.js +26 -0
  40. package/build/src/opera/detect.js +114 -0
  41. package/build/src/opera/doctor.js +317 -0
  42. package/build/src/opera/envConfig.js +229 -0
  43. package/build/src/opera/launcherNotice.js +116 -0
  44. package/build/src/opera/legacyBridgeCleanup.js +297 -0
  45. package/build/src/opera/logs.js +133 -0
  46. package/build/src/opera/mcpServerSupervisor.js +128 -0
  47. package/build/src/opera/migrationShared.js +164 -0
  48. package/build/src/opera/operaPages.js +56 -0
  49. package/build/src/opera/pageIdRouting.js +35 -0
  50. package/build/src/opera/pageRecovery.js +53 -0
  51. package/build/src/opera/profile.js +270 -0
  52. package/build/src/opera/refArgs.js +36 -0
  53. package/build/src/opera/serviceWorkerRetry.js +46 -4
  54. package/build/src/opera/setup.js +290 -0
  55. package/build/src/opera/skills/SKILL.md +160 -0
  56. package/build/src/opera/streamingTools.js +73 -0
  57. package/build/src/opera/suggestions.js +67 -0
  58. package/build/src/opera/toolHandlerHooks.js +25 -1
  59. package/build/src/opera/tools/opera.js +107 -38
  60. package/build/src/opera/urlResolver.js +69 -0
  61. package/build/src/opera/webStorageWarning.js +92 -0
  62. package/build/src/processors/HeapSnapshotManager.js +12 -0
  63. package/build/src/telemetry/ClearcutLogger.js +19 -6
  64. package/build/src/telemetry/transformation.js +4 -0
  65. package/build/src/telemetry/types.js +4 -0
  66. package/build/src/third_party/THIRD_PARTY_NOTICES +5 -5
  67. package/build/src/third_party/bundled-packages.json +3 -3
  68. package/build/src/third_party/devtools-formatter-worker.js +23 -0
  69. package/build/src/third_party/devtools-heap-snapshot-worker.js +101 -20
  70. package/build/src/third_party/index.js +15460 -14256
  71. package/build/src/third_party/issue-descriptions/federatedAuthRequestAccountsBlockedByConnectionAllowlist.md +1 -0
  72. package/build/src/third_party/issue-descriptions/federatedAuthRequestConfigBlockedByConnectionAllowlist.md +1 -0
  73. package/build/src/third_party/issue-descriptions/federatedAuthRequestIdTokenBlockedByConnectionAllowlist.md +1 -0
  74. package/build/src/third_party/issue-descriptions/federatedAuthRequestWellKnownBlockedByConnectionAllowlist.md +1 -0
  75. package/build/src/tools/ToolDefinition.js +15 -0
  76. package/build/src/tools/categories.js +0 -6
  77. package/build/src/tools/comments.js +90 -0
  78. package/build/src/tools/console.js +1 -1
  79. package/build/src/tools/emulation.js +1 -1
  80. package/build/src/tools/extensions.js +1 -1
  81. package/build/src/tools/memory.js +60 -6
  82. package/build/src/tools/network.js +2 -2
  83. package/build/src/tools/pages.js +26 -15
  84. package/build/src/tools/performance.js +4 -3
  85. package/build/src/tools/screencast.js +3 -2
  86. package/build/src/tools/screenshot.js +39 -8
  87. package/build/src/tools/script.js +17 -4
  88. package/build/src/tools/slim/tools.js +41 -33
  89. package/build/src/tools/snapshot.js +1 -1
  90. package/build/src/tools/tools.js +2 -0
  91. package/build/src/utils/WaitForHelper.js +12 -1
  92. package/build/src/utils/bytes.js +105 -0
  93. package/build/src/utils/url.js +79 -0
  94. package/build/src/version.js +1 -1
  95. package/package.json +29 -8
  96. package/build/src/bin/opera-devtools.js +0 -10
@@ -0,0 +1,391 @@
1
+ /**
2
+ * @license
3
+ * Copyright 2026 Opera Norway AS. All rights reserved.
4
+ *
5
+ * This file is an original work developed by Opera.
6
+ */
7
+ /**
8
+ * The exit-code contract, and the browser error strings that map onto it.
9
+ *
10
+ * Ported from opera-browser-cli's `src/cli.ts` (`EXIT_CODES`,
11
+ * `CDP_RESULT_ERRORS`, `callAiTool`'s wrapping) and `src/client.ts`
12
+ * (`ErrorCode`, `CdpError`). The source extended `axi-sdk-js`'s `AxiError`;
13
+ * the fork has no such dependency, so `CdpError` is a plain `Error` subclass
14
+ * carrying the same two extra fields.
15
+ *
16
+ * The codes are a contract, documented in `SKILL.md`: a caller branches on
17
+ * *why* something failed without parsing the message.
18
+ *
19
+ * 2 fix the command 3 environment not ready 4 ask the user
20
+ * 5 retry later 6 page state moved; re-snapshot
21
+ */
22
+ import { CLI_BIN_NAME } from './branding.js';
23
+ export class CdpError extends Error {
24
+ code;
25
+ suggestions;
26
+ constructor(message, code, suggestions = []) {
27
+ super(message);
28
+ this.code = code;
29
+ this.suggestions = suggestions;
30
+ this.name = 'CdpError';
31
+ }
32
+ }
33
+ export const EXIT_CODES = {
34
+ VALIDATION_ERROR: 2,
35
+ UNSUPPORTED_OPERATION: 2,
36
+ BRIDGE_NOT_READY: 3,
37
+ BROWSER_ERROR: 3,
38
+ AUTH_REQUIRED: 4,
39
+ TIMEOUT: 5,
40
+ REF_NOT_FOUND: 6,
41
+ PAGE_CLOSED: 6,
42
+ EXTENSION_NOT_FOUND: 3,
43
+ NOT_FOUND: 2,
44
+ CONVERSATION_NOT_FOUND: 2,
45
+ SERVER_DISCONNECTED: 3,
46
+ UNKNOWN: 1,
47
+ };
48
+ /** A clean run exits 0; every failure is one of `EXIT_CODES`. */
49
+ export const SUCCESS_EXIT_CODE = 0;
50
+ export function exitCodeFor(error) {
51
+ if (error instanceof CdpError) {
52
+ return EXIT_CODES[error.code] ?? 1;
53
+ }
54
+ return 1;
55
+ }
56
+ // ---------------------------------------------------------------------------
57
+ // Opera AI result errors
58
+ // ---------------------------------------------------------------------------
59
+ /**
60
+ * Error keys that are part of the CDP contract between the CLI and the Opera
61
+ * browser extension. Any change here MUST be mirrored in opera-chat:
62
+ * `src/sagas/chat/handleCdpActionRequested.ts` → `CdpResultErrorKey`.
63
+ */
64
+ export const CDP_RESULT_ERROR_KEYS = {
65
+ NOT_SIGNED_IN: '[OPERA_CDP_ERR:NOT_SIGNED_IN]',
66
+ SUBSCRIPTION_REQUIRED: '[OPERA_CDP_ERR:SUBSCRIPTION_REQUIRED]',
67
+ CONSENT_REQUIRED: '[OPERA_CDP_ERR:CONSENT_REQUIRED]',
68
+ NEON_ONLY: '[OPERA_CDP_ERR:NEON_ONLY]',
69
+ CONVERSATION_NOT_FOUND: '[OPERA_CDP_ERR:CONVERSATION_NOT_FOUND]',
70
+ };
71
+ const NEON_ONLY_HELP = [
72
+ 'Install Opera Neon from https://www.operaneon.com',
73
+ `Run \`${CLI_BIN_NAME} setup\` to point at it`,
74
+ `Run \`${CLI_BIN_NAME} doctor\` to inspect the current configuration`,
75
+ ];
76
+ /**
77
+ * The wording that means the dispatch never reached Opera's AI at all: the
78
+ * connected browser has no Opera AI extension, or — for a browser that has one
79
+ * — its service worker was still coming up through every retry.
80
+ *
81
+ * Deliberately narrower than a bare `no target`: this text is scanned on
82
+ * *successful* calls too, where AI prose could contain the two words.
83
+ */
84
+ const DISPATCH_NOT_REACHED = /dispatcher was not able to dispatch|no target to dispatch/i;
85
+ /**
86
+ * What the browser reports when the Opera AI extension is not there to take the
87
+ * dispatch — seen verbatim from a Neon-only command on an Opera build without
88
+ * it: `Protocol error (Opera.dispatchWithStreamedResponse): Opera extension not
89
+ * available for this profile`. Not the same thing as `DISPATCH_NOT_REACHED`,
90
+ * which is about a dispatch that had somewhere to go and did not get there.
91
+ */
92
+ const AI_EXTENSION_UNAVAILABLE = /Opera extension not available/i;
93
+ /**
94
+ * `Opera: …` — the AI extension the commands dispatch into is missing from the
95
+ * browser profile that is running. `chat` runs on any Opera build, so only a
96
+ * command Neon owns can call it a Neon problem; for the rest the profile is what
97
+ * to look at.
98
+ */
99
+ export function aiExtensionUnavailableFailure(command) {
100
+ const neonOnly = command !== 'opera_chat';
101
+ return new CdpError('Opera: the Opera AI extension is not available for this profile', 'EXTENSION_NOT_FOUND', [
102
+ neonOnly
103
+ ? `\`${command}\` requires Opera Neon — install it from https://www.operaneon.com`
104
+ : 'Run it on an Opera profile that has the AI extension loaded',
105
+ `Run \`${CLI_BIN_NAME} setup\` to point at another browser or profile`,
106
+ `Run \`${CLI_BIN_NAME} doctor\` to inspect the current configuration`,
107
+ ]);
108
+ }
109
+ /**
110
+ * Opera reports "no target to dispatch to" when the connected browser has no
111
+ * Opera AI extension — i.e. it is not Opera at all, or not Opera Neon for the
112
+ * tools that require it. The raw CDP text says nothing about that, so it is
113
+ * rewritten into something the user can act on.
114
+ *
115
+ * One diagnosis for both shapes the wording arrives in: thrown out of the tool
116
+ * (`wrapAiToolError`) and appended to a successful result (`CDP_RESULT_ERRORS`).
117
+ */
118
+ export function unsupportedAiBrowserFailure(command) {
119
+ const neonOnly = command !== 'opera_chat';
120
+ return new CdpError(neonOnly
121
+ ? `${command} requires Opera Neon — the connected browser does not support Opera AI`
122
+ : `${command} requires an Opera browser — the connected browser does not support Opera AI`, 'BROWSER_ERROR', [
123
+ neonOnly
124
+ ? 'Install Opera Neon from https://www.operaneon.com'
125
+ : 'Install Opera from https://www.opera.com or Opera Neon from https://www.operaneon.com',
126
+ `Run \`${CLI_BIN_NAME} setup\` to configure the${neonOnly ? ' Opera Neon' : ''} executable path`,
127
+ `Run \`${CLI_BIN_NAME} doctor\` to inspect the current configuration`,
128
+ ]);
129
+ }
130
+ /**
131
+ * The remedies that belong to a *code* rather than to a matched message, for a
132
+ * failure that arrived without one. Only codes whose exit-code contract names a
133
+ * next step have an entry: 6 means the page state moved, so a fresh snapshot is
134
+ * the fix.
135
+ */
136
+ const FRESH_SNAPSHOT = [
137
+ `Run \`${CLI_BIN_NAME} take_snapshot\` to see the current page state and its refs`,
138
+ ];
139
+ const CODE_SUGGESTIONS = {
140
+ REF_NOT_FOUND: FRESH_SNAPSHOT,
141
+ PAGE_CLOSED: FRESH_SNAPSHOT,
142
+ };
143
+ /**
144
+ * Error conditions that Opera returns as plain text content on a successful
145
+ * tool call (no MCP `isError` flag). Each descriptor is checked in order; the
146
+ * first match is converted to a `CdpError`.
147
+ */
148
+ const CDP_RESULT_ERRORS = [
149
+ {
150
+ match: r => r.includes(CDP_RESULT_ERROR_KEYS.NOT_SIGNED_IN),
151
+ message: 'Opera: user is not signed in',
152
+ code: 'AUTH_REQUIRED',
153
+ suggestions: cmd => [
154
+ `Run \`${CLI_BIN_NAME} login\` to sign in to your Opera account`,
155
+ `Re-run \`${CLI_BIN_NAME} ${cmd}\` afterwards`,
156
+ ],
157
+ },
158
+ {
159
+ match: r => r.includes(CDP_RESULT_ERROR_KEYS.SUBSCRIPTION_REQUIRED),
160
+ message: 'Opera: an active subscription is required',
161
+ code: 'AUTH_REQUIRED',
162
+ suggestions: cmd => [
163
+ 'Check your Opera subscription at https://auth.opera.com/account/',
164
+ `Re-run \`${CLI_BIN_NAME} ${cmd}\` after activating a subscription`,
165
+ ],
166
+ },
167
+ {
168
+ match: r => r.includes(CDP_RESULT_ERROR_KEYS.CONSENT_REQUIRED),
169
+ message: 'Opera: user consent has not been accepted',
170
+ code: 'AUTH_REQUIRED',
171
+ suggestions: cmd => [
172
+ `Run \`${CLI_BIN_NAME} login\` — the consent prompt appears on first use`,
173
+ `Re-run \`${CLI_BIN_NAME} ${cmd}\` after accepting consent`,
174
+ ],
175
+ },
176
+ {
177
+ match: r => r.includes(CDP_RESULT_ERROR_KEYS.NEON_ONLY),
178
+ message: cmd => `Opera: ${cmd} is only available on Opera Neon`,
179
+ code: 'UNSUPPORTED_OPERATION',
180
+ suggestions: () => NEON_ONLY_HELP,
181
+ },
182
+ {
183
+ match: r => r.includes(CDP_RESULT_ERROR_KEYS.CONVERSATION_NOT_FOUND),
184
+ message: 'Opera: the specified conversation was not found or has expired',
185
+ code: 'CONVERSATION_NOT_FOUND',
186
+ suggestions: cmd => [
187
+ `Run \`${CLI_BIN_NAME} ${cmd}\` without --conversation-id to start a new conversation`,
188
+ `Use \`${CLI_BIN_NAME} opera_list_models\` to see available models`,
189
+ ],
190
+ },
191
+ {
192
+ // Opera's AI keeps its own records in the browser profile, and reports a
193
+ // store it cannot read as an aborted, not-readable record. The action never
194
+ // starts: `chat` comes back with this text, and a streamed action (`do`,
195
+ // `research`) opens its tab and then reports nothing at all, because the
196
+ // failure happens before its first event.
197
+ match: r => /NotReadableError/i.test(r) &&
198
+ /Data lost due to missing file|irrecoverable/i.test(r),
199
+ message: "Opera AI's own storage rejected the action — the browser reported a missing file",
200
+ code: 'BROWSER_ERROR',
201
+ suggestions: (_cmd) => [
202
+ `Try it on a throwaway profile, which tells browser state from browser behaviour: \`${CLI_BIN_NAME} stop\` then \`${CLI_BIN_NAME} start --isolated --no-headless\``,
203
+ `If it repeats there too the browser side is at fault: the browser's own error is in \`${CLI_BIN_NAME} logs\`, and the profile it launched on is the one named by --userDataDir or OPERA_CLI_USER_DATA_DIR (otherwise ~/.cache/chrome-devtools-mcp-cli/chrome-profile)`,
204
+ `Check whether the other Opera AI commands on the same path fail too: \`${CLI_BIN_NAME} opera_list_models\``,
205
+ ],
206
+ },
207
+ {
208
+ // Raised by the tool itself (`opera/tools/opera.ts`) when a streamed action
209
+ // emitted nothing at all — the shape a stalled action takes, and the one
210
+ // the browser reports no error for.
211
+ match: r => /no progress was reported for/i.test(r),
212
+ message: 'Opera never started the action in the browser',
213
+ code: 'BROWSER_ERROR',
214
+ suggestions: cmd => [
215
+ `Run \`${CLI_BIN_NAME} logs --errors\` to see what the browser side reported for \`${cmd}\``,
216
+ `If it names unreadable AI storage, delete the profile this daemon launched (--userDataDir, or OPERA_CLI_USER_DATA_DIR; otherwise ~/.cache/chrome-devtools-mcp-cli/chrome-profile) and run \`${CLI_BIN_NAME} ${cmd}\` again`,
217
+ ],
218
+ },
219
+ {
220
+ // The dispatch had nowhere to go because the AI extension is not in this
221
+ // profile at all. Like the entry below, the tool appends it to a successful
222
+ // result rather than throwing, so it has to be recognised from the text.
223
+ match: r => AI_EXTENSION_UNAVAILABLE.test(r),
224
+ message: cmd => aiExtensionUnavailableFailure(cmd).message,
225
+ code: 'EXTENSION_NOT_FOUND',
226
+ suggestions: cmd => aiExtensionUnavailableFailure(cmd).suggestions,
227
+ },
228
+ {
229
+ // The dispatch never reached Opera's AI. `serviceWorkerRetry` replays this
230
+ // wording *inside* the tool while the service worker is still coming up, so
231
+ // a copy that arrives here is one the retries already exhausted — the tool
232
+ // appends it to the response instead of throwing, which is what makes it
233
+ // this list's business rather than only `wrapAiToolError`'s. Last, because
234
+ // its match is the loosest.
235
+ match: r => DISPATCH_NOT_REACHED.test(r),
236
+ message: cmd => unsupportedAiBrowserFailure(cmd).message,
237
+ code: 'BROWSER_ERROR',
238
+ suggestions: cmd => unsupportedAiBrowserFailure(cmd).suggestions,
239
+ },
240
+ ];
241
+ /**
242
+ * The `CdpError` a result text describes, or null when it describes no known
243
+ * condition. One lookup behind both the throw (a successful-looking AI result)
244
+ * and the value (a failure classified for its exit code and message).
245
+ */
246
+ export function findCdpResultError(command, result) {
247
+ for (const descriptor of CDP_RESULT_ERRORS) {
248
+ if (descriptor.match(result)) {
249
+ const message = typeof descriptor.message === 'function'
250
+ ? descriptor.message(command)
251
+ : descriptor.message;
252
+ return new CdpError(message, descriptor.code, descriptor.suggestions(command));
253
+ }
254
+ }
255
+ return null;
256
+ }
257
+ /** Throws a `CdpError` when a successful-looking AI result is really an error. */
258
+ export function checkAiResultForCdpError(command, result) {
259
+ const error = findCdpResultError(command, result);
260
+ if (error) {
261
+ throw error;
262
+ }
263
+ }
264
+ /**
265
+ * Everything a caller needs to report a failure that arrived as text: the
266
+ * message, the code to exit with, and the next step.
267
+ *
268
+ * The descriptors come first because they carry a diagnosis and its remedies —
269
+ * an error result holding `[OPERA_CDP_ERR:NOT_SIGNED_IN]` is answered with the
270
+ * same sign-in instructions a successful-looking one gets, rather than with the
271
+ * extension's marker. Text no descriptor knows keeps its own words and takes
272
+ * its code from `classifyToolError`.
273
+ */
274
+ export function describeToolFailure(command, text) {
275
+ const known = findCdpResultError(command, text);
276
+ if (known) {
277
+ return {
278
+ message: known.message,
279
+ code: known.code,
280
+ suggestions: known.suggestions,
281
+ };
282
+ }
283
+ const code = classifyToolError(text);
284
+ return { message: text, code, suggestions: CODE_SUGGESTIONS[code] ?? [] };
285
+ }
286
+ /**
287
+ * Opera reports "no target to dispatch to" when the connected browser has no
288
+ * Opera AI extension — i.e. it is not Opera at all, or not Opera Neon for the
289
+ * tools that require it. The raw CDP text says nothing about that, so it is
290
+ * rewritten into something the user can act on.
291
+ *
292
+ * The tool appends this wording to a *successful* result instead of throwing
293
+ * when its own retries are spent (`tools/opera.ts`), so the same diagnosis is in
294
+ * `CDP_RESULT_ERRORS` for that path.
295
+ */
296
+ export function wrapAiToolError(command, error) {
297
+ const message = error instanceof Error ? error.message : String(error);
298
+ if (AI_EXTENSION_UNAVAILABLE.test(message)) {
299
+ return aiExtensionUnavailableFailure(command);
300
+ }
301
+ if (!DISPATCH_NOT_REACHED.test(message)) {
302
+ return error;
303
+ }
304
+ return unsupportedAiBrowserFailure(command);
305
+ }
306
+ /**
307
+ * Classify a tool failure that arrived as text rather than as a code.
308
+ *
309
+ * Upstream's `ToolHandler` turns a thrown error into `isError: true` with the
310
+ * message as content, so the *reason* has to be recovered from the message to
311
+ * pick an exit code. Anything unrecognised is `UNKNOWN` (1) rather than a wrong
312
+ * specific code.
313
+ *
314
+ * **Order is load-bearing: most specific first.** Every entry must be able to
315
+ * match the message on its own terms, because an earlier broad pattern will
316
+ * shadow a later exact one. The unknown-argument reply is the standing example —
317
+ * it lists the tool's expected arguments, and `navigate_page`'s list contains
318
+ * `"timeout"`, so a bare `timeout` pattern placed before it reports a bad
319
+ * argument as a timeout.
320
+ */
321
+ const TOOL_ERROR_PATTERNS = [
322
+ // Unsigned / unsubscribed Opera AI — the user has to act.
323
+ {
324
+ match: /not signed in|subscription|consent|OPERA_CDP_ERR/i,
325
+ code: 'AUTH_REQUIRED',
326
+ },
327
+ // A stale uid, or a page that moved under the call.
328
+ {
329
+ match: /uid .* not found|element .*not found|no node with given id|stashed element/i,
330
+ code: 'REF_NOT_FOUND',
331
+ },
332
+ {
333
+ match: /detached|execution context was destroyed|cannot find context|target closed|page.*closed/i,
334
+ code: 'PAGE_CLOSED',
335
+ },
336
+ // A bad argument the caller can fix. `ToolHandler.ts`'s reply is a fixed
337
+ // prefix, so it is anchored rather than merely matched.
338
+ {
339
+ match: /^unknown arguments? for tool/i,
340
+ code: 'VALIDATION_ERROR',
341
+ },
342
+ // `evaluate_script` serializes its result with `JSON.stringify`, so a script
343
+ // returning something unserializable (a `Window`, a DOM node, a cyclic object)
344
+ // fails here rather than in the script. The script is the thing to fix.
345
+ {
346
+ match: /converting circular structure to json/i,
347
+ code: 'VALIDATION_ERROR',
348
+ },
349
+ // A tool or feature the browser is not allowed to use. `ToolHandler.ts`'s
350
+ // disabled message is the common one; `utils/url.ts`'s refusal to navigate to
351
+ // a `javascript:` URL is the other shape, and it names the same cause.
352
+ {
353
+ match: /(?:is|are)\s+(?:currently\s+)?disabled|not allowed|requires experimental feature/i,
354
+ code: 'UNSUPPORTED_OPERATION',
355
+ },
356
+ // Only timeout *phrasing*, never the bare word: it is a common argument name.
357
+ {
358
+ match: /timed out\b|timeout of \d|timeout \d+\s*ms exceeded|exceeded the timeout|within the configured timeout|timeout waiting for/i,
359
+ code: 'TIMEOUT',
360
+ },
361
+ // The shapes the CDP descriptors own when they arrive as content on a
362
+ // successful call. `describeToolFailure` asks the descriptors first, so these
363
+ // entries are the backstop for the same wording arriving as a daemon error
364
+ // string instead — without them it would report UNKNOWN (1) rather than the
365
+ // environment failure it is.
366
+ {
367
+ match: /NotReadableError|Data lost due to missing file|irrecoverable|no progress was reported for/i,
368
+ code: 'BROWSER_ERROR',
369
+ },
370
+ {
371
+ match: AI_EXTENSION_UNAVAILABLE,
372
+ code: 'EXTENSION_NOT_FOUND',
373
+ },
374
+ {
375
+ match: DISPATCH_NOT_REACHED,
376
+ code: 'BROWSER_ERROR',
377
+ },
378
+ {
379
+ match: /ECONNREFUSED|ECONNRESET|EPIPE|socket hang up/i,
380
+ code: 'SERVER_DISCONNECTED',
381
+ },
382
+ ];
383
+ export function classifyToolError(message) {
384
+ for (const { match, code } of TOOL_ERROR_PATTERNS) {
385
+ if (match.test(message)) {
386
+ return code;
387
+ }
388
+ }
389
+ return 'UNKNOWN';
390
+ }
391
+ //# sourceMappingURL=cdpErrors.js.map