@vibebrowser/mcp 0.2.5 → 0.2.6
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 +47 -0
- package/dist/browser-cli.d.ts.map +1 -1
- package/dist/browser-cli.js +493 -369
- package/dist/browser-cli.js.map +1 -1
- package/dist/cli.js +2 -0
- 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 +108 -20
- package/dist/connection.js.map +1 -1
- package/dist/relay.d.ts +11 -3
- package/dist/relay.d.ts.map +1 -1
- package/dist/relay.js +238 -78
- 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 +185 -4
- package/dist/server.js.map +1 -1
- package/dist/types.d.ts +14 -10
- package/dist/types.d.ts.map +1 -1
- package/docs/eval.md +101 -16
- package/docs/openclaw-local-browser.md +111 -20
- package/openclaw/{vibe-local-browser → vibebrowser}/SKILL.md +22 -1
- package/package.json +5 -2
package/dist/browser-cli.js
CHANGED
|
@@ -4,6 +4,8 @@ import { setTimeout as delay } from 'node:timers/promises';
|
|
|
4
4
|
import { ExtensionConnection } from './connection.js';
|
|
5
5
|
import { DEFAULT_WS_PORT } from './types.js';
|
|
6
6
|
const DEFAULT_TIMEOUT_MS = 30_000;
|
|
7
|
+
/** Short timeout for the `status` command — it should never block for 30 s. */
|
|
8
|
+
const STATUS_TOOLS_TIMEOUT_MS = 2_000;
|
|
7
9
|
const DEFAULT_BROWSER_PROFILE = process.env.VIBE_BROWSER_PROFILE || 'user';
|
|
8
10
|
const DEFAULT_REMOTE_UUID = process.env.VIBE_EXTENSION_UUID || process.env.VIBE_RELAY_UUID;
|
|
9
11
|
const DEFAULT_REMOTE_RELAY_URL = process.env.VIBE_REMOTE_RELAY_URL || process.env.VIBE_RELAY_URL;
|
|
@@ -23,9 +25,12 @@ function buildBrowserCommand(command) {
|
|
|
23
25
|
.option('-p, --port <number>', 'WebSocket port for local relay (agent) connection', String(DEFAULT_WS_PORT))
|
|
24
26
|
.option('-d, --debug', 'Enable debug logging', false)
|
|
25
27
|
.option('-r, --remote <uuid>', 'Connect to a remote extension via public relay (provide the extension UUID)', DEFAULT_REMOTE_UUID)
|
|
28
|
+
.option('-s, --session <id>', 'Target a specific local browser session ID; defaults to the first connected session')
|
|
26
29
|
.option('--relay-url <url>', 'Custom relay server URL', DEFAULT_REMOTE_RELAY_URL)
|
|
27
30
|
.option('--json', 'Emit machine-readable JSON output', false)
|
|
28
|
-
.option('--timeout <ms>', 'Command timeout in milliseconds', String(DEFAULT_TIMEOUT_MS))
|
|
31
|
+
.option('--timeout <ms>', 'Command timeout in milliseconds', String(DEFAULT_TIMEOUT_MS))
|
|
32
|
+
.option('--page-id <id>', 'Target a specific page/tab by its numeric ID (avoids switching the user\'s active tab)')
|
|
33
|
+
.option('--pageId <id>', 'Alias for --page-id');
|
|
29
34
|
}
|
|
30
35
|
function registerBrowserSubcommands(browser) {
|
|
31
36
|
browser
|
|
@@ -61,6 +66,12 @@ function registerBrowserSubcommands(browser) {
|
|
|
61
66
|
note: 'The Vibe browser bridge does not own the browser process, so stop only disconnects this CLI session',
|
|
62
67
|
}));
|
|
63
68
|
});
|
|
69
|
+
browser
|
|
70
|
+
.command('sessions')
|
|
71
|
+
.description('List connected browser sessions')
|
|
72
|
+
.action(async function () {
|
|
73
|
+
await runBrowserCommand(this, 'sessions', false, async (ctx) => ctx.listSessions());
|
|
74
|
+
});
|
|
64
75
|
browser
|
|
65
76
|
.command('tabs')
|
|
66
77
|
.description('List browser tabs/pages')
|
|
@@ -80,7 +91,7 @@ function registerBrowserSubcommands(browser) {
|
|
|
80
91
|
});
|
|
81
92
|
tab
|
|
82
93
|
.command('select <id>')
|
|
83
|
-
.description('
|
|
94
|
+
.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
95
|
.action(async function (id) {
|
|
85
96
|
await runBrowserCommand(this, 'tab select', true, async (ctx) => ctx.focus(id));
|
|
86
97
|
});
|
|
@@ -104,7 +115,7 @@ function registerBrowserSubcommands(browser) {
|
|
|
104
115
|
});
|
|
105
116
|
browser
|
|
106
117
|
.command('focus <id>')
|
|
107
|
-
.description('
|
|
118
|
+
.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
119
|
.action(async function (id) {
|
|
109
120
|
await runBrowserCommand(this, 'focus', true, async (ctx) => ctx.focus(id));
|
|
110
121
|
});
|
|
@@ -156,31 +167,7 @@ function registerBrowserSubcommands(browser) {
|
|
|
156
167
|
grayscale: Boolean(options.grayscale),
|
|
157
168
|
}));
|
|
158
169
|
});
|
|
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
|
-
});
|
|
170
|
+
// NOTE: pdf, console, errors commands removed — no matching browser tools.
|
|
184
171
|
browser
|
|
185
172
|
.command('requests')
|
|
186
173
|
.description('List network requests')
|
|
@@ -205,12 +192,7 @@ function registerBrowserSubcommands(browser) {
|
|
|
205
192
|
: undefined,
|
|
206
193
|
}));
|
|
207
194
|
});
|
|
208
|
-
|
|
209
|
-
.command('resize <width> <height>')
|
|
210
|
-
.description('Resize the browser viewport')
|
|
211
|
-
.action(async function (width, height) {
|
|
212
|
-
await runBrowserCommand(this, 'resize', true, async (ctx) => ctx.resize(parsePositiveInteger(width, 'width'), parsePositiveInteger(height, 'height')));
|
|
213
|
-
});
|
|
195
|
+
// NOTE: resize command removed — no resize_page tool in the extension.
|
|
214
196
|
browser
|
|
215
197
|
.command('click <ref>')
|
|
216
198
|
.description('Click an element by ref/index')
|
|
@@ -237,12 +219,7 @@ function registerBrowserSubcommands(browser) {
|
|
|
237
219
|
.action(async function (ref) {
|
|
238
220
|
await runBrowserCommand(this, 'hover', true, async (ctx) => ctx.hover(ref));
|
|
239
221
|
});
|
|
240
|
-
|
|
241
|
-
.command('scrollintoview <ref>')
|
|
242
|
-
.description('Scroll an element into view when supported')
|
|
243
|
-
.action(async function (ref) {
|
|
244
|
-
await runBrowserCommand(this, 'scrollintoview', true, async (ctx) => ctx.scrollIntoView(ref));
|
|
245
|
-
});
|
|
222
|
+
// NOTE: scrollintoview, download, waitfordownload, upload commands removed — no matching tools.
|
|
246
223
|
browser
|
|
247
224
|
.command('drag <source> <target>')
|
|
248
225
|
.description('Drag one element to another')
|
|
@@ -255,25 +232,6 @@ function registerBrowserSubcommands(browser) {
|
|
|
255
232
|
.action(async function (ref, values) {
|
|
256
233
|
await runBrowserCommand(this, 'select', true, async (ctx) => ctx.select(ref, values));
|
|
257
234
|
});
|
|
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
235
|
browser
|
|
278
236
|
.command('fill')
|
|
279
237
|
.description('Fill a form using JSON field descriptors')
|
|
@@ -281,19 +239,7 @@ function registerBrowserSubcommands(browser) {
|
|
|
281
239
|
.action(async function () {
|
|
282
240
|
await runBrowserCommand(this, 'fill', true, async (ctx, options) => ctx.fillForm(String(options.fields)));
|
|
283
241
|
});
|
|
284
|
-
|
|
285
|
-
.command('dialog')
|
|
286
|
-
.description('Accept or dismiss a browser dialog')
|
|
287
|
-
.option('--accept', 'Accept the dialog', false)
|
|
288
|
-
.option('--dismiss', 'Dismiss the dialog', false)
|
|
289
|
-
.option('--prompt <text>', 'Prompt text to enter')
|
|
290
|
-
.action(async function () {
|
|
291
|
-
await runBrowserCommand(this, 'dialog', true, async (ctx, options) => ctx.dialog({
|
|
292
|
-
accept: Boolean(options.accept),
|
|
293
|
-
dismiss: Boolean(options.dismiss),
|
|
294
|
-
prompt: options.prompt ? String(options.prompt) : undefined,
|
|
295
|
-
}));
|
|
296
|
-
});
|
|
242
|
+
// NOTE: dialog command removed — no handle_dialog tool in the extension.
|
|
297
243
|
browser
|
|
298
244
|
.command('wait')
|
|
299
245
|
.description('Wait for text or a short delay')
|
|
@@ -320,31 +266,8 @@ function registerBrowserSubcommands(browser) {
|
|
|
320
266
|
argsJson: options.args ? String(options.args) : undefined,
|
|
321
267
|
}));
|
|
322
268
|
});
|
|
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
|
-
});
|
|
269
|
+
// NOTE: highlight command removed — no highlight tool; users can use 'hover' directly.
|
|
270
|
+
// NOTE: trace commands removed — no performance_start_trace/performance_stop_trace tools.
|
|
348
271
|
}
|
|
349
272
|
async function runBrowserCommand(command, commandName, requireExtension, handler) {
|
|
350
273
|
const globalOptions = command.optsWithGlobals();
|
|
@@ -353,11 +276,13 @@ async function runBrowserCommand(command, commandName, requireExtension, handler
|
|
|
353
276
|
port: parsePositiveInteger(globalOptions.port, '--port'),
|
|
354
277
|
debug: Boolean(globalOptions.debug),
|
|
355
278
|
remoteUuid: globalOptions.remote,
|
|
279
|
+
sessionId: globalOptions.session,
|
|
356
280
|
relayUrl: globalOptions.relayUrl,
|
|
357
281
|
profile: globalOptions.browserProfile || DEFAULT_BROWSER_PROFILE,
|
|
358
282
|
json: Boolean(globalOptions.json),
|
|
359
283
|
timeoutMs: parsePositiveInteger(globalOptions.timeout, '--timeout'),
|
|
360
284
|
target: globalOptions.target,
|
|
285
|
+
pageId: globalOptions.pageId ? parsePositiveInteger(globalOptions.pageId, '--page-id') : undefined,
|
|
361
286
|
});
|
|
362
287
|
try {
|
|
363
288
|
await ctx.connect();
|
|
@@ -381,17 +306,22 @@ class BrowserCliContext {
|
|
|
381
306
|
json;
|
|
382
307
|
timeoutMs;
|
|
383
308
|
remoteUuid;
|
|
309
|
+
requestedSessionId;
|
|
384
310
|
target;
|
|
311
|
+
pageId;
|
|
385
312
|
toolsLoaded = false;
|
|
386
313
|
tools = [];
|
|
314
|
+
sessions = [];
|
|
387
315
|
ignoredCompatibilityOptions;
|
|
388
316
|
constructor(init) {
|
|
389
|
-
this.connection = new ExtensionConnection(init.port, init.debug, init.remoteUuid ? { uuid: init.remoteUuid, relayUrl: init.relayUrl } : undefined);
|
|
317
|
+
this.connection = new ExtensionConnection(init.port, init.debug, init.remoteUuid ? { uuid: init.remoteUuid, relayUrl: init.relayUrl } : undefined, init.remoteUuid ? undefined : { sessionId: init.sessionId });
|
|
390
318
|
this.profile = init.profile;
|
|
391
319
|
this.json = init.json;
|
|
392
320
|
this.timeoutMs = init.timeoutMs;
|
|
393
321
|
this.remoteUuid = init.remoteUuid;
|
|
322
|
+
this.requestedSessionId = init.sessionId;
|
|
394
323
|
this.target = init.target;
|
|
324
|
+
this.pageId = init.pageId;
|
|
395
325
|
this.ignoredCompatibilityOptions = [];
|
|
396
326
|
if (this.target) {
|
|
397
327
|
this.ignoredCompatibilityOptions.push(`target=${this.target}`);
|
|
@@ -400,6 +330,7 @@ class BrowserCliContext {
|
|
|
400
330
|
async connect() {
|
|
401
331
|
await this.connection.start();
|
|
402
332
|
await delay(100);
|
|
333
|
+
this.sessions = await this.connection.listSessions(1_500).catch(() => this.connection.getSessions());
|
|
403
334
|
await this.connection.waitForToolsUpdate(500);
|
|
404
335
|
}
|
|
405
336
|
async shutdown() {
|
|
@@ -409,20 +340,41 @@ class BrowserCliContext {
|
|
|
409
340
|
if (this.connection.isExtensionConnected()) {
|
|
410
341
|
return;
|
|
411
342
|
}
|
|
343
|
+
this.sessions = await this.connection.listSessions(1_500).catch(() => this.connection.getSessions());
|
|
412
344
|
await this.ensureToolsLoaded();
|
|
413
345
|
if (!this.connection.isExtensionConnected()) {
|
|
414
|
-
throw new Error(
|
|
346
|
+
throw new Error(this.connection.getConnectionErrorMessage());
|
|
415
347
|
}
|
|
416
348
|
}
|
|
349
|
+
async listSessions() {
|
|
350
|
+
this.sessions = await this.connection.listSessions(this.timeoutMs);
|
|
351
|
+
return {
|
|
352
|
+
ok: true,
|
|
353
|
+
command: 'sessions',
|
|
354
|
+
profile: this.profile,
|
|
355
|
+
mode: this.mode(),
|
|
356
|
+
ignoredCompatibilityOptions: this.ignoredCompatibilityOptions,
|
|
357
|
+
sessionId: this.currentSessionId(),
|
|
358
|
+
requestedSessionId: this.requestedSessionId,
|
|
359
|
+
sessions: this.sessions,
|
|
360
|
+
};
|
|
361
|
+
}
|
|
417
362
|
async status() {
|
|
363
|
+
this.sessions = await this.connection.listSessions(STATUS_TOOLS_TIMEOUT_MS).catch(() => this.connection.getSessions());
|
|
418
364
|
if (this.connection.isExtensionConnected()) {
|
|
419
|
-
|
|
365
|
+
// Use a short timeout for status — this is a diagnostic command that
|
|
366
|
+
// should return quickly. Fall back to cached tools if the extension
|
|
367
|
+
// is slow to respond.
|
|
368
|
+
await this.ensureToolsLoaded(STATUS_TOOLS_TIMEOUT_MS);
|
|
420
369
|
}
|
|
421
370
|
return {
|
|
422
371
|
ok: true,
|
|
423
372
|
command: 'status',
|
|
424
373
|
profile: this.profile,
|
|
425
374
|
mode: this.remoteUuid ? 'remote' : 'local',
|
|
375
|
+
sessionId: this.currentSessionId(),
|
|
376
|
+
requestedSessionId: this.requestedSessionId,
|
|
377
|
+
sessions: this.sessions,
|
|
426
378
|
ignoredCompatibilityOptions: this.ignoredCompatibilityOptions,
|
|
427
379
|
relayConnected: this.connection.getStatus() === 'connected',
|
|
428
380
|
extensionConnected: this.connection.isExtensionConnected(),
|
|
@@ -442,6 +394,8 @@ class BrowserCliContext {
|
|
|
442
394
|
command: 'tabs',
|
|
443
395
|
profile: this.profile,
|
|
444
396
|
mode: this.mode(),
|
|
397
|
+
sessionId: this.currentSessionId(),
|
|
398
|
+
requestedSessionId: this.requestedSessionId,
|
|
445
399
|
ignoredCompatibilityOptions: this.ignoredCompatibilityOptions,
|
|
446
400
|
tool: invocation.tool,
|
|
447
401
|
pages,
|
|
@@ -452,72 +406,74 @@ class BrowserCliContext {
|
|
|
452
406
|
if (!url) {
|
|
453
407
|
return this.callGenericCommand('open', [{ names: ['new_page', 'create_new_tab'] }], {});
|
|
454
408
|
}
|
|
455
|
-
|
|
456
|
-
|
|
457
|
-
|
|
458
|
-
|
|
459
|
-
|
|
460
|
-
|
|
461
|
-
|
|
462
|
-
|
|
463
|
-
|
|
464
|
-
|
|
465
|
-
|
|
409
|
+
try {
|
|
410
|
+
const invocation = await this.callTool('open', [
|
|
411
|
+
{
|
|
412
|
+
names: ['new_page', 'create_new_tab'],
|
|
413
|
+
buildArgs: (tool) => withOpenArgs(tool, url),
|
|
414
|
+
},
|
|
415
|
+
{
|
|
416
|
+
names: ['navigate_page', 'navigate_to_url'],
|
|
417
|
+
buildArgs: (tool) => withNavigateArgs(tool, url, this.timeoutMs),
|
|
418
|
+
},
|
|
419
|
+
], {});
|
|
420
|
+
const output = this.outputFromInvocation('open', invocation);
|
|
421
|
+
return this.ensurePageContentInOutput('open', invocation, output, url);
|
|
422
|
+
}
|
|
423
|
+
catch (error) {
|
|
424
|
+
const recovered = await this.recoverPageContentAfterTimeout('open', url, error);
|
|
425
|
+
if (recovered) {
|
|
426
|
+
return recovered;
|
|
427
|
+
}
|
|
428
|
+
throw error;
|
|
429
|
+
}
|
|
466
430
|
}
|
|
467
431
|
async navigate(url) {
|
|
468
|
-
|
|
469
|
-
|
|
470
|
-
|
|
471
|
-
|
|
472
|
-
|
|
432
|
+
try {
|
|
433
|
+
const invocation = await this.callTool('navigate', [
|
|
434
|
+
{
|
|
435
|
+
names: ['navigate_page', 'navigate_to_url'],
|
|
436
|
+
buildArgs: (tool) => withNavigateArgs(tool, url, this.timeoutMs),
|
|
437
|
+
},
|
|
438
|
+
{
|
|
439
|
+
names: ['new_page', 'create_new_tab'],
|
|
440
|
+
buildArgs: (tool) => withOpenArgs(tool, url),
|
|
441
|
+
},
|
|
442
|
+
], {});
|
|
443
|
+
const output = this.outputFromInvocation('navigate', invocation);
|
|
444
|
+
return this.ensurePageContentInOutput('navigate', invocation, output, url);
|
|
445
|
+
}
|
|
446
|
+
catch (error) {
|
|
447
|
+
const recovered = await this.recoverPageContentAfterTimeout('navigate', url, error);
|
|
448
|
+
if (recovered) {
|
|
449
|
+
return recovered;
|
|
450
|
+
}
|
|
451
|
+
throw error;
|
|
452
|
+
}
|
|
453
|
+
}
|
|
454
|
+
async close(id) {
|
|
455
|
+
const invocation = await this.callTool('close', [
|
|
473
456
|
{
|
|
474
|
-
names: ['
|
|
475
|
-
buildArgs: (tool) =>
|
|
457
|
+
names: ['close_page', 'close_tab'],
|
|
458
|
+
buildArgs: (tool) => withPageArgs(tool, id),
|
|
476
459
|
},
|
|
477
460
|
], {});
|
|
478
|
-
return this.outputFromInvocation('
|
|
461
|
+
return this.outputFromInvocation('close', invocation);
|
|
479
462
|
}
|
|
480
463
|
async focus(id) {
|
|
481
464
|
const invocation = await this.callTool('focus', [
|
|
482
465
|
{
|
|
483
|
-
names: ['
|
|
466
|
+
names: ['switch_to_page', 'switch_to_tab', 'select_page', 'focus_tab'],
|
|
484
467
|
buildArgs: (tool) => withPageArgs(tool, id),
|
|
485
468
|
},
|
|
486
469
|
], {});
|
|
487
470
|
return this.outputFromInvocation('focus', invocation);
|
|
488
471
|
}
|
|
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
472
|
async snapshot(options) {
|
|
499
473
|
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
474
|
const invocation = await this.callTool('snapshot', [
|
|
515
475
|
{
|
|
516
|
-
names: ['take_a11y_snapshot'],
|
|
517
|
-
buildArgs: (tool) => withSnapshotArgs(tool, options),
|
|
518
|
-
},
|
|
519
|
-
{
|
|
520
|
-
names: ['take_snapshot', 'get_page_content'],
|
|
476
|
+
names: [wantsAria ? 'take_a11y_snapshot' : 'take_md_snapshot'],
|
|
521
477
|
buildArgs: (tool) => withSnapshotArgs(tool, options),
|
|
522
478
|
},
|
|
523
479
|
], {});
|
|
@@ -553,38 +509,6 @@ class BrowserCliContext {
|
|
|
553
509
|
raw: normalizeToolResult(invocation.result),
|
|
554
510
|
};
|
|
555
511
|
}
|
|
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
512
|
async requests(options) {
|
|
589
513
|
const invocation = await this.callTool('requests', [
|
|
590
514
|
{
|
|
@@ -638,15 +562,6 @@ class BrowserCliContext {
|
|
|
638
562
|
raw: normalizeToolResult(invocation.result),
|
|
639
563
|
};
|
|
640
564
|
}
|
|
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
565
|
async click(ref, options) {
|
|
651
566
|
const invocation = await this.callTool('click', [
|
|
652
567
|
{
|
|
@@ -687,15 +602,6 @@ class BrowserCliContext {
|
|
|
687
602
|
], {});
|
|
688
603
|
return this.outputFromInvocation('hover', invocation);
|
|
689
604
|
}
|
|
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
605
|
async drag(source, target) {
|
|
700
606
|
const invocation = await this.callTool('drag', [
|
|
701
607
|
{
|
|
@@ -718,37 +624,6 @@ class BrowserCliContext {
|
|
|
718
624
|
], {});
|
|
719
625
|
return this.outputFromInvocation('select', invocation);
|
|
720
626
|
}
|
|
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
627
|
async fillForm(fieldsJson) {
|
|
753
628
|
const fields = parseJsonValue(fieldsJson, '--fields');
|
|
754
629
|
if (!Array.isArray(fields)) {
|
|
@@ -762,16 +637,6 @@ class BrowserCliContext {
|
|
|
762
637
|
], {});
|
|
763
638
|
return this.outputFromInvocation('fill', invocation);
|
|
764
639
|
}
|
|
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
640
|
async wait(options) {
|
|
776
641
|
if (options.text && options.text.length > 0) {
|
|
777
642
|
const invocation = await this.callTool('wait', [
|
|
@@ -804,37 +669,6 @@ class BrowserCliContext {
|
|
|
804
669
|
], {});
|
|
805
670
|
return this.outputFromInvocation('evaluate', invocation);
|
|
806
671
|
}
|
|
807
|
-
async highlight(ref) {
|
|
808
|
-
const invocation = await this.callTool('highlight', [
|
|
809
|
-
{
|
|
810
|
-
names: ['highlight'],
|
|
811
|
-
buildArgs: (tool) => withRefArgs(tool, ref),
|
|
812
|
-
},
|
|
813
|
-
{
|
|
814
|
-
names: ['hover'],
|
|
815
|
-
buildArgs: (tool) => withRefArgs(tool, ref, { duration: 1500 }),
|
|
816
|
-
},
|
|
817
|
-
], {});
|
|
818
|
-
return this.outputFromInvocation('highlight', invocation);
|
|
819
|
-
}
|
|
820
|
-
async traceStart(options) {
|
|
821
|
-
const invocation = await this.callTool('trace start', [
|
|
822
|
-
{
|
|
823
|
-
names: ['performance_start_trace'],
|
|
824
|
-
buildArgs: (tool) => withTraceStartArgs(tool, options),
|
|
825
|
-
},
|
|
826
|
-
], {});
|
|
827
|
-
return this.outputFromInvocation('trace start', invocation);
|
|
828
|
-
}
|
|
829
|
-
async traceStop(outputPath) {
|
|
830
|
-
const invocation = await this.callTool('trace stop', [
|
|
831
|
-
{
|
|
832
|
-
names: ['performance_stop_trace'],
|
|
833
|
-
buildArgs: (tool) => withTraceStopArgs(tool, outputPath),
|
|
834
|
-
},
|
|
835
|
-
], {});
|
|
836
|
-
return this.outputFromInvocation('trace stop', invocation);
|
|
837
|
-
}
|
|
838
672
|
async callGenericCommand(commandName, candidates, canonicalArgs) {
|
|
839
673
|
const invocation = await this.callTool(commandName, candidates, canonicalArgs);
|
|
840
674
|
return this.outputFromInvocation(commandName, invocation);
|
|
@@ -851,23 +685,44 @@ class BrowserCliContext {
|
|
|
851
685
|
const args = candidate.buildArgs
|
|
852
686
|
? candidate.buildArgs(tool)
|
|
853
687
|
: withCanonicalArgs(tool, canonicalArgs);
|
|
854
|
-
|
|
688
|
+
// Inject --page-id into every tool call that accepts pageId/tabId,
|
|
689
|
+
// so the agent doesn't need to use focus/tab select (which disrupts
|
|
690
|
+
// the user's active browser tab).
|
|
691
|
+
if (this.pageId !== undefined) {
|
|
692
|
+
const pageKey = hasProperty(tool, 'pageId', 'tabId');
|
|
693
|
+
if (pageKey && !(pageKey in args)) {
|
|
694
|
+
args[pageKey] = this.pageId;
|
|
695
|
+
}
|
|
696
|
+
}
|
|
697
|
+
if (shouldRequestPageStateForCommand(commandName)) {
|
|
698
|
+
const pageStateKey = hasProperty(tool, 'pageStateFormat', 'page_state_format');
|
|
699
|
+
if (pageStateKey && !('pageStateFormat' in args) && !('page_state_format' in args)) {
|
|
700
|
+
args[pageStateKey] = 'markdown';
|
|
701
|
+
}
|
|
702
|
+
}
|
|
703
|
+
const result = await this.connection.callTool(tool.name, args, this.timeoutMs);
|
|
855
704
|
return { tool: tool.name, args, result: result };
|
|
856
705
|
}
|
|
857
706
|
}
|
|
858
707
|
const requested = candidates.flatMap((candidate) => candidate.names);
|
|
859
708
|
throw new Error(`No compatible browser tool found for "${commandName}". Tried ${requested.join(', ')}. Available tools: ${this.tools.map((tool) => tool.name).join(', ')}`);
|
|
860
709
|
}
|
|
861
|
-
async ensureToolsLoaded() {
|
|
710
|
+
async ensureToolsLoaded(timeoutMs) {
|
|
862
711
|
if (this.toolsLoaded && this.tools.length > 0) {
|
|
863
712
|
return;
|
|
864
713
|
}
|
|
714
|
+
const effectiveTimeout = timeoutMs ?? this.timeoutMs;
|
|
865
715
|
if (this.connection.isExtensionConnected()) {
|
|
866
716
|
try {
|
|
867
|
-
this.tools = await this.connection.refreshTools(
|
|
717
|
+
this.tools = await this.connection.refreshTools(effectiveTimeout);
|
|
868
718
|
}
|
|
869
719
|
catch {
|
|
870
|
-
|
|
720
|
+
// Refresh timed out or failed — fall back to cached tools or a short
|
|
721
|
+
// passive wait so the status command is not blocked.
|
|
722
|
+
this.tools = this.connection.getTools();
|
|
723
|
+
if (this.tools.length === 0) {
|
|
724
|
+
this.tools = await this.connection.waitForToolsUpdate(1_000);
|
|
725
|
+
}
|
|
871
726
|
}
|
|
872
727
|
}
|
|
873
728
|
else {
|
|
@@ -876,19 +731,308 @@ class BrowserCliContext {
|
|
|
876
731
|
this.toolsLoaded = true;
|
|
877
732
|
}
|
|
878
733
|
outputFromInvocation(commandName, invocation) {
|
|
734
|
+
const pageContent = firstText(invocation.result);
|
|
879
735
|
return {
|
|
880
736
|
ok: !invocation.result.isError,
|
|
881
737
|
command: commandName,
|
|
882
738
|
profile: this.profile,
|
|
883
739
|
mode: this.mode(),
|
|
740
|
+
sessionId: this.currentSessionId(),
|
|
741
|
+
requestedSessionId: this.requestedSessionId,
|
|
884
742
|
ignoredCompatibilityOptions: this.ignoredCompatibilityOptions,
|
|
885
743
|
tool: invocation.tool,
|
|
744
|
+
...(looksLikePageContentText(pageContent) ? { pageContent } : {}),
|
|
886
745
|
raw: normalizeToolResult(invocation.result),
|
|
887
746
|
};
|
|
888
747
|
}
|
|
748
|
+
async ensurePageContentInOutput(commandName, invocation, output, targetUrl) {
|
|
749
|
+
if (!output.ok) {
|
|
750
|
+
return output;
|
|
751
|
+
}
|
|
752
|
+
const currentText = firstText(invocation.result);
|
|
753
|
+
if (looksLikePageContentText(currentText) && !isLikelyIncompletePageContent(currentText, targetUrl)) {
|
|
754
|
+
return output;
|
|
755
|
+
}
|
|
756
|
+
const targetPageId = extractPageIdFromResult(invocation.result) ?? this.pageId;
|
|
757
|
+
if (targetPageId === undefined) {
|
|
758
|
+
return output;
|
|
759
|
+
}
|
|
760
|
+
const fallback = await this.takeMarkdownSnapshotForTarget(targetPageId, targetUrl);
|
|
761
|
+
if (!fallback.content) {
|
|
762
|
+
return output;
|
|
763
|
+
}
|
|
764
|
+
return {
|
|
765
|
+
...output,
|
|
766
|
+
pageContent: fallback.content,
|
|
767
|
+
pageContentSource: 'take_md_snapshot',
|
|
768
|
+
...(fallback.pageId !== targetPageId ? { pageId: fallback.pageId } : {}),
|
|
769
|
+
};
|
|
770
|
+
}
|
|
771
|
+
async recoverPageContentAfterTimeout(commandName, url, error) {
|
|
772
|
+
if (!isTimeoutError(error)) {
|
|
773
|
+
return undefined;
|
|
774
|
+
}
|
|
775
|
+
const recoveryTimeoutMs = Math.max(this.timeoutMs, 10_000);
|
|
776
|
+
let targetPageId = this.pageId;
|
|
777
|
+
if (targetPageId === undefined) {
|
|
778
|
+
for (let attempt = 0; attempt < 3; attempt += 1) {
|
|
779
|
+
const pages = await this.listPagesForRecovery(recoveryTimeoutMs);
|
|
780
|
+
const matched = findBestPageMatchByUrl(pages, url);
|
|
781
|
+
if (matched) {
|
|
782
|
+
targetPageId = matched.id;
|
|
783
|
+
break;
|
|
784
|
+
}
|
|
785
|
+
await delay(750);
|
|
786
|
+
}
|
|
787
|
+
}
|
|
788
|
+
if (targetPageId === undefined) {
|
|
789
|
+
return undefined;
|
|
790
|
+
}
|
|
791
|
+
const fallback = await this.takeMarkdownSnapshotForTarget(targetPageId, url, recoveryTimeoutMs);
|
|
792
|
+
if (!fallback.content) {
|
|
793
|
+
return undefined;
|
|
794
|
+
}
|
|
795
|
+
return {
|
|
796
|
+
ok: true,
|
|
797
|
+
command: commandName,
|
|
798
|
+
profile: this.profile,
|
|
799
|
+
mode: this.mode(),
|
|
800
|
+
sessionId: this.currentSessionId(),
|
|
801
|
+
requestedSessionId: this.requestedSessionId,
|
|
802
|
+
ignoredCompatibilityOptions: this.ignoredCompatibilityOptions,
|
|
803
|
+
tool: 'take_md_snapshot',
|
|
804
|
+
recoveredFromTimeout: true,
|
|
805
|
+
pageId: fallback.pageId,
|
|
806
|
+
url,
|
|
807
|
+
pageContent: fallback.content,
|
|
808
|
+
raw: {
|
|
809
|
+
recovery: 'timeout_fallback',
|
|
810
|
+
},
|
|
811
|
+
};
|
|
812
|
+
}
|
|
813
|
+
async takeMarkdownSnapshotForTarget(initialPageId, targetUrl, timeoutMs = this.timeoutMs) {
|
|
814
|
+
let latest;
|
|
815
|
+
let pageId = initialPageId;
|
|
816
|
+
const recoveryTimeoutMs = Math.max(timeoutMs, 10_000);
|
|
817
|
+
const attempts = 8;
|
|
818
|
+
for (let attempt = 0; attempt < attempts; attempt += 1) {
|
|
819
|
+
latest = await this.takeMarkdownSnapshotForPage(pageId, recoveryTimeoutMs);
|
|
820
|
+
if (latest && !isLikelyIncompletePageContent(latest, targetUrl)) {
|
|
821
|
+
return { content: latest, pageId };
|
|
822
|
+
}
|
|
823
|
+
if (targetUrl) {
|
|
824
|
+
const pages = await this.listPagesForRecovery(recoveryTimeoutMs);
|
|
825
|
+
const matched = findBestPageMatchByUrl(pages, targetUrl);
|
|
826
|
+
if (matched) {
|
|
827
|
+
pageId = matched.id;
|
|
828
|
+
}
|
|
829
|
+
}
|
|
830
|
+
if (attempt < attempts - 1) {
|
|
831
|
+
await delay(750);
|
|
832
|
+
}
|
|
833
|
+
}
|
|
834
|
+
return { content: latest, pageId };
|
|
835
|
+
}
|
|
836
|
+
async listPagesForRecovery(timeoutMs) {
|
|
837
|
+
try {
|
|
838
|
+
await this.ensureToolsLoaded(timeoutMs);
|
|
839
|
+
const listTool = this.tools.find((tool) => {
|
|
840
|
+
const normalized = normalizeName(tool.name);
|
|
841
|
+
return normalized === 'list_pages' || normalized === 'get_tabs';
|
|
842
|
+
});
|
|
843
|
+
if (!listTool) {
|
|
844
|
+
return [];
|
|
845
|
+
}
|
|
846
|
+
const result = await this.connection.callTool(listTool.name, {}, timeoutMs);
|
|
847
|
+
return extractPages(result);
|
|
848
|
+
}
|
|
849
|
+
catch {
|
|
850
|
+
return [];
|
|
851
|
+
}
|
|
852
|
+
}
|
|
853
|
+
async takeMarkdownSnapshotForPage(pageId, timeoutMs = this.timeoutMs) {
|
|
854
|
+
await this.ensureToolsLoaded();
|
|
855
|
+
const snapshotTool = this.tools.find((tool) => normalizeName(tool.name) === 'take_md_snapshot');
|
|
856
|
+
if (!snapshotTool) {
|
|
857
|
+
return undefined;
|
|
858
|
+
}
|
|
859
|
+
const args = {};
|
|
860
|
+
const pageKey = hasProperty(snapshotTool, 'pageId', 'tabId');
|
|
861
|
+
if (pageKey) {
|
|
862
|
+
args[pageKey] = pageId;
|
|
863
|
+
}
|
|
864
|
+
const pageStateKey = hasProperty(snapshotTool, 'pageStateFormat', 'page_state_format');
|
|
865
|
+
if (pageStateKey) {
|
|
866
|
+
args[pageStateKey] = 'markdown';
|
|
867
|
+
}
|
|
868
|
+
try {
|
|
869
|
+
const result = await this.connection.callTool(snapshotTool.name, args, timeoutMs);
|
|
870
|
+
const text = firstText(result);
|
|
871
|
+
if (!text) {
|
|
872
|
+
return undefined;
|
|
873
|
+
}
|
|
874
|
+
const trimmed = text.trim();
|
|
875
|
+
if (/^Error:/i.test(trimmed) || /Failed to get valid tab/i.test(trimmed)) {
|
|
876
|
+
return undefined;
|
|
877
|
+
}
|
|
878
|
+
return text;
|
|
879
|
+
}
|
|
880
|
+
catch {
|
|
881
|
+
return undefined;
|
|
882
|
+
}
|
|
883
|
+
}
|
|
889
884
|
mode() {
|
|
890
885
|
return this.remoteUuid ? 'remote' : 'local';
|
|
891
886
|
}
|
|
887
|
+
currentSessionId() {
|
|
888
|
+
if (this.remoteUuid) {
|
|
889
|
+
return this.remoteUuid;
|
|
890
|
+
}
|
|
891
|
+
const connectedSessionIds = new Set(this.sessions
|
|
892
|
+
.filter((session) => session.connected)
|
|
893
|
+
.map((session) => session.sessionId));
|
|
894
|
+
if (this.requestedSessionId && connectedSessionIds.has(this.requestedSessionId)) {
|
|
895
|
+
return this.requestedSessionId;
|
|
896
|
+
}
|
|
897
|
+
return this.sessions.find((session) => session.connected)?.sessionId;
|
|
898
|
+
}
|
|
899
|
+
}
|
|
900
|
+
function shouldRequestPageStateForCommand(commandName) {
|
|
901
|
+
return new Set([
|
|
902
|
+
'open',
|
|
903
|
+
'navigate',
|
|
904
|
+
'click',
|
|
905
|
+
'type',
|
|
906
|
+
'press',
|
|
907
|
+
'hover',
|
|
908
|
+
'drag',
|
|
909
|
+
'select',
|
|
910
|
+
'fill',
|
|
911
|
+
]).has(commandName);
|
|
912
|
+
}
|
|
913
|
+
function looksLikePageContentText(text) {
|
|
914
|
+
const trimmed = text.trim();
|
|
915
|
+
if (!trimmed) {
|
|
916
|
+
return false;
|
|
917
|
+
}
|
|
918
|
+
if (/Error retrieving page content/i.test(trimmed) || /page content extraction failed/i.test(trimmed)) {
|
|
919
|
+
return false;
|
|
920
|
+
}
|
|
921
|
+
return /```(?:markdown|text|html)/i.test(trimmed) || /Page State Format:/i.test(trimmed);
|
|
922
|
+
}
|
|
923
|
+
function isLikelyIncompletePageContent(text, targetUrl) {
|
|
924
|
+
const trimmed = text.trim();
|
|
925
|
+
if (!trimmed) {
|
|
926
|
+
return true;
|
|
927
|
+
}
|
|
928
|
+
const interactiveRefCount = (trimmed.match(/\[M\d+:/g) || []).length;
|
|
929
|
+
// Common in early snapshots right after new_page(waitForReady=false):
|
|
930
|
+
// tab exists but title/url are still empty.
|
|
931
|
+
if (/Tab ID:[^\n]*\nTitle:\s*\nURL:\s*(?:\n|$)/i.test(trimmed)) {
|
|
932
|
+
return true;
|
|
933
|
+
}
|
|
934
|
+
if (/#\s*Markdown Snapshot:\s*Untitled/i.test(trimmed) && interactiveRefCount < 5) {
|
|
935
|
+
return true;
|
|
936
|
+
}
|
|
937
|
+
if (targetUrl) {
|
|
938
|
+
const normalizedTarget = normalizeUrlForMatch(targetUrl);
|
|
939
|
+
const hasAnyUrl = /\bURL:\s*\S+/i.test(trimmed);
|
|
940
|
+
const textNormalized = normalizeUrlForMatch(trimmed);
|
|
941
|
+
if (!hasAnyUrl) {
|
|
942
|
+
return true;
|
|
943
|
+
}
|
|
944
|
+
if (normalizedTarget && !textNormalized.includes(normalizedTarget)) {
|
|
945
|
+
return true;
|
|
946
|
+
}
|
|
947
|
+
}
|
|
948
|
+
return false;
|
|
949
|
+
}
|
|
950
|
+
function extractPageIdFromResult(result) {
|
|
951
|
+
const direct = firstDefined(result, ['pageId', 'tabId', 'id']);
|
|
952
|
+
if (typeof direct === 'number' && Number.isFinite(direct)) {
|
|
953
|
+
return direct;
|
|
954
|
+
}
|
|
955
|
+
const parsed = parseResultText(result);
|
|
956
|
+
if (parsed && typeof parsed === 'object') {
|
|
957
|
+
const parsedId = firstDefined(parsed, ['pageId', 'tabId', 'id']);
|
|
958
|
+
if (typeof parsedId === 'number' && Number.isFinite(parsedId)) {
|
|
959
|
+
return parsedId;
|
|
960
|
+
}
|
|
961
|
+
}
|
|
962
|
+
const text = firstText(result);
|
|
963
|
+
const createdMatch = /new background page \(ID:\s*(\d+)\)/i.exec(text);
|
|
964
|
+
if (createdMatch) {
|
|
965
|
+
return Number.parseInt(createdMatch[1], 10);
|
|
966
|
+
}
|
|
967
|
+
const tabMatch = /\bTab ID:\s*(\d+)\b/i.exec(text);
|
|
968
|
+
if (tabMatch) {
|
|
969
|
+
return Number.parseInt(tabMatch[1], 10);
|
|
970
|
+
}
|
|
971
|
+
const pageMatch = /\bPage ID:\s*(\d+)\b/i.exec(text);
|
|
972
|
+
if (pageMatch) {
|
|
973
|
+
return Number.parseInt(pageMatch[1], 10);
|
|
974
|
+
}
|
|
975
|
+
return undefined;
|
|
976
|
+
}
|
|
977
|
+
function isTimeoutError(error) {
|
|
978
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
979
|
+
return /timed out|timeout/i.test(message);
|
|
980
|
+
}
|
|
981
|
+
function findBestPageMatchByUrl(pages, targetUrl) {
|
|
982
|
+
const normalizedTarget = normalizeUrlForMatch(targetUrl);
|
|
983
|
+
let bestExact;
|
|
984
|
+
let bestPartial;
|
|
985
|
+
for (const page of pages) {
|
|
986
|
+
const id = toFinitePageId(page.id);
|
|
987
|
+
if (id === undefined || typeof page.url !== 'string') {
|
|
988
|
+
continue;
|
|
989
|
+
}
|
|
990
|
+
const normalizedPage = normalizeUrlForMatch(page.url);
|
|
991
|
+
if (normalizedPage === normalizedTarget) {
|
|
992
|
+
if (!bestExact || id > bestExact.id) {
|
|
993
|
+
bestExact = { id, url: page.url };
|
|
994
|
+
}
|
|
995
|
+
}
|
|
996
|
+
if (normalizedPage.includes(normalizedTarget) || normalizedTarget.includes(normalizedPage)) {
|
|
997
|
+
if (!bestPartial || id > bestPartial.id) {
|
|
998
|
+
bestPartial = { id, url: page.url };
|
|
999
|
+
}
|
|
1000
|
+
}
|
|
1001
|
+
}
|
|
1002
|
+
return bestExact ?? bestPartial;
|
|
1003
|
+
}
|
|
1004
|
+
function toFinitePageId(value) {
|
|
1005
|
+
if (typeof value === 'number' && Number.isFinite(value)) {
|
|
1006
|
+
return value;
|
|
1007
|
+
}
|
|
1008
|
+
if (typeof value === 'string') {
|
|
1009
|
+
const parsed = Number.parseInt(value, 10);
|
|
1010
|
+
if (Number.isFinite(parsed)) {
|
|
1011
|
+
return parsed;
|
|
1012
|
+
}
|
|
1013
|
+
}
|
|
1014
|
+
return undefined;
|
|
1015
|
+
}
|
|
1016
|
+
function normalizeUrlForMatch(value) {
|
|
1017
|
+
const trimmed = value.trim().replace(/\/$/, '');
|
|
1018
|
+
if (!trimmed) {
|
|
1019
|
+
return trimmed;
|
|
1020
|
+
}
|
|
1021
|
+
try {
|
|
1022
|
+
const parsed = new URL(trimmed);
|
|
1023
|
+
return safeDecode(`${parsed.origin}${parsed.pathname}${parsed.search}`.replace(/\/$/, ''));
|
|
1024
|
+
}
|
|
1025
|
+
catch {
|
|
1026
|
+
return safeDecode(trimmed);
|
|
1027
|
+
}
|
|
1028
|
+
}
|
|
1029
|
+
function safeDecode(value) {
|
|
1030
|
+
try {
|
|
1031
|
+
return decodeURIComponent(value);
|
|
1032
|
+
}
|
|
1033
|
+
catch {
|
|
1034
|
+
return value;
|
|
1035
|
+
}
|
|
892
1036
|
}
|
|
893
1037
|
function emitOutput(asJson, data, humanOutput) {
|
|
894
1038
|
if (asJson) {
|
|
@@ -898,7 +1042,11 @@ function emitOutput(asJson, data, humanOutput) {
|
|
|
898
1042
|
console.log(humanOutput);
|
|
899
1043
|
}
|
|
900
1044
|
function emitError(asJson, commandName, ctx, error) {
|
|
901
|
-
|
|
1045
|
+
let message = error instanceof Error ? error.message : String(error);
|
|
1046
|
+
// Improve guidance when the extension requires a pageId that wasn't provided
|
|
1047
|
+
if (/\bpageId\b/i.test(message) && /\bmissing\b|\brequired\b/i.test(message)) {
|
|
1048
|
+
message += '\nHint: use `tabs` to list pages, then pass --page-id <id> to target a specific tab.';
|
|
1049
|
+
}
|
|
902
1050
|
if (asJson) {
|
|
903
1051
|
console.log(JSON.stringify({
|
|
904
1052
|
ok: false,
|
|
@@ -919,19 +1067,36 @@ function formatHumanOutput(commandName, output) {
|
|
|
919
1067
|
return [
|
|
920
1068
|
`Profile: ${String(output.profile)}`,
|
|
921
1069
|
`Mode: ${String(output.mode)}`,
|
|
1070
|
+
output.sessionId ? `Session: ${String(output.sessionId)}` : null,
|
|
1071
|
+
output.requestedSessionId && output.requestedSessionId !== output.sessionId ? `Requested session: ${String(output.requestedSessionId)}` : null,
|
|
922
1072
|
`Relay connected: ${boolText(output.relayConnected)}`,
|
|
923
1073
|
`Extension connected: ${boolText(output.extensionConnected)}`,
|
|
924
1074
|
`Managed lifecycle: ${boolText(output.managedLifecycle)}`,
|
|
925
1075
|
output.toolCount !== undefined ? `Tools: ${String(output.toolCount)}` : null,
|
|
926
1076
|
output.note ? String(output.note) : null,
|
|
927
1077
|
].filter(Boolean).join('\n');
|
|
1078
|
+
case 'sessions': {
|
|
1079
|
+
const sessions = Array.isArray(output.sessions) ? output.sessions : [];
|
|
1080
|
+
if (sessions.length === 0) {
|
|
1081
|
+
return 'No browser sessions connected';
|
|
1082
|
+
}
|
|
1083
|
+
return sessions.map((session, index) => {
|
|
1084
|
+
const selected = output.sessionId === session.sessionId ? ' [selected]' : '';
|
|
1085
|
+
const connected = session.connected ? 'connected' : 'disconnected';
|
|
1086
|
+
const tools = session.toolCount !== undefined ? ` tools=${session.toolCount}` : '';
|
|
1087
|
+
return `${index + 1}. ${session.sessionId}${selected} - ${connected}${tools}`;
|
|
1088
|
+
}).join('\n');
|
|
1089
|
+
}
|
|
928
1090
|
case 'tabs':
|
|
929
1091
|
case 'tab': {
|
|
930
1092
|
const pages = Array.isArray(output.pages) ? output.pages : [];
|
|
931
1093
|
if (pages.length === 0) {
|
|
932
1094
|
return firstDefinedText(output.raw, 'No tabs reported by browser');
|
|
933
1095
|
}
|
|
934
|
-
return
|
|
1096
|
+
return [
|
|
1097
|
+
output.sessionId ? `Session: ${String(output.sessionId)}` : null,
|
|
1098
|
+
...pages.map((page, index) => `${index + 1}. ${page.active ? '[active] ' : ''}${page.title || '(untitled)'}${page.url ? ` — ${page.url}` : ''}${page.id !== undefined ? ` [id=${page.id}]` : ''}`),
|
|
1099
|
+
].filter(Boolean).join('\n');
|
|
935
1100
|
}
|
|
936
1101
|
case 'snapshot':
|
|
937
1102
|
return [
|
|
@@ -946,14 +1111,6 @@ function formatHumanOutput(commandName, output) {
|
|
|
946
1111
|
}
|
|
947
1112
|
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
1113
|
}
|
|
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
1114
|
case 'responsebody':
|
|
958
1115
|
return String(output.responseBody ?? firstDefinedText(output.raw, ''));
|
|
959
1116
|
default:
|
|
@@ -970,8 +1127,19 @@ function firstDefinedText(value, fallback) {
|
|
|
970
1127
|
if (typeof value === 'string' && value.length > 0) {
|
|
971
1128
|
return value;
|
|
972
1129
|
}
|
|
973
|
-
if (value && typeof value === 'object'
|
|
974
|
-
|
|
1130
|
+
if (value && typeof value === 'object') {
|
|
1131
|
+
// Direct { text: "..." } wrapper
|
|
1132
|
+
if ('text' in value && typeof value.text === 'string') {
|
|
1133
|
+
return value.text;
|
|
1134
|
+
}
|
|
1135
|
+
// MCP tool result: { content: [{ type: 'text', text: '...' }] }
|
|
1136
|
+
const rec = value;
|
|
1137
|
+
if (Array.isArray(rec.content)) {
|
|
1138
|
+
const textItem = rec.content.find((entry) => entry && typeof entry === 'object' && entry.type === 'text');
|
|
1139
|
+
if (textItem && typeof textItem.text === 'string' && textItem.text.length > 0) {
|
|
1140
|
+
return textItem.text;
|
|
1141
|
+
}
|
|
1142
|
+
}
|
|
975
1143
|
}
|
|
976
1144
|
return fallback;
|
|
977
1145
|
}
|
|
@@ -1026,8 +1194,34 @@ function extractPages(result) {
|
|
|
1026
1194
|
}
|
|
1027
1195
|
}
|
|
1028
1196
|
}
|
|
1197
|
+
// Fallback: parse the plain-text format produced by ListPagesTool
|
|
1198
|
+
// e.g. 'Page 123 [ACTIVE]: "Google" - https://www.google.com'
|
|
1199
|
+
const text = firstText(result);
|
|
1200
|
+
if (text) {
|
|
1201
|
+
const textPages = parsePlainTextPages(text);
|
|
1202
|
+
if (textPages.length > 0) {
|
|
1203
|
+
return textPages;
|
|
1204
|
+
}
|
|
1205
|
+
}
|
|
1029
1206
|
return [];
|
|
1030
1207
|
}
|
|
1208
|
+
// Matches ListPagesTool output: 'Page <id>[ [ACTIVE]]: "title" - url'
|
|
1209
|
+
const PAGE_LINE_RE = /^Page\s+(\d+)\s*(\[ACTIVE\])?\s*:\s*"(.*)"\s*-\s*(.+)$/i;
|
|
1210
|
+
function parsePlainTextPages(text) {
|
|
1211
|
+
const pages = [];
|
|
1212
|
+
for (const line of text.split('\n')) {
|
|
1213
|
+
const match = PAGE_LINE_RE.exec(line.trim());
|
|
1214
|
+
if (match) {
|
|
1215
|
+
pages.push({
|
|
1216
|
+
id: Number(match[1]),
|
|
1217
|
+
active: match[2] !== undefined,
|
|
1218
|
+
title: match[3],
|
|
1219
|
+
url: match[4].trim(),
|
|
1220
|
+
});
|
|
1221
|
+
}
|
|
1222
|
+
}
|
|
1223
|
+
return pages;
|
|
1224
|
+
}
|
|
1031
1225
|
function normalizePage(value) {
|
|
1032
1226
|
if (!value || typeof value !== 'object') {
|
|
1033
1227
|
return {};
|
|
@@ -1071,26 +1265,6 @@ function normalizeRequest(value) {
|
|
|
1071
1265
|
status: firstNumber(record, ['status']),
|
|
1072
1266
|
};
|
|
1073
1267
|
}
|
|
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
1268
|
function extractResponseBody(result) {
|
|
1095
1269
|
const direct = extractStructured(result, ['responseBody', 'body']);
|
|
1096
1270
|
if (typeof direct === 'string') {
|
|
@@ -1218,12 +1392,24 @@ function withUrlArgs(tool, url) {
|
|
|
1218
1392
|
}
|
|
1219
1393
|
return args;
|
|
1220
1394
|
}
|
|
1221
|
-
function
|
|
1395
|
+
function withOpenArgs(tool, url) {
|
|
1396
|
+
const args = withUrlArgs(tool, url);
|
|
1397
|
+
const waitForReadyKey = hasProperty(tool, 'waitForReady');
|
|
1398
|
+
if (waitForReadyKey) {
|
|
1399
|
+
args[waitForReadyKey] = false;
|
|
1400
|
+
}
|
|
1401
|
+
return stripUndefined(args);
|
|
1402
|
+
}
|
|
1403
|
+
function withNavigateArgs(tool, url, timeoutMs) {
|
|
1222
1404
|
const args = withUrlArgs(tool, url);
|
|
1223
1405
|
const pageIdKey = hasProperty(tool, 'pageId', 'tabId');
|
|
1224
1406
|
if (pageIdKey && !(pageIdKey in args)) {
|
|
1225
1407
|
args[pageIdKey] = undefined;
|
|
1226
1408
|
}
|
|
1409
|
+
const timeoutKey = hasProperty(tool, 'timeoutMs', 'timeout');
|
|
1410
|
+
if (timeoutKey && typeof timeoutMs === 'number' && Number.isFinite(timeoutMs)) {
|
|
1411
|
+
args[timeoutKey] = timeoutMs;
|
|
1412
|
+
}
|
|
1227
1413
|
return stripUndefined(args);
|
|
1228
1414
|
}
|
|
1229
1415
|
function withPageArgs(tool, id) {
|
|
@@ -1258,21 +1444,6 @@ function withScreenshotArgs(tool, options) {
|
|
|
1258
1444
|
maybeAssign(args, tool, 'grayscale', options.grayscale || undefined);
|
|
1259
1445
|
return args;
|
|
1260
1446
|
}
|
|
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
1447
|
function withRequestListArgs(tool, options) {
|
|
1277
1448
|
const args = {};
|
|
1278
1449
|
if (options.limit) {
|
|
@@ -1287,12 +1458,6 @@ function withRequestDetailsArgs(tool, requestId) {
|
|
|
1287
1458
|
maybeAssign(args, tool, 'reqid', requestId);
|
|
1288
1459
|
return args;
|
|
1289
1460
|
}
|
|
1290
|
-
function withResizeArgs(tool, width, height) {
|
|
1291
|
-
const args = {};
|
|
1292
|
-
maybeAssign(args, tool, 'width', width);
|
|
1293
|
-
maybeAssign(args, tool, 'height', height);
|
|
1294
|
-
return args;
|
|
1295
|
-
}
|
|
1296
1461
|
function withRefArgs(tool, ref, extra = {}) {
|
|
1297
1462
|
const args = { ...extra };
|
|
1298
1463
|
const parsedRef = parseRef(ref);
|
|
@@ -1358,38 +1523,12 @@ function withSelectArgs(tool, ref, values) {
|
|
|
1358
1523
|
maybeAssign(args, tool, 'values', values);
|
|
1359
1524
|
return args;
|
|
1360
1525
|
}
|
|
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
1526
|
function withFillFormArgs(tool, fields) {
|
|
1382
1527
|
const args = {};
|
|
1383
1528
|
maybeAssign(args, tool, 'fields', fields);
|
|
1384
1529
|
maybeAssign(args, tool, 'elements', fields);
|
|
1385
1530
|
return args;
|
|
1386
1531
|
}
|
|
1387
|
-
function withDialogArgs(tool, action, prompt) {
|
|
1388
|
-
const args = {};
|
|
1389
|
-
maybeAssign(args, tool, 'action', action);
|
|
1390
|
-
maybeAssign(args, tool, 'promptText', prompt);
|
|
1391
|
-
return args;
|
|
1392
|
-
}
|
|
1393
1532
|
function withWaitArgs(tool, text, timeoutMs) {
|
|
1394
1533
|
const args = {};
|
|
1395
1534
|
maybeAssign(args, tool, 'text', text);
|
|
@@ -1417,21 +1556,6 @@ function withEvaluateArgs(tool, fn, ref, argsJson) {
|
|
|
1417
1556
|
}
|
|
1418
1557
|
return args;
|
|
1419
1558
|
}
|
|
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
1559
|
function maybeAssign(target, tool, property, value) {
|
|
1436
1560
|
if (value === undefined) {
|
|
1437
1561
|
return;
|