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