@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.
@@ -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 (ai or aria)', 'ai')
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
- // NOTE: resize command removed — no resize_page tool in the extension.
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
- // NOTE: scrollintoview, download, waitfordownload, upload commands removed — no matching tools.
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
- // NOTE: dialog command removed — no handle_dialog tool in the extension.
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.connection = new ExtensionConnection(init.port, init.debug, init.remoteUuid ? { uuid: init.remoteUuid, relayUrl: init.relayUrl } : undefined, init.remoteUuid ? undefined : { sessionId: init.sessionId });
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
- await delay(100);
333
- this.sessions = await this.connection.listSessions(1_500).catch(() => this.connection.getSessions());
334
- await this.connection.waitForToolsUpdate(500);
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.connection.isExtensionConnected()) {
401
+ if (this.isBackendConnected()) {
341
402
  return;
342
403
  }
343
- this.sessions = await this.connection.listSessions(1_500).catch(() => this.connection.getSessions());
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.connection.isExtensionConnected()) {
346
- throw new Error(this.connection.getConnectionErrorMessage());
410
+ if (!this.isBackendConnected() && this.tools.length === 0) {
411
+ throw new Error(this.getConnectionErrorMessage());
347
412
  }
348
413
  }
349
414
  async listSessions() {
350
- this.sessions = await this.connection.listSessions(this.timeoutMs);
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
- this.sessions = await this.connection.listSessions(STATUS_TOOLS_TIMEOUT_MS).catch(() => this.connection.getSessions());
364
- if (this.connection.isExtensionConnected()) {
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.remoteUuid ? 'remote' : 'local',
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.getStatus() === 'connected',
380
- extensionConnected: this.connection.isExtensionConnected(),
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: [wantsAria ? 'take_a11y_snapshot' : 'take_md_snapshot'],
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
- const result = await this.connection.callTool(tool.name, args, this.timeoutMs);
704
- return { tool: tool.name, args, result: result };
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
- throw new Error(`No compatible browser tool found for "${commandName}". Tried ${requested.join(', ')}. Available tools: ${this.tools.map((tool) => tool.name).join(', ')}`);
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
- if (this.connection.isExtensionConnected()) {
716
- try {
717
- this.tools = await this.connection.refreshTools(effectiveTimeout);
718
- }
719
- catch {
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) {
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: 'take_md_snapshot',
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: 'take_md_snapshot',
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) === 'take_md_snapshot');
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 pageStateKey = hasProperty(snapshotTool, 'pageStateFormat', 'page_state_format');
865
- if (pageStateKey) {
866
- args[pageStateKey] = 'markdown';
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
- profile: DEFAULT_BROWSER_PROFILE,
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 {