opera-devtools-mcp 0.7.0 → 0.8.1
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/ToolHandler.js +9 -2
- package/build/src/bin/chrome-devtools.js +30 -97
- package/build/src/bin/opera-browser-cli.js +102 -0
- package/build/src/bin/opera-devtools-mcp.js +20 -1
- package/build/src/browser.js +18 -9
- package/build/src/daemon/client.js +46 -40
- package/build/src/daemon/daemon.js +62 -39
- 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/third_party/devtools-formatter-worker.js +1 -0
- package/build/src/third_party/devtools-heap-snapshot-worker.js +1 -0
- package/build/src/third_party/index.js +2 -1
- package/build/src/utils/url.js +6 -0
- package/build/src/version.js +1 -1
- package/package.json +12 -10
- package/build/src/bin/opera-devtools.js +0 -10
|
@@ -5,14 +5,22 @@
|
|
|
5
5
|
* This file is an original work developed by Opera.
|
|
6
6
|
*/
|
|
7
7
|
import { closeBrowserIfOpen, ensureBrowserLaunched, getCurrentBrowser, } from '../browser.js';
|
|
8
|
-
import { buildLaunchOptions } from './browserLaunch.js';
|
|
9
8
|
import { logger } from '../utils/logger.js';
|
|
9
|
+
import { otherBrowserUsers } from './browserActivity.js';
|
|
10
|
+
import { buildLaunchOptions } from './browserLaunch.js';
|
|
10
11
|
/**
|
|
11
12
|
* Opera's AI features refuse to run when the page reports itself as
|
|
12
13
|
* automation-controlled, so the browser has to be launched with this flag for
|
|
13
14
|
* them. It is deliberately NOT applied to every launch: it changes observable
|
|
14
15
|
* page behaviour, which would silently alter results for ordinary DevTools
|
|
15
16
|
* tools.
|
|
17
|
+
*
|
|
18
|
+
* The flags are therefore acquired the first time an Opera AI tool needs them
|
|
19
|
+
* and then kept for the life of that browser — see `ensureBrowserFlagsForTool`.
|
|
20
|
+
* A browser carrying them is never relaunched to take them away again: the
|
|
21
|
+
* daemon serves several terminals at once, and the tool that used to trigger
|
|
22
|
+
* that relaunch was an ordinary one (`take_snapshot` in another terminal), which
|
|
23
|
+
* closed the browser a running `opera_do` was streaming from.
|
|
16
24
|
*/
|
|
17
25
|
export const OPERA_AUTOMATION_FLAGS = [
|
|
18
26
|
'--disable-blink-features=AutomationControlled',
|
|
@@ -27,59 +35,197 @@ export function toolRequiresOperaFlags(toolName) {
|
|
|
27
35
|
return TOOLS_REQUIRING_OPERA_FLAGS.has(toolName);
|
|
28
36
|
}
|
|
29
37
|
/**
|
|
30
|
-
*
|
|
31
|
-
*
|
|
32
|
-
* the
|
|
38
|
+
* The browser this server launched with {@link OPERA_AUTOMATION_FLAGS}, held by
|
|
39
|
+
* identity rather than as a flag that outlives it: a browser that died and was
|
|
40
|
+
* relaunched by the next call carries none of them (`index.ts` launches without
|
|
41
|
+
* the Opera flags), and the tools that need them must not be told otherwise.
|
|
42
|
+
* Module-level rather than per-server because the browser itself is a
|
|
43
|
+
* module-level singleton in `../browser.ts`.
|
|
33
44
|
*/
|
|
34
|
-
let
|
|
35
|
-
|
|
36
|
-
return browserHasOperaFlags;
|
|
37
|
-
}
|
|
38
|
-
/** Test seam: forget what we believe about the current browser. */
|
|
45
|
+
let flagsBrowser;
|
|
46
|
+
/** Test seam: forget which browser was launched with the Opera flags. */
|
|
39
47
|
export function resetOperaFlagState() {
|
|
40
|
-
|
|
48
|
+
flagsBrowser = undefined;
|
|
41
49
|
}
|
|
50
|
+
/**
|
|
51
|
+
* One source of truth for "did we launch the browser, or attach to one that was
|
|
52
|
+
* already running". `index.ts` picks the branch with the same three options at
|
|
53
|
+
* invocation time, and the CLI reports the mode from the daemon's stored argv,
|
|
54
|
+
* so the rule must not be re-implemented per caller.
|
|
55
|
+
*/
|
|
56
|
+
const ATTACH_OPTIONS = ['browserUrl', 'wsEndpoint', 'autoConnect'];
|
|
42
57
|
/**
|
|
43
58
|
* True when this server launched the browser itself. When the user attached to
|
|
44
59
|
* an existing browser we must never kill and relaunch it.
|
|
45
60
|
*/
|
|
46
|
-
function isLaunchMode(serverArgs) {
|
|
47
|
-
return
|
|
61
|
+
export function isLaunchMode(serverArgs) {
|
|
62
|
+
return !ATTACH_OPTIONS.some(option => Boolean(serverArgs[option]));
|
|
48
63
|
}
|
|
49
64
|
/**
|
|
50
|
-
*
|
|
51
|
-
*
|
|
52
|
-
*
|
|
65
|
+
* One canonical spelling for a raw flag name. yargs expands `--browser-url` and
|
|
66
|
+
* `--browserUrl` to the same option, and the daemon stores the argv verbatim, so
|
|
67
|
+
* a read-back that does not normalize would call an attached session "launched".
|
|
53
68
|
*/
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
69
|
+
function canonicalOptionName(name) {
|
|
70
|
+
return name.replace(/-([a-z])/g, (_, letter) => letter.toUpperCase());
|
|
71
|
+
}
|
|
72
|
+
/**
|
|
73
|
+
* The flags of a stored argv in one pass: canonical name → value, `true` for a
|
|
74
|
+
* value-less flag. `--flag=value`, `--flag value`, `--no-flag` and
|
|
75
|
+
* `--flag=false` all arrive on a real command line, and all four have to read
|
|
76
|
+
* the same as `isLaunchMode` reads the parsed form.
|
|
77
|
+
*/
|
|
78
|
+
function readFlags(args) {
|
|
79
|
+
const flags = new Map();
|
|
80
|
+
for (let index = 0; index < args.length; index++) {
|
|
81
|
+
const arg = args[index];
|
|
82
|
+
if (!arg.startsWith('--')) {
|
|
83
|
+
continue;
|
|
84
|
+
}
|
|
85
|
+
const body = arg.slice(2);
|
|
86
|
+
const equals = body.indexOf('=');
|
|
87
|
+
if (equals !== -1) {
|
|
88
|
+
flags.set(canonicalOptionName(body.slice(0, equals)), body.slice(equals + 1));
|
|
89
|
+
continue;
|
|
90
|
+
}
|
|
91
|
+
if (body.startsWith('no-')) {
|
|
92
|
+
flags.set(canonicalOptionName(body.slice(3)), false);
|
|
93
|
+
continue;
|
|
94
|
+
}
|
|
95
|
+
const next = args[index + 1];
|
|
96
|
+
if (next !== undefined && !next.startsWith('-')) {
|
|
97
|
+
flags.set(canonicalOptionName(body), next);
|
|
98
|
+
index++;
|
|
99
|
+
continue;
|
|
100
|
+
}
|
|
101
|
+
flags.set(canonicalOptionName(body), true);
|
|
59
102
|
}
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
103
|
+
return flags;
|
|
104
|
+
}
|
|
105
|
+
/**
|
|
106
|
+
* How a serialized CLI argv (`opera-browser-cli status`) describes the browser
|
|
107
|
+
* it drives: `launched (owned by this daemon)` or `attached to <target>`.
|
|
108
|
+
*
|
|
109
|
+
* The daemon stores the MCP argv it was started with, so this needs no new
|
|
110
|
+
* channel between the two processes — and it is the same predicate the server
|
|
111
|
+
* itself applies, read back from the flags that decided it.
|
|
112
|
+
*/
|
|
113
|
+
export function describeBrowserMode(args) {
|
|
114
|
+
const flags = readFlags(args);
|
|
115
|
+
const attachFlag = ATTACH_OPTIONS.find(option => {
|
|
116
|
+
const value = flags.get(option);
|
|
117
|
+
// `--flag=false` and `--no-flag` are how yargs spells "not set", and
|
|
118
|
+
// `isLaunchMode` reads them the same way.
|
|
119
|
+
return value !== undefined && value !== false && value !== 'false';
|
|
120
|
+
});
|
|
121
|
+
if (!attachFlag) {
|
|
122
|
+
return 'launched (owned by this daemon)';
|
|
123
|
+
}
|
|
124
|
+
const value = flags.get(attachFlag);
|
|
125
|
+
return `attached to ${typeof value === 'string' && value ? value : 'an external browser'}`;
|
|
126
|
+
}
|
|
127
|
+
/**
|
|
128
|
+
* The profile a stored argv's launched browser was pointed at, if any.
|
|
129
|
+
*
|
|
130
|
+
* Read back for the same reason `describeBrowserMode` is: a daemon fixes its
|
|
131
|
+
* browser at startup, so its argv is the only record of which profile that
|
|
132
|
+
* browser holds. The CLI uses it to tell its own browser — which never
|
|
133
|
+
* advertises a debug port, because it is launched over a pipe — from the one a
|
|
134
|
+
* user opened, which is the difference between "nothing to do" and "ask".
|
|
135
|
+
*/
|
|
136
|
+
export function launchedUserDataDir(args) {
|
|
137
|
+
const value = readFlags(args).get('userDataDir');
|
|
138
|
+
return typeof value === 'string' && value.length > 0 ? value : undefined;
|
|
139
|
+
}
|
|
140
|
+
/**
|
|
141
|
+
* The `--browser-url` a stored argv carries, if it carries one.
|
|
142
|
+
*
|
|
143
|
+
* That is the form the CLI's own endpoint discovery sets (`browserTarget.ts`
|
|
144
|
+
* finds a live `DevToolsActivePort` and passes the URL on), which makes it the
|
|
145
|
+
* one attach target the CLI may re-derive when the browser behind it is gone: a
|
|
146
|
+
* `--wsEndpoint` or `--autoConnect` was typed by the user, and their browser is
|
|
147
|
+
* theirs to start again.
|
|
148
|
+
*/
|
|
149
|
+
export function storedBrowserUrl(args) {
|
|
150
|
+
const value = readFlags(args).get('browserUrl');
|
|
151
|
+
return typeof value === 'string' && value.length > 0 ? value : undefined;
|
|
152
|
+
}
|
|
153
|
+
/**
|
|
154
|
+
* How long a relaunch waits for the browser to fall idle before it gives up and
|
|
155
|
+
* reports why, and how often it looks. A mutable object so tests can drive the
|
|
156
|
+
* wait without waiting: faking timers is not workable here (sinon's fake clock
|
|
157
|
+
* replaces the globals `node:test` schedules subtests with).
|
|
158
|
+
*/
|
|
159
|
+
export const browserIdleWaitPolicy = {
|
|
160
|
+
timeoutMs: 10_000,
|
|
161
|
+
pollMs: 100,
|
|
162
|
+
};
|
|
163
|
+
function sleep(ms) {
|
|
164
|
+
const { promise, resolve } = Promise.withResolvers();
|
|
165
|
+
setTimeout(resolve, ms);
|
|
166
|
+
return promise;
|
|
167
|
+
}
|
|
168
|
+
const defaultFlagRelaunchDeps = {
|
|
169
|
+
closeBrowserIfOpen,
|
|
170
|
+
ensureBrowserLaunched,
|
|
171
|
+
getCurrentBrowser,
|
|
172
|
+
sleep,
|
|
173
|
+
};
|
|
174
|
+
/**
|
|
175
|
+
* Wait until nothing else is inside a tool invocation, or refuse.
|
|
176
|
+
*
|
|
177
|
+
* A relaunch closes every page in the browser, which is defensible only when
|
|
178
|
+
* nothing is using it: the daemon answers several terminals at once, so the
|
|
179
|
+
* alternative is destroying the pages of an invocation that is still running —
|
|
180
|
+
* the failure this whole module exists to stop.
|
|
181
|
+
*/
|
|
182
|
+
async function waitForBrowserIdle(toolName, sleep) {
|
|
183
|
+
const deadline = Date.now() + browserIdleWaitPolicy.timeoutMs;
|
|
184
|
+
for (;;) {
|
|
185
|
+
const users = otherBrowserUsers(toolName);
|
|
186
|
+
if (users.length === 0) {
|
|
187
|
+
return;
|
|
188
|
+
}
|
|
189
|
+
if (Date.now() >= deadline) {
|
|
190
|
+
throw new Error(`${toolName} needs the browser relaunched with Opera's automation flags, and it is in use by ${users.join(', ')} — relaunching it now would close that work. Timed out waiting for the browser to be free; retry when it is.`);
|
|
191
|
+
}
|
|
192
|
+
await sleep(browserIdleWaitPolicy.pollMs);
|
|
193
|
+
}
|
|
194
|
+
}
|
|
195
|
+
/**
|
|
196
|
+
* Make sure the running browser has the Opera automation flags `toolName` needs.
|
|
197
|
+
*
|
|
198
|
+
* One direction only, and that is the point. A browser this daemon launched
|
|
199
|
+
* *gains* the flags when an Opera AI tool first needs them and keeps them for the
|
|
200
|
+
* rest of its life; a tool that needs no flags is not this module's business at
|
|
201
|
+
* all. The reverse relaunch — taking the flags away again for an ordinary
|
|
202
|
+
* DevTools tool — was what closed the browser under a running `opera_do` when a
|
|
203
|
+
* second terminal ran `take_snapshot`, failing both streams with the AI
|
|
204
|
+
* dispatcher's "no target" error, and it is gone rather than made conditional:
|
|
205
|
+
* the flags are a property of the browser, not of the tool that happens to be
|
|
206
|
+
* running.
|
|
207
|
+
*
|
|
208
|
+
* The one relaunch that remains — the acquisition — waits for the browser to fall
|
|
209
|
+
* idle first, and reports why instead of closing anything if it does not.
|
|
210
|
+
*/
|
|
211
|
+
export async function ensureBrowserFlagsForTool(toolName, serverArgs, logFile, control, deps = defaultFlagRelaunchDeps) {
|
|
212
|
+
if (!isLaunchMode(serverArgs) || !toolRequiresOperaFlags(toolName)) {
|
|
64
213
|
return;
|
|
65
214
|
}
|
|
66
|
-
|
|
215
|
+
const current = deps.getCurrentBrowser();
|
|
216
|
+
if (current?.connected && current === flagsBrowser) {
|
|
67
217
|
return;
|
|
68
218
|
}
|
|
69
|
-
|
|
219
|
+
await waitForBrowserIdle(toolName, deps.sleep);
|
|
220
|
+
logger?.(`Relaunching browser with Opera's automation flags for ${toolName}`);
|
|
70
221
|
control.resetContext();
|
|
71
|
-
|
|
222
|
+
flagsBrowser = undefined;
|
|
72
223
|
await deps.closeBrowserIfOpen();
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
}));
|
|
80
|
-
browserHasOperaFlags = true;
|
|
81
|
-
}
|
|
82
|
-
// Otherwise leave the browser closed: getContext() relaunches it without the
|
|
83
|
-
// Opera flags on the next call.
|
|
224
|
+
// Launch options come from the shared `buildLaunchOptions` so the relaunch
|
|
225
|
+
// applies exactly the same flags (including blocklist/allowlist and proxy)
|
|
226
|
+
// as the normal launch in `index.ts`, plus the automation flags.
|
|
227
|
+
flagsBrowser = await deps.ensureBrowserLaunched(buildLaunchOptions(serverArgs, logFile, {
|
|
228
|
+
extraChromeArgs: OPERA_AUTOMATION_FLAGS,
|
|
229
|
+
}));
|
|
84
230
|
}
|
|
85
231
|
//# sourceMappingURL=browserFlags.js.map
|