browsirai 0.1.0
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/LICENSE +78 -0
- package/README.md +454 -0
- package/dist/bin.js +4929 -0
- package/dist/bin.js.map +1 -0
- package/dist/cli.d.ts +8 -0
- package/dist/cli.js +4925 -0
- package/dist/cli.js.map +1 -0
- package/dist/server.d.ts +17 -0
- package/dist/server.js +4172 -0
- package/dist/server.js.map +1 -0
- package/package.json +61 -0
- package/skills/browsirai/SKILL.md +722 -0
package/dist/cli.js
ADDED
|
@@ -0,0 +1,4925 @@
|
|
|
1
|
+
var __defProp = Object.defineProperty;
|
|
2
|
+
var __getOwnPropNames = Object.getOwnPropertyNames;
|
|
3
|
+
var __esm = (fn, res) => function __init() {
|
|
4
|
+
return fn && (res = (0, fn[__getOwnPropNames(fn)[0]])(fn = 0)), res;
|
|
5
|
+
};
|
|
6
|
+
var __export = (target, all) => {
|
|
7
|
+
for (var name in all)
|
|
8
|
+
__defProp(target, name, { get: all[name], enumerable: true });
|
|
9
|
+
};
|
|
10
|
+
|
|
11
|
+
// src/version.ts
|
|
12
|
+
import { createRequire } from "module";
|
|
13
|
+
var require2, pkg, VERSION;
|
|
14
|
+
var init_version = __esm({
|
|
15
|
+
"src/version.ts"() {
|
|
16
|
+
"use strict";
|
|
17
|
+
require2 = createRequire(import.meta.url);
|
|
18
|
+
pkg = require2("../package.json");
|
|
19
|
+
VERSION = pkg.version;
|
|
20
|
+
}
|
|
21
|
+
});
|
|
22
|
+
|
|
23
|
+
// src/cdp/wait-ready.ts
|
|
24
|
+
var init_wait_ready = __esm({
|
|
25
|
+
"src/cdp/wait-ready.ts"() {
|
|
26
|
+
"use strict";
|
|
27
|
+
}
|
|
28
|
+
});
|
|
29
|
+
|
|
30
|
+
// src/cdp/connection.ts
|
|
31
|
+
var TIMEOUT, DAEMON_CONNECT_RETRIES, DAEMON_CONNECT_DELAY, CDPConnection;
|
|
32
|
+
var init_connection = __esm({
|
|
33
|
+
"src/cdp/connection.ts"() {
|
|
34
|
+
"use strict";
|
|
35
|
+
init_wait_ready();
|
|
36
|
+
TIMEOUT = 15e3;
|
|
37
|
+
DAEMON_CONNECT_RETRIES = 20;
|
|
38
|
+
DAEMON_CONNECT_DELAY = 300;
|
|
39
|
+
CDPConnection = class {
|
|
40
|
+
wsUrl;
|
|
41
|
+
ws = null;
|
|
42
|
+
nextId = 1;
|
|
43
|
+
pending = /* @__PURE__ */ new Map();
|
|
44
|
+
eventHandlers = /* @__PURE__ */ new Map();
|
|
45
|
+
closed = false;
|
|
46
|
+
reconnecting = false;
|
|
47
|
+
_connected = false;
|
|
48
|
+
// Bound listener references for cleanup
|
|
49
|
+
boundOnMessage = null;
|
|
50
|
+
boundOnClose = null;
|
|
51
|
+
boundOnError = null;
|
|
52
|
+
constructor(wsUrl) {
|
|
53
|
+
this.wsUrl = wsUrl;
|
|
54
|
+
}
|
|
55
|
+
// -----------------------------------------------------------------------
|
|
56
|
+
// Public API
|
|
57
|
+
// -----------------------------------------------------------------------
|
|
58
|
+
/** Whether the underlying WebSocket is currently open. */
|
|
59
|
+
get isConnected() {
|
|
60
|
+
return this._connected;
|
|
61
|
+
}
|
|
62
|
+
/**
|
|
63
|
+
* Open the WebSocket connection.
|
|
64
|
+
* Resolves on the `open` event; rejects on `error`.
|
|
65
|
+
*/
|
|
66
|
+
async connect() {
|
|
67
|
+
let WS = globalThis.WebSocket;
|
|
68
|
+
if (!WS) {
|
|
69
|
+
try {
|
|
70
|
+
const wsModule = await import("ws");
|
|
71
|
+
WS = wsModule.default ?? wsModule.WebSocket ?? wsModule;
|
|
72
|
+
} catch {
|
|
73
|
+
throw new Error(
|
|
74
|
+
"No WebSocket implementation found. Install the `ws` package or use Node 22+."
|
|
75
|
+
);
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
const ws = new WS(this.wsUrl);
|
|
79
|
+
this.ws = ws;
|
|
80
|
+
await new Promise((resolve2, reject) => {
|
|
81
|
+
const onOpen = () => {
|
|
82
|
+
ws.removeEventListener("open", onOpen);
|
|
83
|
+
ws.removeEventListener("error", onError);
|
|
84
|
+
resolve2();
|
|
85
|
+
};
|
|
86
|
+
const onError = (ev) => {
|
|
87
|
+
ws.removeEventListener("open", onOpen);
|
|
88
|
+
ws.removeEventListener("error", onError);
|
|
89
|
+
const msg = ev?.message ?? "WebSocket error";
|
|
90
|
+
reject(new Error(msg));
|
|
91
|
+
};
|
|
92
|
+
ws.addEventListener("open", onOpen);
|
|
93
|
+
ws.addEventListener("error", onError);
|
|
94
|
+
});
|
|
95
|
+
this._connected = true;
|
|
96
|
+
this.attachListeners(ws);
|
|
97
|
+
}
|
|
98
|
+
/**
|
|
99
|
+
* Send a CDP command and await its result.
|
|
100
|
+
*
|
|
101
|
+
* @param method CDP method (e.g. `"Runtime.evaluate"`)
|
|
102
|
+
* @param params Optional method parameters
|
|
103
|
+
* @param options Optional timeout / sessionId overrides
|
|
104
|
+
*/
|
|
105
|
+
send(method, params, options) {
|
|
106
|
+
if (this.closed || !this._connected || !this.ws) {
|
|
107
|
+
return Promise.reject(
|
|
108
|
+
new Error(`Connection closed \u2014 cannot send ${method}`)
|
|
109
|
+
);
|
|
110
|
+
}
|
|
111
|
+
const id = this.nextId++;
|
|
112
|
+
const timeout = options?.timeout ?? TIMEOUT;
|
|
113
|
+
const message = { id, method };
|
|
114
|
+
if (params !== void 0) {
|
|
115
|
+
message.params = params;
|
|
116
|
+
}
|
|
117
|
+
if (options?.sessionId !== void 0) {
|
|
118
|
+
message.sessionId = options.sessionId;
|
|
119
|
+
}
|
|
120
|
+
this.ws.send(JSON.stringify(message));
|
|
121
|
+
return new Promise((resolve2, reject) => {
|
|
122
|
+
const timer = setTimeout(() => {
|
|
123
|
+
this.pending.delete(id);
|
|
124
|
+
reject(new Error(`CDP command timeout: ${method} (${timeout}ms)`));
|
|
125
|
+
}, timeout);
|
|
126
|
+
this.pending.set(id, { resolve: resolve2, reject, method, timer });
|
|
127
|
+
});
|
|
128
|
+
}
|
|
129
|
+
/**
|
|
130
|
+
* Register an event handler for a CDP event or lifecycle event.
|
|
131
|
+
*
|
|
132
|
+
* CDP events are dispatched as `handler(params, { method })`.
|
|
133
|
+
* Lifecycle events: `disconnected`, `browserCrashed`, `reconnected`,
|
|
134
|
+
* `reconnectionFailed`.
|
|
135
|
+
*/
|
|
136
|
+
on(event, handler) {
|
|
137
|
+
let handlers = this.eventHandlers.get(event);
|
|
138
|
+
if (!handlers) {
|
|
139
|
+
handlers = /* @__PURE__ */ new Set();
|
|
140
|
+
this.eventHandlers.set(event, handlers);
|
|
141
|
+
}
|
|
142
|
+
handlers.add(handler);
|
|
143
|
+
}
|
|
144
|
+
/** Remove a previously registered event handler. */
|
|
145
|
+
off(event, handler) {
|
|
146
|
+
this.eventHandlers.get(event)?.delete(handler);
|
|
147
|
+
}
|
|
148
|
+
/**
|
|
149
|
+
* Close the connection. Suppresses reconnection.
|
|
150
|
+
* Safe to call multiple times.
|
|
151
|
+
*/
|
|
152
|
+
close() {
|
|
153
|
+
this.closed = true;
|
|
154
|
+
this._connected = false;
|
|
155
|
+
this.rejectAllPending(new Error("Connection closed"));
|
|
156
|
+
if (this.ws) {
|
|
157
|
+
this.detachListeners(this.ws);
|
|
158
|
+
try {
|
|
159
|
+
this.ws.close();
|
|
160
|
+
} catch {
|
|
161
|
+
}
|
|
162
|
+
this.ws = null;
|
|
163
|
+
}
|
|
164
|
+
}
|
|
165
|
+
// -----------------------------------------------------------------------
|
|
166
|
+
// Private
|
|
167
|
+
// -----------------------------------------------------------------------
|
|
168
|
+
/** Attach message / close / error listeners to the WebSocket. */
|
|
169
|
+
attachListeners(ws) {
|
|
170
|
+
this.boundOnMessage = (event) => {
|
|
171
|
+
const data = event?.data;
|
|
172
|
+
if (typeof data !== "string") return;
|
|
173
|
+
let msg;
|
|
174
|
+
try {
|
|
175
|
+
msg = JSON.parse(data);
|
|
176
|
+
} catch {
|
|
177
|
+
return;
|
|
178
|
+
}
|
|
179
|
+
if (msg.id !== void 0) {
|
|
180
|
+
const entry = this.pending.get(msg.id);
|
|
181
|
+
if (entry) {
|
|
182
|
+
this.pending.delete(msg.id);
|
|
183
|
+
clearTimeout(entry.timer);
|
|
184
|
+
if (msg.error) {
|
|
185
|
+
entry.reject(new Error(msg.error.message));
|
|
186
|
+
} else {
|
|
187
|
+
entry.resolve(msg.result);
|
|
188
|
+
}
|
|
189
|
+
}
|
|
190
|
+
return;
|
|
191
|
+
}
|
|
192
|
+
if (msg.method) {
|
|
193
|
+
this.emit(msg.method, msg.params ?? {}, { method: msg.method });
|
|
194
|
+
}
|
|
195
|
+
};
|
|
196
|
+
this.boundOnClose = (event) => {
|
|
197
|
+
const code = event?.code ?? 1006;
|
|
198
|
+
this._connected = false;
|
|
199
|
+
this.rejectAllPending(new Error("WebSocket disconnected \u2014 connection closed"));
|
|
200
|
+
if (code !== 1e3) {
|
|
201
|
+
this.emit("browserCrashed");
|
|
202
|
+
}
|
|
203
|
+
this.emit("disconnected");
|
|
204
|
+
if (!this.closed && code !== 1e3) {
|
|
205
|
+
this.attemptReconnection().catch(() => {
|
|
206
|
+
});
|
|
207
|
+
}
|
|
208
|
+
};
|
|
209
|
+
this.boundOnError = () => {
|
|
210
|
+
};
|
|
211
|
+
ws.addEventListener("message", this.boundOnMessage);
|
|
212
|
+
ws.addEventListener("close", this.boundOnClose);
|
|
213
|
+
ws.addEventListener("error", this.boundOnError);
|
|
214
|
+
}
|
|
215
|
+
/** Detach WebSocket listeners. */
|
|
216
|
+
detachListeners(ws) {
|
|
217
|
+
if (this.boundOnMessage) ws.removeEventListener("message", this.boundOnMessage);
|
|
218
|
+
if (this.boundOnClose) ws.removeEventListener("close", this.boundOnClose);
|
|
219
|
+
if (this.boundOnError) ws.removeEventListener("error", this.boundOnError);
|
|
220
|
+
}
|
|
221
|
+
/** Reject every pending command. */
|
|
222
|
+
rejectAllPending(error) {
|
|
223
|
+
for (const [id, entry] of this.pending) {
|
|
224
|
+
clearTimeout(entry.timer);
|
|
225
|
+
entry.reject(error);
|
|
226
|
+
this.pending.delete(id);
|
|
227
|
+
}
|
|
228
|
+
}
|
|
229
|
+
/** Dispatch an event to all registered handlers. */
|
|
230
|
+
emit(event, ...args) {
|
|
231
|
+
const handlers = this.eventHandlers.get(event);
|
|
232
|
+
if (!handlers) return;
|
|
233
|
+
for (const handler of handlers) {
|
|
234
|
+
try {
|
|
235
|
+
handler(...args);
|
|
236
|
+
} catch {
|
|
237
|
+
}
|
|
238
|
+
}
|
|
239
|
+
}
|
|
240
|
+
/** Attempt reconnection with retries after abnormal close. */
|
|
241
|
+
async attemptReconnection() {
|
|
242
|
+
if (this.reconnecting || this.closed) return;
|
|
243
|
+
this.reconnecting = true;
|
|
244
|
+
for (let attempt = 0; attempt < DAEMON_CONNECT_RETRIES; attempt++) {
|
|
245
|
+
if (this.closed) {
|
|
246
|
+
this.reconnecting = false;
|
|
247
|
+
return;
|
|
248
|
+
}
|
|
249
|
+
await this.delay(DAEMON_CONNECT_DELAY);
|
|
250
|
+
if (this.closed) {
|
|
251
|
+
this.reconnecting = false;
|
|
252
|
+
return;
|
|
253
|
+
}
|
|
254
|
+
try {
|
|
255
|
+
await this.connect();
|
|
256
|
+
this.reconnecting = false;
|
|
257
|
+
this.emit("reconnected");
|
|
258
|
+
return;
|
|
259
|
+
} catch {
|
|
260
|
+
}
|
|
261
|
+
}
|
|
262
|
+
this.reconnecting = false;
|
|
263
|
+
this.emit("reconnectionFailed");
|
|
264
|
+
}
|
|
265
|
+
/** Promise-based delay. */
|
|
266
|
+
delay(ms) {
|
|
267
|
+
return new Promise((resolve2) => setTimeout(resolve2, ms));
|
|
268
|
+
}
|
|
269
|
+
};
|
|
270
|
+
}
|
|
271
|
+
});
|
|
272
|
+
|
|
273
|
+
// src/chrome-launcher.ts
|
|
274
|
+
var chrome_launcher_exports = {};
|
|
275
|
+
__export(chrome_launcher_exports, {
|
|
276
|
+
connectChrome: () => connectChrome,
|
|
277
|
+
findChrome: () => findChrome,
|
|
278
|
+
getCookieSyncState: () => getCookieSyncState,
|
|
279
|
+
getDefaultChromeDataDir: () => getDefaultChromeDataDir,
|
|
280
|
+
isCDPHealthy: () => isCDPHealthy,
|
|
281
|
+
isChromeRunning: () => isChromeRunning,
|
|
282
|
+
isPortReachable: () => isPortReachable,
|
|
283
|
+
launchChromeWithDebugging: () => launchChromeWithDebugging,
|
|
284
|
+
launchHeadlessChrome: () => launchHeadlessChrome,
|
|
285
|
+
needsCookieResync: () => needsCookieResync,
|
|
286
|
+
openChromeInspect: () => openChromeInspect,
|
|
287
|
+
quitChrome: () => quitChrome,
|
|
288
|
+
readDevToolsActivePort: () => readDevToolsActivePort,
|
|
289
|
+
syncCookiesAndTrack: () => syncCookiesAndTrack
|
|
290
|
+
});
|
|
291
|
+
import { execSync, spawn } from "child_process";
|
|
292
|
+
import { existsSync, readFileSync, mkdirSync, copyFileSync, readdirSync, statSync } from "fs";
|
|
293
|
+
import http from "http";
|
|
294
|
+
import { join } from "path";
|
|
295
|
+
import { homedir, tmpdir } from "os";
|
|
296
|
+
import { createConnection } from "net";
|
|
297
|
+
function getDefaultChromeDataDir() {
|
|
298
|
+
const home = homedir();
|
|
299
|
+
switch (process.platform) {
|
|
300
|
+
case "darwin":
|
|
301
|
+
return join(home, "Library", "Application Support", "Google", "Chrome");
|
|
302
|
+
case "win32":
|
|
303
|
+
return join(home, "AppData", "Local", "Google", "Chrome", "User Data");
|
|
304
|
+
default:
|
|
305
|
+
return join(home, ".config", "google-chrome");
|
|
306
|
+
}
|
|
307
|
+
}
|
|
308
|
+
function findChrome() {
|
|
309
|
+
const platform = process.platform;
|
|
310
|
+
const candidates = CHROME_PATHS[platform] ?? [];
|
|
311
|
+
for (const candidate of candidates) {
|
|
312
|
+
if (platform === "darwin" || platform === "win32") {
|
|
313
|
+
if (existsSync(candidate)) return candidate;
|
|
314
|
+
} else {
|
|
315
|
+
try {
|
|
316
|
+
const result = execSync(`which ${candidate}`, { stdio: "pipe" });
|
|
317
|
+
const path = result.toString().trim();
|
|
318
|
+
if (path) return path;
|
|
319
|
+
} catch {
|
|
320
|
+
}
|
|
321
|
+
}
|
|
322
|
+
}
|
|
323
|
+
return null;
|
|
324
|
+
}
|
|
325
|
+
function isPortReachable(port, host = "127.0.0.1") {
|
|
326
|
+
return new Promise((resolve2) => {
|
|
327
|
+
const socket = createConnection({ port, host });
|
|
328
|
+
socket.setTimeout(2e3);
|
|
329
|
+
socket.on("connect", () => {
|
|
330
|
+
socket.end();
|
|
331
|
+
resolve2(true);
|
|
332
|
+
});
|
|
333
|
+
socket.on("error", () => {
|
|
334
|
+
socket.destroy();
|
|
335
|
+
resolve2(false);
|
|
336
|
+
});
|
|
337
|
+
socket.on("timeout", () => {
|
|
338
|
+
socket.destroy();
|
|
339
|
+
resolve2(false);
|
|
340
|
+
});
|
|
341
|
+
});
|
|
342
|
+
}
|
|
343
|
+
function isCDPHealthy(port, host = "127.0.0.1") {
|
|
344
|
+
return new Promise((resolve2) => {
|
|
345
|
+
const req = http.get(`http://${host}:${port}/json/version`, (res) => {
|
|
346
|
+
resolve2(res.statusCode === 200);
|
|
347
|
+
res.resume();
|
|
348
|
+
});
|
|
349
|
+
req.setTimeout(3e3, () => {
|
|
350
|
+
req.destroy();
|
|
351
|
+
resolve2(false);
|
|
352
|
+
});
|
|
353
|
+
req.on("error", () => resolve2(false));
|
|
354
|
+
});
|
|
355
|
+
}
|
|
356
|
+
function readDevToolsActivePort(chromeDataDir) {
|
|
357
|
+
const dataDir = chromeDataDir ?? getDefaultChromeDataDir();
|
|
358
|
+
const portFile = join(dataDir, "DevToolsActivePort");
|
|
359
|
+
if (!existsSync(portFile)) {
|
|
360
|
+
return null;
|
|
361
|
+
}
|
|
362
|
+
try {
|
|
363
|
+
const content = readFileSync(portFile, "utf-8");
|
|
364
|
+
const lines = content.split("\n").map((l) => l.trim()).filter((l) => l.length > 0);
|
|
365
|
+
if (lines.length < 2) return null;
|
|
366
|
+
const port = parseInt(lines[0], 10);
|
|
367
|
+
const wsPath = lines[1];
|
|
368
|
+
if (isNaN(port) || !wsPath.startsWith("/")) return null;
|
|
369
|
+
return {
|
|
370
|
+
port,
|
|
371
|
+
wsPath,
|
|
372
|
+
wsEndpoint: `ws://127.0.0.1:${port}${wsPath}`
|
|
373
|
+
};
|
|
374
|
+
} catch {
|
|
375
|
+
return null;
|
|
376
|
+
}
|
|
377
|
+
}
|
|
378
|
+
function openChromeInspect() {
|
|
379
|
+
const url = "chrome://inspect/#remote-debugging";
|
|
380
|
+
try {
|
|
381
|
+
if (process.platform === "darwin") {
|
|
382
|
+
execSync(`open -a "Google Chrome" "${url}"`, { stdio: "pipe", timeout: 5e3 });
|
|
383
|
+
} else if (process.platform === "win32") {
|
|
384
|
+
execSync(`start chrome "${url}"`, { stdio: "pipe", timeout: 5e3 });
|
|
385
|
+
} else {
|
|
386
|
+
execSync(`google-chrome "${url}" || chromium "${url}"`, { stdio: "pipe", timeout: 5e3 });
|
|
387
|
+
}
|
|
388
|
+
return true;
|
|
389
|
+
} catch {
|
|
390
|
+
return false;
|
|
391
|
+
}
|
|
392
|
+
}
|
|
393
|
+
function isChromeRunning() {
|
|
394
|
+
try {
|
|
395
|
+
if (process.platform === "win32") {
|
|
396
|
+
const r2 = execSync('tasklist /FI "IMAGENAME eq chrome.exe" /NH', { stdio: "pipe" }).toString();
|
|
397
|
+
return r2.includes("chrome.exe");
|
|
398
|
+
}
|
|
399
|
+
const r = execSync("pgrep -x 'Google Chrome' || pgrep -x chrome || pgrep -x chromium", { stdio: "pipe" }).toString().trim();
|
|
400
|
+
return r.length > 0;
|
|
401
|
+
} catch {
|
|
402
|
+
return false;
|
|
403
|
+
}
|
|
404
|
+
}
|
|
405
|
+
async function quitChrome() {
|
|
406
|
+
try {
|
|
407
|
+
if (process.platform === "darwin") {
|
|
408
|
+
execSync(`osascript -e 'tell application "Google Chrome" to quit'`, { stdio: "pipe", timeout: 5e3 });
|
|
409
|
+
} else if (process.platform === "win32") {
|
|
410
|
+
execSync("taskkill /IM chrome.exe", { stdio: "pipe", timeout: 5e3 });
|
|
411
|
+
} else {
|
|
412
|
+
execSync("pkill -TERM chrome || pkill -TERM chromium", { stdio: "pipe", timeout: 5e3 });
|
|
413
|
+
}
|
|
414
|
+
} catch {
|
|
415
|
+
}
|
|
416
|
+
for (let i = 0; i < 15; i++) {
|
|
417
|
+
if (!isChromeRunning()) break;
|
|
418
|
+
await new Promise((r) => setTimeout(r, 200));
|
|
419
|
+
}
|
|
420
|
+
if (isChromeRunning()) {
|
|
421
|
+
try {
|
|
422
|
+
if (process.platform === "win32") {
|
|
423
|
+
execSync("taskkill /F /IM chrome.exe", { stdio: "pipe", timeout: 5e3 });
|
|
424
|
+
} else {
|
|
425
|
+
execSync("pkill -9 'Google Chrome' || pkill -9 chrome || pkill -9 chromium", { stdio: "pipe", timeout: 5e3 });
|
|
426
|
+
}
|
|
427
|
+
} catch {
|
|
428
|
+
}
|
|
429
|
+
for (let i = 0; i < 15; i++) {
|
|
430
|
+
if (!isChromeRunning()) break;
|
|
431
|
+
await new Promise((r) => setTimeout(r, 200));
|
|
432
|
+
}
|
|
433
|
+
}
|
|
434
|
+
await new Promise((r) => setTimeout(r, 500));
|
|
435
|
+
}
|
|
436
|
+
async function launchChromeWithDebugging(port = 9222, headless = false) {
|
|
437
|
+
const healthy = await isCDPHealthy(port);
|
|
438
|
+
if (healthy) {
|
|
439
|
+
const ws = await getWsEndpoint(port);
|
|
440
|
+
return { success: true, port, wsEndpoint: ws };
|
|
441
|
+
}
|
|
442
|
+
const sepHealthy = await isCDPHealthy(SEPARATE_PORT);
|
|
443
|
+
if (sepHealthy) {
|
|
444
|
+
const ws = await getWsEndpoint(SEPARATE_PORT);
|
|
445
|
+
return { success: true, port: SEPARATE_PORT, wsEndpoint: ws };
|
|
446
|
+
}
|
|
447
|
+
const chromePath = findChrome();
|
|
448
|
+
if (!chromePath) {
|
|
449
|
+
return { success: false, port, error: "Chrome not found. Install Chrome and try again." };
|
|
450
|
+
}
|
|
451
|
+
const usesSeparateInstance = isChromeRunning();
|
|
452
|
+
const targetPort = usesSeparateInstance ? SEPARATE_PORT : port;
|
|
453
|
+
const dataDir = usesSeparateInstance ? join(tmpdir(), "browsirai-normal") : void 0;
|
|
454
|
+
if (dataDir) {
|
|
455
|
+
mkdirSync(dataDir, { recursive: true });
|
|
456
|
+
syncCookiesToHeadless(dataDir);
|
|
457
|
+
}
|
|
458
|
+
const args = [
|
|
459
|
+
`--remote-debugging-port=${targetPort}`,
|
|
460
|
+
"--remote-allow-origins=*"
|
|
461
|
+
];
|
|
462
|
+
if (dataDir) {
|
|
463
|
+
args.push(`--user-data-dir=${dataDir}`, "--no-first-run", "--no-default-browser-check", "--disable-extensions");
|
|
464
|
+
}
|
|
465
|
+
if (headless) {
|
|
466
|
+
args.push("--headless=new");
|
|
467
|
+
}
|
|
468
|
+
const child = spawn(chromePath, args, {
|
|
469
|
+
detached: true,
|
|
470
|
+
stdio: "ignore"
|
|
471
|
+
});
|
|
472
|
+
child.unref();
|
|
473
|
+
for (let i = 0; i < 75; i++) {
|
|
474
|
+
await new Promise((r) => setTimeout(r, 200));
|
|
475
|
+
const ok = await isCDPHealthy(targetPort);
|
|
476
|
+
if (ok) {
|
|
477
|
+
const ws = await getWsEndpoint(targetPort);
|
|
478
|
+
return { success: true, port: targetPort, wsEndpoint: ws };
|
|
479
|
+
}
|
|
480
|
+
}
|
|
481
|
+
return {
|
|
482
|
+
success: false,
|
|
483
|
+
port: targetPort,
|
|
484
|
+
error: "Chrome launched but CDP port not reachable after 15s. Check if another Chrome instance is blocking the profile."
|
|
485
|
+
};
|
|
486
|
+
}
|
|
487
|
+
async function getWsEndpoint(port) {
|
|
488
|
+
return new Promise((resolve2) => {
|
|
489
|
+
const req = http.get(`http://127.0.0.1:${port}/json/version`, (res) => {
|
|
490
|
+
let body = "";
|
|
491
|
+
res.on("data", (c) => {
|
|
492
|
+
body += c.toString();
|
|
493
|
+
});
|
|
494
|
+
res.on("end", () => {
|
|
495
|
+
try {
|
|
496
|
+
const data = JSON.parse(body);
|
|
497
|
+
resolve2(data.webSocketDebuggerUrl);
|
|
498
|
+
} catch {
|
|
499
|
+
resolve2(void 0);
|
|
500
|
+
}
|
|
501
|
+
});
|
|
502
|
+
});
|
|
503
|
+
req.setTimeout(3e3, () => {
|
|
504
|
+
req.destroy();
|
|
505
|
+
resolve2(void 0);
|
|
506
|
+
});
|
|
507
|
+
req.on("error", () => resolve2(void 0));
|
|
508
|
+
});
|
|
509
|
+
}
|
|
510
|
+
function getCookieSyncState() {
|
|
511
|
+
return cookieSyncState;
|
|
512
|
+
}
|
|
513
|
+
function needsCookieResync(chromeDataDir) {
|
|
514
|
+
if (!cookieSyncState) return false;
|
|
515
|
+
const dataDir = chromeDataDir ?? getDefaultChromeDataDir();
|
|
516
|
+
const localStatePath = join(dataDir, "Local State");
|
|
517
|
+
if (!existsSync(localStatePath)) return false;
|
|
518
|
+
try {
|
|
519
|
+
const localState = JSON.parse(readFileSync(localStatePath, "utf-8"));
|
|
520
|
+
const profileName = localState.profile?.last_used ?? "Default";
|
|
521
|
+
if (profileName !== cookieSyncState.profileName) return true;
|
|
522
|
+
const cookiePath = join(dataDir, profileName, "Cookies");
|
|
523
|
+
if (!existsSync(cookiePath)) return false;
|
|
524
|
+
const mtime = statSync(cookiePath).mtimeMs;
|
|
525
|
+
return mtime !== cookieSyncState.cookieMtime;
|
|
526
|
+
} catch {
|
|
527
|
+
return false;
|
|
528
|
+
}
|
|
529
|
+
}
|
|
530
|
+
function syncCookiesAndTrack(destDataDir, chromeDataDir) {
|
|
531
|
+
const dataDir = chromeDataDir ?? getDefaultChromeDataDir();
|
|
532
|
+
try {
|
|
533
|
+
const localStatePath = join(dataDir, "Local State");
|
|
534
|
+
if (!existsSync(localStatePath)) return;
|
|
535
|
+
const localState = JSON.parse(readFileSync(localStatePath, "utf-8"));
|
|
536
|
+
const profileName = localState.profile?.last_used ?? "Default";
|
|
537
|
+
const srcProfileDir = join(dataDir, profileName);
|
|
538
|
+
if (!existsSync(join(srcProfileDir, "Cookies"))) return;
|
|
539
|
+
const destProfileDir = join(destDataDir, "Default");
|
|
540
|
+
mkdirSync(destProfileDir, { recursive: true });
|
|
541
|
+
const files = readdirSync(srcProfileDir).filter((f) => f.startsWith("Cookies"));
|
|
542
|
+
for (const file of files) {
|
|
543
|
+
copyFileSync(join(srcProfileDir, file), join(destProfileDir, file));
|
|
544
|
+
}
|
|
545
|
+
const mtime = statSync(join(srcProfileDir, "Cookies")).mtimeMs;
|
|
546
|
+
cookieSyncState = { profileName, cookieMtime: mtime };
|
|
547
|
+
} catch {
|
|
548
|
+
}
|
|
549
|
+
}
|
|
550
|
+
function syncCookiesToHeadless(headlessDataDir) {
|
|
551
|
+
syncCookiesAndTrack(headlessDataDir);
|
|
552
|
+
}
|
|
553
|
+
async function launchHeadlessChrome() {
|
|
554
|
+
const healthy = await isCDPHealthy(HEADLESS_PORT);
|
|
555
|
+
if (healthy) {
|
|
556
|
+
const ws = await getWsEndpoint(HEADLESS_PORT);
|
|
557
|
+
return { success: true, port: HEADLESS_PORT, wsEndpoint: ws };
|
|
558
|
+
}
|
|
559
|
+
const chromePath = findChrome();
|
|
560
|
+
if (!chromePath) {
|
|
561
|
+
return { success: false, port: HEADLESS_PORT, error: "Chrome not found." };
|
|
562
|
+
}
|
|
563
|
+
const dataDir = join(tmpdir(), "browsirai-headless");
|
|
564
|
+
mkdirSync(dataDir, { recursive: true });
|
|
565
|
+
syncCookiesToHeadless(dataDir);
|
|
566
|
+
const child = spawn(chromePath, [
|
|
567
|
+
"--headless=new",
|
|
568
|
+
`--remote-debugging-port=${HEADLESS_PORT}`,
|
|
569
|
+
"--remote-allow-origins=*",
|
|
570
|
+
`--user-data-dir=${dataDir}`,
|
|
571
|
+
"--no-first-run",
|
|
572
|
+
"--no-default-browser-check",
|
|
573
|
+
"--disable-extensions",
|
|
574
|
+
"--disable-gpu"
|
|
575
|
+
], {
|
|
576
|
+
detached: true,
|
|
577
|
+
stdio: "ignore"
|
|
578
|
+
});
|
|
579
|
+
child.unref();
|
|
580
|
+
for (let i = 0; i < 75; i++) {
|
|
581
|
+
await new Promise((r) => setTimeout(r, 200));
|
|
582
|
+
if (await isCDPHealthy(HEADLESS_PORT)) {
|
|
583
|
+
const ws = await getWsEndpoint(HEADLESS_PORT);
|
|
584
|
+
return { success: true, port: HEADLESS_PORT, wsEndpoint: ws };
|
|
585
|
+
}
|
|
586
|
+
}
|
|
587
|
+
return { success: false, port: HEADLESS_PORT, error: "Headless Chrome did not start in 15s." };
|
|
588
|
+
}
|
|
589
|
+
async function connectChrome(options = {}) {
|
|
590
|
+
const targetPort = options.port ?? 9222;
|
|
591
|
+
const activePort = readDevToolsActivePort();
|
|
592
|
+
if (activePort) {
|
|
593
|
+
const healthy2 = await isCDPHealthy(activePort.port);
|
|
594
|
+
if (healthy2) {
|
|
595
|
+
return {
|
|
596
|
+
success: true,
|
|
597
|
+
port: activePort.port,
|
|
598
|
+
wsEndpoint: activePort.wsEndpoint,
|
|
599
|
+
activePortFound: true
|
|
600
|
+
};
|
|
601
|
+
}
|
|
602
|
+
}
|
|
603
|
+
const healthy = await isCDPHealthy(targetPort);
|
|
604
|
+
if (healthy) {
|
|
605
|
+
return {
|
|
606
|
+
success: true,
|
|
607
|
+
port: targetPort,
|
|
608
|
+
activePortFound: false
|
|
609
|
+
};
|
|
610
|
+
}
|
|
611
|
+
if (options.autoLaunch) {
|
|
612
|
+
const launch = await launchChromeWithDebugging(targetPort, options.headless);
|
|
613
|
+
if (launch.success) {
|
|
614
|
+
return {
|
|
615
|
+
success: true,
|
|
616
|
+
port: launch.port,
|
|
617
|
+
wsEndpoint: launch.wsEndpoint,
|
|
618
|
+
activePortFound: false
|
|
619
|
+
};
|
|
620
|
+
}
|
|
621
|
+
return {
|
|
622
|
+
success: false,
|
|
623
|
+
port: targetPort,
|
|
624
|
+
activePortFound: false,
|
|
625
|
+
error: launch.error
|
|
626
|
+
};
|
|
627
|
+
}
|
|
628
|
+
return {
|
|
629
|
+
success: false,
|
|
630
|
+
port: targetPort,
|
|
631
|
+
activePortFound: activePort !== null,
|
|
632
|
+
error: "Chrome remote debugging is not enabled. Enable it at chrome://inspect/#remote-debugging"
|
|
633
|
+
};
|
|
634
|
+
}
|
|
635
|
+
var CHROME_PATHS, SEPARATE_PORT, HEADLESS_PORT, cookieSyncState;
|
|
636
|
+
var init_chrome_launcher = __esm({
|
|
637
|
+
"src/chrome-launcher.ts"() {
|
|
638
|
+
"use strict";
|
|
639
|
+
CHROME_PATHS = {
|
|
640
|
+
darwin: [
|
|
641
|
+
"/Applications/Google Chrome.app/Contents/MacOS/Google Chrome",
|
|
642
|
+
"/Applications/Google Chrome Canary.app/Contents/MacOS/Google Chrome Canary",
|
|
643
|
+
"/Applications/Chromium.app/Contents/MacOS/Chromium",
|
|
644
|
+
"/Applications/Microsoft Edge.app/Contents/MacOS/Microsoft Edge",
|
|
645
|
+
"/Applications/Brave Browser.app/Contents/MacOS/Brave Browser"
|
|
646
|
+
],
|
|
647
|
+
linux: [
|
|
648
|
+
"google-chrome",
|
|
649
|
+
"google-chrome-stable",
|
|
650
|
+
"chromium",
|
|
651
|
+
"chromium-browser",
|
|
652
|
+
"microsoft-edge",
|
|
653
|
+
"brave-browser"
|
|
654
|
+
],
|
|
655
|
+
win32: [
|
|
656
|
+
"C:\\Program Files\\Google\\Chrome\\Application\\chrome.exe",
|
|
657
|
+
"C:\\Program Files (x86)\\Google\\Chrome\\Application\\chrome.exe",
|
|
658
|
+
"C:\\Program Files\\Microsoft\\Edge\\Application\\msedge.exe",
|
|
659
|
+
"C:\\Program Files\\BraveSoftware\\Brave-Browser\\Application\\brave.exe"
|
|
660
|
+
]
|
|
661
|
+
};
|
|
662
|
+
SEPARATE_PORT = 9444;
|
|
663
|
+
HEADLESS_PORT = 9333;
|
|
664
|
+
cookieSyncState = null;
|
|
665
|
+
}
|
|
666
|
+
});
|
|
667
|
+
|
|
668
|
+
// src/tools/browser-navigate.ts
|
|
669
|
+
async function browserNavigate(cdp, params) {
|
|
670
|
+
const { url, timeout = 8 } = params;
|
|
671
|
+
const timeoutMs = timeout * 1e3;
|
|
672
|
+
await cdp.send("Page.enable");
|
|
673
|
+
const result = await Promise.race([
|
|
674
|
+
performNavigation(cdp, url, params.waitUntil),
|
|
675
|
+
createTimeout(timeoutMs)
|
|
676
|
+
]);
|
|
677
|
+
return result;
|
|
678
|
+
}
|
|
679
|
+
async function performNavigation(cdp, url, waitUntil) {
|
|
680
|
+
const navResponse = await cdp.send("Page.navigate", { url });
|
|
681
|
+
if (navResponse.errorText) {
|
|
682
|
+
throw new Error(`Navigation failed: ${navResponse.errorText}`);
|
|
683
|
+
}
|
|
684
|
+
const hasCrossDocNavigation = Boolean(navResponse.loaderId);
|
|
685
|
+
if (hasCrossDocNavigation) {
|
|
686
|
+
await waitForLoadCompletion(cdp, waitUntil);
|
|
687
|
+
}
|
|
688
|
+
return getPageInfo(cdp);
|
|
689
|
+
}
|
|
690
|
+
function waitForLoadCompletion(cdp, waitUntil) {
|
|
691
|
+
const eventName = waitUntil === "domcontentloaded" ? "Page.domContentEventFired" : "Page.loadEventFired";
|
|
692
|
+
return new Promise((resolve2) => {
|
|
693
|
+
let settled = false;
|
|
694
|
+
const handler = () => {
|
|
695
|
+
if (settled) return;
|
|
696
|
+
settled = true;
|
|
697
|
+
cdp.off(eventName, handler);
|
|
698
|
+
resolve2();
|
|
699
|
+
};
|
|
700
|
+
cdp.on(eventName, handler);
|
|
701
|
+
const poll = async () => {
|
|
702
|
+
while (!settled) {
|
|
703
|
+
try {
|
|
704
|
+
const response = await cdp.send("Runtime.evaluate", {
|
|
705
|
+
expression: "document.readyState",
|
|
706
|
+
returnByValue: true
|
|
707
|
+
});
|
|
708
|
+
const readyState = response.result.value;
|
|
709
|
+
const isLoadingState = readyState === "loading" || readyState === "interactive";
|
|
710
|
+
if (readyState === "complete" || !isLoadingState) {
|
|
711
|
+
if (!settled) {
|
|
712
|
+
settled = true;
|
|
713
|
+
cdp.off(eventName, handler);
|
|
714
|
+
resolve2();
|
|
715
|
+
}
|
|
716
|
+
return;
|
|
717
|
+
}
|
|
718
|
+
} catch {
|
|
719
|
+
}
|
|
720
|
+
if (!settled) {
|
|
721
|
+
await delay(POLL_INTERVAL_MS);
|
|
722
|
+
}
|
|
723
|
+
}
|
|
724
|
+
};
|
|
725
|
+
poll();
|
|
726
|
+
});
|
|
727
|
+
}
|
|
728
|
+
async function getPageInfo(cdp) {
|
|
729
|
+
const [titleResponse, urlResponse] = await Promise.all([
|
|
730
|
+
cdp.send("Runtime.evaluate", {
|
|
731
|
+
expression: "document.title"
|
|
732
|
+
}),
|
|
733
|
+
cdp.send("Runtime.evaluate", {
|
|
734
|
+
expression: "location.href"
|
|
735
|
+
})
|
|
736
|
+
]);
|
|
737
|
+
return {
|
|
738
|
+
title: titleResponse.result.value ?? "",
|
|
739
|
+
url: urlResponse.result.value ?? ""
|
|
740
|
+
};
|
|
741
|
+
}
|
|
742
|
+
function createTimeout(ms) {
|
|
743
|
+
return new Promise((_resolve, reject) => {
|
|
744
|
+
setTimeout(() => {
|
|
745
|
+
reject(new Error(`Navigation timeout after ${ms}ms`));
|
|
746
|
+
}, ms);
|
|
747
|
+
});
|
|
748
|
+
}
|
|
749
|
+
function delay(ms) {
|
|
750
|
+
return new Promise((resolve2) => setTimeout(resolve2, ms));
|
|
751
|
+
}
|
|
752
|
+
var POLL_INTERVAL_MS;
|
|
753
|
+
var init_browser_navigate = __esm({
|
|
754
|
+
"src/tools/browser-navigate.ts"() {
|
|
755
|
+
"use strict";
|
|
756
|
+
POLL_INTERVAL_MS = 100;
|
|
757
|
+
}
|
|
758
|
+
});
|
|
759
|
+
|
|
760
|
+
// src/tools/browser-screenshot.ts
|
|
761
|
+
async function detectDPR(cdp) {
|
|
762
|
+
try {
|
|
763
|
+
const metrics = await cdp.send("Page.getLayoutMetrics", {});
|
|
764
|
+
if (metrics.visualViewport && metrics.cssVisualViewport) {
|
|
765
|
+
const physicalWidth = metrics.visualViewport.clientWidth;
|
|
766
|
+
const cssWidth = metrics.cssVisualViewport.clientWidth;
|
|
767
|
+
if (cssWidth > 0 && physicalWidth > 0) {
|
|
768
|
+
const dpr = physicalWidth / cssWidth;
|
|
769
|
+
if (dpr >= 1) {
|
|
770
|
+
return dpr;
|
|
771
|
+
}
|
|
772
|
+
}
|
|
773
|
+
}
|
|
774
|
+
} catch {
|
|
775
|
+
}
|
|
776
|
+
try {
|
|
777
|
+
const evalResult = await cdp.send("Runtime.evaluate", {
|
|
778
|
+
expression: "window.devicePixelRatio",
|
|
779
|
+
returnByValue: true
|
|
780
|
+
});
|
|
781
|
+
if (evalResult.result.type === "number" && typeof evalResult.result.value === "number") {
|
|
782
|
+
return evalResult.result.value;
|
|
783
|
+
}
|
|
784
|
+
} catch {
|
|
785
|
+
}
|
|
786
|
+
return 1;
|
|
787
|
+
}
|
|
788
|
+
async function getElementBoxBySelector(cdp, selector) {
|
|
789
|
+
const doc = await cdp.send("DOM.getDocument", {});
|
|
790
|
+
const queryResult = await cdp.send("DOM.querySelector", {
|
|
791
|
+
nodeId: doc.root.nodeId,
|
|
792
|
+
selector
|
|
793
|
+
});
|
|
794
|
+
if (!queryResult.nodeId) {
|
|
795
|
+
throw new Error(`Element not found: ${selector}`);
|
|
796
|
+
}
|
|
797
|
+
const boxModel = await cdp.send("DOM.getBoxModel", {
|
|
798
|
+
nodeId: queryResult.nodeId
|
|
799
|
+
});
|
|
800
|
+
const content = boxModel.model.content;
|
|
801
|
+
const x = content[0];
|
|
802
|
+
const y = content[1];
|
|
803
|
+
const width = content[2] - content[0];
|
|
804
|
+
const height = content[5] - content[1];
|
|
805
|
+
return { x, y, width, height };
|
|
806
|
+
}
|
|
807
|
+
async function getElementBoxByRef(cdp, ref) {
|
|
808
|
+
const match = /^@e(\d+)$/.exec(ref);
|
|
809
|
+
if (!match) {
|
|
810
|
+
throw new Error(`Invalid ref format: ${ref}`);
|
|
811
|
+
}
|
|
812
|
+
const backendNodeId = parseInt(match[1], 10);
|
|
813
|
+
const resolved = await cdp.send("DOM.resolveNode", {
|
|
814
|
+
backendNodeId
|
|
815
|
+
});
|
|
816
|
+
const boxModel = await cdp.send("DOM.getBoxModel", {
|
|
817
|
+
backendNodeId
|
|
818
|
+
});
|
|
819
|
+
const content = boxModel.model.content;
|
|
820
|
+
const x = content[0];
|
|
821
|
+
const y = content[1];
|
|
822
|
+
const width = content[2] - content[0];
|
|
823
|
+
const height = content[5] - content[1];
|
|
824
|
+
return { x, y, width, height };
|
|
825
|
+
}
|
|
826
|
+
async function buildAnnotations(cdp) {
|
|
827
|
+
const axTree = await cdp.send("Accessibility.getFullAXTree", {}, { timeout: 1e4 });
|
|
828
|
+
const annotations = [];
|
|
829
|
+
let counter = 0;
|
|
830
|
+
for (const node of axTree.nodes) {
|
|
831
|
+
const role = node.role?.value;
|
|
832
|
+
if (role === "WebArea") {
|
|
833
|
+
continue;
|
|
834
|
+
}
|
|
835
|
+
counter++;
|
|
836
|
+
const ref = node.backendDOMNodeId ? `@e${node.backendDOMNodeId}` : `@e${counter}`;
|
|
837
|
+
const label = `[${counter}]`;
|
|
838
|
+
annotations.push({
|
|
839
|
+
ref,
|
|
840
|
+
label,
|
|
841
|
+
role: role ?? "unknown",
|
|
842
|
+
name: node.name?.value ?? ""
|
|
843
|
+
});
|
|
844
|
+
}
|
|
845
|
+
return annotations;
|
|
846
|
+
}
|
|
847
|
+
async function browserScreenshot(cdp, params) {
|
|
848
|
+
const format = params.format ?? "png";
|
|
849
|
+
const captureParams = {
|
|
850
|
+
format
|
|
851
|
+
};
|
|
852
|
+
if (format === "jpeg" && params.quality !== void 0) {
|
|
853
|
+
captureParams.quality = params.quality;
|
|
854
|
+
}
|
|
855
|
+
const _dpr = await detectDPR(cdp);
|
|
856
|
+
if (params.selector) {
|
|
857
|
+
const box = await getElementBoxBySelector(cdp, params.selector);
|
|
858
|
+
captureParams.clip = {
|
|
859
|
+
x: box.x,
|
|
860
|
+
y: box.y,
|
|
861
|
+
width: box.width,
|
|
862
|
+
height: box.height,
|
|
863
|
+
scale: 1
|
|
864
|
+
};
|
|
865
|
+
}
|
|
866
|
+
if (params.ref) {
|
|
867
|
+
const box = await getElementBoxByRef(cdp, params.ref);
|
|
868
|
+
captureParams.clip = {
|
|
869
|
+
x: box.x,
|
|
870
|
+
y: box.y,
|
|
871
|
+
width: box.width,
|
|
872
|
+
height: box.height,
|
|
873
|
+
scale: 1
|
|
874
|
+
};
|
|
875
|
+
}
|
|
876
|
+
if (params.fullPage) {
|
|
877
|
+
const metrics = await cdp.send("Page.getLayoutMetrics", {});
|
|
878
|
+
const contentWidth = metrics.cssContentSize?.width ?? metrics.contentSize.width;
|
|
879
|
+
const contentHeight = metrics.cssContentSize?.height ?? metrics.contentSize.height;
|
|
880
|
+
captureParams.clip = {
|
|
881
|
+
x: 0,
|
|
882
|
+
y: 0,
|
|
883
|
+
width: contentWidth,
|
|
884
|
+
height: contentHeight,
|
|
885
|
+
scale: 1
|
|
886
|
+
};
|
|
887
|
+
}
|
|
888
|
+
const screenshot = await cdp.send("Page.captureScreenshot", captureParams);
|
|
889
|
+
const result = {
|
|
890
|
+
base64: screenshot.data
|
|
891
|
+
};
|
|
892
|
+
if (params.annotate) {
|
|
893
|
+
result.annotations = await buildAnnotations(cdp);
|
|
894
|
+
}
|
|
895
|
+
return result;
|
|
896
|
+
}
|
|
897
|
+
var init_browser_screenshot = __esm({
|
|
898
|
+
"src/tools/browser-screenshot.ts"() {
|
|
899
|
+
"use strict";
|
|
900
|
+
}
|
|
901
|
+
});
|
|
902
|
+
|
|
903
|
+
// src/tools/browser-tabs.ts
|
|
904
|
+
function globToRegExp(pattern) {
|
|
905
|
+
const escaped = pattern.replace(/[.+^${}()|[\]\\]/g, "\\$&");
|
|
906
|
+
const regexStr = escaped.replace(/\*/g, ".*");
|
|
907
|
+
return new RegExp(`^${regexStr}$`, "i");
|
|
908
|
+
}
|
|
909
|
+
async function browserTabs(cdp, params = {}) {
|
|
910
|
+
const response = await cdp.send("Target.getTargets");
|
|
911
|
+
let tabs = response.targetInfos.filter(
|
|
912
|
+
(target) => target.type === "page"
|
|
913
|
+
);
|
|
914
|
+
if (params.filter) {
|
|
915
|
+
const regex = globToRegExp(params.filter);
|
|
916
|
+
tabs = tabs.filter((target) => regex.test(target.url));
|
|
917
|
+
}
|
|
918
|
+
return {
|
|
919
|
+
tabs: tabs.map((target) => ({
|
|
920
|
+
id: target.targetId,
|
|
921
|
+
title: target.title,
|
|
922
|
+
url: target.url
|
|
923
|
+
}))
|
|
924
|
+
};
|
|
925
|
+
}
|
|
926
|
+
var init_browser_tabs = __esm({
|
|
927
|
+
"src/tools/browser-tabs.ts"() {
|
|
928
|
+
"use strict";
|
|
929
|
+
}
|
|
930
|
+
});
|
|
931
|
+
|
|
932
|
+
// src/tools/browser-eval.ts
|
|
933
|
+
function parseRemoteObject(obj) {
|
|
934
|
+
if (obj.type === "undefined") {
|
|
935
|
+
return void 0;
|
|
936
|
+
}
|
|
937
|
+
if (obj.type === "object" && obj.subtype === "null") {
|
|
938
|
+
return null;
|
|
939
|
+
}
|
|
940
|
+
if (obj.type === "object" && obj.subtype === "node") {
|
|
941
|
+
return obj.description ?? `[${obj.className ?? "Node"}]`;
|
|
942
|
+
}
|
|
943
|
+
if (obj.value !== void 0) {
|
|
944
|
+
return obj.value;
|
|
945
|
+
}
|
|
946
|
+
if (obj.description) {
|
|
947
|
+
return obj.description;
|
|
948
|
+
}
|
|
949
|
+
return void 0;
|
|
950
|
+
}
|
|
951
|
+
function formatException(details) {
|
|
952
|
+
if (details.exception?.description) {
|
|
953
|
+
return details.exception.description;
|
|
954
|
+
}
|
|
955
|
+
if (details.exception?.className) {
|
|
956
|
+
return `${details.exception.className}: ${details.text}`;
|
|
957
|
+
}
|
|
958
|
+
return details.text;
|
|
959
|
+
}
|
|
960
|
+
async function browserEval(cdp, params) {
|
|
961
|
+
let expression = params.expression;
|
|
962
|
+
if (params.base64) {
|
|
963
|
+
expression = atob(expression);
|
|
964
|
+
}
|
|
965
|
+
if (params.ref) {
|
|
966
|
+
return evalWithRef(cdp, expression, params.ref);
|
|
967
|
+
}
|
|
968
|
+
return evalGlobal(cdp, expression);
|
|
969
|
+
}
|
|
970
|
+
async function evalGlobal(cdp, expression) {
|
|
971
|
+
const response = await cdp.send("Runtime.evaluate", {
|
|
972
|
+
expression,
|
|
973
|
+
returnByValue: true,
|
|
974
|
+
awaitPromise: true
|
|
975
|
+
});
|
|
976
|
+
if (response.exceptionDetails) {
|
|
977
|
+
return {
|
|
978
|
+
error: formatException(response.exceptionDetails)
|
|
979
|
+
};
|
|
980
|
+
}
|
|
981
|
+
const value = parseRemoteObject(response.result);
|
|
982
|
+
return { result: value };
|
|
983
|
+
}
|
|
984
|
+
async function evalWithRef(cdp, functionDeclaration, ref) {
|
|
985
|
+
const match = /^@e(\d+)$/.exec(ref);
|
|
986
|
+
if (!match) {
|
|
987
|
+
return { error: `Invalid ref format: ${ref}` };
|
|
988
|
+
}
|
|
989
|
+
const backendNodeId = parseInt(match[1], 10);
|
|
990
|
+
const resolved = await cdp.send("DOM.resolveNode", {
|
|
991
|
+
backendNodeId
|
|
992
|
+
});
|
|
993
|
+
const objectId = resolved.object.objectId;
|
|
994
|
+
const response = await cdp.send("Runtime.callFunctionOn", {
|
|
995
|
+
objectId,
|
|
996
|
+
functionDeclaration,
|
|
997
|
+
returnByValue: true,
|
|
998
|
+
awaitPromise: true
|
|
999
|
+
});
|
|
1000
|
+
if (response.exceptionDetails) {
|
|
1001
|
+
return {
|
|
1002
|
+
error: formatException(response.exceptionDetails)
|
|
1003
|
+
};
|
|
1004
|
+
}
|
|
1005
|
+
const value = parseRemoteObject(response.result);
|
|
1006
|
+
return { result: value };
|
|
1007
|
+
}
|
|
1008
|
+
var init_browser_eval = __esm({
|
|
1009
|
+
"src/tools/browser-eval.ts"() {
|
|
1010
|
+
"use strict";
|
|
1011
|
+
}
|
|
1012
|
+
});
|
|
1013
|
+
|
|
1014
|
+
// src/tools/browser-snapshot.ts
|
|
1015
|
+
function shouldShowAxNode(node, options) {
|
|
1016
|
+
const role = node.role?.value ?? "";
|
|
1017
|
+
if (role === "none") {
|
|
1018
|
+
return false;
|
|
1019
|
+
}
|
|
1020
|
+
if (role === "generic") {
|
|
1021
|
+
const name2 = node.name?.value ?? "";
|
|
1022
|
+
if (!name2) {
|
|
1023
|
+
return false;
|
|
1024
|
+
}
|
|
1025
|
+
}
|
|
1026
|
+
if (options?.compact && role === "InlineTextBox") {
|
|
1027
|
+
return false;
|
|
1028
|
+
}
|
|
1029
|
+
const name = node.name?.value ?? "";
|
|
1030
|
+
const value = node.value?.value ?? null;
|
|
1031
|
+
if (!name && (value === null || value === void 0 || value === "")) {
|
|
1032
|
+
return false;
|
|
1033
|
+
}
|
|
1034
|
+
return true;
|
|
1035
|
+
}
|
|
1036
|
+
function getChildNodeIds(node) {
|
|
1037
|
+
if (node.childIds && node.childIds.length > 0) {
|
|
1038
|
+
return node.childIds;
|
|
1039
|
+
}
|
|
1040
|
+
if (node.children && node.children.length > 0) {
|
|
1041
|
+
return node.children.map((c) => c.nodeId);
|
|
1042
|
+
}
|
|
1043
|
+
return [];
|
|
1044
|
+
}
|
|
1045
|
+
function formatNodeAttributes(node) {
|
|
1046
|
+
const parts = [];
|
|
1047
|
+
if (node.value?.value !== void 0 && node.value.value !== "") {
|
|
1048
|
+
parts.push(`value="${node.value.value}"`);
|
|
1049
|
+
}
|
|
1050
|
+
if (node.description?.value) {
|
|
1051
|
+
parts.push(`description="${node.description.value}"`);
|
|
1052
|
+
}
|
|
1053
|
+
if (node.properties) {
|
|
1054
|
+
for (const prop of node.properties) {
|
|
1055
|
+
switch (prop.name) {
|
|
1056
|
+
case "level":
|
|
1057
|
+
parts.push(`level=${prop.value.value}`);
|
|
1058
|
+
break;
|
|
1059
|
+
case "checked": {
|
|
1060
|
+
const val = prop.value.value;
|
|
1061
|
+
if (val === true || val === "true" || val === "mixed") {
|
|
1062
|
+
parts.push("checked");
|
|
1063
|
+
}
|
|
1064
|
+
break;
|
|
1065
|
+
}
|
|
1066
|
+
case "selected":
|
|
1067
|
+
if (prop.value.value === true) {
|
|
1068
|
+
parts.push("selected");
|
|
1069
|
+
}
|
|
1070
|
+
break;
|
|
1071
|
+
case "expanded":
|
|
1072
|
+
if (prop.value.value === true || prop.value.value === "true") {
|
|
1073
|
+
parts.push("expanded");
|
|
1074
|
+
} else {
|
|
1075
|
+
parts.push("collapsed");
|
|
1076
|
+
}
|
|
1077
|
+
break;
|
|
1078
|
+
default:
|
|
1079
|
+
break;
|
|
1080
|
+
}
|
|
1081
|
+
}
|
|
1082
|
+
}
|
|
1083
|
+
return parts.join(" ");
|
|
1084
|
+
}
|
|
1085
|
+
async function browserSnapshot(cdp, params) {
|
|
1086
|
+
const options = {
|
|
1087
|
+
interactive: params?.interactive,
|
|
1088
|
+
cursor: params?.cursor,
|
|
1089
|
+
compact: params?.compact,
|
|
1090
|
+
depth: params?.depth,
|
|
1091
|
+
selector: params?.selector
|
|
1092
|
+
};
|
|
1093
|
+
await cdp.send("Accessibility.enable");
|
|
1094
|
+
let axNodes;
|
|
1095
|
+
if (options.selector) {
|
|
1096
|
+
const docResult = await cdp.send("DOM.getDocument");
|
|
1097
|
+
const queryResult = await cdp.send("DOM.querySelector", {
|
|
1098
|
+
nodeId: docResult.root.nodeId,
|
|
1099
|
+
selector: options.selector
|
|
1100
|
+
});
|
|
1101
|
+
if (queryResult.nodeId === 0) {
|
|
1102
|
+
return { snapshot: `No element found for selector: ${options.selector}` };
|
|
1103
|
+
}
|
|
1104
|
+
const partialResult = await cdp.send("Accessibility.getPartialAXTree", {
|
|
1105
|
+
nodeId: queryResult.nodeId,
|
|
1106
|
+
fetchRelatives: true
|
|
1107
|
+
});
|
|
1108
|
+
axNodes = partialResult.nodes;
|
|
1109
|
+
} else {
|
|
1110
|
+
const fullResult = await cdp.send("Accessibility.getFullAXTree");
|
|
1111
|
+
axNodes = fullResult.nodes;
|
|
1112
|
+
}
|
|
1113
|
+
const totalElements = axNodes.filter(
|
|
1114
|
+
(n) => n.role?.value !== "WebArea"
|
|
1115
|
+
).length;
|
|
1116
|
+
const nodeMap = /* @__PURE__ */ new Map();
|
|
1117
|
+
for (const node of axNodes) {
|
|
1118
|
+
nodeMap.set(node.nodeId, node);
|
|
1119
|
+
}
|
|
1120
|
+
let root;
|
|
1121
|
+
for (const node of axNodes) {
|
|
1122
|
+
if (!node.parentId) {
|
|
1123
|
+
root = node;
|
|
1124
|
+
break;
|
|
1125
|
+
}
|
|
1126
|
+
}
|
|
1127
|
+
if (!root) {
|
|
1128
|
+
root = axNodes[0];
|
|
1129
|
+
}
|
|
1130
|
+
if (!root) {
|
|
1131
|
+
return { snapshot: "" };
|
|
1132
|
+
}
|
|
1133
|
+
const lines = [];
|
|
1134
|
+
let refCounter = 0;
|
|
1135
|
+
const visited = /* @__PURE__ */ new Set();
|
|
1136
|
+
const maxDepth = options.depth ?? 100;
|
|
1137
|
+
function traverse(node, depth) {
|
|
1138
|
+
if (visited.has(node.nodeId)) {
|
|
1139
|
+
return;
|
|
1140
|
+
}
|
|
1141
|
+
visited.add(node.nodeId);
|
|
1142
|
+
if (depth > maxDepth) {
|
|
1143
|
+
return;
|
|
1144
|
+
}
|
|
1145
|
+
const role = node.role?.value ?? "";
|
|
1146
|
+
const isRoot = role === "WebArea";
|
|
1147
|
+
const showNode = isRoot || shouldShow(node, options);
|
|
1148
|
+
if (showNode && !isRoot) {
|
|
1149
|
+
refCounter++;
|
|
1150
|
+
const ref = node.backendDOMNodeId ? `@e${node.backendDOMNodeId}` : `@e${refCounter}`;
|
|
1151
|
+
const indent = " ".repeat(depth);
|
|
1152
|
+
const name = node.name?.value ?? "";
|
|
1153
|
+
const attrs = formatNodeAttributes(node);
|
|
1154
|
+
let line = `${indent}${ref} ${role}`;
|
|
1155
|
+
if (name) {
|
|
1156
|
+
line += ` "${name}"`;
|
|
1157
|
+
}
|
|
1158
|
+
if (attrs) {
|
|
1159
|
+
line += ` ${attrs}`;
|
|
1160
|
+
}
|
|
1161
|
+
lines.push(line);
|
|
1162
|
+
}
|
|
1163
|
+
const childNodeIds = getChildNodeIds(node);
|
|
1164
|
+
const nextDepth = isRoot ? depth : depth + 1;
|
|
1165
|
+
for (const childId of childNodeIds) {
|
|
1166
|
+
const childNode = nodeMap.get(childId);
|
|
1167
|
+
if (childNode) {
|
|
1168
|
+
traverse(childNode, nextDepth);
|
|
1169
|
+
}
|
|
1170
|
+
}
|
|
1171
|
+
}
|
|
1172
|
+
traverse(root, 0);
|
|
1173
|
+
const snapshot = lines.join("\n");
|
|
1174
|
+
const result = { snapshot };
|
|
1175
|
+
if (totalElements > 1e3) {
|
|
1176
|
+
result.truncated = true;
|
|
1177
|
+
result.totalElements = totalElements;
|
|
1178
|
+
}
|
|
1179
|
+
return result;
|
|
1180
|
+
}
|
|
1181
|
+
function shouldShow(node, options) {
|
|
1182
|
+
if (!shouldShowAxNode(node, { compact: options.compact })) {
|
|
1183
|
+
return false;
|
|
1184
|
+
}
|
|
1185
|
+
const role = node.role?.value ?? "";
|
|
1186
|
+
if (options.interactive) {
|
|
1187
|
+
if (INTERACTIVE_ROLES.has(role)) {
|
|
1188
|
+
return true;
|
|
1189
|
+
}
|
|
1190
|
+
if (options.cursor) {
|
|
1191
|
+
return hasCursorPointer(node);
|
|
1192
|
+
}
|
|
1193
|
+
return false;
|
|
1194
|
+
}
|
|
1195
|
+
if (options.cursor) {
|
|
1196
|
+
if (hasCursorPointer(node)) {
|
|
1197
|
+
return true;
|
|
1198
|
+
}
|
|
1199
|
+
}
|
|
1200
|
+
return true;
|
|
1201
|
+
}
|
|
1202
|
+
function hasCursorPointer(node) {
|
|
1203
|
+
if (!node.properties) return false;
|
|
1204
|
+
return node.properties.some(
|
|
1205
|
+
(p) => p.name === "cursor" && p.value.value === "pointer"
|
|
1206
|
+
);
|
|
1207
|
+
}
|
|
1208
|
+
var INTERACTIVE_ROLES;
|
|
1209
|
+
var init_browser_snapshot = __esm({
|
|
1210
|
+
"src/tools/browser-snapshot.ts"() {
|
|
1211
|
+
"use strict";
|
|
1212
|
+
INTERACTIVE_ROLES = /* @__PURE__ */ new Set([
|
|
1213
|
+
"button",
|
|
1214
|
+
"link",
|
|
1215
|
+
"textbox",
|
|
1216
|
+
"checkbox",
|
|
1217
|
+
"radio",
|
|
1218
|
+
"combobox",
|
|
1219
|
+
"listbox",
|
|
1220
|
+
"menuitem",
|
|
1221
|
+
"menuitemcheckbox",
|
|
1222
|
+
"menuitemradio",
|
|
1223
|
+
"option",
|
|
1224
|
+
"searchbox",
|
|
1225
|
+
"slider",
|
|
1226
|
+
"spinbutton",
|
|
1227
|
+
"switch",
|
|
1228
|
+
"tab",
|
|
1229
|
+
"treeitem"
|
|
1230
|
+
]);
|
|
1231
|
+
}
|
|
1232
|
+
});
|
|
1233
|
+
|
|
1234
|
+
// src/tools/browser-click.ts
|
|
1235
|
+
function calculateCenter(content) {
|
|
1236
|
+
const x = (content[0] + content[2] + content[4] + content[6]) / 4;
|
|
1237
|
+
const y = (content[1] + content[3] + content[5] + content[7]) / 4;
|
|
1238
|
+
return { x: Math.round(x), y: Math.round(y) };
|
|
1239
|
+
}
|
|
1240
|
+
async function resolveElementCoordinates(cdp, params) {
|
|
1241
|
+
let backendNodeId;
|
|
1242
|
+
let nodeId;
|
|
1243
|
+
if (params.ref) {
|
|
1244
|
+
const match = REF_PATTERN.exec(params.ref);
|
|
1245
|
+
if (!match) {
|
|
1246
|
+
throw new Error(`Invalid ref format: ${params.ref}`);
|
|
1247
|
+
}
|
|
1248
|
+
backendNodeId = parseInt(match[1], 10);
|
|
1249
|
+
await cdp.send("DOM.resolveNode", {
|
|
1250
|
+
backendNodeId
|
|
1251
|
+
});
|
|
1252
|
+
} else if (params.selector) {
|
|
1253
|
+
const docResponse = await cdp.send("DOM.getDocument");
|
|
1254
|
+
const queryResponse = await cdp.send("DOM.querySelector", {
|
|
1255
|
+
nodeId: docResponse.root.nodeId,
|
|
1256
|
+
selector: params.selector
|
|
1257
|
+
});
|
|
1258
|
+
if (!queryResponse.nodeId || queryResponse.nodeId === 0) {
|
|
1259
|
+
throw new Error(
|
|
1260
|
+
`Element not found: no element matches selector "${params.selector}"`
|
|
1261
|
+
);
|
|
1262
|
+
}
|
|
1263
|
+
nodeId = queryResponse.nodeId;
|
|
1264
|
+
} else {
|
|
1265
|
+
throw new Error("Either ref, selector, or coordinates (x, y) must be provided");
|
|
1266
|
+
}
|
|
1267
|
+
const scrollParams = {};
|
|
1268
|
+
if (backendNodeId !== void 0) {
|
|
1269
|
+
scrollParams.backendNodeId = backendNodeId;
|
|
1270
|
+
} else if (nodeId !== void 0) {
|
|
1271
|
+
scrollParams.nodeId = nodeId;
|
|
1272
|
+
}
|
|
1273
|
+
await cdp.send(
|
|
1274
|
+
"DOM.scrollIntoViewIfNeeded",
|
|
1275
|
+
scrollParams
|
|
1276
|
+
);
|
|
1277
|
+
const boxParams = {};
|
|
1278
|
+
if (backendNodeId !== void 0) {
|
|
1279
|
+
boxParams.backendNodeId = backendNodeId;
|
|
1280
|
+
} else if (nodeId !== void 0) {
|
|
1281
|
+
boxParams.nodeId = nodeId;
|
|
1282
|
+
}
|
|
1283
|
+
const boxResponse = await cdp.send("DOM.getBoxModel", boxParams);
|
|
1284
|
+
const { content, width, height } = boxResponse.model;
|
|
1285
|
+
if (width === 0 && height === 0) {
|
|
1286
|
+
throw new Error(
|
|
1287
|
+
"Element is not visible: zero-size box model. The element may be hidden or not rendered."
|
|
1288
|
+
);
|
|
1289
|
+
}
|
|
1290
|
+
return calculateCenter(content);
|
|
1291
|
+
}
|
|
1292
|
+
function computeModifiers(modifiers) {
|
|
1293
|
+
if (!modifiers || modifiers.length === 0) return 0;
|
|
1294
|
+
let bits = 0;
|
|
1295
|
+
for (const mod of modifiers) {
|
|
1296
|
+
const bit = MODIFIER_BITS[mod];
|
|
1297
|
+
if (bit !== void 0) {
|
|
1298
|
+
bits |= bit;
|
|
1299
|
+
}
|
|
1300
|
+
}
|
|
1301
|
+
return bits;
|
|
1302
|
+
}
|
|
1303
|
+
function delay2(ms) {
|
|
1304
|
+
return new Promise((resolve2) => setTimeout(resolve2, ms));
|
|
1305
|
+
}
|
|
1306
|
+
async function browserClick(cdp, params) {
|
|
1307
|
+
let x;
|
|
1308
|
+
let y;
|
|
1309
|
+
if (params.x !== void 0 && params.y !== void 0) {
|
|
1310
|
+
x = params.x;
|
|
1311
|
+
y = params.y;
|
|
1312
|
+
} else {
|
|
1313
|
+
const coords = await resolveElementCoordinates(cdp, params);
|
|
1314
|
+
x = coords.x;
|
|
1315
|
+
y = coords.y;
|
|
1316
|
+
}
|
|
1317
|
+
const button = params.button ?? "left";
|
|
1318
|
+
let effectiveModifiers = params.modifiers ? [...params.modifiers] : [];
|
|
1319
|
+
if (params.newTab) {
|
|
1320
|
+
const isMac = typeof process !== "undefined" && process.platform === "darwin";
|
|
1321
|
+
const newTabModifier = isMac ? "Meta" : "Control";
|
|
1322
|
+
if (!effectiveModifiers.includes(newTabModifier)) {
|
|
1323
|
+
effectiveModifiers.push(newTabModifier);
|
|
1324
|
+
}
|
|
1325
|
+
}
|
|
1326
|
+
const modifiers = computeModifiers(effectiveModifiers);
|
|
1327
|
+
if (params.doubleClick) {
|
|
1328
|
+
await performDoubleClick(cdp, x, y, button, modifiers);
|
|
1329
|
+
} else {
|
|
1330
|
+
await performClick(cdp, x, y, button, modifiers);
|
|
1331
|
+
}
|
|
1332
|
+
return { success: true };
|
|
1333
|
+
}
|
|
1334
|
+
async function performClick(cdp, x, y, button, modifiers, clickCount = 1) {
|
|
1335
|
+
await cdp.send("Input.dispatchMouseEvent", {
|
|
1336
|
+
type: "mouseMoved",
|
|
1337
|
+
x,
|
|
1338
|
+
y,
|
|
1339
|
+
modifiers
|
|
1340
|
+
});
|
|
1341
|
+
await cdp.send("Input.dispatchMouseEvent", {
|
|
1342
|
+
type: "mousePressed",
|
|
1343
|
+
x,
|
|
1344
|
+
y,
|
|
1345
|
+
button,
|
|
1346
|
+
clickCount,
|
|
1347
|
+
modifiers
|
|
1348
|
+
});
|
|
1349
|
+
await delay2(50);
|
|
1350
|
+
await cdp.send("Input.dispatchMouseEvent", {
|
|
1351
|
+
type: "mouseReleased",
|
|
1352
|
+
x,
|
|
1353
|
+
y,
|
|
1354
|
+
button,
|
|
1355
|
+
clickCount,
|
|
1356
|
+
modifiers
|
|
1357
|
+
});
|
|
1358
|
+
}
|
|
1359
|
+
async function performDoubleClick(cdp, x, y, button, modifiers) {
|
|
1360
|
+
await performClick(cdp, x, y, button, modifiers, 1);
|
|
1361
|
+
await performClick(cdp, x, y, button, modifiers, 2);
|
|
1362
|
+
}
|
|
1363
|
+
var MODIFIER_BITS, REF_PATTERN;
|
|
1364
|
+
var init_browser_click = __esm({
|
|
1365
|
+
"src/tools/browser-click.ts"() {
|
|
1366
|
+
"use strict";
|
|
1367
|
+
MODIFIER_BITS = {
|
|
1368
|
+
Alt: 1,
|
|
1369
|
+
Control: 2,
|
|
1370
|
+
Meta: 4,
|
|
1371
|
+
Shift: 8
|
|
1372
|
+
};
|
|
1373
|
+
REF_PATTERN = /^@e(\d+)$/;
|
|
1374
|
+
}
|
|
1375
|
+
});
|
|
1376
|
+
|
|
1377
|
+
// src/tools/browser-scroll.ts
|
|
1378
|
+
async function resolveSelector(cdp, selector) {
|
|
1379
|
+
const evalResult = await cdp.send("Runtime.evaluate", {
|
|
1380
|
+
expression: `document.querySelector(${JSON.stringify(selector)})`,
|
|
1381
|
+
returnByValue: false
|
|
1382
|
+
});
|
|
1383
|
+
if (evalResult.result.objectId && evalResult.result.subtype !== "null") {
|
|
1384
|
+
return { objectId: evalResult.result.objectId };
|
|
1385
|
+
}
|
|
1386
|
+
if (evalResult.result.subtype === "null" || evalResult.result.value === null) {
|
|
1387
|
+
throw new Error(`Element not found: ${selector}`);
|
|
1388
|
+
}
|
|
1389
|
+
try {
|
|
1390
|
+
const resolveResponse = await cdp.send("DOM.resolveNode", {
|
|
1391
|
+
backendNodeId: void 0
|
|
1392
|
+
});
|
|
1393
|
+
if (resolveResponse.object?.objectId) {
|
|
1394
|
+
return { objectId: resolveResponse.object.objectId };
|
|
1395
|
+
}
|
|
1396
|
+
} catch {
|
|
1397
|
+
}
|
|
1398
|
+
throw new Error(`Could not find element: ${selector}`);
|
|
1399
|
+
}
|
|
1400
|
+
async function browserScroll(cdp, params) {
|
|
1401
|
+
const { direction, selector } = params;
|
|
1402
|
+
const amount = params.amount ?? DEFAULT_SCROLL_AMOUNT;
|
|
1403
|
+
if (selector && !direction) {
|
|
1404
|
+
const { objectId } = await resolveSelector(cdp, selector);
|
|
1405
|
+
await cdp.send("Runtime.callFunctionOn", {
|
|
1406
|
+
objectId,
|
|
1407
|
+
functionDeclaration: `function() { this.scrollIntoView({ behavior: "smooth", block: "center" }); }`,
|
|
1408
|
+
returnByValue: true
|
|
1409
|
+
});
|
|
1410
|
+
return { success: true };
|
|
1411
|
+
}
|
|
1412
|
+
if (selector && direction) {
|
|
1413
|
+
const { objectId } = await resolveSelector(cdp, selector);
|
|
1414
|
+
const scrollX2 = direction === "left" ? -amount : direction === "right" ? amount : 0;
|
|
1415
|
+
const scrollY2 = direction === "up" ? -amount : direction === "down" ? amount : 0;
|
|
1416
|
+
await cdp.send("Runtime.callFunctionOn", {
|
|
1417
|
+
objectId,
|
|
1418
|
+
functionDeclaration: `function() { this.scrollBy(${scrollX2}, ${scrollY2}); }`,
|
|
1419
|
+
returnByValue: true
|
|
1420
|
+
});
|
|
1421
|
+
return { success: true };
|
|
1422
|
+
}
|
|
1423
|
+
const scrollX = direction === "left" ? -amount : direction === "right" ? amount : 0;
|
|
1424
|
+
const scrollY = direction === "up" ? -amount : direction === "down" ? amount : 0;
|
|
1425
|
+
await cdp.send("Runtime.evaluate", {
|
|
1426
|
+
expression: `window.scrollBy(${scrollX}, ${scrollY})`,
|
|
1427
|
+
returnByValue: true
|
|
1428
|
+
});
|
|
1429
|
+
return { success: true };
|
|
1430
|
+
}
|
|
1431
|
+
var DEFAULT_SCROLL_AMOUNT;
|
|
1432
|
+
var init_browser_scroll = __esm({
|
|
1433
|
+
"src/tools/browser-scroll.ts"() {
|
|
1434
|
+
"use strict";
|
|
1435
|
+
DEFAULT_SCROLL_AMOUNT = 300;
|
|
1436
|
+
}
|
|
1437
|
+
});
|
|
1438
|
+
|
|
1439
|
+
// src/tools/browser-html.ts
|
|
1440
|
+
async function browserHtml(cdp, params) {
|
|
1441
|
+
if (!params.selector) {
|
|
1442
|
+
const evalResult = await cdp.send("Runtime.evaluate", {
|
|
1443
|
+
expression: "document.documentElement.outerHTML",
|
|
1444
|
+
returnByValue: true
|
|
1445
|
+
});
|
|
1446
|
+
return { html: evalResult.result.value };
|
|
1447
|
+
}
|
|
1448
|
+
const doc = await cdp.send("DOM.getDocument", {});
|
|
1449
|
+
const queryResult = await cdp.send("DOM.querySelector", {
|
|
1450
|
+
nodeId: doc.root.nodeId,
|
|
1451
|
+
selector: params.selector
|
|
1452
|
+
});
|
|
1453
|
+
if (!queryResult.nodeId) {
|
|
1454
|
+
return {
|
|
1455
|
+
html: "",
|
|
1456
|
+
error: `Element not found: ${params.selector}`
|
|
1457
|
+
};
|
|
1458
|
+
}
|
|
1459
|
+
const outerResult = await cdp.send("DOM.getOuterHTML", {
|
|
1460
|
+
nodeId: queryResult.nodeId
|
|
1461
|
+
});
|
|
1462
|
+
return { html: outerResult.outerHTML };
|
|
1463
|
+
}
|
|
1464
|
+
var init_browser_html = __esm({
|
|
1465
|
+
"src/tools/browser-html.ts"() {
|
|
1466
|
+
"use strict";
|
|
1467
|
+
}
|
|
1468
|
+
});
|
|
1469
|
+
|
|
1470
|
+
// src/tools/browser-navigate-back.ts
|
|
1471
|
+
async function browserNavigateBack(cdp, params) {
|
|
1472
|
+
const direction = params.direction ?? "back";
|
|
1473
|
+
const history = await cdp.send("Page.getNavigationHistory");
|
|
1474
|
+
const targetIndex = direction === "back" ? history.currentIndex - 1 : history.currentIndex + 1;
|
|
1475
|
+
if (targetIndex < 0 || targetIndex >= history.entries.length) {
|
|
1476
|
+
return { success: false, url: history.entries[history.currentIndex]?.url };
|
|
1477
|
+
}
|
|
1478
|
+
const entry = history.entries[targetIndex];
|
|
1479
|
+
await cdp.send("Page.navigateToHistoryEntry", { entryId: entry.id });
|
|
1480
|
+
return { success: true, url: entry.url };
|
|
1481
|
+
}
|
|
1482
|
+
var init_browser_navigate_back = __esm({
|
|
1483
|
+
"src/tools/browser-navigate-back.ts"() {
|
|
1484
|
+
"use strict";
|
|
1485
|
+
}
|
|
1486
|
+
});
|
|
1487
|
+
|
|
1488
|
+
// src/tools/browser-press-key.ts
|
|
1489
|
+
function getKeyCode(key) {
|
|
1490
|
+
if (KEY_CODE_MAP[key] !== void 0) {
|
|
1491
|
+
return KEY_CODE_MAP[key];
|
|
1492
|
+
}
|
|
1493
|
+
if (key.length === 1) {
|
|
1494
|
+
return key.toUpperCase().charCodeAt(0);
|
|
1495
|
+
}
|
|
1496
|
+
return 0;
|
|
1497
|
+
}
|
|
1498
|
+
function getCode(key) {
|
|
1499
|
+
if (KEY_TO_CODE[key] !== void 0) {
|
|
1500
|
+
return KEY_TO_CODE[key];
|
|
1501
|
+
}
|
|
1502
|
+
if (key.length === 1) {
|
|
1503
|
+
const upper = key.toUpperCase();
|
|
1504
|
+
if (upper >= "A" && upper <= "Z") {
|
|
1505
|
+
return `Key${upper}`;
|
|
1506
|
+
}
|
|
1507
|
+
if (upper >= "0" && upper <= "9") {
|
|
1508
|
+
return `Digit${upper}`;
|
|
1509
|
+
}
|
|
1510
|
+
}
|
|
1511
|
+
return key;
|
|
1512
|
+
}
|
|
1513
|
+
function getKeyText(key) {
|
|
1514
|
+
if (KEY_TEXT_MAP[key] !== void 0) {
|
|
1515
|
+
return KEY_TEXT_MAP[key] || void 0;
|
|
1516
|
+
}
|
|
1517
|
+
if (key.length === 1) {
|
|
1518
|
+
return key;
|
|
1519
|
+
}
|
|
1520
|
+
return void 0;
|
|
1521
|
+
}
|
|
1522
|
+
async function browserPressKey(cdp, params) {
|
|
1523
|
+
const parts = params.key.split("+");
|
|
1524
|
+
const mainKey = parts[parts.length - 1];
|
|
1525
|
+
const modifierKeys = parts.slice(0, -1);
|
|
1526
|
+
let modifiers = 0;
|
|
1527
|
+
for (const mod of modifierKeys) {
|
|
1528
|
+
if (MODIFIER_BITS2[mod] !== void 0) {
|
|
1529
|
+
modifiers |= MODIFIER_BITS2[mod];
|
|
1530
|
+
}
|
|
1531
|
+
}
|
|
1532
|
+
for (const mod of modifierKeys) {
|
|
1533
|
+
await cdp.send("Input.dispatchKeyEvent", {
|
|
1534
|
+
type: "keyDown",
|
|
1535
|
+
key: mod,
|
|
1536
|
+
code: getCode(mod),
|
|
1537
|
+
windowsVirtualKeyCode: getKeyCode(mod),
|
|
1538
|
+
modifiers
|
|
1539
|
+
});
|
|
1540
|
+
}
|
|
1541
|
+
const text = getKeyText(mainKey);
|
|
1542
|
+
const keyDownParams = {
|
|
1543
|
+
type: "keyDown",
|
|
1544
|
+
key: mainKey,
|
|
1545
|
+
code: getCode(mainKey),
|
|
1546
|
+
windowsVirtualKeyCode: getKeyCode(mainKey),
|
|
1547
|
+
modifiers
|
|
1548
|
+
};
|
|
1549
|
+
if (text !== void 0) {
|
|
1550
|
+
keyDownParams.text = text;
|
|
1551
|
+
}
|
|
1552
|
+
await cdp.send("Input.dispatchKeyEvent", keyDownParams);
|
|
1553
|
+
if (text && modifierKeys.length === 0) {
|
|
1554
|
+
await cdp.send("Input.dispatchKeyEvent", {
|
|
1555
|
+
type: "char",
|
|
1556
|
+
key: mainKey,
|
|
1557
|
+
code: getCode(mainKey),
|
|
1558
|
+
windowsVirtualKeyCode: getKeyCode(mainKey),
|
|
1559
|
+
modifiers,
|
|
1560
|
+
text
|
|
1561
|
+
});
|
|
1562
|
+
}
|
|
1563
|
+
await cdp.send("Input.dispatchKeyEvent", {
|
|
1564
|
+
type: "keyUp",
|
|
1565
|
+
key: mainKey,
|
|
1566
|
+
code: getCode(mainKey),
|
|
1567
|
+
windowsVirtualKeyCode: getKeyCode(mainKey),
|
|
1568
|
+
modifiers
|
|
1569
|
+
});
|
|
1570
|
+
for (let i = modifierKeys.length - 1; i >= 0; i--) {
|
|
1571
|
+
const mod = modifierKeys[i];
|
|
1572
|
+
await cdp.send("Input.dispatchKeyEvent", {
|
|
1573
|
+
type: "keyUp",
|
|
1574
|
+
key: mod,
|
|
1575
|
+
code: getCode(mod),
|
|
1576
|
+
windowsVirtualKeyCode: getKeyCode(mod),
|
|
1577
|
+
modifiers: 0
|
|
1578
|
+
});
|
|
1579
|
+
}
|
|
1580
|
+
return { success: true };
|
|
1581
|
+
}
|
|
1582
|
+
var KEY_CODE_MAP, KEY_TO_CODE, KEY_TEXT_MAP, MODIFIER_BITS2;
|
|
1583
|
+
var init_browser_press_key = __esm({
|
|
1584
|
+
"src/tools/browser-press-key.ts"() {
|
|
1585
|
+
"use strict";
|
|
1586
|
+
KEY_CODE_MAP = {
|
|
1587
|
+
Enter: 13,
|
|
1588
|
+
Tab: 9,
|
|
1589
|
+
Escape: 27,
|
|
1590
|
+
Backspace: 8,
|
|
1591
|
+
Delete: 46,
|
|
1592
|
+
ArrowLeft: 37,
|
|
1593
|
+
ArrowUp: 38,
|
|
1594
|
+
ArrowRight: 39,
|
|
1595
|
+
ArrowDown: 40,
|
|
1596
|
+
Home: 36,
|
|
1597
|
+
End: 35,
|
|
1598
|
+
PageUp: 33,
|
|
1599
|
+
PageDown: 34,
|
|
1600
|
+
Insert: 45,
|
|
1601
|
+
Space: 32,
|
|
1602
|
+
" ": 32,
|
|
1603
|
+
F1: 112,
|
|
1604
|
+
F2: 113,
|
|
1605
|
+
F3: 114,
|
|
1606
|
+
F4: 115,
|
|
1607
|
+
F5: 116,
|
|
1608
|
+
F6: 117,
|
|
1609
|
+
F7: 118,
|
|
1610
|
+
F8: 119,
|
|
1611
|
+
F9: 120,
|
|
1612
|
+
F10: 121,
|
|
1613
|
+
F11: 122,
|
|
1614
|
+
F12: 123,
|
|
1615
|
+
Control: 17,
|
|
1616
|
+
Shift: 16,
|
|
1617
|
+
Alt: 18,
|
|
1618
|
+
Meta: 91
|
|
1619
|
+
};
|
|
1620
|
+
KEY_TO_CODE = {
|
|
1621
|
+
Enter: "Enter",
|
|
1622
|
+
Tab: "Tab",
|
|
1623
|
+
Escape: "Escape",
|
|
1624
|
+
Backspace: "Backspace",
|
|
1625
|
+
Delete: "Delete",
|
|
1626
|
+
ArrowLeft: "ArrowLeft",
|
|
1627
|
+
ArrowUp: "ArrowUp",
|
|
1628
|
+
ArrowRight: "ArrowRight",
|
|
1629
|
+
ArrowDown: "ArrowDown",
|
|
1630
|
+
Home: "Home",
|
|
1631
|
+
End: "End",
|
|
1632
|
+
PageUp: "PageUp",
|
|
1633
|
+
PageDown: "PageDown",
|
|
1634
|
+
Insert: "Insert",
|
|
1635
|
+
Space: "Space",
|
|
1636
|
+
" ": "Space",
|
|
1637
|
+
Control: "ControlLeft",
|
|
1638
|
+
Shift: "ShiftLeft",
|
|
1639
|
+
Alt: "AltLeft",
|
|
1640
|
+
Meta: "MetaLeft"
|
|
1641
|
+
};
|
|
1642
|
+
KEY_TEXT_MAP = {
|
|
1643
|
+
Enter: "\r",
|
|
1644
|
+
Tab: " ",
|
|
1645
|
+
Escape: "",
|
|
1646
|
+
Space: " ",
|
|
1647
|
+
Backspace: "\b"
|
|
1648
|
+
};
|
|
1649
|
+
MODIFIER_BITS2 = {
|
|
1650
|
+
Alt: 1,
|
|
1651
|
+
Control: 2,
|
|
1652
|
+
Meta: 4,
|
|
1653
|
+
Shift: 8
|
|
1654
|
+
};
|
|
1655
|
+
}
|
|
1656
|
+
});
|
|
1657
|
+
|
|
1658
|
+
// src/tools/browser-hover.ts
|
|
1659
|
+
function calculateCenter2(content) {
|
|
1660
|
+
const x = (content[0] + content[2] + content[4] + content[6]) / 4;
|
|
1661
|
+
const y = (content[1] + content[3] + content[5] + content[7]) / 4;
|
|
1662
|
+
return { x: Math.round(x), y: Math.round(y) };
|
|
1663
|
+
}
|
|
1664
|
+
async function resolveElementCoordinates2(cdp, params) {
|
|
1665
|
+
let backendNodeId;
|
|
1666
|
+
let nodeId;
|
|
1667
|
+
if (params.ref) {
|
|
1668
|
+
const match = REF_PATTERN2.exec(params.ref);
|
|
1669
|
+
if (!match) {
|
|
1670
|
+
throw new Error(`Invalid ref format: ${params.ref}`);
|
|
1671
|
+
}
|
|
1672
|
+
backendNodeId = parseInt(match[1], 10);
|
|
1673
|
+
await cdp.send("DOM.resolveNode", {
|
|
1674
|
+
backendNodeId
|
|
1675
|
+
});
|
|
1676
|
+
} else if (params.selector) {
|
|
1677
|
+
const docResponse = await cdp.send("DOM.getDocument");
|
|
1678
|
+
const queryResponse = await cdp.send("DOM.querySelector", {
|
|
1679
|
+
nodeId: docResponse.root.nodeId,
|
|
1680
|
+
selector: params.selector
|
|
1681
|
+
});
|
|
1682
|
+
if (!queryResponse.nodeId || queryResponse.nodeId === 0) {
|
|
1683
|
+
throw new Error(
|
|
1684
|
+
`Element not found: no element matches selector "${params.selector}"`
|
|
1685
|
+
);
|
|
1686
|
+
}
|
|
1687
|
+
nodeId = queryResponse.nodeId;
|
|
1688
|
+
} else {
|
|
1689
|
+
throw new Error("Either ref or selector must be provided");
|
|
1690
|
+
}
|
|
1691
|
+
const scrollParams = {};
|
|
1692
|
+
if (backendNodeId !== void 0) {
|
|
1693
|
+
scrollParams.backendNodeId = backendNodeId;
|
|
1694
|
+
} else if (nodeId !== void 0) {
|
|
1695
|
+
scrollParams.nodeId = nodeId;
|
|
1696
|
+
}
|
|
1697
|
+
await cdp.send("DOM.scrollIntoViewIfNeeded", scrollParams);
|
|
1698
|
+
const boxParams = {};
|
|
1699
|
+
if (backendNodeId !== void 0) {
|
|
1700
|
+
boxParams.backendNodeId = backendNodeId;
|
|
1701
|
+
} else if (nodeId !== void 0) {
|
|
1702
|
+
boxParams.nodeId = nodeId;
|
|
1703
|
+
}
|
|
1704
|
+
const boxResponse = await cdp.send("DOM.getBoxModel", boxParams);
|
|
1705
|
+
return calculateCenter2(boxResponse.model.content);
|
|
1706
|
+
}
|
|
1707
|
+
async function browserHover(cdp, params) {
|
|
1708
|
+
const { x, y } = await resolveElementCoordinates2(cdp, params);
|
|
1709
|
+
await cdp.send("Input.dispatchMouseEvent", {
|
|
1710
|
+
type: "mouseMoved",
|
|
1711
|
+
x,
|
|
1712
|
+
y
|
|
1713
|
+
});
|
|
1714
|
+
return { success: true };
|
|
1715
|
+
}
|
|
1716
|
+
var REF_PATTERN2;
|
|
1717
|
+
var init_browser_hover = __esm({
|
|
1718
|
+
"src/tools/browser-hover.ts"() {
|
|
1719
|
+
"use strict";
|
|
1720
|
+
REF_PATTERN2 = /^@?e(\d+)$/;
|
|
1721
|
+
}
|
|
1722
|
+
});
|
|
1723
|
+
|
|
1724
|
+
// src/tools/browser-resize.ts
|
|
1725
|
+
async function browserResize(cdp, params) {
|
|
1726
|
+
let width;
|
|
1727
|
+
let height;
|
|
1728
|
+
if (params.preset?.toLowerCase() === "reset") {
|
|
1729
|
+
await cdp.send("Emulation.clearDeviceMetricsOverride");
|
|
1730
|
+
return { success: true, width: 0, height: 0 };
|
|
1731
|
+
}
|
|
1732
|
+
if (params.preset) {
|
|
1733
|
+
const preset = PRESETS[params.preset.toLowerCase()];
|
|
1734
|
+
if (!preset) {
|
|
1735
|
+
throw new Error(
|
|
1736
|
+
`Unknown preset "${params.preset}". Available: ${Object.keys(PRESETS).join(", ")}, reset`
|
|
1737
|
+
);
|
|
1738
|
+
}
|
|
1739
|
+
width = preset.width;
|
|
1740
|
+
height = preset.height;
|
|
1741
|
+
} else {
|
|
1742
|
+
width = params.width ?? 1280;
|
|
1743
|
+
height = params.height ?? 720;
|
|
1744
|
+
}
|
|
1745
|
+
if (params.width !== void 0) width = params.width;
|
|
1746
|
+
if (params.height !== void 0) height = params.height;
|
|
1747
|
+
const deviceScaleFactor = params.deviceScaleFactor ?? 0;
|
|
1748
|
+
const mobile = width < 768;
|
|
1749
|
+
await cdp.send("Emulation.setDeviceMetricsOverride", {
|
|
1750
|
+
width,
|
|
1751
|
+
height,
|
|
1752
|
+
deviceScaleFactor,
|
|
1753
|
+
mobile
|
|
1754
|
+
});
|
|
1755
|
+
return {
|
|
1756
|
+
success: true,
|
|
1757
|
+
width,
|
|
1758
|
+
height
|
|
1759
|
+
};
|
|
1760
|
+
}
|
|
1761
|
+
var PRESETS;
|
|
1762
|
+
var init_browser_resize = __esm({
|
|
1763
|
+
"src/tools/browser-resize.ts"() {
|
|
1764
|
+
"use strict";
|
|
1765
|
+
PRESETS = {
|
|
1766
|
+
mobile: { width: 375, height: 667 },
|
|
1767
|
+
tablet: { width: 768, height: 1024 },
|
|
1768
|
+
desktop: { width: 1280, height: 720 },
|
|
1769
|
+
fullhd: { width: 1920, height: 1080 }
|
|
1770
|
+
};
|
|
1771
|
+
}
|
|
1772
|
+
});
|
|
1773
|
+
|
|
1774
|
+
// src/tools/browser-close.ts
|
|
1775
|
+
async function browserClose(cdp, params) {
|
|
1776
|
+
let closedCount = 0;
|
|
1777
|
+
if (params.closeAll) {
|
|
1778
|
+
const targetsResponse = await cdp.send("Target.getTargets");
|
|
1779
|
+
const pageTargets = targetsResponse.targetInfos.filter(
|
|
1780
|
+
(t) => t.type === "page"
|
|
1781
|
+
);
|
|
1782
|
+
for (const target of pageTargets) {
|
|
1783
|
+
try {
|
|
1784
|
+
await cdp.send("Target.closeTarget", {
|
|
1785
|
+
targetId: target.targetId
|
|
1786
|
+
});
|
|
1787
|
+
closedCount++;
|
|
1788
|
+
} catch {
|
|
1789
|
+
}
|
|
1790
|
+
}
|
|
1791
|
+
} else if (params.targetId) {
|
|
1792
|
+
await cdp.send("Target.closeTarget", {
|
|
1793
|
+
targetId: params.targetId
|
|
1794
|
+
});
|
|
1795
|
+
closedCount = 1;
|
|
1796
|
+
} else {
|
|
1797
|
+
const targetsResponse = await cdp.send("Target.getTargets");
|
|
1798
|
+
const pageTargets = targetsResponse.targetInfos.filter(
|
|
1799
|
+
(t) => t.type === "page"
|
|
1800
|
+
);
|
|
1801
|
+
if (pageTargets.length === 0) {
|
|
1802
|
+
throw new Error("No page targets found to close");
|
|
1803
|
+
}
|
|
1804
|
+
const activeTarget = pageTargets.find((t) => t.attached) ?? pageTargets[0];
|
|
1805
|
+
await cdp.send("Target.closeTarget", {
|
|
1806
|
+
targetId: activeTarget.targetId
|
|
1807
|
+
});
|
|
1808
|
+
closedCount = 1;
|
|
1809
|
+
}
|
|
1810
|
+
return {
|
|
1811
|
+
success: true,
|
|
1812
|
+
closedTargets: closedCount
|
|
1813
|
+
};
|
|
1814
|
+
}
|
|
1815
|
+
var init_browser_close = __esm({
|
|
1816
|
+
"src/tools/browser-close.ts"() {
|
|
1817
|
+
"use strict";
|
|
1818
|
+
}
|
|
1819
|
+
});
|
|
1820
|
+
|
|
1821
|
+
// src/tools/browser-fill-form.ts
|
|
1822
|
+
function parseRef(ref) {
|
|
1823
|
+
const match = ref.match(/@?e(\d+)/);
|
|
1824
|
+
if (!match) {
|
|
1825
|
+
throw new Error(`Invalid ref format: ${ref}`);
|
|
1826
|
+
}
|
|
1827
|
+
return parseInt(match[1], 10);
|
|
1828
|
+
}
|
|
1829
|
+
async function resolveObjectId(cdp, field) {
|
|
1830
|
+
if (field.ref) {
|
|
1831
|
+
const backendNodeId = parseRef(field.ref);
|
|
1832
|
+
const result = await cdp.send("DOM.resolveNode", {
|
|
1833
|
+
backendNodeId
|
|
1834
|
+
});
|
|
1835
|
+
return result.object.objectId;
|
|
1836
|
+
}
|
|
1837
|
+
if (field.selector) {
|
|
1838
|
+
const evalResult = await cdp.send("Runtime.evaluate", {
|
|
1839
|
+
expression: `document.querySelector(${JSON.stringify(field.selector)})`,
|
|
1840
|
+
returnByValue: false
|
|
1841
|
+
});
|
|
1842
|
+
if (evalResult.result.objectId) {
|
|
1843
|
+
return evalResult.result.objectId;
|
|
1844
|
+
}
|
|
1845
|
+
const docResult = await cdp.send(
|
|
1846
|
+
"DOM.getDocument",
|
|
1847
|
+
{}
|
|
1848
|
+
);
|
|
1849
|
+
const queryResult = await cdp.send("DOM.querySelector", {
|
|
1850
|
+
nodeId: docResult.root.nodeId,
|
|
1851
|
+
selector: field.selector
|
|
1852
|
+
});
|
|
1853
|
+
const resolveResult = await cdp.send("DOM.resolveNode", {
|
|
1854
|
+
nodeId: queryResult.nodeId
|
|
1855
|
+
});
|
|
1856
|
+
return resolveResult.object.objectId;
|
|
1857
|
+
}
|
|
1858
|
+
throw new Error(`Field "${field.name}" has neither ref nor selector`);
|
|
1859
|
+
}
|
|
1860
|
+
async function isReadonlyOrDisabled(cdp, objectId) {
|
|
1861
|
+
const result = await cdp.send("Runtime.callFunctionOn", {
|
|
1862
|
+
objectId,
|
|
1863
|
+
functionDeclaration: `function() { return this.readOnly || this.disabled; }`,
|
|
1864
|
+
returnByValue: true
|
|
1865
|
+
});
|
|
1866
|
+
return result.result.value === true;
|
|
1867
|
+
}
|
|
1868
|
+
async function clickElement(cdp, field) {
|
|
1869
|
+
let backendNodeId;
|
|
1870
|
+
if (field.ref) {
|
|
1871
|
+
backendNodeId = parseRef(field.ref);
|
|
1872
|
+
}
|
|
1873
|
+
if (backendNodeId !== void 0) {
|
|
1874
|
+
await cdp.send("DOM.scrollIntoViewIfNeeded", {
|
|
1875
|
+
backendNodeId
|
|
1876
|
+
});
|
|
1877
|
+
}
|
|
1878
|
+
const boxResult = await cdp.send("DOM.getBoxModel", {
|
|
1879
|
+
backendNodeId
|
|
1880
|
+
});
|
|
1881
|
+
const quad = boxResult.model.content;
|
|
1882
|
+
const centerX = (quad[0] + quad[2] + quad[4] + quad[6]) / 4;
|
|
1883
|
+
const centerY = (quad[1] + quad[3] + quad[5] + quad[7]) / 4;
|
|
1884
|
+
await cdp.send("Input.dispatchMouseEvent", {
|
|
1885
|
+
type: "mousePressed",
|
|
1886
|
+
x: centerX,
|
|
1887
|
+
y: centerY,
|
|
1888
|
+
button: "left",
|
|
1889
|
+
clickCount: 1
|
|
1890
|
+
});
|
|
1891
|
+
await cdp.send("Input.dispatchMouseEvent", {
|
|
1892
|
+
type: "mouseReleased",
|
|
1893
|
+
x: centerX,
|
|
1894
|
+
y: centerY,
|
|
1895
|
+
button: "left",
|
|
1896
|
+
clickCount: 1
|
|
1897
|
+
});
|
|
1898
|
+
}
|
|
1899
|
+
async function fillTextbox(cdp, field, objectId) {
|
|
1900
|
+
if (field.ref) {
|
|
1901
|
+
const backendNodeId = parseRef(field.ref);
|
|
1902
|
+
await cdp.send("DOM.focus", {
|
|
1903
|
+
backendNodeId
|
|
1904
|
+
});
|
|
1905
|
+
} else {
|
|
1906
|
+
await cdp.send("Runtime.callFunctionOn", {
|
|
1907
|
+
objectId,
|
|
1908
|
+
functionDeclaration: `function() { this.focus(); }`,
|
|
1909
|
+
returnByValue: true
|
|
1910
|
+
});
|
|
1911
|
+
}
|
|
1912
|
+
await cdp.send("Runtime.callFunctionOn", {
|
|
1913
|
+
objectId,
|
|
1914
|
+
functionDeclaration: `function() { this.value = ''; this.dispatchEvent(new Event('input', { bubbles: true })); }`,
|
|
1915
|
+
returnByValue: true
|
|
1916
|
+
});
|
|
1917
|
+
await cdp.send("Input.insertText", {
|
|
1918
|
+
text: field.value
|
|
1919
|
+
});
|
|
1920
|
+
await cdp.send("Runtime.callFunctionOn", {
|
|
1921
|
+
objectId,
|
|
1922
|
+
functionDeclaration: `function() {
|
|
1923
|
+
this.dispatchEvent(new Event('input', { bubbles: true }));
|
|
1924
|
+
this.dispatchEvent(new Event('change', { bubbles: true }));
|
|
1925
|
+
}`,
|
|
1926
|
+
returnByValue: true
|
|
1927
|
+
});
|
|
1928
|
+
}
|
|
1929
|
+
async function fillCombobox(cdp, _field, objectId, value) {
|
|
1930
|
+
await cdp.send("Runtime.callFunctionOn", {
|
|
1931
|
+
objectId,
|
|
1932
|
+
functionDeclaration: `function(val) {
|
|
1933
|
+
this.value = val;
|
|
1934
|
+
this.dispatchEvent(new Event('input', { bubbles: true }));
|
|
1935
|
+
this.dispatchEvent(new Event('change', { bubbles: true }));
|
|
1936
|
+
return [val];
|
|
1937
|
+
}`,
|
|
1938
|
+
arguments: [{ value }],
|
|
1939
|
+
returnByValue: true
|
|
1940
|
+
});
|
|
1941
|
+
}
|
|
1942
|
+
async function fillSlider(cdp, _field, objectId, value) {
|
|
1943
|
+
await cdp.send("Runtime.callFunctionOn", {
|
|
1944
|
+
objectId,
|
|
1945
|
+
functionDeclaration: `function(val) {
|
|
1946
|
+
this.value = val;
|
|
1947
|
+
this.dispatchEvent(new Event('input', { bubbles: true }));
|
|
1948
|
+
this.dispatchEvent(new Event('change', { bubbles: true }));
|
|
1949
|
+
}`,
|
|
1950
|
+
arguments: [{ value }],
|
|
1951
|
+
returnByValue: true
|
|
1952
|
+
});
|
|
1953
|
+
}
|
|
1954
|
+
async function browserFillForm(cdp, params) {
|
|
1955
|
+
let filledCount = 0;
|
|
1956
|
+
const errors = [];
|
|
1957
|
+
for (const field of params.fields) {
|
|
1958
|
+
try {
|
|
1959
|
+
const objectId = await resolveObjectId(cdp, field);
|
|
1960
|
+
switch (field.type) {
|
|
1961
|
+
case "textbox": {
|
|
1962
|
+
const isBlocked = await isReadonlyOrDisabled(cdp, objectId);
|
|
1963
|
+
if (isBlocked) {
|
|
1964
|
+
errors.push({
|
|
1965
|
+
field: field.name,
|
|
1966
|
+
error: `Cannot fill readonly or disabled field "${field.name}"`
|
|
1967
|
+
});
|
|
1968
|
+
continue;
|
|
1969
|
+
}
|
|
1970
|
+
await fillTextbox(cdp, field, objectId);
|
|
1971
|
+
filledCount++;
|
|
1972
|
+
break;
|
|
1973
|
+
}
|
|
1974
|
+
case "checkbox": {
|
|
1975
|
+
await clickElement(cdp, field);
|
|
1976
|
+
filledCount++;
|
|
1977
|
+
break;
|
|
1978
|
+
}
|
|
1979
|
+
case "radio": {
|
|
1980
|
+
await clickElement(cdp, field);
|
|
1981
|
+
filledCount++;
|
|
1982
|
+
break;
|
|
1983
|
+
}
|
|
1984
|
+
case "combobox": {
|
|
1985
|
+
await fillCombobox(cdp, field, objectId, field.value);
|
|
1986
|
+
filledCount++;
|
|
1987
|
+
break;
|
|
1988
|
+
}
|
|
1989
|
+
case "slider": {
|
|
1990
|
+
await fillSlider(cdp, field, objectId, field.value);
|
|
1991
|
+
filledCount++;
|
|
1992
|
+
break;
|
|
1993
|
+
}
|
|
1994
|
+
default: {
|
|
1995
|
+
await fillTextbox(cdp, field, objectId);
|
|
1996
|
+
filledCount++;
|
|
1997
|
+
break;
|
|
1998
|
+
}
|
|
1999
|
+
}
|
|
2000
|
+
} catch (err) {
|
|
2001
|
+
errors.push({
|
|
2002
|
+
field: field.name,
|
|
2003
|
+
error: err instanceof Error ? err.message : `Unknown error filling ${field.name}`
|
|
2004
|
+
});
|
|
2005
|
+
}
|
|
2006
|
+
}
|
|
2007
|
+
return {
|
|
2008
|
+
success: errors.length === 0,
|
|
2009
|
+
filledCount,
|
|
2010
|
+
errors: errors.length > 0 ? errors : void 0
|
|
2011
|
+
};
|
|
2012
|
+
}
|
|
2013
|
+
var init_browser_fill_form = __esm({
|
|
2014
|
+
"src/tools/browser-fill-form.ts"() {
|
|
2015
|
+
"use strict";
|
|
2016
|
+
}
|
|
2017
|
+
});
|
|
2018
|
+
|
|
2019
|
+
// src/tools/browser-type.ts
|
|
2020
|
+
function parseRef2(ref) {
|
|
2021
|
+
const match = ref.match(/@e(\d+)/);
|
|
2022
|
+
if (!match) {
|
|
2023
|
+
throw new Error(`Invalid ref format: ${ref}`);
|
|
2024
|
+
}
|
|
2025
|
+
return parseInt(match[1], 10);
|
|
2026
|
+
}
|
|
2027
|
+
function getKeyCode2(key) {
|
|
2028
|
+
if (key.length === 1) {
|
|
2029
|
+
return key.toUpperCase().charCodeAt(0);
|
|
2030
|
+
}
|
|
2031
|
+
const codes = {
|
|
2032
|
+
Enter: 13,
|
|
2033
|
+
Tab: 9,
|
|
2034
|
+
Backspace: 8
|
|
2035
|
+
};
|
|
2036
|
+
return codes[key] ?? 0;
|
|
2037
|
+
}
|
|
2038
|
+
function getCode2(key) {
|
|
2039
|
+
if (key.length === 1) {
|
|
2040
|
+
const upper = key.toUpperCase();
|
|
2041
|
+
if (upper >= "A" && upper <= "Z") return `Key${upper}`;
|
|
2042
|
+
if (upper >= "0" && upper <= "9") return `Digit${upper}`;
|
|
2043
|
+
}
|
|
2044
|
+
return key;
|
|
2045
|
+
}
|
|
2046
|
+
function delay3(ms) {
|
|
2047
|
+
return new Promise((resolve2) => setTimeout(resolve2, ms));
|
|
2048
|
+
}
|
|
2049
|
+
async function dispatchCharEvents(cdp, char) {
|
|
2050
|
+
const keyCode = getKeyCode2(char);
|
|
2051
|
+
const code = getCode2(char);
|
|
2052
|
+
await cdp.send("Input.dispatchKeyEvent", {
|
|
2053
|
+
type: "keyDown",
|
|
2054
|
+
key: char,
|
|
2055
|
+
code,
|
|
2056
|
+
windowsVirtualKeyCode: keyCode,
|
|
2057
|
+
text: char
|
|
2058
|
+
});
|
|
2059
|
+
await cdp.send("Input.dispatchKeyEvent", {
|
|
2060
|
+
type: "char",
|
|
2061
|
+
key: char,
|
|
2062
|
+
code,
|
|
2063
|
+
windowsVirtualKeyCode: keyCode,
|
|
2064
|
+
text: char
|
|
2065
|
+
});
|
|
2066
|
+
await cdp.send("Input.dispatchKeyEvent", {
|
|
2067
|
+
type: "keyUp",
|
|
2068
|
+
key: char,
|
|
2069
|
+
code,
|
|
2070
|
+
windowsVirtualKeyCode: keyCode
|
|
2071
|
+
});
|
|
2072
|
+
}
|
|
2073
|
+
async function browserType(cdp, params) {
|
|
2074
|
+
if (params.ref) {
|
|
2075
|
+
const backendNodeId = parseRef2(params.ref);
|
|
2076
|
+
await cdp.send("DOM.focus", {
|
|
2077
|
+
backendNodeId
|
|
2078
|
+
});
|
|
2079
|
+
}
|
|
2080
|
+
if (!params.ref && params.selector) {
|
|
2081
|
+
const evalResult = await cdp.send("Runtime.evaluate", {
|
|
2082
|
+
expression: `document.querySelector(${JSON.stringify(params.selector)})`,
|
|
2083
|
+
returnByValue: false
|
|
2084
|
+
});
|
|
2085
|
+
if (evalResult.result.objectId) {
|
|
2086
|
+
await cdp.send("Runtime.callFunctionOn", {
|
|
2087
|
+
objectId: evalResult.result.objectId,
|
|
2088
|
+
functionDeclaration: `function() { this.focus(); }`,
|
|
2089
|
+
returnByValue: true
|
|
2090
|
+
});
|
|
2091
|
+
}
|
|
2092
|
+
}
|
|
2093
|
+
if (params.slowly) {
|
|
2094
|
+
for (let i = 0; i < params.text.length; i++) {
|
|
2095
|
+
await dispatchCharEvents(cdp, params.text[i]);
|
|
2096
|
+
if (i < params.text.length - 1) {
|
|
2097
|
+
await delay3(50);
|
|
2098
|
+
}
|
|
2099
|
+
}
|
|
2100
|
+
} else {
|
|
2101
|
+
await cdp.send("Input.insertText", {
|
|
2102
|
+
text: params.text
|
|
2103
|
+
});
|
|
2104
|
+
}
|
|
2105
|
+
if (params.submit) {
|
|
2106
|
+
await cdp.send("Input.dispatchKeyEvent", {
|
|
2107
|
+
type: "keyDown",
|
|
2108
|
+
key: "Enter",
|
|
2109
|
+
code: "Enter",
|
|
2110
|
+
windowsVirtualKeyCode: 13
|
|
2111
|
+
});
|
|
2112
|
+
await cdp.send("Input.dispatchKeyEvent", {
|
|
2113
|
+
type: "keyUp",
|
|
2114
|
+
key: "Enter",
|
|
2115
|
+
code: "Enter",
|
|
2116
|
+
windowsVirtualKeyCode: 13
|
|
2117
|
+
});
|
|
2118
|
+
}
|
|
2119
|
+
return { success: true };
|
|
2120
|
+
}
|
|
2121
|
+
var init_browser_type = __esm({
|
|
2122
|
+
"src/tools/browser-type.ts"() {
|
|
2123
|
+
"use strict";
|
|
2124
|
+
}
|
|
2125
|
+
});
|
|
2126
|
+
|
|
2127
|
+
// src/tools/browser-select-option.ts
|
|
2128
|
+
function refToBackendNodeId(ref) {
|
|
2129
|
+
const match = /^@?e(\d+)$/.exec(ref);
|
|
2130
|
+
if (!match) {
|
|
2131
|
+
throw new Error(`Invalid ref format: ${ref}`);
|
|
2132
|
+
}
|
|
2133
|
+
return parseInt(match[1], 10);
|
|
2134
|
+
}
|
|
2135
|
+
async function browserSelectOption(cdp, params) {
|
|
2136
|
+
const backendNodeId = refToBackendNodeId(params.ref);
|
|
2137
|
+
const resolveResponse = await cdp.send("DOM.resolveNode", {
|
|
2138
|
+
backendNodeId
|
|
2139
|
+
});
|
|
2140
|
+
const objectId = resolveResponse.object.objectId;
|
|
2141
|
+
const valuesJson = JSON.stringify(params.values);
|
|
2142
|
+
const result = await cdp.send("Runtime.callFunctionOn", {
|
|
2143
|
+
objectId,
|
|
2144
|
+
functionDeclaration: `function() {
|
|
2145
|
+
var select = this;
|
|
2146
|
+
if (select.tagName !== 'SELECT') {
|
|
2147
|
+
throw new Error('Element is not a SELECT');
|
|
2148
|
+
}
|
|
2149
|
+
var values = ${valuesJson};
|
|
2150
|
+
var matched = [];
|
|
2151
|
+
|
|
2152
|
+
for (var i = 0; i < select.options.length; i++) {
|
|
2153
|
+
var option = select.options[i];
|
|
2154
|
+
var isMatch = values.indexOf(option.value) !== -1 ||
|
|
2155
|
+
values.indexOf(option.textContent.trim()) !== -1;
|
|
2156
|
+
option.selected = isMatch;
|
|
2157
|
+
if (isMatch) {
|
|
2158
|
+
matched.push(option.value);
|
|
2159
|
+
}
|
|
2160
|
+
}
|
|
2161
|
+
|
|
2162
|
+
select.dispatchEvent(new Event('input', { bubbles: true }));
|
|
2163
|
+
select.dispatchEvent(new Event('change', { bubbles: true }));
|
|
2164
|
+
|
|
2165
|
+
return matched;
|
|
2166
|
+
}`,
|
|
2167
|
+
returnByValue: true
|
|
2168
|
+
});
|
|
2169
|
+
const selected = result.result.value ?? [];
|
|
2170
|
+
return {
|
|
2171
|
+
success: true,
|
|
2172
|
+
selected
|
|
2173
|
+
};
|
|
2174
|
+
}
|
|
2175
|
+
var init_browser_select_option = __esm({
|
|
2176
|
+
"src/tools/browser-select-option.ts"() {
|
|
2177
|
+
"use strict";
|
|
2178
|
+
}
|
|
2179
|
+
});
|
|
2180
|
+
|
|
2181
|
+
// src/tools/browser-wait-for.ts
|
|
2182
|
+
function normalizeTimeoutMs(timeout) {
|
|
2183
|
+
if (timeout > 60) {
|
|
2184
|
+
return timeout;
|
|
2185
|
+
}
|
|
2186
|
+
return timeout * 1e3;
|
|
2187
|
+
}
|
|
2188
|
+
async function browserWaitFor(cdp, params) {
|
|
2189
|
+
const timeoutMs = normalizeTimeoutMs(params.timeout ?? DEFAULT_TIMEOUT_S);
|
|
2190
|
+
const start = Date.now();
|
|
2191
|
+
if (params.time !== void 0) {
|
|
2192
|
+
const delayMs = params.time * 1e3;
|
|
2193
|
+
await delay4(delayMs);
|
|
2194
|
+
return { success: true, elapsed: Date.now() - start };
|
|
2195
|
+
}
|
|
2196
|
+
const condition = buildCondition(params);
|
|
2197
|
+
while (true) {
|
|
2198
|
+
const elapsed = Date.now() - start;
|
|
2199
|
+
if (elapsed >= timeoutMs) {
|
|
2200
|
+
throw new Error(
|
|
2201
|
+
`Timeout after ${timeoutMs}ms waiting for condition: ${describeCondition(params)}`
|
|
2202
|
+
);
|
|
2203
|
+
}
|
|
2204
|
+
let met = false;
|
|
2205
|
+
try {
|
|
2206
|
+
met = await evaluateCondition(cdp, condition);
|
|
2207
|
+
} catch {
|
|
2208
|
+
}
|
|
2209
|
+
if (met) {
|
|
2210
|
+
return { success: true, elapsed: Date.now() - start };
|
|
2211
|
+
}
|
|
2212
|
+
await delay4(POLL_INTERVAL_MS2);
|
|
2213
|
+
}
|
|
2214
|
+
}
|
|
2215
|
+
function buildCondition(params) {
|
|
2216
|
+
if (params.url !== void 0) {
|
|
2217
|
+
return { kind: "url", expression: params.url };
|
|
2218
|
+
}
|
|
2219
|
+
if (params.fn !== void 0) {
|
|
2220
|
+
return {
|
|
2221
|
+
kind: "fn",
|
|
2222
|
+
expression: `Boolean(${params.fn})`
|
|
2223
|
+
};
|
|
2224
|
+
}
|
|
2225
|
+
if (params.selector !== void 0 && params.state === "hidden") {
|
|
2226
|
+
return {
|
|
2227
|
+
kind: "selectorHidden",
|
|
2228
|
+
expression: buildVisibilityCheck(params.selector)
|
|
2229
|
+
};
|
|
2230
|
+
}
|
|
2231
|
+
if (params.selector !== void 0 && params.visible) {
|
|
2232
|
+
return {
|
|
2233
|
+
kind: "selectorVisible",
|
|
2234
|
+
expression: buildVisibilityCheck(params.selector)
|
|
2235
|
+
};
|
|
2236
|
+
}
|
|
2237
|
+
if (params.selector !== void 0) {
|
|
2238
|
+
return {
|
|
2239
|
+
kind: "selector",
|
|
2240
|
+
expression: `document.querySelector(${JSON.stringify(params.selector)})`
|
|
2241
|
+
};
|
|
2242
|
+
}
|
|
2243
|
+
if (params.text !== void 0) {
|
|
2244
|
+
return {
|
|
2245
|
+
kind: "text",
|
|
2246
|
+
expression: `document.body && document.body.innerText.includes(${JSON.stringify(params.text)})`
|
|
2247
|
+
};
|
|
2248
|
+
}
|
|
2249
|
+
if (params.textGone !== void 0) {
|
|
2250
|
+
return {
|
|
2251
|
+
kind: "textGone",
|
|
2252
|
+
expression: `document.body && !document.body.innerText.includes(${JSON.stringify(params.textGone)})`
|
|
2253
|
+
};
|
|
2254
|
+
}
|
|
2255
|
+
if (params.networkIdle) {
|
|
2256
|
+
return {
|
|
2257
|
+
kind: "networkIdle",
|
|
2258
|
+
expression: "true"
|
|
2259
|
+
// Simplified: check via Runtime.evaluate
|
|
2260
|
+
};
|
|
2261
|
+
}
|
|
2262
|
+
if (params.loadState !== void 0) {
|
|
2263
|
+
return {
|
|
2264
|
+
kind: "loadState",
|
|
2265
|
+
expression: params.loadState
|
|
2266
|
+
};
|
|
2267
|
+
}
|
|
2268
|
+
if (params.load) {
|
|
2269
|
+
return {
|
|
2270
|
+
kind: "load",
|
|
2271
|
+
expression: "document.readyState"
|
|
2272
|
+
};
|
|
2273
|
+
}
|
|
2274
|
+
throw new Error("browserWaitFor: no wait condition specified");
|
|
2275
|
+
}
|
|
2276
|
+
function buildVisibilityCheck(selector) {
|
|
2277
|
+
const sel = JSON.stringify(selector);
|
|
2278
|
+
return `(function() {
|
|
2279
|
+
var el = document.querySelector(${sel});
|
|
2280
|
+
if (!el) return false;
|
|
2281
|
+
var style = window.getComputedStyle(el);
|
|
2282
|
+
return style.display !== 'none' && style.visibility !== 'hidden' && el.offsetParent !== null;
|
|
2283
|
+
})()`;
|
|
2284
|
+
}
|
|
2285
|
+
async function evaluateCondition(cdp, condition) {
|
|
2286
|
+
if (condition.kind === "url") {
|
|
2287
|
+
return evaluateUrlCondition(cdp, condition.expression);
|
|
2288
|
+
}
|
|
2289
|
+
if (condition.kind === "load") {
|
|
2290
|
+
return evaluateLoadCondition(cdp, condition.expression);
|
|
2291
|
+
}
|
|
2292
|
+
if (condition.kind === "loadState") {
|
|
2293
|
+
return evaluateLoadStateCondition(cdp, condition.expression);
|
|
2294
|
+
}
|
|
2295
|
+
if (condition.kind === "selectorHidden") {
|
|
2296
|
+
const response2 = await cdp.send("Runtime.evaluate", {
|
|
2297
|
+
expression: condition.expression,
|
|
2298
|
+
returnByValue: true
|
|
2299
|
+
});
|
|
2300
|
+
return response2.result.value === false;
|
|
2301
|
+
}
|
|
2302
|
+
if (condition.kind === "selector") {
|
|
2303
|
+
const response2 = await cdp.send("Runtime.evaluate", {
|
|
2304
|
+
expression: condition.expression,
|
|
2305
|
+
returnByValue: true
|
|
2306
|
+
});
|
|
2307
|
+
return response2.result.value !== null && response2.result.subtype !== "null";
|
|
2308
|
+
}
|
|
2309
|
+
const response = await cdp.send("Runtime.evaluate", {
|
|
2310
|
+
expression: condition.expression,
|
|
2311
|
+
returnByValue: true
|
|
2312
|
+
});
|
|
2313
|
+
return response.result.value === true;
|
|
2314
|
+
}
|
|
2315
|
+
async function evaluateLoadCondition(cdp, expression) {
|
|
2316
|
+
const response = await cdp.send("Runtime.evaluate", {
|
|
2317
|
+
expression,
|
|
2318
|
+
returnByValue: true
|
|
2319
|
+
});
|
|
2320
|
+
if (response.result.type === "string") {
|
|
2321
|
+
return response.result.value === "complete";
|
|
2322
|
+
}
|
|
2323
|
+
return response.result.value === true;
|
|
2324
|
+
}
|
|
2325
|
+
async function evaluateLoadStateCondition(cdp, targetState) {
|
|
2326
|
+
const response = await cdp.send("Runtime.evaluate", {
|
|
2327
|
+
expression: "document.readyState",
|
|
2328
|
+
returnByValue: true
|
|
2329
|
+
});
|
|
2330
|
+
const current = response.result.value;
|
|
2331
|
+
if (targetState === "complete") {
|
|
2332
|
+
return current === "complete";
|
|
2333
|
+
}
|
|
2334
|
+
if (targetState === "interactive") {
|
|
2335
|
+
return current === "interactive" || current === "complete";
|
|
2336
|
+
}
|
|
2337
|
+
return current === targetState;
|
|
2338
|
+
}
|
|
2339
|
+
async function evaluateUrlCondition(cdp, pattern) {
|
|
2340
|
+
const response = await cdp.send("Runtime.evaluate", {
|
|
2341
|
+
expression: "location.href",
|
|
2342
|
+
returnByValue: true
|
|
2343
|
+
});
|
|
2344
|
+
const currentUrl = response.result.value;
|
|
2345
|
+
return globMatch(pattern, currentUrl);
|
|
2346
|
+
}
|
|
2347
|
+
function globMatch(pattern, value) {
|
|
2348
|
+
const regexStr = pattern.replace(/[.+^${}()|[\]\\]/g, "\\$&").replace(/\*\*/g, "\0").replace(/\*/g, "[^/]*").replace(/\u0000/g, ".*").replace(/\?/g, ".");
|
|
2349
|
+
const regex = new RegExp(regexStr);
|
|
2350
|
+
return regex.test(value);
|
|
2351
|
+
}
|
|
2352
|
+
function describeCondition(params) {
|
|
2353
|
+
if (params.text) return `text "${params.text}" to appear`;
|
|
2354
|
+
if (params.textGone) return `text "${params.textGone}" to disappear`;
|
|
2355
|
+
if (params.selector && params.state === "hidden")
|
|
2356
|
+
return `selector "${params.selector}" to become hidden`;
|
|
2357
|
+
if (params.selector && params.visible)
|
|
2358
|
+
return `selector "${params.selector}" to become visible`;
|
|
2359
|
+
if (params.selector) return `selector "${params.selector}" to appear`;
|
|
2360
|
+
if (params.url) return `URL matching "${params.url}"`;
|
|
2361
|
+
if (params.fn) return `JS condition: ${params.fn}`;
|
|
2362
|
+
if (params.loadState) return `document.readyState === "${params.loadState}"`;
|
|
2363
|
+
if (params.networkIdle) return "network idle";
|
|
2364
|
+
if (params.load) return "page load complete";
|
|
2365
|
+
return "unknown condition";
|
|
2366
|
+
}
|
|
2367
|
+
function delay4(ms) {
|
|
2368
|
+
return new Promise((resolve2) => setTimeout(resolve2, ms));
|
|
2369
|
+
}
|
|
2370
|
+
var DEFAULT_TIMEOUT_S, POLL_INTERVAL_MS2;
|
|
2371
|
+
var init_browser_wait_for = __esm({
|
|
2372
|
+
"src/tools/browser-wait-for.ts"() {
|
|
2373
|
+
"use strict";
|
|
2374
|
+
DEFAULT_TIMEOUT_S = 30;
|
|
2375
|
+
POLL_INTERVAL_MS2 = 100;
|
|
2376
|
+
}
|
|
2377
|
+
});
|
|
2378
|
+
|
|
2379
|
+
// src/tools/browser-drag.ts
|
|
2380
|
+
function calculateCenter3(content) {
|
|
2381
|
+
const x = (content[0] + content[2] + content[4] + content[6]) / 4;
|
|
2382
|
+
const y = (content[1] + content[3] + content[5] + content[7]) / 4;
|
|
2383
|
+
return { x: Math.round(x), y: Math.round(y) };
|
|
2384
|
+
}
|
|
2385
|
+
async function resolveRefCoordinates(cdp, ref) {
|
|
2386
|
+
const match = REF_PATTERN3.exec(ref);
|
|
2387
|
+
if (!match) {
|
|
2388
|
+
throw new Error(`Invalid ref format: ${ref}`);
|
|
2389
|
+
}
|
|
2390
|
+
const backendNodeId = parseInt(match[1], 10);
|
|
2391
|
+
const opts = { timeout: 5e3 };
|
|
2392
|
+
await cdp.send("DOM.resolveNode", {
|
|
2393
|
+
backendNodeId
|
|
2394
|
+
}, opts);
|
|
2395
|
+
try {
|
|
2396
|
+
await cdp.send("DOM.scrollIntoViewIfNeeded", {
|
|
2397
|
+
backendNodeId
|
|
2398
|
+
}, opts);
|
|
2399
|
+
} catch {
|
|
2400
|
+
}
|
|
2401
|
+
const boxResponse = await cdp.send("DOM.getBoxModel", {
|
|
2402
|
+
backendNodeId
|
|
2403
|
+
}, opts);
|
|
2404
|
+
return calculateCenter3(boxResponse.model.content);
|
|
2405
|
+
}
|
|
2406
|
+
function delay5(ms) {
|
|
2407
|
+
return new Promise((resolve2) => setTimeout(resolve2, ms));
|
|
2408
|
+
}
|
|
2409
|
+
function interpolatePoints(startX, startY, endX, endY, steps = 8) {
|
|
2410
|
+
const points = [];
|
|
2411
|
+
for (let i = 1; i < steps; i++) {
|
|
2412
|
+
const t = i / steps;
|
|
2413
|
+
points.push({
|
|
2414
|
+
x: Math.round(startX + (endX - startX) * t),
|
|
2415
|
+
y: Math.round(startY + (endY - startY) * t)
|
|
2416
|
+
});
|
|
2417
|
+
}
|
|
2418
|
+
return points;
|
|
2419
|
+
}
|
|
2420
|
+
async function browserDrag(cdp, params) {
|
|
2421
|
+
let startX;
|
|
2422
|
+
let startY;
|
|
2423
|
+
if (params.startX !== void 0 && params.startY !== void 0) {
|
|
2424
|
+
startX = params.startX;
|
|
2425
|
+
startY = params.startY;
|
|
2426
|
+
} else if (params.startRef) {
|
|
2427
|
+
const coords = await resolveRefCoordinates(cdp, params.startRef);
|
|
2428
|
+
startX = coords.x;
|
|
2429
|
+
startY = coords.y;
|
|
2430
|
+
} else {
|
|
2431
|
+
throw new Error("Either startRef or startX/startY must be provided");
|
|
2432
|
+
}
|
|
2433
|
+
let endX;
|
|
2434
|
+
let endY;
|
|
2435
|
+
if (params.endX !== void 0 && params.endY !== void 0) {
|
|
2436
|
+
endX = params.endX;
|
|
2437
|
+
endY = params.endY;
|
|
2438
|
+
} else if (params.endRef) {
|
|
2439
|
+
const coords = await resolveRefCoordinates(cdp, params.endRef);
|
|
2440
|
+
endX = coords.x;
|
|
2441
|
+
endY = coords.y;
|
|
2442
|
+
} else {
|
|
2443
|
+
throw new Error("Either endRef or endX/endY must be provided");
|
|
2444
|
+
}
|
|
2445
|
+
const mouseOpts = { timeout: 3e3 };
|
|
2446
|
+
await cdp.send("Input.dispatchMouseEvent", {
|
|
2447
|
+
type: "mouseMoved",
|
|
2448
|
+
x: startX,
|
|
2449
|
+
y: startY
|
|
2450
|
+
}, mouseOpts);
|
|
2451
|
+
await cdp.send("Input.dispatchMouseEvent", {
|
|
2452
|
+
type: "mousePressed",
|
|
2453
|
+
x: startX,
|
|
2454
|
+
y: startY,
|
|
2455
|
+
button: "left",
|
|
2456
|
+
clickCount: 1
|
|
2457
|
+
}, mouseOpts);
|
|
2458
|
+
const intermediatePoints = interpolatePoints(startX, startY, endX, endY, 4);
|
|
2459
|
+
for (const point of intermediatePoints) {
|
|
2460
|
+
await cdp.send("Input.dispatchMouseEvent", {
|
|
2461
|
+
type: "mouseMoved",
|
|
2462
|
+
x: point.x,
|
|
2463
|
+
y: point.y
|
|
2464
|
+
}, mouseOpts);
|
|
2465
|
+
await delay5(10);
|
|
2466
|
+
}
|
|
2467
|
+
await cdp.send("Input.dispatchMouseEvent", {
|
|
2468
|
+
type: "mouseMoved",
|
|
2469
|
+
x: endX,
|
|
2470
|
+
y: endY
|
|
2471
|
+
}, mouseOpts);
|
|
2472
|
+
await cdp.send("Input.dispatchMouseEvent", {
|
|
2473
|
+
type: "mouseReleased",
|
|
2474
|
+
x: endX,
|
|
2475
|
+
y: endY,
|
|
2476
|
+
button: "left",
|
|
2477
|
+
clickCount: 1
|
|
2478
|
+
}, mouseOpts);
|
|
2479
|
+
return { success: true };
|
|
2480
|
+
}
|
|
2481
|
+
var REF_PATTERN3;
|
|
2482
|
+
var init_browser_drag = __esm({
|
|
2483
|
+
"src/tools/browser-drag.ts"() {
|
|
2484
|
+
"use strict";
|
|
2485
|
+
REF_PATTERN3 = /^@?e(\d+)$/;
|
|
2486
|
+
}
|
|
2487
|
+
});
|
|
2488
|
+
|
|
2489
|
+
// src/tools/browser-handle-dialog.ts
|
|
2490
|
+
async function browserHandleDialog(cdp, params) {
|
|
2491
|
+
const dialogParams = {
|
|
2492
|
+
accept: params.accept
|
|
2493
|
+
};
|
|
2494
|
+
if (params.promptText !== void 0) {
|
|
2495
|
+
dialogParams.promptText = params.promptText;
|
|
2496
|
+
}
|
|
2497
|
+
try {
|
|
2498
|
+
await cdp.send("Page.handleJavaScriptDialog", dialogParams);
|
|
2499
|
+
return { success: true };
|
|
2500
|
+
} catch (err) {
|
|
2501
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
2502
|
+
if (message.includes("No dialog is showing") || message.includes("no dialog")) {
|
|
2503
|
+
return {
|
|
2504
|
+
success: false,
|
|
2505
|
+
error: "No JavaScript dialog is currently pending. A dialog must be open before it can be handled."
|
|
2506
|
+
};
|
|
2507
|
+
}
|
|
2508
|
+
return {
|
|
2509
|
+
success: false,
|
|
2510
|
+
error: `Failed to handle dialog: ${message}`
|
|
2511
|
+
};
|
|
2512
|
+
}
|
|
2513
|
+
}
|
|
2514
|
+
var init_browser_handle_dialog = __esm({
|
|
2515
|
+
"src/tools/browser-handle-dialog.ts"() {
|
|
2516
|
+
"use strict";
|
|
2517
|
+
}
|
|
2518
|
+
});
|
|
2519
|
+
|
|
2520
|
+
// src/tools/browser-file-upload.ts
|
|
2521
|
+
async function browserFileUpload(cdp, params) {
|
|
2522
|
+
const match = REF_PATTERN4.exec(params.ref);
|
|
2523
|
+
if (!match) {
|
|
2524
|
+
return {
|
|
2525
|
+
success: false,
|
|
2526
|
+
filesCount: 0,
|
|
2527
|
+
error: `Invalid ref format: ${params.ref}. Expected @eN pattern (e.g. @e5).`
|
|
2528
|
+
};
|
|
2529
|
+
}
|
|
2530
|
+
const backendNodeId = parseInt(match[1], 10);
|
|
2531
|
+
const resolved = await cdp.send("DOM.resolveNode", {
|
|
2532
|
+
backendNodeId
|
|
2533
|
+
});
|
|
2534
|
+
const objectId = resolved.object.objectId;
|
|
2535
|
+
await cdp.send("DOM.setFileInputFiles", {
|
|
2536
|
+
files: params.paths,
|
|
2537
|
+
objectId
|
|
2538
|
+
});
|
|
2539
|
+
return {
|
|
2540
|
+
success: true,
|
|
2541
|
+
filesCount: params.paths.length
|
|
2542
|
+
};
|
|
2543
|
+
}
|
|
2544
|
+
var REF_PATTERN4;
|
|
2545
|
+
var init_browser_file_upload = __esm({
|
|
2546
|
+
"src/tools/browser-file-upload.ts"() {
|
|
2547
|
+
"use strict";
|
|
2548
|
+
REF_PATTERN4 = /^@e(\d+)$/;
|
|
2549
|
+
}
|
|
2550
|
+
});
|
|
2551
|
+
|
|
2552
|
+
// src/event-buffer.ts
|
|
2553
|
+
var EventBuffer;
|
|
2554
|
+
var init_event_buffer = __esm({
|
|
2555
|
+
"src/event-buffer.ts"() {
|
|
2556
|
+
"use strict";
|
|
2557
|
+
EventBuffer = class {
|
|
2558
|
+
_buffer;
|
|
2559
|
+
_capacity;
|
|
2560
|
+
_head;
|
|
2561
|
+
// next write index
|
|
2562
|
+
_size;
|
|
2563
|
+
_totalPushed;
|
|
2564
|
+
constructor(capacity = 500) {
|
|
2565
|
+
this._capacity = capacity;
|
|
2566
|
+
this._buffer = new Array(capacity);
|
|
2567
|
+
this._head = 0;
|
|
2568
|
+
this._size = 0;
|
|
2569
|
+
this._totalPushed = 0;
|
|
2570
|
+
}
|
|
2571
|
+
/** Append an event, evicting the oldest if at capacity. */
|
|
2572
|
+
push(event) {
|
|
2573
|
+
this._buffer[this._head] = event;
|
|
2574
|
+
this._head = (this._head + 1) % this._capacity;
|
|
2575
|
+
if (this._size < this._capacity) {
|
|
2576
|
+
this._size++;
|
|
2577
|
+
}
|
|
2578
|
+
this._totalPushed++;
|
|
2579
|
+
}
|
|
2580
|
+
/**
|
|
2581
|
+
* Return the last `n` events in chronological (oldest-first) order.
|
|
2582
|
+
* Defaults to all events when `n` is omitted.
|
|
2583
|
+
*/
|
|
2584
|
+
last(n) {
|
|
2585
|
+
const count = n === void 0 ? this._size : Math.min(n, this._size);
|
|
2586
|
+
if (count === 0) return [];
|
|
2587
|
+
const result = new Array(count);
|
|
2588
|
+
let start = (this._head - count + this._capacity) % this._capacity;
|
|
2589
|
+
for (let i = 0; i < count; i++) {
|
|
2590
|
+
result[i] = this._buffer[(start + i) % this._capacity];
|
|
2591
|
+
}
|
|
2592
|
+
return result;
|
|
2593
|
+
}
|
|
2594
|
+
/** Remove all events from the buffer. */
|
|
2595
|
+
clear() {
|
|
2596
|
+
this._buffer = new Array(this._capacity);
|
|
2597
|
+
this._head = 0;
|
|
2598
|
+
this._size = 0;
|
|
2599
|
+
}
|
|
2600
|
+
/**
|
|
2601
|
+
* Return the last `n` events AND remove them from the buffer.
|
|
2602
|
+
* Defaults to all events when `n` is omitted.
|
|
2603
|
+
*/
|
|
2604
|
+
drain(n) {
|
|
2605
|
+
const events = this.last(n);
|
|
2606
|
+
this.clear();
|
|
2607
|
+
return events;
|
|
2608
|
+
}
|
|
2609
|
+
/** Current number of events stored. */
|
|
2610
|
+
get size() {
|
|
2611
|
+
return this._size;
|
|
2612
|
+
}
|
|
2613
|
+
/** Maximum number of events this buffer can hold. */
|
|
2614
|
+
get capacity() {
|
|
2615
|
+
return this._capacity;
|
|
2616
|
+
}
|
|
2617
|
+
/** Total number of events ever pushed (including evicted ones). */
|
|
2618
|
+
get totalPushed() {
|
|
2619
|
+
return this._totalPushed;
|
|
2620
|
+
}
|
|
2621
|
+
/** Return events matching `predicate` without modifying the buffer. */
|
|
2622
|
+
filter(predicate) {
|
|
2623
|
+
return this.last().filter(predicate);
|
|
2624
|
+
}
|
|
2625
|
+
/** Snapshot of buffer statistics. */
|
|
2626
|
+
get stats() {
|
|
2627
|
+
return {
|
|
2628
|
+
size: this._size,
|
|
2629
|
+
capacity: this._capacity,
|
|
2630
|
+
totalPushed: this._totalPushed,
|
|
2631
|
+
evicted: this._totalPushed - this._size
|
|
2632
|
+
};
|
|
2633
|
+
}
|
|
2634
|
+
};
|
|
2635
|
+
}
|
|
2636
|
+
});
|
|
2637
|
+
|
|
2638
|
+
// src/redactor.ts
|
|
2639
|
+
function redactInlineSecrets(text, opts) {
|
|
2640
|
+
if (opts?.enabled === false) {
|
|
2641
|
+
return text;
|
|
2642
|
+
}
|
|
2643
|
+
let result = text.replace(JWT_PATTERN, REDACTED_JWT);
|
|
2644
|
+
result = result.replace(BEARER_PATTERN, (match) => {
|
|
2645
|
+
const bearerWord = match.split(/\s+/)[0];
|
|
2646
|
+
return `${bearerWord} ${REDACTED}`;
|
|
2647
|
+
});
|
|
2648
|
+
return result;
|
|
2649
|
+
}
|
|
2650
|
+
var JWT_PATTERN, BEARER_PATTERN, REDACTED, REDACTED_JWT;
|
|
2651
|
+
var init_redactor = __esm({
|
|
2652
|
+
"src/redactor.ts"() {
|
|
2653
|
+
"use strict";
|
|
2654
|
+
JWT_PATTERN = /eyJ[A-Za-z0-9_-]{10,}\.[A-Za-z0-9_-]{10,}\.[A-Za-z0-9_-]+/g;
|
|
2655
|
+
BEARER_PATTERN = /Bearer\s+\S+/gi;
|
|
2656
|
+
REDACTED = "[REDACTED]";
|
|
2657
|
+
REDACTED_JWT = "[REDACTED_JWT]";
|
|
2658
|
+
}
|
|
2659
|
+
});
|
|
2660
|
+
|
|
2661
|
+
// src/tools/browser-network-requests.ts
|
|
2662
|
+
function setupNetworkCapture(cdp) {
|
|
2663
|
+
cdp.on("Network.requestWillBeSent", (params) => {
|
|
2664
|
+
const p = params;
|
|
2665
|
+
const entry = {
|
|
2666
|
+
requestId: p.requestId,
|
|
2667
|
+
url: p.request.url,
|
|
2668
|
+
method: p.request.method,
|
|
2669
|
+
type: p.type ?? "Other",
|
|
2670
|
+
timestamp: p.timestamp ? Math.floor(p.timestamp * 1e3) : Date.now()
|
|
2671
|
+
};
|
|
2672
|
+
pendingRequests.set(p.requestId, entry);
|
|
2673
|
+
networkBuffer.push(entry);
|
|
2674
|
+
});
|
|
2675
|
+
cdp.on("Network.responseReceived", (params) => {
|
|
2676
|
+
const p = params;
|
|
2677
|
+
const entry = pendingRequests.get(p.requestId);
|
|
2678
|
+
if (entry) {
|
|
2679
|
+
entry.status = p.response.status;
|
|
2680
|
+
pendingRequests.delete(p.requestId);
|
|
2681
|
+
}
|
|
2682
|
+
});
|
|
2683
|
+
}
|
|
2684
|
+
function resetNetworkBuffer() {
|
|
2685
|
+
networkBuffer.clear();
|
|
2686
|
+
pendingRequests.clear();
|
|
2687
|
+
}
|
|
2688
|
+
async function browserNetworkRequests(_cdp, params) {
|
|
2689
|
+
let entries = networkBuffer.last();
|
|
2690
|
+
if (!params.includeStatic) {
|
|
2691
|
+
entries = entries.filter((e) => !STATIC_TYPES.has(e.type));
|
|
2692
|
+
}
|
|
2693
|
+
if (params.filter) {
|
|
2694
|
+
const filterLower = params.filter.toLowerCase();
|
|
2695
|
+
entries = entries.filter((e) => e.url.toLowerCase().includes(filterLower));
|
|
2696
|
+
}
|
|
2697
|
+
const limit = params.limit ?? 100;
|
|
2698
|
+
entries = entries.slice(0, limit);
|
|
2699
|
+
const requests = entries.map((e) => ({
|
|
2700
|
+
url: redactInlineSecrets(e.url),
|
|
2701
|
+
method: e.method,
|
|
2702
|
+
status: e.status,
|
|
2703
|
+
type: e.type
|
|
2704
|
+
}));
|
|
2705
|
+
return { requests };
|
|
2706
|
+
}
|
|
2707
|
+
var STATIC_TYPES, networkBuffer, pendingRequests;
|
|
2708
|
+
var init_browser_network_requests = __esm({
|
|
2709
|
+
"src/tools/browser-network-requests.ts"() {
|
|
2710
|
+
"use strict";
|
|
2711
|
+
init_event_buffer();
|
|
2712
|
+
init_redactor();
|
|
2713
|
+
STATIC_TYPES = /* @__PURE__ */ new Set([
|
|
2714
|
+
"Image",
|
|
2715
|
+
"Stylesheet",
|
|
2716
|
+
"Font",
|
|
2717
|
+
"Script",
|
|
2718
|
+
"Media"
|
|
2719
|
+
]);
|
|
2720
|
+
networkBuffer = new EventBuffer(500);
|
|
2721
|
+
pendingRequests = /* @__PURE__ */ new Map();
|
|
2722
|
+
}
|
|
2723
|
+
});
|
|
2724
|
+
|
|
2725
|
+
// src/tools/browser-console-messages.ts
|
|
2726
|
+
function setupConsoleCapture(cdp) {
|
|
2727
|
+
cdp.on("Runtime.consoleAPICalled", (params) => {
|
|
2728
|
+
const p = params;
|
|
2729
|
+
if (!SUPPORTED_LEVELS.has(p.type)) return;
|
|
2730
|
+
const text = (p.args ?? []).map((arg) => {
|
|
2731
|
+
if (arg.type === "string") return String(arg.value);
|
|
2732
|
+
if (arg.type === "undefined") return "undefined";
|
|
2733
|
+
if (arg.value !== void 0) return String(arg.value);
|
|
2734
|
+
if (arg.description) return arg.description;
|
|
2735
|
+
return "";
|
|
2736
|
+
}).join(" ");
|
|
2737
|
+
consoleBuffer.push({
|
|
2738
|
+
level: p.type === "warning" ? "warn" : p.type,
|
|
2739
|
+
text,
|
|
2740
|
+
timestamp: p.timestamp ? Math.floor(p.timestamp) : Date.now()
|
|
2741
|
+
});
|
|
2742
|
+
});
|
|
2743
|
+
}
|
|
2744
|
+
function resetConsoleBuffer() {
|
|
2745
|
+
consoleBuffer.clear();
|
|
2746
|
+
}
|
|
2747
|
+
async function browserConsoleMessages(_cdp, params) {
|
|
2748
|
+
let messages = consoleBuffer.last();
|
|
2749
|
+
messages = messages.map((m) => ({ ...m, text: redactInlineSecrets(m.text) }));
|
|
2750
|
+
if (params.level) {
|
|
2751
|
+
messages = messages.filter((m) => m.level === params.level);
|
|
2752
|
+
}
|
|
2753
|
+
const limit = params.limit ?? 100;
|
|
2754
|
+
if (messages.length > limit) {
|
|
2755
|
+
messages = messages.slice(-limit);
|
|
2756
|
+
}
|
|
2757
|
+
return { messages };
|
|
2758
|
+
}
|
|
2759
|
+
var SUPPORTED_LEVELS, consoleBuffer;
|
|
2760
|
+
var init_browser_console_messages = __esm({
|
|
2761
|
+
"src/tools/browser-console-messages.ts"() {
|
|
2762
|
+
"use strict";
|
|
2763
|
+
init_event_buffer();
|
|
2764
|
+
init_redactor();
|
|
2765
|
+
SUPPORTED_LEVELS = /* @__PURE__ */ new Set(["log", "warn", "warning", "error", "info"]);
|
|
2766
|
+
consoleBuffer = new EventBuffer(500);
|
|
2767
|
+
}
|
|
2768
|
+
});
|
|
2769
|
+
|
|
2770
|
+
// src/tools/browser-annotated-screenshot.ts
|
|
2771
|
+
async function browserAnnotatedScreenshot(cdp, params) {
|
|
2772
|
+
const result = await browserScreenshot(cdp, {
|
|
2773
|
+
annotate: true,
|
|
2774
|
+
selector: params.selector
|
|
2775
|
+
});
|
|
2776
|
+
return {
|
|
2777
|
+
base64: result.base64,
|
|
2778
|
+
annotations: result.annotations ?? []
|
|
2779
|
+
};
|
|
2780
|
+
}
|
|
2781
|
+
var init_browser_annotated_screenshot = __esm({
|
|
2782
|
+
"src/tools/browser-annotated-screenshot.ts"() {
|
|
2783
|
+
"use strict";
|
|
2784
|
+
init_browser_screenshot();
|
|
2785
|
+
}
|
|
2786
|
+
});
|
|
2787
|
+
|
|
2788
|
+
// src/tools/browser-inspect-source.ts
|
|
2789
|
+
async function browserInspectSource(cdp, params) {
|
|
2790
|
+
let objectId;
|
|
2791
|
+
if (params.ref) {
|
|
2792
|
+
const match = REF_PATTERN5.exec(params.ref);
|
|
2793
|
+
if (!match) throw new Error(`Invalid ref format: ${params.ref}`);
|
|
2794
|
+
const backendNodeId = parseInt(match[1], 10);
|
|
2795
|
+
const resolved = await cdp.send("DOM.resolveNode", { backendNodeId });
|
|
2796
|
+
objectId = resolved.object.objectId;
|
|
2797
|
+
} else if (params.selector) {
|
|
2798
|
+
const evalResult = await cdp.send("Runtime.evaluate", {
|
|
2799
|
+
expression: `document.querySelector(${JSON.stringify(params.selector)})`,
|
|
2800
|
+
returnByValue: false
|
|
2801
|
+
});
|
|
2802
|
+
if (!evalResult.result.objectId || evalResult.result.subtype === "null") {
|
|
2803
|
+
throw new Error(`Element not found: ${params.selector}`);
|
|
2804
|
+
}
|
|
2805
|
+
objectId = evalResult.result.objectId;
|
|
2806
|
+
} else {
|
|
2807
|
+
throw new Error("Either ref or selector must be provided");
|
|
2808
|
+
}
|
|
2809
|
+
const cdpResult = await cdp.send("Runtime.callFunctionOn", {
|
|
2810
|
+
objectId,
|
|
2811
|
+
functionDeclaration: `function() {
|
|
2812
|
+
var el = this;
|
|
2813
|
+
var tagName = (el.tagName || '').toLowerCase();
|
|
2814
|
+
|
|
2815
|
+
// Find React Fiber
|
|
2816
|
+
var fiberKey = Object.keys(el).find(function(k) { return k.startsWith('__reactFiber'); });
|
|
2817
|
+
|
|
2818
|
+
// Also check Vue, Svelte
|
|
2819
|
+
var vueComp = el.__vueParentComponent;
|
|
2820
|
+
var svelteMeta = el.__svelte_meta;
|
|
2821
|
+
|
|
2822
|
+
if (!fiberKey && !vueComp && !svelteMeta) {
|
|
2823
|
+
return JSON.stringify({ tagName: tagName, componentName: null, source: null, stack: [], framework: null });
|
|
2824
|
+
}
|
|
2825
|
+
|
|
2826
|
+
// --- React path ---
|
|
2827
|
+
if (fiberKey) {
|
|
2828
|
+
var fiber = el[fiberKey];
|
|
2829
|
+
var stack = [];
|
|
2830
|
+
var current = fiber;
|
|
2831
|
+
var firstSource = null;
|
|
2832
|
+
var firstName = null;
|
|
2833
|
+
|
|
2834
|
+
while (current && stack.length < 15) {
|
|
2835
|
+
if (typeof current.type === 'function' && current.type.name) {
|
|
2836
|
+
var fn = current.type;
|
|
2837
|
+
var fnStr = fn.toString();
|
|
2838
|
+
var fileName = null;
|
|
2839
|
+
var lineNumber = null;
|
|
2840
|
+
var columnNumber = null;
|
|
2841
|
+
|
|
2842
|
+
// Parse jsxDEV calls for embedded fileName/lineNumber
|
|
2843
|
+
var fileMatch = fnStr.match(/fileName:\\s*"([^"]+)"/);
|
|
2844
|
+
if (fileMatch) {
|
|
2845
|
+
fileName = fileMatch[1];
|
|
2846
|
+
var lineMatch = fnStr.match(/lineNumber:\\s*(\\d+)/);
|
|
2847
|
+
if (lineMatch) lineNumber = parseInt(lineMatch[1]);
|
|
2848
|
+
var colMatch = fnStr.match(/columnNumber:\\s*(\\d+)/);
|
|
2849
|
+
if (colMatch) columnNumber = parseInt(colMatch[1]);
|
|
2850
|
+
}
|
|
2851
|
+
|
|
2852
|
+
var entry = {
|
|
2853
|
+
filePath: fileName,
|
|
2854
|
+
lineNumber: lineNumber,
|
|
2855
|
+
columnNumber: columnNumber,
|
|
2856
|
+
componentName: fn.name
|
|
2857
|
+
};
|
|
2858
|
+
|
|
2859
|
+
stack.push(entry);
|
|
2860
|
+
|
|
2861
|
+
if (fileName && !firstSource) {
|
|
2862
|
+
firstSource = entry;
|
|
2863
|
+
}
|
|
2864
|
+
if (!firstName && fn.name.length > 1) {
|
|
2865
|
+
firstName = fn.name;
|
|
2866
|
+
}
|
|
2867
|
+
}
|
|
2868
|
+
current = current.return;
|
|
2869
|
+
}
|
|
2870
|
+
|
|
2871
|
+
return JSON.stringify({
|
|
2872
|
+
tagName: tagName,
|
|
2873
|
+
componentName: firstName || null,
|
|
2874
|
+
source: firstSource || (stack.length > 0 ? { filePath: null, lineNumber: null, columnNumber: null, componentName: stack[0].componentName } : null),
|
|
2875
|
+
stack: stack.filter(function(s) { return s.filePath; }),
|
|
2876
|
+
framework: 'react'
|
|
2877
|
+
});
|
|
2878
|
+
}
|
|
2879
|
+
|
|
2880
|
+
// --- Svelte path ---
|
|
2881
|
+
if (svelteMeta) {
|
|
2882
|
+
var loc = svelteMeta.loc || {};
|
|
2883
|
+
return JSON.stringify({
|
|
2884
|
+
tagName: tagName,
|
|
2885
|
+
componentName: loc.char ? null : (svelteMeta.component || null),
|
|
2886
|
+
source: loc.file ? { filePath: loc.file, lineNumber: loc.line || null, columnNumber: (loc.column || 0) + 1, componentName: null } : null,
|
|
2887
|
+
stack: [],
|
|
2888
|
+
framework: 'svelte'
|
|
2889
|
+
});
|
|
2890
|
+
}
|
|
2891
|
+
|
|
2892
|
+
// --- Vue path ---
|
|
2893
|
+
if (vueComp) {
|
|
2894
|
+
var comp = vueComp;
|
|
2895
|
+
var vueName = comp.type?.__name || comp.type?.name || null;
|
|
2896
|
+
var vueFile = comp.type?.__file || null;
|
|
2897
|
+
return JSON.stringify({
|
|
2898
|
+
tagName: tagName,
|
|
2899
|
+
componentName: vueName,
|
|
2900
|
+
source: vueFile ? { filePath: vueFile, lineNumber: null, columnNumber: null, componentName: vueName } : null,
|
|
2901
|
+
stack: [],
|
|
2902
|
+
framework: 'vue'
|
|
2903
|
+
});
|
|
2904
|
+
}
|
|
2905
|
+
|
|
2906
|
+
return JSON.stringify({ tagName: tagName, componentName: null, source: null, stack: [], framework: null });
|
|
2907
|
+
}`,
|
|
2908
|
+
returnByValue: true
|
|
2909
|
+
});
|
|
2910
|
+
return JSON.parse(cdpResult.result.value);
|
|
2911
|
+
}
|
|
2912
|
+
var REF_PATTERN5;
|
|
2913
|
+
var init_browser_inspect_source = __esm({
|
|
2914
|
+
"src/tools/browser-inspect-source.ts"() {
|
|
2915
|
+
"use strict";
|
|
2916
|
+
REF_PATTERN5 = /^@?e(\d+)$/;
|
|
2917
|
+
}
|
|
2918
|
+
});
|
|
2919
|
+
|
|
2920
|
+
// src/tools/browser-intercept.ts
|
|
2921
|
+
function matchGlob(pattern, url) {
|
|
2922
|
+
const regex = pattern.replace(/[.+^${}()|[\]\\]/g, "\\$&").replace(/\*\*/g, "___DOUBLESTAR___").replace(/\*/g, "[^/]*").replace(/___DOUBLESTAR___/g, ".*");
|
|
2923
|
+
return new RegExp(`^${regex}$`).test(url);
|
|
2924
|
+
}
|
|
2925
|
+
async function syncFetchPatterns(cdp) {
|
|
2926
|
+
const patterns = [
|
|
2927
|
+
...Array.from(activeRoutes.keys()),
|
|
2928
|
+
...Array.from(activeAborts.keys())
|
|
2929
|
+
].map((p) => ({ urlPattern: p }));
|
|
2930
|
+
if (patterns.length === 0) {
|
|
2931
|
+
if (fetchEnabled) {
|
|
2932
|
+
await cdp.send("Fetch.disable");
|
|
2933
|
+
fetchEnabled = false;
|
|
2934
|
+
}
|
|
2935
|
+
return;
|
|
2936
|
+
}
|
|
2937
|
+
await cdp.send("Fetch.enable", { patterns });
|
|
2938
|
+
fetchEnabled = true;
|
|
2939
|
+
if (!handlerAttached) {
|
|
2940
|
+
cdp.on("Fetch.requestPaused", async (params) => {
|
|
2941
|
+
const url = params.request.url;
|
|
2942
|
+
const requestId = params.requestId;
|
|
2943
|
+
try {
|
|
2944
|
+
for (const [pattern] of activeAborts) {
|
|
2945
|
+
if (matchGlob(pattern, url)) {
|
|
2946
|
+
await cdp.send("Fetch.failRequest", {
|
|
2947
|
+
requestId,
|
|
2948
|
+
reason: "BlockedByClient"
|
|
2949
|
+
});
|
|
2950
|
+
return;
|
|
2951
|
+
}
|
|
2952
|
+
}
|
|
2953
|
+
for (const [pattern, rule] of activeRoutes) {
|
|
2954
|
+
if (matchGlob(pattern, url)) {
|
|
2955
|
+
const responseHeaders = Object.entries(rule.headers).map(
|
|
2956
|
+
([name, value]) => ({ name, value })
|
|
2957
|
+
);
|
|
2958
|
+
await cdp.send("Fetch.fulfillRequest", {
|
|
2959
|
+
requestId,
|
|
2960
|
+
responseCode: rule.status,
|
|
2961
|
+
body: btoa(rule.body),
|
|
2962
|
+
responseHeaders
|
|
2963
|
+
});
|
|
2964
|
+
return;
|
|
2965
|
+
}
|
|
2966
|
+
}
|
|
2967
|
+
await cdp.send("Fetch.continueRequest", { requestId });
|
|
2968
|
+
} catch {
|
|
2969
|
+
}
|
|
2970
|
+
});
|
|
2971
|
+
handlerAttached = true;
|
|
2972
|
+
}
|
|
2973
|
+
}
|
|
2974
|
+
async function browserRoute(cdp, params) {
|
|
2975
|
+
const body = typeof params.body === "object" && params.body !== null ? JSON.stringify(params.body) : String(params.body);
|
|
2976
|
+
const status = params.status ?? 200;
|
|
2977
|
+
const headers = params.headers ?? { "Content-Type": "application/json" };
|
|
2978
|
+
const rule = {
|
|
2979
|
+
urlPattern: params.url,
|
|
2980
|
+
body,
|
|
2981
|
+
status,
|
|
2982
|
+
headers
|
|
2983
|
+
};
|
|
2984
|
+
activeRoutes.set(params.url, rule);
|
|
2985
|
+
await syncFetchPatterns(cdp);
|
|
2986
|
+
return {
|
|
2987
|
+
url: params.url,
|
|
2988
|
+
status,
|
|
2989
|
+
activeRoutes: activeRoutes.size
|
|
2990
|
+
};
|
|
2991
|
+
}
|
|
2992
|
+
async function browserAbort(cdp, params) {
|
|
2993
|
+
const rule = {
|
|
2994
|
+
urlPattern: params.url
|
|
2995
|
+
};
|
|
2996
|
+
activeAborts.set(params.url, rule);
|
|
2997
|
+
await syncFetchPatterns(cdp);
|
|
2998
|
+
return {
|
|
2999
|
+
url: params.url,
|
|
3000
|
+
activeAborts: activeAborts.size
|
|
3001
|
+
};
|
|
3002
|
+
}
|
|
3003
|
+
async function browserUnroute(cdp, params) {
|
|
3004
|
+
let removed = 0;
|
|
3005
|
+
if (params.all) {
|
|
3006
|
+
removed = activeRoutes.size + activeAborts.size;
|
|
3007
|
+
activeRoutes.clear();
|
|
3008
|
+
activeAborts.clear();
|
|
3009
|
+
} else if (params.url) {
|
|
3010
|
+
if (activeRoutes.delete(params.url)) removed++;
|
|
3011
|
+
if (activeAborts.delete(params.url)) removed++;
|
|
3012
|
+
}
|
|
3013
|
+
await syncFetchPatterns(cdp);
|
|
3014
|
+
return {
|
|
3015
|
+
removed,
|
|
3016
|
+
remaining: activeRoutes.size + activeAborts.size
|
|
3017
|
+
};
|
|
3018
|
+
}
|
|
3019
|
+
var activeRoutes, activeAborts, fetchEnabled, handlerAttached;
|
|
3020
|
+
var init_browser_intercept = __esm({
|
|
3021
|
+
"src/tools/browser-intercept.ts"() {
|
|
3022
|
+
"use strict";
|
|
3023
|
+
activeRoutes = /* @__PURE__ */ new Map();
|
|
3024
|
+
activeAborts = /* @__PURE__ */ new Map();
|
|
3025
|
+
fetchEnabled = false;
|
|
3026
|
+
handlerAttached = false;
|
|
3027
|
+
}
|
|
3028
|
+
});
|
|
3029
|
+
|
|
3030
|
+
// src/tools/browser-find.ts
|
|
3031
|
+
async function browserFind(cdp, params) {
|
|
3032
|
+
const nth = params.nth ?? 0;
|
|
3033
|
+
const response = await cdp.send("Accessibility.getFullAXTree", void 0, {
|
|
3034
|
+
timeout: 1e4
|
|
3035
|
+
});
|
|
3036
|
+
const matches = response.nodes.filter((node) => {
|
|
3037
|
+
if (params.role) {
|
|
3038
|
+
const nodeRole = node.role?.value ?? "";
|
|
3039
|
+
if (nodeRole.toLowerCase() !== params.role.toLowerCase()) {
|
|
3040
|
+
return false;
|
|
3041
|
+
}
|
|
3042
|
+
}
|
|
3043
|
+
if (params.name) {
|
|
3044
|
+
const nodeName = node.name?.value ?? "";
|
|
3045
|
+
if (!nodeName.includes(params.name)) {
|
|
3046
|
+
return false;
|
|
3047
|
+
}
|
|
3048
|
+
}
|
|
3049
|
+
if (params.text) {
|
|
3050
|
+
const nodeName = node.name?.value ?? "";
|
|
3051
|
+
if (!nodeName.includes(params.text)) {
|
|
3052
|
+
return false;
|
|
3053
|
+
}
|
|
3054
|
+
}
|
|
3055
|
+
return true;
|
|
3056
|
+
});
|
|
3057
|
+
const count = matches.length;
|
|
3058
|
+
if (nth >= count || count === 0) {
|
|
3059
|
+
return {
|
|
3060
|
+
found: false,
|
|
3061
|
+
ref: null,
|
|
3062
|
+
role: null,
|
|
3063
|
+
name: null,
|
|
3064
|
+
count
|
|
3065
|
+
};
|
|
3066
|
+
}
|
|
3067
|
+
const match = matches[nth];
|
|
3068
|
+
const ref = match.backendDOMNodeId ? `@e${match.backendDOMNodeId}` : null;
|
|
3069
|
+
return {
|
|
3070
|
+
found: true,
|
|
3071
|
+
ref,
|
|
3072
|
+
role: match.role?.value ?? null,
|
|
3073
|
+
name: match.name?.value ?? null,
|
|
3074
|
+
count
|
|
3075
|
+
};
|
|
3076
|
+
}
|
|
3077
|
+
var init_browser_find = __esm({
|
|
3078
|
+
"src/tools/browser-find.ts"() {
|
|
3079
|
+
"use strict";
|
|
3080
|
+
}
|
|
3081
|
+
});
|
|
3082
|
+
|
|
3083
|
+
// src/tools/browser-diff.ts
|
|
3084
|
+
async function captureScreenshot(cdp, selector) {
|
|
3085
|
+
const captureParams = { format: "png" };
|
|
3086
|
+
if (selector) {
|
|
3087
|
+
const doc = await cdp.send("DOM.getDocument", {});
|
|
3088
|
+
const queryResult = await cdp.send("DOM.querySelector", {
|
|
3089
|
+
nodeId: doc.root.nodeId,
|
|
3090
|
+
selector
|
|
3091
|
+
});
|
|
3092
|
+
if (!queryResult.nodeId) {
|
|
3093
|
+
throw new Error(`Element not found: ${selector}`);
|
|
3094
|
+
}
|
|
3095
|
+
const boxModel = await cdp.send("DOM.getBoxModel", {
|
|
3096
|
+
nodeId: queryResult.nodeId
|
|
3097
|
+
});
|
|
3098
|
+
const content = boxModel.model.content;
|
|
3099
|
+
captureParams.clip = {
|
|
3100
|
+
x: content[0],
|
|
3101
|
+
y: content[1],
|
|
3102
|
+
width: content[2] - content[0],
|
|
3103
|
+
height: content[5] - content[1],
|
|
3104
|
+
scale: 1
|
|
3105
|
+
};
|
|
3106
|
+
}
|
|
3107
|
+
const screenshot = await cdp.send(
|
|
3108
|
+
"Page.captureScreenshot",
|
|
3109
|
+
captureParams
|
|
3110
|
+
);
|
|
3111
|
+
return screenshot.data;
|
|
3112
|
+
}
|
|
3113
|
+
function buildComparisonExpression(threshold) {
|
|
3114
|
+
return [
|
|
3115
|
+
"(async () => {",
|
|
3116
|
+
" const beforeSrc = window._diffBefore;",
|
|
3117
|
+
" const afterSrc = window._diffAfter;",
|
|
3118
|
+
"",
|
|
3119
|
+
" const loadImg = (src) => new Promise((res, rej) => {",
|
|
3120
|
+
" const img = new Image();",
|
|
3121
|
+
" img.onload = () => res(img);",
|
|
3122
|
+
" img.onerror = (e) => rej(new Error('Failed to load image'));",
|
|
3123
|
+
" img.src = src;",
|
|
3124
|
+
" });",
|
|
3125
|
+
"",
|
|
3126
|
+
" const img1 = await loadImg('data:image/png;base64,' + beforeSrc);",
|
|
3127
|
+
" const img2 = await loadImg('data:image/png;base64,' + afterSrc);",
|
|
3128
|
+
"",
|
|
3129
|
+
" const w = Math.max(img1.width, img2.width);",
|
|
3130
|
+
" const h = Math.max(img1.height, img2.height);",
|
|
3131
|
+
"",
|
|
3132
|
+
" const c1 = document.createElement('canvas');",
|
|
3133
|
+
" c1.width = w; c1.height = h;",
|
|
3134
|
+
" const ctx1 = c1.getContext('2d');",
|
|
3135
|
+
" ctx1.drawImage(img1, 0, 0);",
|
|
3136
|
+
"",
|
|
3137
|
+
" const c2 = document.createElement('canvas');",
|
|
3138
|
+
" c2.width = w; c2.height = h;",
|
|
3139
|
+
" const ctx2 = c2.getContext('2d');",
|
|
3140
|
+
" ctx2.drawImage(img2, 0, 0);",
|
|
3141
|
+
"",
|
|
3142
|
+
" const d1 = ctx1.getImageData(0, 0, w, h).data;",
|
|
3143
|
+
" const d2 = ctx2.getImageData(0, 0, w, h).data;",
|
|
3144
|
+
"",
|
|
3145
|
+
" const diff = document.createElement('canvas');",
|
|
3146
|
+
" diff.width = w; diff.height = h;",
|
|
3147
|
+
" const dCtx = diff.getContext('2d');",
|
|
3148
|
+
" dCtx.drawImage(img2, 0, 0);",
|
|
3149
|
+
" const dData = dCtx.getImageData(0, 0, w, h);",
|
|
3150
|
+
"",
|
|
3151
|
+
" let diffCount = 0;",
|
|
3152
|
+
" const threshold = " + threshold + ";",
|
|
3153
|
+
" for (let i = 0; i < d1.length; i += 4) {",
|
|
3154
|
+
" const dr = Math.abs(d1[i] - d2[i]);",
|
|
3155
|
+
" const dg = Math.abs(d1[i+1] - d2[i+1]);",
|
|
3156
|
+
" const db = Math.abs(d1[i+2] - d2[i+2]);",
|
|
3157
|
+
" if (dr > threshold || dg > threshold || db > threshold) {",
|
|
3158
|
+
" diffCount++;",
|
|
3159
|
+
" dData.data[i] = 255;",
|
|
3160
|
+
" dData.data[i+1] = 0;",
|
|
3161
|
+
" dData.data[i+2] = 0;",
|
|
3162
|
+
" dData.data[i+3] = 200;",
|
|
3163
|
+
" }",
|
|
3164
|
+
" }",
|
|
3165
|
+
"",
|
|
3166
|
+
" dCtx.putImageData(dData, 0, 0);",
|
|
3167
|
+
" const diffBase64 = diff.toDataURL('image/png').split(',')[1];",
|
|
3168
|
+
"",
|
|
3169
|
+
" const total = w * h;",
|
|
3170
|
+
" return JSON.stringify({",
|
|
3171
|
+
" diffPercentage: parseFloat((diffCount / total * 100).toFixed(4)),",
|
|
3172
|
+
" totalPixels: total,",
|
|
3173
|
+
" diffPixels: diffCount,",
|
|
3174
|
+
" identical: (diffCount / total) < 0.001,",
|
|
3175
|
+
" diffImage: diffBase64,",
|
|
3176
|
+
" width: w,",
|
|
3177
|
+
" height: h",
|
|
3178
|
+
" });",
|
|
3179
|
+
"})()"
|
|
3180
|
+
].join("\n");
|
|
3181
|
+
}
|
|
3182
|
+
async function browserDiff(cdp, params) {
|
|
3183
|
+
const threshold = params.threshold ?? 30;
|
|
3184
|
+
let beforeBase64;
|
|
3185
|
+
if (params.before === "current") {
|
|
3186
|
+
beforeBase64 = await captureScreenshot(cdp, params.selector);
|
|
3187
|
+
} else {
|
|
3188
|
+
beforeBase64 = params.before;
|
|
3189
|
+
}
|
|
3190
|
+
let afterBase64;
|
|
3191
|
+
if (!params.after || params.after === "current") {
|
|
3192
|
+
afterBase64 = await captureScreenshot(cdp, params.selector);
|
|
3193
|
+
} else {
|
|
3194
|
+
afterBase64 = params.after;
|
|
3195
|
+
}
|
|
3196
|
+
await cdp.send("Runtime.evaluate", {
|
|
3197
|
+
expression: "window._diffBefore = " + JSON.stringify(beforeBase64) + ";",
|
|
3198
|
+
returnByValue: true
|
|
3199
|
+
});
|
|
3200
|
+
await cdp.send("Runtime.evaluate", {
|
|
3201
|
+
expression: "window._diffAfter = " + JSON.stringify(afterBase64) + ";",
|
|
3202
|
+
returnByValue: true
|
|
3203
|
+
});
|
|
3204
|
+
const result = await cdp.send("Runtime.evaluate", {
|
|
3205
|
+
expression: buildComparisonExpression(threshold),
|
|
3206
|
+
awaitPromise: true,
|
|
3207
|
+
returnByValue: true
|
|
3208
|
+
});
|
|
3209
|
+
if (result.exceptionDetails) {
|
|
3210
|
+
throw new Error(
|
|
3211
|
+
"Diff comparison failed: " + result.exceptionDetails.text
|
|
3212
|
+
);
|
|
3213
|
+
}
|
|
3214
|
+
await cdp.send("Runtime.evaluate", {
|
|
3215
|
+
expression: "delete window._diffBefore; delete window._diffAfter;",
|
|
3216
|
+
returnByValue: true
|
|
3217
|
+
});
|
|
3218
|
+
return JSON.parse(result.result.value);
|
|
3219
|
+
}
|
|
3220
|
+
var init_browser_diff = __esm({
|
|
3221
|
+
"src/tools/browser-diff.ts"() {
|
|
3222
|
+
"use strict";
|
|
3223
|
+
}
|
|
3224
|
+
});
|
|
3225
|
+
|
|
3226
|
+
// src/tools/browser-session-state.ts
|
|
3227
|
+
import { writeFileSync, readFileSync as readFileSync2, existsSync as existsSync2, mkdirSync as mkdirSync2 } from "fs";
|
|
3228
|
+
import { join as join2 } from "path";
|
|
3229
|
+
import { homedir as homedir2 } from "os";
|
|
3230
|
+
function getStatesDir() {
|
|
3231
|
+
return join2(homedir2(), ".browsirai", "states");
|
|
3232
|
+
}
|
|
3233
|
+
function ensureStatesDir() {
|
|
3234
|
+
const dir = getStatesDir();
|
|
3235
|
+
if (!existsSync2(dir)) {
|
|
3236
|
+
mkdirSync2(dir, { recursive: true });
|
|
3237
|
+
}
|
|
3238
|
+
return dir;
|
|
3239
|
+
}
|
|
3240
|
+
function getStatePath(name) {
|
|
3241
|
+
return join2(getStatesDir(), `${name}.json`);
|
|
3242
|
+
}
|
|
3243
|
+
async function browserSaveState(cdp, params) {
|
|
3244
|
+
const { name } = params;
|
|
3245
|
+
const cookieResponse = await cdp.send("Network.getAllCookies");
|
|
3246
|
+
const cookies = cookieResponse.cookies ?? [];
|
|
3247
|
+
const urlResponse = await cdp.send("Runtime.evaluate", {
|
|
3248
|
+
expression: "window.location.href",
|
|
3249
|
+
returnByValue: true
|
|
3250
|
+
});
|
|
3251
|
+
const url = urlResponse.result.value ?? "";
|
|
3252
|
+
const localStorageResponse = await cdp.send("Runtime.evaluate", {
|
|
3253
|
+
expression: "JSON.stringify(Object.entries(localStorage))",
|
|
3254
|
+
returnByValue: true
|
|
3255
|
+
});
|
|
3256
|
+
const localStorageEntries = JSON.parse(
|
|
3257
|
+
localStorageResponse.result.value ?? "[]"
|
|
3258
|
+
);
|
|
3259
|
+
const localStorage = Object.fromEntries(localStorageEntries);
|
|
3260
|
+
const sessionStorageResponse = await cdp.send("Runtime.evaluate", {
|
|
3261
|
+
expression: "JSON.stringify(Object.entries(sessionStorage))",
|
|
3262
|
+
returnByValue: true
|
|
3263
|
+
});
|
|
3264
|
+
const sessionStorageEntries = JSON.parse(
|
|
3265
|
+
sessionStorageResponse.result.value ?? "[]"
|
|
3266
|
+
);
|
|
3267
|
+
const sessionStorage = Object.fromEntries(sessionStorageEntries);
|
|
3268
|
+
const dir = ensureStatesDir();
|
|
3269
|
+
const filePath = join2(dir, `${name}.json`);
|
|
3270
|
+
const stateFile = {
|
|
3271
|
+
version: 1,
|
|
3272
|
+
savedAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
3273
|
+
url,
|
|
3274
|
+
cookies,
|
|
3275
|
+
localStorage,
|
|
3276
|
+
sessionStorage
|
|
3277
|
+
};
|
|
3278
|
+
writeFileSync(filePath, JSON.stringify(stateFile, null, 2), "utf-8");
|
|
3279
|
+
return {
|
|
3280
|
+
name,
|
|
3281
|
+
path: filePath,
|
|
3282
|
+
cookies: cookies.length,
|
|
3283
|
+
localStorage: localStorageEntries.length,
|
|
3284
|
+
sessionStorage: sessionStorageEntries.length
|
|
3285
|
+
};
|
|
3286
|
+
}
|
|
3287
|
+
async function browserLoadState(cdp, params) {
|
|
3288
|
+
const { name, url: customUrl } = params;
|
|
3289
|
+
const filePath = getStatePath(name);
|
|
3290
|
+
if (!existsSync2(filePath)) {
|
|
3291
|
+
throw new Error(`State file not found: ${filePath}`);
|
|
3292
|
+
}
|
|
3293
|
+
const stateFile = JSON.parse(readFileSync2(filePath, "utf-8"));
|
|
3294
|
+
const targetUrl = customUrl ?? stateFile.url;
|
|
3295
|
+
if (targetUrl) {
|
|
3296
|
+
await cdp.send("Page.enable");
|
|
3297
|
+
await cdp.send("Page.navigate", { url: targetUrl });
|
|
3298
|
+
}
|
|
3299
|
+
if (stateFile.cookies.length > 0) {
|
|
3300
|
+
await cdp.send("Network.setCookies", { cookies: stateFile.cookies });
|
|
3301
|
+
}
|
|
3302
|
+
const localEntries = Object.entries(stateFile.localStorage);
|
|
3303
|
+
if (localEntries.length > 0) {
|
|
3304
|
+
const localScript = localEntries.map(([k, v]) => `localStorage.setItem(${JSON.stringify(k)}, ${JSON.stringify(v)})`).join(";");
|
|
3305
|
+
await cdp.send("Runtime.evaluate", {
|
|
3306
|
+
expression: localScript,
|
|
3307
|
+
returnByValue: true
|
|
3308
|
+
});
|
|
3309
|
+
}
|
|
3310
|
+
const sessionEntries = Object.entries(stateFile.sessionStorage);
|
|
3311
|
+
if (sessionEntries.length > 0) {
|
|
3312
|
+
const sessionScript = sessionEntries.map(([k, v]) => `sessionStorage.setItem(${JSON.stringify(k)}, ${JSON.stringify(v)})`).join(";");
|
|
3313
|
+
await cdp.send("Runtime.evaluate", {
|
|
3314
|
+
expression: sessionScript,
|
|
3315
|
+
returnByValue: true
|
|
3316
|
+
});
|
|
3317
|
+
}
|
|
3318
|
+
await cdp.send("Page.reload");
|
|
3319
|
+
return {
|
|
3320
|
+
name,
|
|
3321
|
+
cookies: stateFile.cookies.length,
|
|
3322
|
+
localStorage: localEntries.length,
|
|
3323
|
+
sessionStorage: sessionEntries.length
|
|
3324
|
+
};
|
|
3325
|
+
}
|
|
3326
|
+
var init_browser_session_state = __esm({
|
|
3327
|
+
"src/tools/browser-session-state.ts"() {
|
|
3328
|
+
"use strict";
|
|
3329
|
+
}
|
|
3330
|
+
});
|
|
3331
|
+
|
|
3332
|
+
// src/upgrade.ts
|
|
3333
|
+
var upgrade_exports = {};
|
|
3334
|
+
__export(upgrade_exports, {
|
|
3335
|
+
checkForUpgrade: () => checkForUpgrade,
|
|
3336
|
+
getInstallMethod: () => getInstallMethod,
|
|
3337
|
+
getInstallPath: () => getInstallPath,
|
|
3338
|
+
getUpgradeStatus: () => getUpgradeStatus
|
|
3339
|
+
});
|
|
3340
|
+
import { spawn as spawn2, execSync as execSync2 } from "child_process";
|
|
3341
|
+
import { existsSync as existsSync3, mkdirSync as mkdirSync3, readFileSync as readFileSync3, writeFileSync as writeFileSync2 } from "fs";
|
|
3342
|
+
import { homedir as homedir3 } from "os";
|
|
3343
|
+
import { join as join3, dirname } from "path";
|
|
3344
|
+
import { fileURLToPath } from "url";
|
|
3345
|
+
function getInstallMethod() {
|
|
3346
|
+
try {
|
|
3347
|
+
const scriptPath = fileURLToPath(import.meta.url);
|
|
3348
|
+
const parentDir = dirname(dirname(scriptPath));
|
|
3349
|
+
if (existsSync3(join3(parentDir, ".git")) || existsSync3(join3(parentDir, "tsconfig.json"))) {
|
|
3350
|
+
return "dev";
|
|
3351
|
+
}
|
|
3352
|
+
if (scriptPath.includes("_npx") || scriptPath.includes(".npm")) {
|
|
3353
|
+
return "npx";
|
|
3354
|
+
}
|
|
3355
|
+
try {
|
|
3356
|
+
const globalPrefix = execSync2("npm prefix -g", { stdio: "pipe" }).toString().trim();
|
|
3357
|
+
if (scriptPath.startsWith(globalPrefix)) {
|
|
3358
|
+
return "global";
|
|
3359
|
+
}
|
|
3360
|
+
} catch {
|
|
3361
|
+
}
|
|
3362
|
+
return "npx";
|
|
3363
|
+
} catch {
|
|
3364
|
+
return "npx";
|
|
3365
|
+
}
|
|
3366
|
+
}
|
|
3367
|
+
function getInstallPath() {
|
|
3368
|
+
try {
|
|
3369
|
+
return dirname(dirname(fileURLToPath(import.meta.url)));
|
|
3370
|
+
} catch {
|
|
3371
|
+
return "unknown";
|
|
3372
|
+
}
|
|
3373
|
+
}
|
|
3374
|
+
function isNewer(current, latest) {
|
|
3375
|
+
const a = current.split(".").map(Number);
|
|
3376
|
+
const b = latest.split(".").map(Number);
|
|
3377
|
+
for (let i = 0; i < 3; i++) {
|
|
3378
|
+
if ((b[i] ?? 0) > (a[i] ?? 0)) return true;
|
|
3379
|
+
if ((b[i] ?? 0) < (a[i] ?? 0)) return false;
|
|
3380
|
+
}
|
|
3381
|
+
return false;
|
|
3382
|
+
}
|
|
3383
|
+
function getUpgradeStatus() {
|
|
3384
|
+
try {
|
|
3385
|
+
if (!existsSync3(UPGRADE_FILE)) return null;
|
|
3386
|
+
return JSON.parse(readFileSync3(UPGRADE_FILE, "utf-8"));
|
|
3387
|
+
} catch {
|
|
3388
|
+
return null;
|
|
3389
|
+
}
|
|
3390
|
+
}
|
|
3391
|
+
function writeUpgradeStatus(status) {
|
|
3392
|
+
try {
|
|
3393
|
+
mkdirSync3(BROWSIR_DIR, { recursive: true });
|
|
3394
|
+
writeFileSync2(UPGRADE_FILE, JSON.stringify(status, null, 2));
|
|
3395
|
+
} catch {
|
|
3396
|
+
}
|
|
3397
|
+
}
|
|
3398
|
+
async function checkForUpgrade() {
|
|
3399
|
+
try {
|
|
3400
|
+
const method = getInstallMethod();
|
|
3401
|
+
if (method === "dev") return null;
|
|
3402
|
+
const cached = getUpgradeStatus();
|
|
3403
|
+
if (cached) {
|
|
3404
|
+
const elapsed = Date.now() - new Date(cached.checkedAt).getTime();
|
|
3405
|
+
if (elapsed < CHECK_INTERVAL_MS) return cached;
|
|
3406
|
+
}
|
|
3407
|
+
const controller = new AbortController();
|
|
3408
|
+
const timeout = setTimeout(() => controller.abort(), 5e3);
|
|
3409
|
+
const res = await fetch("https://registry.npmjs.org/browsirai/latest", {
|
|
3410
|
+
signal: controller.signal,
|
|
3411
|
+
headers: { Accept: "application/json" }
|
|
3412
|
+
});
|
|
3413
|
+
clearTimeout(timeout);
|
|
3414
|
+
if (!res.ok) return null;
|
|
3415
|
+
const data = await res.json();
|
|
3416
|
+
const latest = data.version;
|
|
3417
|
+
if (!latest) return null;
|
|
3418
|
+
const status = {
|
|
3419
|
+
current: VERSION,
|
|
3420
|
+
latest,
|
|
3421
|
+
checkedAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
3422
|
+
installMethod: method
|
|
3423
|
+
};
|
|
3424
|
+
writeUpgradeStatus(status);
|
|
3425
|
+
if (isNewer(VERSION, latest)) {
|
|
3426
|
+
process.stderr.write(
|
|
3427
|
+
`browsirai: v${latest} available (current: v${VERSION}). Upgrading in background...
|
|
3428
|
+
`
|
|
3429
|
+
);
|
|
3430
|
+
if (method === "npx") {
|
|
3431
|
+
spawn2("npm", ["cache", "clean", "--force"], {
|
|
3432
|
+
stdio: "ignore",
|
|
3433
|
+
detached: true
|
|
3434
|
+
}).unref();
|
|
3435
|
+
} else if (method === "global") {
|
|
3436
|
+
spawn2("npm", ["install", "-g", `browsirai@${latest}`], {
|
|
3437
|
+
stdio: "ignore",
|
|
3438
|
+
detached: true
|
|
3439
|
+
}).unref();
|
|
3440
|
+
}
|
|
3441
|
+
}
|
|
3442
|
+
return status;
|
|
3443
|
+
} catch {
|
|
3444
|
+
return null;
|
|
3445
|
+
}
|
|
3446
|
+
}
|
|
3447
|
+
var BROWSIR_DIR, UPGRADE_FILE, CHECK_INTERVAL_MS;
|
|
3448
|
+
var init_upgrade = __esm({
|
|
3449
|
+
"src/upgrade.ts"() {
|
|
3450
|
+
"use strict";
|
|
3451
|
+
init_version();
|
|
3452
|
+
BROWSIR_DIR = join3(homedir3(), ".browsirai");
|
|
3453
|
+
UPGRADE_FILE = join3(BROWSIR_DIR, "upgrade.json");
|
|
3454
|
+
CHECK_INTERVAL_MS = 60 * 60 * 1e3;
|
|
3455
|
+
}
|
|
3456
|
+
});
|
|
3457
|
+
|
|
3458
|
+
// src/tools/index.ts
|
|
3459
|
+
import { z } from "zod";
|
|
3460
|
+
function attachLifecycleListeners(conn) {
|
|
3461
|
+
const resetState = () => {
|
|
3462
|
+
browserCdp = null;
|
|
3463
|
+
activeSessionId = null;
|
|
3464
|
+
resetConsoleBuffer();
|
|
3465
|
+
resetNetworkBuffer();
|
|
3466
|
+
};
|
|
3467
|
+
conn.on("reconnectionFailed", resetState);
|
|
3468
|
+
conn.on("browserCrashed", resetState);
|
|
3469
|
+
}
|
|
3470
|
+
async function getCDP() {
|
|
3471
|
+
if (browserCdp?.isConnected && activeSessionId) {
|
|
3472
|
+
return new SessionCDP(browserCdp, activeSessionId);
|
|
3473
|
+
}
|
|
3474
|
+
if (headlessMode) {
|
|
3475
|
+
const launch = await launchHeadlessChrome();
|
|
3476
|
+
if (!launch.success) {
|
|
3477
|
+
throw new Error(launch.error ?? "Failed to launch headless Chrome.");
|
|
3478
|
+
}
|
|
3479
|
+
const wsUrl2 = launch.wsEndpoint ?? `ws://127.0.0.1:${launch.port}/devtools/browser`;
|
|
3480
|
+
browserCdp = new CDPConnection(wsUrl2);
|
|
3481
|
+
await browserCdp.connect();
|
|
3482
|
+
attachLifecycleListeners(browserCdp);
|
|
3483
|
+
const targets2 = await browserCdp.send("Target.getTargets");
|
|
3484
|
+
let page2 = targets2.targetInfos.find((t) => t.type === "page");
|
|
3485
|
+
if (!page2) {
|
|
3486
|
+
const created = await browserCdp.send("Target.createTarget", { url: "about:blank" });
|
|
3487
|
+
page2 = { targetId: created.targetId, type: "page", url: "about:blank", title: "" };
|
|
3488
|
+
}
|
|
3489
|
+
const attached2 = await browserCdp.send("Target.attachToTarget", {
|
|
3490
|
+
targetId: page2.targetId,
|
|
3491
|
+
flatten: true
|
|
3492
|
+
});
|
|
3493
|
+
activeSessionId = attached2.sessionId;
|
|
3494
|
+
const session2 = new SessionCDP(browserCdp, activeSessionId);
|
|
3495
|
+
await Promise.all([
|
|
3496
|
+
session2.send("Network.enable"),
|
|
3497
|
+
session2.send("Runtime.enable"),
|
|
3498
|
+
session2.send("Page.enable")
|
|
3499
|
+
]).catch(() => {
|
|
3500
|
+
});
|
|
3501
|
+
setupConsoleCapture(session2);
|
|
3502
|
+
setupNetworkCapture(session2);
|
|
3503
|
+
return session2;
|
|
3504
|
+
}
|
|
3505
|
+
const connection = await connectChrome({ autoLaunch: true });
|
|
3506
|
+
if (!connection.success) {
|
|
3507
|
+
throw new Error(
|
|
3508
|
+
connection.error ?? "Cannot connect to Chrome. Run `browsirai doctor` to set up."
|
|
3509
|
+
);
|
|
3510
|
+
}
|
|
3511
|
+
const wsUrl = connection.wsEndpoint ?? `ws://127.0.0.1:${connection.port}/devtools/browser`;
|
|
3512
|
+
browserCdp = new CDPConnection(wsUrl);
|
|
3513
|
+
await browserCdp.connect();
|
|
3514
|
+
attachLifecycleListeners(browserCdp);
|
|
3515
|
+
const targets = await browserCdp.send("Target.getTargets");
|
|
3516
|
+
let page = targets.targetInfos.find(
|
|
3517
|
+
(t) => t.type === "page" && !t.url.startsWith("chrome://")
|
|
3518
|
+
) ?? targets.targetInfos.find(
|
|
3519
|
+
(t) => t.type === "page"
|
|
3520
|
+
);
|
|
3521
|
+
if (!page) {
|
|
3522
|
+
const created = await browserCdp.send("Target.createTarget", { url: "about:blank" });
|
|
3523
|
+
page = { targetId: created.targetId, type: "page", url: "about:blank", title: "" };
|
|
3524
|
+
}
|
|
3525
|
+
const attached = await browserCdp.send("Target.attachToTarget", {
|
|
3526
|
+
targetId: page.targetId,
|
|
3527
|
+
flatten: true
|
|
3528
|
+
});
|
|
3529
|
+
activeSessionId = attached.sessionId;
|
|
3530
|
+
const session = new SessionCDP(browserCdp, activeSessionId);
|
|
3531
|
+
await Promise.all([
|
|
3532
|
+
session.send("Network.enable"),
|
|
3533
|
+
session.send("Runtime.enable"),
|
|
3534
|
+
session.send("Page.enable")
|
|
3535
|
+
]).catch(() => {
|
|
3536
|
+
});
|
|
3537
|
+
setupConsoleCapture(session);
|
|
3538
|
+
setupNetworkCapture(session);
|
|
3539
|
+
return session;
|
|
3540
|
+
}
|
|
3541
|
+
function textResult(text) {
|
|
3542
|
+
return { content: [{ type: "text", text }] };
|
|
3543
|
+
}
|
|
3544
|
+
function errorResult(msg) {
|
|
3545
|
+
return { content: [{ type: "text", text: `Error: ${msg}` }], isError: true };
|
|
3546
|
+
}
|
|
3547
|
+
function appendHint(result, toolName) {
|
|
3548
|
+
const hint = toolHints[toolName];
|
|
3549
|
+
if (!hint) return result;
|
|
3550
|
+
const lastTextIdx = result.content.findLastIndex((c) => c.type === "text");
|
|
3551
|
+
if (lastTextIdx >= 0) {
|
|
3552
|
+
result.content[lastTextIdx] = {
|
|
3553
|
+
...result.content[lastTextIdx],
|
|
3554
|
+
text: (result.content[lastTextIdx].text ?? "") + hint
|
|
3555
|
+
};
|
|
3556
|
+
} else {
|
|
3557
|
+
result.content.push({ type: "text", text: hint.trimStart() });
|
|
3558
|
+
}
|
|
3559
|
+
return result;
|
|
3560
|
+
}
|
|
3561
|
+
function coerceArgs(args) {
|
|
3562
|
+
const out = {};
|
|
3563
|
+
for (const [k, v] of Object.entries(args)) {
|
|
3564
|
+
if (typeof v === "string") {
|
|
3565
|
+
if (v === "true") {
|
|
3566
|
+
out[k] = true;
|
|
3567
|
+
continue;
|
|
3568
|
+
}
|
|
3569
|
+
if (v === "false") {
|
|
3570
|
+
out[k] = false;
|
|
3571
|
+
continue;
|
|
3572
|
+
}
|
|
3573
|
+
const n = Number(v);
|
|
3574
|
+
if (v !== "" && !isNaN(n)) {
|
|
3575
|
+
out[k] = n;
|
|
3576
|
+
continue;
|
|
3577
|
+
}
|
|
3578
|
+
}
|
|
3579
|
+
out[k] = v;
|
|
3580
|
+
}
|
|
3581
|
+
return out;
|
|
3582
|
+
}
|
|
3583
|
+
function createHandlers() {
|
|
3584
|
+
return {
|
|
3585
|
+
// --- Core tools with real implementations ---
|
|
3586
|
+
browser_navigate: async (args) => {
|
|
3587
|
+
try {
|
|
3588
|
+
if (needsCookieResync() && browserCdp?.isConnected) {
|
|
3589
|
+
browserCdp.close();
|
|
3590
|
+
browserCdp = void 0;
|
|
3591
|
+
activeSessionId = void 0;
|
|
3592
|
+
}
|
|
3593
|
+
const conn = await getCDP();
|
|
3594
|
+
const result = await browserNavigate(conn, args);
|
|
3595
|
+
return textResult(`Navigated to ${result.url}
|
|
3596
|
+
Title: ${result.title}`);
|
|
3597
|
+
} catch (e) {
|
|
3598
|
+
return errorResult(e.message);
|
|
3599
|
+
}
|
|
3600
|
+
},
|
|
3601
|
+
browser_screenshot: async (args) => {
|
|
3602
|
+
try {
|
|
3603
|
+
const conn = await getCDP();
|
|
3604
|
+
const needsImage = args.visual === true || args.fullPage === true || args.selector || args.annotate === true;
|
|
3605
|
+
if (!needsImage) {
|
|
3606
|
+
const snap = await browserSnapshot(conn, {});
|
|
3607
|
+
const text = typeof snap === "string" ? snap : snap.snapshot ?? JSON.stringify(snap, null, 2);
|
|
3608
|
+
return textResult(`[auto-optimized: snapshot returned \u2014 pass visual: true for image]
|
|
3609
|
+
|
|
3610
|
+
${text}`);
|
|
3611
|
+
}
|
|
3612
|
+
const result = await browserScreenshot(conn, args);
|
|
3613
|
+
const mimeType = args.format === "jpeg" ? "image/jpeg" : "image/png";
|
|
3614
|
+
const content = [{
|
|
3615
|
+
type: "image",
|
|
3616
|
+
data: result.base64,
|
|
3617
|
+
mimeType
|
|
3618
|
+
}];
|
|
3619
|
+
if (result.annotations?.length) {
|
|
3620
|
+
content.push({
|
|
3621
|
+
type: "text",
|
|
3622
|
+
text: result.annotations.map((a) => `${a.label} ${a.role}: ${a.name} (${a.ref})`).join("\n")
|
|
3623
|
+
});
|
|
3624
|
+
}
|
|
3625
|
+
return { content };
|
|
3626
|
+
} catch (e) {
|
|
3627
|
+
return errorResult(e.message);
|
|
3628
|
+
}
|
|
3629
|
+
},
|
|
3630
|
+
browser_tabs: async (args) => {
|
|
3631
|
+
try {
|
|
3632
|
+
const conn = await getCDP();
|
|
3633
|
+
const result = await browserTabs(conn, args);
|
|
3634
|
+
const lines = result.tabs.map((t) => `[${t.id}] ${t.title}
|
|
3635
|
+
${t.url}`);
|
|
3636
|
+
return textResult(lines.length ? lines.join("\n\n") : "No tabs found");
|
|
3637
|
+
} catch (e) {
|
|
3638
|
+
return errorResult(e.message);
|
|
3639
|
+
}
|
|
3640
|
+
},
|
|
3641
|
+
browser_evaluate: async (args) => {
|
|
3642
|
+
try {
|
|
3643
|
+
const conn = await getCDP();
|
|
3644
|
+
const result = await browserEval(conn, args);
|
|
3645
|
+
if (result.error) return errorResult(result.error);
|
|
3646
|
+
return textResult(JSON.stringify(result.result, null, 2));
|
|
3647
|
+
} catch (e) {
|
|
3648
|
+
return errorResult(e.message);
|
|
3649
|
+
}
|
|
3650
|
+
},
|
|
3651
|
+
browser_snapshot: async (args) => {
|
|
3652
|
+
try {
|
|
3653
|
+
const conn = await getCDP();
|
|
3654
|
+
const result = await browserSnapshot(conn, args);
|
|
3655
|
+
return textResult(typeof result === "string" ? result : JSON.stringify(result, null, 2));
|
|
3656
|
+
} catch (e) {
|
|
3657
|
+
return errorResult(e.message);
|
|
3658
|
+
}
|
|
3659
|
+
},
|
|
3660
|
+
browser_click: async (args) => {
|
|
3661
|
+
try {
|
|
3662
|
+
const conn = await getCDP();
|
|
3663
|
+
const result = await browserClick(conn, args);
|
|
3664
|
+
return textResult(typeof result === "string" ? result : JSON.stringify(result));
|
|
3665
|
+
} catch (e) {
|
|
3666
|
+
return errorResult(e.message);
|
|
3667
|
+
}
|
|
3668
|
+
},
|
|
3669
|
+
browser_scroll: async (args) => {
|
|
3670
|
+
try {
|
|
3671
|
+
const conn = await getCDP();
|
|
3672
|
+
const result = await browserScroll(conn, args);
|
|
3673
|
+
return textResult(typeof result === "string" ? result : JSON.stringify(result));
|
|
3674
|
+
} catch (e) {
|
|
3675
|
+
return errorResult(e.message);
|
|
3676
|
+
}
|
|
3677
|
+
},
|
|
3678
|
+
browser_html: async (args) => {
|
|
3679
|
+
try {
|
|
3680
|
+
const conn = await getCDP();
|
|
3681
|
+
const result = await browserHtml(conn, args);
|
|
3682
|
+
return textResult(typeof result === "string" ? result : JSON.stringify(result));
|
|
3683
|
+
} catch (e) {
|
|
3684
|
+
return errorResult(e.message);
|
|
3685
|
+
}
|
|
3686
|
+
},
|
|
3687
|
+
browser_connect: async (args) => {
|
|
3688
|
+
try {
|
|
3689
|
+
const typed = args;
|
|
3690
|
+
const wasHeadless = headlessMode;
|
|
3691
|
+
headlessMode = typed.headless === true;
|
|
3692
|
+
if ((wasHeadless !== headlessMode || typed.resync === true) && browserCdp?.isConnected) {
|
|
3693
|
+
browserCdp.close();
|
|
3694
|
+
browserCdp = void 0;
|
|
3695
|
+
activeSessionId = void 0;
|
|
3696
|
+
}
|
|
3697
|
+
const conn = await getCDP();
|
|
3698
|
+
const mode = headlessMode ? " (headless)" : "";
|
|
3699
|
+
let summary = SKILL_SUMMARY.replace("Connected to Chrome via CDP.", `Connected to Chrome via CDP${mode}.`);
|
|
3700
|
+
try {
|
|
3701
|
+
const { getUpgradeStatus: getUpgradeStatus2 } = await Promise.resolve().then(() => (init_upgrade(), upgrade_exports));
|
|
3702
|
+
const status = getUpgradeStatus2();
|
|
3703
|
+
if (status && status.latest !== status.current) {
|
|
3704
|
+
summary += `
|
|
3705
|
+
|
|
3706
|
+
\u26A0\uFE0F browsirai v${status.latest} available (current: v${status.current}). Restart to apply.`;
|
|
3707
|
+
}
|
|
3708
|
+
} catch {
|
|
3709
|
+
}
|
|
3710
|
+
return textResult(summary);
|
|
3711
|
+
} catch (e) {
|
|
3712
|
+
return errorResult(e.message);
|
|
3713
|
+
}
|
|
3714
|
+
},
|
|
3715
|
+
browser_list: async () => {
|
|
3716
|
+
try {
|
|
3717
|
+
if (!browserCdp?.isConnected) {
|
|
3718
|
+
await getCDP();
|
|
3719
|
+
}
|
|
3720
|
+
const targets = await browserCdp.send("Target.getTargets");
|
|
3721
|
+
const pages = targets.targetInfos.filter((t) => t.type === "page");
|
|
3722
|
+
const lines = pages.map((t) => `[${t.targetId}] ${t.title}
|
|
3723
|
+
${t.url}`);
|
|
3724
|
+
return textResult(lines.length ? lines.join("\n\n") : "No pages found");
|
|
3725
|
+
} catch (e) {
|
|
3726
|
+
return errorResult(e.message);
|
|
3727
|
+
}
|
|
3728
|
+
},
|
|
3729
|
+
// --- Newly wired tools ---
|
|
3730
|
+
browser_navigate_back: async (args) => {
|
|
3731
|
+
try {
|
|
3732
|
+
const conn = await getCDP();
|
|
3733
|
+
const result = await browserNavigateBack(conn, args);
|
|
3734
|
+
return textResult(result.url ? `Navigated ${args.direction || "back"} to ${result.url}` : `Navigated ${args.direction || "back"}`);
|
|
3735
|
+
} catch (e) {
|
|
3736
|
+
return errorResult(e.message);
|
|
3737
|
+
}
|
|
3738
|
+
},
|
|
3739
|
+
browser_press_key: async (args) => {
|
|
3740
|
+
try {
|
|
3741
|
+
const conn = await getCDP();
|
|
3742
|
+
await browserPressKey(conn, args);
|
|
3743
|
+
return textResult(`Pressed key: ${args.key}`);
|
|
3744
|
+
} catch (e) {
|
|
3745
|
+
return errorResult(e.message);
|
|
3746
|
+
}
|
|
3747
|
+
},
|
|
3748
|
+
browser_type: async (args) => {
|
|
3749
|
+
try {
|
|
3750
|
+
const conn = await getCDP();
|
|
3751
|
+
await browserType(conn, args);
|
|
3752
|
+
return textResult(`Typed ${args.text.length} characters`);
|
|
3753
|
+
} catch (e) {
|
|
3754
|
+
return errorResult(e.message);
|
|
3755
|
+
}
|
|
3756
|
+
},
|
|
3757
|
+
browser_fill_form: async (args) => {
|
|
3758
|
+
try {
|
|
3759
|
+
const conn = await getCDP();
|
|
3760
|
+
const fields = [{
|
|
3761
|
+
name: args.selector ?? args.ref ?? "field",
|
|
3762
|
+
type: "textbox",
|
|
3763
|
+
ref: args.ref,
|
|
3764
|
+
selector: args.selector,
|
|
3765
|
+
value: args.value
|
|
3766
|
+
}];
|
|
3767
|
+
await browserFillForm(conn, { fields });
|
|
3768
|
+
return textResult(`Filled form field with value`);
|
|
3769
|
+
} catch (e) {
|
|
3770
|
+
return errorResult(e.message);
|
|
3771
|
+
}
|
|
3772
|
+
},
|
|
3773
|
+
browser_hover: async (args) => {
|
|
3774
|
+
try {
|
|
3775
|
+
const conn = await getCDP();
|
|
3776
|
+
await browserHover(conn, args);
|
|
3777
|
+
return textResult(`Hovered over element`);
|
|
3778
|
+
} catch (e) {
|
|
3779
|
+
return errorResult(e.message);
|
|
3780
|
+
}
|
|
3781
|
+
},
|
|
3782
|
+
browser_drag: async (args) => {
|
|
3783
|
+
try {
|
|
3784
|
+
const conn = await getCDP();
|
|
3785
|
+
await browserDrag(conn, args);
|
|
3786
|
+
return textResult(`Dragged from ${args.startRef} to ${args.endRef}`);
|
|
3787
|
+
} catch (e) {
|
|
3788
|
+
return errorResult(e.message);
|
|
3789
|
+
}
|
|
3790
|
+
},
|
|
3791
|
+
browser_select_option: async (args) => {
|
|
3792
|
+
try {
|
|
3793
|
+
const conn = await getCDP();
|
|
3794
|
+
const result = await browserSelectOption(conn, args);
|
|
3795
|
+
return textResult(`Selected: ${result.selected.join(", ")}`);
|
|
3796
|
+
} catch (e) {
|
|
3797
|
+
return errorResult(e.message);
|
|
3798
|
+
}
|
|
3799
|
+
},
|
|
3800
|
+
browser_handle_dialog: async (args) => {
|
|
3801
|
+
try {
|
|
3802
|
+
const conn = await getCDP();
|
|
3803
|
+
await browserHandleDialog(conn, args);
|
|
3804
|
+
return textResult(`Dialog ${args.accept ? "accepted" : "dismissed"}`);
|
|
3805
|
+
} catch (e) {
|
|
3806
|
+
return errorResult(e.message);
|
|
3807
|
+
}
|
|
3808
|
+
},
|
|
3809
|
+
browser_file_upload: async (args) => {
|
|
3810
|
+
try {
|
|
3811
|
+
const conn = await getCDP();
|
|
3812
|
+
const result = await browserFileUpload(conn, args);
|
|
3813
|
+
return textResult(`Uploaded ${result.filesCount} file(s)`);
|
|
3814
|
+
} catch (e) {
|
|
3815
|
+
return errorResult(e.message);
|
|
3816
|
+
}
|
|
3817
|
+
},
|
|
3818
|
+
browser_wait_for: async (args) => {
|
|
3819
|
+
try {
|
|
3820
|
+
const conn = await getCDP();
|
|
3821
|
+
const result = await browserWaitFor(conn, args);
|
|
3822
|
+
return textResult(`Wait completed in ${result.elapsed}ms`);
|
|
3823
|
+
} catch (e) {
|
|
3824
|
+
return errorResult(e.message);
|
|
3825
|
+
}
|
|
3826
|
+
},
|
|
3827
|
+
browser_resize: async (args) => {
|
|
3828
|
+
try {
|
|
3829
|
+
const conn = await getCDP();
|
|
3830
|
+
const result = await browserResize(conn, args);
|
|
3831
|
+
return textResult(`Resized to ${result.width}x${result.height}`);
|
|
3832
|
+
} catch (e) {
|
|
3833
|
+
return errorResult(e.message);
|
|
3834
|
+
}
|
|
3835
|
+
},
|
|
3836
|
+
browser_close: async (args) => {
|
|
3837
|
+
try {
|
|
3838
|
+
const conn = await getCDP();
|
|
3839
|
+
const result = await browserClose(conn, args);
|
|
3840
|
+
return textResult(`Closed ${result.closedTargets} tab(s)`);
|
|
3841
|
+
} catch (e) {
|
|
3842
|
+
return errorResult(e.message);
|
|
3843
|
+
}
|
|
3844
|
+
},
|
|
3845
|
+
browser_network_requests: async (args) => {
|
|
3846
|
+
try {
|
|
3847
|
+
const conn = await getCDP();
|
|
3848
|
+
const result = await browserNetworkRequests(conn, args);
|
|
3849
|
+
const lines = result.requests.map((r) => `${r.method} ${r.status ?? "?"} ${r.url}`);
|
|
3850
|
+
return textResult(lines.length ? lines.join("\n") : "No network requests captured");
|
|
3851
|
+
} catch (e) {
|
|
3852
|
+
return errorResult(e.message);
|
|
3853
|
+
}
|
|
3854
|
+
},
|
|
3855
|
+
browser_console_messages: async (args) => {
|
|
3856
|
+
try {
|
|
3857
|
+
const conn = await getCDP();
|
|
3858
|
+
const result = await browserConsoleMessages(conn, args);
|
|
3859
|
+
const lines = result.messages.map((m) => `[${m.level}] ${m.text}`);
|
|
3860
|
+
return textResult(lines.length ? lines.join("\n") : "No console messages");
|
|
3861
|
+
} catch (e) {
|
|
3862
|
+
return errorResult(e.message);
|
|
3863
|
+
}
|
|
3864
|
+
},
|
|
3865
|
+
browser_inspect_source: async (args) => {
|
|
3866
|
+
try {
|
|
3867
|
+
const conn = await getCDP();
|
|
3868
|
+
const result = await browserInspectSource(conn, args);
|
|
3869
|
+
const lines = [];
|
|
3870
|
+
lines.push(`Element: <${result.tagName}>`);
|
|
3871
|
+
if (result.componentName) {
|
|
3872
|
+
lines.push(`Component: ${result.componentName}`);
|
|
3873
|
+
}
|
|
3874
|
+
if (result.source) {
|
|
3875
|
+
lines.push(`Source: ${result.source.filePath}:${result.source.lineNumber ?? "?"}:${result.source.columnNumber ?? "?"}`);
|
|
3876
|
+
} else {
|
|
3877
|
+
lines.push("Source: not found (no framework metadata detected)");
|
|
3878
|
+
}
|
|
3879
|
+
if (result.stack.length > 0) {
|
|
3880
|
+
lines.push("\nComponent Stack:");
|
|
3881
|
+
for (const frame of result.stack) {
|
|
3882
|
+
const loc = `${frame.filePath}:${frame.lineNumber ?? "?"}`;
|
|
3883
|
+
lines.push(` ${frame.componentName ?? "anonymous"} (${loc})`);
|
|
3884
|
+
}
|
|
3885
|
+
}
|
|
3886
|
+
return textResult(lines.join("\n"));
|
|
3887
|
+
} catch (e) {
|
|
3888
|
+
return errorResult(e.message);
|
|
3889
|
+
}
|
|
3890
|
+
},
|
|
3891
|
+
browser_route: async (args) => {
|
|
3892
|
+
try {
|
|
3893
|
+
const conn = await getCDP();
|
|
3894
|
+
const result = await browserRoute(conn, args);
|
|
3895
|
+
return textResult(`Route registered: ${result.url} \u2192 ${result.status}
|
|
3896
|
+
Active routes: ${result.activeRoutes}`);
|
|
3897
|
+
} catch (e) {
|
|
3898
|
+
return errorResult(e.message);
|
|
3899
|
+
}
|
|
3900
|
+
},
|
|
3901
|
+
browser_abort: async (args) => {
|
|
3902
|
+
try {
|
|
3903
|
+
const conn = await getCDP();
|
|
3904
|
+
const result = await browserAbort(conn, args);
|
|
3905
|
+
return textResult(`Abort registered: ${result.url}
|
|
3906
|
+
Active aborts: ${result.activeAborts}`);
|
|
3907
|
+
} catch (e) {
|
|
3908
|
+
return errorResult(e.message);
|
|
3909
|
+
}
|
|
3910
|
+
},
|
|
3911
|
+
browser_unroute: async (args) => {
|
|
3912
|
+
try {
|
|
3913
|
+
const conn = await getCDP();
|
|
3914
|
+
const result = await browserUnroute(conn, args);
|
|
3915
|
+
return textResult(`Removed ${result.removed} rule(s)
|
|
3916
|
+
Remaining: ${result.remaining}`);
|
|
3917
|
+
} catch (e) {
|
|
3918
|
+
return errorResult(e.message);
|
|
3919
|
+
}
|
|
3920
|
+
},
|
|
3921
|
+
browser_annotated_screenshot: async (args) => {
|
|
3922
|
+
try {
|
|
3923
|
+
const conn = await getCDP();
|
|
3924
|
+
const result = await browserAnnotatedScreenshot(conn, args);
|
|
3925
|
+
const content = [{
|
|
3926
|
+
type: "image",
|
|
3927
|
+
data: result.base64,
|
|
3928
|
+
mimeType: "image/png"
|
|
3929
|
+
}];
|
|
3930
|
+
if (result.annotations?.length) {
|
|
3931
|
+
content.push({
|
|
3932
|
+
type: "text",
|
|
3933
|
+
text: result.annotations.map((a) => `${a.label} ${a.role}: ${a.name} (${a.ref})`).join("\n")
|
|
3934
|
+
});
|
|
3935
|
+
}
|
|
3936
|
+
return { content };
|
|
3937
|
+
} catch (e) {
|
|
3938
|
+
return errorResult(e.message);
|
|
3939
|
+
}
|
|
3940
|
+
},
|
|
3941
|
+
browser_diff: async (args) => {
|
|
3942
|
+
try {
|
|
3943
|
+
const conn = await getCDP();
|
|
3944
|
+
const result = await browserDiff(conn, args);
|
|
3945
|
+
const content = [{
|
|
3946
|
+
type: "image",
|
|
3947
|
+
data: result.diffImage,
|
|
3948
|
+
mimeType: "image/png"
|
|
3949
|
+
}, {
|
|
3950
|
+
type: "text",
|
|
3951
|
+
text: [
|
|
3952
|
+
`Diff: ${result.diffPercentage}% changed`,
|
|
3953
|
+
`Pixels: ${result.diffPixels} / ${result.totalPixels} differ`,
|
|
3954
|
+
`Dimensions: ${result.width}x${result.height}`,
|
|
3955
|
+
`Identical: ${result.identical}`
|
|
3956
|
+
].join("\n")
|
|
3957
|
+
}];
|
|
3958
|
+
return { content };
|
|
3959
|
+
} catch (e) {
|
|
3960
|
+
return errorResult(e.message);
|
|
3961
|
+
}
|
|
3962
|
+
},
|
|
3963
|
+
browser_find: async (args) => {
|
|
3964
|
+
try {
|
|
3965
|
+
const conn = await getCDP();
|
|
3966
|
+
const result = await browserFind(conn, args);
|
|
3967
|
+
if (!result.found) {
|
|
3968
|
+
return textResult(`No match found (${result.count} candidates)`);
|
|
3969
|
+
}
|
|
3970
|
+
const lines = [];
|
|
3971
|
+
lines.push(`Found: ${result.ref ?? "no ref"}`);
|
|
3972
|
+
lines.push(`Role: ${result.role ?? "unknown"}`);
|
|
3973
|
+
lines.push(`Name: ${result.name ?? ""}`);
|
|
3974
|
+
lines.push(`Matches: ${result.count}`);
|
|
3975
|
+
return textResult(lines.join("\n"));
|
|
3976
|
+
} catch (e) {
|
|
3977
|
+
return errorResult(e.message);
|
|
3978
|
+
}
|
|
3979
|
+
},
|
|
3980
|
+
browser_save_state: async (args) => {
|
|
3981
|
+
try {
|
|
3982
|
+
const conn = await getCDP();
|
|
3983
|
+
const result = await browserSaveState(conn, args);
|
|
3984
|
+
return textResult(
|
|
3985
|
+
`State "${result.name}" saved to ${result.path}
|
|
3986
|
+
Cookies: ${result.cookies}, localStorage: ${result.localStorage}, sessionStorage: ${result.sessionStorage}`
|
|
3987
|
+
);
|
|
3988
|
+
} catch (e) {
|
|
3989
|
+
return errorResult(e.message);
|
|
3990
|
+
}
|
|
3991
|
+
},
|
|
3992
|
+
browser_load_state: async (args) => {
|
|
3993
|
+
try {
|
|
3994
|
+
const conn = await getCDP();
|
|
3995
|
+
const result = await browserLoadState(conn, args);
|
|
3996
|
+
return textResult(
|
|
3997
|
+
`State "${result.name}" restored
|
|
3998
|
+
Cookies: ${result.cookies}, localStorage: ${result.localStorage}, sessionStorage: ${result.sessionStorage}`
|
|
3999
|
+
);
|
|
4000
|
+
} catch (e) {
|
|
4001
|
+
return errorResult(e.message);
|
|
4002
|
+
}
|
|
4003
|
+
}
|
|
4004
|
+
};
|
|
4005
|
+
}
|
|
4006
|
+
function registerTools(server, _manager) {
|
|
4007
|
+
const handlers = createHandlers();
|
|
4008
|
+
const toolNames = Object.keys(schemas);
|
|
4009
|
+
for (const name of toolNames) {
|
|
4010
|
+
const description = descriptions[name] ?? name;
|
|
4011
|
+
const shape = toolShapes[name] ?? {};
|
|
4012
|
+
const rawHandler = handlers[name] ?? (async () => textResult(`${name}: not yet implemented`));
|
|
4013
|
+
const handler = async (args) => {
|
|
4014
|
+
const coerced = coerceArgs(args);
|
|
4015
|
+
const result = await rawHandler(coerced);
|
|
4016
|
+
return appendHint(result, name);
|
|
4017
|
+
};
|
|
4018
|
+
server.tool(name, description, shape, handler);
|
|
4019
|
+
}
|
|
4020
|
+
}
|
|
4021
|
+
var browserCdp, activeSessionId, SessionCDP, headlessMode, SKILL_SUMMARY, toolHints, browser_connect, browser_tabs, browser_snapshot, browser_screenshot, browser_click, browser_fill_form, browser_type, browser_press_key, browser_navigate, browser_navigate_back, browser_evaluate, browser_scroll, browser_network_requests, browser_console_messages, browser_html, browser_close, browser_wait_for, browser_hover, browser_drag, browser_select_option, browser_handle_dialog, browser_file_upload, browser_resize, browser_annotated_screenshot, browser_inspect_source, browser_route, browser_abort, browser_unroute, browser_find, browser_diff, browser_save_state, browser_load_state, browser_list, schemas, descriptions, cNum, cBool, toolShapes;
|
|
4022
|
+
var init_tools = __esm({
|
|
4023
|
+
"src/tools/index.ts"() {
|
|
4024
|
+
"use strict";
|
|
4025
|
+
init_connection();
|
|
4026
|
+
init_chrome_launcher();
|
|
4027
|
+
init_browser_navigate();
|
|
4028
|
+
init_browser_screenshot();
|
|
4029
|
+
init_browser_tabs();
|
|
4030
|
+
init_browser_eval();
|
|
4031
|
+
init_browser_snapshot();
|
|
4032
|
+
init_browser_click();
|
|
4033
|
+
init_browser_scroll();
|
|
4034
|
+
init_browser_html();
|
|
4035
|
+
init_browser_navigate_back();
|
|
4036
|
+
init_browser_press_key();
|
|
4037
|
+
init_browser_hover();
|
|
4038
|
+
init_browser_resize();
|
|
4039
|
+
init_browser_close();
|
|
4040
|
+
init_browser_fill_form();
|
|
4041
|
+
init_browser_type();
|
|
4042
|
+
init_browser_select_option();
|
|
4043
|
+
init_browser_wait_for();
|
|
4044
|
+
init_browser_drag();
|
|
4045
|
+
init_browser_handle_dialog();
|
|
4046
|
+
init_browser_file_upload();
|
|
4047
|
+
init_browser_network_requests();
|
|
4048
|
+
init_browser_console_messages();
|
|
4049
|
+
init_browser_annotated_screenshot();
|
|
4050
|
+
init_browser_inspect_source();
|
|
4051
|
+
init_browser_intercept();
|
|
4052
|
+
init_browser_find();
|
|
4053
|
+
init_browser_diff();
|
|
4054
|
+
init_browser_session_state();
|
|
4055
|
+
browserCdp = null;
|
|
4056
|
+
activeSessionId = null;
|
|
4057
|
+
SessionCDP = class {
|
|
4058
|
+
constructor(conn, sessionId) {
|
|
4059
|
+
this.conn = conn;
|
|
4060
|
+
this.sessionId = sessionId;
|
|
4061
|
+
}
|
|
4062
|
+
get isConnected() {
|
|
4063
|
+
return this.conn.isConnected;
|
|
4064
|
+
}
|
|
4065
|
+
send(method, params, options) {
|
|
4066
|
+
return this.conn.send(method, params, {
|
|
4067
|
+
...options,
|
|
4068
|
+
sessionId: options?.sessionId ?? this.sessionId
|
|
4069
|
+
});
|
|
4070
|
+
}
|
|
4071
|
+
on(event, handler) {
|
|
4072
|
+
this.conn.on(event, handler);
|
|
4073
|
+
}
|
|
4074
|
+
off(event, handler) {
|
|
4075
|
+
this.conn.off(event, handler);
|
|
4076
|
+
}
|
|
4077
|
+
close() {
|
|
4078
|
+
}
|
|
4079
|
+
};
|
|
4080
|
+
headlessMode = process.env.BROWSIR_HEADLESS === "1" || process.env.BROWSIR_HEADLESS === "true";
|
|
4081
|
+
SKILL_SUMMARY = `Connected to Chrome via CDP.
|
|
4082
|
+
|
|
4083
|
+
## browsirai \u2014 Quick Reference
|
|
4084
|
+
|
|
4085
|
+
**Cost hierarchy (cheapest first):**
|
|
4086
|
+
1. \`browser_evaluate\` \u2014 JS expression for single values (~10 tokens). Use when you need one data point (count, text, attribute).
|
|
4087
|
+
2. \`browser_snapshot\` \u2014 accessibility tree with @eN refs (~500 tokens). Use for page understanding and interaction.
|
|
4088
|
+
3. \`browser_screenshot { visual: true }\` \u2014 full image (~10K tokens). ONLY for layout/colors/visual bugs.
|
|
4089
|
+
|
|
4090
|
+
**Core workflow: snapshot \u2192 ref \u2192 interact \u2192 snapshot**
|
|
4091
|
+
|
|
4092
|
+
1. \`browser_snapshot\` \u2014 get accessibility tree with @eN refs
|
|
4093
|
+
2. Use @eN refs with: \`browser_click\`, \`browser_fill_form\`, \`browser_hover\`, \`browser_type\`, \`browser_select_option\`, \`browser_drag\`, \`browser_inspect_source\`
|
|
4094
|
+
3. \`browser_snapshot\` \u2014 verify result (ALWAYS use this)
|
|
4095
|
+
|
|
4096
|
+
**Cost optimization (enforced server-side):**
|
|
4097
|
+
- \`browser_screenshot\` without \`visual: true\` auto-returns snapshot text
|
|
4098
|
+
- For single data extraction: \`browser_evaluate\` > \`browser_snapshot\` (50x cheaper)
|
|
4099
|
+
- Reserve \`browser_screenshot { visual: true }\` for CSS/layout debugging only
|
|
4100
|
+
|
|
4101
|
+
**Identity resolution (cookie-based, always-first):**
|
|
4102
|
+
- NEVER guess usernames. The browser has an active session \u2014 use it.
|
|
4103
|
+
- To find the logged-in user: \`browser_evaluate\` on the site (e.g. GitHub avatar menu, X profile link).
|
|
4104
|
+
- When asked "go to my profile/repo/account" \u2192 navigate to the site root first, extract identity from session, then proceed.
|
|
4105
|
+
- Cookie sync means the browser IS the user. Trust the session, not assumptions.
|
|
4106
|
+
|
|
4107
|
+
**Key patterns:**
|
|
4108
|
+
- \`browser_fill_form\` clears existing value. Use \`browser_type\` to append.
|
|
4109
|
+
- \`browser_type\` with \`submit: true\` presses Enter after typing.
|
|
4110
|
+
- \`browser_wait_for\` \u2014 wait for text, selector, URL, or JS condition before proceeding.
|
|
4111
|
+
- \`browser_inspect_source\` \u2014 find source file, line, component name (React/Vue/Svelte, dev mode only).
|
|
4112
|
+
- \`browser_resize\` with \`preset: "reset"\` \u2014 restore native viewport.
|
|
4113
|
+
- Refs become stale after navigation or major DOM changes \u2014 take a new snapshot.
|
|
4114
|
+
- \`browser_evaluate\` cannot access cross-origin iframes \u2014 use \`browser_type\` instead.
|
|
4115
|
+
- \`browser_console_messages\` / \`browser_network_requests\` \u2014 check for errors after interactions.`;
|
|
4116
|
+
toolHints = {
|
|
4117
|
+
browser_navigate: "\n\n\u2192 Next: browser_evaluate for quick data extraction (~10 tokens), or browser_snapshot for page structure and @eN refs (~500 tokens).",
|
|
4118
|
+
browser_navigate_back: "\n\n\u2192 Next: browser_snapshot to see updated page.",
|
|
4119
|
+
browser_snapshot: "\n\n\u2192 Next: Use @eN refs with browser_click, browser_fill_form, browser_hover, browser_type, browser_select_option, browser_drag, or browser_inspect_source.",
|
|
4120
|
+
browser_screenshot: "\n\n\u2192 Cost: This call was auto-optimized to snapshot text. For full image, pass { visual: true }. Prefer browser_snapshot (~500 tokens) over browser_screenshot (~10K tokens).",
|
|
4121
|
+
browser_annotated_screenshot: "\n\n\u2192 Cost: ~12K tokens. Consider browser_snapshot { interactive: true } (~500 tokens) for same info as text.",
|
|
4122
|
+
browser_click: "\n\n\u2192 Next: browser_snapshot to verify result. Refs may be stale after DOM changes.",
|
|
4123
|
+
browser_fill_form: "\n\n\u2192 Next: browser_snapshot to verify. Note: fill_form clears existing value first.",
|
|
4124
|
+
browser_type: "\n\n\u2192 Tip: Use submit: true to press Enter. Does NOT clear existing value (unlike fill_form).",
|
|
4125
|
+
browser_press_key: "\n\n\u2192 Next: browser_snapshot to verify effect.",
|
|
4126
|
+
browser_hover: "\n\n\u2192 Next: browser_snapshot to verify hover state. Use browser_screenshot { visual: true } only for visual hover effects.",
|
|
4127
|
+
browser_drag: "\n\n\u2192 Next: browser_snapshot to verify drag result.",
|
|
4128
|
+
browser_scroll: "\n\n\u2192 Next: browser_snapshot to see new viewport content.",
|
|
4129
|
+
browser_select_option: "\n\n\u2192 Next: browser_snapshot to verify selection.",
|
|
4130
|
+
browser_handle_dialog: "\n\n\u2192 Next: browser_snapshot to see page state after dialog.",
|
|
4131
|
+
browser_file_upload: "\n\n\u2192 Next: browser_snapshot to verify upload.",
|
|
4132
|
+
browser_wait_for: "\n\n\u2192 Next: browser_snapshot to see current page state.",
|
|
4133
|
+
browser_resize: '\n\n\u2192 Next: browser_snapshot to verify viewport. Use browser_screenshot { visual: true } only to check visual layout. Use preset: "reset" to restore.',
|
|
4134
|
+
browser_close: "\n\n\u2192 Next: browser_tabs to see remaining tabs.",
|
|
4135
|
+
browser_evaluate: "\n\n\u2192 Tip: Cheapest tool (~10 tokens). Use for single data extraction (counts, text, attributes). Prefer browser_snapshot + @eN refs for DOM interaction. Cannot access cross-origin iframes.",
|
|
4136
|
+
browser_html: "\n\n\u2192 Tip: Prefer browser_snapshot for structured page understanding with @eN refs.",
|
|
4137
|
+
browser_tabs: "\n\n\u2192 Tip: Use browser_navigate to open a URL in current tab.",
|
|
4138
|
+
browser_console_messages: '\n\n\u2192 Tip: Use level: "error" to filter for errors only.',
|
|
4139
|
+
browser_network_requests: '\n\n\u2192 Tip: Use filter: "*api*" and includeStatic: false for API calls only.',
|
|
4140
|
+
browser_inspect_source: "\n\n\u2192 Tip: Dev mode only. Use browser_snapshot first to get @eN refs for specific elements.",
|
|
4141
|
+
browser_route: "\n\n\u2192 Tip: Use browser_network_requests to verify intercepted responses. Use browser_unroute to remove.",
|
|
4142
|
+
browser_abort: "\n\n\u2192 Tip: Use browser_network_requests to verify blocked requests. Use browser_unroute to remove.",
|
|
4143
|
+
browser_unroute: "\n\n\u2192 Tip: Use {all: true} to clear all intercepts at once.",
|
|
4144
|
+
browser_find: "\n\n\u2192 Next: Use the @eN ref with browser_click, browser_fill_form, browser_hover, or browser_inspect_source.",
|
|
4145
|
+
browser_diff: "\n\n\u2192 Tip: Use 'current' as before value to capture the page now. Make changes, then call again with the first result as before.",
|
|
4146
|
+
browser_save_state: "\n\n\u2192 Tip: State saved to ~/.browsirai/states/. Use browser_load_state to restore later.",
|
|
4147
|
+
browser_load_state: "\n\n\u2192 Next: browser_snapshot to see the restored page state.",
|
|
4148
|
+
browser_connect: "",
|
|
4149
|
+
// SKILL_SUMMARY is returned directly
|
|
4150
|
+
browser_list: "\n\n\u2192 Tip: Use browser_connect to connect to a specific instance."
|
|
4151
|
+
};
|
|
4152
|
+
browser_connect = z.object({
|
|
4153
|
+
port: z.number().optional(),
|
|
4154
|
+
host: z.string().optional(),
|
|
4155
|
+
headless: z.boolean().optional()
|
|
4156
|
+
});
|
|
4157
|
+
browser_tabs = z.object({
|
|
4158
|
+
filter: z.string().optional()
|
|
4159
|
+
});
|
|
4160
|
+
browser_snapshot = z.object({
|
|
4161
|
+
selector: z.string().optional(),
|
|
4162
|
+
compact: z.boolean().optional(),
|
|
4163
|
+
interactive: z.boolean().optional(),
|
|
4164
|
+
cursor: z.boolean().optional(),
|
|
4165
|
+
depth: z.number().optional()
|
|
4166
|
+
});
|
|
4167
|
+
browser_screenshot = z.object({
|
|
4168
|
+
selector: z.string().optional(),
|
|
4169
|
+
fullPage: z.boolean().optional(),
|
|
4170
|
+
format: z.enum(["png", "jpeg"]).optional(),
|
|
4171
|
+
quality: z.number().optional(),
|
|
4172
|
+
annotate: z.boolean().optional(),
|
|
4173
|
+
visual: z.boolean().optional()
|
|
4174
|
+
});
|
|
4175
|
+
browser_click = z.object({
|
|
4176
|
+
selector: z.string().optional(),
|
|
4177
|
+
ref: z.string().optional(),
|
|
4178
|
+
x: z.number().optional(),
|
|
4179
|
+
y: z.number().optional(),
|
|
4180
|
+
newTab: z.boolean().optional()
|
|
4181
|
+
}).refine(
|
|
4182
|
+
(data) => {
|
|
4183
|
+
if (data.selector) return true;
|
|
4184
|
+
if (data.ref) return true;
|
|
4185
|
+
if (data.x !== void 0 && data.y !== void 0) return true;
|
|
4186
|
+
return false;
|
|
4187
|
+
},
|
|
4188
|
+
{ message: "Must provide selector, ref, or both x and y coordinates" }
|
|
4189
|
+
);
|
|
4190
|
+
browser_fill_form = z.object({
|
|
4191
|
+
ref: z.string().optional(),
|
|
4192
|
+
selector: z.string().optional(),
|
|
4193
|
+
value: z.string()
|
|
4194
|
+
}).refine(
|
|
4195
|
+
(data) => data.ref !== void 0 || data.selector !== void 0,
|
|
4196
|
+
{ message: "Must provide ref or selector" }
|
|
4197
|
+
);
|
|
4198
|
+
browser_type = z.object({
|
|
4199
|
+
text: z.string(),
|
|
4200
|
+
ref: z.string().optional(),
|
|
4201
|
+
slowly: z.boolean().optional(),
|
|
4202
|
+
submit: z.boolean().optional()
|
|
4203
|
+
});
|
|
4204
|
+
browser_press_key = z.object({
|
|
4205
|
+
key: z.string()
|
|
4206
|
+
});
|
|
4207
|
+
browser_navigate = z.object({
|
|
4208
|
+
url: z.string(),
|
|
4209
|
+
waitUntil: z.enum(["load", "domcontentloaded", "networkidle"]).optional()
|
|
4210
|
+
});
|
|
4211
|
+
browser_navigate_back = z.object({
|
|
4212
|
+
direction: z.enum(["back", "forward"]).optional()
|
|
4213
|
+
});
|
|
4214
|
+
browser_evaluate = z.object({
|
|
4215
|
+
expression: z.string(),
|
|
4216
|
+
frameId: z.string().optional()
|
|
4217
|
+
});
|
|
4218
|
+
browser_scroll = z.object({
|
|
4219
|
+
direction: z.enum(["up", "down", "left", "right"]).optional(),
|
|
4220
|
+
pixels: z.number().optional(),
|
|
4221
|
+
selector: z.string().optional()
|
|
4222
|
+
});
|
|
4223
|
+
browser_network_requests = z.object({
|
|
4224
|
+
filter: z.string().optional(),
|
|
4225
|
+
limit: z.number().optional(),
|
|
4226
|
+
includeHeaders: z.boolean().optional(),
|
|
4227
|
+
includeStatic: z.boolean().optional()
|
|
4228
|
+
});
|
|
4229
|
+
browser_console_messages = z.object({
|
|
4230
|
+
limit: z.number().optional(),
|
|
4231
|
+
level: z.enum(["log", "warn", "error", "info"]).optional()
|
|
4232
|
+
});
|
|
4233
|
+
browser_html = z.object({
|
|
4234
|
+
selector: z.string().optional()
|
|
4235
|
+
});
|
|
4236
|
+
browser_close = z.object({
|
|
4237
|
+
targetId: z.string().optional(),
|
|
4238
|
+
force: z.boolean().optional(),
|
|
4239
|
+
closeAll: z.boolean().optional()
|
|
4240
|
+
});
|
|
4241
|
+
browser_wait_for = z.object({
|
|
4242
|
+
text: z.string().optional(),
|
|
4243
|
+
textGone: z.string().optional(),
|
|
4244
|
+
time: z.number().optional(),
|
|
4245
|
+
timeout: z.number().optional(),
|
|
4246
|
+
url: z.string().optional(),
|
|
4247
|
+
fn: z.string().optional(),
|
|
4248
|
+
selector: z.string().optional(),
|
|
4249
|
+
state: z.string().optional(),
|
|
4250
|
+
loadState: z.string().optional()
|
|
4251
|
+
});
|
|
4252
|
+
browser_hover = z.object({
|
|
4253
|
+
ref: z.string()
|
|
4254
|
+
});
|
|
4255
|
+
browser_drag = z.object({
|
|
4256
|
+
startRef: z.string(),
|
|
4257
|
+
endRef: z.string()
|
|
4258
|
+
});
|
|
4259
|
+
browser_select_option = z.object({
|
|
4260
|
+
ref: z.string(),
|
|
4261
|
+
values: z.array(z.string())
|
|
4262
|
+
});
|
|
4263
|
+
browser_handle_dialog = z.object({
|
|
4264
|
+
accept: z.boolean(),
|
|
4265
|
+
promptText: z.string().optional()
|
|
4266
|
+
});
|
|
4267
|
+
browser_file_upload = z.object({
|
|
4268
|
+
ref: z.string(),
|
|
4269
|
+
paths: z.array(z.string())
|
|
4270
|
+
});
|
|
4271
|
+
browser_resize = z.object({
|
|
4272
|
+
width: z.number().optional(),
|
|
4273
|
+
height: z.number().optional(),
|
|
4274
|
+
deviceScaleFactor: z.number().optional(),
|
|
4275
|
+
preset: z.string().optional()
|
|
4276
|
+
}).refine(
|
|
4277
|
+
(data) => {
|
|
4278
|
+
if (data.preset) return true;
|
|
4279
|
+
if (data.width !== void 0 && data.height !== void 0) return true;
|
|
4280
|
+
return false;
|
|
4281
|
+
},
|
|
4282
|
+
{ message: "Must provide both width and height together, or a preset" }
|
|
4283
|
+
);
|
|
4284
|
+
browser_annotated_screenshot = z.object({
|
|
4285
|
+
selector: z.string().optional()
|
|
4286
|
+
});
|
|
4287
|
+
browser_inspect_source = z.object({
|
|
4288
|
+
ref: z.string().optional(),
|
|
4289
|
+
selector: z.string().optional()
|
|
4290
|
+
}).refine(
|
|
4291
|
+
(data) => data.ref !== void 0 || data.selector !== void 0,
|
|
4292
|
+
{ message: "Must provide ref or selector" }
|
|
4293
|
+
);
|
|
4294
|
+
browser_route = z.object({
|
|
4295
|
+
url: z.string(),
|
|
4296
|
+
body: z.string(),
|
|
4297
|
+
status: z.number().optional(),
|
|
4298
|
+
headers: z.record(z.string()).optional()
|
|
4299
|
+
});
|
|
4300
|
+
browser_abort = z.object({
|
|
4301
|
+
url: z.string()
|
|
4302
|
+
});
|
|
4303
|
+
browser_unroute = z.object({
|
|
4304
|
+
url: z.string().optional(),
|
|
4305
|
+
all: z.boolean().optional()
|
|
4306
|
+
}).refine(
|
|
4307
|
+
(data) => data.url || data.all,
|
|
4308
|
+
{ message: "Must provide url or all" }
|
|
4309
|
+
);
|
|
4310
|
+
browser_find = z.object({
|
|
4311
|
+
role: z.string().optional(),
|
|
4312
|
+
name: z.string().optional(),
|
|
4313
|
+
text: z.string().optional(),
|
|
4314
|
+
nth: z.number().optional()
|
|
4315
|
+
}).refine(
|
|
4316
|
+
(data) => data.role !== void 0 || data.name !== void 0 || data.text !== void 0,
|
|
4317
|
+
{ message: "Must provide at least one of role, name, or text" }
|
|
4318
|
+
);
|
|
4319
|
+
browser_diff = z.object({
|
|
4320
|
+
before: z.string(),
|
|
4321
|
+
after: z.string().optional(),
|
|
4322
|
+
selector: z.string().optional(),
|
|
4323
|
+
threshold: z.number().optional()
|
|
4324
|
+
});
|
|
4325
|
+
browser_save_state = z.object({
|
|
4326
|
+
name: z.string()
|
|
4327
|
+
});
|
|
4328
|
+
browser_load_state = z.object({
|
|
4329
|
+
name: z.string(),
|
|
4330
|
+
url: z.string().optional()
|
|
4331
|
+
});
|
|
4332
|
+
browser_list = z.object({}).passthrough().optional().transform((val) => val ?? {});
|
|
4333
|
+
schemas = {
|
|
4334
|
+
browser_connect,
|
|
4335
|
+
browser_tabs,
|
|
4336
|
+
browser_snapshot,
|
|
4337
|
+
browser_screenshot,
|
|
4338
|
+
browser_click,
|
|
4339
|
+
browser_fill_form,
|
|
4340
|
+
browser_type,
|
|
4341
|
+
browser_press_key,
|
|
4342
|
+
browser_navigate,
|
|
4343
|
+
browser_navigate_back,
|
|
4344
|
+
browser_evaluate,
|
|
4345
|
+
browser_scroll,
|
|
4346
|
+
browser_network_requests,
|
|
4347
|
+
browser_console_messages,
|
|
4348
|
+
browser_html,
|
|
4349
|
+
browser_close,
|
|
4350
|
+
browser_wait_for,
|
|
4351
|
+
browser_hover,
|
|
4352
|
+
browser_drag,
|
|
4353
|
+
browser_select_option,
|
|
4354
|
+
browser_handle_dialog,
|
|
4355
|
+
browser_file_upload,
|
|
4356
|
+
browser_resize,
|
|
4357
|
+
browser_annotated_screenshot,
|
|
4358
|
+
browser_inspect_source,
|
|
4359
|
+
browser_route,
|
|
4360
|
+
browser_abort,
|
|
4361
|
+
browser_unroute,
|
|
4362
|
+
browser_find,
|
|
4363
|
+
browser_diff,
|
|
4364
|
+
browser_save_state,
|
|
4365
|
+
browser_load_state,
|
|
4366
|
+
browser_list
|
|
4367
|
+
};
|
|
4368
|
+
descriptions = {
|
|
4369
|
+
browser_connect: "Connect to a running browser instance via Chrome DevTools Protocol. Use headless: true to run in background without visible window. Use resync: true to force cookie re-sync (e.g. after switching Chrome profile).",
|
|
4370
|
+
browser_tabs: "List open browser tabs, optionally filtered by title or URL",
|
|
4371
|
+
browser_snapshot: "Capture an accessibility snapshot of the current page or a specific element. [~500 tokens, PREFERRED for page understanding]",
|
|
4372
|
+
browser_screenshot: "Take a screenshot. Auto-returns snapshot text unless visual: true, fullPage: true, or selector is specified. [~10K tokens when image returned]",
|
|
4373
|
+
browser_click: "Click an element identified by selector, ref, or coordinates",
|
|
4374
|
+
browser_fill_form: "Fill a form field identified by ref or selector with a value",
|
|
4375
|
+
browser_type: "Type text into the focused element or a specific ref",
|
|
4376
|
+
browser_press_key: "Press a keyboard key or key combination",
|
|
4377
|
+
browser_navigate: "Navigate to a URL",
|
|
4378
|
+
browser_navigate_back: "Navigate back or forward in browser history",
|
|
4379
|
+
browser_evaluate: "Evaluate a JavaScript expression in the page context",
|
|
4380
|
+
browser_scroll: "Scroll the page or a specific element in a given direction",
|
|
4381
|
+
browser_network_requests: "List captured network requests, optionally filtered",
|
|
4382
|
+
browser_console_messages: "Retrieve console messages from the page, optionally filtered by level",
|
|
4383
|
+
browser_html: "Get the HTML content of the page or a specific element",
|
|
4384
|
+
browser_close: "Close a browser tab or the entire browser",
|
|
4385
|
+
browser_wait_for: "Wait for a condition: text appearance/disappearance, time, URL, selector, or JS function",
|
|
4386
|
+
browser_hover: "Hover over an element identified by ref",
|
|
4387
|
+
browser_drag: "Drag from one element to another by ref",
|
|
4388
|
+
browser_select_option: "Select option(s) in a select element identified by ref",
|
|
4389
|
+
browser_handle_dialog: "Accept or dismiss a browser dialog (alert, confirm, prompt)",
|
|
4390
|
+
browser_file_upload: "Upload file(s) to a file input element identified by ref",
|
|
4391
|
+
browser_resize: "Resize the browser viewport to specific dimensions or a preset",
|
|
4392
|
+
browser_annotated_screenshot: "Take a screenshot with element annotations overlaid. [~12K tokens, prefer browser_snapshot { interactive: true } for text alternative]",
|
|
4393
|
+
browser_inspect_source: "Inspect a DOM element and return its source code location (file, line, component name). Works with React, Vue, Svelte, Solid.",
|
|
4394
|
+
browser_route: "Intercept requests matching a URL pattern and respond with a custom body, status, and headers",
|
|
4395
|
+
browser_abort: "Block requests matching a URL pattern",
|
|
4396
|
+
browser_unroute: "Remove request intercept rules \u2014 a specific pattern or all at once",
|
|
4397
|
+
browser_find: "Find elements by ARIA role, accessible name, or text content. Returns @eN ref for use with other tools.",
|
|
4398
|
+
browser_diff: "Compare two screenshots pixel-by-pixel. Returns diff percentage and visual diff image highlighting changes. [~11K tokens]",
|
|
4399
|
+
browser_save_state: "Save browser state (cookies, localStorage, sessionStorage) to a named file for later restoration",
|
|
4400
|
+
browser_load_state: "Load a previously saved browser state (cookies, storage) and optionally navigate to a URL",
|
|
4401
|
+
browser_list: "List available browser instances"
|
|
4402
|
+
};
|
|
4403
|
+
cNum = z.coerce.number();
|
|
4404
|
+
cBool = z.preprocess(
|
|
4405
|
+
(v) => v === "true" ? true : v === "false" ? false : v,
|
|
4406
|
+
z.boolean()
|
|
4407
|
+
);
|
|
4408
|
+
toolShapes = {
|
|
4409
|
+
browser_connect: { port: cNum.optional(), host: z.string().optional(), headless: cBool.optional(), resync: cBool.optional() },
|
|
4410
|
+
browser_tabs: { filter: z.string().optional() },
|
|
4411
|
+
browser_snapshot: {
|
|
4412
|
+
selector: z.string().optional(),
|
|
4413
|
+
compact: cBool.optional(),
|
|
4414
|
+
interactive: cBool.optional(),
|
|
4415
|
+
cursor: cBool.optional(),
|
|
4416
|
+
depth: cNum.optional()
|
|
4417
|
+
},
|
|
4418
|
+
browser_screenshot: {
|
|
4419
|
+
selector: z.string().optional(),
|
|
4420
|
+
fullPage: cBool.optional(),
|
|
4421
|
+
format: z.enum(["png", "jpeg"]).optional(),
|
|
4422
|
+
quality: cNum.optional(),
|
|
4423
|
+
annotate: cBool.optional(),
|
|
4424
|
+
visual: cBool.optional()
|
|
4425
|
+
},
|
|
4426
|
+
browser_click: {
|
|
4427
|
+
selector: z.string().optional(),
|
|
4428
|
+
ref: z.string().optional(),
|
|
4429
|
+
x: cNum.optional(),
|
|
4430
|
+
y: cNum.optional(),
|
|
4431
|
+
newTab: cBool.optional()
|
|
4432
|
+
},
|
|
4433
|
+
browser_fill_form: { ref: z.string().optional(), selector: z.string().optional(), value: z.string() },
|
|
4434
|
+
browser_type: { text: z.string(), ref: z.string().optional(), slowly: cBool.optional(), submit: cBool.optional() },
|
|
4435
|
+
browser_press_key: { key: z.string() },
|
|
4436
|
+
browser_navigate: { url: z.string(), waitUntil: z.enum(["load", "domcontentloaded", "networkidle"]).optional() },
|
|
4437
|
+
browser_navigate_back: { direction: z.enum(["back", "forward"]).optional() },
|
|
4438
|
+
browser_evaluate: { expression: z.string(), frameId: z.string().optional() },
|
|
4439
|
+
browser_scroll: { direction: z.enum(["up", "down", "left", "right"]).optional(), pixels: cNum.optional(), selector: z.string().optional() },
|
|
4440
|
+
browser_network_requests: { filter: z.string().optional(), limit: cNum.optional(), includeHeaders: cBool.optional(), includeStatic: cBool.optional() },
|
|
4441
|
+
browser_console_messages: { limit: cNum.optional(), level: z.enum(["log", "warn", "error", "info"]).optional() },
|
|
4442
|
+
browser_html: { selector: z.string().optional() },
|
|
4443
|
+
browser_close: { targetId: z.string().optional(), force: cBool.optional(), closeAll: cBool.optional() },
|
|
4444
|
+
browser_wait_for: { text: z.string().optional(), textGone: z.string().optional(), time: cNum.optional(), timeout: cNum.optional(), url: z.string().optional(), fn: z.string().optional(), selector: z.string().optional(), state: z.string().optional(), loadState: z.string().optional() },
|
|
4445
|
+
browser_hover: { ref: z.string() },
|
|
4446
|
+
browser_drag: { startRef: z.string(), endRef: z.string() },
|
|
4447
|
+
browser_select_option: { ref: z.string(), values: z.array(z.string()) },
|
|
4448
|
+
browser_handle_dialog: { accept: cBool, promptText: z.string().optional() },
|
|
4449
|
+
browser_file_upload: { ref: z.string(), paths: z.array(z.string()) },
|
|
4450
|
+
browser_resize: { width: cNum.optional(), height: cNum.optional(), deviceScaleFactor: cNum.optional(), preset: z.string().optional() },
|
|
4451
|
+
browser_annotated_screenshot: { selector: z.string().optional() },
|
|
4452
|
+
browser_inspect_source: { ref: z.string().optional(), selector: z.string().optional() },
|
|
4453
|
+
browser_route: { url: z.string(), body: z.string(), status: cNum.optional(), headers: z.record(z.string()).optional() },
|
|
4454
|
+
browser_abort: { url: z.string() },
|
|
4455
|
+
browser_unroute: { url: z.string().optional(), all: cBool.optional() },
|
|
4456
|
+
browser_find: { role: z.string().optional(), name: z.string().optional(), text: z.string().optional(), nth: cNum.optional() },
|
|
4457
|
+
browser_diff: { before: z.string(), after: z.string().optional(), selector: z.string().optional(), threshold: cNum.optional() },
|
|
4458
|
+
browser_save_state: { name: z.string() },
|
|
4459
|
+
browser_load_state: { name: z.string(), url: z.string().optional() },
|
|
4460
|
+
browser_list: {}
|
|
4461
|
+
};
|
|
4462
|
+
}
|
|
4463
|
+
});
|
|
4464
|
+
|
|
4465
|
+
// src/server.ts
|
|
4466
|
+
var server_exports = {};
|
|
4467
|
+
__export(server_exports, {
|
|
4468
|
+
createServer: () => createServer
|
|
4469
|
+
});
|
|
4470
|
+
import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
|
|
4471
|
+
async function createServer() {
|
|
4472
|
+
const server = new McpServer({
|
|
4473
|
+
name: "browsirai",
|
|
4474
|
+
version: VERSION
|
|
4475
|
+
});
|
|
4476
|
+
registerTools(server);
|
|
4477
|
+
return server;
|
|
4478
|
+
}
|
|
4479
|
+
var init_server = __esm({
|
|
4480
|
+
"src/server.ts"() {
|
|
4481
|
+
"use strict";
|
|
4482
|
+
init_version();
|
|
4483
|
+
init_tools();
|
|
4484
|
+
}
|
|
4485
|
+
});
|
|
4486
|
+
|
|
4487
|
+
// src/adapters/detect.ts
|
|
4488
|
+
function detectPlatform() {
|
|
4489
|
+
if (process.env.CLAUDE_PROJECT_DIR) {
|
|
4490
|
+
return {
|
|
4491
|
+
platform: "claude-code",
|
|
4492
|
+
confidence: "high",
|
|
4493
|
+
reason: `CLAUDE_PROJECT_DIR is set (${process.env.CLAUDE_PROJECT_DIR})`
|
|
4494
|
+
};
|
|
4495
|
+
}
|
|
4496
|
+
if (process.env.CURSOR_TRACE_ID) {
|
|
4497
|
+
return {
|
|
4498
|
+
platform: "cursor",
|
|
4499
|
+
confidence: "high",
|
|
4500
|
+
reason: `CURSOR_TRACE_ID is set (${process.env.CURSOR_TRACE_ID})`
|
|
4501
|
+
};
|
|
4502
|
+
}
|
|
4503
|
+
if (process.env.GEMINI_CLI) {
|
|
4504
|
+
return {
|
|
4505
|
+
platform: "gemini-cli",
|
|
4506
|
+
confidence: "high",
|
|
4507
|
+
reason: `GEMINI_CLI is set (${process.env.GEMINI_CLI})`
|
|
4508
|
+
};
|
|
4509
|
+
}
|
|
4510
|
+
if (process.env.VSCODE_PID) {
|
|
4511
|
+
return {
|
|
4512
|
+
platform: "vscode-copilot",
|
|
4513
|
+
confidence: "medium",
|
|
4514
|
+
reason: `VSCODE_PID is set (${process.env.VSCODE_PID})`
|
|
4515
|
+
};
|
|
4516
|
+
}
|
|
4517
|
+
if (process.env.TERM_PROGRAM === "vscode") {
|
|
4518
|
+
return {
|
|
4519
|
+
platform: "vscode-copilot",
|
|
4520
|
+
confidence: "medium",
|
|
4521
|
+
reason: `TERM_PROGRAM is set to vscode`
|
|
4522
|
+
};
|
|
4523
|
+
}
|
|
4524
|
+
if (process.env.OPENCODE_CONFIG) {
|
|
4525
|
+
return {
|
|
4526
|
+
platform: "opencode",
|
|
4527
|
+
confidence: "high",
|
|
4528
|
+
reason: `OPENCODE_CONFIG is set (${process.env.OPENCODE_CONFIG})`
|
|
4529
|
+
};
|
|
4530
|
+
}
|
|
4531
|
+
return {
|
|
4532
|
+
platform: "generic",
|
|
4533
|
+
confidence: "low",
|
|
4534
|
+
reason: "No platform-specific environment variables detected"
|
|
4535
|
+
};
|
|
4536
|
+
}
|
|
4537
|
+
function getInstallConfig(platformId) {
|
|
4538
|
+
const config = installConfigs[platformId];
|
|
4539
|
+
return {
|
|
4540
|
+
configPath: config.configPath,
|
|
4541
|
+
configKey: config.configKey,
|
|
4542
|
+
serverEntry: {
|
|
4543
|
+
command: "npx",
|
|
4544
|
+
args: ["-y", "browsirai"]
|
|
4545
|
+
}
|
|
4546
|
+
};
|
|
4547
|
+
}
|
|
4548
|
+
var installConfigs;
|
|
4549
|
+
var init_detect = __esm({
|
|
4550
|
+
"src/adapters/detect.ts"() {
|
|
4551
|
+
"use strict";
|
|
4552
|
+
installConfigs = {
|
|
4553
|
+
"claude-code": {
|
|
4554
|
+
configPath: ".mcp.json",
|
|
4555
|
+
configKey: "mcpServers"
|
|
4556
|
+
},
|
|
4557
|
+
cursor: {
|
|
4558
|
+
configPath: ".cursor/mcp.json",
|
|
4559
|
+
configKey: "mcpServers"
|
|
4560
|
+
},
|
|
4561
|
+
"gemini-cli": {
|
|
4562
|
+
configPath: "~/.gemini/settings.json",
|
|
4563
|
+
configKey: "mcpServers"
|
|
4564
|
+
},
|
|
4565
|
+
windsurf: {
|
|
4566
|
+
configPath: "~/.codeium/windsurf/mcp_config.json",
|
|
4567
|
+
configKey: "mcpServers"
|
|
4568
|
+
},
|
|
4569
|
+
cline: {
|
|
4570
|
+
configPath: "~/.config/Code/User/globalStorage/saoudrizwan.claude-dev/settings/cline_mcp_settings.json",
|
|
4571
|
+
configKey: "mcpServers"
|
|
4572
|
+
},
|
|
4573
|
+
"vscode-copilot": {
|
|
4574
|
+
configPath: ".vscode/mcp.json",
|
|
4575
|
+
configKey: "servers"
|
|
4576
|
+
},
|
|
4577
|
+
opencode: {
|
|
4578
|
+
configPath: "opencode.json",
|
|
4579
|
+
configKey: "mcpServers"
|
|
4580
|
+
},
|
|
4581
|
+
zed: {
|
|
4582
|
+
configPath: "~/.config/zed/settings.json",
|
|
4583
|
+
configKey: "context_servers"
|
|
4584
|
+
},
|
|
4585
|
+
continue: {
|
|
4586
|
+
configPath: "~/.continue/config.yaml",
|
|
4587
|
+
configKey: "mcpServers"
|
|
4588
|
+
},
|
|
4589
|
+
generic: {
|
|
4590
|
+
configPath: "mcp.json",
|
|
4591
|
+
configKey: "mcpServers"
|
|
4592
|
+
}
|
|
4593
|
+
};
|
|
4594
|
+
}
|
|
4595
|
+
});
|
|
4596
|
+
|
|
4597
|
+
// src/doctor.ts
|
|
4598
|
+
var doctor_exports = {};
|
|
4599
|
+
__export(doctor_exports, {
|
|
4600
|
+
runDoctor: () => runDoctor
|
|
4601
|
+
});
|
|
4602
|
+
import { execSync as execSync3 } from "child_process";
|
|
4603
|
+
import { existsSync as existsSync4, readFileSync as readFileSync4 } from "fs";
|
|
4604
|
+
function findChromePath() {
|
|
4605
|
+
const whichCommands = process.platform === "win32" ? ["where chrome", "where chromium", "where msedge"] : ["which google-chrome", "which chromium", "which chromium-browser", "which chrome"];
|
|
4606
|
+
for (const cmd of whichCommands) {
|
|
4607
|
+
try {
|
|
4608
|
+
const result = execSync3(cmd, { stdio: "pipe" });
|
|
4609
|
+
const path = result.toString().trim();
|
|
4610
|
+
if (path) return path;
|
|
4611
|
+
} catch {
|
|
4612
|
+
}
|
|
4613
|
+
}
|
|
4614
|
+
if (process.platform === "darwin") {
|
|
4615
|
+
const macPaths = [
|
|
4616
|
+
"/Applications/Google Chrome.app/Contents/MacOS/Google Chrome",
|
|
4617
|
+
"/Applications/Microsoft Edge.app/Contents/MacOS/Microsoft Edge",
|
|
4618
|
+
"/Applications/Brave Browser.app/Contents/MacOS/Brave Browser"
|
|
4619
|
+
];
|
|
4620
|
+
for (const p of macPaths) {
|
|
4621
|
+
try {
|
|
4622
|
+
execSync3(`test -x '${p}'`, { stdio: "pipe" });
|
|
4623
|
+
return p;
|
|
4624
|
+
} catch {
|
|
4625
|
+
}
|
|
4626
|
+
}
|
|
4627
|
+
}
|
|
4628
|
+
return null;
|
|
4629
|
+
}
|
|
4630
|
+
function checkNodeVersion() {
|
|
4631
|
+
const versionStr = process.version.replace(/^v/, "");
|
|
4632
|
+
const major = parseInt(versionStr.split(".")[0], 10);
|
|
4633
|
+
const ok = major >= 18;
|
|
4634
|
+
return {
|
|
4635
|
+
ok,
|
|
4636
|
+
label: "Node.js version",
|
|
4637
|
+
message: ok ? `v${versionStr} (>= 18 required)` : `v${versionStr} \u2014 Node.js >= 18 is required`
|
|
4638
|
+
};
|
|
4639
|
+
}
|
|
4640
|
+
function checkPlatformConfig() {
|
|
4641
|
+
const detection = detectPlatform();
|
|
4642
|
+
const config = getInstallConfig(detection.platform);
|
|
4643
|
+
const configPath = config.configPath;
|
|
4644
|
+
if (!existsSync4(configPath)) {
|
|
4645
|
+
return {
|
|
4646
|
+
ok: false,
|
|
4647
|
+
label: "Platform config",
|
|
4648
|
+
message: `Config file not found: ${configPath} (platform: ${detection.platform})`
|
|
4649
|
+
};
|
|
4650
|
+
}
|
|
4651
|
+
try {
|
|
4652
|
+
const content = readFileSync4(configPath, "utf-8");
|
|
4653
|
+
const parsed = JSON.parse(content);
|
|
4654
|
+
const servers = parsed[config.configKey];
|
|
4655
|
+
if (servers && typeof servers === "object" && "browsirai" in servers) {
|
|
4656
|
+
return {
|
|
4657
|
+
ok: true,
|
|
4658
|
+
label: "Platform config",
|
|
4659
|
+
message: `browsirai found in ${configPath} (platform: ${detection.platform})`
|
|
4660
|
+
};
|
|
4661
|
+
}
|
|
4662
|
+
return {
|
|
4663
|
+
ok: false,
|
|
4664
|
+
label: "Platform config",
|
|
4665
|
+
message: `browsirai not found in ${configPath} under "${config.configKey}"`
|
|
4666
|
+
};
|
|
4667
|
+
} catch {
|
|
4668
|
+
return {
|
|
4669
|
+
ok: false,
|
|
4670
|
+
label: "Platform config",
|
|
4671
|
+
message: `Failed to parse config file: ${configPath}`
|
|
4672
|
+
};
|
|
4673
|
+
}
|
|
4674
|
+
}
|
|
4675
|
+
async function runDoctor() {
|
|
4676
|
+
const checks = [];
|
|
4677
|
+
const method = getInstallMethod();
|
|
4678
|
+
checks.push({
|
|
4679
|
+
ok: true,
|
|
4680
|
+
label: "browsirai version",
|
|
4681
|
+
message: `v${VERSION} (${method})`
|
|
4682
|
+
});
|
|
4683
|
+
checks.push({
|
|
4684
|
+
ok: true,
|
|
4685
|
+
label: "Install path",
|
|
4686
|
+
message: getInstallPath()
|
|
4687
|
+
});
|
|
4688
|
+
let status = getUpgradeStatus();
|
|
4689
|
+
if (!status) {
|
|
4690
|
+
status = await checkForUpgrade();
|
|
4691
|
+
}
|
|
4692
|
+
if (status) {
|
|
4693
|
+
const upToDate = status.current === status.latest;
|
|
4694
|
+
checks.push({
|
|
4695
|
+
ok: upToDate,
|
|
4696
|
+
label: "Latest version",
|
|
4697
|
+
message: upToDate ? `v${status.latest} (up to date)` : `v${status.latest} available (current: v${status.current}, restart to apply)`
|
|
4698
|
+
});
|
|
4699
|
+
}
|
|
4700
|
+
const chromePath = findChromePath();
|
|
4701
|
+
checks.push({
|
|
4702
|
+
ok: chromePath !== null,
|
|
4703
|
+
label: "Chrome/Chromium installed",
|
|
4704
|
+
message: chromePath ? `Found at ${chromePath}` : "Chrome or Chromium not found in PATH"
|
|
4705
|
+
});
|
|
4706
|
+
checks.push(checkNodeVersion());
|
|
4707
|
+
const { connectChrome: connectChrome2 } = await Promise.resolve().then(() => (init_chrome_launcher(), chrome_launcher_exports));
|
|
4708
|
+
const connection = await connectChrome2({ autoLaunch: !!chromePath });
|
|
4709
|
+
if (connection.success) {
|
|
4710
|
+
checks.push({
|
|
4711
|
+
ok: true,
|
|
4712
|
+
label: "CDP connection",
|
|
4713
|
+
message: connection.wsEndpoint ? `Connected (port ${connection.port}, --remote-debugging-port)` : `CDP reachable on port ${connection.port}`
|
|
4714
|
+
});
|
|
4715
|
+
} else {
|
|
4716
|
+
checks.push({
|
|
4717
|
+
ok: false,
|
|
4718
|
+
label: "CDP connection",
|
|
4719
|
+
message: connection.error ?? "CDP not available"
|
|
4720
|
+
});
|
|
4721
|
+
}
|
|
4722
|
+
checks.push(checkPlatformConfig());
|
|
4723
|
+
console.log("browsirai doctor: running diagnostics...\n");
|
|
4724
|
+
for (const check of checks) {
|
|
4725
|
+
const icon = check.ok ? "PASS" : "FAIL";
|
|
4726
|
+
console.log(` [${icon}] ${check.label}`);
|
|
4727
|
+
if (check.message) {
|
|
4728
|
+
console.log(` ${check.message}`);
|
|
4729
|
+
}
|
|
4730
|
+
}
|
|
4731
|
+
const allPassed = checks.every((c) => c.ok);
|
|
4732
|
+
console.log(allPassed ? "\nAll checks passed!" : "\nSome checks failed.");
|
|
4733
|
+
return checks;
|
|
4734
|
+
}
|
|
4735
|
+
var init_doctor = __esm({
|
|
4736
|
+
"src/doctor.ts"() {
|
|
4737
|
+
"use strict";
|
|
4738
|
+
init_detect();
|
|
4739
|
+
init_version();
|
|
4740
|
+
init_upgrade();
|
|
4741
|
+
}
|
|
4742
|
+
});
|
|
4743
|
+
|
|
4744
|
+
// src/install.ts
|
|
4745
|
+
var install_exports = {};
|
|
4746
|
+
__export(install_exports, {
|
|
4747
|
+
runInstall: () => runInstall
|
|
4748
|
+
});
|
|
4749
|
+
import { intro, select, confirm, spinner, outro, isCancel, cancel, log } from "@clack/prompts";
|
|
4750
|
+
import { existsSync as existsSync5, readFileSync as readFileSync5, writeFileSync as writeFileSync3, mkdirSync as mkdirSync4 } from "fs";
|
|
4751
|
+
import { resolve, dirname as dirname2 } from "path";
|
|
4752
|
+
import { homedir as homedir4 } from "os";
|
|
4753
|
+
function resolveConfigPath(configPath, scope) {
|
|
4754
|
+
if (configPath.startsWith("~")) {
|
|
4755
|
+
return resolve(homedir4(), configPath.slice(2));
|
|
4756
|
+
}
|
|
4757
|
+
if (scope === "global") {
|
|
4758
|
+
return resolve(homedir4(), configPath);
|
|
4759
|
+
}
|
|
4760
|
+
return resolve(process.cwd(), configPath);
|
|
4761
|
+
}
|
|
4762
|
+
async function runInstall() {
|
|
4763
|
+
intro("browsirai installer");
|
|
4764
|
+
const detected = detectPlatform();
|
|
4765
|
+
const installedPlatforms = /* @__PURE__ */ new Set();
|
|
4766
|
+
for (const opt of platformOptions) {
|
|
4767
|
+
const config2 = getInstallConfig(opt.value);
|
|
4768
|
+
const paths = [
|
|
4769
|
+
resolve(process.cwd(), config2.configPath),
|
|
4770
|
+
config2.configPath.startsWith("~") ? resolve(homedir4(), config2.configPath.slice(2)) : resolve(homedir4(), config2.configPath)
|
|
4771
|
+
];
|
|
4772
|
+
for (const filePath2 of paths) {
|
|
4773
|
+
if (existsSync5(filePath2)) {
|
|
4774
|
+
try {
|
|
4775
|
+
const existing = JSON.parse(readFileSync5(filePath2, "utf-8"));
|
|
4776
|
+
const section = existing[config2.configKey];
|
|
4777
|
+
if (section?.browsirai) installedPlatforms.add(opt.value);
|
|
4778
|
+
} catch {
|
|
4779
|
+
}
|
|
4780
|
+
}
|
|
4781
|
+
}
|
|
4782
|
+
}
|
|
4783
|
+
const platform = await select({
|
|
4784
|
+
message: "Select your AI coding platform:",
|
|
4785
|
+
options: platformOptions.map((opt) => ({
|
|
4786
|
+
value: opt.value,
|
|
4787
|
+
label: installedPlatforms.has(opt.value) ? `${opt.label} (installed)` : opt.label
|
|
4788
|
+
})),
|
|
4789
|
+
initialValue: detected.platform
|
|
4790
|
+
});
|
|
4791
|
+
if (isCancel(platform)) {
|
|
4792
|
+
cancel("Installation cancelled.");
|
|
4793
|
+
return;
|
|
4794
|
+
}
|
|
4795
|
+
const scope = await select({
|
|
4796
|
+
message: "Install scope:",
|
|
4797
|
+
options: [
|
|
4798
|
+
{ value: "project", label: "Project (current directory)" },
|
|
4799
|
+
{ value: "global", label: "Global (user home)" }
|
|
4800
|
+
]
|
|
4801
|
+
});
|
|
4802
|
+
if (isCancel(scope)) {
|
|
4803
|
+
cancel("Installation cancelled.");
|
|
4804
|
+
return;
|
|
4805
|
+
}
|
|
4806
|
+
const selectedPlatform = platform;
|
|
4807
|
+
const selectedScope = scope;
|
|
4808
|
+
const config = getInstallConfig(selectedPlatform);
|
|
4809
|
+
const serverConfig = {
|
|
4810
|
+
[config.configKey]: {
|
|
4811
|
+
browsirai: config.serverEntry
|
|
4812
|
+
}
|
|
4813
|
+
};
|
|
4814
|
+
const filePath = resolveConfigPath(config.configPath, selectedScope);
|
|
4815
|
+
if (existsSync5(filePath)) {
|
|
4816
|
+
const existingRaw = readFileSync5(filePath, "utf-8");
|
|
4817
|
+
const existingConfig = JSON.parse(existingRaw);
|
|
4818
|
+
const shouldMerge = await confirm({
|
|
4819
|
+
message: `Config file already exists at ${filePath}. Merge browsirai into it?`
|
|
4820
|
+
});
|
|
4821
|
+
if (isCancel(shouldMerge) || !shouldMerge) {
|
|
4822
|
+
cancel("Installation cancelled.");
|
|
4823
|
+
return;
|
|
4824
|
+
}
|
|
4825
|
+
const existingSection = existingConfig[config.configKey] ?? {};
|
|
4826
|
+
existingConfig[config.configKey] = {
|
|
4827
|
+
...existingSection,
|
|
4828
|
+
browsirai: config.serverEntry
|
|
4829
|
+
};
|
|
4830
|
+
mkdirSync4(dirname2(filePath), { recursive: true });
|
|
4831
|
+
writeFileSync3(filePath, JSON.stringify(existingConfig, null, 2));
|
|
4832
|
+
} else {
|
|
4833
|
+
mkdirSync4(dirname2(filePath), { recursive: true });
|
|
4834
|
+
writeFileSync3(filePath, JSON.stringify(serverConfig, null, 2));
|
|
4835
|
+
}
|
|
4836
|
+
log.success(`Config written to ${filePath}`);
|
|
4837
|
+
const s = spinner();
|
|
4838
|
+
s.start("Connecting to Chrome DevTools Protocol...");
|
|
4839
|
+
const connection = await connectChrome({ autoLaunch: true });
|
|
4840
|
+
if (connection.success) {
|
|
4841
|
+
s.stop(connection.wsEndpoint ? `Connected to Chrome (port ${connection.port})` : `CDP reachable on port ${connection.port}`);
|
|
4842
|
+
} else {
|
|
4843
|
+
s.stop(connection.error ?? "Could not connect to Chrome");
|
|
4844
|
+
log.warn("Run `browsirai doctor` to diagnose.");
|
|
4845
|
+
}
|
|
4846
|
+
outro("browsirai is ready! Your AI agent can now control your browser.");
|
|
4847
|
+
}
|
|
4848
|
+
var platformOptions;
|
|
4849
|
+
var init_install = __esm({
|
|
4850
|
+
"src/install.ts"() {
|
|
4851
|
+
"use strict";
|
|
4852
|
+
init_detect();
|
|
4853
|
+
init_chrome_launcher();
|
|
4854
|
+
platformOptions = [
|
|
4855
|
+
{ value: "claude-code", label: "Claude Code" },
|
|
4856
|
+
{ value: "cursor", label: "Cursor" },
|
|
4857
|
+
{ value: "gemini-cli", label: "Gemini CLI" },
|
|
4858
|
+
{ value: "vscode-copilot", label: "VS Code Copilot" },
|
|
4859
|
+
{ value: "opencode", label: "OpenCode" },
|
|
4860
|
+
{ value: "zed", label: "Zed" },
|
|
4861
|
+
{ value: "windsurf", label: "Windsurf" },
|
|
4862
|
+
{ value: "cline", label: "Cline" },
|
|
4863
|
+
{ value: "continue", label: "Continue" }
|
|
4864
|
+
];
|
|
4865
|
+
}
|
|
4866
|
+
});
|
|
4867
|
+
|
|
4868
|
+
// src/cli.ts
|
|
4869
|
+
init_version();
|
|
4870
|
+
process.on("unhandledRejection", () => {
|
|
4871
|
+
});
|
|
4872
|
+
process.on("uncaughtException", () => {
|
|
4873
|
+
});
|
|
4874
|
+
async function runCli(args) {
|
|
4875
|
+
const command = args[0];
|
|
4876
|
+
switch (command) {
|
|
4877
|
+
case void 0: {
|
|
4878
|
+
const { createServer: createServer2 } = await Promise.resolve().then(() => (init_server(), server_exports));
|
|
4879
|
+
const { StdioServerTransport } = await import("@modelcontextprotocol/sdk/server/stdio.js");
|
|
4880
|
+
const server = await createServer2();
|
|
4881
|
+
const transport = new StdioServerTransport();
|
|
4882
|
+
await server.connect(transport);
|
|
4883
|
+
Promise.resolve().then(() => (init_upgrade(), upgrade_exports)).then((m) => m.checkForUpgrade()).catch(() => {
|
|
4884
|
+
});
|
|
4885
|
+
break;
|
|
4886
|
+
}
|
|
4887
|
+
case "doctor": {
|
|
4888
|
+
const { runDoctor: runDoctor2 } = await Promise.resolve().then(() => (init_doctor(), doctor_exports));
|
|
4889
|
+
await runDoctor2();
|
|
4890
|
+
break;
|
|
4891
|
+
}
|
|
4892
|
+
case "install": {
|
|
4893
|
+
const { runInstall: runInstall2 } = await Promise.resolve().then(() => (init_install(), install_exports));
|
|
4894
|
+
await runInstall2();
|
|
4895
|
+
break;
|
|
4896
|
+
}
|
|
4897
|
+
case "--version":
|
|
4898
|
+
case "-v": {
|
|
4899
|
+
console.log(VERSION);
|
|
4900
|
+
break;
|
|
4901
|
+
}
|
|
4902
|
+
default: {
|
|
4903
|
+
console.error(`Unknown command: ${command}`);
|
|
4904
|
+
console.log(
|
|
4905
|
+
[
|
|
4906
|
+
`browsirai v${VERSION}`,
|
|
4907
|
+
"",
|
|
4908
|
+
"Usage: browsirai [command]",
|
|
4909
|
+
"",
|
|
4910
|
+
"Commands:",
|
|
4911
|
+
" (none) Start the MCP server (default)",
|
|
4912
|
+
" doctor Run diagnostics",
|
|
4913
|
+
" install Install browser integration",
|
|
4914
|
+
" --version Print version",
|
|
4915
|
+
""
|
|
4916
|
+
].join("\n")
|
|
4917
|
+
);
|
|
4918
|
+
break;
|
|
4919
|
+
}
|
|
4920
|
+
}
|
|
4921
|
+
}
|
|
4922
|
+
export {
|
|
4923
|
+
runCli
|
|
4924
|
+
};
|
|
4925
|
+
//# sourceMappingURL=cli.js.map
|