@vibebrowser/mcp 0.2.5 → 0.2.7
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 +70 -2
- package/dist/browser-cli.d.ts.map +1 -1
- package/dist/browser-cli.js +767 -358
- package/dist/browser-cli.js.map +1 -1
- package/dist/browser-main.js +1 -0
- package/dist/browser-main.js.map +1 -1
- package/dist/cli.js +52 -4
- package/dist/cli.js.map +1 -1
- package/dist/connection.d.ts +14 -7
- package/dist/connection.d.ts.map +1 -1
- package/dist/connection.js +106 -21
- package/dist/connection.js.map +1 -1
- package/dist/devtools-fallback.d.ts +22 -0
- package/dist/devtools-fallback.d.ts.map +1 -0
- package/dist/devtools-fallback.js +172 -0
- package/dist/devtools-fallback.js.map +1 -0
- package/dist/relay.d.ts +23 -3
- package/dist/relay.d.ts.map +1 -1
- package/dist/relay.js +448 -103
- package/dist/relay.js.map +1 -1
- package/dist/server.d.ts +4 -0
- package/dist/server.d.ts.map +1 -1
- package/dist/server.js +248 -12
- package/dist/server.js.map +1 -1
- package/dist/types.d.ts +27 -10
- package/dist/types.d.ts.map +1 -1
- package/docs/chrome-devtools-relay.md +7 -0
- package/docs/eval.md +101 -16
- package/docs/openclaw-local-browser.md +119 -18
- package/openclaw/vibebrowser/SKILL.md +219 -0
- package/package.json +9 -2
- package/openclaw/vibe-local-browser/SKILL.md +0 -132
package/dist/browser-cli.js
CHANGED
|
@@ -1,11 +1,31 @@
|
|
|
1
|
-
import { writeFileSync } from 'node:fs';
|
|
2
|
-
import { resolve } from 'node:path';
|
|
1
|
+
import { readFileSync, writeFileSync } from 'node:fs';
|
|
2
|
+
import { basename, extname, resolve } from 'node:path';
|
|
3
3
|
import { setTimeout as delay } from 'node:timers/promises';
|
|
4
4
|
import { ExtensionConnection } from './connection.js';
|
|
5
|
+
import { DevtoolsFallbackConnection } from './devtools-fallback.js';
|
|
5
6
|
import { DEFAULT_WS_PORT } from './types.js';
|
|
6
7
|
const DEFAULT_TIMEOUT_MS = 30_000;
|
|
8
|
+
/** Short timeout for the `status` command — it should never block for 30 s. */
|
|
9
|
+
const STATUS_TOOLS_TIMEOUT_MS = 2_000;
|
|
10
|
+
const MIME_TYPE_BY_EXTENSION = {
|
|
11
|
+
'.css': 'text/css',
|
|
12
|
+
'.csv': 'text/csv',
|
|
13
|
+
'.gif': 'image/gif',
|
|
14
|
+
'.html': 'text/html',
|
|
15
|
+
'.jpeg': 'image/jpeg',
|
|
16
|
+
'.jpg': 'image/jpeg',
|
|
17
|
+
'.js': 'text/javascript',
|
|
18
|
+
'.json': 'application/json',
|
|
19
|
+
'.md': 'text/markdown',
|
|
20
|
+
'.pdf': 'application/pdf',
|
|
21
|
+
'.png': 'image/png',
|
|
22
|
+
'.svg': 'image/svg+xml',
|
|
23
|
+
'.txt': 'text/plain',
|
|
24
|
+
'.webp': 'image/webp',
|
|
25
|
+
'.xml': 'application/xml',
|
|
26
|
+
};
|
|
7
27
|
const DEFAULT_BROWSER_PROFILE = process.env.VIBE_BROWSER_PROFILE || 'user';
|
|
8
|
-
const DEFAULT_REMOTE_UUID = process.env.VIBE_EXTENSION_UUID || process.env.VIBE_RELAY_UUID;
|
|
28
|
+
const DEFAULT_REMOTE_UUID = process.env.VIBE_REMOTE_URL || process.env.VIBE_EXTENSION_UUID || process.env.VIBE_RELAY_UUID;
|
|
9
29
|
const DEFAULT_REMOTE_RELAY_URL = process.env.VIBE_REMOTE_RELAY_URL || process.env.VIBE_RELAY_URL;
|
|
10
30
|
export function registerBrowserCommand(program) {
|
|
11
31
|
const browser = buildBrowserCommand(program.command('browser'));
|
|
@@ -22,17 +42,32 @@ function buildBrowserCommand(command) {
|
|
|
22
42
|
.option('--target <target>', 'OpenClaw compatibility target selector (accepted, not used by the Vibe browser CLI)')
|
|
23
43
|
.option('-p, --port <number>', 'WebSocket port for local relay (agent) connection', String(DEFAULT_WS_PORT))
|
|
24
44
|
.option('-d, --debug', 'Enable debug logging', false)
|
|
45
|
+
.option('--devtools', 'Use only chrome-devtools backend (bypasses extension relay)', false)
|
|
25
46
|
.option('-r, --remote <uuid>', 'Connect to a remote extension via public relay (provide the extension UUID)', DEFAULT_REMOTE_UUID)
|
|
47
|
+
.option('-s, --session <id>', 'Target a specific local browser session ID; defaults to the first connected session')
|
|
26
48
|
.option('--relay-url <url>', 'Custom relay server URL', DEFAULT_REMOTE_RELAY_URL)
|
|
27
49
|
.option('--json', 'Emit machine-readable JSON output', false)
|
|
28
|
-
.option('--timeout <ms>', 'Command timeout in milliseconds', String(DEFAULT_TIMEOUT_MS))
|
|
50
|
+
.option('--timeout <ms>', 'Command timeout in milliseconds', String(DEFAULT_TIMEOUT_MS))
|
|
51
|
+
.option('--page-id <id>', 'Target a specific page/tab by its numeric ID (avoids switching the user\'s active tab)')
|
|
52
|
+
.option('--pageId <id>', 'Alias for --page-id');
|
|
29
53
|
}
|
|
30
54
|
function registerBrowserSubcommands(browser) {
|
|
31
55
|
browser
|
|
32
56
|
.command('status')
|
|
33
57
|
.description('Show browser bridge status')
|
|
58
|
+
.option('--wait-for-extension', 'Wait for extension connection before returning status', false)
|
|
59
|
+
.option('--wait-timeout <ms>', 'Maximum wait time when --wait-for-extension is enabled')
|
|
60
|
+
.option('--poll-interval <ms>', 'Polling interval while waiting for extension')
|
|
34
61
|
.action(async function () {
|
|
35
|
-
await runBrowserCommand(this, 'status', false, async (ctx) => ctx.status(
|
|
62
|
+
await runBrowserCommand(this, 'status', false, async (ctx, options) => ctx.status({
|
|
63
|
+
waitForExtension: Boolean(options.waitForExtension),
|
|
64
|
+
waitTimeoutMs: options.waitTimeout
|
|
65
|
+
? parsePositiveInteger(String(options.waitTimeout), '--wait-timeout')
|
|
66
|
+
: undefined,
|
|
67
|
+
pollIntervalMs: options.pollInterval
|
|
68
|
+
? parsePositiveInteger(String(options.pollInterval), '--poll-interval')
|
|
69
|
+
: undefined,
|
|
70
|
+
}));
|
|
36
71
|
});
|
|
37
72
|
browser
|
|
38
73
|
.command('start')
|
|
@@ -61,6 +96,12 @@ function registerBrowserSubcommands(browser) {
|
|
|
61
96
|
note: 'The Vibe browser bridge does not own the browser process, so stop only disconnects this CLI session',
|
|
62
97
|
}));
|
|
63
98
|
});
|
|
99
|
+
browser
|
|
100
|
+
.command('sessions')
|
|
101
|
+
.description('List connected browser sessions')
|
|
102
|
+
.action(async function () {
|
|
103
|
+
await runBrowserCommand(this, 'sessions', false, async (ctx) => ctx.listSessions());
|
|
104
|
+
});
|
|
64
105
|
browser
|
|
65
106
|
.command('tabs')
|
|
66
107
|
.description('List browser tabs/pages')
|
|
@@ -80,7 +121,7 @@ function registerBrowserSubcommands(browser) {
|
|
|
80
121
|
});
|
|
81
122
|
tab
|
|
82
123
|
.command('select <id>')
|
|
83
|
-
.description('
|
|
124
|
+
.description('Switch to a tab/page by its ID (rarely needed — most commands accept a tab id argument instead, prefer that to avoid disrupting the user\'s browser)')
|
|
84
125
|
.action(async function (id) {
|
|
85
126
|
await runBrowserCommand(this, 'tab select', true, async (ctx) => ctx.focus(id));
|
|
86
127
|
});
|
|
@@ -104,7 +145,7 @@ function registerBrowserSubcommands(browser) {
|
|
|
104
145
|
});
|
|
105
146
|
browser
|
|
106
147
|
.command('focus <id>')
|
|
107
|
-
.description('
|
|
148
|
+
.description('Switch focus to a tab/page by its ID (rarely needed — most commands accept a tab id argument instead, prefer that to avoid disrupting the user\'s browser)')
|
|
108
149
|
.action(async function (id) {
|
|
109
150
|
await runBrowserCommand(this, 'focus', true, async (ctx) => ctx.focus(id));
|
|
110
151
|
});
|
|
@@ -117,7 +158,7 @@ function registerBrowserSubcommands(browser) {
|
|
|
117
158
|
browser
|
|
118
159
|
.command('snapshot')
|
|
119
160
|
.description('Capture a textual browser snapshot')
|
|
120
|
-
.option('--format <format>', 'Snapshot format
|
|
161
|
+
.option('--format <format>', 'Snapshot format: "ai" (default, content-script markdown — may fail on background tabs or complex SPAs) or "aria" (CDP accessibility tree — reliable for all tabs)', 'ai')
|
|
121
162
|
.option('--limit <count>', 'Max visible lines/items to print in human mode')
|
|
122
163
|
.option('--interactive', 'Prefer interactive/ARIA-flavored snapshot output', false)
|
|
123
164
|
.option('--selector <selector>', 'Selector to scope the snapshot to')
|
|
@@ -156,31 +197,7 @@ function registerBrowserSubcommands(browser) {
|
|
|
156
197
|
grayscale: Boolean(options.grayscale),
|
|
157
198
|
}));
|
|
158
199
|
});
|
|
159
|
-
browser
|
|
160
|
-
.command('pdf')
|
|
161
|
-
.description('Render the current page as PDF when supported')
|
|
162
|
-
.option('--output <path>', 'Write PDF bytes to a file')
|
|
163
|
-
.action(async function () {
|
|
164
|
-
await runBrowserCommand(this, 'pdf', true, async (ctx, options) => ctx.pdf(options.output ? String(options.output) : undefined));
|
|
165
|
-
});
|
|
166
|
-
browser
|
|
167
|
-
.command('console')
|
|
168
|
-
.description('List console messages')
|
|
169
|
-
.option('--level <level>', 'Console level filter')
|
|
170
|
-
.option('--preserve', 'Include preserved messages when supported', false)
|
|
171
|
-
.action(async function () {
|
|
172
|
-
await runBrowserCommand(this, 'console', true, async (ctx, options) => ctx.consoleMessages({
|
|
173
|
-
level: options.level ? String(options.level) : undefined,
|
|
174
|
-
preserve: Boolean(options.preserve),
|
|
175
|
-
}));
|
|
176
|
-
});
|
|
177
|
-
browser
|
|
178
|
-
.command('errors')
|
|
179
|
-
.description('List console errors')
|
|
180
|
-
.option('--clear', 'Compatibility flag', false)
|
|
181
|
-
.action(async function () {
|
|
182
|
-
await runBrowserCommand(this, 'errors', true, async (ctx) => ctx.consoleMessages({ level: 'error' }));
|
|
183
|
-
});
|
|
200
|
+
// NOTE: pdf, console, errors commands removed — no matching browser tools.
|
|
184
201
|
browser
|
|
185
202
|
.command('requests')
|
|
186
203
|
.description('List network requests')
|
|
@@ -207,9 +224,9 @@ function registerBrowserSubcommands(browser) {
|
|
|
207
224
|
});
|
|
208
225
|
browser
|
|
209
226
|
.command('resize <width> <height>')
|
|
210
|
-
.description('Resize the
|
|
227
|
+
.description('Resize the current page viewport/window')
|
|
211
228
|
.action(async function (width, height) {
|
|
212
|
-
await runBrowserCommand(this, 'resize', true, async (ctx) => ctx.resize(parsePositiveInteger(width, 'width'), parsePositiveInteger(height, 'height')));
|
|
229
|
+
await runBrowserCommand(this, 'resize', true, async (ctx) => ctx.resize(parsePositiveInteger(width, '<width>'), parsePositiveInteger(height, '<height>')));
|
|
213
230
|
});
|
|
214
231
|
browser
|
|
215
232
|
.command('click <ref>')
|
|
@@ -238,11 +255,12 @@ function registerBrowserSubcommands(browser) {
|
|
|
238
255
|
await runBrowserCommand(this, 'hover', true, async (ctx) => ctx.hover(ref));
|
|
239
256
|
});
|
|
240
257
|
browser
|
|
241
|
-
.command('
|
|
242
|
-
.description('
|
|
243
|
-
.action(async function (ref) {
|
|
244
|
-
await runBrowserCommand(this, '
|
|
258
|
+
.command('upload <ref> <path>')
|
|
259
|
+
.description('Upload a local file via an input element reference')
|
|
260
|
+
.action(async function (ref, path) {
|
|
261
|
+
await runBrowserCommand(this, 'upload', true, async (ctx) => ctx.upload(ref, path));
|
|
245
262
|
});
|
|
263
|
+
// NOTE: scrollintoview, download, waitfordownload commands removed — no matching tools.
|
|
246
264
|
browser
|
|
247
265
|
.command('drag <source> <target>')
|
|
248
266
|
.description('Drag one element to another')
|
|
@@ -255,25 +273,6 @@ function registerBrowserSubcommands(browser) {
|
|
|
255
273
|
.action(async function (ref, values) {
|
|
256
274
|
await runBrowserCommand(this, 'select', true, async (ctx) => ctx.select(ref, values));
|
|
257
275
|
});
|
|
258
|
-
browser
|
|
259
|
-
.command('download <ref> [filename]')
|
|
260
|
-
.description('Trigger/download an element when supported')
|
|
261
|
-
.action(async function (ref, filename) {
|
|
262
|
-
await runBrowserCommand(this, 'download', true, async (ctx) => ctx.download(ref, filename));
|
|
263
|
-
});
|
|
264
|
-
browser
|
|
265
|
-
.command('waitfordownload [filename]')
|
|
266
|
-
.description('Wait for a download when supported')
|
|
267
|
-
.action(async function (filename) {
|
|
268
|
-
await runBrowserCommand(this, 'waitfordownload', true, async (ctx) => ctx.waitForDownload(filename));
|
|
269
|
-
});
|
|
270
|
-
browser
|
|
271
|
-
.command('upload <path>')
|
|
272
|
-
.description('Upload a file when supported')
|
|
273
|
-
.option('--ref <ref>', 'Element ref/index')
|
|
274
|
-
.action(async function (path) {
|
|
275
|
-
await runBrowserCommand(this, 'upload', true, async (ctx, options) => ctx.upload(path, options.ref ? String(options.ref) : undefined));
|
|
276
|
-
});
|
|
277
276
|
browser
|
|
278
277
|
.command('fill')
|
|
279
278
|
.description('Fill a form using JSON field descriptors')
|
|
@@ -283,15 +282,15 @@ function registerBrowserSubcommands(browser) {
|
|
|
283
282
|
});
|
|
284
283
|
browser
|
|
285
284
|
.command('dialog')
|
|
286
|
-
.description('
|
|
287
|
-
.option('--accept', 'Accept
|
|
288
|
-
.option('--dismiss', 'Dismiss
|
|
289
|
-
.option('--prompt <text>', '
|
|
285
|
+
.description('Handle browser dialog (accept/dismiss prompt/confirm/alert)')
|
|
286
|
+
.option('--accept', 'Accept dialog', false)
|
|
287
|
+
.option('--dismiss', 'Dismiss dialog', false)
|
|
288
|
+
.option('--prompt <text>', 'Optional prompt text when accepting')
|
|
290
289
|
.action(async function () {
|
|
291
290
|
await runBrowserCommand(this, 'dialog', true, async (ctx, options) => ctx.dialog({
|
|
292
291
|
accept: Boolean(options.accept),
|
|
293
292
|
dismiss: Boolean(options.dismiss),
|
|
294
|
-
|
|
293
|
+
promptText: options.prompt ? String(options.prompt) : undefined,
|
|
295
294
|
}));
|
|
296
295
|
});
|
|
297
296
|
browser
|
|
@@ -320,31 +319,8 @@ function registerBrowserSubcommands(browser) {
|
|
|
320
319
|
argsJson: options.args ? String(options.args) : undefined,
|
|
321
320
|
}));
|
|
322
321
|
});
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
.description('Highlight an element when supported')
|
|
326
|
-
.action(async function (ref) {
|
|
327
|
-
await runBrowserCommand(this, 'highlight', true, async (ctx) => ctx.highlight(ref));
|
|
328
|
-
});
|
|
329
|
-
const trace = browser.command('trace').description('Start/stop browser tracing');
|
|
330
|
-
trace
|
|
331
|
-
.command('start')
|
|
332
|
-
.description('Start a trace')
|
|
333
|
-
.option('--reload', 'Reload the page during trace start when supported', false)
|
|
334
|
-
.option('--output <path>', 'Trace output path')
|
|
335
|
-
.action(async function () {
|
|
336
|
-
await runBrowserCommand(this, 'trace start', true, async (ctx, options) => ctx.traceStart({
|
|
337
|
-
reload: Boolean(options.reload),
|
|
338
|
-
outputPath: options.output ? String(options.output) : undefined,
|
|
339
|
-
}));
|
|
340
|
-
});
|
|
341
|
-
trace
|
|
342
|
-
.command('stop')
|
|
343
|
-
.description('Stop an active trace')
|
|
344
|
-
.option('--output <path>', 'Trace output path')
|
|
345
|
-
.action(async function () {
|
|
346
|
-
await runBrowserCommand(this, 'trace stop', true, async (ctx, options) => ctx.traceStop(options.output ? String(options.output) : undefined));
|
|
347
|
-
});
|
|
322
|
+
// NOTE: highlight command removed — no highlight tool; users can use 'hover' directly.
|
|
323
|
+
// NOTE: trace commands removed — no performance_start_trace/performance_stop_trace tools.
|
|
348
324
|
}
|
|
349
325
|
async function runBrowserCommand(command, commandName, requireExtension, handler) {
|
|
350
326
|
const globalOptions = command.optsWithGlobals();
|
|
@@ -352,12 +328,15 @@ async function runBrowserCommand(command, commandName, requireExtension, handler
|
|
|
352
328
|
const ctx = new BrowserCliContext({
|
|
353
329
|
port: parsePositiveInteger(globalOptions.port, '--port'),
|
|
354
330
|
debug: Boolean(globalOptions.debug),
|
|
331
|
+
devtools: Boolean(globalOptions.devtools),
|
|
355
332
|
remoteUuid: globalOptions.remote,
|
|
333
|
+
sessionId: globalOptions.session,
|
|
356
334
|
relayUrl: globalOptions.relayUrl,
|
|
357
335
|
profile: globalOptions.browserProfile || DEFAULT_BROWSER_PROFILE,
|
|
358
336
|
json: Boolean(globalOptions.json),
|
|
359
337
|
timeoutMs: parsePositiveInteger(globalOptions.timeout, '--timeout'),
|
|
360
338
|
target: globalOptions.target,
|
|
339
|
+
pageId: globalOptions.pageId ? parsePositiveInteger(globalOptions.pageId, '--page-id') : undefined,
|
|
361
340
|
});
|
|
362
341
|
try {
|
|
363
342
|
await ctx.connect();
|
|
@@ -380,18 +359,27 @@ class BrowserCliContext {
|
|
|
380
359
|
profile;
|
|
381
360
|
json;
|
|
382
361
|
timeoutMs;
|
|
362
|
+
devtoolsOnly;
|
|
383
363
|
remoteUuid;
|
|
364
|
+
requestedSessionId;
|
|
384
365
|
target;
|
|
366
|
+
pageId;
|
|
385
367
|
toolsLoaded = false;
|
|
386
368
|
tools = [];
|
|
369
|
+
sessions = [];
|
|
387
370
|
ignoredCompatibilityOptions;
|
|
388
371
|
constructor(init) {
|
|
389
|
-
this.
|
|
372
|
+
this.devtoolsOnly = init.devtools;
|
|
373
|
+
this.connection = init.devtools
|
|
374
|
+
? new DevtoolsFallbackConnection(init.debug)
|
|
375
|
+
: new ExtensionConnection(init.port, init.debug, init.remoteUuid ? { uuid: init.remoteUuid, relayUrl: init.relayUrl } : undefined, init.remoteUuid ? undefined : { sessionId: init.sessionId });
|
|
390
376
|
this.profile = init.profile;
|
|
391
377
|
this.json = init.json;
|
|
392
378
|
this.timeoutMs = init.timeoutMs;
|
|
393
379
|
this.remoteUuid = init.remoteUuid;
|
|
380
|
+
this.requestedSessionId = init.sessionId;
|
|
394
381
|
this.target = init.target;
|
|
382
|
+
this.pageId = init.pageId;
|
|
395
383
|
this.ignoredCompatibilityOptions = [];
|
|
396
384
|
if (this.target) {
|
|
397
385
|
this.ignoredCompatibilityOptions.push(`target=${this.target}`);
|
|
@@ -399,37 +387,104 @@ class BrowserCliContext {
|
|
|
399
387
|
}
|
|
400
388
|
async connect() {
|
|
401
389
|
await this.connection.start();
|
|
402
|
-
|
|
403
|
-
|
|
390
|
+
if (this.connection instanceof ExtensionConnection) {
|
|
391
|
+
const extension = this.connection;
|
|
392
|
+
await delay(100);
|
|
393
|
+
this.sessions = await extension.listSessions(1_500).catch(() => extension.getSessions());
|
|
394
|
+
await extension.waitForToolsUpdate(500);
|
|
395
|
+
}
|
|
404
396
|
}
|
|
405
397
|
async shutdown() {
|
|
406
398
|
await this.connection.stop();
|
|
407
399
|
}
|
|
408
400
|
async ensureExtensionConnected() {
|
|
409
|
-
if (this.
|
|
401
|
+
if (this.isBackendConnected()) {
|
|
410
402
|
return;
|
|
411
403
|
}
|
|
404
|
+
if (this.connection instanceof ExtensionConnection) {
|
|
405
|
+
const extension = this.connection;
|
|
406
|
+
this.sessions = await extension.listSessions(1_500).catch(() => extension.getSessions());
|
|
407
|
+
}
|
|
408
|
+
this.toolsLoaded = false;
|
|
412
409
|
await this.ensureToolsLoaded();
|
|
413
|
-
if (!this.
|
|
414
|
-
throw new Error(
|
|
410
|
+
if (!this.isBackendConnected() && this.tools.length === 0) {
|
|
411
|
+
throw new Error(this.getConnectionErrorMessage());
|
|
415
412
|
}
|
|
416
413
|
}
|
|
417
|
-
async
|
|
418
|
-
if (this.connection
|
|
419
|
-
await this.
|
|
414
|
+
async listSessions() {
|
|
415
|
+
if (this.connection instanceof ExtensionConnection) {
|
|
416
|
+
this.sessions = await this.connection.listSessions(this.timeoutMs);
|
|
417
|
+
}
|
|
418
|
+
else {
|
|
419
|
+
this.sessions = [];
|
|
420
|
+
}
|
|
421
|
+
return {
|
|
422
|
+
ok: true,
|
|
423
|
+
command: 'sessions',
|
|
424
|
+
profile: this.profile,
|
|
425
|
+
mode: this.mode(),
|
|
426
|
+
ignoredCompatibilityOptions: this.ignoredCompatibilityOptions,
|
|
427
|
+
sessionId: this.currentSessionId(),
|
|
428
|
+
requestedSessionId: this.requestedSessionId,
|
|
429
|
+
sessions: this.sessions,
|
|
430
|
+
};
|
|
431
|
+
}
|
|
432
|
+
async status(options = {}) {
|
|
433
|
+
const waitForExtension = options.waitForExtension === true;
|
|
434
|
+
const waitTimeoutMs = options.waitTimeoutMs ?? this.timeoutMs;
|
|
435
|
+
const pollIntervalMs = options.pollIntervalMs ?? 250;
|
|
436
|
+
const waitStartedAt = Date.now();
|
|
437
|
+
if (this.connection instanceof ExtensionConnection) {
|
|
438
|
+
const extension = this.connection;
|
|
439
|
+
this.sessions = await extension.listSessions(STATUS_TOOLS_TIMEOUT_MS).catch(() => extension.getSessions());
|
|
440
|
+
}
|
|
441
|
+
else {
|
|
442
|
+
this.sessions = [];
|
|
420
443
|
}
|
|
444
|
+
if (waitForExtension && !this.isBackendConnected()) {
|
|
445
|
+
const deadline = Date.now() + waitTimeoutMs;
|
|
446
|
+
while (!this.isBackendConnected() && Date.now() < deadline) {
|
|
447
|
+
const remainingMs = deadline - Date.now();
|
|
448
|
+
if (remainingMs <= 0) {
|
|
449
|
+
break;
|
|
450
|
+
}
|
|
451
|
+
await delay(Math.min(pollIntervalMs, remainingMs));
|
|
452
|
+
if (this.connection instanceof ExtensionConnection) {
|
|
453
|
+
const extension = this.connection;
|
|
454
|
+
this.sessions = await extension.listSessions(STATUS_TOOLS_TIMEOUT_MS).catch(() => extension.getSessions());
|
|
455
|
+
}
|
|
456
|
+
}
|
|
457
|
+
}
|
|
458
|
+
if (this.isBackendConnected()) {
|
|
459
|
+
// Use a short timeout for status — this is a diagnostic command that
|
|
460
|
+
// should return quickly. Fall back to cached tools if the extension
|
|
461
|
+
// is slow to respond.
|
|
462
|
+
await this.ensureToolsLoaded(STATUS_TOOLS_TIMEOUT_MS);
|
|
463
|
+
}
|
|
464
|
+
await this.ensureToolsLoaded(STATUS_TOOLS_TIMEOUT_MS);
|
|
421
465
|
return {
|
|
422
466
|
ok: true,
|
|
423
467
|
command: 'status',
|
|
424
468
|
profile: this.profile,
|
|
425
|
-
mode: this.
|
|
469
|
+
mode: this.mode(),
|
|
470
|
+
sessionId: this.currentSessionId(),
|
|
471
|
+
requestedSessionId: this.requestedSessionId,
|
|
472
|
+
sessions: this.sessions,
|
|
426
473
|
ignoredCompatibilityOptions: this.ignoredCompatibilityOptions,
|
|
427
|
-
relayConnected: this.connection
|
|
428
|
-
|
|
474
|
+
relayConnected: this.connection instanceof ExtensionConnection
|
|
475
|
+
? this.connection.getStatus() === 'connected'
|
|
476
|
+
: false,
|
|
477
|
+
extensionConnected: this.isBackendConnected(),
|
|
429
478
|
managedLifecycle: false,
|
|
430
479
|
transport: 'vibebrowser-mcp',
|
|
431
480
|
toolCount: this.tools.length,
|
|
432
481
|
tools: this.tools.map((tool) => tool.name),
|
|
482
|
+
...(waitForExtension
|
|
483
|
+
? {
|
|
484
|
+
waitForExtension: true,
|
|
485
|
+
waitedMs: Date.now() - waitStartedAt,
|
|
486
|
+
}
|
|
487
|
+
: {}),
|
|
433
488
|
};
|
|
434
489
|
}
|
|
435
490
|
async listPages() {
|
|
@@ -442,6 +497,8 @@ class BrowserCliContext {
|
|
|
442
497
|
command: 'tabs',
|
|
443
498
|
profile: this.profile,
|
|
444
499
|
mode: this.mode(),
|
|
500
|
+
sessionId: this.currentSessionId(),
|
|
501
|
+
requestedSessionId: this.requestedSessionId,
|
|
445
502
|
ignoredCompatibilityOptions: this.ignoredCompatibilityOptions,
|
|
446
503
|
tool: invocation.tool,
|
|
447
504
|
pages,
|
|
@@ -452,72 +509,74 @@ class BrowserCliContext {
|
|
|
452
509
|
if (!url) {
|
|
453
510
|
return this.callGenericCommand('open', [{ names: ['new_page', 'create_new_tab'] }], {});
|
|
454
511
|
}
|
|
455
|
-
|
|
456
|
-
|
|
457
|
-
|
|
458
|
-
|
|
459
|
-
|
|
460
|
-
|
|
461
|
-
|
|
462
|
-
|
|
463
|
-
|
|
464
|
-
|
|
465
|
-
|
|
512
|
+
try {
|
|
513
|
+
const invocation = await this.callTool('open', [
|
|
514
|
+
{
|
|
515
|
+
names: ['new_page', 'create_new_tab'],
|
|
516
|
+
buildArgs: (tool) => withOpenArgs(tool, url),
|
|
517
|
+
},
|
|
518
|
+
{
|
|
519
|
+
names: ['navigate_page', 'navigate_to_url'],
|
|
520
|
+
buildArgs: (tool) => withNavigateArgs(tool, url, this.timeoutMs),
|
|
521
|
+
},
|
|
522
|
+
], {});
|
|
523
|
+
const output = this.outputFromInvocation('open', invocation);
|
|
524
|
+
return this.ensurePageContentInOutput('open', invocation, output, url);
|
|
525
|
+
}
|
|
526
|
+
catch (error) {
|
|
527
|
+
const recovered = await this.recoverPageContentAfterTimeout('open', url, error);
|
|
528
|
+
if (recovered) {
|
|
529
|
+
return recovered;
|
|
530
|
+
}
|
|
531
|
+
throw error;
|
|
532
|
+
}
|
|
466
533
|
}
|
|
467
534
|
async navigate(url) {
|
|
468
|
-
|
|
469
|
-
|
|
470
|
-
|
|
471
|
-
|
|
472
|
-
|
|
535
|
+
try {
|
|
536
|
+
const invocation = await this.callTool('navigate', [
|
|
537
|
+
{
|
|
538
|
+
names: ['navigate_page', 'navigate_to_url'],
|
|
539
|
+
buildArgs: (tool) => withNavigateArgs(tool, url, this.timeoutMs),
|
|
540
|
+
},
|
|
541
|
+
{
|
|
542
|
+
names: ['new_page', 'create_new_tab'],
|
|
543
|
+
buildArgs: (tool) => withOpenArgs(tool, url),
|
|
544
|
+
},
|
|
545
|
+
], {});
|
|
546
|
+
const output = this.outputFromInvocation('navigate', invocation);
|
|
547
|
+
return this.ensurePageContentInOutput('navigate', invocation, output, url);
|
|
548
|
+
}
|
|
549
|
+
catch (error) {
|
|
550
|
+
const recovered = await this.recoverPageContentAfterTimeout('navigate', url, error);
|
|
551
|
+
if (recovered) {
|
|
552
|
+
return recovered;
|
|
553
|
+
}
|
|
554
|
+
throw error;
|
|
555
|
+
}
|
|
556
|
+
}
|
|
557
|
+
async close(id) {
|
|
558
|
+
const invocation = await this.callTool('close', [
|
|
473
559
|
{
|
|
474
|
-
names: ['
|
|
475
|
-
buildArgs: (tool) =>
|
|
560
|
+
names: ['close_page', 'close_tab'],
|
|
561
|
+
buildArgs: (tool) => withPageArgs(tool, id),
|
|
476
562
|
},
|
|
477
563
|
], {});
|
|
478
|
-
return this.outputFromInvocation('
|
|
564
|
+
return this.outputFromInvocation('close', invocation);
|
|
479
565
|
}
|
|
480
566
|
async focus(id) {
|
|
481
567
|
const invocation = await this.callTool('focus', [
|
|
482
568
|
{
|
|
483
|
-
names: ['
|
|
569
|
+
names: ['switch_to_page', 'switch_to_tab', 'select_page', 'focus_tab'],
|
|
484
570
|
buildArgs: (tool) => withPageArgs(tool, id),
|
|
485
571
|
},
|
|
486
572
|
], {});
|
|
487
573
|
return this.outputFromInvocation('focus', invocation);
|
|
488
574
|
}
|
|
489
|
-
async close(id) {
|
|
490
|
-
const invocation = await this.callTool('close', [
|
|
491
|
-
{
|
|
492
|
-
names: ['close_page', 'close_tab'],
|
|
493
|
-
buildArgs: (tool) => withPageArgs(tool, id),
|
|
494
|
-
},
|
|
495
|
-
], {});
|
|
496
|
-
return this.outputFromInvocation('close', invocation);
|
|
497
|
-
}
|
|
498
575
|
async snapshot(options) {
|
|
499
576
|
const wantsAria = options.format === 'aria' || options.interactive || Boolean(options.selector) || Boolean(options.frame);
|
|
500
|
-
if (!wantsAria) {
|
|
501
|
-
const result = await this.connection.getSnapshot();
|
|
502
|
-
return {
|
|
503
|
-
ok: true,
|
|
504
|
-
command: 'snapshot',
|
|
505
|
-
profile: this.profile,
|
|
506
|
-
mode: this.mode(),
|
|
507
|
-
ignoredCompatibilityOptions: this.ignoredCompatibilityOptions,
|
|
508
|
-
format: options.format,
|
|
509
|
-
url: result.url,
|
|
510
|
-
title: result.title,
|
|
511
|
-
snapshot: limitText(result.snapshot, options.limit),
|
|
512
|
-
};
|
|
513
|
-
}
|
|
514
577
|
const invocation = await this.callTool('snapshot', [
|
|
515
578
|
{
|
|
516
|
-
names: ['
|
|
517
|
-
buildArgs: (tool) => withSnapshotArgs(tool, options),
|
|
518
|
-
},
|
|
519
|
-
{
|
|
520
|
-
names: ['take_snapshot', 'get_page_content'],
|
|
579
|
+
names: ['take_snapshot'],
|
|
521
580
|
buildArgs: (tool) => withSnapshotArgs(tool, options),
|
|
522
581
|
},
|
|
523
582
|
], {});
|
|
@@ -553,38 +612,6 @@ class BrowserCliContext {
|
|
|
553
612
|
raw: normalizeToolResult(invocation.result),
|
|
554
613
|
};
|
|
555
614
|
}
|
|
556
|
-
async pdf(outputPath) {
|
|
557
|
-
const invocation = await this.callTool('pdf', [{ names: ['pdf', 'generate_pdf'] }], {});
|
|
558
|
-
const savedPath = maybeWriteBinaryOutput(invocation.result, outputPath);
|
|
559
|
-
return {
|
|
560
|
-
ok: true,
|
|
561
|
-
command: 'pdf',
|
|
562
|
-
profile: this.profile,
|
|
563
|
-
mode: this.mode(),
|
|
564
|
-
ignoredCompatibilityOptions: this.ignoredCompatibilityOptions,
|
|
565
|
-
tool: invocation.tool,
|
|
566
|
-
outputPath: savedPath || outputPath,
|
|
567
|
-
raw: normalizeToolResult(invocation.result),
|
|
568
|
-
};
|
|
569
|
-
}
|
|
570
|
-
async consoleMessages(options) {
|
|
571
|
-
const invocation = await this.callTool('console', [
|
|
572
|
-
{
|
|
573
|
-
names: ['list_console_messages'],
|
|
574
|
-
buildArgs: (tool) => withConsoleArgs(tool, options),
|
|
575
|
-
},
|
|
576
|
-
], {});
|
|
577
|
-
return {
|
|
578
|
-
ok: true,
|
|
579
|
-
command: options.level === 'error' ? 'errors' : 'console',
|
|
580
|
-
profile: this.profile,
|
|
581
|
-
mode: this.mode(),
|
|
582
|
-
ignoredCompatibilityOptions: this.ignoredCompatibilityOptions,
|
|
583
|
-
tool: invocation.tool,
|
|
584
|
-
messages: extractMessages(invocation.result),
|
|
585
|
-
raw: normalizeToolResult(invocation.result),
|
|
586
|
-
};
|
|
587
|
-
}
|
|
588
615
|
async requests(options) {
|
|
589
616
|
const invocation = await this.callTool('requests', [
|
|
590
617
|
{
|
|
@@ -638,15 +665,6 @@ class BrowserCliContext {
|
|
|
638
665
|
raw: normalizeToolResult(invocation.result),
|
|
639
666
|
};
|
|
640
667
|
}
|
|
641
|
-
async resize(width, height) {
|
|
642
|
-
const invocation = await this.callTool('resize', [
|
|
643
|
-
{
|
|
644
|
-
names: ['resize_page'],
|
|
645
|
-
buildArgs: (tool) => withResizeArgs(tool, width, height),
|
|
646
|
-
},
|
|
647
|
-
], {});
|
|
648
|
-
return this.outputFromInvocation('resize', invocation);
|
|
649
|
-
}
|
|
650
668
|
async click(ref, options) {
|
|
651
669
|
const invocation = await this.callTool('click', [
|
|
652
670
|
{
|
|
@@ -687,15 +705,6 @@ class BrowserCliContext {
|
|
|
687
705
|
], {});
|
|
688
706
|
return this.outputFromInvocation('hover', invocation);
|
|
689
707
|
}
|
|
690
|
-
async scrollIntoView(ref) {
|
|
691
|
-
const invocation = await this.callTool('scrollintoview', [
|
|
692
|
-
{
|
|
693
|
-
names: ['scroll_into_view', 'scrollintoview'],
|
|
694
|
-
buildArgs: (tool) => withRefArgs(tool, ref),
|
|
695
|
-
},
|
|
696
|
-
], {});
|
|
697
|
-
return this.outputFromInvocation('scrollintoview', invocation);
|
|
698
|
-
}
|
|
699
708
|
async drag(source, target) {
|
|
700
709
|
const invocation = await this.callTool('drag', [
|
|
701
710
|
{
|
|
@@ -718,37 +727,6 @@ class BrowserCliContext {
|
|
|
718
727
|
], {});
|
|
719
728
|
return this.outputFromInvocation('select', invocation);
|
|
720
729
|
}
|
|
721
|
-
async download(ref, filename) {
|
|
722
|
-
const invocation = await this.callTool('download', [
|
|
723
|
-
{
|
|
724
|
-
names: ['download'],
|
|
725
|
-
buildArgs: (tool) => withDownloadArgs(tool, ref, filename),
|
|
726
|
-
},
|
|
727
|
-
{
|
|
728
|
-
names: ['click'],
|
|
729
|
-
buildArgs: (tool) => withRefArgs(tool, ref),
|
|
730
|
-
},
|
|
731
|
-
], {});
|
|
732
|
-
return this.outputFromInvocation('download', invocation);
|
|
733
|
-
}
|
|
734
|
-
async waitForDownload(filename) {
|
|
735
|
-
const invocation = await this.callTool('waitfordownload', [
|
|
736
|
-
{
|
|
737
|
-
names: ['wait_for_download', 'waitfordownload'],
|
|
738
|
-
buildArgs: (tool) => withFilenameArgs(tool, filename),
|
|
739
|
-
},
|
|
740
|
-
], {});
|
|
741
|
-
return this.outputFromInvocation('waitfordownload', invocation);
|
|
742
|
-
}
|
|
743
|
-
async upload(path, ref) {
|
|
744
|
-
const invocation = await this.callTool('upload', [
|
|
745
|
-
{
|
|
746
|
-
names: ['upload_file'],
|
|
747
|
-
buildArgs: (tool) => withUploadArgs(tool, path, ref),
|
|
748
|
-
},
|
|
749
|
-
], {});
|
|
750
|
-
return this.outputFromInvocation('upload', invocation);
|
|
751
|
-
}
|
|
752
730
|
async fillForm(fieldsJson) {
|
|
753
731
|
const fields = parseJsonValue(fieldsJson, '--fields');
|
|
754
732
|
if (!Array.isArray(fields)) {
|
|
@@ -762,16 +740,6 @@ class BrowserCliContext {
|
|
|
762
740
|
], {});
|
|
763
741
|
return this.outputFromInvocation('fill', invocation);
|
|
764
742
|
}
|
|
765
|
-
async dialog(options) {
|
|
766
|
-
const action = options.accept ? 'accept' : options.dismiss ? 'dismiss' : 'accept';
|
|
767
|
-
const invocation = await this.callTool('dialog', [
|
|
768
|
-
{
|
|
769
|
-
names: ['handle_dialog'],
|
|
770
|
-
buildArgs: (tool) => withDialogArgs(tool, action, options.prompt),
|
|
771
|
-
},
|
|
772
|
-
], {});
|
|
773
|
-
return this.outputFromInvocation('dialog', invocation);
|
|
774
|
-
}
|
|
775
743
|
async wait(options) {
|
|
776
744
|
if (options.text && options.text.length > 0) {
|
|
777
745
|
const invocation = await this.callTool('wait', [
|
|
@@ -804,36 +772,36 @@ class BrowserCliContext {
|
|
|
804
772
|
], {});
|
|
805
773
|
return this.outputFromInvocation('evaluate', invocation);
|
|
806
774
|
}
|
|
807
|
-
async
|
|
808
|
-
const invocation = await this.callTool('
|
|
809
|
-
{
|
|
810
|
-
names: ['highlight'],
|
|
811
|
-
buildArgs: (tool) => withRefArgs(tool, ref),
|
|
812
|
-
},
|
|
775
|
+
async resize(width, height) {
|
|
776
|
+
const invocation = await this.callTool('resize', [
|
|
813
777
|
{
|
|
814
|
-
names: ['
|
|
815
|
-
buildArgs: (tool) =>
|
|
778
|
+
names: ['resize_page'],
|
|
779
|
+
buildArgs: (tool) => withResizeArgs(tool, width, height),
|
|
816
780
|
},
|
|
817
781
|
], {});
|
|
818
|
-
return this.outputFromInvocation('
|
|
782
|
+
return this.outputFromInvocation('resize', invocation);
|
|
819
783
|
}
|
|
820
|
-
async
|
|
821
|
-
const invocation = await this.callTool('
|
|
784
|
+
async upload(ref, path) {
|
|
785
|
+
const invocation = await this.callTool('upload', [
|
|
822
786
|
{
|
|
823
|
-
names: ['
|
|
824
|
-
buildArgs: (tool) =>
|
|
787
|
+
names: ['upload_file', 'file_upload'],
|
|
788
|
+
buildArgs: (tool) => withUploadArgs(tool, ref, path),
|
|
825
789
|
},
|
|
826
790
|
], {});
|
|
827
|
-
return this.outputFromInvocation('
|
|
791
|
+
return this.outputFromInvocation('upload', invocation);
|
|
828
792
|
}
|
|
829
|
-
async
|
|
830
|
-
|
|
793
|
+
async dialog(options) {
|
|
794
|
+
if (options.accept && options.dismiss) {
|
|
795
|
+
throw new Error('dialog supports either --accept or --dismiss, not both');
|
|
796
|
+
}
|
|
797
|
+
const accept = options.dismiss ? false : true;
|
|
798
|
+
const invocation = await this.callTool('dialog', [
|
|
831
799
|
{
|
|
832
|
-
names: ['
|
|
833
|
-
buildArgs: (tool) =>
|
|
800
|
+
names: ['handle_dialog'],
|
|
801
|
+
buildArgs: (tool) => withDialogArgs(tool, accept, options.promptText),
|
|
834
802
|
},
|
|
835
803
|
], {});
|
|
836
|
-
return this.outputFromInvocation('
|
|
804
|
+
return this.outputFromInvocation('dialog', invocation);
|
|
837
805
|
}
|
|
838
806
|
async callGenericCommand(commandName, candidates, canonicalArgs) {
|
|
839
807
|
const invocation = await this.callTool(commandName, candidates, canonicalArgs);
|
|
@@ -842,6 +810,7 @@ class BrowserCliContext {
|
|
|
842
810
|
async callTool(commandName, candidates, canonicalArgs) {
|
|
843
811
|
await this.ensureToolsLoaded();
|
|
844
812
|
const available = new Map(this.tools.map((tool) => [normalizeName(tool.name), tool]));
|
|
813
|
+
const compatibilityErrors = [];
|
|
845
814
|
for (const candidate of candidates) {
|
|
846
815
|
for (const candidateName of candidate.names) {
|
|
847
816
|
const tool = available.get(normalizeName(candidateName));
|
|
@@ -851,44 +820,430 @@ class BrowserCliContext {
|
|
|
851
820
|
const args = candidate.buildArgs
|
|
852
821
|
? candidate.buildArgs(tool)
|
|
853
822
|
: withCanonicalArgs(tool, canonicalArgs);
|
|
854
|
-
|
|
855
|
-
|
|
823
|
+
// Inject --page-id into every tool call that accepts pageId/tabId,
|
|
824
|
+
// so the agent doesn't need to use focus/tab select (which disrupts
|
|
825
|
+
// the user's active browser tab).
|
|
826
|
+
if (this.pageId !== undefined) {
|
|
827
|
+
const pageKey = hasProperty(tool, 'pageId', 'tabId');
|
|
828
|
+
if (pageKey && !(pageKey in args)) {
|
|
829
|
+
args[pageKey] = this.pageId;
|
|
830
|
+
}
|
|
831
|
+
}
|
|
832
|
+
if (shouldRequestPageStateForCommand(commandName)) {
|
|
833
|
+
const pageStateKey = hasProperty(tool, 'pageStateFormat', 'page_state_format');
|
|
834
|
+
if (pageStateKey && !('pageStateFormat' in args) && !('page_state_format' in args)) {
|
|
835
|
+
args[pageStateKey] = 'markdown';
|
|
836
|
+
}
|
|
837
|
+
}
|
|
838
|
+
try {
|
|
839
|
+
const result = await this.connection.callTool(tool.name, args, this.timeoutMs);
|
|
840
|
+
return { tool: tool.name, args, result: result };
|
|
841
|
+
}
|
|
842
|
+
catch (error) {
|
|
843
|
+
if (!isToolArgumentCompatibilityError(error)) {
|
|
844
|
+
throw error;
|
|
845
|
+
}
|
|
846
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
847
|
+
compatibilityErrors.push(`${tool.name}: ${message}`);
|
|
848
|
+
}
|
|
856
849
|
}
|
|
857
850
|
}
|
|
858
851
|
const requested = candidates.flatMap((candidate) => candidate.names);
|
|
859
|
-
|
|
852
|
+
const compatibilityHint = compatibilityErrors.length > 0
|
|
853
|
+
? ` Compatibility errors: ${compatibilityErrors.join(' | ')}`
|
|
854
|
+
: '';
|
|
855
|
+
throw new Error(`No compatible browser tool found for "${commandName}". Tried ${requested.join(', ')}. Available tools: ${this.tools.map((tool) => tool.name).join(', ')}.${compatibilityHint}`);
|
|
860
856
|
}
|
|
861
|
-
async ensureToolsLoaded() {
|
|
857
|
+
async ensureToolsLoaded(timeoutMs) {
|
|
862
858
|
if (this.toolsLoaded && this.tools.length > 0) {
|
|
863
859
|
return;
|
|
864
860
|
}
|
|
865
|
-
|
|
866
|
-
|
|
867
|
-
|
|
868
|
-
}
|
|
869
|
-
catch {
|
|
870
|
-
this.tools = await this.connection.waitForToolsUpdate(1_000);
|
|
871
|
-
}
|
|
861
|
+
const effectiveTimeout = timeoutMs ?? this.timeoutMs;
|
|
862
|
+
try {
|
|
863
|
+
this.tools = await this.connection.refreshTools(effectiveTimeout);
|
|
872
864
|
}
|
|
873
|
-
|
|
865
|
+
catch {
|
|
866
|
+
// Refresh timed out or failed — fall back to cached tools or a short
|
|
867
|
+
// passive wait so the status command is not blocked.
|
|
874
868
|
this.tools = this.connection.getTools();
|
|
869
|
+
if (this.tools.length === 0) {
|
|
870
|
+
if (this.connection instanceof ExtensionConnection) {
|
|
871
|
+
this.tools = await this.connection.waitForToolsUpdate(1_000);
|
|
872
|
+
}
|
|
873
|
+
}
|
|
875
874
|
}
|
|
876
875
|
this.toolsLoaded = true;
|
|
877
876
|
}
|
|
878
877
|
outputFromInvocation(commandName, invocation) {
|
|
878
|
+
const pageContent = firstText(invocation.result);
|
|
879
879
|
return {
|
|
880
880
|
ok: !invocation.result.isError,
|
|
881
881
|
command: commandName,
|
|
882
882
|
profile: this.profile,
|
|
883
883
|
mode: this.mode(),
|
|
884
|
+
sessionId: this.currentSessionId(),
|
|
885
|
+
requestedSessionId: this.requestedSessionId,
|
|
884
886
|
ignoredCompatibilityOptions: this.ignoredCompatibilityOptions,
|
|
885
887
|
tool: invocation.tool,
|
|
888
|
+
...(looksLikePageContentText(pageContent) ? { pageContent } : {}),
|
|
886
889
|
raw: normalizeToolResult(invocation.result),
|
|
887
890
|
};
|
|
888
891
|
}
|
|
892
|
+
async ensurePageContentInOutput(commandName, invocation, output, targetUrl) {
|
|
893
|
+
if (!output.ok) {
|
|
894
|
+
return output;
|
|
895
|
+
}
|
|
896
|
+
if (!hasExplicitPageContext(invocation.args)) {
|
|
897
|
+
return output;
|
|
898
|
+
}
|
|
899
|
+
const currentText = firstText(invocation.result);
|
|
900
|
+
if (looksLikePageContentText(currentText) && !isLikelyIncompletePageContent(currentText, targetUrl)) {
|
|
901
|
+
return output;
|
|
902
|
+
}
|
|
903
|
+
const targetPageId = extractPageIdFromResult(invocation.result) ?? this.pageId;
|
|
904
|
+
if (targetPageId === undefined) {
|
|
905
|
+
return output;
|
|
906
|
+
}
|
|
907
|
+
const fallback = await this.takeMarkdownSnapshotForTarget(targetPageId, targetUrl);
|
|
908
|
+
if (!fallback.content) {
|
|
909
|
+
return output;
|
|
910
|
+
}
|
|
911
|
+
return {
|
|
912
|
+
...output,
|
|
913
|
+
pageContent: fallback.content,
|
|
914
|
+
pageContentSource: 'take_snapshot',
|
|
915
|
+
...(fallback.pageId !== targetPageId ? { pageId: fallback.pageId } : {}),
|
|
916
|
+
};
|
|
917
|
+
}
|
|
918
|
+
async recoverPageContentAfterTimeout(commandName, url, error) {
|
|
919
|
+
if (!isTimeoutError(error)) {
|
|
920
|
+
return undefined;
|
|
921
|
+
}
|
|
922
|
+
if (this.pageId === undefined) {
|
|
923
|
+
return undefined;
|
|
924
|
+
}
|
|
925
|
+
const recoveryTimeoutMs = Math.max(this.timeoutMs, 10_000);
|
|
926
|
+
let targetPageId = this.pageId;
|
|
927
|
+
if (targetPageId === undefined) {
|
|
928
|
+
return undefined;
|
|
929
|
+
}
|
|
930
|
+
const fallback = await this.takeMarkdownSnapshotForTarget(targetPageId, url, recoveryTimeoutMs);
|
|
931
|
+
if (!fallback.content) {
|
|
932
|
+
return undefined;
|
|
933
|
+
}
|
|
934
|
+
return {
|
|
935
|
+
ok: true,
|
|
936
|
+
command: commandName,
|
|
937
|
+
profile: this.profile,
|
|
938
|
+
mode: this.mode(),
|
|
939
|
+
sessionId: this.currentSessionId(),
|
|
940
|
+
requestedSessionId: this.requestedSessionId,
|
|
941
|
+
ignoredCompatibilityOptions: this.ignoredCompatibilityOptions,
|
|
942
|
+
tool: 'take_snapshot',
|
|
943
|
+
recoveredFromTimeout: true,
|
|
944
|
+
pageId: fallback.pageId,
|
|
945
|
+
url,
|
|
946
|
+
pageContent: fallback.content,
|
|
947
|
+
raw: {
|
|
948
|
+
recovery: 'timeout_fallback',
|
|
949
|
+
},
|
|
950
|
+
};
|
|
951
|
+
}
|
|
952
|
+
async takeMarkdownSnapshotForTarget(initialPageId, targetUrl, timeoutMs = this.timeoutMs) {
|
|
953
|
+
let latest;
|
|
954
|
+
let pageId = initialPageId;
|
|
955
|
+
const recoveryTimeoutMs = Math.max(timeoutMs, 10_000);
|
|
956
|
+
const attempts = 8;
|
|
957
|
+
for (let attempt = 0; attempt < attempts; attempt += 1) {
|
|
958
|
+
latest = await this.takeMarkdownSnapshotForPage(pageId, recoveryTimeoutMs);
|
|
959
|
+
if (latest && !isLikelyIncompletePageContent(latest, targetUrl)) {
|
|
960
|
+
return { content: latest, pageId };
|
|
961
|
+
}
|
|
962
|
+
if (targetUrl) {
|
|
963
|
+
const pages = await this.listPagesForRecovery(recoveryTimeoutMs);
|
|
964
|
+
const matched = findBestPageMatchByUrl(pages, targetUrl);
|
|
965
|
+
if (matched) {
|
|
966
|
+
pageId = matched.id;
|
|
967
|
+
}
|
|
968
|
+
}
|
|
969
|
+
if (attempt < attempts - 1) {
|
|
970
|
+
await delay(750);
|
|
971
|
+
}
|
|
972
|
+
}
|
|
973
|
+
return { content: latest, pageId };
|
|
974
|
+
}
|
|
975
|
+
async listPagesForRecovery(timeoutMs) {
|
|
976
|
+
try {
|
|
977
|
+
await this.ensureToolsLoaded(timeoutMs);
|
|
978
|
+
const listTool = this.tools.find((tool) => {
|
|
979
|
+
const normalized = normalizeName(tool.name);
|
|
980
|
+
return normalized === 'list_pages' || normalized === 'get_tabs';
|
|
981
|
+
});
|
|
982
|
+
if (!listTool) {
|
|
983
|
+
return [];
|
|
984
|
+
}
|
|
985
|
+
const result = await this.connection.callTool(listTool.name, {}, timeoutMs);
|
|
986
|
+
return extractPages(result);
|
|
987
|
+
}
|
|
988
|
+
catch {
|
|
989
|
+
return [];
|
|
990
|
+
}
|
|
991
|
+
}
|
|
992
|
+
async takeMarkdownSnapshotForPage(pageId, timeoutMs = this.timeoutMs) {
|
|
993
|
+
await this.ensureToolsLoaded();
|
|
994
|
+
const snapshotTool = this.tools.find((tool) => normalizeName(tool.name) === 'take_snapshot');
|
|
995
|
+
if (!snapshotTool) {
|
|
996
|
+
return undefined;
|
|
997
|
+
}
|
|
998
|
+
const args = {};
|
|
999
|
+
const pageKey = hasProperty(snapshotTool, 'pageId', 'tabId');
|
|
1000
|
+
if (pageKey) {
|
|
1001
|
+
args[pageKey] = pageId;
|
|
1002
|
+
}
|
|
1003
|
+
const formatKey = hasProperty(snapshotTool, 'format', 'pageStateFormat', 'page_state_format');
|
|
1004
|
+
if (formatKey) {
|
|
1005
|
+
args[formatKey] = 'markdown';
|
|
1006
|
+
}
|
|
1007
|
+
try {
|
|
1008
|
+
const result = await this.connection.callTool(snapshotTool.name, args, timeoutMs);
|
|
1009
|
+
const text = firstText(result);
|
|
1010
|
+
if (!text) {
|
|
1011
|
+
return undefined;
|
|
1012
|
+
}
|
|
1013
|
+
const trimmed = text.trim();
|
|
1014
|
+
if (/^Error:/i.test(trimmed) || /Failed to get valid tab/i.test(trimmed)) {
|
|
1015
|
+
return undefined;
|
|
1016
|
+
}
|
|
1017
|
+
return text;
|
|
1018
|
+
}
|
|
1019
|
+
catch {
|
|
1020
|
+
return undefined;
|
|
1021
|
+
}
|
|
1022
|
+
}
|
|
889
1023
|
mode() {
|
|
1024
|
+
if (this.devtoolsOnly) {
|
|
1025
|
+
return 'devtools';
|
|
1026
|
+
}
|
|
890
1027
|
return this.remoteUuid ? 'remote' : 'local';
|
|
891
1028
|
}
|
|
1029
|
+
currentSessionId() {
|
|
1030
|
+
if (this.devtoolsOnly) {
|
|
1031
|
+
return undefined;
|
|
1032
|
+
}
|
|
1033
|
+
if (this.remoteUuid) {
|
|
1034
|
+
return this.remoteUuid;
|
|
1035
|
+
}
|
|
1036
|
+
const connectedSessionIds = new Set(this.sessions
|
|
1037
|
+
.filter((session) => session.connected)
|
|
1038
|
+
.map((session) => session.sessionId));
|
|
1039
|
+
if (this.requestedSessionId && connectedSessionIds.has(this.requestedSessionId)) {
|
|
1040
|
+
return this.requestedSessionId;
|
|
1041
|
+
}
|
|
1042
|
+
return this.sessions.find((session) => session.connected)?.sessionId;
|
|
1043
|
+
}
|
|
1044
|
+
isBackendConnected() {
|
|
1045
|
+
if (this.connection instanceof ExtensionConnection) {
|
|
1046
|
+
return this.connection.isExtensionConnected();
|
|
1047
|
+
}
|
|
1048
|
+
return this.connection.isAvailable();
|
|
1049
|
+
}
|
|
1050
|
+
getConnectionErrorMessage() {
|
|
1051
|
+
if (this.connection instanceof ExtensionConnection) {
|
|
1052
|
+
return this.connection.getConnectionErrorMessage();
|
|
1053
|
+
}
|
|
1054
|
+
return this.connection.getUnavailableReason() || 'chrome-devtools backend unavailable';
|
|
1055
|
+
}
|
|
1056
|
+
outputMode() {
|
|
1057
|
+
return this.mode();
|
|
1058
|
+
}
|
|
1059
|
+
outputContext() {
|
|
1060
|
+
return {
|
|
1061
|
+
profile: this.profile,
|
|
1062
|
+
mode: this.mode(),
|
|
1063
|
+
sessionId: this.currentSessionId(),
|
|
1064
|
+
requestedSessionId: this.requestedSessionId,
|
|
1065
|
+
ignoredCompatibilityOptions: this.ignoredCompatibilityOptions,
|
|
1066
|
+
};
|
|
1067
|
+
}
|
|
1068
|
+
}
|
|
1069
|
+
function shouldRequestPageStateForCommand(commandName) {
|
|
1070
|
+
return new Set([
|
|
1071
|
+
'open',
|
|
1072
|
+
'navigate',
|
|
1073
|
+
'click',
|
|
1074
|
+
'type',
|
|
1075
|
+
'press',
|
|
1076
|
+
'hover',
|
|
1077
|
+
'drag',
|
|
1078
|
+
'select',
|
|
1079
|
+
'fill',
|
|
1080
|
+
]).has(commandName);
|
|
1081
|
+
}
|
|
1082
|
+
function looksLikePageContentText(text) {
|
|
1083
|
+
const trimmed = text.trim();
|
|
1084
|
+
if (!trimmed) {
|
|
1085
|
+
return false;
|
|
1086
|
+
}
|
|
1087
|
+
if (/Error retrieving page content/i.test(trimmed) || /page content extraction failed/i.test(trimmed)) {
|
|
1088
|
+
return false;
|
|
1089
|
+
}
|
|
1090
|
+
return /```(?:markdown|text|html)/i.test(trimmed) || /Page State Format:/i.test(trimmed);
|
|
1091
|
+
}
|
|
1092
|
+
function hasExplicitPageContext(args) {
|
|
1093
|
+
const candidates = [args.pageId, args.tabId, args.page_id, args.tab_id];
|
|
1094
|
+
return candidates.some((candidate) => {
|
|
1095
|
+
if (typeof candidate === 'number' && Number.isFinite(candidate)) {
|
|
1096
|
+
return true;
|
|
1097
|
+
}
|
|
1098
|
+
if (typeof candidate === 'string') {
|
|
1099
|
+
return /^\d+$/.test(candidate.trim());
|
|
1100
|
+
}
|
|
1101
|
+
return false;
|
|
1102
|
+
});
|
|
1103
|
+
}
|
|
1104
|
+
function shouldAttachImplicitPageContent(commandName, args) {
|
|
1105
|
+
if (!hasExplicitPageContext(args)) {
|
|
1106
|
+
return false;
|
|
1107
|
+
}
|
|
1108
|
+
return commandName !== 'close';
|
|
1109
|
+
}
|
|
1110
|
+
function stripImplicitPageContent(result) {
|
|
1111
|
+
const firstTextIndex = result.content.findIndex((entry) => entry.type === 'text');
|
|
1112
|
+
if (firstTextIndex < 0) {
|
|
1113
|
+
return result;
|
|
1114
|
+
}
|
|
1115
|
+
const firstTextEntry = result.content[firstTextIndex];
|
|
1116
|
+
if (!('text' in firstTextEntry) || !looksLikePageContentText(firstTextEntry.text)) {
|
|
1117
|
+
return result;
|
|
1118
|
+
}
|
|
1119
|
+
return {
|
|
1120
|
+
...result,
|
|
1121
|
+
content: result.content.filter((_, index) => index !== firstTextIndex),
|
|
1122
|
+
};
|
|
1123
|
+
}
|
|
1124
|
+
function isLikelyIncompletePageContent(text, targetUrl) {
|
|
1125
|
+
const trimmed = text.trim();
|
|
1126
|
+
if (!trimmed) {
|
|
1127
|
+
return true;
|
|
1128
|
+
}
|
|
1129
|
+
const interactiveRefCount = (trimmed.match(/\[M\d+:/g) || []).length;
|
|
1130
|
+
// Common in early snapshots right after new_page(waitForReady=false):
|
|
1131
|
+
// tab exists but title/url are still empty.
|
|
1132
|
+
if (/Tab ID:[^\n]*\nTitle:\s*\nURL:\s*(?:\n|$)/i.test(trimmed)) {
|
|
1133
|
+
return true;
|
|
1134
|
+
}
|
|
1135
|
+
if (/#\s*Markdown Snapshot:\s*Untitled/i.test(trimmed) && interactiveRefCount < 5) {
|
|
1136
|
+
return true;
|
|
1137
|
+
}
|
|
1138
|
+
if (targetUrl) {
|
|
1139
|
+
const normalizedTarget = normalizeUrlForMatch(targetUrl);
|
|
1140
|
+
const hasAnyUrl = /\bURL:\s*\S+/i.test(trimmed);
|
|
1141
|
+
const textNormalized = normalizeUrlForMatch(trimmed);
|
|
1142
|
+
if (!hasAnyUrl) {
|
|
1143
|
+
return true;
|
|
1144
|
+
}
|
|
1145
|
+
if (normalizedTarget && !textNormalized.includes(normalizedTarget)) {
|
|
1146
|
+
return true;
|
|
1147
|
+
}
|
|
1148
|
+
}
|
|
1149
|
+
return false;
|
|
1150
|
+
}
|
|
1151
|
+
function extractPageIdFromResult(result) {
|
|
1152
|
+
const direct = firstDefined(result, ['pageId', 'tabId', 'id']);
|
|
1153
|
+
if (typeof direct === 'number' && Number.isFinite(direct)) {
|
|
1154
|
+
return direct;
|
|
1155
|
+
}
|
|
1156
|
+
const parsed = parseResultText(result);
|
|
1157
|
+
if (parsed && typeof parsed === 'object') {
|
|
1158
|
+
const parsedId = firstDefined(parsed, ['pageId', 'tabId', 'id']);
|
|
1159
|
+
if (typeof parsedId === 'number' && Number.isFinite(parsedId)) {
|
|
1160
|
+
return parsedId;
|
|
1161
|
+
}
|
|
1162
|
+
}
|
|
1163
|
+
const text = firstText(result);
|
|
1164
|
+
const createdMatch = /new background page \(ID:\s*(\d+)\)/i.exec(text);
|
|
1165
|
+
if (createdMatch) {
|
|
1166
|
+
return Number.parseInt(createdMatch[1], 10);
|
|
1167
|
+
}
|
|
1168
|
+
const tabMatch = /\bTab ID:\s*(\d+)\b/i.exec(text);
|
|
1169
|
+
if (tabMatch) {
|
|
1170
|
+
return Number.parseInt(tabMatch[1], 10);
|
|
1171
|
+
}
|
|
1172
|
+
const pageMatch = /\bPage ID:\s*(\d+)\b/i.exec(text);
|
|
1173
|
+
if (pageMatch) {
|
|
1174
|
+
return Number.parseInt(pageMatch[1], 10);
|
|
1175
|
+
}
|
|
1176
|
+
return undefined;
|
|
1177
|
+
}
|
|
1178
|
+
function isTimeoutError(error) {
|
|
1179
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
1180
|
+
return /timed out|timeout/i.test(message);
|
|
1181
|
+
}
|
|
1182
|
+
function isToolArgumentCompatibilityError(error) {
|
|
1183
|
+
const message = (error instanceof Error ? error.message : String(error)).toLowerCase();
|
|
1184
|
+
if (!message) {
|
|
1185
|
+
return false;
|
|
1186
|
+
}
|
|
1187
|
+
if (/timed out|timeout|relay connection|connection lost|not connected|no connection|disconnected/.test(message)) {
|
|
1188
|
+
return false;
|
|
1189
|
+
}
|
|
1190
|
+
return /missing required|required (field|property|argument)|invalid argument|invalid args|invalid input|unexpected argument|unexpected property|unknown argument|does not accept|unrecognized (field|property|argument)/.test(message);
|
|
1191
|
+
}
|
|
1192
|
+
function findBestPageMatchByUrl(pages, targetUrl) {
|
|
1193
|
+
const normalizedTarget = normalizeUrlForMatch(targetUrl);
|
|
1194
|
+
let bestExact;
|
|
1195
|
+
let bestPartial;
|
|
1196
|
+
for (const page of pages) {
|
|
1197
|
+
const id = toFinitePageId(page.id);
|
|
1198
|
+
if (id === undefined || typeof page.url !== 'string') {
|
|
1199
|
+
continue;
|
|
1200
|
+
}
|
|
1201
|
+
const normalizedPage = normalizeUrlForMatch(page.url);
|
|
1202
|
+
if (normalizedPage === normalizedTarget) {
|
|
1203
|
+
if (!bestExact || id > bestExact.id) {
|
|
1204
|
+
bestExact = { id, url: page.url };
|
|
1205
|
+
}
|
|
1206
|
+
}
|
|
1207
|
+
if (normalizedPage.includes(normalizedTarget) || normalizedTarget.includes(normalizedPage)) {
|
|
1208
|
+
if (!bestPartial || id > bestPartial.id) {
|
|
1209
|
+
bestPartial = { id, url: page.url };
|
|
1210
|
+
}
|
|
1211
|
+
}
|
|
1212
|
+
}
|
|
1213
|
+
return bestExact ?? bestPartial;
|
|
1214
|
+
}
|
|
1215
|
+
function toFinitePageId(value) {
|
|
1216
|
+
if (typeof value === 'number' && Number.isFinite(value)) {
|
|
1217
|
+
return value;
|
|
1218
|
+
}
|
|
1219
|
+
if (typeof value === 'string') {
|
|
1220
|
+
const parsed = Number.parseInt(value, 10);
|
|
1221
|
+
if (Number.isFinite(parsed)) {
|
|
1222
|
+
return parsed;
|
|
1223
|
+
}
|
|
1224
|
+
}
|
|
1225
|
+
return undefined;
|
|
1226
|
+
}
|
|
1227
|
+
function normalizeUrlForMatch(value) {
|
|
1228
|
+
const trimmed = value.trim().replace(/\/$/, '');
|
|
1229
|
+
if (!trimmed) {
|
|
1230
|
+
return trimmed;
|
|
1231
|
+
}
|
|
1232
|
+
try {
|
|
1233
|
+
const parsed = new URL(trimmed);
|
|
1234
|
+
return safeDecode(`${parsed.origin}${parsed.pathname}${parsed.search}`.replace(/\/$/, ''));
|
|
1235
|
+
}
|
|
1236
|
+
catch {
|
|
1237
|
+
return safeDecode(trimmed);
|
|
1238
|
+
}
|
|
1239
|
+
}
|
|
1240
|
+
function safeDecode(value) {
|
|
1241
|
+
try {
|
|
1242
|
+
return decodeURIComponent(value);
|
|
1243
|
+
}
|
|
1244
|
+
catch {
|
|
1245
|
+
return value;
|
|
1246
|
+
}
|
|
892
1247
|
}
|
|
893
1248
|
function emitOutput(asJson, data, humanOutput) {
|
|
894
1249
|
if (asJson) {
|
|
@@ -898,13 +1253,17 @@ function emitOutput(asJson, data, humanOutput) {
|
|
|
898
1253
|
console.log(humanOutput);
|
|
899
1254
|
}
|
|
900
1255
|
function emitError(asJson, commandName, ctx, error) {
|
|
901
|
-
|
|
1256
|
+
let message = error instanceof Error ? error.message : String(error);
|
|
1257
|
+
// Improve guidance when the extension requires a pageId that wasn't provided
|
|
1258
|
+
if (/\bpageId\b/i.test(message) && /\bmissing\b|\brequired\b/i.test(message)) {
|
|
1259
|
+
message += '\nHint: use `tabs` to list pages, then pass --page-id <id> to target a specific tab.';
|
|
1260
|
+
}
|
|
902
1261
|
if (asJson) {
|
|
1262
|
+
const context = ctx.outputContext();
|
|
903
1263
|
console.log(JSON.stringify({
|
|
904
1264
|
ok: false,
|
|
905
1265
|
command: commandName,
|
|
906
|
-
|
|
907
|
-
mode: 'local',
|
|
1266
|
+
...context,
|
|
908
1267
|
error: message,
|
|
909
1268
|
}, null, 2));
|
|
910
1269
|
return;
|
|
@@ -919,19 +1278,36 @@ function formatHumanOutput(commandName, output) {
|
|
|
919
1278
|
return [
|
|
920
1279
|
`Profile: ${String(output.profile)}`,
|
|
921
1280
|
`Mode: ${String(output.mode)}`,
|
|
1281
|
+
output.sessionId ? `Session: ${String(output.sessionId)}` : null,
|
|
1282
|
+
output.requestedSessionId && output.requestedSessionId !== output.sessionId ? `Requested session: ${String(output.requestedSessionId)}` : null,
|
|
922
1283
|
`Relay connected: ${boolText(output.relayConnected)}`,
|
|
923
1284
|
`Extension connected: ${boolText(output.extensionConnected)}`,
|
|
924
1285
|
`Managed lifecycle: ${boolText(output.managedLifecycle)}`,
|
|
925
1286
|
output.toolCount !== undefined ? `Tools: ${String(output.toolCount)}` : null,
|
|
926
1287
|
output.note ? String(output.note) : null,
|
|
927
1288
|
].filter(Boolean).join('\n');
|
|
1289
|
+
case 'sessions': {
|
|
1290
|
+
const sessions = Array.isArray(output.sessions) ? output.sessions : [];
|
|
1291
|
+
if (sessions.length === 0) {
|
|
1292
|
+
return 'No browser sessions connected';
|
|
1293
|
+
}
|
|
1294
|
+
return sessions.map((session, index) => {
|
|
1295
|
+
const selected = output.sessionId === session.sessionId ? ' [selected]' : '';
|
|
1296
|
+
const connected = session.connected ? 'connected' : 'disconnected';
|
|
1297
|
+
const tools = session.toolCount !== undefined ? ` tools=${session.toolCount}` : '';
|
|
1298
|
+
return `${index + 1}. ${session.sessionId}${selected} - ${connected}${tools}`;
|
|
1299
|
+
}).join('\n');
|
|
1300
|
+
}
|
|
928
1301
|
case 'tabs':
|
|
929
1302
|
case 'tab': {
|
|
930
1303
|
const pages = Array.isArray(output.pages) ? output.pages : [];
|
|
931
1304
|
if (pages.length === 0) {
|
|
932
1305
|
return firstDefinedText(output.raw, 'No tabs reported by browser');
|
|
933
1306
|
}
|
|
934
|
-
return
|
|
1307
|
+
return [
|
|
1308
|
+
output.sessionId ? `Session: ${String(output.sessionId)}` : null,
|
|
1309
|
+
...pages.map((page, index) => `${index + 1}. ${page.active ? '[active] ' : ''}${page.title || '(untitled)'}${page.url ? ` — ${page.url}` : ''}${page.id !== undefined ? ` [id=${page.id}]` : ''}`),
|
|
1310
|
+
].filter(Boolean).join('\n');
|
|
935
1311
|
}
|
|
936
1312
|
case 'snapshot':
|
|
937
1313
|
return [
|
|
@@ -946,14 +1322,6 @@ function formatHumanOutput(commandName, output) {
|
|
|
946
1322
|
}
|
|
947
1323
|
return requests.map((request, index) => `${index + 1}. ${request.method || 'GET'} ${request.url || '(unknown)'}${request.status !== undefined ? ` [${request.status}]` : ''}${request.requestId ? ` [id=${request.requestId}]` : ''}`).join('\n');
|
|
948
1324
|
}
|
|
949
|
-
case 'console':
|
|
950
|
-
case 'errors': {
|
|
951
|
-
const messages = Array.isArray(output.messages) ? output.messages : [];
|
|
952
|
-
if (messages.length > 0) {
|
|
953
|
-
return messages.map((message) => stringifyJson(message)).join('\n');
|
|
954
|
-
}
|
|
955
|
-
return firstDefinedText(output.raw, 'No console messages reported by browser');
|
|
956
|
-
}
|
|
957
1325
|
case 'responsebody':
|
|
958
1326
|
return String(output.responseBody ?? firstDefinedText(output.raw, ''));
|
|
959
1327
|
default:
|
|
@@ -970,8 +1338,19 @@ function firstDefinedText(value, fallback) {
|
|
|
970
1338
|
if (typeof value === 'string' && value.length > 0) {
|
|
971
1339
|
return value;
|
|
972
1340
|
}
|
|
973
|
-
if (value && typeof value === 'object'
|
|
974
|
-
|
|
1341
|
+
if (value && typeof value === 'object') {
|
|
1342
|
+
// Direct { text: "..." } wrapper
|
|
1343
|
+
if ('text' in value && typeof value.text === 'string') {
|
|
1344
|
+
return value.text;
|
|
1345
|
+
}
|
|
1346
|
+
// MCP tool result: { content: [{ type: 'text', text: '...' }] }
|
|
1347
|
+
const rec = value;
|
|
1348
|
+
if (Array.isArray(rec.content)) {
|
|
1349
|
+
const textItem = rec.content.find((entry) => entry && typeof entry === 'object' && entry.type === 'text');
|
|
1350
|
+
if (textItem && typeof textItem.text === 'string' && textItem.text.length > 0) {
|
|
1351
|
+
return textItem.text;
|
|
1352
|
+
}
|
|
1353
|
+
}
|
|
975
1354
|
}
|
|
976
1355
|
return fallback;
|
|
977
1356
|
}
|
|
@@ -1026,8 +1405,34 @@ function extractPages(result) {
|
|
|
1026
1405
|
}
|
|
1027
1406
|
}
|
|
1028
1407
|
}
|
|
1408
|
+
// Fallback: parse the plain-text format produced by ListPagesTool
|
|
1409
|
+
// e.g. 'Page 123 [ACTIVE]: "Google" - https://www.google.com'
|
|
1410
|
+
const text = firstText(result);
|
|
1411
|
+
if (text) {
|
|
1412
|
+
const textPages = parsePlainTextPages(text);
|
|
1413
|
+
if (textPages.length > 0) {
|
|
1414
|
+
return textPages;
|
|
1415
|
+
}
|
|
1416
|
+
}
|
|
1029
1417
|
return [];
|
|
1030
1418
|
}
|
|
1419
|
+
// Matches ListPagesTool output: 'Page <id>[ [ACTIVE]]: "title" - url'
|
|
1420
|
+
const PAGE_LINE_RE = /^Page\s+(\d+)\s*(\[ACTIVE\])?\s*:\s*"(.*)"\s*-\s*(.+)$/i;
|
|
1421
|
+
function parsePlainTextPages(text) {
|
|
1422
|
+
const pages = [];
|
|
1423
|
+
for (const line of text.split('\n')) {
|
|
1424
|
+
const match = PAGE_LINE_RE.exec(line.trim());
|
|
1425
|
+
if (match) {
|
|
1426
|
+
pages.push({
|
|
1427
|
+
id: Number(match[1]),
|
|
1428
|
+
active: match[2] !== undefined,
|
|
1429
|
+
title: match[3],
|
|
1430
|
+
url: match[4].trim(),
|
|
1431
|
+
});
|
|
1432
|
+
}
|
|
1433
|
+
}
|
|
1434
|
+
return pages;
|
|
1435
|
+
}
|
|
1031
1436
|
function normalizePage(value) {
|
|
1032
1437
|
if (!value || typeof value !== 'object') {
|
|
1033
1438
|
return {};
|
|
@@ -1071,26 +1476,6 @@ function normalizeRequest(value) {
|
|
|
1071
1476
|
status: firstNumber(record, ['status']),
|
|
1072
1477
|
};
|
|
1073
1478
|
}
|
|
1074
|
-
function extractMessages(result) {
|
|
1075
|
-
const direct = extractStructured(result, ['messages', 'consoleMessages']);
|
|
1076
|
-
if (Array.isArray(direct)) {
|
|
1077
|
-
return direct.map((entry) => sanitizeUnknown(entry) ?? null);
|
|
1078
|
-
}
|
|
1079
|
-
const parsed = parseResultText(result);
|
|
1080
|
-
if (Array.isArray(parsed)) {
|
|
1081
|
-
return parsed.map((entry) => sanitizeUnknown(entry) ?? null);
|
|
1082
|
-
}
|
|
1083
|
-
if (parsed && typeof parsed === 'object') {
|
|
1084
|
-
for (const key of ['messages', 'consoleMessages']) {
|
|
1085
|
-
const candidate = parsed[key];
|
|
1086
|
-
if (Array.isArray(candidate)) {
|
|
1087
|
-
return candidate.map((entry) => sanitizeUnknown(entry) ?? null);
|
|
1088
|
-
}
|
|
1089
|
-
}
|
|
1090
|
-
}
|
|
1091
|
-
const text = firstText(result);
|
|
1092
|
-
return text ? [text] : [];
|
|
1093
|
-
}
|
|
1094
1479
|
function extractResponseBody(result) {
|
|
1095
1480
|
const direct = extractStructured(result, ['responseBody', 'body']);
|
|
1096
1481
|
if (typeof direct === 'string') {
|
|
@@ -1218,12 +1603,24 @@ function withUrlArgs(tool, url) {
|
|
|
1218
1603
|
}
|
|
1219
1604
|
return args;
|
|
1220
1605
|
}
|
|
1221
|
-
function
|
|
1606
|
+
function withOpenArgs(tool, url) {
|
|
1607
|
+
const args = withUrlArgs(tool, url);
|
|
1608
|
+
const waitForReadyKey = hasProperty(tool, 'waitForReady');
|
|
1609
|
+
if (waitForReadyKey) {
|
|
1610
|
+
args[waitForReadyKey] = false;
|
|
1611
|
+
}
|
|
1612
|
+
return stripUndefined(args);
|
|
1613
|
+
}
|
|
1614
|
+
function withNavigateArgs(tool, url, timeoutMs) {
|
|
1222
1615
|
const args = withUrlArgs(tool, url);
|
|
1223
1616
|
const pageIdKey = hasProperty(tool, 'pageId', 'tabId');
|
|
1224
1617
|
if (pageIdKey && !(pageIdKey in args)) {
|
|
1225
1618
|
args[pageIdKey] = undefined;
|
|
1226
1619
|
}
|
|
1620
|
+
const timeoutKey = hasProperty(tool, 'timeoutMs', 'timeout');
|
|
1621
|
+
if (timeoutKey && typeof timeoutMs === 'number' && Number.isFinite(timeoutMs)) {
|
|
1622
|
+
args[timeoutKey] = timeoutMs;
|
|
1623
|
+
}
|
|
1227
1624
|
return stripUndefined(args);
|
|
1228
1625
|
}
|
|
1229
1626
|
function withPageArgs(tool, id) {
|
|
@@ -1258,21 +1655,6 @@ function withScreenshotArgs(tool, options) {
|
|
|
1258
1655
|
maybeAssign(args, tool, 'grayscale', options.grayscale || undefined);
|
|
1259
1656
|
return args;
|
|
1260
1657
|
}
|
|
1261
|
-
function withConsoleArgs(tool, options) {
|
|
1262
|
-
const args = {};
|
|
1263
|
-
if (options.level) {
|
|
1264
|
-
if (hasProperty(tool, 'types')) {
|
|
1265
|
-
args.types = [options.level];
|
|
1266
|
-
}
|
|
1267
|
-
else {
|
|
1268
|
-
maybeAssign(args, tool, 'level', options.level);
|
|
1269
|
-
}
|
|
1270
|
-
}
|
|
1271
|
-
if (options.preserve) {
|
|
1272
|
-
maybeAssign(args, tool, 'includePreservedMessages', true);
|
|
1273
|
-
}
|
|
1274
|
-
return args;
|
|
1275
|
-
}
|
|
1276
1658
|
function withRequestListArgs(tool, options) {
|
|
1277
1659
|
const args = {};
|
|
1278
1660
|
if (options.limit) {
|
|
@@ -1358,36 +1740,60 @@ function withSelectArgs(tool, ref, values) {
|
|
|
1358
1740
|
maybeAssign(args, tool, 'values', values);
|
|
1359
1741
|
return args;
|
|
1360
1742
|
}
|
|
1361
|
-
function withDownloadArgs(tool, ref, filename) {
|
|
1362
|
-
const args = withRefArgs(tool, ref);
|
|
1363
|
-
maybeAssign(args, tool, 'filename', filename);
|
|
1364
|
-
maybeAssign(args, tool, 'path', filename);
|
|
1365
|
-
return args;
|
|
1366
|
-
}
|
|
1367
|
-
function withFilenameArgs(tool, filename) {
|
|
1368
|
-
const args = {};
|
|
1369
|
-
maybeAssign(args, tool, 'filename', filename);
|
|
1370
|
-
maybeAssign(args, tool, 'path', filename);
|
|
1371
|
-
return args;
|
|
1372
|
-
}
|
|
1373
|
-
function withUploadArgs(tool, path, ref) {
|
|
1374
|
-
const args = {};
|
|
1375
|
-
maybeAssign(args, tool, 'filePath', resolve(path));
|
|
1376
|
-
if (ref) {
|
|
1377
|
-
Object.assign(args, withRefArgs(tool, ref));
|
|
1378
|
-
}
|
|
1379
|
-
return args;
|
|
1380
|
-
}
|
|
1381
1743
|
function withFillFormArgs(tool, fields) {
|
|
1382
1744
|
const args = {};
|
|
1383
1745
|
maybeAssign(args, tool, 'fields', fields);
|
|
1384
1746
|
maybeAssign(args, tool, 'elements', fields);
|
|
1385
1747
|
return args;
|
|
1386
1748
|
}
|
|
1387
|
-
function
|
|
1749
|
+
function withUploadArgs(tool, ref, path) {
|
|
1750
|
+
const args = withRefArgs(tool, ref);
|
|
1751
|
+
maybeAssign(args, tool, 'filePath', path);
|
|
1752
|
+
maybeAssign(args, tool, 'path', path);
|
|
1753
|
+
if (hasProperty(tool, 'paths')) {
|
|
1754
|
+
args.paths = [path];
|
|
1755
|
+
}
|
|
1756
|
+
const filenameKey = hasProperty(tool, 'filename');
|
|
1757
|
+
const mimeTypeKey = hasProperty(tool, 'mimeType');
|
|
1758
|
+
const contentBase64Key = hasProperty(tool, 'contentBase64');
|
|
1759
|
+
const hasTopLevelPayload = Boolean(filenameKey && mimeTypeKey && contentBase64Key);
|
|
1760
|
+
const fileKey = hasProperty(tool, 'file');
|
|
1761
|
+
const fileSchema = fileKey ? toolProperties(tool)[fileKey] : undefined;
|
|
1762
|
+
const fileProperties = isRecord(fileSchema) && isRecord(fileSchema.properties) ? fileSchema.properties : undefined;
|
|
1763
|
+
const hasNestedPayload = Boolean(fileKey
|
|
1764
|
+
&& fileProperties
|
|
1765
|
+
&& Object.prototype.hasOwnProperty.call(fileProperties, 'filename')
|
|
1766
|
+
&& Object.prototype.hasOwnProperty.call(fileProperties, 'mimeType')
|
|
1767
|
+
&& Object.prototype.hasOwnProperty.call(fileProperties, 'contentBase64'));
|
|
1768
|
+
if (!hasTopLevelPayload && !hasNestedPayload) {
|
|
1769
|
+
return args;
|
|
1770
|
+
}
|
|
1771
|
+
const filePayload = buildUploadFilePayload(path);
|
|
1772
|
+
if (hasTopLevelPayload && filenameKey && mimeTypeKey && contentBase64Key) {
|
|
1773
|
+
args[filenameKey] = filePayload.filename;
|
|
1774
|
+
args[mimeTypeKey] = filePayload.mimeType;
|
|
1775
|
+
args[contentBase64Key] = filePayload.contentBase64;
|
|
1776
|
+
}
|
|
1777
|
+
if (hasNestedPayload && fileKey) {
|
|
1778
|
+
args[fileKey] = filePayload;
|
|
1779
|
+
}
|
|
1780
|
+
return args;
|
|
1781
|
+
}
|
|
1782
|
+
function withDialogArgs(tool, accept, promptText) {
|
|
1388
1783
|
const args = {};
|
|
1389
|
-
|
|
1390
|
-
|
|
1784
|
+
if (hasProperty(tool, 'action')) {
|
|
1785
|
+
args.action = accept ? 'accept' : 'dismiss';
|
|
1786
|
+
}
|
|
1787
|
+
if (hasProperty(tool, 'accept')) {
|
|
1788
|
+
args.accept = accept;
|
|
1789
|
+
}
|
|
1790
|
+
if (promptText !== undefined) {
|
|
1791
|
+
maybeAssign(args, tool, 'promptText', promptText);
|
|
1792
|
+
maybeAssign(args, tool, 'prompt', promptText);
|
|
1793
|
+
if (hasProperty(tool, 'prompt_text')) {
|
|
1794
|
+
args.prompt_text = promptText;
|
|
1795
|
+
}
|
|
1796
|
+
}
|
|
1391
1797
|
return args;
|
|
1392
1798
|
}
|
|
1393
1799
|
function withWaitArgs(tool, text, timeoutMs) {
|
|
@@ -1417,21 +1823,6 @@ function withEvaluateArgs(tool, fn, ref, argsJson) {
|
|
|
1417
1823
|
}
|
|
1418
1824
|
return args;
|
|
1419
1825
|
}
|
|
1420
|
-
function withTraceStartArgs(tool, options) {
|
|
1421
|
-
const args = {};
|
|
1422
|
-
maybeAssign(args, tool, 'reload', options.reload || undefined);
|
|
1423
|
-
if (options.outputPath) {
|
|
1424
|
-
maybeAssign(args, tool, 'filePath', resolve(options.outputPath));
|
|
1425
|
-
}
|
|
1426
|
-
return args;
|
|
1427
|
-
}
|
|
1428
|
-
function withTraceStopArgs(tool, outputPath) {
|
|
1429
|
-
const args = {};
|
|
1430
|
-
if (outputPath) {
|
|
1431
|
-
maybeAssign(args, tool, 'filePath', resolve(outputPath));
|
|
1432
|
-
}
|
|
1433
|
-
return args;
|
|
1434
|
-
}
|
|
1435
1826
|
function maybeAssign(target, tool, property, value) {
|
|
1436
1827
|
if (value === undefined) {
|
|
1437
1828
|
return;
|
|
@@ -1443,6 +1834,24 @@ function maybeAssign(target, tool, property, value) {
|
|
|
1443
1834
|
function stripUndefined(value) {
|
|
1444
1835
|
return Object.fromEntries(Object.entries(value).filter(([, entry]) => entry !== undefined));
|
|
1445
1836
|
}
|
|
1837
|
+
function buildUploadFilePayload(path) {
|
|
1838
|
+
let contentBase64;
|
|
1839
|
+
try {
|
|
1840
|
+
contentBase64 = readFileSync(path).toString('base64');
|
|
1841
|
+
}
|
|
1842
|
+
catch (error) {
|
|
1843
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
1844
|
+
throw new Error(`Failed to read upload file at "${path}": ${message}`);
|
|
1845
|
+
}
|
|
1846
|
+
return {
|
|
1847
|
+
filename: basename(path),
|
|
1848
|
+
mimeType: MIME_TYPE_BY_EXTENSION[extname(path).toLowerCase()] ?? 'application/octet-stream',
|
|
1849
|
+
contentBase64,
|
|
1850
|
+
};
|
|
1851
|
+
}
|
|
1852
|
+
function isRecord(value) {
|
|
1853
|
+
return typeof value === 'object' && value !== null;
|
|
1854
|
+
}
|
|
1446
1855
|
function parseRef(ref) {
|
|
1447
1856
|
const numeric = Number.parseInt(ref, 10);
|
|
1448
1857
|
return {
|