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.
- package/README.md +1 -1
- package/build/src/McpContext.js +78 -41
- package/build/src/McpPage.js +5 -1
- package/build/src/ToolHandler.js +54 -76
- package/build/src/bin/chrome-devtools-mcp-main.js +4 -4
- package/build/src/bin/chrome-devtools.js +56 -124
- package/build/src/bin/opera-browser-cli.js +102 -0
- package/build/src/bin/opera-devtools-cli-options.js +1 -1
- package/build/src/bin/opera-devtools-mcp-cli-options.js +1 -1
- package/build/src/bin/opera-devtools-mcp.js +20 -1
- package/build/src/browser.js +23 -25
- package/build/src/config/browser-options.js +126 -0
- package/build/src/config/category-options.js +81 -0
- package/build/src/{bin/chrome-devtools-cli-options.js → config/cli-options.js} +368 -26
- package/build/src/{bin/chrome-devtools-mcp-cli-options.js → config/mcp-options.js} +143 -164
- package/build/src/daemon/client.js +55 -40
- package/build/src/daemon/daemon.js +62 -39
- package/build/src/daemon/utils.js +6 -0
- package/build/src/devtools/DevtoolsUtils.js +27 -21
- package/build/src/formatters/NetworkFormatter.js +5 -2
- package/build/src/index.js +166 -100
- package/build/src/opera/branding.js +4 -2
- package/build/src/opera/browserActivity.js +62 -0
- package/build/src/opera/browserCleanup.js +123 -0
- package/build/src/opera/browserErrors.js +66 -0
- package/build/src/opera/browserFlags.js +184 -38
- package/build/src/opera/browserTarget.js +513 -0
- package/build/src/opera/cdpErrors.js +391 -0
- package/build/src/opera/cliCommands.js +378 -0
- package/build/src/opera/cliOutput.js +284 -0
- package/build/src/opera/compactSnapshot.js +525 -0
- package/build/src/opera/config.js +166 -0
- package/build/src/opera/daemonLifecycle.js +257 -0
- package/build/src/opera/daemonLog.js +103 -0
- package/build/src/opera/daemonPidFile.js +83 -0
- package/build/src/opera/daemonShutdown.js +66 -0
- package/build/src/opera/daemonSocket.js +87 -0
- package/build/src/opera/daemonStreaming.js +130 -0
- package/build/src/opera/daemonToolCall.js +26 -0
- package/build/src/opera/detect.js +114 -0
- package/build/src/opera/doctor.js +317 -0
- package/build/src/opera/envConfig.js +229 -0
- package/build/src/opera/launcherNotice.js +116 -0
- package/build/src/opera/legacyBridgeCleanup.js +297 -0
- package/build/src/opera/logs.js +133 -0
- package/build/src/opera/mcpServerSupervisor.js +128 -0
- package/build/src/opera/migrationShared.js +164 -0
- package/build/src/opera/operaPages.js +56 -0
- package/build/src/opera/pageIdRouting.js +35 -0
- package/build/src/opera/pageRecovery.js +53 -0
- package/build/src/opera/profile.js +270 -0
- package/build/src/opera/refArgs.js +36 -0
- package/build/src/opera/serviceWorkerRetry.js +46 -4
- package/build/src/opera/setup.js +290 -0
- package/build/src/opera/skills/SKILL.md +160 -0
- package/build/src/opera/streamingTools.js +73 -0
- package/build/src/opera/suggestions.js +67 -0
- package/build/src/opera/toolHandlerHooks.js +25 -1
- package/build/src/opera/tools/opera.js +107 -38
- package/build/src/opera/urlResolver.js +69 -0
- package/build/src/opera/webStorageWarning.js +92 -0
- package/build/src/processors/HeapSnapshotManager.js +12 -0
- package/build/src/telemetry/ClearcutLogger.js +19 -6
- package/build/src/telemetry/transformation.js +4 -0
- package/build/src/telemetry/types.js +4 -0
- package/build/src/third_party/THIRD_PARTY_NOTICES +5 -5
- package/build/src/third_party/bundled-packages.json +3 -3
- package/build/src/third_party/devtools-formatter-worker.js +23 -0
- package/build/src/third_party/devtools-heap-snapshot-worker.js +101 -20
- package/build/src/third_party/index.js +15460 -14256
- package/build/src/third_party/issue-descriptions/federatedAuthRequestAccountsBlockedByConnectionAllowlist.md +1 -0
- package/build/src/third_party/issue-descriptions/federatedAuthRequestConfigBlockedByConnectionAllowlist.md +1 -0
- package/build/src/third_party/issue-descriptions/federatedAuthRequestIdTokenBlockedByConnectionAllowlist.md +1 -0
- package/build/src/third_party/issue-descriptions/federatedAuthRequestWellKnownBlockedByConnectionAllowlist.md +1 -0
- package/build/src/tools/ToolDefinition.js +15 -0
- package/build/src/tools/categories.js +0 -6
- package/build/src/tools/comments.js +90 -0
- package/build/src/tools/console.js +1 -1
- package/build/src/tools/emulation.js +1 -1
- package/build/src/tools/extensions.js +1 -1
- package/build/src/tools/memory.js +60 -6
- package/build/src/tools/network.js +2 -2
- package/build/src/tools/pages.js +26 -15
- package/build/src/tools/performance.js +4 -3
- package/build/src/tools/screencast.js +3 -2
- package/build/src/tools/screenshot.js +39 -8
- package/build/src/tools/script.js +17 -4
- package/build/src/tools/slim/tools.js +41 -33
- package/build/src/tools/snapshot.js +1 -1
- package/build/src/tools/tools.js +2 -0
- package/build/src/utils/WaitForHelper.js +12 -1
- package/build/src/utils/bytes.js +105 -0
- package/build/src/utils/url.js +79 -0
- package/build/src/version.js +1 -1
- package/package.json +29 -8
- 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
|