@vibebrowser/cli 0.2.8
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 +13 -0
- package/dist/browser-cli.js +1881 -0
- package/dist/browser-main.js +11 -0
- package/dist/connection.js +536 -0
- package/dist/devtools-fallback.js +172 -0
- package/dist/relay-daemon.js +13 -0
- package/dist/relay.js +813 -0
- package/dist/types.js +22 -0
- package/dist/version.js +15 -0
- package/package.json +56 -0
|
@@ -0,0 +1,1881 @@
|
|
|
1
|
+
import { readFileSync, writeFileSync } from 'node:fs';
|
|
2
|
+
import { basename, extname, resolve } from 'node:path';
|
|
3
|
+
import { setTimeout as delay } from 'node:timers/promises';
|
|
4
|
+
import { ExtensionConnection } from './connection.js';
|
|
5
|
+
import { DevtoolsFallbackConnection } from './devtools-fallback.js';
|
|
6
|
+
import { DEFAULT_WS_PORT } from './types.js';
|
|
7
|
+
const DEFAULT_TIMEOUT_MS = 30_000;
|
|
8
|
+
/** Short timeout for the `status` command — it should never block for 30 s. */
|
|
9
|
+
const STATUS_TOOLS_TIMEOUT_MS = 2_000;
|
|
10
|
+
const MIME_TYPE_BY_EXTENSION = {
|
|
11
|
+
'.css': 'text/css',
|
|
12
|
+
'.csv': 'text/csv',
|
|
13
|
+
'.gif': 'image/gif',
|
|
14
|
+
'.html': 'text/html',
|
|
15
|
+
'.jpeg': 'image/jpeg',
|
|
16
|
+
'.jpg': 'image/jpeg',
|
|
17
|
+
'.js': 'text/javascript',
|
|
18
|
+
'.json': 'application/json',
|
|
19
|
+
'.md': 'text/markdown',
|
|
20
|
+
'.pdf': 'application/pdf',
|
|
21
|
+
'.png': 'image/png',
|
|
22
|
+
'.svg': 'image/svg+xml',
|
|
23
|
+
'.txt': 'text/plain',
|
|
24
|
+
'.webp': 'image/webp',
|
|
25
|
+
'.xml': 'application/xml',
|
|
26
|
+
};
|
|
27
|
+
const DEFAULT_BROWSER_PROFILE = process.env.VIBE_BROWSER_PROFILE || 'user';
|
|
28
|
+
const DEFAULT_REMOTE = process.env.VIBE_REMOTE_URL || process.env.VIBE_EXTENSION_UUID || process.env.VIBE_RELAY_UUID;
|
|
29
|
+
export function registerBrowserCommand(program) {
|
|
30
|
+
const browser = buildBrowserCommand(program.command('browser'));
|
|
31
|
+
registerBrowserSubcommands(browser);
|
|
32
|
+
}
|
|
33
|
+
export function registerStandaloneBrowserCli(program) {
|
|
34
|
+
const browser = buildBrowserCommand(program);
|
|
35
|
+
registerBrowserSubcommands(browser);
|
|
36
|
+
}
|
|
37
|
+
function buildBrowserCommand(command) {
|
|
38
|
+
return command
|
|
39
|
+
.description('OpenClaw-compatible browser CLI for the connected Vibe browser session')
|
|
40
|
+
.option('--browser-profile <name>', 'Compatibility profile name', DEFAULT_BROWSER_PROFILE)
|
|
41
|
+
.option('--target <target>', 'OpenClaw compatibility target selector (accepted, not used by the Vibe browser CLI)')
|
|
42
|
+
.option('-p, --port <number>', 'WebSocket port for local relay (agent) connection', String(DEFAULT_WS_PORT))
|
|
43
|
+
.option('-d, --debug', 'Enable debug logging', false)
|
|
44
|
+
.option('--devtools', 'Use only chrome-devtools backend (bypasses extension relay)', false)
|
|
45
|
+
.option('-r, --remote <uuid-or-url>', 'Connect to a remote extension via relay (provide the extension UUID or full ws(s) relay URL)', DEFAULT_REMOTE)
|
|
46
|
+
.option('-s, --session <id>', 'Target a specific local browser session ID; defaults to the first connected session')
|
|
47
|
+
.option('--json', 'Emit machine-readable JSON output', false)
|
|
48
|
+
.option('--timeout <ms>', 'Command timeout in milliseconds', String(DEFAULT_TIMEOUT_MS))
|
|
49
|
+
.option('--page-id <id>', 'Target a specific page/tab by its numeric ID (avoids switching the user\'s active tab)')
|
|
50
|
+
.option('--pageId <id>', 'Alias for --page-id');
|
|
51
|
+
}
|
|
52
|
+
function registerBrowserSubcommands(browser) {
|
|
53
|
+
browser
|
|
54
|
+
.command('status')
|
|
55
|
+
.description('Show browser bridge status')
|
|
56
|
+
.option('--wait-for-extension', 'Wait for extension connection before returning status', false)
|
|
57
|
+
.option('--wait-timeout <ms>', 'Maximum wait time when --wait-for-extension is enabled')
|
|
58
|
+
.option('--poll-interval <ms>', 'Polling interval while waiting for extension')
|
|
59
|
+
.action(async function () {
|
|
60
|
+
await runBrowserCommand(this, 'status', false, async (ctx, options) => ctx.status({
|
|
61
|
+
waitForExtension: Boolean(options.waitForExtension),
|
|
62
|
+
waitTimeoutMs: options.waitTimeout
|
|
63
|
+
? parsePositiveInteger(String(options.waitTimeout), '--wait-timeout')
|
|
64
|
+
: undefined,
|
|
65
|
+
pollIntervalMs: options.pollInterval
|
|
66
|
+
? parsePositiveInteger(String(options.pollInterval), '--poll-interval')
|
|
67
|
+
: undefined,
|
|
68
|
+
}));
|
|
69
|
+
});
|
|
70
|
+
browser
|
|
71
|
+
.command('start')
|
|
72
|
+
.description('Connect to the browser bridge and verify the session is reachable')
|
|
73
|
+
.action(async function () {
|
|
74
|
+
await runBrowserCommand(this, 'start', false, async (ctx) => {
|
|
75
|
+
const status = await ctx.status();
|
|
76
|
+
return {
|
|
77
|
+
...status,
|
|
78
|
+
started: status.extensionConnected,
|
|
79
|
+
managedLifecycle: false,
|
|
80
|
+
note: status.extensionConnected
|
|
81
|
+
? 'Connected to Vibe browser session'
|
|
82
|
+
: 'Vibe uses an attach-only browser session; no managed browser process was started',
|
|
83
|
+
};
|
|
84
|
+
});
|
|
85
|
+
});
|
|
86
|
+
browser
|
|
87
|
+
.command('stop')
|
|
88
|
+
.description('Compatibility no-op: the Vibe browser bridge does not manage browser process lifecycle')
|
|
89
|
+
.action(async function () {
|
|
90
|
+
await runBrowserCommand(this, 'stop', false, async (ctx) => ({
|
|
91
|
+
...await ctx.status(),
|
|
92
|
+
stopped: false,
|
|
93
|
+
managedLifecycle: false,
|
|
94
|
+
note: 'The Vibe browser bridge does not own the browser process, so stop only disconnects this CLI session',
|
|
95
|
+
}));
|
|
96
|
+
});
|
|
97
|
+
browser
|
|
98
|
+
.command('sessions')
|
|
99
|
+
.description('List connected browser sessions')
|
|
100
|
+
.action(async function () {
|
|
101
|
+
await runBrowserCommand(this, 'sessions', false, async (ctx) => ctx.listSessions());
|
|
102
|
+
});
|
|
103
|
+
browser
|
|
104
|
+
.command('tabs')
|
|
105
|
+
.description('List browser tabs/pages')
|
|
106
|
+
.action(async function () {
|
|
107
|
+
await runBrowserCommand(this, 'tabs', true, async (ctx) => ctx.listPages());
|
|
108
|
+
});
|
|
109
|
+
const tab = browser.command('tab').description('Tab helpers');
|
|
110
|
+
tab
|
|
111
|
+
.action(async function () {
|
|
112
|
+
await runBrowserCommand(this, 'tab', true, async (ctx) => ctx.listPages());
|
|
113
|
+
});
|
|
114
|
+
tab
|
|
115
|
+
.command('new [url]')
|
|
116
|
+
.description('Open a new tab')
|
|
117
|
+
.action(async function (url) {
|
|
118
|
+
await runBrowserCommand(this, 'tab new', true, async (ctx) => ctx.open(url));
|
|
119
|
+
});
|
|
120
|
+
tab
|
|
121
|
+
.command('select <id>')
|
|
122
|
+
.description('Switch to a tab/page by its ID (rarely needed — most commands accept a tab id argument instead, prefer that to avoid disrupting the user\'s browser)')
|
|
123
|
+
.action(async function (id) {
|
|
124
|
+
await runBrowserCommand(this, 'tab select', true, async (ctx) => ctx.focus(id));
|
|
125
|
+
});
|
|
126
|
+
tab
|
|
127
|
+
.command('close <id>')
|
|
128
|
+
.description('Close a tab/page')
|
|
129
|
+
.action(async function (id) {
|
|
130
|
+
await runBrowserCommand(this, 'tab close', true, async (ctx) => ctx.close(id));
|
|
131
|
+
});
|
|
132
|
+
browser
|
|
133
|
+
.command('open <url>')
|
|
134
|
+
.description('Open a URL in a new page or navigate using the best available tool')
|
|
135
|
+
.action(async function (url) {
|
|
136
|
+
await runBrowserCommand(this, 'open', true, async (ctx) => ctx.open(url));
|
|
137
|
+
});
|
|
138
|
+
browser
|
|
139
|
+
.command('navigate <url>')
|
|
140
|
+
.description('Navigate the active page to a URL')
|
|
141
|
+
.action(async function (url) {
|
|
142
|
+
await runBrowserCommand(this, 'navigate', true, async (ctx) => ctx.navigate(url));
|
|
143
|
+
});
|
|
144
|
+
browser
|
|
145
|
+
.command('focus <id>')
|
|
146
|
+
.description('Switch focus to a tab/page by its ID (rarely needed — most commands accept a tab id argument instead, prefer that to avoid disrupting the user\'s browser)')
|
|
147
|
+
.action(async function (id) {
|
|
148
|
+
await runBrowserCommand(this, 'focus', true, async (ctx) => ctx.focus(id));
|
|
149
|
+
});
|
|
150
|
+
browser
|
|
151
|
+
.command('close <id>')
|
|
152
|
+
.description('Close a tab/page')
|
|
153
|
+
.action(async function (id) {
|
|
154
|
+
await runBrowserCommand(this, 'close', true, async (ctx) => ctx.close(id));
|
|
155
|
+
});
|
|
156
|
+
browser
|
|
157
|
+
.command('snapshot')
|
|
158
|
+
.description('Capture a textual browser snapshot')
|
|
159
|
+
.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')
|
|
160
|
+
.option('--limit <count>', 'Max visible lines/items to print in human mode')
|
|
161
|
+
.option('--interactive', 'Prefer interactive/ARIA-flavored snapshot output', false)
|
|
162
|
+
.option('--selector <selector>', 'Selector to scope the snapshot to')
|
|
163
|
+
.option('--frame <selector>', 'Frame selector for the snapshot')
|
|
164
|
+
.option('--compact', 'Compatibility flag', false)
|
|
165
|
+
.option('--depth <depth>', 'Compatibility flag')
|
|
166
|
+
.option('--efficient', 'Compatibility flag', false)
|
|
167
|
+
.option('--labels', 'Include labels when supported by backend', false)
|
|
168
|
+
.action(async function () {
|
|
169
|
+
await runBrowserCommand(this, 'snapshot', true, async (ctx, options) => ctx.snapshot({
|
|
170
|
+
format: String(options.format || 'ai'),
|
|
171
|
+
limit: options.limit ? parsePositiveInteger(String(options.limit), '--limit') : undefined,
|
|
172
|
+
interactive: Boolean(options.interactive),
|
|
173
|
+
selector: options.selector ? String(options.selector) : undefined,
|
|
174
|
+
frame: options.frame ? String(options.frame) : undefined,
|
|
175
|
+
compact: Boolean(options.compact),
|
|
176
|
+
depth: options.depth ? parsePositiveInteger(String(options.depth), '--depth') : undefined,
|
|
177
|
+
efficient: Boolean(options.efficient),
|
|
178
|
+
labels: Boolean(options.labels),
|
|
179
|
+
}));
|
|
180
|
+
});
|
|
181
|
+
browser
|
|
182
|
+
.command('screenshot')
|
|
183
|
+
.description('Capture a browser screenshot')
|
|
184
|
+
.option('--full-page', 'Request a full page screenshot when supported', false)
|
|
185
|
+
.option('--ref <ref>', 'Element ref/index')
|
|
186
|
+
.option('--output <path>', 'Write screenshot bytes to a file')
|
|
187
|
+
.option('--detail <level>', 'Screenshot detail level')
|
|
188
|
+
.option('--grayscale', 'Use grayscale when supported', false)
|
|
189
|
+
.action(async function () {
|
|
190
|
+
await runBrowserCommand(this, 'screenshot', true, async (ctx, options) => ctx.screenshot({
|
|
191
|
+
fullPage: Boolean(options.fullPage),
|
|
192
|
+
ref: options.ref ? String(options.ref) : undefined,
|
|
193
|
+
outputPath: options.output ? String(options.output) : undefined,
|
|
194
|
+
detail: options.detail ? String(options.detail) : undefined,
|
|
195
|
+
grayscale: Boolean(options.grayscale),
|
|
196
|
+
}));
|
|
197
|
+
});
|
|
198
|
+
// NOTE: pdf, console, errors commands removed — no matching browser tools.
|
|
199
|
+
browser
|
|
200
|
+
.command('requests')
|
|
201
|
+
.description('List network requests')
|
|
202
|
+
.option('--filter <pattern>', 'Substring filter')
|
|
203
|
+
.option('--limit <count>', 'Maximum requests to print')
|
|
204
|
+
.option('--clear', 'Compatibility flag', false)
|
|
205
|
+
.action(async function () {
|
|
206
|
+
await runBrowserCommand(this, 'requests', true, async (ctx, options) => ctx.requests({
|
|
207
|
+
filter: options.filter ? String(options.filter) : undefined,
|
|
208
|
+
limit: options.limit ? parsePositiveInteger(String(options.limit), '--limit') : undefined,
|
|
209
|
+
}));
|
|
210
|
+
});
|
|
211
|
+
browser
|
|
212
|
+
.command('responsebody [pattern]')
|
|
213
|
+
.description('Show a matching response body when network inspection is available')
|
|
214
|
+
.option('--max-chars <count>', 'Truncate response body output')
|
|
215
|
+
.action(async function (pattern) {
|
|
216
|
+
await runBrowserCommand(this, 'responsebody', true, async (ctx, options) => ctx.responseBody({
|
|
217
|
+
pattern,
|
|
218
|
+
maxChars: options.maxChars
|
|
219
|
+
? parsePositiveInteger(String(options.maxChars), '--max-chars')
|
|
220
|
+
: undefined,
|
|
221
|
+
}));
|
|
222
|
+
});
|
|
223
|
+
browser
|
|
224
|
+
.command('resize <width> <height>')
|
|
225
|
+
.description('Resize the current page viewport/window')
|
|
226
|
+
.action(async function (width, height) {
|
|
227
|
+
await runBrowserCommand(this, 'resize', true, async (ctx) => ctx.resize(parsePositiveInteger(width, '<width>'), parsePositiveInteger(height, '<height>')));
|
|
228
|
+
});
|
|
229
|
+
browser
|
|
230
|
+
.command('click <ref>')
|
|
231
|
+
.description('Click an element by ref/index')
|
|
232
|
+
.option('--double', 'Double click when supported', false)
|
|
233
|
+
.action(async function (ref) {
|
|
234
|
+
await runBrowserCommand(this, 'click', true, async (ctx, options) => ctx.click(ref, { double: Boolean(options.double) }));
|
|
235
|
+
});
|
|
236
|
+
browser
|
|
237
|
+
.command('type <ref> <text>')
|
|
238
|
+
.description('Type/fill text into an element')
|
|
239
|
+
.option('--submit', 'Submit after typing', false)
|
|
240
|
+
.action(async function (ref, text) {
|
|
241
|
+
await runBrowserCommand(this, 'type', true, async (ctx, options) => ctx.type(ref, text, { submit: Boolean(options.submit) }));
|
|
242
|
+
});
|
|
243
|
+
browser
|
|
244
|
+
.command('press <keys>')
|
|
245
|
+
.description('Press a key or key chord')
|
|
246
|
+
.action(async function (keys) {
|
|
247
|
+
await runBrowserCommand(this, 'press', true, async (ctx) => ctx.press(keys));
|
|
248
|
+
});
|
|
249
|
+
browser
|
|
250
|
+
.command('hover <ref>')
|
|
251
|
+
.description('Hover an element by ref/index')
|
|
252
|
+
.action(async function (ref) {
|
|
253
|
+
await runBrowserCommand(this, 'hover', true, async (ctx) => ctx.hover(ref));
|
|
254
|
+
});
|
|
255
|
+
browser
|
|
256
|
+
.command('upload <ref> <path>')
|
|
257
|
+
.description('Upload a local file via an input element reference')
|
|
258
|
+
.action(async function (ref, path) {
|
|
259
|
+
await runBrowserCommand(this, 'upload', true, async (ctx) => ctx.upload(ref, path));
|
|
260
|
+
});
|
|
261
|
+
// NOTE: scrollintoview, download, waitfordownload commands removed — no matching tools.
|
|
262
|
+
browser
|
|
263
|
+
.command('drag <source> <target>')
|
|
264
|
+
.description('Drag one element to another')
|
|
265
|
+
.action(async function (source, target) {
|
|
266
|
+
await runBrowserCommand(this, 'drag', true, async (ctx) => ctx.drag(source, target));
|
|
267
|
+
});
|
|
268
|
+
browser
|
|
269
|
+
.command('select <ref> <values...>')
|
|
270
|
+
.description('Select one or more values in a field')
|
|
271
|
+
.action(async function (ref, values) {
|
|
272
|
+
await runBrowserCommand(this, 'select', true, async (ctx) => ctx.select(ref, values));
|
|
273
|
+
});
|
|
274
|
+
browser
|
|
275
|
+
.command('fill')
|
|
276
|
+
.description('Fill a form using JSON field descriptors')
|
|
277
|
+
.requiredOption('--fields <json>', 'JSON array of field descriptors')
|
|
278
|
+
.action(async function () {
|
|
279
|
+
await runBrowserCommand(this, 'fill', true, async (ctx, options) => ctx.fillForm(String(options.fields)));
|
|
280
|
+
});
|
|
281
|
+
browser
|
|
282
|
+
.command('dialog')
|
|
283
|
+
.description('Handle browser dialog (accept/dismiss prompt/confirm/alert)')
|
|
284
|
+
.option('--accept', 'Accept dialog', false)
|
|
285
|
+
.option('--dismiss', 'Dismiss dialog', false)
|
|
286
|
+
.option('--prompt <text>', 'Optional prompt text when accepting')
|
|
287
|
+
.action(async function () {
|
|
288
|
+
await runBrowserCommand(this, 'dialog', true, async (ctx, options) => ctx.dialog({
|
|
289
|
+
accept: Boolean(options.accept),
|
|
290
|
+
dismiss: Boolean(options.dismiss),
|
|
291
|
+
promptText: options.prompt ? String(options.prompt) : undefined,
|
|
292
|
+
}));
|
|
293
|
+
});
|
|
294
|
+
browser
|
|
295
|
+
.command('wait')
|
|
296
|
+
.description('Wait for text or a short delay')
|
|
297
|
+
.option('--text <value...>', 'Texts to wait for')
|
|
298
|
+
.option('--seconds <seconds>', 'Wait for a fixed number of seconds')
|
|
299
|
+
.option('--timeout <ms>', 'Override command timeout')
|
|
300
|
+
.action(async function () {
|
|
301
|
+
await runBrowserCommand(this, 'wait', true, async (ctx, options, baseOptions) => ctx.wait({
|
|
302
|
+
text: Array.isArray(options.text) ? options.text.map(String) : undefined,
|
|
303
|
+
seconds: options.seconds ? Number(options.seconds) : undefined,
|
|
304
|
+
timeoutMs: options.timeout ? parsePositiveInteger(String(options.timeout), '--timeout') : parsePositiveInteger(baseOptions.timeout, '--timeout'),
|
|
305
|
+
}));
|
|
306
|
+
});
|
|
307
|
+
browser
|
|
308
|
+
.command('evaluate')
|
|
309
|
+
.description('Evaluate JavaScript through the browser tool backend')
|
|
310
|
+
.requiredOption('--fn <function>', 'Function to evaluate')
|
|
311
|
+
.option('--ref <ref>', 'Element ref/index argument')
|
|
312
|
+
.option('--args <json>', 'JSON array of additional arguments')
|
|
313
|
+
.action(async function () {
|
|
314
|
+
await runBrowserCommand(this, 'evaluate', true, async (ctx, options) => ctx.evaluate({
|
|
315
|
+
fn: String(options.fn),
|
|
316
|
+
ref: options.ref ? String(options.ref) : undefined,
|
|
317
|
+
argsJson: options.args ? String(options.args) : undefined,
|
|
318
|
+
}));
|
|
319
|
+
});
|
|
320
|
+
// NOTE: highlight command removed — no highlight tool; users can use 'hover' directly.
|
|
321
|
+
// NOTE: trace commands removed — no performance_start_trace/performance_stop_trace tools.
|
|
322
|
+
}
|
|
323
|
+
async function runBrowserCommand(command, commandName, requireExtension, handler) {
|
|
324
|
+
const globalOptions = command.optsWithGlobals();
|
|
325
|
+
const localOptions = command.opts();
|
|
326
|
+
const ctx = new BrowserCliContext({
|
|
327
|
+
port: parsePositiveInteger(globalOptions.port, '--port'),
|
|
328
|
+
debug: Boolean(globalOptions.debug),
|
|
329
|
+
devtools: Boolean(globalOptions.devtools),
|
|
330
|
+
remoteUuid: globalOptions.remote,
|
|
331
|
+
sessionId: globalOptions.session,
|
|
332
|
+
profile: globalOptions.browserProfile || DEFAULT_BROWSER_PROFILE,
|
|
333
|
+
json: Boolean(globalOptions.json),
|
|
334
|
+
timeoutMs: parsePositiveInteger(globalOptions.timeout, '--timeout'),
|
|
335
|
+
target: globalOptions.target,
|
|
336
|
+
pageId: globalOptions.pageId ? parsePositiveInteger(globalOptions.pageId, '--page-id') : undefined,
|
|
337
|
+
});
|
|
338
|
+
try {
|
|
339
|
+
await ctx.connect();
|
|
340
|
+
if (requireExtension) {
|
|
341
|
+
await ctx.ensureExtensionConnected();
|
|
342
|
+
}
|
|
343
|
+
const output = await handler(ctx, localOptions, globalOptions);
|
|
344
|
+
emitOutput(Boolean(globalOptions.json), output, formatHumanOutput(commandName, output));
|
|
345
|
+
}
|
|
346
|
+
catch (error) {
|
|
347
|
+
emitError(Boolean(globalOptions.json), commandName, ctx, error);
|
|
348
|
+
process.exitCode = 1;
|
|
349
|
+
}
|
|
350
|
+
finally {
|
|
351
|
+
await ctx.shutdown();
|
|
352
|
+
}
|
|
353
|
+
}
|
|
354
|
+
class BrowserCliContext {
|
|
355
|
+
connection;
|
|
356
|
+
profile;
|
|
357
|
+
json;
|
|
358
|
+
timeoutMs;
|
|
359
|
+
devtoolsOnly;
|
|
360
|
+
remoteUuid;
|
|
361
|
+
requestedSessionId;
|
|
362
|
+
target;
|
|
363
|
+
pageId;
|
|
364
|
+
toolsLoaded = false;
|
|
365
|
+
tools = [];
|
|
366
|
+
sessions = [];
|
|
367
|
+
ignoredCompatibilityOptions;
|
|
368
|
+
constructor(init) {
|
|
369
|
+
this.devtoolsOnly = init.devtools;
|
|
370
|
+
this.connection = init.devtools
|
|
371
|
+
? new DevtoolsFallbackConnection(init.debug)
|
|
372
|
+
: new ExtensionConnection(init.port, init.debug, init.remoteUuid ? { uuid: init.remoteUuid } : undefined, init.remoteUuid ? undefined : { sessionId: init.sessionId });
|
|
373
|
+
this.profile = init.profile;
|
|
374
|
+
this.json = init.json;
|
|
375
|
+
this.timeoutMs = init.timeoutMs;
|
|
376
|
+
this.remoteUuid = init.remoteUuid;
|
|
377
|
+
this.requestedSessionId = init.sessionId;
|
|
378
|
+
this.target = init.target;
|
|
379
|
+
this.pageId = init.pageId;
|
|
380
|
+
this.ignoredCompatibilityOptions = [];
|
|
381
|
+
if (this.target) {
|
|
382
|
+
this.ignoredCompatibilityOptions.push(`target=${this.target}`);
|
|
383
|
+
}
|
|
384
|
+
}
|
|
385
|
+
async connect() {
|
|
386
|
+
await this.connection.start();
|
|
387
|
+
if (this.connection instanceof ExtensionConnection) {
|
|
388
|
+
const extension = this.connection;
|
|
389
|
+
await delay(100);
|
|
390
|
+
this.sessions = await extension.listSessions(1_500).catch(() => extension.getSessions());
|
|
391
|
+
await extension.waitForToolsUpdate(500);
|
|
392
|
+
}
|
|
393
|
+
}
|
|
394
|
+
async shutdown() {
|
|
395
|
+
await this.connection.stop();
|
|
396
|
+
}
|
|
397
|
+
async ensureExtensionConnected() {
|
|
398
|
+
if (this.isBackendConnected()) {
|
|
399
|
+
return;
|
|
400
|
+
}
|
|
401
|
+
if (this.connection instanceof ExtensionConnection) {
|
|
402
|
+
const extension = this.connection;
|
|
403
|
+
this.sessions = await extension.listSessions(1_500).catch(() => extension.getSessions());
|
|
404
|
+
}
|
|
405
|
+
this.toolsLoaded = false;
|
|
406
|
+
await this.ensureToolsLoaded();
|
|
407
|
+
if (!this.isBackendConnected() && this.tools.length === 0) {
|
|
408
|
+
throw new Error(this.getConnectionErrorMessage());
|
|
409
|
+
}
|
|
410
|
+
}
|
|
411
|
+
async listSessions() {
|
|
412
|
+
if (this.connection instanceof ExtensionConnection) {
|
|
413
|
+
this.sessions = await this.connection.listSessions(this.timeoutMs);
|
|
414
|
+
}
|
|
415
|
+
else {
|
|
416
|
+
this.sessions = [];
|
|
417
|
+
}
|
|
418
|
+
return {
|
|
419
|
+
ok: true,
|
|
420
|
+
command: 'sessions',
|
|
421
|
+
profile: this.profile,
|
|
422
|
+
mode: this.mode(),
|
|
423
|
+
ignoredCompatibilityOptions: this.ignoredCompatibilityOptions,
|
|
424
|
+
sessionId: this.currentSessionId(),
|
|
425
|
+
requestedSessionId: this.requestedSessionId,
|
|
426
|
+
sessions: this.sessions,
|
|
427
|
+
};
|
|
428
|
+
}
|
|
429
|
+
async status(options = {}) {
|
|
430
|
+
const waitForExtension = options.waitForExtension === true;
|
|
431
|
+
const waitTimeoutMs = options.waitTimeoutMs ?? this.timeoutMs;
|
|
432
|
+
const pollIntervalMs = options.pollIntervalMs ?? 250;
|
|
433
|
+
const waitStartedAt = Date.now();
|
|
434
|
+
if (this.connection instanceof ExtensionConnection) {
|
|
435
|
+
const extension = this.connection;
|
|
436
|
+
this.sessions = await extension.listSessions(STATUS_TOOLS_TIMEOUT_MS).catch(() => extension.getSessions());
|
|
437
|
+
}
|
|
438
|
+
else {
|
|
439
|
+
this.sessions = [];
|
|
440
|
+
}
|
|
441
|
+
if (waitForExtension && !this.isBackendConnected()) {
|
|
442
|
+
const deadline = Date.now() + waitTimeoutMs;
|
|
443
|
+
while (!this.isBackendConnected() && Date.now() < deadline) {
|
|
444
|
+
const remainingMs = deadline - Date.now();
|
|
445
|
+
if (remainingMs <= 0) {
|
|
446
|
+
break;
|
|
447
|
+
}
|
|
448
|
+
await delay(Math.min(pollIntervalMs, remainingMs));
|
|
449
|
+
if (this.connection instanceof ExtensionConnection) {
|
|
450
|
+
const extension = this.connection;
|
|
451
|
+
this.sessions = await extension.listSessions(STATUS_TOOLS_TIMEOUT_MS).catch(() => extension.getSessions());
|
|
452
|
+
}
|
|
453
|
+
}
|
|
454
|
+
}
|
|
455
|
+
if (this.isBackendConnected()) {
|
|
456
|
+
// Use a short timeout for status — this is a diagnostic command that
|
|
457
|
+
// should return quickly. Fall back to cached tools if the extension
|
|
458
|
+
// is slow to respond.
|
|
459
|
+
await this.ensureToolsLoaded(STATUS_TOOLS_TIMEOUT_MS);
|
|
460
|
+
}
|
|
461
|
+
await this.ensureToolsLoaded(STATUS_TOOLS_TIMEOUT_MS);
|
|
462
|
+
return {
|
|
463
|
+
ok: true,
|
|
464
|
+
command: 'status',
|
|
465
|
+
profile: this.profile,
|
|
466
|
+
mode: this.mode(),
|
|
467
|
+
sessionId: this.currentSessionId(),
|
|
468
|
+
requestedSessionId: this.requestedSessionId,
|
|
469
|
+
sessions: this.sessions,
|
|
470
|
+
ignoredCompatibilityOptions: this.ignoredCompatibilityOptions,
|
|
471
|
+
relayConnected: this.connection instanceof ExtensionConnection
|
|
472
|
+
? this.connection.getStatus() === 'connected'
|
|
473
|
+
: false,
|
|
474
|
+
extensionConnected: this.isBackendConnected(),
|
|
475
|
+
managedLifecycle: false,
|
|
476
|
+
transport: 'vibebrowser-mcp',
|
|
477
|
+
toolCount: this.tools.length,
|
|
478
|
+
tools: this.tools.map((tool) => tool.name),
|
|
479
|
+
...(waitForExtension
|
|
480
|
+
? {
|
|
481
|
+
waitForExtension: true,
|
|
482
|
+
waitedMs: Date.now() - waitStartedAt,
|
|
483
|
+
}
|
|
484
|
+
: {}),
|
|
485
|
+
};
|
|
486
|
+
}
|
|
487
|
+
async listPages() {
|
|
488
|
+
const invocation = await this.callTool('tabs', [
|
|
489
|
+
{ names: ['list_pages', 'get_tabs'] },
|
|
490
|
+
], {});
|
|
491
|
+
const pages = extractPages(invocation.result);
|
|
492
|
+
return {
|
|
493
|
+
ok: true,
|
|
494
|
+
command: 'tabs',
|
|
495
|
+
profile: this.profile,
|
|
496
|
+
mode: this.mode(),
|
|
497
|
+
sessionId: this.currentSessionId(),
|
|
498
|
+
requestedSessionId: this.requestedSessionId,
|
|
499
|
+
ignoredCompatibilityOptions: this.ignoredCompatibilityOptions,
|
|
500
|
+
tool: invocation.tool,
|
|
501
|
+
pages,
|
|
502
|
+
raw: normalizeToolResult(invocation.result),
|
|
503
|
+
};
|
|
504
|
+
}
|
|
505
|
+
async open(url) {
|
|
506
|
+
if (!url) {
|
|
507
|
+
return this.callGenericCommand('open', [{ names: ['new_page', 'create_new_tab'] }], {});
|
|
508
|
+
}
|
|
509
|
+
try {
|
|
510
|
+
const invocation = await this.callTool('open', [
|
|
511
|
+
{
|
|
512
|
+
names: ['new_page', 'create_new_tab'],
|
|
513
|
+
buildArgs: (tool) => withOpenArgs(tool, url),
|
|
514
|
+
},
|
|
515
|
+
{
|
|
516
|
+
names: ['navigate_page', 'navigate_to_url'],
|
|
517
|
+
buildArgs: (tool) => withNavigateArgs(tool, url, this.timeoutMs),
|
|
518
|
+
},
|
|
519
|
+
], {});
|
|
520
|
+
const output = this.outputFromInvocation('open', invocation);
|
|
521
|
+
return this.ensurePageContentInOutput('open', invocation, output, url);
|
|
522
|
+
}
|
|
523
|
+
catch (error) {
|
|
524
|
+
const recovered = await this.recoverPageContentAfterTimeout('open', url, error);
|
|
525
|
+
if (recovered) {
|
|
526
|
+
return recovered;
|
|
527
|
+
}
|
|
528
|
+
throw error;
|
|
529
|
+
}
|
|
530
|
+
}
|
|
531
|
+
async navigate(url) {
|
|
532
|
+
try {
|
|
533
|
+
const invocation = await this.callTool('navigate', [
|
|
534
|
+
{
|
|
535
|
+
names: ['navigate_page', 'navigate_to_url'],
|
|
536
|
+
buildArgs: (tool) => withNavigateArgs(tool, url, this.timeoutMs),
|
|
537
|
+
},
|
|
538
|
+
{
|
|
539
|
+
names: ['new_page', 'create_new_tab'],
|
|
540
|
+
buildArgs: (tool) => withOpenArgs(tool, url),
|
|
541
|
+
},
|
|
542
|
+
], {});
|
|
543
|
+
const output = this.outputFromInvocation('navigate', invocation);
|
|
544
|
+
return this.ensurePageContentInOutput('navigate', invocation, output, url);
|
|
545
|
+
}
|
|
546
|
+
catch (error) {
|
|
547
|
+
const recovered = await this.recoverPageContentAfterTimeout('navigate', url, error);
|
|
548
|
+
if (recovered) {
|
|
549
|
+
return recovered;
|
|
550
|
+
}
|
|
551
|
+
throw error;
|
|
552
|
+
}
|
|
553
|
+
}
|
|
554
|
+
async close(id) {
|
|
555
|
+
const invocation = await this.callTool('close', [
|
|
556
|
+
{
|
|
557
|
+
names: ['close_page', 'close_tab'],
|
|
558
|
+
buildArgs: (tool) => withPageArgs(tool, id),
|
|
559
|
+
},
|
|
560
|
+
], {});
|
|
561
|
+
return this.outputFromInvocation('close', invocation);
|
|
562
|
+
}
|
|
563
|
+
async focus(id) {
|
|
564
|
+
const invocation = await this.callTool('focus', [
|
|
565
|
+
{
|
|
566
|
+
names: ['switch_to_page', 'switch_to_tab', 'select_page', 'focus_tab'],
|
|
567
|
+
buildArgs: (tool) => withPageArgs(tool, id),
|
|
568
|
+
},
|
|
569
|
+
], {});
|
|
570
|
+
return this.outputFromInvocation('focus', invocation);
|
|
571
|
+
}
|
|
572
|
+
async snapshot(options) {
|
|
573
|
+
const wantsAria = options.format === 'aria' || options.interactive || Boolean(options.selector) || Boolean(options.frame);
|
|
574
|
+
const invocation = await this.callTool('snapshot', [
|
|
575
|
+
{
|
|
576
|
+
names: [wantsAria ? 'take_a11y_snapshot' : 'take_md_snapshot'],
|
|
577
|
+
buildArgs: (tool) => withSnapshotArgs(tool, options),
|
|
578
|
+
},
|
|
579
|
+
], {});
|
|
580
|
+
const text = firstText(invocation.result);
|
|
581
|
+
return {
|
|
582
|
+
ok: true,
|
|
583
|
+
command: 'snapshot',
|
|
584
|
+
profile: this.profile,
|
|
585
|
+
mode: this.mode(),
|
|
586
|
+
ignoredCompatibilityOptions: this.ignoredCompatibilityOptions,
|
|
587
|
+
tool: invocation.tool,
|
|
588
|
+
format: wantsAria ? 'aria' : options.format,
|
|
589
|
+
snapshot: limitText(text, options.limit),
|
|
590
|
+
raw: normalizeToolResult(invocation.result),
|
|
591
|
+
};
|
|
592
|
+
}
|
|
593
|
+
async screenshot(options) {
|
|
594
|
+
const invocation = await this.callTool('screenshot', [
|
|
595
|
+
{
|
|
596
|
+
names: ['take_screenshot', 'screenshot'],
|
|
597
|
+
buildArgs: (tool) => withScreenshotArgs(tool, options),
|
|
598
|
+
},
|
|
599
|
+
], {});
|
|
600
|
+
const savedPath = maybeWriteBinaryOutput(invocation.result, options.outputPath);
|
|
601
|
+
return {
|
|
602
|
+
ok: true,
|
|
603
|
+
command: 'screenshot',
|
|
604
|
+
profile: this.profile,
|
|
605
|
+
mode: this.mode(),
|
|
606
|
+
ignoredCompatibilityOptions: this.ignoredCompatibilityOptions,
|
|
607
|
+
tool: invocation.tool,
|
|
608
|
+
outputPath: savedPath || options.outputPath,
|
|
609
|
+
raw: normalizeToolResult(invocation.result),
|
|
610
|
+
};
|
|
611
|
+
}
|
|
612
|
+
async requests(options) {
|
|
613
|
+
const invocation = await this.callTool('requests', [
|
|
614
|
+
{
|
|
615
|
+
names: ['list_network_requests'],
|
|
616
|
+
buildArgs: (tool) => withRequestListArgs(tool, options),
|
|
617
|
+
},
|
|
618
|
+
], {});
|
|
619
|
+
let requests = extractRequests(invocation.result);
|
|
620
|
+
if (options.filter) {
|
|
621
|
+
const needle = options.filter.toLowerCase();
|
|
622
|
+
requests = requests.filter((request) => JSON.stringify(request).toLowerCase().includes(needle));
|
|
623
|
+
}
|
|
624
|
+
if (options.limit) {
|
|
625
|
+
requests = requests.slice(0, options.limit);
|
|
626
|
+
}
|
|
627
|
+
return {
|
|
628
|
+
ok: true,
|
|
629
|
+
command: 'requests',
|
|
630
|
+
profile: this.profile,
|
|
631
|
+
mode: this.mode(),
|
|
632
|
+
ignoredCompatibilityOptions: this.ignoredCompatibilityOptions,
|
|
633
|
+
tool: invocation.tool,
|
|
634
|
+
requests,
|
|
635
|
+
raw: normalizeToolResult(invocation.result),
|
|
636
|
+
};
|
|
637
|
+
}
|
|
638
|
+
async responseBody(options) {
|
|
639
|
+
const requests = await this.requests({ filter: options.pattern, limit: 1 });
|
|
640
|
+
const list = Array.isArray(requests.requests) ? requests.requests : [];
|
|
641
|
+
const first = list[0];
|
|
642
|
+
const requestId = first?.requestId ?? guessRequestId(requests.raw);
|
|
643
|
+
if (!requestId) {
|
|
644
|
+
throw new Error('No matching network request found');
|
|
645
|
+
}
|
|
646
|
+
const invocation = await this.callTool('responsebody', [
|
|
647
|
+
{
|
|
648
|
+
names: ['get_network_request'],
|
|
649
|
+
buildArgs: (tool) => withRequestDetailsArgs(tool, requestId),
|
|
650
|
+
},
|
|
651
|
+
], {});
|
|
652
|
+
const body = limitText(extractResponseBody(invocation.result), options.maxChars);
|
|
653
|
+
return {
|
|
654
|
+
ok: true,
|
|
655
|
+
command: 'responsebody',
|
|
656
|
+
profile: this.profile,
|
|
657
|
+
mode: this.mode(),
|
|
658
|
+
ignoredCompatibilityOptions: this.ignoredCompatibilityOptions,
|
|
659
|
+
requestId,
|
|
660
|
+
tool: invocation.tool,
|
|
661
|
+
responseBody: body,
|
|
662
|
+
raw: normalizeToolResult(invocation.result),
|
|
663
|
+
};
|
|
664
|
+
}
|
|
665
|
+
async click(ref, options) {
|
|
666
|
+
const invocation = await this.callTool('click', [
|
|
667
|
+
{
|
|
668
|
+
names: ['click'],
|
|
669
|
+
buildArgs: (tool) => withRefArgs(tool, ref, { dblClick: options.double }),
|
|
670
|
+
},
|
|
671
|
+
], {});
|
|
672
|
+
return this.outputFromInvocation('click', invocation);
|
|
673
|
+
}
|
|
674
|
+
async type(ref, text, options) {
|
|
675
|
+
const invocation = await this.callTool('type', [
|
|
676
|
+
{
|
|
677
|
+
names: ['fill'],
|
|
678
|
+
buildArgs: (tool) => withFillArgs(tool, ref, text),
|
|
679
|
+
},
|
|
680
|
+
{
|
|
681
|
+
names: ['type_text', 'type'],
|
|
682
|
+
buildArgs: (tool) => withTypeArgs(tool, text, options.submit ? 'Enter' : undefined),
|
|
683
|
+
},
|
|
684
|
+
], {});
|
|
685
|
+
return this.outputFromInvocation('type', invocation);
|
|
686
|
+
}
|
|
687
|
+
async press(keys) {
|
|
688
|
+
const invocation = await this.callTool('press', [
|
|
689
|
+
{
|
|
690
|
+
names: ['press_key', 'keyboard_shortcut'],
|
|
691
|
+
buildArgs: (tool) => withKeysArgs(tool, keys),
|
|
692
|
+
},
|
|
693
|
+
], {});
|
|
694
|
+
return this.outputFromInvocation('press', invocation);
|
|
695
|
+
}
|
|
696
|
+
async hover(ref) {
|
|
697
|
+
const invocation = await this.callTool('hover', [
|
|
698
|
+
{
|
|
699
|
+
names: ['hover'],
|
|
700
|
+
buildArgs: (tool) => withRefArgs(tool, ref),
|
|
701
|
+
},
|
|
702
|
+
], {});
|
|
703
|
+
return this.outputFromInvocation('hover', invocation);
|
|
704
|
+
}
|
|
705
|
+
async drag(source, target) {
|
|
706
|
+
const invocation = await this.callTool('drag', [
|
|
707
|
+
{
|
|
708
|
+
names: ['drag'],
|
|
709
|
+
buildArgs: (tool) => withDragArgs(tool, source, target),
|
|
710
|
+
},
|
|
711
|
+
], {});
|
|
712
|
+
return this.outputFromInvocation('drag', invocation);
|
|
713
|
+
}
|
|
714
|
+
async select(ref, values) {
|
|
715
|
+
const invocation = await this.callTool('select', [
|
|
716
|
+
{
|
|
717
|
+
names: ['fill'],
|
|
718
|
+
buildArgs: (tool) => withFillArgs(tool, ref, values.length === 1 ? values[0] : values),
|
|
719
|
+
},
|
|
720
|
+
{
|
|
721
|
+
names: ['select_option', 'select'],
|
|
722
|
+
buildArgs: (tool) => withSelectArgs(tool, ref, values),
|
|
723
|
+
},
|
|
724
|
+
], {});
|
|
725
|
+
return this.outputFromInvocation('select', invocation);
|
|
726
|
+
}
|
|
727
|
+
async fillForm(fieldsJson) {
|
|
728
|
+
const fields = parseJsonValue(fieldsJson, '--fields');
|
|
729
|
+
if (!Array.isArray(fields)) {
|
|
730
|
+
throw new Error('--fields must be a JSON array');
|
|
731
|
+
}
|
|
732
|
+
const invocation = await this.callTool('fill', [
|
|
733
|
+
{
|
|
734
|
+
names: ['fill_form'],
|
|
735
|
+
buildArgs: (tool) => withFillFormArgs(tool, fields),
|
|
736
|
+
},
|
|
737
|
+
], {});
|
|
738
|
+
return this.outputFromInvocation('fill', invocation);
|
|
739
|
+
}
|
|
740
|
+
async wait(options) {
|
|
741
|
+
if (options.text && options.text.length > 0) {
|
|
742
|
+
const invocation = await this.callTool('wait', [
|
|
743
|
+
{
|
|
744
|
+
names: ['wait_for'],
|
|
745
|
+
buildArgs: (tool) => withWaitArgs(tool, options.text, options.timeoutMs),
|
|
746
|
+
},
|
|
747
|
+
], {});
|
|
748
|
+
return this.outputFromInvocation('wait', invocation);
|
|
749
|
+
}
|
|
750
|
+
if (typeof options.seconds === 'number' && Number.isFinite(options.seconds) && options.seconds >= 0) {
|
|
751
|
+
await delay(Math.round(options.seconds * 1000));
|
|
752
|
+
return {
|
|
753
|
+
ok: true,
|
|
754
|
+
command: 'wait',
|
|
755
|
+
profile: this.profile,
|
|
756
|
+
mode: this.mode(),
|
|
757
|
+
ignoredCompatibilityOptions: this.ignoredCompatibilityOptions,
|
|
758
|
+
waitedSeconds: options.seconds,
|
|
759
|
+
};
|
|
760
|
+
}
|
|
761
|
+
throw new Error('wait requires either --text or --seconds');
|
|
762
|
+
}
|
|
763
|
+
async evaluate(options) {
|
|
764
|
+
const invocation = await this.callTool('evaluate', [
|
|
765
|
+
{
|
|
766
|
+
names: ['evaluate_script'],
|
|
767
|
+
buildArgs: (tool) => withEvaluateArgs(tool, options.fn, options.ref, options.argsJson),
|
|
768
|
+
},
|
|
769
|
+
], {});
|
|
770
|
+
return this.outputFromInvocation('evaluate', invocation);
|
|
771
|
+
}
|
|
772
|
+
async resize(width, height) {
|
|
773
|
+
const invocation = await this.callTool('resize', [
|
|
774
|
+
{
|
|
775
|
+
names: ['resize_page'],
|
|
776
|
+
buildArgs: (tool) => withResizeArgs(tool, width, height),
|
|
777
|
+
},
|
|
778
|
+
], {});
|
|
779
|
+
return this.outputFromInvocation('resize', invocation);
|
|
780
|
+
}
|
|
781
|
+
async upload(ref, path) {
|
|
782
|
+
const invocation = await this.callTool('upload', [
|
|
783
|
+
{
|
|
784
|
+
names: ['upload_file', 'file_upload'],
|
|
785
|
+
buildArgs: (tool) => withUploadArgs(tool, ref, path),
|
|
786
|
+
},
|
|
787
|
+
], {});
|
|
788
|
+
return this.outputFromInvocation('upload', invocation);
|
|
789
|
+
}
|
|
790
|
+
async dialog(options) {
|
|
791
|
+
if (options.accept && options.dismiss) {
|
|
792
|
+
throw new Error('dialog supports either --accept or --dismiss, not both');
|
|
793
|
+
}
|
|
794
|
+
const accept = options.dismiss ? false : true;
|
|
795
|
+
const invocation = await this.callTool('dialog', [
|
|
796
|
+
{
|
|
797
|
+
names: ['handle_dialog'],
|
|
798
|
+
buildArgs: (tool) => withDialogArgs(tool, accept, options.promptText),
|
|
799
|
+
},
|
|
800
|
+
], {});
|
|
801
|
+
return this.outputFromInvocation('dialog', invocation);
|
|
802
|
+
}
|
|
803
|
+
async callGenericCommand(commandName, candidates, canonicalArgs) {
|
|
804
|
+
const invocation = await this.callTool(commandName, candidates, canonicalArgs);
|
|
805
|
+
return this.outputFromInvocation(commandName, invocation);
|
|
806
|
+
}
|
|
807
|
+
async callTool(commandName, candidates, canonicalArgs) {
|
|
808
|
+
await this.ensureToolsLoaded();
|
|
809
|
+
const available = new Map(this.tools.map((tool) => [normalizeName(tool.name), tool]));
|
|
810
|
+
const compatibilityErrors = [];
|
|
811
|
+
for (const candidate of candidates) {
|
|
812
|
+
for (const candidateName of candidate.names) {
|
|
813
|
+
const tool = available.get(normalizeName(candidateName));
|
|
814
|
+
if (!tool) {
|
|
815
|
+
continue;
|
|
816
|
+
}
|
|
817
|
+
const args = candidate.buildArgs
|
|
818
|
+
? candidate.buildArgs(tool)
|
|
819
|
+
: withCanonicalArgs(tool, canonicalArgs);
|
|
820
|
+
// Inject --page-id into every tool call that accepts pageId/tabId,
|
|
821
|
+
// so the agent doesn't need to use focus/tab select (which disrupts
|
|
822
|
+
// the user's active browser tab).
|
|
823
|
+
if (this.pageId !== undefined) {
|
|
824
|
+
const pageKey = hasProperty(tool, 'pageId', 'tabId');
|
|
825
|
+
if (pageKey && !(pageKey in args)) {
|
|
826
|
+
args[pageKey] = this.pageId;
|
|
827
|
+
}
|
|
828
|
+
}
|
|
829
|
+
if (shouldRequestPageStateForCommand(commandName)) {
|
|
830
|
+
const pageStateKey = hasProperty(tool, 'pageStateFormat', 'page_state_format');
|
|
831
|
+
if (pageStateKey && !('pageStateFormat' in args) && !('page_state_format' in args)) {
|
|
832
|
+
args[pageStateKey] = 'markdown';
|
|
833
|
+
}
|
|
834
|
+
}
|
|
835
|
+
try {
|
|
836
|
+
const result = await this.connection.callTool(tool.name, args, this.timeoutMs);
|
|
837
|
+
return { tool: tool.name, args, result: result };
|
|
838
|
+
}
|
|
839
|
+
catch (error) {
|
|
840
|
+
if (!isToolArgumentCompatibilityError(error)) {
|
|
841
|
+
throw error;
|
|
842
|
+
}
|
|
843
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
844
|
+
compatibilityErrors.push(`${tool.name}: ${message}`);
|
|
845
|
+
}
|
|
846
|
+
}
|
|
847
|
+
}
|
|
848
|
+
const requested = candidates.flatMap((candidate) => candidate.names);
|
|
849
|
+
const compatibilityHint = compatibilityErrors.length > 0
|
|
850
|
+
? ` Compatibility errors: ${compatibilityErrors.join(' | ')}`
|
|
851
|
+
: '';
|
|
852
|
+
throw new Error(`No compatible browser tool found for "${commandName}". Tried ${requested.join(', ')}. Available tools: ${this.tools.map((tool) => tool.name).join(', ')}.${compatibilityHint}`);
|
|
853
|
+
}
|
|
854
|
+
async ensureToolsLoaded(timeoutMs) {
|
|
855
|
+
if (this.toolsLoaded && this.tools.length > 0) {
|
|
856
|
+
return;
|
|
857
|
+
}
|
|
858
|
+
const effectiveTimeout = timeoutMs ?? this.timeoutMs;
|
|
859
|
+
try {
|
|
860
|
+
this.tools = await this.connection.refreshTools(effectiveTimeout);
|
|
861
|
+
}
|
|
862
|
+
catch {
|
|
863
|
+
// Refresh timed out or failed — fall back to cached tools or a short
|
|
864
|
+
// passive wait so the status command is not blocked.
|
|
865
|
+
this.tools = this.connection.getTools();
|
|
866
|
+
if (this.tools.length === 0) {
|
|
867
|
+
if (this.connection instanceof ExtensionConnection) {
|
|
868
|
+
this.tools = await this.connection.waitForToolsUpdate(1_000);
|
|
869
|
+
}
|
|
870
|
+
}
|
|
871
|
+
}
|
|
872
|
+
this.toolsLoaded = true;
|
|
873
|
+
}
|
|
874
|
+
outputFromInvocation(commandName, invocation) {
|
|
875
|
+
const pageContent = firstText(invocation.result);
|
|
876
|
+
return {
|
|
877
|
+
ok: !invocation.result.isError,
|
|
878
|
+
command: commandName,
|
|
879
|
+
profile: this.profile,
|
|
880
|
+
mode: this.mode(),
|
|
881
|
+
sessionId: this.currentSessionId(),
|
|
882
|
+
requestedSessionId: this.requestedSessionId,
|
|
883
|
+
ignoredCompatibilityOptions: this.ignoredCompatibilityOptions,
|
|
884
|
+
tool: invocation.tool,
|
|
885
|
+
...(looksLikePageContentText(pageContent) ? { pageContent } : {}),
|
|
886
|
+
raw: normalizeToolResult(invocation.result),
|
|
887
|
+
};
|
|
888
|
+
}
|
|
889
|
+
async ensurePageContentInOutput(commandName, invocation, output, targetUrl) {
|
|
890
|
+
if (!output.ok) {
|
|
891
|
+
return output;
|
|
892
|
+
}
|
|
893
|
+
if (!hasExplicitPageContext(invocation.args)) {
|
|
894
|
+
return output;
|
|
895
|
+
}
|
|
896
|
+
const currentText = firstText(invocation.result);
|
|
897
|
+
if (looksLikePageContentText(currentText) && !isLikelyIncompletePageContent(currentText, targetUrl)) {
|
|
898
|
+
return output;
|
|
899
|
+
}
|
|
900
|
+
const targetPageId = extractPageIdFromResult(invocation.result) ?? this.pageId;
|
|
901
|
+
if (targetPageId === undefined) {
|
|
902
|
+
return output;
|
|
903
|
+
}
|
|
904
|
+
const fallback = await this.takeMarkdownSnapshotForTarget(targetPageId, targetUrl);
|
|
905
|
+
if (!fallback.content) {
|
|
906
|
+
return output;
|
|
907
|
+
}
|
|
908
|
+
return {
|
|
909
|
+
...output,
|
|
910
|
+
pageContent: fallback.content,
|
|
911
|
+
pageContentSource: 'take_md_snapshot',
|
|
912
|
+
...(fallback.pageId !== targetPageId ? { pageId: fallback.pageId } : {}),
|
|
913
|
+
};
|
|
914
|
+
}
|
|
915
|
+
async recoverPageContentAfterTimeout(commandName, url, error) {
|
|
916
|
+
if (!isTimeoutError(error)) {
|
|
917
|
+
return undefined;
|
|
918
|
+
}
|
|
919
|
+
if (this.pageId === undefined) {
|
|
920
|
+
return undefined;
|
|
921
|
+
}
|
|
922
|
+
const recoveryTimeoutMs = Math.max(this.timeoutMs, 10_000);
|
|
923
|
+
let targetPageId = this.pageId;
|
|
924
|
+
if (targetPageId === undefined) {
|
|
925
|
+
return undefined;
|
|
926
|
+
}
|
|
927
|
+
const fallback = await this.takeMarkdownSnapshotForTarget(targetPageId, url, recoveryTimeoutMs);
|
|
928
|
+
if (!fallback.content) {
|
|
929
|
+
return undefined;
|
|
930
|
+
}
|
|
931
|
+
return {
|
|
932
|
+
ok: true,
|
|
933
|
+
command: commandName,
|
|
934
|
+
profile: this.profile,
|
|
935
|
+
mode: this.mode(),
|
|
936
|
+
sessionId: this.currentSessionId(),
|
|
937
|
+
requestedSessionId: this.requestedSessionId,
|
|
938
|
+
ignoredCompatibilityOptions: this.ignoredCompatibilityOptions,
|
|
939
|
+
tool: 'take_md_snapshot',
|
|
940
|
+
recoveredFromTimeout: true,
|
|
941
|
+
pageId: fallback.pageId,
|
|
942
|
+
url,
|
|
943
|
+
pageContent: fallback.content,
|
|
944
|
+
raw: {
|
|
945
|
+
recovery: 'timeout_fallback',
|
|
946
|
+
},
|
|
947
|
+
};
|
|
948
|
+
}
|
|
949
|
+
async takeMarkdownSnapshotForTarget(initialPageId, targetUrl, timeoutMs = this.timeoutMs) {
|
|
950
|
+
let latest;
|
|
951
|
+
let pageId = initialPageId;
|
|
952
|
+
const recoveryTimeoutMs = Math.max(timeoutMs, 10_000);
|
|
953
|
+
const attempts = 8;
|
|
954
|
+
for (let attempt = 0; attempt < attempts; attempt += 1) {
|
|
955
|
+
latest = await this.takeMarkdownSnapshotForPage(pageId, recoveryTimeoutMs);
|
|
956
|
+
if (latest && !isLikelyIncompletePageContent(latest, targetUrl)) {
|
|
957
|
+
return { content: latest, pageId };
|
|
958
|
+
}
|
|
959
|
+
if (targetUrl) {
|
|
960
|
+
const pages = await this.listPagesForRecovery(recoveryTimeoutMs);
|
|
961
|
+
const matched = findBestPageMatchByUrl(pages, targetUrl);
|
|
962
|
+
if (matched) {
|
|
963
|
+
pageId = matched.id;
|
|
964
|
+
}
|
|
965
|
+
}
|
|
966
|
+
if (attempt < attempts - 1) {
|
|
967
|
+
await delay(750);
|
|
968
|
+
}
|
|
969
|
+
}
|
|
970
|
+
return { content: latest, pageId };
|
|
971
|
+
}
|
|
972
|
+
async listPagesForRecovery(timeoutMs) {
|
|
973
|
+
try {
|
|
974
|
+
await this.ensureToolsLoaded(timeoutMs);
|
|
975
|
+
const listTool = this.tools.find((tool) => {
|
|
976
|
+
const normalized = normalizeName(tool.name);
|
|
977
|
+
return normalized === 'list_pages' || normalized === 'get_tabs';
|
|
978
|
+
});
|
|
979
|
+
if (!listTool) {
|
|
980
|
+
return [];
|
|
981
|
+
}
|
|
982
|
+
const result = await this.connection.callTool(listTool.name, {}, timeoutMs);
|
|
983
|
+
return extractPages(result);
|
|
984
|
+
}
|
|
985
|
+
catch {
|
|
986
|
+
return [];
|
|
987
|
+
}
|
|
988
|
+
}
|
|
989
|
+
async takeMarkdownSnapshotForPage(pageId, timeoutMs = this.timeoutMs) {
|
|
990
|
+
await this.ensureToolsLoaded();
|
|
991
|
+
const snapshotTool = this.tools.find((tool) => normalizeName(tool.name) === 'take_md_snapshot');
|
|
992
|
+
if (!snapshotTool) {
|
|
993
|
+
return undefined;
|
|
994
|
+
}
|
|
995
|
+
const args = {};
|
|
996
|
+
const pageKey = hasProperty(snapshotTool, 'pageId', 'tabId');
|
|
997
|
+
if (pageKey) {
|
|
998
|
+
args[pageKey] = pageId;
|
|
999
|
+
}
|
|
1000
|
+
const pageStateKey = hasProperty(snapshotTool, 'pageStateFormat', 'page_state_format');
|
|
1001
|
+
if (pageStateKey) {
|
|
1002
|
+
args[pageStateKey] = 'markdown';
|
|
1003
|
+
}
|
|
1004
|
+
try {
|
|
1005
|
+
const result = await this.connection.callTool(snapshotTool.name, args, timeoutMs);
|
|
1006
|
+
const text = firstText(result);
|
|
1007
|
+
if (!text) {
|
|
1008
|
+
return undefined;
|
|
1009
|
+
}
|
|
1010
|
+
const trimmed = text.trim();
|
|
1011
|
+
if (/^Error:/i.test(trimmed) || /Failed to get valid tab/i.test(trimmed)) {
|
|
1012
|
+
return undefined;
|
|
1013
|
+
}
|
|
1014
|
+
return text;
|
|
1015
|
+
}
|
|
1016
|
+
catch {
|
|
1017
|
+
return undefined;
|
|
1018
|
+
}
|
|
1019
|
+
}
|
|
1020
|
+
mode() {
|
|
1021
|
+
if (this.devtoolsOnly) {
|
|
1022
|
+
return 'devtools';
|
|
1023
|
+
}
|
|
1024
|
+
return this.remoteUuid ? 'remote' : 'local';
|
|
1025
|
+
}
|
|
1026
|
+
currentSessionId() {
|
|
1027
|
+
if (this.devtoolsOnly) {
|
|
1028
|
+
return undefined;
|
|
1029
|
+
}
|
|
1030
|
+
if (this.remoteUuid) {
|
|
1031
|
+
return this.connection instanceof ExtensionConnection
|
|
1032
|
+
? this.connection.getRemoteConfig()?.uuid ?? this.remoteUuid
|
|
1033
|
+
: this.remoteUuid;
|
|
1034
|
+
}
|
|
1035
|
+
const connectedSessionIds = new Set(this.sessions
|
|
1036
|
+
.filter((session) => session.connected)
|
|
1037
|
+
.map((session) => session.sessionId));
|
|
1038
|
+
if (this.requestedSessionId && connectedSessionIds.has(this.requestedSessionId)) {
|
|
1039
|
+
return this.requestedSessionId;
|
|
1040
|
+
}
|
|
1041
|
+
return this.sessions.find((session) => session.connected)?.sessionId;
|
|
1042
|
+
}
|
|
1043
|
+
isBackendConnected() {
|
|
1044
|
+
if (this.connection instanceof ExtensionConnection) {
|
|
1045
|
+
return this.connection.isExtensionConnected();
|
|
1046
|
+
}
|
|
1047
|
+
return this.connection.isAvailable();
|
|
1048
|
+
}
|
|
1049
|
+
getConnectionErrorMessage() {
|
|
1050
|
+
if (this.connection instanceof ExtensionConnection) {
|
|
1051
|
+
return this.connection.getConnectionErrorMessage();
|
|
1052
|
+
}
|
|
1053
|
+
return this.connection.getUnavailableReason() || 'chrome-devtools backend unavailable';
|
|
1054
|
+
}
|
|
1055
|
+
outputMode() {
|
|
1056
|
+
return this.mode();
|
|
1057
|
+
}
|
|
1058
|
+
outputContext() {
|
|
1059
|
+
return {
|
|
1060
|
+
profile: this.profile,
|
|
1061
|
+
mode: this.mode(),
|
|
1062
|
+
sessionId: this.currentSessionId(),
|
|
1063
|
+
requestedSessionId: this.requestedSessionId,
|
|
1064
|
+
ignoredCompatibilityOptions: this.ignoredCompatibilityOptions,
|
|
1065
|
+
};
|
|
1066
|
+
}
|
|
1067
|
+
}
|
|
1068
|
+
function shouldRequestPageStateForCommand(commandName) {
|
|
1069
|
+
return new Set([
|
|
1070
|
+
'open',
|
|
1071
|
+
'navigate',
|
|
1072
|
+
'click',
|
|
1073
|
+
'type',
|
|
1074
|
+
'press',
|
|
1075
|
+
'hover',
|
|
1076
|
+
'drag',
|
|
1077
|
+
'select',
|
|
1078
|
+
'fill',
|
|
1079
|
+
]).has(commandName);
|
|
1080
|
+
}
|
|
1081
|
+
function looksLikePageContentText(text) {
|
|
1082
|
+
const trimmed = text.trim();
|
|
1083
|
+
if (!trimmed) {
|
|
1084
|
+
return false;
|
|
1085
|
+
}
|
|
1086
|
+
if (/Error retrieving page content/i.test(trimmed) || /page content extraction failed/i.test(trimmed)) {
|
|
1087
|
+
return false;
|
|
1088
|
+
}
|
|
1089
|
+
return /```(?:markdown|text|html)/i.test(trimmed) || /Page State Format:/i.test(trimmed);
|
|
1090
|
+
}
|
|
1091
|
+
function hasExplicitPageContext(args) {
|
|
1092
|
+
const candidates = [args.pageId, args.tabId, args.page_id, args.tab_id];
|
|
1093
|
+
return candidates.some((candidate) => {
|
|
1094
|
+
if (typeof candidate === 'number' && Number.isFinite(candidate)) {
|
|
1095
|
+
return true;
|
|
1096
|
+
}
|
|
1097
|
+
if (typeof candidate === 'string') {
|
|
1098
|
+
return /^\d+$/.test(candidate.trim());
|
|
1099
|
+
}
|
|
1100
|
+
return false;
|
|
1101
|
+
});
|
|
1102
|
+
}
|
|
1103
|
+
function shouldAttachImplicitPageContent(commandName, args) {
|
|
1104
|
+
if (!hasExplicitPageContext(args)) {
|
|
1105
|
+
return false;
|
|
1106
|
+
}
|
|
1107
|
+
return commandName !== 'close';
|
|
1108
|
+
}
|
|
1109
|
+
function stripImplicitPageContent(result) {
|
|
1110
|
+
const firstTextIndex = result.content.findIndex((entry) => entry.type === 'text');
|
|
1111
|
+
if (firstTextIndex < 0) {
|
|
1112
|
+
return result;
|
|
1113
|
+
}
|
|
1114
|
+
const firstTextEntry = result.content[firstTextIndex];
|
|
1115
|
+
if (!('text' in firstTextEntry) || !looksLikePageContentText(firstTextEntry.text)) {
|
|
1116
|
+
return result;
|
|
1117
|
+
}
|
|
1118
|
+
return {
|
|
1119
|
+
...result,
|
|
1120
|
+
content: result.content.filter((_, index) => index !== firstTextIndex),
|
|
1121
|
+
};
|
|
1122
|
+
}
|
|
1123
|
+
function isLikelyIncompletePageContent(text, targetUrl) {
|
|
1124
|
+
const trimmed = text.trim();
|
|
1125
|
+
if (!trimmed) {
|
|
1126
|
+
return true;
|
|
1127
|
+
}
|
|
1128
|
+
const interactiveRefCount = (trimmed.match(/\[M\d+:/g) || []).length;
|
|
1129
|
+
// Common in early snapshots right after new_page(waitForReady=false):
|
|
1130
|
+
// tab exists but title/url are still empty.
|
|
1131
|
+
if (/Tab ID:[^\n]*\nTitle:\s*\nURL:\s*(?:\n|$)/i.test(trimmed)) {
|
|
1132
|
+
return true;
|
|
1133
|
+
}
|
|
1134
|
+
if (/#\s*Markdown Snapshot:\s*Untitled/i.test(trimmed) && interactiveRefCount < 5) {
|
|
1135
|
+
return true;
|
|
1136
|
+
}
|
|
1137
|
+
if (targetUrl) {
|
|
1138
|
+
const normalizedTarget = normalizeUrlForMatch(targetUrl);
|
|
1139
|
+
const hasAnyUrl = /\bURL:\s*\S+/i.test(trimmed);
|
|
1140
|
+
const textNormalized = normalizeUrlForMatch(trimmed);
|
|
1141
|
+
if (!hasAnyUrl) {
|
|
1142
|
+
return true;
|
|
1143
|
+
}
|
|
1144
|
+
if (normalizedTarget && !textNormalized.includes(normalizedTarget)) {
|
|
1145
|
+
return true;
|
|
1146
|
+
}
|
|
1147
|
+
}
|
|
1148
|
+
return false;
|
|
1149
|
+
}
|
|
1150
|
+
function extractPageIdFromResult(result) {
|
|
1151
|
+
const direct = firstDefined(result, ['pageId', 'tabId', 'id']);
|
|
1152
|
+
if (typeof direct === 'number' && Number.isFinite(direct)) {
|
|
1153
|
+
return direct;
|
|
1154
|
+
}
|
|
1155
|
+
const parsed = parseResultText(result);
|
|
1156
|
+
if (parsed && typeof parsed === 'object') {
|
|
1157
|
+
const parsedId = firstDefined(parsed, ['pageId', 'tabId', 'id']);
|
|
1158
|
+
if (typeof parsedId === 'number' && Number.isFinite(parsedId)) {
|
|
1159
|
+
return parsedId;
|
|
1160
|
+
}
|
|
1161
|
+
}
|
|
1162
|
+
const text = firstText(result);
|
|
1163
|
+
const createdMatch = /new background page \(ID:\s*(\d+)\)/i.exec(text);
|
|
1164
|
+
if (createdMatch) {
|
|
1165
|
+
return Number.parseInt(createdMatch[1], 10);
|
|
1166
|
+
}
|
|
1167
|
+
const tabMatch = /\bTab ID:\s*(\d+)\b/i.exec(text);
|
|
1168
|
+
if (tabMatch) {
|
|
1169
|
+
return Number.parseInt(tabMatch[1], 10);
|
|
1170
|
+
}
|
|
1171
|
+
const pageMatch = /\bPage ID:\s*(\d+)\b/i.exec(text);
|
|
1172
|
+
if (pageMatch) {
|
|
1173
|
+
return Number.parseInt(pageMatch[1], 10);
|
|
1174
|
+
}
|
|
1175
|
+
return undefined;
|
|
1176
|
+
}
|
|
1177
|
+
function isTimeoutError(error) {
|
|
1178
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
1179
|
+
return /timed out|timeout/i.test(message);
|
|
1180
|
+
}
|
|
1181
|
+
function isToolArgumentCompatibilityError(error) {
|
|
1182
|
+
const message = (error instanceof Error ? error.message : String(error)).toLowerCase();
|
|
1183
|
+
if (!message) {
|
|
1184
|
+
return false;
|
|
1185
|
+
}
|
|
1186
|
+
if (/timed out|timeout|relay connection|connection lost|not connected|no connection|disconnected/.test(message)) {
|
|
1187
|
+
return false;
|
|
1188
|
+
}
|
|
1189
|
+
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);
|
|
1190
|
+
}
|
|
1191
|
+
function findBestPageMatchByUrl(pages, targetUrl) {
|
|
1192
|
+
const normalizedTarget = normalizeUrlForMatch(targetUrl);
|
|
1193
|
+
let bestExact;
|
|
1194
|
+
let bestPartial;
|
|
1195
|
+
for (const page of pages) {
|
|
1196
|
+
const id = toFinitePageId(page.id);
|
|
1197
|
+
if (id === undefined || typeof page.url !== 'string') {
|
|
1198
|
+
continue;
|
|
1199
|
+
}
|
|
1200
|
+
const normalizedPage = normalizeUrlForMatch(page.url);
|
|
1201
|
+
if (normalizedPage === normalizedTarget) {
|
|
1202
|
+
if (!bestExact || id > bestExact.id) {
|
|
1203
|
+
bestExact = { id, url: page.url };
|
|
1204
|
+
}
|
|
1205
|
+
}
|
|
1206
|
+
if (normalizedPage.includes(normalizedTarget) || normalizedTarget.includes(normalizedPage)) {
|
|
1207
|
+
if (!bestPartial || id > bestPartial.id) {
|
|
1208
|
+
bestPartial = { id, url: page.url };
|
|
1209
|
+
}
|
|
1210
|
+
}
|
|
1211
|
+
}
|
|
1212
|
+
return bestExact ?? bestPartial;
|
|
1213
|
+
}
|
|
1214
|
+
function toFinitePageId(value) {
|
|
1215
|
+
if (typeof value === 'number' && Number.isFinite(value)) {
|
|
1216
|
+
return value;
|
|
1217
|
+
}
|
|
1218
|
+
if (typeof value === 'string') {
|
|
1219
|
+
const parsed = Number.parseInt(value, 10);
|
|
1220
|
+
if (Number.isFinite(parsed)) {
|
|
1221
|
+
return parsed;
|
|
1222
|
+
}
|
|
1223
|
+
}
|
|
1224
|
+
return undefined;
|
|
1225
|
+
}
|
|
1226
|
+
function normalizeUrlForMatch(value) {
|
|
1227
|
+
const trimmed = value.trim().replace(/\/$/, '');
|
|
1228
|
+
if (!trimmed) {
|
|
1229
|
+
return trimmed;
|
|
1230
|
+
}
|
|
1231
|
+
try {
|
|
1232
|
+
const parsed = new URL(trimmed);
|
|
1233
|
+
return safeDecode(`${parsed.origin}${parsed.pathname}${parsed.search}`.replace(/\/$/, ''));
|
|
1234
|
+
}
|
|
1235
|
+
catch {
|
|
1236
|
+
return safeDecode(trimmed);
|
|
1237
|
+
}
|
|
1238
|
+
}
|
|
1239
|
+
function safeDecode(value) {
|
|
1240
|
+
try {
|
|
1241
|
+
return decodeURIComponent(value);
|
|
1242
|
+
}
|
|
1243
|
+
catch {
|
|
1244
|
+
return value;
|
|
1245
|
+
}
|
|
1246
|
+
}
|
|
1247
|
+
function emitOutput(asJson, data, humanOutput) {
|
|
1248
|
+
if (asJson) {
|
|
1249
|
+
console.log(JSON.stringify(data, null, 2));
|
|
1250
|
+
return;
|
|
1251
|
+
}
|
|
1252
|
+
console.log(humanOutput);
|
|
1253
|
+
}
|
|
1254
|
+
function emitError(asJson, commandName, ctx, error) {
|
|
1255
|
+
let message = error instanceof Error ? error.message : String(error);
|
|
1256
|
+
// Improve guidance when the extension requires a pageId that wasn't provided
|
|
1257
|
+
if (/\bpageId\b/i.test(message) && /\bmissing\b|\brequired\b/i.test(message)) {
|
|
1258
|
+
message += '\nHint: use `tabs` to list pages, then pass --page-id <id> to target a specific tab.';
|
|
1259
|
+
}
|
|
1260
|
+
if (asJson) {
|
|
1261
|
+
const context = ctx.outputContext();
|
|
1262
|
+
console.log(JSON.stringify({
|
|
1263
|
+
ok: false,
|
|
1264
|
+
command: commandName,
|
|
1265
|
+
...context,
|
|
1266
|
+
error: message,
|
|
1267
|
+
}, null, 2));
|
|
1268
|
+
return;
|
|
1269
|
+
}
|
|
1270
|
+
console.error(`Error: ${message}`);
|
|
1271
|
+
}
|
|
1272
|
+
function formatHumanOutput(commandName, output) {
|
|
1273
|
+
switch (commandName) {
|
|
1274
|
+
case 'status':
|
|
1275
|
+
case 'start':
|
|
1276
|
+
case 'stop':
|
|
1277
|
+
return [
|
|
1278
|
+
`Profile: ${String(output.profile)}`,
|
|
1279
|
+
`Mode: ${String(output.mode)}`,
|
|
1280
|
+
output.sessionId ? `Session: ${String(output.sessionId)}` : null,
|
|
1281
|
+
output.requestedSessionId && output.requestedSessionId !== output.sessionId ? `Requested session: ${String(output.requestedSessionId)}` : null,
|
|
1282
|
+
`Relay connected: ${boolText(output.relayConnected)}`,
|
|
1283
|
+
`Extension connected: ${boolText(output.extensionConnected)}`,
|
|
1284
|
+
`Managed lifecycle: ${boolText(output.managedLifecycle)}`,
|
|
1285
|
+
output.toolCount !== undefined ? `Tools: ${String(output.toolCount)}` : null,
|
|
1286
|
+
output.note ? String(output.note) : null,
|
|
1287
|
+
].filter(Boolean).join('\n');
|
|
1288
|
+
case 'sessions': {
|
|
1289
|
+
const sessions = Array.isArray(output.sessions) ? output.sessions : [];
|
|
1290
|
+
if (sessions.length === 0) {
|
|
1291
|
+
return 'No browser sessions connected';
|
|
1292
|
+
}
|
|
1293
|
+
return sessions.map((session, index) => {
|
|
1294
|
+
const selected = output.sessionId === session.sessionId ? ' [selected]' : '';
|
|
1295
|
+
const connected = session.connected ? 'connected' : 'disconnected';
|
|
1296
|
+
const tools = session.toolCount !== undefined ? ` tools=${session.toolCount}` : '';
|
|
1297
|
+
return `${index + 1}. ${session.sessionId}${selected} - ${connected}${tools}`;
|
|
1298
|
+
}).join('\n');
|
|
1299
|
+
}
|
|
1300
|
+
case 'tabs':
|
|
1301
|
+
case 'tab': {
|
|
1302
|
+
const pages = Array.isArray(output.pages) ? output.pages : [];
|
|
1303
|
+
if (pages.length === 0) {
|
|
1304
|
+
return firstDefinedText(output.raw, 'No tabs reported by browser');
|
|
1305
|
+
}
|
|
1306
|
+
return [
|
|
1307
|
+
output.sessionId ? `Session: ${String(output.sessionId)}` : null,
|
|
1308
|
+
...pages.map((page, index) => `${index + 1}. ${page.active ? '[active] ' : ''}${page.title || '(untitled)'}${page.url ? ` — ${page.url}` : ''}${page.id !== undefined ? ` [id=${page.id}]` : ''}`),
|
|
1309
|
+
].filter(Boolean).join('\n');
|
|
1310
|
+
}
|
|
1311
|
+
case 'snapshot':
|
|
1312
|
+
return [
|
|
1313
|
+
output.title ? `Title: ${String(output.title)}` : null,
|
|
1314
|
+
output.url ? `URL: ${String(output.url)}` : null,
|
|
1315
|
+
output.snapshot ? String(output.snapshot) : firstDefinedText(output.raw, ''),
|
|
1316
|
+
].filter(Boolean).join('\n');
|
|
1317
|
+
case 'requests': {
|
|
1318
|
+
const requests = Array.isArray(output.requests) ? output.requests : [];
|
|
1319
|
+
if (requests.length === 0) {
|
|
1320
|
+
return firstDefinedText(output.raw, 'No requests reported by browser');
|
|
1321
|
+
}
|
|
1322
|
+
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');
|
|
1323
|
+
}
|
|
1324
|
+
case 'responsebody':
|
|
1325
|
+
return String(output.responseBody ?? firstDefinedText(output.raw, ''));
|
|
1326
|
+
default:
|
|
1327
|
+
if (output.outputPath) {
|
|
1328
|
+
return `Saved to ${String(output.outputPath)}`;
|
|
1329
|
+
}
|
|
1330
|
+
return firstDefinedText(output.raw, JSON.stringify(output, null, 2));
|
|
1331
|
+
}
|
|
1332
|
+
}
|
|
1333
|
+
function boolText(value) {
|
|
1334
|
+
return value ? 'yes' : 'no';
|
|
1335
|
+
}
|
|
1336
|
+
function firstDefinedText(value, fallback) {
|
|
1337
|
+
if (typeof value === 'string' && value.length > 0) {
|
|
1338
|
+
return value;
|
|
1339
|
+
}
|
|
1340
|
+
if (value && typeof value === 'object') {
|
|
1341
|
+
// Direct { text: "..." } wrapper
|
|
1342
|
+
if ('text' in value && typeof value.text === 'string') {
|
|
1343
|
+
return value.text;
|
|
1344
|
+
}
|
|
1345
|
+
// MCP tool result: { content: [{ type: 'text', text: '...' }] }
|
|
1346
|
+
const rec = value;
|
|
1347
|
+
if (Array.isArray(rec.content)) {
|
|
1348
|
+
const textItem = rec.content.find((entry) => entry && typeof entry === 'object' && entry.type === 'text');
|
|
1349
|
+
if (textItem && typeof textItem.text === 'string' && textItem.text.length > 0) {
|
|
1350
|
+
return textItem.text;
|
|
1351
|
+
}
|
|
1352
|
+
}
|
|
1353
|
+
}
|
|
1354
|
+
return fallback;
|
|
1355
|
+
}
|
|
1356
|
+
function normalizeToolResult(result) {
|
|
1357
|
+
return sanitizeUnknown(result) ?? null;
|
|
1358
|
+
}
|
|
1359
|
+
function sanitizeUnknown(value) {
|
|
1360
|
+
if (value === null)
|
|
1361
|
+
return null;
|
|
1362
|
+
if (typeof value === 'boolean' || typeof value === 'number' || typeof value === 'string')
|
|
1363
|
+
return value;
|
|
1364
|
+
if (Array.isArray(value))
|
|
1365
|
+
return value.map((item) => sanitizeUnknown(item) ?? null);
|
|
1366
|
+
if (value && typeof value === 'object') {
|
|
1367
|
+
const output = {};
|
|
1368
|
+
for (const [key, entry] of Object.entries(value)) {
|
|
1369
|
+
const sanitized = sanitizeUnknown(entry);
|
|
1370
|
+
if (sanitized !== undefined) {
|
|
1371
|
+
output[key] = sanitized;
|
|
1372
|
+
}
|
|
1373
|
+
}
|
|
1374
|
+
return output;
|
|
1375
|
+
}
|
|
1376
|
+
return undefined;
|
|
1377
|
+
}
|
|
1378
|
+
function maybeWriteBinaryOutput(result, outputPath) {
|
|
1379
|
+
if (!outputPath) {
|
|
1380
|
+
return undefined;
|
|
1381
|
+
}
|
|
1382
|
+
const image = result.content?.find((item) => item.type === 'image');
|
|
1383
|
+
if (!image || !('data' in image) || typeof image.data !== 'string') {
|
|
1384
|
+
return undefined;
|
|
1385
|
+
}
|
|
1386
|
+
const target = resolve(outputPath);
|
|
1387
|
+
writeFileSync(target, Buffer.from(image.data, 'base64'));
|
|
1388
|
+
return target;
|
|
1389
|
+
}
|
|
1390
|
+
function extractPages(result) {
|
|
1391
|
+
const direct = extractStructured(result, ['pages', 'tabs', 'targets']);
|
|
1392
|
+
if (Array.isArray(direct)) {
|
|
1393
|
+
return direct.map((entry) => normalizePage(entry));
|
|
1394
|
+
}
|
|
1395
|
+
const parsed = parseResultText(result);
|
|
1396
|
+
if (Array.isArray(parsed)) {
|
|
1397
|
+
return parsed.map((entry) => normalizePage(entry));
|
|
1398
|
+
}
|
|
1399
|
+
if (parsed && typeof parsed === 'object') {
|
|
1400
|
+
for (const key of ['pages', 'tabs', 'targets']) {
|
|
1401
|
+
const candidate = parsed[key];
|
|
1402
|
+
if (Array.isArray(candidate)) {
|
|
1403
|
+
return candidate.map((entry) => normalizePage(entry));
|
|
1404
|
+
}
|
|
1405
|
+
}
|
|
1406
|
+
}
|
|
1407
|
+
// Fallback: parse the plain-text format produced by ListPagesTool
|
|
1408
|
+
// e.g. 'Page 123 [ACTIVE]: "Google" - https://www.google.com'
|
|
1409
|
+
const text = firstText(result);
|
|
1410
|
+
if (text) {
|
|
1411
|
+
const textPages = parsePlainTextPages(text);
|
|
1412
|
+
if (textPages.length > 0) {
|
|
1413
|
+
return textPages;
|
|
1414
|
+
}
|
|
1415
|
+
}
|
|
1416
|
+
return [];
|
|
1417
|
+
}
|
|
1418
|
+
// Matches ListPagesTool output: 'Page <id>[ [ACTIVE]]: "title" - url'
|
|
1419
|
+
const PAGE_LINE_RE = /^Page\s+(\d+)\s*(\[ACTIVE\])?\s*:\s*"(.*)"\s*-\s*(.+)$/i;
|
|
1420
|
+
function parsePlainTextPages(text) {
|
|
1421
|
+
const pages = [];
|
|
1422
|
+
for (const line of text.split('\n')) {
|
|
1423
|
+
const match = PAGE_LINE_RE.exec(line.trim());
|
|
1424
|
+
if (match) {
|
|
1425
|
+
pages.push({
|
|
1426
|
+
id: Number(match[1]),
|
|
1427
|
+
active: match[2] !== undefined,
|
|
1428
|
+
title: match[3],
|
|
1429
|
+
url: match[4].trim(),
|
|
1430
|
+
});
|
|
1431
|
+
}
|
|
1432
|
+
}
|
|
1433
|
+
return pages;
|
|
1434
|
+
}
|
|
1435
|
+
function normalizePage(value) {
|
|
1436
|
+
if (!value || typeof value !== 'object') {
|
|
1437
|
+
return {};
|
|
1438
|
+
}
|
|
1439
|
+
const record = value;
|
|
1440
|
+
return {
|
|
1441
|
+
...sanitizeUnknown(record),
|
|
1442
|
+
id: firstDefined(record, ['pageId', 'tabId', 'id', 'targetId']),
|
|
1443
|
+
title: firstString(record, ['title', 'name']),
|
|
1444
|
+
url: firstString(record, ['url']),
|
|
1445
|
+
active: firstBoolean(record, ['active', 'selected', 'focused']),
|
|
1446
|
+
};
|
|
1447
|
+
}
|
|
1448
|
+
function extractRequests(result) {
|
|
1449
|
+
const direct = extractStructured(result, ['requests']);
|
|
1450
|
+
if (Array.isArray(direct)) {
|
|
1451
|
+
return direct.map((entry) => normalizeRequest(entry));
|
|
1452
|
+
}
|
|
1453
|
+
const parsed = parseResultText(result);
|
|
1454
|
+
if (Array.isArray(parsed)) {
|
|
1455
|
+
return parsed.map((entry) => normalizeRequest(entry));
|
|
1456
|
+
}
|
|
1457
|
+
if (parsed && typeof parsed === 'object') {
|
|
1458
|
+
const requests = parsed.requests;
|
|
1459
|
+
if (Array.isArray(requests)) {
|
|
1460
|
+
return requests.map((entry) => normalizeRequest(entry));
|
|
1461
|
+
}
|
|
1462
|
+
}
|
|
1463
|
+
return [];
|
|
1464
|
+
}
|
|
1465
|
+
function normalizeRequest(value) {
|
|
1466
|
+
if (!value || typeof value !== 'object') {
|
|
1467
|
+
return {};
|
|
1468
|
+
}
|
|
1469
|
+
const record = value;
|
|
1470
|
+
return {
|
|
1471
|
+
...sanitizeUnknown(record),
|
|
1472
|
+
requestId: firstString(record, ['requestId', 'reqid', 'id']),
|
|
1473
|
+
url: firstString(record, ['url']),
|
|
1474
|
+
method: firstString(record, ['method']),
|
|
1475
|
+
status: firstNumber(record, ['status']),
|
|
1476
|
+
};
|
|
1477
|
+
}
|
|
1478
|
+
function extractResponseBody(result) {
|
|
1479
|
+
const direct = extractStructured(result, ['responseBody', 'body']);
|
|
1480
|
+
if (typeof direct === 'string') {
|
|
1481
|
+
return direct;
|
|
1482
|
+
}
|
|
1483
|
+
const parsed = parseResultText(result);
|
|
1484
|
+
if (parsed && typeof parsed === 'object') {
|
|
1485
|
+
for (const key of ['responseBody', 'body']) {
|
|
1486
|
+
const candidate = parsed[key];
|
|
1487
|
+
if (typeof candidate === 'string') {
|
|
1488
|
+
return candidate;
|
|
1489
|
+
}
|
|
1490
|
+
}
|
|
1491
|
+
}
|
|
1492
|
+
return firstText(result);
|
|
1493
|
+
}
|
|
1494
|
+
function guessRequestId(value) {
|
|
1495
|
+
if (!value || typeof value !== 'object') {
|
|
1496
|
+
return undefined;
|
|
1497
|
+
}
|
|
1498
|
+
const record = value;
|
|
1499
|
+
if (typeof record.requestId === 'string') {
|
|
1500
|
+
return record.requestId;
|
|
1501
|
+
}
|
|
1502
|
+
return undefined;
|
|
1503
|
+
}
|
|
1504
|
+
function extractStructured(result, keys) {
|
|
1505
|
+
for (const key of keys) {
|
|
1506
|
+
if (key in result) {
|
|
1507
|
+
return result[key];
|
|
1508
|
+
}
|
|
1509
|
+
}
|
|
1510
|
+
return undefined;
|
|
1511
|
+
}
|
|
1512
|
+
function parseResultText(result) {
|
|
1513
|
+
const text = firstText(result);
|
|
1514
|
+
if (!text) {
|
|
1515
|
+
return undefined;
|
|
1516
|
+
}
|
|
1517
|
+
return parseJsonText(text);
|
|
1518
|
+
}
|
|
1519
|
+
function firstText(result) {
|
|
1520
|
+
const item = result.content?.find((entry) => entry.type === 'text');
|
|
1521
|
+
return item && 'text' in item && typeof item.text === 'string' ? item.text : '';
|
|
1522
|
+
}
|
|
1523
|
+
function parseJsonText(text) {
|
|
1524
|
+
const trimmed = text.trim();
|
|
1525
|
+
if (!trimmed) {
|
|
1526
|
+
return undefined;
|
|
1527
|
+
}
|
|
1528
|
+
const candidates = [trimmed];
|
|
1529
|
+
if (trimmed.startsWith('```')) {
|
|
1530
|
+
const fenced = trimmed
|
|
1531
|
+
.replace(/^```[a-zA-Z0-9_-]*\n/, '')
|
|
1532
|
+
.replace(/\n```$/, '')
|
|
1533
|
+
.trim();
|
|
1534
|
+
candidates.push(fenced);
|
|
1535
|
+
}
|
|
1536
|
+
for (const candidate of candidates) {
|
|
1537
|
+
try {
|
|
1538
|
+
return JSON.parse(candidate);
|
|
1539
|
+
}
|
|
1540
|
+
catch {
|
|
1541
|
+
// Try next candidate.
|
|
1542
|
+
}
|
|
1543
|
+
}
|
|
1544
|
+
return undefined;
|
|
1545
|
+
}
|
|
1546
|
+
function parseJsonValue(text, label) {
|
|
1547
|
+
try {
|
|
1548
|
+
return JSON.parse(text);
|
|
1549
|
+
}
|
|
1550
|
+
catch (error) {
|
|
1551
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
1552
|
+
throw new Error(`${label} must be valid JSON: ${message}`);
|
|
1553
|
+
}
|
|
1554
|
+
}
|
|
1555
|
+
function stringifyJson(value) {
|
|
1556
|
+
return typeof value === 'string' ? value : JSON.stringify(value, null, 2);
|
|
1557
|
+
}
|
|
1558
|
+
function limitText(text, limit) {
|
|
1559
|
+
if (!limit || limit <= 0) {
|
|
1560
|
+
return text;
|
|
1561
|
+
}
|
|
1562
|
+
const lines = text.split('\n');
|
|
1563
|
+
return lines.length <= limit ? text : `${lines.slice(0, limit).join('\n')}\n...`;
|
|
1564
|
+
}
|
|
1565
|
+
function parsePositiveInteger(value, label) {
|
|
1566
|
+
const parsed = Number.parseInt(value, 10);
|
|
1567
|
+
if (!Number.isFinite(parsed) || parsed <= 0) {
|
|
1568
|
+
throw new Error(`${label} must be a positive integer`);
|
|
1569
|
+
}
|
|
1570
|
+
return parsed;
|
|
1571
|
+
}
|
|
1572
|
+
function normalizeName(value) {
|
|
1573
|
+
return value.replace(/[-\s]/g, '_').toLowerCase();
|
|
1574
|
+
}
|
|
1575
|
+
function toolProperties(tool) {
|
|
1576
|
+
return tool.inputSchema.properties ?? {};
|
|
1577
|
+
}
|
|
1578
|
+
function hasProperty(tool, ...names) {
|
|
1579
|
+
const properties = toolProperties(tool);
|
|
1580
|
+
return names.find((name) => Object.prototype.hasOwnProperty.call(properties, name));
|
|
1581
|
+
}
|
|
1582
|
+
function withCanonicalArgs(tool, canonicalArgs) {
|
|
1583
|
+
const output = {};
|
|
1584
|
+
for (const [key, value] of Object.entries(canonicalArgs)) {
|
|
1585
|
+
if (value === undefined) {
|
|
1586
|
+
continue;
|
|
1587
|
+
}
|
|
1588
|
+
if (Object.prototype.hasOwnProperty.call(toolProperties(tool), key)) {
|
|
1589
|
+
output[key] = value;
|
|
1590
|
+
}
|
|
1591
|
+
}
|
|
1592
|
+
return output;
|
|
1593
|
+
}
|
|
1594
|
+
function withUrlArgs(tool, url) {
|
|
1595
|
+
const args = {};
|
|
1596
|
+
const urlKey = hasProperty(tool, 'url');
|
|
1597
|
+
if (urlKey)
|
|
1598
|
+
args[urlKey] = url;
|
|
1599
|
+
const typeKey = hasProperty(tool, 'type');
|
|
1600
|
+
if (typeKey && normalizeName(tool.name) === 'navigate_page') {
|
|
1601
|
+
args[typeKey] = 'url';
|
|
1602
|
+
}
|
|
1603
|
+
return args;
|
|
1604
|
+
}
|
|
1605
|
+
function withOpenArgs(tool, url) {
|
|
1606
|
+
const args = withUrlArgs(tool, url);
|
|
1607
|
+
const waitForReadyKey = hasProperty(tool, 'waitForReady');
|
|
1608
|
+
if (waitForReadyKey) {
|
|
1609
|
+
args[waitForReadyKey] = false;
|
|
1610
|
+
}
|
|
1611
|
+
return stripUndefined(args);
|
|
1612
|
+
}
|
|
1613
|
+
function withNavigateArgs(tool, url, timeoutMs) {
|
|
1614
|
+
const args = withUrlArgs(tool, url);
|
|
1615
|
+
const pageIdKey = hasProperty(tool, 'pageId', 'tabId');
|
|
1616
|
+
if (pageIdKey && !(pageIdKey in args)) {
|
|
1617
|
+
args[pageIdKey] = undefined;
|
|
1618
|
+
}
|
|
1619
|
+
const timeoutKey = hasProperty(tool, 'timeoutMs', 'timeout');
|
|
1620
|
+
if (timeoutKey && typeof timeoutMs === 'number' && Number.isFinite(timeoutMs)) {
|
|
1621
|
+
args[timeoutKey] = timeoutMs;
|
|
1622
|
+
}
|
|
1623
|
+
return stripUndefined(args);
|
|
1624
|
+
}
|
|
1625
|
+
function withPageArgs(tool, id) {
|
|
1626
|
+
const args = {};
|
|
1627
|
+
const numeric = Number.parseInt(id, 10);
|
|
1628
|
+
const key = hasProperty(tool, 'pageId', 'tabId', 'id');
|
|
1629
|
+
if (key) {
|
|
1630
|
+
args[key] = Number.isFinite(numeric) && String(numeric) === id ? numeric : id;
|
|
1631
|
+
}
|
|
1632
|
+
return args;
|
|
1633
|
+
}
|
|
1634
|
+
function withSnapshotArgs(tool, options) {
|
|
1635
|
+
const args = {};
|
|
1636
|
+
maybeAssign(args, tool, 'format', options.format);
|
|
1637
|
+
maybeAssign(args, tool, 'selector', options.selector);
|
|
1638
|
+
maybeAssign(args, tool, 'frame', options.frame);
|
|
1639
|
+
maybeAssign(args, tool, 'compact', options.compact || undefined);
|
|
1640
|
+
maybeAssign(args, tool, 'depth', options.depth);
|
|
1641
|
+
maybeAssign(args, tool, 'efficient', options.efficient || undefined);
|
|
1642
|
+
maybeAssign(args, tool, 'labels', options.labels || undefined);
|
|
1643
|
+
maybeAssign(args, tool, 'a11y', true);
|
|
1644
|
+
maybeAssign(args, tool, 'markdown', false);
|
|
1645
|
+
return args;
|
|
1646
|
+
}
|
|
1647
|
+
function withScreenshotArgs(tool, options) {
|
|
1648
|
+
const args = {};
|
|
1649
|
+
if (options.ref) {
|
|
1650
|
+
Object.assign(args, withRefArgs(tool, options.ref));
|
|
1651
|
+
}
|
|
1652
|
+
maybeAssign(args, tool, 'fullPage', options.fullPage || undefined);
|
|
1653
|
+
maybeAssign(args, tool, 'detail', options.detail);
|
|
1654
|
+
maybeAssign(args, tool, 'grayscale', options.grayscale || undefined);
|
|
1655
|
+
return args;
|
|
1656
|
+
}
|
|
1657
|
+
function withRequestListArgs(tool, options) {
|
|
1658
|
+
const args = {};
|
|
1659
|
+
if (options.limit) {
|
|
1660
|
+
maybeAssign(args, tool, 'limit', options.limit);
|
|
1661
|
+
maybeAssign(args, tool, 'pageSize', options.limit);
|
|
1662
|
+
}
|
|
1663
|
+
return args;
|
|
1664
|
+
}
|
|
1665
|
+
function withRequestDetailsArgs(tool, requestId) {
|
|
1666
|
+
const args = {};
|
|
1667
|
+
maybeAssign(args, tool, 'requestId', requestId);
|
|
1668
|
+
maybeAssign(args, tool, 'reqid', requestId);
|
|
1669
|
+
return args;
|
|
1670
|
+
}
|
|
1671
|
+
function withResizeArgs(tool, width, height) {
|
|
1672
|
+
const args = {};
|
|
1673
|
+
maybeAssign(args, tool, 'width', width);
|
|
1674
|
+
maybeAssign(args, tool, 'height', height);
|
|
1675
|
+
return args;
|
|
1676
|
+
}
|
|
1677
|
+
function withRefArgs(tool, ref, extra = {}) {
|
|
1678
|
+
const args = { ...extra };
|
|
1679
|
+
const parsedRef = parseRef(ref);
|
|
1680
|
+
if (hasProperty(tool, 'ref')) {
|
|
1681
|
+
args.ref = parsedRef.raw;
|
|
1682
|
+
}
|
|
1683
|
+
if (parsedRef.numeric !== undefined && hasProperty(tool, 'index')) {
|
|
1684
|
+
args.index = parsedRef.numeric;
|
|
1685
|
+
}
|
|
1686
|
+
if (hasProperty(tool, 'uid')) {
|
|
1687
|
+
args.uid = parsedRef.raw;
|
|
1688
|
+
}
|
|
1689
|
+
if (hasProperty(tool, 'selector')) {
|
|
1690
|
+
args.selector = parsedRef.raw;
|
|
1691
|
+
}
|
|
1692
|
+
return args;
|
|
1693
|
+
}
|
|
1694
|
+
function withFillArgs(tool, ref, value) {
|
|
1695
|
+
const args = withRefArgs(tool, ref);
|
|
1696
|
+
if (hasProperty(tool, 'value')) {
|
|
1697
|
+
args.value = Array.isArray(value) ? value[0] : value;
|
|
1698
|
+
}
|
|
1699
|
+
if (hasProperty(tool, 'text')) {
|
|
1700
|
+
args.text = Array.isArray(value) ? value.join(' ') : value;
|
|
1701
|
+
}
|
|
1702
|
+
return args;
|
|
1703
|
+
}
|
|
1704
|
+
function withTypeArgs(tool, text, submitKey) {
|
|
1705
|
+
const args = {};
|
|
1706
|
+
maybeAssign(args, tool, 'text', text);
|
|
1707
|
+
maybeAssign(args, tool, 'value', text);
|
|
1708
|
+
if (submitKey) {
|
|
1709
|
+
maybeAssign(args, tool, 'submitKey', submitKey);
|
|
1710
|
+
}
|
|
1711
|
+
return args;
|
|
1712
|
+
}
|
|
1713
|
+
function withKeysArgs(tool, keys) {
|
|
1714
|
+
const args = {};
|
|
1715
|
+
maybeAssign(args, tool, 'keys', keys);
|
|
1716
|
+
maybeAssign(args, tool, 'shortcut', keys);
|
|
1717
|
+
maybeAssign(args, tool, 'key', keys);
|
|
1718
|
+
return args;
|
|
1719
|
+
}
|
|
1720
|
+
function withDragArgs(tool, source, target) {
|
|
1721
|
+
const args = {};
|
|
1722
|
+
if (hasProperty(tool, 'source')) {
|
|
1723
|
+
args.source = source;
|
|
1724
|
+
}
|
|
1725
|
+
if (hasProperty(tool, 'target')) {
|
|
1726
|
+
args.target = target;
|
|
1727
|
+
}
|
|
1728
|
+
if (hasProperty(tool, 'from_uid')) {
|
|
1729
|
+
args.from_uid = source;
|
|
1730
|
+
}
|
|
1731
|
+
if (hasProperty(tool, 'to_uid')) {
|
|
1732
|
+
args.to_uid = target;
|
|
1733
|
+
}
|
|
1734
|
+
return args;
|
|
1735
|
+
}
|
|
1736
|
+
function withSelectArgs(tool, ref, values) {
|
|
1737
|
+
const args = withRefArgs(tool, ref);
|
|
1738
|
+
maybeAssign(args, tool, 'value', values.length === 1 ? values[0] : values);
|
|
1739
|
+
maybeAssign(args, tool, 'values', values);
|
|
1740
|
+
return args;
|
|
1741
|
+
}
|
|
1742
|
+
function withFillFormArgs(tool, fields) {
|
|
1743
|
+
const args = {};
|
|
1744
|
+
maybeAssign(args, tool, 'fields', fields);
|
|
1745
|
+
maybeAssign(args, tool, 'elements', fields);
|
|
1746
|
+
return args;
|
|
1747
|
+
}
|
|
1748
|
+
function withUploadArgs(tool, ref, path) {
|
|
1749
|
+
const args = withRefArgs(tool, ref);
|
|
1750
|
+
maybeAssign(args, tool, 'filePath', path);
|
|
1751
|
+
maybeAssign(args, tool, 'path', path);
|
|
1752
|
+
if (hasProperty(tool, 'paths')) {
|
|
1753
|
+
args.paths = [path];
|
|
1754
|
+
}
|
|
1755
|
+
const filenameKey = hasProperty(tool, 'filename');
|
|
1756
|
+
const mimeTypeKey = hasProperty(tool, 'mimeType');
|
|
1757
|
+
const contentBase64Key = hasProperty(tool, 'contentBase64');
|
|
1758
|
+
const hasTopLevelPayload = Boolean(filenameKey && mimeTypeKey && contentBase64Key);
|
|
1759
|
+
const fileKey = hasProperty(tool, 'file');
|
|
1760
|
+
const fileSchema = fileKey ? toolProperties(tool)[fileKey] : undefined;
|
|
1761
|
+
const fileProperties = isRecord(fileSchema) && isRecord(fileSchema.properties) ? fileSchema.properties : undefined;
|
|
1762
|
+
const hasNestedPayload = Boolean(fileKey
|
|
1763
|
+
&& fileProperties
|
|
1764
|
+
&& Object.prototype.hasOwnProperty.call(fileProperties, 'filename')
|
|
1765
|
+
&& Object.prototype.hasOwnProperty.call(fileProperties, 'mimeType')
|
|
1766
|
+
&& Object.prototype.hasOwnProperty.call(fileProperties, 'contentBase64'));
|
|
1767
|
+
if (!hasTopLevelPayload && !hasNestedPayload) {
|
|
1768
|
+
return args;
|
|
1769
|
+
}
|
|
1770
|
+
const filePayload = buildUploadFilePayload(path);
|
|
1771
|
+
if (hasTopLevelPayload && filenameKey && mimeTypeKey && contentBase64Key) {
|
|
1772
|
+
args[filenameKey] = filePayload.filename;
|
|
1773
|
+
args[mimeTypeKey] = filePayload.mimeType;
|
|
1774
|
+
args[contentBase64Key] = filePayload.contentBase64;
|
|
1775
|
+
}
|
|
1776
|
+
if (hasNestedPayload && fileKey) {
|
|
1777
|
+
args[fileKey] = filePayload;
|
|
1778
|
+
}
|
|
1779
|
+
return args;
|
|
1780
|
+
}
|
|
1781
|
+
function withDialogArgs(tool, accept, promptText) {
|
|
1782
|
+
const args = {};
|
|
1783
|
+
if (hasProperty(tool, 'action')) {
|
|
1784
|
+
args.action = accept ? 'accept' : 'dismiss';
|
|
1785
|
+
}
|
|
1786
|
+
if (hasProperty(tool, 'accept')) {
|
|
1787
|
+
args.accept = accept;
|
|
1788
|
+
}
|
|
1789
|
+
if (promptText !== undefined) {
|
|
1790
|
+
maybeAssign(args, tool, 'promptText', promptText);
|
|
1791
|
+
maybeAssign(args, tool, 'prompt', promptText);
|
|
1792
|
+
if (hasProperty(tool, 'prompt_text')) {
|
|
1793
|
+
args.prompt_text = promptText;
|
|
1794
|
+
}
|
|
1795
|
+
}
|
|
1796
|
+
return args;
|
|
1797
|
+
}
|
|
1798
|
+
function withWaitArgs(tool, text, timeoutMs) {
|
|
1799
|
+
const args = {};
|
|
1800
|
+
maybeAssign(args, tool, 'text', text);
|
|
1801
|
+
maybeAssign(args, tool, 'timeout', timeoutMs);
|
|
1802
|
+
return args;
|
|
1803
|
+
}
|
|
1804
|
+
function withEvaluateArgs(tool, fn, ref, argsJson) {
|
|
1805
|
+
const args = {};
|
|
1806
|
+
maybeAssign(args, tool, 'function', fn);
|
|
1807
|
+
const values = [];
|
|
1808
|
+
if (ref) {
|
|
1809
|
+
values.push(ref);
|
|
1810
|
+
}
|
|
1811
|
+
if (argsJson) {
|
|
1812
|
+
const parsed = parseJsonValue(argsJson, '--args');
|
|
1813
|
+
if (!Array.isArray(parsed)) {
|
|
1814
|
+
throw new Error('--args must be a JSON array');
|
|
1815
|
+
}
|
|
1816
|
+
for (const entry of parsed) {
|
|
1817
|
+
values.push(typeof entry === 'string' ? entry : JSON.stringify(entry));
|
|
1818
|
+
}
|
|
1819
|
+
}
|
|
1820
|
+
if (values.length > 0) {
|
|
1821
|
+
maybeAssign(args, tool, 'args', values);
|
|
1822
|
+
}
|
|
1823
|
+
return args;
|
|
1824
|
+
}
|
|
1825
|
+
function maybeAssign(target, tool, property, value) {
|
|
1826
|
+
if (value === undefined) {
|
|
1827
|
+
return;
|
|
1828
|
+
}
|
|
1829
|
+
if (hasProperty(tool, property)) {
|
|
1830
|
+
target[property] = value;
|
|
1831
|
+
}
|
|
1832
|
+
}
|
|
1833
|
+
function stripUndefined(value) {
|
|
1834
|
+
return Object.fromEntries(Object.entries(value).filter(([, entry]) => entry !== undefined));
|
|
1835
|
+
}
|
|
1836
|
+
function buildUploadFilePayload(path) {
|
|
1837
|
+
let contentBase64;
|
|
1838
|
+
try {
|
|
1839
|
+
contentBase64 = readFileSync(path).toString('base64');
|
|
1840
|
+
}
|
|
1841
|
+
catch (error) {
|
|
1842
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
1843
|
+
throw new Error(`Failed to read upload file at "${path}": ${message}`);
|
|
1844
|
+
}
|
|
1845
|
+
return {
|
|
1846
|
+
filename: basename(path),
|
|
1847
|
+
mimeType: MIME_TYPE_BY_EXTENSION[extname(path).toLowerCase()] ?? 'application/octet-stream',
|
|
1848
|
+
contentBase64,
|
|
1849
|
+
};
|
|
1850
|
+
}
|
|
1851
|
+
function isRecord(value) {
|
|
1852
|
+
return typeof value === 'object' && value !== null;
|
|
1853
|
+
}
|
|
1854
|
+
function parseRef(ref) {
|
|
1855
|
+
const numeric = Number.parseInt(ref, 10);
|
|
1856
|
+
return {
|
|
1857
|
+
raw: ref,
|
|
1858
|
+
numeric: Number.isFinite(numeric) && String(numeric) === ref ? numeric : undefined,
|
|
1859
|
+
};
|
|
1860
|
+
}
|
|
1861
|
+
function firstDefined(record, keys) {
|
|
1862
|
+
for (const key of keys) {
|
|
1863
|
+
if (record[key] !== undefined) {
|
|
1864
|
+
return record[key];
|
|
1865
|
+
}
|
|
1866
|
+
}
|
|
1867
|
+
return undefined;
|
|
1868
|
+
}
|
|
1869
|
+
function firstString(record, keys) {
|
|
1870
|
+
const value = firstDefined(record, keys);
|
|
1871
|
+
return typeof value === 'string' ? value : undefined;
|
|
1872
|
+
}
|
|
1873
|
+
function firstBoolean(record, keys) {
|
|
1874
|
+
const value = firstDefined(record, keys);
|
|
1875
|
+
return typeof value === 'boolean' ? value : undefined;
|
|
1876
|
+
}
|
|
1877
|
+
function firstNumber(record, keys) {
|
|
1878
|
+
const value = firstDefined(record, keys);
|
|
1879
|
+
return typeof value === 'number' ? value : undefined;
|
|
1880
|
+
}
|
|
1881
|
+
//# sourceMappingURL=browser-cli.js.map
|