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