@vibebrowser/mcp 0.2.4 → 0.2.5

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.
@@ -0,0 +1,1473 @@
1
+ import { writeFileSync } from 'node:fs';
2
+ import { resolve } from 'node:path';
3
+ import { setTimeout as delay } from 'node:timers/promises';
4
+ import { ExtensionConnection } from './connection.js';
5
+ import { DEFAULT_WS_PORT } from './types.js';
6
+ const DEFAULT_TIMEOUT_MS = 30_000;
7
+ 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;
9
+ const DEFAULT_REMOTE_RELAY_URL = process.env.VIBE_REMOTE_RELAY_URL || process.env.VIBE_RELAY_URL;
10
+ export function registerBrowserCommand(program) {
11
+ const browser = buildBrowserCommand(program.command('browser'));
12
+ registerBrowserSubcommands(browser);
13
+ }
14
+ export function registerStandaloneBrowserCli(program) {
15
+ const browser = buildBrowserCommand(program);
16
+ registerBrowserSubcommands(browser);
17
+ }
18
+ function buildBrowserCommand(command) {
19
+ return command
20
+ .description('OpenClaw-compatible browser CLI for the connected Vibe browser session')
21
+ .option('--browser-profile <name>', 'Compatibility profile name', DEFAULT_BROWSER_PROFILE)
22
+ .option('--target <target>', 'OpenClaw compatibility target selector (accepted, not used by the Vibe browser CLI)')
23
+ .option('-p, --port <number>', 'WebSocket port for local relay (agent) connection', String(DEFAULT_WS_PORT))
24
+ .option('-d, --debug', 'Enable debug logging', false)
25
+ .option('-r, --remote <uuid>', 'Connect to a remote extension via public relay (provide the extension UUID)', DEFAULT_REMOTE_UUID)
26
+ .option('--relay-url <url>', 'Custom relay server URL', DEFAULT_REMOTE_RELAY_URL)
27
+ .option('--json', 'Emit machine-readable JSON output', false)
28
+ .option('--timeout <ms>', 'Command timeout in milliseconds', String(DEFAULT_TIMEOUT_MS));
29
+ }
30
+ function registerBrowserSubcommands(browser) {
31
+ browser
32
+ .command('status')
33
+ .description('Show browser bridge status')
34
+ .action(async function () {
35
+ await runBrowserCommand(this, 'status', false, async (ctx) => ctx.status());
36
+ });
37
+ browser
38
+ .command('start')
39
+ .description('Connect to the browser bridge and verify the session is reachable')
40
+ .action(async function () {
41
+ await runBrowserCommand(this, 'start', false, async (ctx) => {
42
+ const status = await ctx.status();
43
+ return {
44
+ ...status,
45
+ started: status.extensionConnected,
46
+ managedLifecycle: false,
47
+ note: status.extensionConnected
48
+ ? 'Connected to Vibe browser session'
49
+ : 'Vibe uses an attach-only browser session; no managed browser process was started',
50
+ };
51
+ });
52
+ });
53
+ browser
54
+ .command('stop')
55
+ .description('Compatibility no-op: the Vibe browser bridge does not manage browser process lifecycle')
56
+ .action(async function () {
57
+ await runBrowserCommand(this, 'stop', false, async (ctx) => ({
58
+ ...await ctx.status(),
59
+ stopped: false,
60
+ managedLifecycle: false,
61
+ note: 'The Vibe browser bridge does not own the browser process, so stop only disconnects this CLI session',
62
+ }));
63
+ });
64
+ browser
65
+ .command('tabs')
66
+ .description('List browser tabs/pages')
67
+ .action(async function () {
68
+ await runBrowserCommand(this, 'tabs', true, async (ctx) => ctx.listPages());
69
+ });
70
+ const tab = browser.command('tab').description('Tab helpers');
71
+ tab
72
+ .action(async function () {
73
+ await runBrowserCommand(this, 'tab', true, async (ctx) => ctx.listPages());
74
+ });
75
+ tab
76
+ .command('new [url]')
77
+ .description('Open a new tab')
78
+ .action(async function (url) {
79
+ await runBrowserCommand(this, 'tab new', true, async (ctx) => ctx.open(url));
80
+ });
81
+ tab
82
+ .command('select <id>')
83
+ .description('Focus a tab/page')
84
+ .action(async function (id) {
85
+ await runBrowserCommand(this, 'tab select', true, async (ctx) => ctx.focus(id));
86
+ });
87
+ tab
88
+ .command('close <id>')
89
+ .description('Close a tab/page')
90
+ .action(async function (id) {
91
+ await runBrowserCommand(this, 'tab close', true, async (ctx) => ctx.close(id));
92
+ });
93
+ browser
94
+ .command('open <url>')
95
+ .description('Open a URL in a new page or navigate using the best available tool')
96
+ .action(async function (url) {
97
+ await runBrowserCommand(this, 'open', true, async (ctx) => ctx.open(url));
98
+ });
99
+ browser
100
+ .command('navigate <url>')
101
+ .description('Navigate the active page to a URL')
102
+ .action(async function (url) {
103
+ await runBrowserCommand(this, 'navigate', true, async (ctx) => ctx.navigate(url));
104
+ });
105
+ browser
106
+ .command('focus <id>')
107
+ .description('Focus a tab/page')
108
+ .action(async function (id) {
109
+ await runBrowserCommand(this, 'focus', true, async (ctx) => ctx.focus(id));
110
+ });
111
+ browser
112
+ .command('close <id>')
113
+ .description('Close a tab/page')
114
+ .action(async function (id) {
115
+ await runBrowserCommand(this, 'close', true, async (ctx) => ctx.close(id));
116
+ });
117
+ browser
118
+ .command('snapshot')
119
+ .description('Capture a textual browser snapshot')
120
+ .option('--format <format>', 'Snapshot format (ai or aria)', 'ai')
121
+ .option('--limit <count>', 'Max visible lines/items to print in human mode')
122
+ .option('--interactive', 'Prefer interactive/ARIA-flavored snapshot output', false)
123
+ .option('--selector <selector>', 'Selector to scope the snapshot to')
124
+ .option('--frame <selector>', 'Frame selector for the snapshot')
125
+ .option('--compact', 'Compatibility flag', false)
126
+ .option('--depth <depth>', 'Compatibility flag')
127
+ .option('--efficient', 'Compatibility flag', false)
128
+ .option('--labels', 'Include labels when supported by backend', false)
129
+ .action(async function () {
130
+ await runBrowserCommand(this, 'snapshot', true, async (ctx, options) => ctx.snapshot({
131
+ format: String(options.format || 'ai'),
132
+ limit: options.limit ? parsePositiveInteger(String(options.limit), '--limit') : undefined,
133
+ interactive: Boolean(options.interactive),
134
+ selector: options.selector ? String(options.selector) : undefined,
135
+ frame: options.frame ? String(options.frame) : undefined,
136
+ compact: Boolean(options.compact),
137
+ depth: options.depth ? parsePositiveInteger(String(options.depth), '--depth') : undefined,
138
+ efficient: Boolean(options.efficient),
139
+ labels: Boolean(options.labels),
140
+ }));
141
+ });
142
+ browser
143
+ .command('screenshot')
144
+ .description('Capture a browser screenshot')
145
+ .option('--full-page', 'Request a full page screenshot when supported', false)
146
+ .option('--ref <ref>', 'Element ref/index')
147
+ .option('--output <path>', 'Write screenshot bytes to a file')
148
+ .option('--detail <level>', 'Screenshot detail level')
149
+ .option('--grayscale', 'Use grayscale when supported', false)
150
+ .action(async function () {
151
+ await runBrowserCommand(this, 'screenshot', true, async (ctx, options) => ctx.screenshot({
152
+ fullPage: Boolean(options.fullPage),
153
+ ref: options.ref ? String(options.ref) : undefined,
154
+ outputPath: options.output ? String(options.output) : undefined,
155
+ detail: options.detail ? String(options.detail) : undefined,
156
+ grayscale: Boolean(options.grayscale),
157
+ }));
158
+ });
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
+ });
184
+ browser
185
+ .command('requests')
186
+ .description('List network requests')
187
+ .option('--filter <pattern>', 'Substring filter')
188
+ .option('--limit <count>', 'Maximum requests to print')
189
+ .option('--clear', 'Compatibility flag', false)
190
+ .action(async function () {
191
+ await runBrowserCommand(this, 'requests', true, async (ctx, options) => ctx.requests({
192
+ filter: options.filter ? String(options.filter) : undefined,
193
+ limit: options.limit ? parsePositiveInteger(String(options.limit), '--limit') : undefined,
194
+ }));
195
+ });
196
+ browser
197
+ .command('responsebody [pattern]')
198
+ .description('Show a matching response body when network inspection is available')
199
+ .option('--max-chars <count>', 'Truncate response body output')
200
+ .action(async function (pattern) {
201
+ await runBrowserCommand(this, 'responsebody', true, async (ctx, options) => ctx.responseBody({
202
+ pattern,
203
+ maxChars: options.maxChars
204
+ ? parsePositiveInteger(String(options.maxChars), '--max-chars')
205
+ : undefined,
206
+ }));
207
+ });
208
+ browser
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
+ });
214
+ browser
215
+ .command('click <ref>')
216
+ .description('Click an element by ref/index')
217
+ .option('--double', 'Double click when supported', false)
218
+ .action(async function (ref) {
219
+ await runBrowserCommand(this, 'click', true, async (ctx, options) => ctx.click(ref, { double: Boolean(options.double) }));
220
+ });
221
+ browser
222
+ .command('type <ref> <text>')
223
+ .description('Type/fill text into an element')
224
+ .option('--submit', 'Submit after typing', false)
225
+ .action(async function (ref, text) {
226
+ await runBrowserCommand(this, 'type', true, async (ctx, options) => ctx.type(ref, text, { submit: Boolean(options.submit) }));
227
+ });
228
+ browser
229
+ .command('press <keys>')
230
+ .description('Press a key or key chord')
231
+ .action(async function (keys) {
232
+ await runBrowserCommand(this, 'press', true, async (ctx) => ctx.press(keys));
233
+ });
234
+ browser
235
+ .command('hover <ref>')
236
+ .description('Hover an element by ref/index')
237
+ .action(async function (ref) {
238
+ await runBrowserCommand(this, 'hover', true, async (ctx) => ctx.hover(ref));
239
+ });
240
+ browser
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
+ });
246
+ browser
247
+ .command('drag <source> <target>')
248
+ .description('Drag one element to another')
249
+ .action(async function (source, target) {
250
+ await runBrowserCommand(this, 'drag', true, async (ctx) => ctx.drag(source, target));
251
+ });
252
+ browser
253
+ .command('select <ref> <values...>')
254
+ .description('Select one or more values in a field')
255
+ .action(async function (ref, values) {
256
+ await runBrowserCommand(this, 'select', true, async (ctx) => ctx.select(ref, values));
257
+ });
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
+ browser
278
+ .command('fill')
279
+ .description('Fill a form using JSON field descriptors')
280
+ .requiredOption('--fields <json>', 'JSON array of field descriptors')
281
+ .action(async function () {
282
+ await runBrowserCommand(this, 'fill', true, async (ctx, options) => ctx.fillForm(String(options.fields)));
283
+ });
284
+ browser
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
+ });
297
+ browser
298
+ .command('wait')
299
+ .description('Wait for text or a short delay')
300
+ .option('--text <value...>', 'Texts to wait for')
301
+ .option('--seconds <seconds>', 'Wait for a fixed number of seconds')
302
+ .option('--timeout <ms>', 'Override command timeout')
303
+ .action(async function () {
304
+ await runBrowserCommand(this, 'wait', true, async (ctx, options, baseOptions) => ctx.wait({
305
+ text: Array.isArray(options.text) ? options.text.map(String) : undefined,
306
+ seconds: options.seconds ? Number(options.seconds) : undefined,
307
+ timeoutMs: options.timeout ? parsePositiveInteger(String(options.timeout), '--timeout') : parsePositiveInteger(baseOptions.timeout, '--timeout'),
308
+ }));
309
+ });
310
+ browser
311
+ .command('evaluate')
312
+ .description('Evaluate JavaScript through the browser tool backend')
313
+ .requiredOption('--fn <function>', 'Function to evaluate')
314
+ .option('--ref <ref>', 'Element ref/index argument')
315
+ .option('--args <json>', 'JSON array of additional arguments')
316
+ .action(async function () {
317
+ await runBrowserCommand(this, 'evaluate', true, async (ctx, options) => ctx.evaluate({
318
+ fn: String(options.fn),
319
+ ref: options.ref ? String(options.ref) : undefined,
320
+ argsJson: options.args ? String(options.args) : undefined,
321
+ }));
322
+ });
323
+ browser
324
+ .command('highlight <ref>')
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
+ });
348
+ }
349
+ async function runBrowserCommand(command, commandName, requireExtension, handler) {
350
+ const globalOptions = command.optsWithGlobals();
351
+ const localOptions = command.opts();
352
+ const ctx = new BrowserCliContext({
353
+ port: parsePositiveInteger(globalOptions.port, '--port'),
354
+ debug: Boolean(globalOptions.debug),
355
+ remoteUuid: globalOptions.remote,
356
+ relayUrl: globalOptions.relayUrl,
357
+ profile: globalOptions.browserProfile || DEFAULT_BROWSER_PROFILE,
358
+ json: Boolean(globalOptions.json),
359
+ timeoutMs: parsePositiveInteger(globalOptions.timeout, '--timeout'),
360
+ target: globalOptions.target,
361
+ });
362
+ try {
363
+ await ctx.connect();
364
+ if (requireExtension) {
365
+ await ctx.ensureExtensionConnected();
366
+ }
367
+ const output = await handler(ctx, localOptions, globalOptions);
368
+ emitOutput(Boolean(globalOptions.json), output, formatHumanOutput(commandName, output));
369
+ }
370
+ catch (error) {
371
+ emitError(Boolean(globalOptions.json), commandName, ctx, error);
372
+ process.exitCode = 1;
373
+ }
374
+ finally {
375
+ await ctx.shutdown();
376
+ }
377
+ }
378
+ class BrowserCliContext {
379
+ connection;
380
+ profile;
381
+ json;
382
+ timeoutMs;
383
+ remoteUuid;
384
+ target;
385
+ toolsLoaded = false;
386
+ tools = [];
387
+ ignoredCompatibilityOptions;
388
+ constructor(init) {
389
+ this.connection = new ExtensionConnection(init.port, init.debug, init.remoteUuid ? { uuid: init.remoteUuid, relayUrl: init.relayUrl } : undefined);
390
+ this.profile = init.profile;
391
+ this.json = init.json;
392
+ this.timeoutMs = init.timeoutMs;
393
+ this.remoteUuid = init.remoteUuid;
394
+ this.target = init.target;
395
+ this.ignoredCompatibilityOptions = [];
396
+ if (this.target) {
397
+ this.ignoredCompatibilityOptions.push(`target=${this.target}`);
398
+ }
399
+ }
400
+ async connect() {
401
+ await this.connection.start();
402
+ await delay(100);
403
+ await this.connection.waitForToolsUpdate(500);
404
+ }
405
+ async shutdown() {
406
+ await this.connection.stop();
407
+ }
408
+ async ensureExtensionConnected() {
409
+ if (this.connection.isExtensionConnected()) {
410
+ return;
411
+ }
412
+ await this.ensureToolsLoaded();
413
+ if (!this.connection.isExtensionConnected()) {
414
+ throw new Error('No browser extension is connected to the Vibe relay');
415
+ }
416
+ }
417
+ async status() {
418
+ if (this.connection.isExtensionConnected()) {
419
+ await this.ensureToolsLoaded();
420
+ }
421
+ return {
422
+ ok: true,
423
+ command: 'status',
424
+ profile: this.profile,
425
+ mode: this.remoteUuid ? 'remote' : 'local',
426
+ ignoredCompatibilityOptions: this.ignoredCompatibilityOptions,
427
+ relayConnected: this.connection.getStatus() === 'connected',
428
+ extensionConnected: this.connection.isExtensionConnected(),
429
+ managedLifecycle: false,
430
+ transport: 'vibebrowser-mcp',
431
+ toolCount: this.tools.length,
432
+ tools: this.tools.map((tool) => tool.name),
433
+ };
434
+ }
435
+ async listPages() {
436
+ const invocation = await this.callTool('tabs', [
437
+ { names: ['list_pages', 'get_tabs'] },
438
+ ], {});
439
+ const pages = extractPages(invocation.result);
440
+ return {
441
+ ok: true,
442
+ command: 'tabs',
443
+ profile: this.profile,
444
+ mode: this.mode(),
445
+ ignoredCompatibilityOptions: this.ignoredCompatibilityOptions,
446
+ tool: invocation.tool,
447
+ pages,
448
+ raw: normalizeToolResult(invocation.result),
449
+ };
450
+ }
451
+ async open(url) {
452
+ if (!url) {
453
+ return this.callGenericCommand('open', [{ names: ['new_page', 'create_new_tab'] }], {});
454
+ }
455
+ const invocation = await this.callTool('open', [
456
+ {
457
+ names: ['new_page', 'create_new_tab'],
458
+ buildArgs: (tool) => withUrlArgs(tool, url),
459
+ },
460
+ {
461
+ names: ['navigate_page', 'navigate_to_url'],
462
+ buildArgs: (tool) => withNavigateArgs(tool, url),
463
+ },
464
+ ], {});
465
+ return this.outputFromInvocation('open', invocation);
466
+ }
467
+ async navigate(url) {
468
+ const invocation = await this.callTool('navigate', [
469
+ {
470
+ names: ['navigate_page', 'navigate_to_url'],
471
+ buildArgs: (tool) => withNavigateArgs(tool, url),
472
+ },
473
+ {
474
+ names: ['new_page', 'create_new_tab'],
475
+ buildArgs: (tool) => withUrlArgs(tool, url),
476
+ },
477
+ ], {});
478
+ return this.outputFromInvocation('navigate', invocation);
479
+ }
480
+ async focus(id) {
481
+ const invocation = await this.callTool('focus', [
482
+ {
483
+ names: ['select_page', 'switch_to_tab', 'focus_tab'],
484
+ buildArgs: (tool) => withPageArgs(tool, id),
485
+ },
486
+ ], {});
487
+ return this.outputFromInvocation('focus', invocation);
488
+ }
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
+ async snapshot(options) {
499
+ 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
+ const invocation = await this.callTool('snapshot', [
515
+ {
516
+ names: ['take_a11y_snapshot'],
517
+ buildArgs: (tool) => withSnapshotArgs(tool, options),
518
+ },
519
+ {
520
+ names: ['take_snapshot', 'get_page_content'],
521
+ buildArgs: (tool) => withSnapshotArgs(tool, options),
522
+ },
523
+ ], {});
524
+ const text = firstText(invocation.result);
525
+ return {
526
+ ok: true,
527
+ command: 'snapshot',
528
+ profile: this.profile,
529
+ mode: this.mode(),
530
+ ignoredCompatibilityOptions: this.ignoredCompatibilityOptions,
531
+ tool: invocation.tool,
532
+ format: wantsAria ? 'aria' : options.format,
533
+ snapshot: limitText(text, options.limit),
534
+ raw: normalizeToolResult(invocation.result),
535
+ };
536
+ }
537
+ async screenshot(options) {
538
+ const invocation = await this.callTool('screenshot', [
539
+ {
540
+ names: ['take_screenshot', 'screenshot'],
541
+ buildArgs: (tool) => withScreenshotArgs(tool, options),
542
+ },
543
+ ], {});
544
+ const savedPath = maybeWriteBinaryOutput(invocation.result, options.outputPath);
545
+ return {
546
+ ok: true,
547
+ command: 'screenshot',
548
+ profile: this.profile,
549
+ mode: this.mode(),
550
+ ignoredCompatibilityOptions: this.ignoredCompatibilityOptions,
551
+ tool: invocation.tool,
552
+ outputPath: savedPath || options.outputPath,
553
+ raw: normalizeToolResult(invocation.result),
554
+ };
555
+ }
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
+ async requests(options) {
589
+ const invocation = await this.callTool('requests', [
590
+ {
591
+ names: ['list_network_requests'],
592
+ buildArgs: (tool) => withRequestListArgs(tool, options),
593
+ },
594
+ ], {});
595
+ let requests = extractRequests(invocation.result);
596
+ if (options.filter) {
597
+ const needle = options.filter.toLowerCase();
598
+ requests = requests.filter((request) => JSON.stringify(request).toLowerCase().includes(needle));
599
+ }
600
+ if (options.limit) {
601
+ requests = requests.slice(0, options.limit);
602
+ }
603
+ return {
604
+ ok: true,
605
+ command: 'requests',
606
+ profile: this.profile,
607
+ mode: this.mode(),
608
+ ignoredCompatibilityOptions: this.ignoredCompatibilityOptions,
609
+ tool: invocation.tool,
610
+ requests,
611
+ raw: normalizeToolResult(invocation.result),
612
+ };
613
+ }
614
+ async responseBody(options) {
615
+ const requests = await this.requests({ filter: options.pattern, limit: 1 });
616
+ const list = Array.isArray(requests.requests) ? requests.requests : [];
617
+ const first = list[0];
618
+ const requestId = first?.requestId ?? guessRequestId(requests.raw);
619
+ if (!requestId) {
620
+ throw new Error('No matching network request found');
621
+ }
622
+ const invocation = await this.callTool('responsebody', [
623
+ {
624
+ names: ['get_network_request'],
625
+ buildArgs: (tool) => withRequestDetailsArgs(tool, requestId),
626
+ },
627
+ ], {});
628
+ const body = limitText(extractResponseBody(invocation.result), options.maxChars);
629
+ return {
630
+ ok: true,
631
+ command: 'responsebody',
632
+ profile: this.profile,
633
+ mode: this.mode(),
634
+ ignoredCompatibilityOptions: this.ignoredCompatibilityOptions,
635
+ requestId,
636
+ tool: invocation.tool,
637
+ responseBody: body,
638
+ raw: normalizeToolResult(invocation.result),
639
+ };
640
+ }
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
+ async click(ref, options) {
651
+ const invocation = await this.callTool('click', [
652
+ {
653
+ names: ['click'],
654
+ buildArgs: (tool) => withRefArgs(tool, ref, { dblClick: options.double }),
655
+ },
656
+ ], {});
657
+ return this.outputFromInvocation('click', invocation);
658
+ }
659
+ async type(ref, text, options) {
660
+ const invocation = await this.callTool('type', [
661
+ {
662
+ names: ['fill'],
663
+ buildArgs: (tool) => withFillArgs(tool, ref, text),
664
+ },
665
+ {
666
+ names: ['type_text', 'type'],
667
+ buildArgs: (tool) => withTypeArgs(tool, text, options.submit ? 'Enter' : undefined),
668
+ },
669
+ ], {});
670
+ return this.outputFromInvocation('type', invocation);
671
+ }
672
+ async press(keys) {
673
+ const invocation = await this.callTool('press', [
674
+ {
675
+ names: ['press_key', 'keyboard_shortcut'],
676
+ buildArgs: (tool) => withKeysArgs(tool, keys),
677
+ },
678
+ ], {});
679
+ return this.outputFromInvocation('press', invocation);
680
+ }
681
+ async hover(ref) {
682
+ const invocation = await this.callTool('hover', [
683
+ {
684
+ names: ['hover'],
685
+ buildArgs: (tool) => withRefArgs(tool, ref),
686
+ },
687
+ ], {});
688
+ return this.outputFromInvocation('hover', invocation);
689
+ }
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
+ async drag(source, target) {
700
+ const invocation = await this.callTool('drag', [
701
+ {
702
+ names: ['drag'],
703
+ buildArgs: (tool) => withDragArgs(tool, source, target),
704
+ },
705
+ ], {});
706
+ return this.outputFromInvocation('drag', invocation);
707
+ }
708
+ async select(ref, values) {
709
+ const invocation = await this.callTool('select', [
710
+ {
711
+ names: ['fill'],
712
+ buildArgs: (tool) => withFillArgs(tool, ref, values.length === 1 ? values[0] : values),
713
+ },
714
+ {
715
+ names: ['select_option', 'select'],
716
+ buildArgs: (tool) => withSelectArgs(tool, ref, values),
717
+ },
718
+ ], {});
719
+ return this.outputFromInvocation('select', invocation);
720
+ }
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
+ async fillForm(fieldsJson) {
753
+ const fields = parseJsonValue(fieldsJson, '--fields');
754
+ if (!Array.isArray(fields)) {
755
+ throw new Error('--fields must be a JSON array');
756
+ }
757
+ const invocation = await this.callTool('fill', [
758
+ {
759
+ names: ['fill_form'],
760
+ buildArgs: (tool) => withFillFormArgs(tool, fields),
761
+ },
762
+ ], {});
763
+ return this.outputFromInvocation('fill', invocation);
764
+ }
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
+ async wait(options) {
776
+ if (options.text && options.text.length > 0) {
777
+ const invocation = await this.callTool('wait', [
778
+ {
779
+ names: ['wait_for'],
780
+ buildArgs: (tool) => withWaitArgs(tool, options.text, options.timeoutMs),
781
+ },
782
+ ], {});
783
+ return this.outputFromInvocation('wait', invocation);
784
+ }
785
+ if (typeof options.seconds === 'number' && Number.isFinite(options.seconds) && options.seconds >= 0) {
786
+ await delay(Math.round(options.seconds * 1000));
787
+ return {
788
+ ok: true,
789
+ command: 'wait',
790
+ profile: this.profile,
791
+ mode: this.mode(),
792
+ ignoredCompatibilityOptions: this.ignoredCompatibilityOptions,
793
+ waitedSeconds: options.seconds,
794
+ };
795
+ }
796
+ throw new Error('wait requires either --text or --seconds');
797
+ }
798
+ async evaluate(options) {
799
+ const invocation = await this.callTool('evaluate', [
800
+ {
801
+ names: ['evaluate_script'],
802
+ buildArgs: (tool) => withEvaluateArgs(tool, options.fn, options.ref, options.argsJson),
803
+ },
804
+ ], {});
805
+ return this.outputFromInvocation('evaluate', invocation);
806
+ }
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
+ async callGenericCommand(commandName, candidates, canonicalArgs) {
839
+ const invocation = await this.callTool(commandName, candidates, canonicalArgs);
840
+ return this.outputFromInvocation(commandName, invocation);
841
+ }
842
+ async callTool(commandName, candidates, canonicalArgs) {
843
+ await this.ensureToolsLoaded();
844
+ const available = new Map(this.tools.map((tool) => [normalizeName(tool.name), tool]));
845
+ for (const candidate of candidates) {
846
+ for (const candidateName of candidate.names) {
847
+ const tool = available.get(normalizeName(candidateName));
848
+ if (!tool) {
849
+ continue;
850
+ }
851
+ const args = candidate.buildArgs
852
+ ? candidate.buildArgs(tool)
853
+ : withCanonicalArgs(tool, canonicalArgs);
854
+ const result = await this.connection.callTool(tool.name, args);
855
+ return { tool: tool.name, args, result: result };
856
+ }
857
+ }
858
+ const requested = candidates.flatMap((candidate) => candidate.names);
859
+ throw new Error(`No compatible browser tool found for "${commandName}". Tried ${requested.join(', ')}. Available tools: ${this.tools.map((tool) => tool.name).join(', ')}`);
860
+ }
861
+ async ensureToolsLoaded() {
862
+ if (this.toolsLoaded && this.tools.length > 0) {
863
+ return;
864
+ }
865
+ if (this.connection.isExtensionConnected()) {
866
+ try {
867
+ this.tools = await this.connection.refreshTools(this.timeoutMs);
868
+ }
869
+ catch {
870
+ this.tools = await this.connection.waitForToolsUpdate(1_000);
871
+ }
872
+ }
873
+ else {
874
+ this.tools = this.connection.getTools();
875
+ }
876
+ this.toolsLoaded = true;
877
+ }
878
+ outputFromInvocation(commandName, invocation) {
879
+ return {
880
+ ok: !invocation.result.isError,
881
+ command: commandName,
882
+ profile: this.profile,
883
+ mode: this.mode(),
884
+ ignoredCompatibilityOptions: this.ignoredCompatibilityOptions,
885
+ tool: invocation.tool,
886
+ raw: normalizeToolResult(invocation.result),
887
+ };
888
+ }
889
+ mode() {
890
+ return this.remoteUuid ? 'remote' : 'local';
891
+ }
892
+ }
893
+ function emitOutput(asJson, data, humanOutput) {
894
+ if (asJson) {
895
+ console.log(JSON.stringify(data, null, 2));
896
+ return;
897
+ }
898
+ console.log(humanOutput);
899
+ }
900
+ function emitError(asJson, commandName, ctx, error) {
901
+ const message = error instanceof Error ? error.message : String(error);
902
+ if (asJson) {
903
+ console.log(JSON.stringify({
904
+ ok: false,
905
+ command: commandName,
906
+ profile: DEFAULT_BROWSER_PROFILE,
907
+ mode: 'local',
908
+ error: message,
909
+ }, null, 2));
910
+ return;
911
+ }
912
+ console.error(`Error: ${message}`);
913
+ }
914
+ function formatHumanOutput(commandName, output) {
915
+ switch (commandName) {
916
+ case 'status':
917
+ case 'start':
918
+ case 'stop':
919
+ return [
920
+ `Profile: ${String(output.profile)}`,
921
+ `Mode: ${String(output.mode)}`,
922
+ `Relay connected: ${boolText(output.relayConnected)}`,
923
+ `Extension connected: ${boolText(output.extensionConnected)}`,
924
+ `Managed lifecycle: ${boolText(output.managedLifecycle)}`,
925
+ output.toolCount !== undefined ? `Tools: ${String(output.toolCount)}` : null,
926
+ output.note ? String(output.note) : null,
927
+ ].filter(Boolean).join('\n');
928
+ case 'tabs':
929
+ case 'tab': {
930
+ const pages = Array.isArray(output.pages) ? output.pages : [];
931
+ if (pages.length === 0) {
932
+ return firstDefinedText(output.raw, 'No tabs reported by browser');
933
+ }
934
+ return pages.map((page, index) => `${index + 1}. ${page.active ? '[active] ' : ''}${page.title || '(untitled)'}${page.url ? ` — ${page.url}` : ''}${page.id !== undefined ? ` [id=${page.id}]` : ''}`).join('\n');
935
+ }
936
+ case 'snapshot':
937
+ return [
938
+ output.title ? `Title: ${String(output.title)}` : null,
939
+ output.url ? `URL: ${String(output.url)}` : null,
940
+ output.snapshot ? String(output.snapshot) : firstDefinedText(output.raw, ''),
941
+ ].filter(Boolean).join('\n');
942
+ case 'requests': {
943
+ const requests = Array.isArray(output.requests) ? output.requests : [];
944
+ if (requests.length === 0) {
945
+ return firstDefinedText(output.raw, 'No requests reported by browser');
946
+ }
947
+ 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
+ }
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
+ case 'responsebody':
958
+ return String(output.responseBody ?? firstDefinedText(output.raw, ''));
959
+ default:
960
+ if (output.outputPath) {
961
+ return `Saved to ${String(output.outputPath)}`;
962
+ }
963
+ return firstDefinedText(output.raw, JSON.stringify(output, null, 2));
964
+ }
965
+ }
966
+ function boolText(value) {
967
+ return value ? 'yes' : 'no';
968
+ }
969
+ function firstDefinedText(value, fallback) {
970
+ if (typeof value === 'string' && value.length > 0) {
971
+ return value;
972
+ }
973
+ if (value && typeof value === 'object' && 'text' in value && typeof value.text === 'string') {
974
+ return value.text;
975
+ }
976
+ return fallback;
977
+ }
978
+ function normalizeToolResult(result) {
979
+ return sanitizeUnknown(result) ?? null;
980
+ }
981
+ function sanitizeUnknown(value) {
982
+ if (value === null)
983
+ return null;
984
+ if (typeof value === 'boolean' || typeof value === 'number' || typeof value === 'string')
985
+ return value;
986
+ if (Array.isArray(value))
987
+ return value.map((item) => sanitizeUnknown(item) ?? null);
988
+ if (value && typeof value === 'object') {
989
+ const output = {};
990
+ for (const [key, entry] of Object.entries(value)) {
991
+ const sanitized = sanitizeUnknown(entry);
992
+ if (sanitized !== undefined) {
993
+ output[key] = sanitized;
994
+ }
995
+ }
996
+ return output;
997
+ }
998
+ return undefined;
999
+ }
1000
+ function maybeWriteBinaryOutput(result, outputPath) {
1001
+ if (!outputPath) {
1002
+ return undefined;
1003
+ }
1004
+ const image = result.content?.find((item) => item.type === 'image');
1005
+ if (!image || !('data' in image) || typeof image.data !== 'string') {
1006
+ return undefined;
1007
+ }
1008
+ const target = resolve(outputPath);
1009
+ writeFileSync(target, Buffer.from(image.data, 'base64'));
1010
+ return target;
1011
+ }
1012
+ function extractPages(result) {
1013
+ const direct = extractStructured(result, ['pages', 'tabs', 'targets']);
1014
+ if (Array.isArray(direct)) {
1015
+ return direct.map((entry) => normalizePage(entry));
1016
+ }
1017
+ const parsed = parseResultText(result);
1018
+ if (Array.isArray(parsed)) {
1019
+ return parsed.map((entry) => normalizePage(entry));
1020
+ }
1021
+ if (parsed && typeof parsed === 'object') {
1022
+ for (const key of ['pages', 'tabs', 'targets']) {
1023
+ const candidate = parsed[key];
1024
+ if (Array.isArray(candidate)) {
1025
+ return candidate.map((entry) => normalizePage(entry));
1026
+ }
1027
+ }
1028
+ }
1029
+ return [];
1030
+ }
1031
+ function normalizePage(value) {
1032
+ if (!value || typeof value !== 'object') {
1033
+ return {};
1034
+ }
1035
+ const record = value;
1036
+ return {
1037
+ ...sanitizeUnknown(record),
1038
+ id: firstDefined(record, ['pageId', 'tabId', 'id', 'targetId']),
1039
+ title: firstString(record, ['title', 'name']),
1040
+ url: firstString(record, ['url']),
1041
+ active: firstBoolean(record, ['active', 'selected', 'focused']),
1042
+ };
1043
+ }
1044
+ function extractRequests(result) {
1045
+ const direct = extractStructured(result, ['requests']);
1046
+ if (Array.isArray(direct)) {
1047
+ return direct.map((entry) => normalizeRequest(entry));
1048
+ }
1049
+ const parsed = parseResultText(result);
1050
+ if (Array.isArray(parsed)) {
1051
+ return parsed.map((entry) => normalizeRequest(entry));
1052
+ }
1053
+ if (parsed && typeof parsed === 'object') {
1054
+ const requests = parsed.requests;
1055
+ if (Array.isArray(requests)) {
1056
+ return requests.map((entry) => normalizeRequest(entry));
1057
+ }
1058
+ }
1059
+ return [];
1060
+ }
1061
+ function normalizeRequest(value) {
1062
+ if (!value || typeof value !== 'object') {
1063
+ return {};
1064
+ }
1065
+ const record = value;
1066
+ return {
1067
+ ...sanitizeUnknown(record),
1068
+ requestId: firstString(record, ['requestId', 'reqid', 'id']),
1069
+ url: firstString(record, ['url']),
1070
+ method: firstString(record, ['method']),
1071
+ status: firstNumber(record, ['status']),
1072
+ };
1073
+ }
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
+ function extractResponseBody(result) {
1095
+ const direct = extractStructured(result, ['responseBody', 'body']);
1096
+ if (typeof direct === 'string') {
1097
+ return direct;
1098
+ }
1099
+ const parsed = parseResultText(result);
1100
+ if (parsed && typeof parsed === 'object') {
1101
+ for (const key of ['responseBody', 'body']) {
1102
+ const candidate = parsed[key];
1103
+ if (typeof candidate === 'string') {
1104
+ return candidate;
1105
+ }
1106
+ }
1107
+ }
1108
+ return firstText(result);
1109
+ }
1110
+ function guessRequestId(value) {
1111
+ if (!value || typeof value !== 'object') {
1112
+ return undefined;
1113
+ }
1114
+ const record = value;
1115
+ if (typeof record.requestId === 'string') {
1116
+ return record.requestId;
1117
+ }
1118
+ return undefined;
1119
+ }
1120
+ function extractStructured(result, keys) {
1121
+ for (const key of keys) {
1122
+ if (key in result) {
1123
+ return result[key];
1124
+ }
1125
+ }
1126
+ return undefined;
1127
+ }
1128
+ function parseResultText(result) {
1129
+ const text = firstText(result);
1130
+ if (!text) {
1131
+ return undefined;
1132
+ }
1133
+ return parseJsonText(text);
1134
+ }
1135
+ function firstText(result) {
1136
+ const item = result.content?.find((entry) => entry.type === 'text');
1137
+ return item && 'text' in item && typeof item.text === 'string' ? item.text : '';
1138
+ }
1139
+ function parseJsonText(text) {
1140
+ const trimmed = text.trim();
1141
+ if (!trimmed) {
1142
+ return undefined;
1143
+ }
1144
+ const candidates = [trimmed];
1145
+ if (trimmed.startsWith('```')) {
1146
+ const fenced = trimmed
1147
+ .replace(/^```[a-zA-Z0-9_-]*\n/, '')
1148
+ .replace(/\n```$/, '')
1149
+ .trim();
1150
+ candidates.push(fenced);
1151
+ }
1152
+ for (const candidate of candidates) {
1153
+ try {
1154
+ return JSON.parse(candidate);
1155
+ }
1156
+ catch {
1157
+ // Try next candidate.
1158
+ }
1159
+ }
1160
+ return undefined;
1161
+ }
1162
+ function parseJsonValue(text, label) {
1163
+ try {
1164
+ return JSON.parse(text);
1165
+ }
1166
+ catch (error) {
1167
+ const message = error instanceof Error ? error.message : String(error);
1168
+ throw new Error(`${label} must be valid JSON: ${message}`);
1169
+ }
1170
+ }
1171
+ function stringifyJson(value) {
1172
+ return typeof value === 'string' ? value : JSON.stringify(value, null, 2);
1173
+ }
1174
+ function limitText(text, limit) {
1175
+ if (!limit || limit <= 0) {
1176
+ return text;
1177
+ }
1178
+ const lines = text.split('\n');
1179
+ return lines.length <= limit ? text : `${lines.slice(0, limit).join('\n')}\n...`;
1180
+ }
1181
+ function parsePositiveInteger(value, label) {
1182
+ const parsed = Number.parseInt(value, 10);
1183
+ if (!Number.isFinite(parsed) || parsed <= 0) {
1184
+ throw new Error(`${label} must be a positive integer`);
1185
+ }
1186
+ return parsed;
1187
+ }
1188
+ function normalizeName(value) {
1189
+ return value.replace(/[-\s]/g, '_').toLowerCase();
1190
+ }
1191
+ function toolProperties(tool) {
1192
+ return tool.inputSchema.properties ?? {};
1193
+ }
1194
+ function hasProperty(tool, ...names) {
1195
+ const properties = toolProperties(tool);
1196
+ return names.find((name) => Object.prototype.hasOwnProperty.call(properties, name));
1197
+ }
1198
+ function withCanonicalArgs(tool, canonicalArgs) {
1199
+ const output = {};
1200
+ for (const [key, value] of Object.entries(canonicalArgs)) {
1201
+ if (value === undefined) {
1202
+ continue;
1203
+ }
1204
+ if (Object.prototype.hasOwnProperty.call(toolProperties(tool), key)) {
1205
+ output[key] = value;
1206
+ }
1207
+ }
1208
+ return output;
1209
+ }
1210
+ function withUrlArgs(tool, url) {
1211
+ const args = {};
1212
+ const urlKey = hasProperty(tool, 'url');
1213
+ if (urlKey)
1214
+ args[urlKey] = url;
1215
+ const typeKey = hasProperty(tool, 'type');
1216
+ if (typeKey && normalizeName(tool.name) === 'navigate_page') {
1217
+ args[typeKey] = 'url';
1218
+ }
1219
+ return args;
1220
+ }
1221
+ function withNavigateArgs(tool, url) {
1222
+ const args = withUrlArgs(tool, url);
1223
+ const pageIdKey = hasProperty(tool, 'pageId', 'tabId');
1224
+ if (pageIdKey && !(pageIdKey in args)) {
1225
+ args[pageIdKey] = undefined;
1226
+ }
1227
+ return stripUndefined(args);
1228
+ }
1229
+ function withPageArgs(tool, id) {
1230
+ const args = {};
1231
+ const numeric = Number.parseInt(id, 10);
1232
+ const key = hasProperty(tool, 'pageId', 'tabId', 'id');
1233
+ if (key) {
1234
+ args[key] = Number.isFinite(numeric) && String(numeric) === id ? numeric : id;
1235
+ }
1236
+ return args;
1237
+ }
1238
+ function withSnapshotArgs(tool, options) {
1239
+ const args = {};
1240
+ maybeAssign(args, tool, 'format', options.format);
1241
+ maybeAssign(args, tool, 'selector', options.selector);
1242
+ maybeAssign(args, tool, 'frame', options.frame);
1243
+ maybeAssign(args, tool, 'compact', options.compact || undefined);
1244
+ maybeAssign(args, tool, 'depth', options.depth);
1245
+ maybeAssign(args, tool, 'efficient', options.efficient || undefined);
1246
+ maybeAssign(args, tool, 'labels', options.labels || undefined);
1247
+ maybeAssign(args, tool, 'a11y', true);
1248
+ maybeAssign(args, tool, 'markdown', false);
1249
+ return args;
1250
+ }
1251
+ function withScreenshotArgs(tool, options) {
1252
+ const args = {};
1253
+ if (options.ref) {
1254
+ Object.assign(args, withRefArgs(tool, options.ref));
1255
+ }
1256
+ maybeAssign(args, tool, 'fullPage', options.fullPage || undefined);
1257
+ maybeAssign(args, tool, 'detail', options.detail);
1258
+ maybeAssign(args, tool, 'grayscale', options.grayscale || undefined);
1259
+ return args;
1260
+ }
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
+ function withRequestListArgs(tool, options) {
1277
+ const args = {};
1278
+ if (options.limit) {
1279
+ maybeAssign(args, tool, 'limit', options.limit);
1280
+ maybeAssign(args, tool, 'pageSize', options.limit);
1281
+ }
1282
+ return args;
1283
+ }
1284
+ function withRequestDetailsArgs(tool, requestId) {
1285
+ const args = {};
1286
+ maybeAssign(args, tool, 'requestId', requestId);
1287
+ maybeAssign(args, tool, 'reqid', requestId);
1288
+ return args;
1289
+ }
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
+ function withRefArgs(tool, ref, extra = {}) {
1297
+ const args = { ...extra };
1298
+ const parsedRef = parseRef(ref);
1299
+ if (hasProperty(tool, 'ref')) {
1300
+ args.ref = parsedRef.raw;
1301
+ }
1302
+ if (parsedRef.numeric !== undefined && hasProperty(tool, 'index')) {
1303
+ args.index = parsedRef.numeric;
1304
+ }
1305
+ if (hasProperty(tool, 'uid')) {
1306
+ args.uid = parsedRef.raw;
1307
+ }
1308
+ if (hasProperty(tool, 'selector')) {
1309
+ args.selector = parsedRef.raw;
1310
+ }
1311
+ return args;
1312
+ }
1313
+ function withFillArgs(tool, ref, value) {
1314
+ const args = withRefArgs(tool, ref);
1315
+ if (hasProperty(tool, 'value')) {
1316
+ args.value = Array.isArray(value) ? value[0] : value;
1317
+ }
1318
+ if (hasProperty(tool, 'text')) {
1319
+ args.text = Array.isArray(value) ? value.join(' ') : value;
1320
+ }
1321
+ return args;
1322
+ }
1323
+ function withTypeArgs(tool, text, submitKey) {
1324
+ const args = {};
1325
+ maybeAssign(args, tool, 'text', text);
1326
+ maybeAssign(args, tool, 'value', text);
1327
+ if (submitKey) {
1328
+ maybeAssign(args, tool, 'submitKey', submitKey);
1329
+ }
1330
+ return args;
1331
+ }
1332
+ function withKeysArgs(tool, keys) {
1333
+ const args = {};
1334
+ maybeAssign(args, tool, 'keys', keys);
1335
+ maybeAssign(args, tool, 'shortcut', keys);
1336
+ maybeAssign(args, tool, 'key', keys);
1337
+ return args;
1338
+ }
1339
+ function withDragArgs(tool, source, target) {
1340
+ const args = {};
1341
+ if (hasProperty(tool, 'source')) {
1342
+ args.source = source;
1343
+ }
1344
+ if (hasProperty(tool, 'target')) {
1345
+ args.target = target;
1346
+ }
1347
+ if (hasProperty(tool, 'from_uid')) {
1348
+ args.from_uid = source;
1349
+ }
1350
+ if (hasProperty(tool, 'to_uid')) {
1351
+ args.to_uid = target;
1352
+ }
1353
+ return args;
1354
+ }
1355
+ function withSelectArgs(tool, ref, values) {
1356
+ const args = withRefArgs(tool, ref);
1357
+ maybeAssign(args, tool, 'value', values.length === 1 ? values[0] : values);
1358
+ maybeAssign(args, tool, 'values', values);
1359
+ return args;
1360
+ }
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
+ function withFillFormArgs(tool, fields) {
1382
+ const args = {};
1383
+ maybeAssign(args, tool, 'fields', fields);
1384
+ maybeAssign(args, tool, 'elements', fields);
1385
+ return args;
1386
+ }
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
+ function withWaitArgs(tool, text, timeoutMs) {
1394
+ const args = {};
1395
+ maybeAssign(args, tool, 'text', text);
1396
+ maybeAssign(args, tool, 'timeout', timeoutMs);
1397
+ return args;
1398
+ }
1399
+ function withEvaluateArgs(tool, fn, ref, argsJson) {
1400
+ const args = {};
1401
+ maybeAssign(args, tool, 'function', fn);
1402
+ const values = [];
1403
+ if (ref) {
1404
+ values.push(ref);
1405
+ }
1406
+ if (argsJson) {
1407
+ const parsed = parseJsonValue(argsJson, '--args');
1408
+ if (!Array.isArray(parsed)) {
1409
+ throw new Error('--args must be a JSON array');
1410
+ }
1411
+ for (const entry of parsed) {
1412
+ values.push(typeof entry === 'string' ? entry : JSON.stringify(entry));
1413
+ }
1414
+ }
1415
+ if (values.length > 0) {
1416
+ maybeAssign(args, tool, 'args', values);
1417
+ }
1418
+ return args;
1419
+ }
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
+ function maybeAssign(target, tool, property, value) {
1436
+ if (value === undefined) {
1437
+ return;
1438
+ }
1439
+ if (hasProperty(tool, property)) {
1440
+ target[property] = value;
1441
+ }
1442
+ }
1443
+ function stripUndefined(value) {
1444
+ return Object.fromEntries(Object.entries(value).filter(([, entry]) => entry !== undefined));
1445
+ }
1446
+ function parseRef(ref) {
1447
+ const numeric = Number.parseInt(ref, 10);
1448
+ return {
1449
+ raw: ref,
1450
+ numeric: Number.isFinite(numeric) && String(numeric) === ref ? numeric : undefined,
1451
+ };
1452
+ }
1453
+ function firstDefined(record, keys) {
1454
+ for (const key of keys) {
1455
+ if (record[key] !== undefined) {
1456
+ return record[key];
1457
+ }
1458
+ }
1459
+ return undefined;
1460
+ }
1461
+ function firstString(record, keys) {
1462
+ const value = firstDefined(record, keys);
1463
+ return typeof value === 'string' ? value : undefined;
1464
+ }
1465
+ function firstBoolean(record, keys) {
1466
+ const value = firstDefined(record, keys);
1467
+ return typeof value === 'boolean' ? value : undefined;
1468
+ }
1469
+ function firstNumber(record, keys) {
1470
+ const value = firstDefined(record, keys);
1471
+ return typeof value === 'number' ? value : undefined;
1472
+ }
1473
+ //# sourceMappingURL=browser-cli.js.map