@vibebrowser/mcp 0.2.4 → 0.2.5
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +75 -12
- package/dist/browser-cli.d.ts +4 -0
- package/dist/browser-cli.d.ts.map +1 -0
- package/dist/browser-cli.js +1473 -0
- package/dist/browser-cli.js.map +1 -0
- package/dist/browser-main.d.ts +3 -0
- package/dist/browser-main.d.ts.map +1 -0
- package/dist/browser-main.js +10 -0
- package/dist/browser-main.js.map +1 -0
- package/dist/cli.d.ts +0 -2
- package/dist/cli.d.ts.map +1 -1
- package/dist/cli.js +104 -13
- package/dist/cli.js.map +1 -1
- package/dist/connection.d.ts +1 -0
- package/dist/connection.d.ts.map +1 -1
- package/dist/connection.js +10 -4
- package/dist/connection.js.map +1 -1
- package/dist/ollama.d.ts +1 -1
- package/dist/ollama.js +4 -4
- package/dist/ollama.js.map +1 -1
- package/dist/relay.d.ts +8 -7
- package/dist/relay.d.ts.map +1 -1
- package/dist/relay.js +53 -39
- package/dist/relay.js.map +1 -1
- package/dist/server.d.ts +52 -11
- package/dist/server.d.ts.map +1 -1
- package/dist/server.js +303 -64
- package/dist/server.js.map +1 -1
- package/dist/types.d.ts +16 -0
- package/dist/types.d.ts.map +1 -1
- package/dist/types.js +8 -0
- package/dist/types.js.map +1 -1
- package/dist/version.d.ts +2 -0
- package/dist/version.d.ts.map +1 -0
- package/dist/version.js +15 -0
- package/dist/version.js.map +1 -0
- package/docs/chrome-devtools-relay.md +351 -0
- package/docs/eval.md +209 -0
- package/docs/openclaw-local-browser.md +173 -0
- package/openclaw/vibe-local-browser/SKILL.md +132 -0
- package/package.json +8 -2
|
@@ -0,0 +1,1473 @@
|
|
|
1
|
+
import { writeFileSync } from 'node:fs';
|
|
2
|
+
import { resolve } from 'node:path';
|
|
3
|
+
import { setTimeout as delay } from 'node:timers/promises';
|
|
4
|
+
import { ExtensionConnection } from './connection.js';
|
|
5
|
+
import { DEFAULT_WS_PORT } from './types.js';
|
|
6
|
+
const DEFAULT_TIMEOUT_MS = 30_000;
|
|
7
|
+
const DEFAULT_BROWSER_PROFILE = process.env.VIBE_BROWSER_PROFILE || 'user';
|
|
8
|
+
const DEFAULT_REMOTE_UUID = process.env.VIBE_EXTENSION_UUID || process.env.VIBE_RELAY_UUID;
|
|
9
|
+
const DEFAULT_REMOTE_RELAY_URL = process.env.VIBE_REMOTE_RELAY_URL || process.env.VIBE_RELAY_URL;
|
|
10
|
+
export function registerBrowserCommand(program) {
|
|
11
|
+
const browser = buildBrowserCommand(program.command('browser'));
|
|
12
|
+
registerBrowserSubcommands(browser);
|
|
13
|
+
}
|
|
14
|
+
export function registerStandaloneBrowserCli(program) {
|
|
15
|
+
const browser = buildBrowserCommand(program);
|
|
16
|
+
registerBrowserSubcommands(browser);
|
|
17
|
+
}
|
|
18
|
+
function buildBrowserCommand(command) {
|
|
19
|
+
return command
|
|
20
|
+
.description('OpenClaw-compatible browser CLI for the connected Vibe browser session')
|
|
21
|
+
.option('--browser-profile <name>', 'Compatibility profile name', DEFAULT_BROWSER_PROFILE)
|
|
22
|
+
.option('--target <target>', 'OpenClaw compatibility target selector (accepted, not used by the Vibe browser CLI)')
|
|
23
|
+
.option('-p, --port <number>', 'WebSocket port for local relay (agent) connection', String(DEFAULT_WS_PORT))
|
|
24
|
+
.option('-d, --debug', 'Enable debug logging', false)
|
|
25
|
+
.option('-r, --remote <uuid>', 'Connect to a remote extension via public relay (provide the extension UUID)', DEFAULT_REMOTE_UUID)
|
|
26
|
+
.option('--relay-url <url>', 'Custom relay server URL', DEFAULT_REMOTE_RELAY_URL)
|
|
27
|
+
.option('--json', 'Emit machine-readable JSON output', false)
|
|
28
|
+
.option('--timeout <ms>', 'Command timeout in milliseconds', String(DEFAULT_TIMEOUT_MS));
|
|
29
|
+
}
|
|
30
|
+
function registerBrowserSubcommands(browser) {
|
|
31
|
+
browser
|
|
32
|
+
.command('status')
|
|
33
|
+
.description('Show browser bridge status')
|
|
34
|
+
.action(async function () {
|
|
35
|
+
await runBrowserCommand(this, 'status', false, async (ctx) => ctx.status());
|
|
36
|
+
});
|
|
37
|
+
browser
|
|
38
|
+
.command('start')
|
|
39
|
+
.description('Connect to the browser bridge and verify the session is reachable')
|
|
40
|
+
.action(async function () {
|
|
41
|
+
await runBrowserCommand(this, 'start', false, async (ctx) => {
|
|
42
|
+
const status = await ctx.status();
|
|
43
|
+
return {
|
|
44
|
+
...status,
|
|
45
|
+
started: status.extensionConnected,
|
|
46
|
+
managedLifecycle: false,
|
|
47
|
+
note: status.extensionConnected
|
|
48
|
+
? 'Connected to Vibe browser session'
|
|
49
|
+
: 'Vibe uses an attach-only browser session; no managed browser process was started',
|
|
50
|
+
};
|
|
51
|
+
});
|
|
52
|
+
});
|
|
53
|
+
browser
|
|
54
|
+
.command('stop')
|
|
55
|
+
.description('Compatibility no-op: the Vibe browser bridge does not manage browser process lifecycle')
|
|
56
|
+
.action(async function () {
|
|
57
|
+
await runBrowserCommand(this, 'stop', false, async (ctx) => ({
|
|
58
|
+
...await ctx.status(),
|
|
59
|
+
stopped: false,
|
|
60
|
+
managedLifecycle: false,
|
|
61
|
+
note: 'The Vibe browser bridge does not own the browser process, so stop only disconnects this CLI session',
|
|
62
|
+
}));
|
|
63
|
+
});
|
|
64
|
+
browser
|
|
65
|
+
.command('tabs')
|
|
66
|
+
.description('List browser tabs/pages')
|
|
67
|
+
.action(async function () {
|
|
68
|
+
await runBrowserCommand(this, 'tabs', true, async (ctx) => ctx.listPages());
|
|
69
|
+
});
|
|
70
|
+
const tab = browser.command('tab').description('Tab helpers');
|
|
71
|
+
tab
|
|
72
|
+
.action(async function () {
|
|
73
|
+
await runBrowserCommand(this, 'tab', true, async (ctx) => ctx.listPages());
|
|
74
|
+
});
|
|
75
|
+
tab
|
|
76
|
+
.command('new [url]')
|
|
77
|
+
.description('Open a new tab')
|
|
78
|
+
.action(async function (url) {
|
|
79
|
+
await runBrowserCommand(this, 'tab new', true, async (ctx) => ctx.open(url));
|
|
80
|
+
});
|
|
81
|
+
tab
|
|
82
|
+
.command('select <id>')
|
|
83
|
+
.description('Focus a tab/page')
|
|
84
|
+
.action(async function (id) {
|
|
85
|
+
await runBrowserCommand(this, 'tab select', true, async (ctx) => ctx.focus(id));
|
|
86
|
+
});
|
|
87
|
+
tab
|
|
88
|
+
.command('close <id>')
|
|
89
|
+
.description('Close a tab/page')
|
|
90
|
+
.action(async function (id) {
|
|
91
|
+
await runBrowserCommand(this, 'tab close', true, async (ctx) => ctx.close(id));
|
|
92
|
+
});
|
|
93
|
+
browser
|
|
94
|
+
.command('open <url>')
|
|
95
|
+
.description('Open a URL in a new page or navigate using the best available tool')
|
|
96
|
+
.action(async function (url) {
|
|
97
|
+
await runBrowserCommand(this, 'open', true, async (ctx) => ctx.open(url));
|
|
98
|
+
});
|
|
99
|
+
browser
|
|
100
|
+
.command('navigate <url>')
|
|
101
|
+
.description('Navigate the active page to a URL')
|
|
102
|
+
.action(async function (url) {
|
|
103
|
+
await runBrowserCommand(this, 'navigate', true, async (ctx) => ctx.navigate(url));
|
|
104
|
+
});
|
|
105
|
+
browser
|
|
106
|
+
.command('focus <id>')
|
|
107
|
+
.description('Focus a tab/page')
|
|
108
|
+
.action(async function (id) {
|
|
109
|
+
await runBrowserCommand(this, 'focus', true, async (ctx) => ctx.focus(id));
|
|
110
|
+
});
|
|
111
|
+
browser
|
|
112
|
+
.command('close <id>')
|
|
113
|
+
.description('Close a tab/page')
|
|
114
|
+
.action(async function (id) {
|
|
115
|
+
await runBrowserCommand(this, 'close', true, async (ctx) => ctx.close(id));
|
|
116
|
+
});
|
|
117
|
+
browser
|
|
118
|
+
.command('snapshot')
|
|
119
|
+
.description('Capture a textual browser snapshot')
|
|
120
|
+
.option('--format <format>', 'Snapshot format (ai or aria)', 'ai')
|
|
121
|
+
.option('--limit <count>', 'Max visible lines/items to print in human mode')
|
|
122
|
+
.option('--interactive', 'Prefer interactive/ARIA-flavored snapshot output', false)
|
|
123
|
+
.option('--selector <selector>', 'Selector to scope the snapshot to')
|
|
124
|
+
.option('--frame <selector>', 'Frame selector for the snapshot')
|
|
125
|
+
.option('--compact', 'Compatibility flag', false)
|
|
126
|
+
.option('--depth <depth>', 'Compatibility flag')
|
|
127
|
+
.option('--efficient', 'Compatibility flag', false)
|
|
128
|
+
.option('--labels', 'Include labels when supported by backend', false)
|
|
129
|
+
.action(async function () {
|
|
130
|
+
await runBrowserCommand(this, 'snapshot', true, async (ctx, options) => ctx.snapshot({
|
|
131
|
+
format: String(options.format || 'ai'),
|
|
132
|
+
limit: options.limit ? parsePositiveInteger(String(options.limit), '--limit') : undefined,
|
|
133
|
+
interactive: Boolean(options.interactive),
|
|
134
|
+
selector: options.selector ? String(options.selector) : undefined,
|
|
135
|
+
frame: options.frame ? String(options.frame) : undefined,
|
|
136
|
+
compact: Boolean(options.compact),
|
|
137
|
+
depth: options.depth ? parsePositiveInteger(String(options.depth), '--depth') : undefined,
|
|
138
|
+
efficient: Boolean(options.efficient),
|
|
139
|
+
labels: Boolean(options.labels),
|
|
140
|
+
}));
|
|
141
|
+
});
|
|
142
|
+
browser
|
|
143
|
+
.command('screenshot')
|
|
144
|
+
.description('Capture a browser screenshot')
|
|
145
|
+
.option('--full-page', 'Request a full page screenshot when supported', false)
|
|
146
|
+
.option('--ref <ref>', 'Element ref/index')
|
|
147
|
+
.option('--output <path>', 'Write screenshot bytes to a file')
|
|
148
|
+
.option('--detail <level>', 'Screenshot detail level')
|
|
149
|
+
.option('--grayscale', 'Use grayscale when supported', false)
|
|
150
|
+
.action(async function () {
|
|
151
|
+
await runBrowserCommand(this, 'screenshot', true, async (ctx, options) => ctx.screenshot({
|
|
152
|
+
fullPage: Boolean(options.fullPage),
|
|
153
|
+
ref: options.ref ? String(options.ref) : undefined,
|
|
154
|
+
outputPath: options.output ? String(options.output) : undefined,
|
|
155
|
+
detail: options.detail ? String(options.detail) : undefined,
|
|
156
|
+
grayscale: Boolean(options.grayscale),
|
|
157
|
+
}));
|
|
158
|
+
});
|
|
159
|
+
browser
|
|
160
|
+
.command('pdf')
|
|
161
|
+
.description('Render the current page as PDF when supported')
|
|
162
|
+
.option('--output <path>', 'Write PDF bytes to a file')
|
|
163
|
+
.action(async function () {
|
|
164
|
+
await runBrowserCommand(this, 'pdf', true, async (ctx, options) => ctx.pdf(options.output ? String(options.output) : undefined));
|
|
165
|
+
});
|
|
166
|
+
browser
|
|
167
|
+
.command('console')
|
|
168
|
+
.description('List console messages')
|
|
169
|
+
.option('--level <level>', 'Console level filter')
|
|
170
|
+
.option('--preserve', 'Include preserved messages when supported', false)
|
|
171
|
+
.action(async function () {
|
|
172
|
+
await runBrowserCommand(this, 'console', true, async (ctx, options) => ctx.consoleMessages({
|
|
173
|
+
level: options.level ? String(options.level) : undefined,
|
|
174
|
+
preserve: Boolean(options.preserve),
|
|
175
|
+
}));
|
|
176
|
+
});
|
|
177
|
+
browser
|
|
178
|
+
.command('errors')
|
|
179
|
+
.description('List console errors')
|
|
180
|
+
.option('--clear', 'Compatibility flag', false)
|
|
181
|
+
.action(async function () {
|
|
182
|
+
await runBrowserCommand(this, 'errors', true, async (ctx) => ctx.consoleMessages({ level: 'error' }));
|
|
183
|
+
});
|
|
184
|
+
browser
|
|
185
|
+
.command('requests')
|
|
186
|
+
.description('List network requests')
|
|
187
|
+
.option('--filter <pattern>', 'Substring filter')
|
|
188
|
+
.option('--limit <count>', 'Maximum requests to print')
|
|
189
|
+
.option('--clear', 'Compatibility flag', false)
|
|
190
|
+
.action(async function () {
|
|
191
|
+
await runBrowserCommand(this, 'requests', true, async (ctx, options) => ctx.requests({
|
|
192
|
+
filter: options.filter ? String(options.filter) : undefined,
|
|
193
|
+
limit: options.limit ? parsePositiveInteger(String(options.limit), '--limit') : undefined,
|
|
194
|
+
}));
|
|
195
|
+
});
|
|
196
|
+
browser
|
|
197
|
+
.command('responsebody [pattern]')
|
|
198
|
+
.description('Show a matching response body when network inspection is available')
|
|
199
|
+
.option('--max-chars <count>', 'Truncate response body output')
|
|
200
|
+
.action(async function (pattern) {
|
|
201
|
+
await runBrowserCommand(this, 'responsebody', true, async (ctx, options) => ctx.responseBody({
|
|
202
|
+
pattern,
|
|
203
|
+
maxChars: options.maxChars
|
|
204
|
+
? parsePositiveInteger(String(options.maxChars), '--max-chars')
|
|
205
|
+
: undefined,
|
|
206
|
+
}));
|
|
207
|
+
});
|
|
208
|
+
browser
|
|
209
|
+
.command('resize <width> <height>')
|
|
210
|
+
.description('Resize the browser viewport')
|
|
211
|
+
.action(async function (width, height) {
|
|
212
|
+
await runBrowserCommand(this, 'resize', true, async (ctx) => ctx.resize(parsePositiveInteger(width, 'width'), parsePositiveInteger(height, 'height')));
|
|
213
|
+
});
|
|
214
|
+
browser
|
|
215
|
+
.command('click <ref>')
|
|
216
|
+
.description('Click an element by ref/index')
|
|
217
|
+
.option('--double', 'Double click when supported', false)
|
|
218
|
+
.action(async function (ref) {
|
|
219
|
+
await runBrowserCommand(this, 'click', true, async (ctx, options) => ctx.click(ref, { double: Boolean(options.double) }));
|
|
220
|
+
});
|
|
221
|
+
browser
|
|
222
|
+
.command('type <ref> <text>')
|
|
223
|
+
.description('Type/fill text into an element')
|
|
224
|
+
.option('--submit', 'Submit after typing', false)
|
|
225
|
+
.action(async function (ref, text) {
|
|
226
|
+
await runBrowserCommand(this, 'type', true, async (ctx, options) => ctx.type(ref, text, { submit: Boolean(options.submit) }));
|
|
227
|
+
});
|
|
228
|
+
browser
|
|
229
|
+
.command('press <keys>')
|
|
230
|
+
.description('Press a key or key chord')
|
|
231
|
+
.action(async function (keys) {
|
|
232
|
+
await runBrowserCommand(this, 'press', true, async (ctx) => ctx.press(keys));
|
|
233
|
+
});
|
|
234
|
+
browser
|
|
235
|
+
.command('hover <ref>')
|
|
236
|
+
.description('Hover an element by ref/index')
|
|
237
|
+
.action(async function (ref) {
|
|
238
|
+
await runBrowserCommand(this, 'hover', true, async (ctx) => ctx.hover(ref));
|
|
239
|
+
});
|
|
240
|
+
browser
|
|
241
|
+
.command('scrollintoview <ref>')
|
|
242
|
+
.description('Scroll an element into view when supported')
|
|
243
|
+
.action(async function (ref) {
|
|
244
|
+
await runBrowserCommand(this, 'scrollintoview', true, async (ctx) => ctx.scrollIntoView(ref));
|
|
245
|
+
});
|
|
246
|
+
browser
|
|
247
|
+
.command('drag <source> <target>')
|
|
248
|
+
.description('Drag one element to another')
|
|
249
|
+
.action(async function (source, target) {
|
|
250
|
+
await runBrowserCommand(this, 'drag', true, async (ctx) => ctx.drag(source, target));
|
|
251
|
+
});
|
|
252
|
+
browser
|
|
253
|
+
.command('select <ref> <values...>')
|
|
254
|
+
.description('Select one or more values in a field')
|
|
255
|
+
.action(async function (ref, values) {
|
|
256
|
+
await runBrowserCommand(this, 'select', true, async (ctx) => ctx.select(ref, values));
|
|
257
|
+
});
|
|
258
|
+
browser
|
|
259
|
+
.command('download <ref> [filename]')
|
|
260
|
+
.description('Trigger/download an element when supported')
|
|
261
|
+
.action(async function (ref, filename) {
|
|
262
|
+
await runBrowserCommand(this, 'download', true, async (ctx) => ctx.download(ref, filename));
|
|
263
|
+
});
|
|
264
|
+
browser
|
|
265
|
+
.command('waitfordownload [filename]')
|
|
266
|
+
.description('Wait for a download when supported')
|
|
267
|
+
.action(async function (filename) {
|
|
268
|
+
await runBrowserCommand(this, 'waitfordownload', true, async (ctx) => ctx.waitForDownload(filename));
|
|
269
|
+
});
|
|
270
|
+
browser
|
|
271
|
+
.command('upload <path>')
|
|
272
|
+
.description('Upload a file when supported')
|
|
273
|
+
.option('--ref <ref>', 'Element ref/index')
|
|
274
|
+
.action(async function (path) {
|
|
275
|
+
await runBrowserCommand(this, 'upload', true, async (ctx, options) => ctx.upload(path, options.ref ? String(options.ref) : undefined));
|
|
276
|
+
});
|
|
277
|
+
browser
|
|
278
|
+
.command('fill')
|
|
279
|
+
.description('Fill a form using JSON field descriptors')
|
|
280
|
+
.requiredOption('--fields <json>', 'JSON array of field descriptors')
|
|
281
|
+
.action(async function () {
|
|
282
|
+
await runBrowserCommand(this, 'fill', true, async (ctx, options) => ctx.fillForm(String(options.fields)));
|
|
283
|
+
});
|
|
284
|
+
browser
|
|
285
|
+
.command('dialog')
|
|
286
|
+
.description('Accept or dismiss a browser dialog')
|
|
287
|
+
.option('--accept', 'Accept the dialog', false)
|
|
288
|
+
.option('--dismiss', 'Dismiss the dialog', false)
|
|
289
|
+
.option('--prompt <text>', 'Prompt text to enter')
|
|
290
|
+
.action(async function () {
|
|
291
|
+
await runBrowserCommand(this, 'dialog', true, async (ctx, options) => ctx.dialog({
|
|
292
|
+
accept: Boolean(options.accept),
|
|
293
|
+
dismiss: Boolean(options.dismiss),
|
|
294
|
+
prompt: options.prompt ? String(options.prompt) : undefined,
|
|
295
|
+
}));
|
|
296
|
+
});
|
|
297
|
+
browser
|
|
298
|
+
.command('wait')
|
|
299
|
+
.description('Wait for text or a short delay')
|
|
300
|
+
.option('--text <value...>', 'Texts to wait for')
|
|
301
|
+
.option('--seconds <seconds>', 'Wait for a fixed number of seconds')
|
|
302
|
+
.option('--timeout <ms>', 'Override command timeout')
|
|
303
|
+
.action(async function () {
|
|
304
|
+
await runBrowserCommand(this, 'wait', true, async (ctx, options, baseOptions) => ctx.wait({
|
|
305
|
+
text: Array.isArray(options.text) ? options.text.map(String) : undefined,
|
|
306
|
+
seconds: options.seconds ? Number(options.seconds) : undefined,
|
|
307
|
+
timeoutMs: options.timeout ? parsePositiveInteger(String(options.timeout), '--timeout') : parsePositiveInteger(baseOptions.timeout, '--timeout'),
|
|
308
|
+
}));
|
|
309
|
+
});
|
|
310
|
+
browser
|
|
311
|
+
.command('evaluate')
|
|
312
|
+
.description('Evaluate JavaScript through the browser tool backend')
|
|
313
|
+
.requiredOption('--fn <function>', 'Function to evaluate')
|
|
314
|
+
.option('--ref <ref>', 'Element ref/index argument')
|
|
315
|
+
.option('--args <json>', 'JSON array of additional arguments')
|
|
316
|
+
.action(async function () {
|
|
317
|
+
await runBrowserCommand(this, 'evaluate', true, async (ctx, options) => ctx.evaluate({
|
|
318
|
+
fn: String(options.fn),
|
|
319
|
+
ref: options.ref ? String(options.ref) : undefined,
|
|
320
|
+
argsJson: options.args ? String(options.args) : undefined,
|
|
321
|
+
}));
|
|
322
|
+
});
|
|
323
|
+
browser
|
|
324
|
+
.command('highlight <ref>')
|
|
325
|
+
.description('Highlight an element when supported')
|
|
326
|
+
.action(async function (ref) {
|
|
327
|
+
await runBrowserCommand(this, 'highlight', true, async (ctx) => ctx.highlight(ref));
|
|
328
|
+
});
|
|
329
|
+
const trace = browser.command('trace').description('Start/stop browser tracing');
|
|
330
|
+
trace
|
|
331
|
+
.command('start')
|
|
332
|
+
.description('Start a trace')
|
|
333
|
+
.option('--reload', 'Reload the page during trace start when supported', false)
|
|
334
|
+
.option('--output <path>', 'Trace output path')
|
|
335
|
+
.action(async function () {
|
|
336
|
+
await runBrowserCommand(this, 'trace start', true, async (ctx, options) => ctx.traceStart({
|
|
337
|
+
reload: Boolean(options.reload),
|
|
338
|
+
outputPath: options.output ? String(options.output) : undefined,
|
|
339
|
+
}));
|
|
340
|
+
});
|
|
341
|
+
trace
|
|
342
|
+
.command('stop')
|
|
343
|
+
.description('Stop an active trace')
|
|
344
|
+
.option('--output <path>', 'Trace output path')
|
|
345
|
+
.action(async function () {
|
|
346
|
+
await runBrowserCommand(this, 'trace stop', true, async (ctx, options) => ctx.traceStop(options.output ? String(options.output) : undefined));
|
|
347
|
+
});
|
|
348
|
+
}
|
|
349
|
+
async function runBrowserCommand(command, commandName, requireExtension, handler) {
|
|
350
|
+
const globalOptions = command.optsWithGlobals();
|
|
351
|
+
const localOptions = command.opts();
|
|
352
|
+
const ctx = new BrowserCliContext({
|
|
353
|
+
port: parsePositiveInteger(globalOptions.port, '--port'),
|
|
354
|
+
debug: Boolean(globalOptions.debug),
|
|
355
|
+
remoteUuid: globalOptions.remote,
|
|
356
|
+
relayUrl: globalOptions.relayUrl,
|
|
357
|
+
profile: globalOptions.browserProfile || DEFAULT_BROWSER_PROFILE,
|
|
358
|
+
json: Boolean(globalOptions.json),
|
|
359
|
+
timeoutMs: parsePositiveInteger(globalOptions.timeout, '--timeout'),
|
|
360
|
+
target: globalOptions.target,
|
|
361
|
+
});
|
|
362
|
+
try {
|
|
363
|
+
await ctx.connect();
|
|
364
|
+
if (requireExtension) {
|
|
365
|
+
await ctx.ensureExtensionConnected();
|
|
366
|
+
}
|
|
367
|
+
const output = await handler(ctx, localOptions, globalOptions);
|
|
368
|
+
emitOutput(Boolean(globalOptions.json), output, formatHumanOutput(commandName, output));
|
|
369
|
+
}
|
|
370
|
+
catch (error) {
|
|
371
|
+
emitError(Boolean(globalOptions.json), commandName, ctx, error);
|
|
372
|
+
process.exitCode = 1;
|
|
373
|
+
}
|
|
374
|
+
finally {
|
|
375
|
+
await ctx.shutdown();
|
|
376
|
+
}
|
|
377
|
+
}
|
|
378
|
+
class BrowserCliContext {
|
|
379
|
+
connection;
|
|
380
|
+
profile;
|
|
381
|
+
json;
|
|
382
|
+
timeoutMs;
|
|
383
|
+
remoteUuid;
|
|
384
|
+
target;
|
|
385
|
+
toolsLoaded = false;
|
|
386
|
+
tools = [];
|
|
387
|
+
ignoredCompatibilityOptions;
|
|
388
|
+
constructor(init) {
|
|
389
|
+
this.connection = new ExtensionConnection(init.port, init.debug, init.remoteUuid ? { uuid: init.remoteUuid, relayUrl: init.relayUrl } : undefined);
|
|
390
|
+
this.profile = init.profile;
|
|
391
|
+
this.json = init.json;
|
|
392
|
+
this.timeoutMs = init.timeoutMs;
|
|
393
|
+
this.remoteUuid = init.remoteUuid;
|
|
394
|
+
this.target = init.target;
|
|
395
|
+
this.ignoredCompatibilityOptions = [];
|
|
396
|
+
if (this.target) {
|
|
397
|
+
this.ignoredCompatibilityOptions.push(`target=${this.target}`);
|
|
398
|
+
}
|
|
399
|
+
}
|
|
400
|
+
async connect() {
|
|
401
|
+
await this.connection.start();
|
|
402
|
+
await delay(100);
|
|
403
|
+
await this.connection.waitForToolsUpdate(500);
|
|
404
|
+
}
|
|
405
|
+
async shutdown() {
|
|
406
|
+
await this.connection.stop();
|
|
407
|
+
}
|
|
408
|
+
async ensureExtensionConnected() {
|
|
409
|
+
if (this.connection.isExtensionConnected()) {
|
|
410
|
+
return;
|
|
411
|
+
}
|
|
412
|
+
await this.ensureToolsLoaded();
|
|
413
|
+
if (!this.connection.isExtensionConnected()) {
|
|
414
|
+
throw new Error('No browser extension is connected to the Vibe relay');
|
|
415
|
+
}
|
|
416
|
+
}
|
|
417
|
+
async status() {
|
|
418
|
+
if (this.connection.isExtensionConnected()) {
|
|
419
|
+
await this.ensureToolsLoaded();
|
|
420
|
+
}
|
|
421
|
+
return {
|
|
422
|
+
ok: true,
|
|
423
|
+
command: 'status',
|
|
424
|
+
profile: this.profile,
|
|
425
|
+
mode: this.remoteUuid ? 'remote' : 'local',
|
|
426
|
+
ignoredCompatibilityOptions: this.ignoredCompatibilityOptions,
|
|
427
|
+
relayConnected: this.connection.getStatus() === 'connected',
|
|
428
|
+
extensionConnected: this.connection.isExtensionConnected(),
|
|
429
|
+
managedLifecycle: false,
|
|
430
|
+
transport: 'vibebrowser-mcp',
|
|
431
|
+
toolCount: this.tools.length,
|
|
432
|
+
tools: this.tools.map((tool) => tool.name),
|
|
433
|
+
};
|
|
434
|
+
}
|
|
435
|
+
async listPages() {
|
|
436
|
+
const invocation = await this.callTool('tabs', [
|
|
437
|
+
{ names: ['list_pages', 'get_tabs'] },
|
|
438
|
+
], {});
|
|
439
|
+
const pages = extractPages(invocation.result);
|
|
440
|
+
return {
|
|
441
|
+
ok: true,
|
|
442
|
+
command: 'tabs',
|
|
443
|
+
profile: this.profile,
|
|
444
|
+
mode: this.mode(),
|
|
445
|
+
ignoredCompatibilityOptions: this.ignoredCompatibilityOptions,
|
|
446
|
+
tool: invocation.tool,
|
|
447
|
+
pages,
|
|
448
|
+
raw: normalizeToolResult(invocation.result),
|
|
449
|
+
};
|
|
450
|
+
}
|
|
451
|
+
async open(url) {
|
|
452
|
+
if (!url) {
|
|
453
|
+
return this.callGenericCommand('open', [{ names: ['new_page', 'create_new_tab'] }], {});
|
|
454
|
+
}
|
|
455
|
+
const invocation = await this.callTool('open', [
|
|
456
|
+
{
|
|
457
|
+
names: ['new_page', 'create_new_tab'],
|
|
458
|
+
buildArgs: (tool) => withUrlArgs(tool, url),
|
|
459
|
+
},
|
|
460
|
+
{
|
|
461
|
+
names: ['navigate_page', 'navigate_to_url'],
|
|
462
|
+
buildArgs: (tool) => withNavigateArgs(tool, url),
|
|
463
|
+
},
|
|
464
|
+
], {});
|
|
465
|
+
return this.outputFromInvocation('open', invocation);
|
|
466
|
+
}
|
|
467
|
+
async navigate(url) {
|
|
468
|
+
const invocation = await this.callTool('navigate', [
|
|
469
|
+
{
|
|
470
|
+
names: ['navigate_page', 'navigate_to_url'],
|
|
471
|
+
buildArgs: (tool) => withNavigateArgs(tool, url),
|
|
472
|
+
},
|
|
473
|
+
{
|
|
474
|
+
names: ['new_page', 'create_new_tab'],
|
|
475
|
+
buildArgs: (tool) => withUrlArgs(tool, url),
|
|
476
|
+
},
|
|
477
|
+
], {});
|
|
478
|
+
return this.outputFromInvocation('navigate', invocation);
|
|
479
|
+
}
|
|
480
|
+
async focus(id) {
|
|
481
|
+
const invocation = await this.callTool('focus', [
|
|
482
|
+
{
|
|
483
|
+
names: ['select_page', 'switch_to_tab', 'focus_tab'],
|
|
484
|
+
buildArgs: (tool) => withPageArgs(tool, id),
|
|
485
|
+
},
|
|
486
|
+
], {});
|
|
487
|
+
return this.outputFromInvocation('focus', invocation);
|
|
488
|
+
}
|
|
489
|
+
async close(id) {
|
|
490
|
+
const invocation = await this.callTool('close', [
|
|
491
|
+
{
|
|
492
|
+
names: ['close_page', 'close_tab'],
|
|
493
|
+
buildArgs: (tool) => withPageArgs(tool, id),
|
|
494
|
+
},
|
|
495
|
+
], {});
|
|
496
|
+
return this.outputFromInvocation('close', invocation);
|
|
497
|
+
}
|
|
498
|
+
async snapshot(options) {
|
|
499
|
+
const wantsAria = options.format === 'aria' || options.interactive || Boolean(options.selector) || Boolean(options.frame);
|
|
500
|
+
if (!wantsAria) {
|
|
501
|
+
const result = await this.connection.getSnapshot();
|
|
502
|
+
return {
|
|
503
|
+
ok: true,
|
|
504
|
+
command: 'snapshot',
|
|
505
|
+
profile: this.profile,
|
|
506
|
+
mode: this.mode(),
|
|
507
|
+
ignoredCompatibilityOptions: this.ignoredCompatibilityOptions,
|
|
508
|
+
format: options.format,
|
|
509
|
+
url: result.url,
|
|
510
|
+
title: result.title,
|
|
511
|
+
snapshot: limitText(result.snapshot, options.limit),
|
|
512
|
+
};
|
|
513
|
+
}
|
|
514
|
+
const invocation = await this.callTool('snapshot', [
|
|
515
|
+
{
|
|
516
|
+
names: ['take_a11y_snapshot'],
|
|
517
|
+
buildArgs: (tool) => withSnapshotArgs(tool, options),
|
|
518
|
+
},
|
|
519
|
+
{
|
|
520
|
+
names: ['take_snapshot', 'get_page_content'],
|
|
521
|
+
buildArgs: (tool) => withSnapshotArgs(tool, options),
|
|
522
|
+
},
|
|
523
|
+
], {});
|
|
524
|
+
const text = firstText(invocation.result);
|
|
525
|
+
return {
|
|
526
|
+
ok: true,
|
|
527
|
+
command: 'snapshot',
|
|
528
|
+
profile: this.profile,
|
|
529
|
+
mode: this.mode(),
|
|
530
|
+
ignoredCompatibilityOptions: this.ignoredCompatibilityOptions,
|
|
531
|
+
tool: invocation.tool,
|
|
532
|
+
format: wantsAria ? 'aria' : options.format,
|
|
533
|
+
snapshot: limitText(text, options.limit),
|
|
534
|
+
raw: normalizeToolResult(invocation.result),
|
|
535
|
+
};
|
|
536
|
+
}
|
|
537
|
+
async screenshot(options) {
|
|
538
|
+
const invocation = await this.callTool('screenshot', [
|
|
539
|
+
{
|
|
540
|
+
names: ['take_screenshot', 'screenshot'],
|
|
541
|
+
buildArgs: (tool) => withScreenshotArgs(tool, options),
|
|
542
|
+
},
|
|
543
|
+
], {});
|
|
544
|
+
const savedPath = maybeWriteBinaryOutput(invocation.result, options.outputPath);
|
|
545
|
+
return {
|
|
546
|
+
ok: true,
|
|
547
|
+
command: 'screenshot',
|
|
548
|
+
profile: this.profile,
|
|
549
|
+
mode: this.mode(),
|
|
550
|
+
ignoredCompatibilityOptions: this.ignoredCompatibilityOptions,
|
|
551
|
+
tool: invocation.tool,
|
|
552
|
+
outputPath: savedPath || options.outputPath,
|
|
553
|
+
raw: normalizeToolResult(invocation.result),
|
|
554
|
+
};
|
|
555
|
+
}
|
|
556
|
+
async pdf(outputPath) {
|
|
557
|
+
const invocation = await this.callTool('pdf', [{ names: ['pdf', 'generate_pdf'] }], {});
|
|
558
|
+
const savedPath = maybeWriteBinaryOutput(invocation.result, outputPath);
|
|
559
|
+
return {
|
|
560
|
+
ok: true,
|
|
561
|
+
command: 'pdf',
|
|
562
|
+
profile: this.profile,
|
|
563
|
+
mode: this.mode(),
|
|
564
|
+
ignoredCompatibilityOptions: this.ignoredCompatibilityOptions,
|
|
565
|
+
tool: invocation.tool,
|
|
566
|
+
outputPath: savedPath || outputPath,
|
|
567
|
+
raw: normalizeToolResult(invocation.result),
|
|
568
|
+
};
|
|
569
|
+
}
|
|
570
|
+
async consoleMessages(options) {
|
|
571
|
+
const invocation = await this.callTool('console', [
|
|
572
|
+
{
|
|
573
|
+
names: ['list_console_messages'],
|
|
574
|
+
buildArgs: (tool) => withConsoleArgs(tool, options),
|
|
575
|
+
},
|
|
576
|
+
], {});
|
|
577
|
+
return {
|
|
578
|
+
ok: true,
|
|
579
|
+
command: options.level === 'error' ? 'errors' : 'console',
|
|
580
|
+
profile: this.profile,
|
|
581
|
+
mode: this.mode(),
|
|
582
|
+
ignoredCompatibilityOptions: this.ignoredCompatibilityOptions,
|
|
583
|
+
tool: invocation.tool,
|
|
584
|
+
messages: extractMessages(invocation.result),
|
|
585
|
+
raw: normalizeToolResult(invocation.result),
|
|
586
|
+
};
|
|
587
|
+
}
|
|
588
|
+
async requests(options) {
|
|
589
|
+
const invocation = await this.callTool('requests', [
|
|
590
|
+
{
|
|
591
|
+
names: ['list_network_requests'],
|
|
592
|
+
buildArgs: (tool) => withRequestListArgs(tool, options),
|
|
593
|
+
},
|
|
594
|
+
], {});
|
|
595
|
+
let requests = extractRequests(invocation.result);
|
|
596
|
+
if (options.filter) {
|
|
597
|
+
const needle = options.filter.toLowerCase();
|
|
598
|
+
requests = requests.filter((request) => JSON.stringify(request).toLowerCase().includes(needle));
|
|
599
|
+
}
|
|
600
|
+
if (options.limit) {
|
|
601
|
+
requests = requests.slice(0, options.limit);
|
|
602
|
+
}
|
|
603
|
+
return {
|
|
604
|
+
ok: true,
|
|
605
|
+
command: 'requests',
|
|
606
|
+
profile: this.profile,
|
|
607
|
+
mode: this.mode(),
|
|
608
|
+
ignoredCompatibilityOptions: this.ignoredCompatibilityOptions,
|
|
609
|
+
tool: invocation.tool,
|
|
610
|
+
requests,
|
|
611
|
+
raw: normalizeToolResult(invocation.result),
|
|
612
|
+
};
|
|
613
|
+
}
|
|
614
|
+
async responseBody(options) {
|
|
615
|
+
const requests = await this.requests({ filter: options.pattern, limit: 1 });
|
|
616
|
+
const list = Array.isArray(requests.requests) ? requests.requests : [];
|
|
617
|
+
const first = list[0];
|
|
618
|
+
const requestId = first?.requestId ?? guessRequestId(requests.raw);
|
|
619
|
+
if (!requestId) {
|
|
620
|
+
throw new Error('No matching network request found');
|
|
621
|
+
}
|
|
622
|
+
const invocation = await this.callTool('responsebody', [
|
|
623
|
+
{
|
|
624
|
+
names: ['get_network_request'],
|
|
625
|
+
buildArgs: (tool) => withRequestDetailsArgs(tool, requestId),
|
|
626
|
+
},
|
|
627
|
+
], {});
|
|
628
|
+
const body = limitText(extractResponseBody(invocation.result), options.maxChars);
|
|
629
|
+
return {
|
|
630
|
+
ok: true,
|
|
631
|
+
command: 'responsebody',
|
|
632
|
+
profile: this.profile,
|
|
633
|
+
mode: this.mode(),
|
|
634
|
+
ignoredCompatibilityOptions: this.ignoredCompatibilityOptions,
|
|
635
|
+
requestId,
|
|
636
|
+
tool: invocation.tool,
|
|
637
|
+
responseBody: body,
|
|
638
|
+
raw: normalizeToolResult(invocation.result),
|
|
639
|
+
};
|
|
640
|
+
}
|
|
641
|
+
async resize(width, height) {
|
|
642
|
+
const invocation = await this.callTool('resize', [
|
|
643
|
+
{
|
|
644
|
+
names: ['resize_page'],
|
|
645
|
+
buildArgs: (tool) => withResizeArgs(tool, width, height),
|
|
646
|
+
},
|
|
647
|
+
], {});
|
|
648
|
+
return this.outputFromInvocation('resize', invocation);
|
|
649
|
+
}
|
|
650
|
+
async click(ref, options) {
|
|
651
|
+
const invocation = await this.callTool('click', [
|
|
652
|
+
{
|
|
653
|
+
names: ['click'],
|
|
654
|
+
buildArgs: (tool) => withRefArgs(tool, ref, { dblClick: options.double }),
|
|
655
|
+
},
|
|
656
|
+
], {});
|
|
657
|
+
return this.outputFromInvocation('click', invocation);
|
|
658
|
+
}
|
|
659
|
+
async type(ref, text, options) {
|
|
660
|
+
const invocation = await this.callTool('type', [
|
|
661
|
+
{
|
|
662
|
+
names: ['fill'],
|
|
663
|
+
buildArgs: (tool) => withFillArgs(tool, ref, text),
|
|
664
|
+
},
|
|
665
|
+
{
|
|
666
|
+
names: ['type_text', 'type'],
|
|
667
|
+
buildArgs: (tool) => withTypeArgs(tool, text, options.submit ? 'Enter' : undefined),
|
|
668
|
+
},
|
|
669
|
+
], {});
|
|
670
|
+
return this.outputFromInvocation('type', invocation);
|
|
671
|
+
}
|
|
672
|
+
async press(keys) {
|
|
673
|
+
const invocation = await this.callTool('press', [
|
|
674
|
+
{
|
|
675
|
+
names: ['press_key', 'keyboard_shortcut'],
|
|
676
|
+
buildArgs: (tool) => withKeysArgs(tool, keys),
|
|
677
|
+
},
|
|
678
|
+
], {});
|
|
679
|
+
return this.outputFromInvocation('press', invocation);
|
|
680
|
+
}
|
|
681
|
+
async hover(ref) {
|
|
682
|
+
const invocation = await this.callTool('hover', [
|
|
683
|
+
{
|
|
684
|
+
names: ['hover'],
|
|
685
|
+
buildArgs: (tool) => withRefArgs(tool, ref),
|
|
686
|
+
},
|
|
687
|
+
], {});
|
|
688
|
+
return this.outputFromInvocation('hover', invocation);
|
|
689
|
+
}
|
|
690
|
+
async scrollIntoView(ref) {
|
|
691
|
+
const invocation = await this.callTool('scrollintoview', [
|
|
692
|
+
{
|
|
693
|
+
names: ['scroll_into_view', 'scrollintoview'],
|
|
694
|
+
buildArgs: (tool) => withRefArgs(tool, ref),
|
|
695
|
+
},
|
|
696
|
+
], {});
|
|
697
|
+
return this.outputFromInvocation('scrollintoview', invocation);
|
|
698
|
+
}
|
|
699
|
+
async drag(source, target) {
|
|
700
|
+
const invocation = await this.callTool('drag', [
|
|
701
|
+
{
|
|
702
|
+
names: ['drag'],
|
|
703
|
+
buildArgs: (tool) => withDragArgs(tool, source, target),
|
|
704
|
+
},
|
|
705
|
+
], {});
|
|
706
|
+
return this.outputFromInvocation('drag', invocation);
|
|
707
|
+
}
|
|
708
|
+
async select(ref, values) {
|
|
709
|
+
const invocation = await this.callTool('select', [
|
|
710
|
+
{
|
|
711
|
+
names: ['fill'],
|
|
712
|
+
buildArgs: (tool) => withFillArgs(tool, ref, values.length === 1 ? values[0] : values),
|
|
713
|
+
},
|
|
714
|
+
{
|
|
715
|
+
names: ['select_option', 'select'],
|
|
716
|
+
buildArgs: (tool) => withSelectArgs(tool, ref, values),
|
|
717
|
+
},
|
|
718
|
+
], {});
|
|
719
|
+
return this.outputFromInvocation('select', invocation);
|
|
720
|
+
}
|
|
721
|
+
async download(ref, filename) {
|
|
722
|
+
const invocation = await this.callTool('download', [
|
|
723
|
+
{
|
|
724
|
+
names: ['download'],
|
|
725
|
+
buildArgs: (tool) => withDownloadArgs(tool, ref, filename),
|
|
726
|
+
},
|
|
727
|
+
{
|
|
728
|
+
names: ['click'],
|
|
729
|
+
buildArgs: (tool) => withRefArgs(tool, ref),
|
|
730
|
+
},
|
|
731
|
+
], {});
|
|
732
|
+
return this.outputFromInvocation('download', invocation);
|
|
733
|
+
}
|
|
734
|
+
async waitForDownload(filename) {
|
|
735
|
+
const invocation = await this.callTool('waitfordownload', [
|
|
736
|
+
{
|
|
737
|
+
names: ['wait_for_download', 'waitfordownload'],
|
|
738
|
+
buildArgs: (tool) => withFilenameArgs(tool, filename),
|
|
739
|
+
},
|
|
740
|
+
], {});
|
|
741
|
+
return this.outputFromInvocation('waitfordownload', invocation);
|
|
742
|
+
}
|
|
743
|
+
async upload(path, ref) {
|
|
744
|
+
const invocation = await this.callTool('upload', [
|
|
745
|
+
{
|
|
746
|
+
names: ['upload_file'],
|
|
747
|
+
buildArgs: (tool) => withUploadArgs(tool, path, ref),
|
|
748
|
+
},
|
|
749
|
+
], {});
|
|
750
|
+
return this.outputFromInvocation('upload', invocation);
|
|
751
|
+
}
|
|
752
|
+
async fillForm(fieldsJson) {
|
|
753
|
+
const fields = parseJsonValue(fieldsJson, '--fields');
|
|
754
|
+
if (!Array.isArray(fields)) {
|
|
755
|
+
throw new Error('--fields must be a JSON array');
|
|
756
|
+
}
|
|
757
|
+
const invocation = await this.callTool('fill', [
|
|
758
|
+
{
|
|
759
|
+
names: ['fill_form'],
|
|
760
|
+
buildArgs: (tool) => withFillFormArgs(tool, fields),
|
|
761
|
+
},
|
|
762
|
+
], {});
|
|
763
|
+
return this.outputFromInvocation('fill', invocation);
|
|
764
|
+
}
|
|
765
|
+
async dialog(options) {
|
|
766
|
+
const action = options.accept ? 'accept' : options.dismiss ? 'dismiss' : 'accept';
|
|
767
|
+
const invocation = await this.callTool('dialog', [
|
|
768
|
+
{
|
|
769
|
+
names: ['handle_dialog'],
|
|
770
|
+
buildArgs: (tool) => withDialogArgs(tool, action, options.prompt),
|
|
771
|
+
},
|
|
772
|
+
], {});
|
|
773
|
+
return this.outputFromInvocation('dialog', invocation);
|
|
774
|
+
}
|
|
775
|
+
async wait(options) {
|
|
776
|
+
if (options.text && options.text.length > 0) {
|
|
777
|
+
const invocation = await this.callTool('wait', [
|
|
778
|
+
{
|
|
779
|
+
names: ['wait_for'],
|
|
780
|
+
buildArgs: (tool) => withWaitArgs(tool, options.text, options.timeoutMs),
|
|
781
|
+
},
|
|
782
|
+
], {});
|
|
783
|
+
return this.outputFromInvocation('wait', invocation);
|
|
784
|
+
}
|
|
785
|
+
if (typeof options.seconds === 'number' && Number.isFinite(options.seconds) && options.seconds >= 0) {
|
|
786
|
+
await delay(Math.round(options.seconds * 1000));
|
|
787
|
+
return {
|
|
788
|
+
ok: true,
|
|
789
|
+
command: 'wait',
|
|
790
|
+
profile: this.profile,
|
|
791
|
+
mode: this.mode(),
|
|
792
|
+
ignoredCompatibilityOptions: this.ignoredCompatibilityOptions,
|
|
793
|
+
waitedSeconds: options.seconds,
|
|
794
|
+
};
|
|
795
|
+
}
|
|
796
|
+
throw new Error('wait requires either --text or --seconds');
|
|
797
|
+
}
|
|
798
|
+
async evaluate(options) {
|
|
799
|
+
const invocation = await this.callTool('evaluate', [
|
|
800
|
+
{
|
|
801
|
+
names: ['evaluate_script'],
|
|
802
|
+
buildArgs: (tool) => withEvaluateArgs(tool, options.fn, options.ref, options.argsJson),
|
|
803
|
+
},
|
|
804
|
+
], {});
|
|
805
|
+
return this.outputFromInvocation('evaluate', invocation);
|
|
806
|
+
}
|
|
807
|
+
async highlight(ref) {
|
|
808
|
+
const invocation = await this.callTool('highlight', [
|
|
809
|
+
{
|
|
810
|
+
names: ['highlight'],
|
|
811
|
+
buildArgs: (tool) => withRefArgs(tool, ref),
|
|
812
|
+
},
|
|
813
|
+
{
|
|
814
|
+
names: ['hover'],
|
|
815
|
+
buildArgs: (tool) => withRefArgs(tool, ref, { duration: 1500 }),
|
|
816
|
+
},
|
|
817
|
+
], {});
|
|
818
|
+
return this.outputFromInvocation('highlight', invocation);
|
|
819
|
+
}
|
|
820
|
+
async traceStart(options) {
|
|
821
|
+
const invocation = await this.callTool('trace start', [
|
|
822
|
+
{
|
|
823
|
+
names: ['performance_start_trace'],
|
|
824
|
+
buildArgs: (tool) => withTraceStartArgs(tool, options),
|
|
825
|
+
},
|
|
826
|
+
], {});
|
|
827
|
+
return this.outputFromInvocation('trace start', invocation);
|
|
828
|
+
}
|
|
829
|
+
async traceStop(outputPath) {
|
|
830
|
+
const invocation = await this.callTool('trace stop', [
|
|
831
|
+
{
|
|
832
|
+
names: ['performance_stop_trace'],
|
|
833
|
+
buildArgs: (tool) => withTraceStopArgs(tool, outputPath),
|
|
834
|
+
},
|
|
835
|
+
], {});
|
|
836
|
+
return this.outputFromInvocation('trace stop', invocation);
|
|
837
|
+
}
|
|
838
|
+
async callGenericCommand(commandName, candidates, canonicalArgs) {
|
|
839
|
+
const invocation = await this.callTool(commandName, candidates, canonicalArgs);
|
|
840
|
+
return this.outputFromInvocation(commandName, invocation);
|
|
841
|
+
}
|
|
842
|
+
async callTool(commandName, candidates, canonicalArgs) {
|
|
843
|
+
await this.ensureToolsLoaded();
|
|
844
|
+
const available = new Map(this.tools.map((tool) => [normalizeName(tool.name), tool]));
|
|
845
|
+
for (const candidate of candidates) {
|
|
846
|
+
for (const candidateName of candidate.names) {
|
|
847
|
+
const tool = available.get(normalizeName(candidateName));
|
|
848
|
+
if (!tool) {
|
|
849
|
+
continue;
|
|
850
|
+
}
|
|
851
|
+
const args = candidate.buildArgs
|
|
852
|
+
? candidate.buildArgs(tool)
|
|
853
|
+
: withCanonicalArgs(tool, canonicalArgs);
|
|
854
|
+
const result = await this.connection.callTool(tool.name, args);
|
|
855
|
+
return { tool: tool.name, args, result: result };
|
|
856
|
+
}
|
|
857
|
+
}
|
|
858
|
+
const requested = candidates.flatMap((candidate) => candidate.names);
|
|
859
|
+
throw new Error(`No compatible browser tool found for "${commandName}". Tried ${requested.join(', ')}. Available tools: ${this.tools.map((tool) => tool.name).join(', ')}`);
|
|
860
|
+
}
|
|
861
|
+
async ensureToolsLoaded() {
|
|
862
|
+
if (this.toolsLoaded && this.tools.length > 0) {
|
|
863
|
+
return;
|
|
864
|
+
}
|
|
865
|
+
if (this.connection.isExtensionConnected()) {
|
|
866
|
+
try {
|
|
867
|
+
this.tools = await this.connection.refreshTools(this.timeoutMs);
|
|
868
|
+
}
|
|
869
|
+
catch {
|
|
870
|
+
this.tools = await this.connection.waitForToolsUpdate(1_000);
|
|
871
|
+
}
|
|
872
|
+
}
|
|
873
|
+
else {
|
|
874
|
+
this.tools = this.connection.getTools();
|
|
875
|
+
}
|
|
876
|
+
this.toolsLoaded = true;
|
|
877
|
+
}
|
|
878
|
+
outputFromInvocation(commandName, invocation) {
|
|
879
|
+
return {
|
|
880
|
+
ok: !invocation.result.isError,
|
|
881
|
+
command: commandName,
|
|
882
|
+
profile: this.profile,
|
|
883
|
+
mode: this.mode(),
|
|
884
|
+
ignoredCompatibilityOptions: this.ignoredCompatibilityOptions,
|
|
885
|
+
tool: invocation.tool,
|
|
886
|
+
raw: normalizeToolResult(invocation.result),
|
|
887
|
+
};
|
|
888
|
+
}
|
|
889
|
+
mode() {
|
|
890
|
+
return this.remoteUuid ? 'remote' : 'local';
|
|
891
|
+
}
|
|
892
|
+
}
|
|
893
|
+
function emitOutput(asJson, data, humanOutput) {
|
|
894
|
+
if (asJson) {
|
|
895
|
+
console.log(JSON.stringify(data, null, 2));
|
|
896
|
+
return;
|
|
897
|
+
}
|
|
898
|
+
console.log(humanOutput);
|
|
899
|
+
}
|
|
900
|
+
function emitError(asJson, commandName, ctx, error) {
|
|
901
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
902
|
+
if (asJson) {
|
|
903
|
+
console.log(JSON.stringify({
|
|
904
|
+
ok: false,
|
|
905
|
+
command: commandName,
|
|
906
|
+
profile: DEFAULT_BROWSER_PROFILE,
|
|
907
|
+
mode: 'local',
|
|
908
|
+
error: message,
|
|
909
|
+
}, null, 2));
|
|
910
|
+
return;
|
|
911
|
+
}
|
|
912
|
+
console.error(`Error: ${message}`);
|
|
913
|
+
}
|
|
914
|
+
function formatHumanOutput(commandName, output) {
|
|
915
|
+
switch (commandName) {
|
|
916
|
+
case 'status':
|
|
917
|
+
case 'start':
|
|
918
|
+
case 'stop':
|
|
919
|
+
return [
|
|
920
|
+
`Profile: ${String(output.profile)}`,
|
|
921
|
+
`Mode: ${String(output.mode)}`,
|
|
922
|
+
`Relay connected: ${boolText(output.relayConnected)}`,
|
|
923
|
+
`Extension connected: ${boolText(output.extensionConnected)}`,
|
|
924
|
+
`Managed lifecycle: ${boolText(output.managedLifecycle)}`,
|
|
925
|
+
output.toolCount !== undefined ? `Tools: ${String(output.toolCount)}` : null,
|
|
926
|
+
output.note ? String(output.note) : null,
|
|
927
|
+
].filter(Boolean).join('\n');
|
|
928
|
+
case 'tabs':
|
|
929
|
+
case 'tab': {
|
|
930
|
+
const pages = Array.isArray(output.pages) ? output.pages : [];
|
|
931
|
+
if (pages.length === 0) {
|
|
932
|
+
return firstDefinedText(output.raw, 'No tabs reported by browser');
|
|
933
|
+
}
|
|
934
|
+
return pages.map((page, index) => `${index + 1}. ${page.active ? '[active] ' : ''}${page.title || '(untitled)'}${page.url ? ` — ${page.url}` : ''}${page.id !== undefined ? ` [id=${page.id}]` : ''}`).join('\n');
|
|
935
|
+
}
|
|
936
|
+
case 'snapshot':
|
|
937
|
+
return [
|
|
938
|
+
output.title ? `Title: ${String(output.title)}` : null,
|
|
939
|
+
output.url ? `URL: ${String(output.url)}` : null,
|
|
940
|
+
output.snapshot ? String(output.snapshot) : firstDefinedText(output.raw, ''),
|
|
941
|
+
].filter(Boolean).join('\n');
|
|
942
|
+
case 'requests': {
|
|
943
|
+
const requests = Array.isArray(output.requests) ? output.requests : [];
|
|
944
|
+
if (requests.length === 0) {
|
|
945
|
+
return firstDefinedText(output.raw, 'No requests reported by browser');
|
|
946
|
+
}
|
|
947
|
+
return requests.map((request, index) => `${index + 1}. ${request.method || 'GET'} ${request.url || '(unknown)'}${request.status !== undefined ? ` [${request.status}]` : ''}${request.requestId ? ` [id=${request.requestId}]` : ''}`).join('\n');
|
|
948
|
+
}
|
|
949
|
+
case 'console':
|
|
950
|
+
case 'errors': {
|
|
951
|
+
const messages = Array.isArray(output.messages) ? output.messages : [];
|
|
952
|
+
if (messages.length > 0) {
|
|
953
|
+
return messages.map((message) => stringifyJson(message)).join('\n');
|
|
954
|
+
}
|
|
955
|
+
return firstDefinedText(output.raw, 'No console messages reported by browser');
|
|
956
|
+
}
|
|
957
|
+
case 'responsebody':
|
|
958
|
+
return String(output.responseBody ?? firstDefinedText(output.raw, ''));
|
|
959
|
+
default:
|
|
960
|
+
if (output.outputPath) {
|
|
961
|
+
return `Saved to ${String(output.outputPath)}`;
|
|
962
|
+
}
|
|
963
|
+
return firstDefinedText(output.raw, JSON.stringify(output, null, 2));
|
|
964
|
+
}
|
|
965
|
+
}
|
|
966
|
+
function boolText(value) {
|
|
967
|
+
return value ? 'yes' : 'no';
|
|
968
|
+
}
|
|
969
|
+
function firstDefinedText(value, fallback) {
|
|
970
|
+
if (typeof value === 'string' && value.length > 0) {
|
|
971
|
+
return value;
|
|
972
|
+
}
|
|
973
|
+
if (value && typeof value === 'object' && 'text' in value && typeof value.text === 'string') {
|
|
974
|
+
return value.text;
|
|
975
|
+
}
|
|
976
|
+
return fallback;
|
|
977
|
+
}
|
|
978
|
+
function normalizeToolResult(result) {
|
|
979
|
+
return sanitizeUnknown(result) ?? null;
|
|
980
|
+
}
|
|
981
|
+
function sanitizeUnknown(value) {
|
|
982
|
+
if (value === null)
|
|
983
|
+
return null;
|
|
984
|
+
if (typeof value === 'boolean' || typeof value === 'number' || typeof value === 'string')
|
|
985
|
+
return value;
|
|
986
|
+
if (Array.isArray(value))
|
|
987
|
+
return value.map((item) => sanitizeUnknown(item) ?? null);
|
|
988
|
+
if (value && typeof value === 'object') {
|
|
989
|
+
const output = {};
|
|
990
|
+
for (const [key, entry] of Object.entries(value)) {
|
|
991
|
+
const sanitized = sanitizeUnknown(entry);
|
|
992
|
+
if (sanitized !== undefined) {
|
|
993
|
+
output[key] = sanitized;
|
|
994
|
+
}
|
|
995
|
+
}
|
|
996
|
+
return output;
|
|
997
|
+
}
|
|
998
|
+
return undefined;
|
|
999
|
+
}
|
|
1000
|
+
function maybeWriteBinaryOutput(result, outputPath) {
|
|
1001
|
+
if (!outputPath) {
|
|
1002
|
+
return undefined;
|
|
1003
|
+
}
|
|
1004
|
+
const image = result.content?.find((item) => item.type === 'image');
|
|
1005
|
+
if (!image || !('data' in image) || typeof image.data !== 'string') {
|
|
1006
|
+
return undefined;
|
|
1007
|
+
}
|
|
1008
|
+
const target = resolve(outputPath);
|
|
1009
|
+
writeFileSync(target, Buffer.from(image.data, 'base64'));
|
|
1010
|
+
return target;
|
|
1011
|
+
}
|
|
1012
|
+
function extractPages(result) {
|
|
1013
|
+
const direct = extractStructured(result, ['pages', 'tabs', 'targets']);
|
|
1014
|
+
if (Array.isArray(direct)) {
|
|
1015
|
+
return direct.map((entry) => normalizePage(entry));
|
|
1016
|
+
}
|
|
1017
|
+
const parsed = parseResultText(result);
|
|
1018
|
+
if (Array.isArray(parsed)) {
|
|
1019
|
+
return parsed.map((entry) => normalizePage(entry));
|
|
1020
|
+
}
|
|
1021
|
+
if (parsed && typeof parsed === 'object') {
|
|
1022
|
+
for (const key of ['pages', 'tabs', 'targets']) {
|
|
1023
|
+
const candidate = parsed[key];
|
|
1024
|
+
if (Array.isArray(candidate)) {
|
|
1025
|
+
return candidate.map((entry) => normalizePage(entry));
|
|
1026
|
+
}
|
|
1027
|
+
}
|
|
1028
|
+
}
|
|
1029
|
+
return [];
|
|
1030
|
+
}
|
|
1031
|
+
function normalizePage(value) {
|
|
1032
|
+
if (!value || typeof value !== 'object') {
|
|
1033
|
+
return {};
|
|
1034
|
+
}
|
|
1035
|
+
const record = value;
|
|
1036
|
+
return {
|
|
1037
|
+
...sanitizeUnknown(record),
|
|
1038
|
+
id: firstDefined(record, ['pageId', 'tabId', 'id', 'targetId']),
|
|
1039
|
+
title: firstString(record, ['title', 'name']),
|
|
1040
|
+
url: firstString(record, ['url']),
|
|
1041
|
+
active: firstBoolean(record, ['active', 'selected', 'focused']),
|
|
1042
|
+
};
|
|
1043
|
+
}
|
|
1044
|
+
function extractRequests(result) {
|
|
1045
|
+
const direct = extractStructured(result, ['requests']);
|
|
1046
|
+
if (Array.isArray(direct)) {
|
|
1047
|
+
return direct.map((entry) => normalizeRequest(entry));
|
|
1048
|
+
}
|
|
1049
|
+
const parsed = parseResultText(result);
|
|
1050
|
+
if (Array.isArray(parsed)) {
|
|
1051
|
+
return parsed.map((entry) => normalizeRequest(entry));
|
|
1052
|
+
}
|
|
1053
|
+
if (parsed && typeof parsed === 'object') {
|
|
1054
|
+
const requests = parsed.requests;
|
|
1055
|
+
if (Array.isArray(requests)) {
|
|
1056
|
+
return requests.map((entry) => normalizeRequest(entry));
|
|
1057
|
+
}
|
|
1058
|
+
}
|
|
1059
|
+
return [];
|
|
1060
|
+
}
|
|
1061
|
+
function normalizeRequest(value) {
|
|
1062
|
+
if (!value || typeof value !== 'object') {
|
|
1063
|
+
return {};
|
|
1064
|
+
}
|
|
1065
|
+
const record = value;
|
|
1066
|
+
return {
|
|
1067
|
+
...sanitizeUnknown(record),
|
|
1068
|
+
requestId: firstString(record, ['requestId', 'reqid', 'id']),
|
|
1069
|
+
url: firstString(record, ['url']),
|
|
1070
|
+
method: firstString(record, ['method']),
|
|
1071
|
+
status: firstNumber(record, ['status']),
|
|
1072
|
+
};
|
|
1073
|
+
}
|
|
1074
|
+
function extractMessages(result) {
|
|
1075
|
+
const direct = extractStructured(result, ['messages', 'consoleMessages']);
|
|
1076
|
+
if (Array.isArray(direct)) {
|
|
1077
|
+
return direct.map((entry) => sanitizeUnknown(entry) ?? null);
|
|
1078
|
+
}
|
|
1079
|
+
const parsed = parseResultText(result);
|
|
1080
|
+
if (Array.isArray(parsed)) {
|
|
1081
|
+
return parsed.map((entry) => sanitizeUnknown(entry) ?? null);
|
|
1082
|
+
}
|
|
1083
|
+
if (parsed && typeof parsed === 'object') {
|
|
1084
|
+
for (const key of ['messages', 'consoleMessages']) {
|
|
1085
|
+
const candidate = parsed[key];
|
|
1086
|
+
if (Array.isArray(candidate)) {
|
|
1087
|
+
return candidate.map((entry) => sanitizeUnknown(entry) ?? null);
|
|
1088
|
+
}
|
|
1089
|
+
}
|
|
1090
|
+
}
|
|
1091
|
+
const text = firstText(result);
|
|
1092
|
+
return text ? [text] : [];
|
|
1093
|
+
}
|
|
1094
|
+
function extractResponseBody(result) {
|
|
1095
|
+
const direct = extractStructured(result, ['responseBody', 'body']);
|
|
1096
|
+
if (typeof direct === 'string') {
|
|
1097
|
+
return direct;
|
|
1098
|
+
}
|
|
1099
|
+
const parsed = parseResultText(result);
|
|
1100
|
+
if (parsed && typeof parsed === 'object') {
|
|
1101
|
+
for (const key of ['responseBody', 'body']) {
|
|
1102
|
+
const candidate = parsed[key];
|
|
1103
|
+
if (typeof candidate === 'string') {
|
|
1104
|
+
return candidate;
|
|
1105
|
+
}
|
|
1106
|
+
}
|
|
1107
|
+
}
|
|
1108
|
+
return firstText(result);
|
|
1109
|
+
}
|
|
1110
|
+
function guessRequestId(value) {
|
|
1111
|
+
if (!value || typeof value !== 'object') {
|
|
1112
|
+
return undefined;
|
|
1113
|
+
}
|
|
1114
|
+
const record = value;
|
|
1115
|
+
if (typeof record.requestId === 'string') {
|
|
1116
|
+
return record.requestId;
|
|
1117
|
+
}
|
|
1118
|
+
return undefined;
|
|
1119
|
+
}
|
|
1120
|
+
function extractStructured(result, keys) {
|
|
1121
|
+
for (const key of keys) {
|
|
1122
|
+
if (key in result) {
|
|
1123
|
+
return result[key];
|
|
1124
|
+
}
|
|
1125
|
+
}
|
|
1126
|
+
return undefined;
|
|
1127
|
+
}
|
|
1128
|
+
function parseResultText(result) {
|
|
1129
|
+
const text = firstText(result);
|
|
1130
|
+
if (!text) {
|
|
1131
|
+
return undefined;
|
|
1132
|
+
}
|
|
1133
|
+
return parseJsonText(text);
|
|
1134
|
+
}
|
|
1135
|
+
function firstText(result) {
|
|
1136
|
+
const item = result.content?.find((entry) => entry.type === 'text');
|
|
1137
|
+
return item && 'text' in item && typeof item.text === 'string' ? item.text : '';
|
|
1138
|
+
}
|
|
1139
|
+
function parseJsonText(text) {
|
|
1140
|
+
const trimmed = text.trim();
|
|
1141
|
+
if (!trimmed) {
|
|
1142
|
+
return undefined;
|
|
1143
|
+
}
|
|
1144
|
+
const candidates = [trimmed];
|
|
1145
|
+
if (trimmed.startsWith('```')) {
|
|
1146
|
+
const fenced = trimmed
|
|
1147
|
+
.replace(/^```[a-zA-Z0-9_-]*\n/, '')
|
|
1148
|
+
.replace(/\n```$/, '')
|
|
1149
|
+
.trim();
|
|
1150
|
+
candidates.push(fenced);
|
|
1151
|
+
}
|
|
1152
|
+
for (const candidate of candidates) {
|
|
1153
|
+
try {
|
|
1154
|
+
return JSON.parse(candidate);
|
|
1155
|
+
}
|
|
1156
|
+
catch {
|
|
1157
|
+
// Try next candidate.
|
|
1158
|
+
}
|
|
1159
|
+
}
|
|
1160
|
+
return undefined;
|
|
1161
|
+
}
|
|
1162
|
+
function parseJsonValue(text, label) {
|
|
1163
|
+
try {
|
|
1164
|
+
return JSON.parse(text);
|
|
1165
|
+
}
|
|
1166
|
+
catch (error) {
|
|
1167
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
1168
|
+
throw new Error(`${label} must be valid JSON: ${message}`);
|
|
1169
|
+
}
|
|
1170
|
+
}
|
|
1171
|
+
function stringifyJson(value) {
|
|
1172
|
+
return typeof value === 'string' ? value : JSON.stringify(value, null, 2);
|
|
1173
|
+
}
|
|
1174
|
+
function limitText(text, limit) {
|
|
1175
|
+
if (!limit || limit <= 0) {
|
|
1176
|
+
return text;
|
|
1177
|
+
}
|
|
1178
|
+
const lines = text.split('\n');
|
|
1179
|
+
return lines.length <= limit ? text : `${lines.slice(0, limit).join('\n')}\n...`;
|
|
1180
|
+
}
|
|
1181
|
+
function parsePositiveInteger(value, label) {
|
|
1182
|
+
const parsed = Number.parseInt(value, 10);
|
|
1183
|
+
if (!Number.isFinite(parsed) || parsed <= 0) {
|
|
1184
|
+
throw new Error(`${label} must be a positive integer`);
|
|
1185
|
+
}
|
|
1186
|
+
return parsed;
|
|
1187
|
+
}
|
|
1188
|
+
function normalizeName(value) {
|
|
1189
|
+
return value.replace(/[-\s]/g, '_').toLowerCase();
|
|
1190
|
+
}
|
|
1191
|
+
function toolProperties(tool) {
|
|
1192
|
+
return tool.inputSchema.properties ?? {};
|
|
1193
|
+
}
|
|
1194
|
+
function hasProperty(tool, ...names) {
|
|
1195
|
+
const properties = toolProperties(tool);
|
|
1196
|
+
return names.find((name) => Object.prototype.hasOwnProperty.call(properties, name));
|
|
1197
|
+
}
|
|
1198
|
+
function withCanonicalArgs(tool, canonicalArgs) {
|
|
1199
|
+
const output = {};
|
|
1200
|
+
for (const [key, value] of Object.entries(canonicalArgs)) {
|
|
1201
|
+
if (value === undefined) {
|
|
1202
|
+
continue;
|
|
1203
|
+
}
|
|
1204
|
+
if (Object.prototype.hasOwnProperty.call(toolProperties(tool), key)) {
|
|
1205
|
+
output[key] = value;
|
|
1206
|
+
}
|
|
1207
|
+
}
|
|
1208
|
+
return output;
|
|
1209
|
+
}
|
|
1210
|
+
function withUrlArgs(tool, url) {
|
|
1211
|
+
const args = {};
|
|
1212
|
+
const urlKey = hasProperty(tool, 'url');
|
|
1213
|
+
if (urlKey)
|
|
1214
|
+
args[urlKey] = url;
|
|
1215
|
+
const typeKey = hasProperty(tool, 'type');
|
|
1216
|
+
if (typeKey && normalizeName(tool.name) === 'navigate_page') {
|
|
1217
|
+
args[typeKey] = 'url';
|
|
1218
|
+
}
|
|
1219
|
+
return args;
|
|
1220
|
+
}
|
|
1221
|
+
function withNavigateArgs(tool, url) {
|
|
1222
|
+
const args = withUrlArgs(tool, url);
|
|
1223
|
+
const pageIdKey = hasProperty(tool, 'pageId', 'tabId');
|
|
1224
|
+
if (pageIdKey && !(pageIdKey in args)) {
|
|
1225
|
+
args[pageIdKey] = undefined;
|
|
1226
|
+
}
|
|
1227
|
+
return stripUndefined(args);
|
|
1228
|
+
}
|
|
1229
|
+
function withPageArgs(tool, id) {
|
|
1230
|
+
const args = {};
|
|
1231
|
+
const numeric = Number.parseInt(id, 10);
|
|
1232
|
+
const key = hasProperty(tool, 'pageId', 'tabId', 'id');
|
|
1233
|
+
if (key) {
|
|
1234
|
+
args[key] = Number.isFinite(numeric) && String(numeric) === id ? numeric : id;
|
|
1235
|
+
}
|
|
1236
|
+
return args;
|
|
1237
|
+
}
|
|
1238
|
+
function withSnapshotArgs(tool, options) {
|
|
1239
|
+
const args = {};
|
|
1240
|
+
maybeAssign(args, tool, 'format', options.format);
|
|
1241
|
+
maybeAssign(args, tool, 'selector', options.selector);
|
|
1242
|
+
maybeAssign(args, tool, 'frame', options.frame);
|
|
1243
|
+
maybeAssign(args, tool, 'compact', options.compact || undefined);
|
|
1244
|
+
maybeAssign(args, tool, 'depth', options.depth);
|
|
1245
|
+
maybeAssign(args, tool, 'efficient', options.efficient || undefined);
|
|
1246
|
+
maybeAssign(args, tool, 'labels', options.labels || undefined);
|
|
1247
|
+
maybeAssign(args, tool, 'a11y', true);
|
|
1248
|
+
maybeAssign(args, tool, 'markdown', false);
|
|
1249
|
+
return args;
|
|
1250
|
+
}
|
|
1251
|
+
function withScreenshotArgs(tool, options) {
|
|
1252
|
+
const args = {};
|
|
1253
|
+
if (options.ref) {
|
|
1254
|
+
Object.assign(args, withRefArgs(tool, options.ref));
|
|
1255
|
+
}
|
|
1256
|
+
maybeAssign(args, tool, 'fullPage', options.fullPage || undefined);
|
|
1257
|
+
maybeAssign(args, tool, 'detail', options.detail);
|
|
1258
|
+
maybeAssign(args, tool, 'grayscale', options.grayscale || undefined);
|
|
1259
|
+
return args;
|
|
1260
|
+
}
|
|
1261
|
+
function withConsoleArgs(tool, options) {
|
|
1262
|
+
const args = {};
|
|
1263
|
+
if (options.level) {
|
|
1264
|
+
if (hasProperty(tool, 'types')) {
|
|
1265
|
+
args.types = [options.level];
|
|
1266
|
+
}
|
|
1267
|
+
else {
|
|
1268
|
+
maybeAssign(args, tool, 'level', options.level);
|
|
1269
|
+
}
|
|
1270
|
+
}
|
|
1271
|
+
if (options.preserve) {
|
|
1272
|
+
maybeAssign(args, tool, 'includePreservedMessages', true);
|
|
1273
|
+
}
|
|
1274
|
+
return args;
|
|
1275
|
+
}
|
|
1276
|
+
function withRequestListArgs(tool, options) {
|
|
1277
|
+
const args = {};
|
|
1278
|
+
if (options.limit) {
|
|
1279
|
+
maybeAssign(args, tool, 'limit', options.limit);
|
|
1280
|
+
maybeAssign(args, tool, 'pageSize', options.limit);
|
|
1281
|
+
}
|
|
1282
|
+
return args;
|
|
1283
|
+
}
|
|
1284
|
+
function withRequestDetailsArgs(tool, requestId) {
|
|
1285
|
+
const args = {};
|
|
1286
|
+
maybeAssign(args, tool, 'requestId', requestId);
|
|
1287
|
+
maybeAssign(args, tool, 'reqid', requestId);
|
|
1288
|
+
return args;
|
|
1289
|
+
}
|
|
1290
|
+
function withResizeArgs(tool, width, height) {
|
|
1291
|
+
const args = {};
|
|
1292
|
+
maybeAssign(args, tool, 'width', width);
|
|
1293
|
+
maybeAssign(args, tool, 'height', height);
|
|
1294
|
+
return args;
|
|
1295
|
+
}
|
|
1296
|
+
function withRefArgs(tool, ref, extra = {}) {
|
|
1297
|
+
const args = { ...extra };
|
|
1298
|
+
const parsedRef = parseRef(ref);
|
|
1299
|
+
if (hasProperty(tool, 'ref')) {
|
|
1300
|
+
args.ref = parsedRef.raw;
|
|
1301
|
+
}
|
|
1302
|
+
if (parsedRef.numeric !== undefined && hasProperty(tool, 'index')) {
|
|
1303
|
+
args.index = parsedRef.numeric;
|
|
1304
|
+
}
|
|
1305
|
+
if (hasProperty(tool, 'uid')) {
|
|
1306
|
+
args.uid = parsedRef.raw;
|
|
1307
|
+
}
|
|
1308
|
+
if (hasProperty(tool, 'selector')) {
|
|
1309
|
+
args.selector = parsedRef.raw;
|
|
1310
|
+
}
|
|
1311
|
+
return args;
|
|
1312
|
+
}
|
|
1313
|
+
function withFillArgs(tool, ref, value) {
|
|
1314
|
+
const args = withRefArgs(tool, ref);
|
|
1315
|
+
if (hasProperty(tool, 'value')) {
|
|
1316
|
+
args.value = Array.isArray(value) ? value[0] : value;
|
|
1317
|
+
}
|
|
1318
|
+
if (hasProperty(tool, 'text')) {
|
|
1319
|
+
args.text = Array.isArray(value) ? value.join(' ') : value;
|
|
1320
|
+
}
|
|
1321
|
+
return args;
|
|
1322
|
+
}
|
|
1323
|
+
function withTypeArgs(tool, text, submitKey) {
|
|
1324
|
+
const args = {};
|
|
1325
|
+
maybeAssign(args, tool, 'text', text);
|
|
1326
|
+
maybeAssign(args, tool, 'value', text);
|
|
1327
|
+
if (submitKey) {
|
|
1328
|
+
maybeAssign(args, tool, 'submitKey', submitKey);
|
|
1329
|
+
}
|
|
1330
|
+
return args;
|
|
1331
|
+
}
|
|
1332
|
+
function withKeysArgs(tool, keys) {
|
|
1333
|
+
const args = {};
|
|
1334
|
+
maybeAssign(args, tool, 'keys', keys);
|
|
1335
|
+
maybeAssign(args, tool, 'shortcut', keys);
|
|
1336
|
+
maybeAssign(args, tool, 'key', keys);
|
|
1337
|
+
return args;
|
|
1338
|
+
}
|
|
1339
|
+
function withDragArgs(tool, source, target) {
|
|
1340
|
+
const args = {};
|
|
1341
|
+
if (hasProperty(tool, 'source')) {
|
|
1342
|
+
args.source = source;
|
|
1343
|
+
}
|
|
1344
|
+
if (hasProperty(tool, 'target')) {
|
|
1345
|
+
args.target = target;
|
|
1346
|
+
}
|
|
1347
|
+
if (hasProperty(tool, 'from_uid')) {
|
|
1348
|
+
args.from_uid = source;
|
|
1349
|
+
}
|
|
1350
|
+
if (hasProperty(tool, 'to_uid')) {
|
|
1351
|
+
args.to_uid = target;
|
|
1352
|
+
}
|
|
1353
|
+
return args;
|
|
1354
|
+
}
|
|
1355
|
+
function withSelectArgs(tool, ref, values) {
|
|
1356
|
+
const args = withRefArgs(tool, ref);
|
|
1357
|
+
maybeAssign(args, tool, 'value', values.length === 1 ? values[0] : values);
|
|
1358
|
+
maybeAssign(args, tool, 'values', values);
|
|
1359
|
+
return args;
|
|
1360
|
+
}
|
|
1361
|
+
function withDownloadArgs(tool, ref, filename) {
|
|
1362
|
+
const args = withRefArgs(tool, ref);
|
|
1363
|
+
maybeAssign(args, tool, 'filename', filename);
|
|
1364
|
+
maybeAssign(args, tool, 'path', filename);
|
|
1365
|
+
return args;
|
|
1366
|
+
}
|
|
1367
|
+
function withFilenameArgs(tool, filename) {
|
|
1368
|
+
const args = {};
|
|
1369
|
+
maybeAssign(args, tool, 'filename', filename);
|
|
1370
|
+
maybeAssign(args, tool, 'path', filename);
|
|
1371
|
+
return args;
|
|
1372
|
+
}
|
|
1373
|
+
function withUploadArgs(tool, path, ref) {
|
|
1374
|
+
const args = {};
|
|
1375
|
+
maybeAssign(args, tool, 'filePath', resolve(path));
|
|
1376
|
+
if (ref) {
|
|
1377
|
+
Object.assign(args, withRefArgs(tool, ref));
|
|
1378
|
+
}
|
|
1379
|
+
return args;
|
|
1380
|
+
}
|
|
1381
|
+
function withFillFormArgs(tool, fields) {
|
|
1382
|
+
const args = {};
|
|
1383
|
+
maybeAssign(args, tool, 'fields', fields);
|
|
1384
|
+
maybeAssign(args, tool, 'elements', fields);
|
|
1385
|
+
return args;
|
|
1386
|
+
}
|
|
1387
|
+
function withDialogArgs(tool, action, prompt) {
|
|
1388
|
+
const args = {};
|
|
1389
|
+
maybeAssign(args, tool, 'action', action);
|
|
1390
|
+
maybeAssign(args, tool, 'promptText', prompt);
|
|
1391
|
+
return args;
|
|
1392
|
+
}
|
|
1393
|
+
function withWaitArgs(tool, text, timeoutMs) {
|
|
1394
|
+
const args = {};
|
|
1395
|
+
maybeAssign(args, tool, 'text', text);
|
|
1396
|
+
maybeAssign(args, tool, 'timeout', timeoutMs);
|
|
1397
|
+
return args;
|
|
1398
|
+
}
|
|
1399
|
+
function withEvaluateArgs(tool, fn, ref, argsJson) {
|
|
1400
|
+
const args = {};
|
|
1401
|
+
maybeAssign(args, tool, 'function', fn);
|
|
1402
|
+
const values = [];
|
|
1403
|
+
if (ref) {
|
|
1404
|
+
values.push(ref);
|
|
1405
|
+
}
|
|
1406
|
+
if (argsJson) {
|
|
1407
|
+
const parsed = parseJsonValue(argsJson, '--args');
|
|
1408
|
+
if (!Array.isArray(parsed)) {
|
|
1409
|
+
throw new Error('--args must be a JSON array');
|
|
1410
|
+
}
|
|
1411
|
+
for (const entry of parsed) {
|
|
1412
|
+
values.push(typeof entry === 'string' ? entry : JSON.stringify(entry));
|
|
1413
|
+
}
|
|
1414
|
+
}
|
|
1415
|
+
if (values.length > 0) {
|
|
1416
|
+
maybeAssign(args, tool, 'args', values);
|
|
1417
|
+
}
|
|
1418
|
+
return args;
|
|
1419
|
+
}
|
|
1420
|
+
function withTraceStartArgs(tool, options) {
|
|
1421
|
+
const args = {};
|
|
1422
|
+
maybeAssign(args, tool, 'reload', options.reload || undefined);
|
|
1423
|
+
if (options.outputPath) {
|
|
1424
|
+
maybeAssign(args, tool, 'filePath', resolve(options.outputPath));
|
|
1425
|
+
}
|
|
1426
|
+
return args;
|
|
1427
|
+
}
|
|
1428
|
+
function withTraceStopArgs(tool, outputPath) {
|
|
1429
|
+
const args = {};
|
|
1430
|
+
if (outputPath) {
|
|
1431
|
+
maybeAssign(args, tool, 'filePath', resolve(outputPath));
|
|
1432
|
+
}
|
|
1433
|
+
return args;
|
|
1434
|
+
}
|
|
1435
|
+
function maybeAssign(target, tool, property, value) {
|
|
1436
|
+
if (value === undefined) {
|
|
1437
|
+
return;
|
|
1438
|
+
}
|
|
1439
|
+
if (hasProperty(tool, property)) {
|
|
1440
|
+
target[property] = value;
|
|
1441
|
+
}
|
|
1442
|
+
}
|
|
1443
|
+
function stripUndefined(value) {
|
|
1444
|
+
return Object.fromEntries(Object.entries(value).filter(([, entry]) => entry !== undefined));
|
|
1445
|
+
}
|
|
1446
|
+
function parseRef(ref) {
|
|
1447
|
+
const numeric = Number.parseInt(ref, 10);
|
|
1448
|
+
return {
|
|
1449
|
+
raw: ref,
|
|
1450
|
+
numeric: Number.isFinite(numeric) && String(numeric) === ref ? numeric : undefined,
|
|
1451
|
+
};
|
|
1452
|
+
}
|
|
1453
|
+
function firstDefined(record, keys) {
|
|
1454
|
+
for (const key of keys) {
|
|
1455
|
+
if (record[key] !== undefined) {
|
|
1456
|
+
return record[key];
|
|
1457
|
+
}
|
|
1458
|
+
}
|
|
1459
|
+
return undefined;
|
|
1460
|
+
}
|
|
1461
|
+
function firstString(record, keys) {
|
|
1462
|
+
const value = firstDefined(record, keys);
|
|
1463
|
+
return typeof value === 'string' ? value : undefined;
|
|
1464
|
+
}
|
|
1465
|
+
function firstBoolean(record, keys) {
|
|
1466
|
+
const value = firstDefined(record, keys);
|
|
1467
|
+
return typeof value === 'boolean' ? value : undefined;
|
|
1468
|
+
}
|
|
1469
|
+
function firstNumber(record, keys) {
|
|
1470
|
+
const value = firstDefined(record, keys);
|
|
1471
|
+
return typeof value === 'number' ? value : undefined;
|
|
1472
|
+
}
|
|
1473
|
+
//# sourceMappingURL=browser-cli.js.map
|