@vibebrowser/mcp 0.2.6 → 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 +26 -5
- package/dist/browser-cli.d.ts.map +1 -1
- package/dist/browser-cli.js +343 -58
- 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 +50 -4
- package/dist/cli.js.map +1 -1
- package/dist/connection.d.ts.map +1 -1
- package/dist/connection.js +0 -3
- 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 +12 -0
- package/dist/relay.d.ts.map +1 -1
- package/dist/relay.js +233 -48
- package/dist/relay.js.map +1 -1
- package/dist/server.d.ts.map +1 -1
- package/dist/server.js +67 -12
- package/dist/server.js.map +1 -1
- package/dist/types.d.ts +13 -0
- package/dist/types.d.ts.map +1 -1
- package/docs/chrome-devtools-relay.md +7 -0
- package/docs/openclaw-local-browser.md +11 -1
- package/openclaw/vibebrowser/SKILL.md +70 -4
- package/package.json +5 -1
package/dist/browser-cli.js
CHANGED
|
@@ -1,13 +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;
|
|
7
8
|
/** Short timeout for the `status` command — it should never block for 30 s. */
|
|
8
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
|
+
};
|
|
9
27
|
const DEFAULT_BROWSER_PROFILE = process.env.VIBE_BROWSER_PROFILE || 'user';
|
|
10
|
-
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;
|
|
11
29
|
const DEFAULT_REMOTE_RELAY_URL = process.env.VIBE_REMOTE_RELAY_URL || process.env.VIBE_RELAY_URL;
|
|
12
30
|
export function registerBrowserCommand(program) {
|
|
13
31
|
const browser = buildBrowserCommand(program.command('browser'));
|
|
@@ -24,6 +42,7 @@ function buildBrowserCommand(command) {
|
|
|
24
42
|
.option('--target <target>', 'OpenClaw compatibility target selector (accepted, not used by the Vibe browser CLI)')
|
|
25
43
|
.option('-p, --port <number>', 'WebSocket port for local relay (agent) connection', String(DEFAULT_WS_PORT))
|
|
26
44
|
.option('-d, --debug', 'Enable debug logging', false)
|
|
45
|
+
.option('--devtools', 'Use only chrome-devtools backend (bypasses extension relay)', false)
|
|
27
46
|
.option('-r, --remote <uuid>', 'Connect to a remote extension via public relay (provide the extension UUID)', DEFAULT_REMOTE_UUID)
|
|
28
47
|
.option('-s, --session <id>', 'Target a specific local browser session ID; defaults to the first connected session')
|
|
29
48
|
.option('--relay-url <url>', 'Custom relay server URL', DEFAULT_REMOTE_RELAY_URL)
|
|
@@ -36,8 +55,19 @@ function registerBrowserSubcommands(browser) {
|
|
|
36
55
|
browser
|
|
37
56
|
.command('status')
|
|
38
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')
|
|
39
61
|
.action(async function () {
|
|
40
|
-
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
|
+
}));
|
|
41
71
|
});
|
|
42
72
|
browser
|
|
43
73
|
.command('start')
|
|
@@ -128,7 +158,7 @@ function registerBrowserSubcommands(browser) {
|
|
|
128
158
|
browser
|
|
129
159
|
.command('snapshot')
|
|
130
160
|
.description('Capture a textual browser snapshot')
|
|
131
|
-
.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')
|
|
132
162
|
.option('--limit <count>', 'Max visible lines/items to print in human mode')
|
|
133
163
|
.option('--interactive', 'Prefer interactive/ARIA-flavored snapshot output', false)
|
|
134
164
|
.option('--selector <selector>', 'Selector to scope the snapshot to')
|
|
@@ -192,7 +222,12 @@ function registerBrowserSubcommands(browser) {
|
|
|
192
222
|
: undefined,
|
|
193
223
|
}));
|
|
194
224
|
});
|
|
195
|
-
|
|
225
|
+
browser
|
|
226
|
+
.command('resize <width> <height>')
|
|
227
|
+
.description('Resize the current page viewport/window')
|
|
228
|
+
.action(async function (width, height) {
|
|
229
|
+
await runBrowserCommand(this, 'resize', true, async (ctx) => ctx.resize(parsePositiveInteger(width, '<width>'), parsePositiveInteger(height, '<height>')));
|
|
230
|
+
});
|
|
196
231
|
browser
|
|
197
232
|
.command('click <ref>')
|
|
198
233
|
.description('Click an element by ref/index')
|
|
@@ -219,7 +254,13 @@ function registerBrowserSubcommands(browser) {
|
|
|
219
254
|
.action(async function (ref) {
|
|
220
255
|
await runBrowserCommand(this, 'hover', true, async (ctx) => ctx.hover(ref));
|
|
221
256
|
});
|
|
222
|
-
|
|
257
|
+
browser
|
|
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));
|
|
262
|
+
});
|
|
263
|
+
// NOTE: scrollintoview, download, waitfordownload commands removed — no matching tools.
|
|
223
264
|
browser
|
|
224
265
|
.command('drag <source> <target>')
|
|
225
266
|
.description('Drag one element to another')
|
|
@@ -239,7 +280,19 @@ function registerBrowserSubcommands(browser) {
|
|
|
239
280
|
.action(async function () {
|
|
240
281
|
await runBrowserCommand(this, 'fill', true, async (ctx, options) => ctx.fillForm(String(options.fields)));
|
|
241
282
|
});
|
|
242
|
-
|
|
283
|
+
browser
|
|
284
|
+
.command('dialog')
|
|
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')
|
|
289
|
+
.action(async function () {
|
|
290
|
+
await runBrowserCommand(this, 'dialog', true, async (ctx, options) => ctx.dialog({
|
|
291
|
+
accept: Boolean(options.accept),
|
|
292
|
+
dismiss: Boolean(options.dismiss),
|
|
293
|
+
promptText: options.prompt ? String(options.prompt) : undefined,
|
|
294
|
+
}));
|
|
295
|
+
});
|
|
243
296
|
browser
|
|
244
297
|
.command('wait')
|
|
245
298
|
.description('Wait for text or a short delay')
|
|
@@ -275,6 +328,7 @@ async function runBrowserCommand(command, commandName, requireExtension, handler
|
|
|
275
328
|
const ctx = new BrowserCliContext({
|
|
276
329
|
port: parsePositiveInteger(globalOptions.port, '--port'),
|
|
277
330
|
debug: Boolean(globalOptions.debug),
|
|
331
|
+
devtools: Boolean(globalOptions.devtools),
|
|
278
332
|
remoteUuid: globalOptions.remote,
|
|
279
333
|
sessionId: globalOptions.session,
|
|
280
334
|
relayUrl: globalOptions.relayUrl,
|
|
@@ -305,6 +359,7 @@ class BrowserCliContext {
|
|
|
305
359
|
profile;
|
|
306
360
|
json;
|
|
307
361
|
timeoutMs;
|
|
362
|
+
devtoolsOnly;
|
|
308
363
|
remoteUuid;
|
|
309
364
|
requestedSessionId;
|
|
310
365
|
target;
|
|
@@ -314,7 +369,10 @@ class BrowserCliContext {
|
|
|
314
369
|
sessions = [];
|
|
315
370
|
ignoredCompatibilityOptions;
|
|
316
371
|
constructor(init) {
|
|
317
|
-
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 });
|
|
318
376
|
this.profile = init.profile;
|
|
319
377
|
this.json = init.json;
|
|
320
378
|
this.timeoutMs = init.timeoutMs;
|
|
@@ -329,25 +387,37 @@ class BrowserCliContext {
|
|
|
329
387
|
}
|
|
330
388
|
async connect() {
|
|
331
389
|
await this.connection.start();
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
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
|
+
}
|
|
335
396
|
}
|
|
336
397
|
async shutdown() {
|
|
337
398
|
await this.connection.stop();
|
|
338
399
|
}
|
|
339
400
|
async ensureExtensionConnected() {
|
|
340
|
-
if (this.
|
|
401
|
+
if (this.isBackendConnected()) {
|
|
341
402
|
return;
|
|
342
403
|
}
|
|
343
|
-
|
|
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;
|
|
344
409
|
await this.ensureToolsLoaded();
|
|
345
|
-
if (!this.
|
|
346
|
-
throw new Error(this.
|
|
410
|
+
if (!this.isBackendConnected() && this.tools.length === 0) {
|
|
411
|
+
throw new Error(this.getConnectionErrorMessage());
|
|
347
412
|
}
|
|
348
413
|
}
|
|
349
414
|
async listSessions() {
|
|
350
|
-
this.
|
|
415
|
+
if (this.connection instanceof ExtensionConnection) {
|
|
416
|
+
this.sessions = await this.connection.listSessions(this.timeoutMs);
|
|
417
|
+
}
|
|
418
|
+
else {
|
|
419
|
+
this.sessions = [];
|
|
420
|
+
}
|
|
351
421
|
return {
|
|
352
422
|
ok: true,
|
|
353
423
|
command: 'sessions',
|
|
@@ -359,29 +429,62 @@ class BrowserCliContext {
|
|
|
359
429
|
sessions: this.sessions,
|
|
360
430
|
};
|
|
361
431
|
}
|
|
362
|
-
async status() {
|
|
363
|
-
|
|
364
|
-
|
|
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 = [];
|
|
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()) {
|
|
365
459
|
// Use a short timeout for status — this is a diagnostic command that
|
|
366
460
|
// should return quickly. Fall back to cached tools if the extension
|
|
367
461
|
// is slow to respond.
|
|
368
462
|
await this.ensureToolsLoaded(STATUS_TOOLS_TIMEOUT_MS);
|
|
369
463
|
}
|
|
464
|
+
await this.ensureToolsLoaded(STATUS_TOOLS_TIMEOUT_MS);
|
|
370
465
|
return {
|
|
371
466
|
ok: true,
|
|
372
467
|
command: 'status',
|
|
373
468
|
profile: this.profile,
|
|
374
|
-
mode: this.
|
|
469
|
+
mode: this.mode(),
|
|
375
470
|
sessionId: this.currentSessionId(),
|
|
376
471
|
requestedSessionId: this.requestedSessionId,
|
|
377
472
|
sessions: this.sessions,
|
|
378
473
|
ignoredCompatibilityOptions: this.ignoredCompatibilityOptions,
|
|
379
|
-
relayConnected: this.connection
|
|
380
|
-
|
|
474
|
+
relayConnected: this.connection instanceof ExtensionConnection
|
|
475
|
+
? this.connection.getStatus() === 'connected'
|
|
476
|
+
: false,
|
|
477
|
+
extensionConnected: this.isBackendConnected(),
|
|
381
478
|
managedLifecycle: false,
|
|
382
479
|
transport: 'vibebrowser-mcp',
|
|
383
480
|
toolCount: this.tools.length,
|
|
384
481
|
tools: this.tools.map((tool) => tool.name),
|
|
482
|
+
...(waitForExtension
|
|
483
|
+
? {
|
|
484
|
+
waitForExtension: true,
|
|
485
|
+
waitedMs: Date.now() - waitStartedAt,
|
|
486
|
+
}
|
|
487
|
+
: {}),
|
|
385
488
|
};
|
|
386
489
|
}
|
|
387
490
|
async listPages() {
|
|
@@ -473,7 +576,7 @@ class BrowserCliContext {
|
|
|
473
576
|
const wantsAria = options.format === 'aria' || options.interactive || Boolean(options.selector) || Boolean(options.frame);
|
|
474
577
|
const invocation = await this.callTool('snapshot', [
|
|
475
578
|
{
|
|
476
|
-
names: [
|
|
579
|
+
names: ['take_snapshot'],
|
|
477
580
|
buildArgs: (tool) => withSnapshotArgs(tool, options),
|
|
478
581
|
},
|
|
479
582
|
], {});
|
|
@@ -669,6 +772,37 @@ class BrowserCliContext {
|
|
|
669
772
|
], {});
|
|
670
773
|
return this.outputFromInvocation('evaluate', invocation);
|
|
671
774
|
}
|
|
775
|
+
async resize(width, height) {
|
|
776
|
+
const invocation = await this.callTool('resize', [
|
|
777
|
+
{
|
|
778
|
+
names: ['resize_page'],
|
|
779
|
+
buildArgs: (tool) => withResizeArgs(tool, width, height),
|
|
780
|
+
},
|
|
781
|
+
], {});
|
|
782
|
+
return this.outputFromInvocation('resize', invocation);
|
|
783
|
+
}
|
|
784
|
+
async upload(ref, path) {
|
|
785
|
+
const invocation = await this.callTool('upload', [
|
|
786
|
+
{
|
|
787
|
+
names: ['upload_file', 'file_upload'],
|
|
788
|
+
buildArgs: (tool) => withUploadArgs(tool, ref, path),
|
|
789
|
+
},
|
|
790
|
+
], {});
|
|
791
|
+
return this.outputFromInvocation('upload', invocation);
|
|
792
|
+
}
|
|
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', [
|
|
799
|
+
{
|
|
800
|
+
names: ['handle_dialog'],
|
|
801
|
+
buildArgs: (tool) => withDialogArgs(tool, accept, options.promptText),
|
|
802
|
+
},
|
|
803
|
+
], {});
|
|
804
|
+
return this.outputFromInvocation('dialog', invocation);
|
|
805
|
+
}
|
|
672
806
|
async callGenericCommand(commandName, candidates, canonicalArgs) {
|
|
673
807
|
const invocation = await this.callTool(commandName, candidates, canonicalArgs);
|
|
674
808
|
return this.outputFromInvocation(commandName, invocation);
|
|
@@ -676,6 +810,7 @@ class BrowserCliContext {
|
|
|
676
810
|
async callTool(commandName, candidates, canonicalArgs) {
|
|
677
811
|
await this.ensureToolsLoaded();
|
|
678
812
|
const available = new Map(this.tools.map((tool) => [normalizeName(tool.name), tool]));
|
|
813
|
+
const compatibilityErrors = [];
|
|
679
814
|
for (const candidate of candidates) {
|
|
680
815
|
for (const candidateName of candidate.names) {
|
|
681
816
|
const tool = available.get(normalizeName(candidateName));
|
|
@@ -700,34 +835,43 @@ class BrowserCliContext {
|
|
|
700
835
|
args[pageStateKey] = 'markdown';
|
|
701
836
|
}
|
|
702
837
|
}
|
|
703
|
-
|
|
704
|
-
|
|
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
|
+
}
|
|
705
849
|
}
|
|
706
850
|
}
|
|
707
851
|
const requested = candidates.flatMap((candidate) => candidate.names);
|
|
708
|
-
|
|
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}`);
|
|
709
856
|
}
|
|
710
857
|
async ensureToolsLoaded(timeoutMs) {
|
|
711
858
|
if (this.toolsLoaded && this.tools.length > 0) {
|
|
712
859
|
return;
|
|
713
860
|
}
|
|
714
861
|
const effectiveTimeout = timeoutMs ?? this.timeoutMs;
|
|
715
|
-
|
|
716
|
-
|
|
717
|
-
|
|
718
|
-
|
|
719
|
-
|
|
720
|
-
|
|
721
|
-
|
|
722
|
-
|
|
723
|
-
if (this.
|
|
862
|
+
try {
|
|
863
|
+
this.tools = await this.connection.refreshTools(effectiveTimeout);
|
|
864
|
+
}
|
|
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.
|
|
868
|
+
this.tools = this.connection.getTools();
|
|
869
|
+
if (this.tools.length === 0) {
|
|
870
|
+
if (this.connection instanceof ExtensionConnection) {
|
|
724
871
|
this.tools = await this.connection.waitForToolsUpdate(1_000);
|
|
725
872
|
}
|
|
726
873
|
}
|
|
727
874
|
}
|
|
728
|
-
else {
|
|
729
|
-
this.tools = this.connection.getTools();
|
|
730
|
-
}
|
|
731
875
|
this.toolsLoaded = true;
|
|
732
876
|
}
|
|
733
877
|
outputFromInvocation(commandName, invocation) {
|
|
@@ -749,6 +893,9 @@ class BrowserCliContext {
|
|
|
749
893
|
if (!output.ok) {
|
|
750
894
|
return output;
|
|
751
895
|
}
|
|
896
|
+
if (!hasExplicitPageContext(invocation.args)) {
|
|
897
|
+
return output;
|
|
898
|
+
}
|
|
752
899
|
const currentText = firstText(invocation.result);
|
|
753
900
|
if (looksLikePageContentText(currentText) && !isLikelyIncompletePageContent(currentText, targetUrl)) {
|
|
754
901
|
return output;
|
|
@@ -764,7 +911,7 @@ class BrowserCliContext {
|
|
|
764
911
|
return {
|
|
765
912
|
...output,
|
|
766
913
|
pageContent: fallback.content,
|
|
767
|
-
pageContentSource: '
|
|
914
|
+
pageContentSource: 'take_snapshot',
|
|
768
915
|
...(fallback.pageId !== targetPageId ? { pageId: fallback.pageId } : {}),
|
|
769
916
|
};
|
|
770
917
|
}
|
|
@@ -772,19 +919,11 @@ class BrowserCliContext {
|
|
|
772
919
|
if (!isTimeoutError(error)) {
|
|
773
920
|
return undefined;
|
|
774
921
|
}
|
|
922
|
+
if (this.pageId === undefined) {
|
|
923
|
+
return undefined;
|
|
924
|
+
}
|
|
775
925
|
const recoveryTimeoutMs = Math.max(this.timeoutMs, 10_000);
|
|
776
926
|
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
927
|
if (targetPageId === undefined) {
|
|
789
928
|
return undefined;
|
|
790
929
|
}
|
|
@@ -800,7 +939,7 @@ class BrowserCliContext {
|
|
|
800
939
|
sessionId: this.currentSessionId(),
|
|
801
940
|
requestedSessionId: this.requestedSessionId,
|
|
802
941
|
ignoredCompatibilityOptions: this.ignoredCompatibilityOptions,
|
|
803
|
-
tool: '
|
|
942
|
+
tool: 'take_snapshot',
|
|
804
943
|
recoveredFromTimeout: true,
|
|
805
944
|
pageId: fallback.pageId,
|
|
806
945
|
url,
|
|
@@ -852,7 +991,7 @@ class BrowserCliContext {
|
|
|
852
991
|
}
|
|
853
992
|
async takeMarkdownSnapshotForPage(pageId, timeoutMs = this.timeoutMs) {
|
|
854
993
|
await this.ensureToolsLoaded();
|
|
855
|
-
const snapshotTool = this.tools.find((tool) => normalizeName(tool.name) === '
|
|
994
|
+
const snapshotTool = this.tools.find((tool) => normalizeName(tool.name) === 'take_snapshot');
|
|
856
995
|
if (!snapshotTool) {
|
|
857
996
|
return undefined;
|
|
858
997
|
}
|
|
@@ -861,9 +1000,9 @@ class BrowserCliContext {
|
|
|
861
1000
|
if (pageKey) {
|
|
862
1001
|
args[pageKey] = pageId;
|
|
863
1002
|
}
|
|
864
|
-
const
|
|
865
|
-
if (
|
|
866
|
-
args[
|
|
1003
|
+
const formatKey = hasProperty(snapshotTool, 'format', 'pageStateFormat', 'page_state_format');
|
|
1004
|
+
if (formatKey) {
|
|
1005
|
+
args[formatKey] = 'markdown';
|
|
867
1006
|
}
|
|
868
1007
|
try {
|
|
869
1008
|
const result = await this.connection.callTool(snapshotTool.name, args, timeoutMs);
|
|
@@ -882,9 +1021,15 @@ class BrowserCliContext {
|
|
|
882
1021
|
}
|
|
883
1022
|
}
|
|
884
1023
|
mode() {
|
|
1024
|
+
if (this.devtoolsOnly) {
|
|
1025
|
+
return 'devtools';
|
|
1026
|
+
}
|
|
885
1027
|
return this.remoteUuid ? 'remote' : 'local';
|
|
886
1028
|
}
|
|
887
1029
|
currentSessionId() {
|
|
1030
|
+
if (this.devtoolsOnly) {
|
|
1031
|
+
return undefined;
|
|
1032
|
+
}
|
|
888
1033
|
if (this.remoteUuid) {
|
|
889
1034
|
return this.remoteUuid;
|
|
890
1035
|
}
|
|
@@ -896,6 +1041,30 @@ class BrowserCliContext {
|
|
|
896
1041
|
}
|
|
897
1042
|
return this.sessions.find((session) => session.connected)?.sessionId;
|
|
898
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
|
+
}
|
|
899
1068
|
}
|
|
900
1069
|
function shouldRequestPageStateForCommand(commandName) {
|
|
901
1070
|
return new Set([
|
|
@@ -920,6 +1089,38 @@ function looksLikePageContentText(text) {
|
|
|
920
1089
|
}
|
|
921
1090
|
return /```(?:markdown|text|html)/i.test(trimmed) || /Page State Format:/i.test(trimmed);
|
|
922
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
|
+
}
|
|
923
1124
|
function isLikelyIncompletePageContent(text, targetUrl) {
|
|
924
1125
|
const trimmed = text.trim();
|
|
925
1126
|
if (!trimmed) {
|
|
@@ -978,6 +1179,16 @@ function isTimeoutError(error) {
|
|
|
978
1179
|
const message = error instanceof Error ? error.message : String(error);
|
|
979
1180
|
return /timed out|timeout/i.test(message);
|
|
980
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
|
+
}
|
|
981
1192
|
function findBestPageMatchByUrl(pages, targetUrl) {
|
|
982
1193
|
const normalizedTarget = normalizeUrlForMatch(targetUrl);
|
|
983
1194
|
let bestExact;
|
|
@@ -1048,11 +1259,11 @@ function emitError(asJson, commandName, ctx, error) {
|
|
|
1048
1259
|
message += '\nHint: use `tabs` to list pages, then pass --page-id <id> to target a specific tab.';
|
|
1049
1260
|
}
|
|
1050
1261
|
if (asJson) {
|
|
1262
|
+
const context = ctx.outputContext();
|
|
1051
1263
|
console.log(JSON.stringify({
|
|
1052
1264
|
ok: false,
|
|
1053
1265
|
command: commandName,
|
|
1054
|
-
|
|
1055
|
-
mode: 'local',
|
|
1266
|
+
...context,
|
|
1056
1267
|
error: message,
|
|
1057
1268
|
}, null, 2));
|
|
1058
1269
|
return;
|
|
@@ -1458,6 +1669,12 @@ function withRequestDetailsArgs(tool, requestId) {
|
|
|
1458
1669
|
maybeAssign(args, tool, 'reqid', requestId);
|
|
1459
1670
|
return args;
|
|
1460
1671
|
}
|
|
1672
|
+
function withResizeArgs(tool, width, height) {
|
|
1673
|
+
const args = {};
|
|
1674
|
+
maybeAssign(args, tool, 'width', width);
|
|
1675
|
+
maybeAssign(args, tool, 'height', height);
|
|
1676
|
+
return args;
|
|
1677
|
+
}
|
|
1461
1678
|
function withRefArgs(tool, ref, extra = {}) {
|
|
1462
1679
|
const args = { ...extra };
|
|
1463
1680
|
const parsedRef = parseRef(ref);
|
|
@@ -1529,6 +1746,56 @@ function withFillFormArgs(tool, fields) {
|
|
|
1529
1746
|
maybeAssign(args, tool, 'elements', fields);
|
|
1530
1747
|
return args;
|
|
1531
1748
|
}
|
|
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) {
|
|
1783
|
+
const args = {};
|
|
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
|
+
}
|
|
1797
|
+
return args;
|
|
1798
|
+
}
|
|
1532
1799
|
function withWaitArgs(tool, text, timeoutMs) {
|
|
1533
1800
|
const args = {};
|
|
1534
1801
|
maybeAssign(args, tool, 'text', text);
|
|
@@ -1567,6 +1834,24 @@ function maybeAssign(target, tool, property, value) {
|
|
|
1567
1834
|
function stripUndefined(value) {
|
|
1568
1835
|
return Object.fromEntries(Object.entries(value).filter(([, entry]) => entry !== undefined));
|
|
1569
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
|
+
}
|
|
1570
1855
|
function parseRef(ref) {
|
|
1571
1856
|
const numeric = Number.parseInt(ref, 10);
|
|
1572
1857
|
return {
|