browserclaw 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.

Potentially problematic release.


This version of browserclaw might be problematic. Click here for more details.

package/dist/index.js ADDED
@@ -0,0 +1,2173 @@
1
+ import os from 'os';
2
+ import path from 'path';
3
+ import fs from 'fs';
4
+ import net from 'net';
5
+ import { spawn, execFileSync } from 'child_process';
6
+ import { chromium } from 'playwright-core';
7
+
8
+ // src/chrome-launcher.ts
9
+ var CHROMIUM_BUNDLE_IDS = /* @__PURE__ */ new Set([
10
+ "com.google.Chrome",
11
+ "com.google.Chrome.beta",
12
+ "com.google.Chrome.canary",
13
+ "com.google.Chrome.dev",
14
+ "com.brave.Browser",
15
+ "com.brave.Browser.beta",
16
+ "com.brave.Browser.nightly",
17
+ "com.microsoft.Edge",
18
+ "com.microsoft.EdgeBeta",
19
+ "com.microsoft.EdgeDev",
20
+ "com.microsoft.EdgeCanary",
21
+ "org.chromium.Chromium",
22
+ "com.vivaldi.Vivaldi",
23
+ "com.operasoftware.Opera",
24
+ "com.operasoftware.OperaGX",
25
+ "com.yandex.desktop.yandex-browser",
26
+ "company.thebrowser.Browser"
27
+ ]);
28
+ var CHROMIUM_DESKTOP_IDS = /* @__PURE__ */ new Set([
29
+ "google-chrome.desktop",
30
+ "google-chrome-beta.desktop",
31
+ "google-chrome-unstable.desktop",
32
+ "brave-browser.desktop",
33
+ "microsoft-edge.desktop",
34
+ "microsoft-edge-beta.desktop",
35
+ "microsoft-edge-dev.desktop",
36
+ "microsoft-edge-canary.desktop",
37
+ "chromium.desktop",
38
+ "chromium-browser.desktop",
39
+ "vivaldi.desktop",
40
+ "vivaldi-stable.desktop",
41
+ "opera.desktop",
42
+ "opera-gx.desktop",
43
+ "yandex-browser.desktop",
44
+ "org.chromium.Chromium.desktop"
45
+ ]);
46
+ var CHROMIUM_EXE_NAMES = /* @__PURE__ */ new Set([
47
+ "chrome.exe",
48
+ "msedge.exe",
49
+ "brave.exe",
50
+ "brave-browser.exe",
51
+ "chromium.exe",
52
+ "vivaldi.exe",
53
+ "opera.exe",
54
+ "launcher.exe",
55
+ "yandex.exe",
56
+ "yandexbrowser.exe",
57
+ "google chrome",
58
+ "google chrome canary",
59
+ "brave browser",
60
+ "microsoft edge",
61
+ "chromium",
62
+ "chrome",
63
+ "brave",
64
+ "msedge",
65
+ "brave-browser",
66
+ "google-chrome",
67
+ "google-chrome-stable",
68
+ "google-chrome-beta",
69
+ "google-chrome-unstable",
70
+ "microsoft-edge",
71
+ "microsoft-edge-beta",
72
+ "microsoft-edge-dev",
73
+ "microsoft-edge-canary",
74
+ "chromium-browser",
75
+ "vivaldi",
76
+ "vivaldi-stable",
77
+ "opera",
78
+ "opera-stable",
79
+ "opera-gx",
80
+ "yandex-browser"
81
+ ]);
82
+ function fileExists(filePath) {
83
+ try {
84
+ return fs.existsSync(filePath);
85
+ } catch {
86
+ return false;
87
+ }
88
+ }
89
+ function execText(command, args, timeoutMs = 1200) {
90
+ try {
91
+ const output = execFileSync(command, args, {
92
+ timeout: timeoutMs,
93
+ encoding: "utf8",
94
+ maxBuffer: 1024 * 1024
95
+ });
96
+ return String(output ?? "").trim() || null;
97
+ } catch {
98
+ return null;
99
+ }
100
+ }
101
+ function inferKindFromIdentifier(identifier) {
102
+ const id = identifier.toLowerCase();
103
+ if (id.includes("brave")) return "brave";
104
+ if (id.includes("edge")) return "edge";
105
+ if (id.includes("chromium")) return "chromium";
106
+ if (id.includes("canary")) return "canary";
107
+ if (id.includes("opera") || id.includes("vivaldi") || id.includes("yandex") || id.includes("thebrowser")) return "chromium";
108
+ return "chrome";
109
+ }
110
+ function inferKindFromExeName(name) {
111
+ const lower = name.toLowerCase();
112
+ if (lower.includes("brave")) return "brave";
113
+ if (lower.includes("edge") || lower.includes("msedge")) return "edge";
114
+ if (lower.includes("chromium")) return "chromium";
115
+ if (lower.includes("canary") || lower.includes("sxs")) return "canary";
116
+ if (lower.includes("opera") || lower.includes("vivaldi") || lower.includes("yandex")) return "chromium";
117
+ return "chrome";
118
+ }
119
+ function findFirstExe(candidates) {
120
+ for (const c of candidates) if (fileExists(c.path)) return c;
121
+ return null;
122
+ }
123
+ function detectDefaultBrowserBundleIdMac() {
124
+ const plistPath = path.join(os.homedir(), "Library/Preferences/com.apple.LaunchServices/com.apple.launchservices.secure.plist");
125
+ if (!fileExists(plistPath)) return null;
126
+ const handlersRaw = execText("/usr/bin/plutil", ["-extract", "LSHandlers", "json", "-o", "-", "--", plistPath], 2e3);
127
+ if (!handlersRaw) return null;
128
+ let handlers;
129
+ try {
130
+ handlers = JSON.parse(handlersRaw);
131
+ } catch {
132
+ return null;
133
+ }
134
+ if (!Array.isArray(handlers)) return null;
135
+ const resolveScheme = (scheme) => {
136
+ let candidate = null;
137
+ for (const entry of handlers) {
138
+ if (!entry || typeof entry !== "object") continue;
139
+ if (entry.LSHandlerURLScheme !== scheme) continue;
140
+ const role = typeof entry.LSHandlerRoleAll === "string" && entry.LSHandlerRoleAll || typeof entry.LSHandlerRoleViewer === "string" && entry.LSHandlerRoleViewer || null;
141
+ if (role) candidate = role;
142
+ }
143
+ return candidate;
144
+ };
145
+ return resolveScheme("http") ?? resolveScheme("https");
146
+ }
147
+ function detectDefaultChromiumMac() {
148
+ const bundleId = detectDefaultBrowserBundleIdMac();
149
+ if (!bundleId || !CHROMIUM_BUNDLE_IDS.has(bundleId)) return null;
150
+ const appPathRaw = execText("/usr/bin/osascript", ["-e", `POSIX path of (path to application id "${bundleId}")`]);
151
+ if (!appPathRaw) return null;
152
+ const appPath = appPathRaw.trim().replace(/\/$/, "");
153
+ const exeName = execText("/usr/bin/defaults", ["read", path.join(appPath, "Contents", "Info"), "CFBundleExecutable"]);
154
+ if (!exeName) return null;
155
+ const exePath = path.join(appPath, "Contents", "MacOS", exeName.trim());
156
+ if (!fileExists(exePath)) return null;
157
+ return { kind: inferKindFromIdentifier(bundleId), path: exePath };
158
+ }
159
+ function findChromeMac() {
160
+ return findFirstExe([
161
+ { kind: "chrome", path: "/Applications/Google Chrome.app/Contents/MacOS/Google Chrome" },
162
+ { kind: "chrome", path: path.join(os.homedir(), "Applications/Google Chrome.app/Contents/MacOS/Google Chrome") },
163
+ { kind: "brave", path: "/Applications/Brave Browser.app/Contents/MacOS/Brave Browser" },
164
+ { kind: "brave", path: path.join(os.homedir(), "Applications/Brave Browser.app/Contents/MacOS/Brave Browser") },
165
+ { kind: "edge", path: "/Applications/Microsoft Edge.app/Contents/MacOS/Microsoft Edge" },
166
+ { kind: "edge", path: path.join(os.homedir(), "Applications/Microsoft Edge.app/Contents/MacOS/Microsoft Edge") },
167
+ { kind: "chromium", path: "/Applications/Chromium.app/Contents/MacOS/Chromium" },
168
+ { kind: "chromium", path: path.join(os.homedir(), "Applications/Chromium.app/Contents/MacOS/Chromium") },
169
+ { kind: "canary", path: "/Applications/Google Chrome Canary.app/Contents/MacOS/Google Chrome Canary" },
170
+ { kind: "canary", path: path.join(os.homedir(), "Applications/Google Chrome Canary.app/Contents/MacOS/Google Chrome Canary") }
171
+ ]);
172
+ }
173
+ function detectDefaultChromiumLinux() {
174
+ const desktopId = execText("xdg-settings", ["get", "default-web-browser"]) || execText("xdg-mime", ["query", "default", "x-scheme-handler/http"]);
175
+ if (!desktopId) return null;
176
+ const trimmed = desktopId.trim();
177
+ if (!CHROMIUM_DESKTOP_IDS.has(trimmed)) return null;
178
+ const searchDirs = [
179
+ path.join(os.homedir(), ".local", "share", "applications"),
180
+ "/usr/local/share/applications",
181
+ "/usr/share/applications",
182
+ "/var/lib/snapd/desktop/applications"
183
+ ];
184
+ let desktopPath = null;
185
+ for (const dir of searchDirs) {
186
+ const candidate = path.join(dir, trimmed);
187
+ if (fileExists(candidate)) {
188
+ desktopPath = candidate;
189
+ break;
190
+ }
191
+ }
192
+ if (!desktopPath) return null;
193
+ let execLine = null;
194
+ try {
195
+ const lines = fs.readFileSync(desktopPath, "utf8").split(/\r?\n/);
196
+ for (const line of lines) if (line.startsWith("Exec=")) {
197
+ execLine = line.slice(5).trim();
198
+ break;
199
+ }
200
+ } catch {
201
+ }
202
+ if (!execLine) return null;
203
+ const tokens = execLine.split(/\s+/);
204
+ let command = null;
205
+ for (const token of tokens) {
206
+ if (!token || token === "env" || token.includes("=") && !token.startsWith("/")) continue;
207
+ command = token.replace(/^["']|["']$/g, "");
208
+ break;
209
+ }
210
+ if (!command) return null;
211
+ const resolved = command.startsWith("/") ? command : execText("which", [command], 800)?.trim() ?? null;
212
+ if (!resolved) return null;
213
+ const exeName = path.posix.basename(resolved).toLowerCase();
214
+ if (!CHROMIUM_EXE_NAMES.has(exeName)) return null;
215
+ return { kind: inferKindFromExeName(exeName), path: resolved };
216
+ }
217
+ function findChromeLinux() {
218
+ return findFirstExe([
219
+ { kind: "chrome", path: "/usr/bin/google-chrome" },
220
+ { kind: "chrome", path: "/usr/bin/google-chrome-stable" },
221
+ { kind: "chrome", path: "/usr/bin/chrome" },
222
+ { kind: "brave", path: "/usr/bin/brave-browser" },
223
+ { kind: "brave", path: "/usr/bin/brave-browser-stable" },
224
+ { kind: "brave", path: "/usr/bin/brave" },
225
+ { kind: "brave", path: "/snap/bin/brave" },
226
+ { kind: "edge", path: "/usr/bin/microsoft-edge" },
227
+ { kind: "edge", path: "/usr/bin/microsoft-edge-stable" },
228
+ { kind: "chromium", path: "/usr/bin/chromium" },
229
+ { kind: "chromium", path: "/usr/bin/chromium-browser" },
230
+ { kind: "chromium", path: "/snap/bin/chromium" }
231
+ ]);
232
+ }
233
+ function findChromeWindows() {
234
+ const localAppData = process.env.LOCALAPPDATA ?? "";
235
+ const programFiles = process.env.ProgramFiles ?? "C:\\Program Files";
236
+ const programFilesX86 = process.env["ProgramFiles(x86)"] ?? "C:\\Program Files (x86)";
237
+ const j = path.win32.join;
238
+ const candidates = [];
239
+ if (localAppData) {
240
+ candidates.push({ kind: "chrome", path: j(localAppData, "Google", "Chrome", "Application", "chrome.exe") });
241
+ candidates.push({ kind: "brave", path: j(localAppData, "BraveSoftware", "Brave-Browser", "Application", "brave.exe") });
242
+ candidates.push({ kind: "edge", path: j(localAppData, "Microsoft", "Edge", "Application", "msedge.exe") });
243
+ candidates.push({ kind: "chromium", path: j(localAppData, "Chromium", "Application", "chrome.exe") });
244
+ candidates.push({ kind: "canary", path: j(localAppData, "Google", "Chrome SxS", "Application", "chrome.exe") });
245
+ }
246
+ candidates.push({ kind: "chrome", path: j(programFiles, "Google", "Chrome", "Application", "chrome.exe") });
247
+ candidates.push({ kind: "chrome", path: j(programFilesX86, "Google", "Chrome", "Application", "chrome.exe") });
248
+ candidates.push({ kind: "brave", path: j(programFiles, "BraveSoftware", "Brave-Browser", "Application", "brave.exe") });
249
+ candidates.push({ kind: "brave", path: j(programFilesX86, "BraveSoftware", "Brave-Browser", "Application", "brave.exe") });
250
+ candidates.push({ kind: "edge", path: j(programFiles, "Microsoft", "Edge", "Application", "msedge.exe") });
251
+ candidates.push({ kind: "edge", path: j(programFilesX86, "Microsoft", "Edge", "Application", "msedge.exe") });
252
+ return findFirstExe(candidates);
253
+ }
254
+ function resolveBrowserExecutable(opts) {
255
+ if (opts?.executablePath) {
256
+ if (!fileExists(opts.executablePath)) throw new Error(`executablePath not found: ${opts.executablePath}`);
257
+ return { kind: "custom", path: opts.executablePath };
258
+ }
259
+ const platform = process.platform;
260
+ if (platform === "darwin") return detectDefaultChromiumMac() ?? findChromeMac();
261
+ if (platform === "linux") return detectDefaultChromiumLinux() ?? findChromeLinux();
262
+ if (platform === "win32") return findChromeWindows();
263
+ return null;
264
+ }
265
+ async function ensurePortAvailable(port) {
266
+ await new Promise((resolve, reject) => {
267
+ const tester = net.createServer().once("error", (err) => {
268
+ if (err.code === "EADDRINUSE") reject(new Error(`Port ${port} is already in use`));
269
+ else reject(err);
270
+ }).once("listening", () => {
271
+ tester.close(() => resolve());
272
+ }).listen(port);
273
+ });
274
+ }
275
+ function safeReadJson(filePath) {
276
+ try {
277
+ if (!fs.existsSync(filePath)) return null;
278
+ const parsed = JSON.parse(fs.readFileSync(filePath, "utf-8"));
279
+ if (typeof parsed !== "object" || parsed === null || Array.isArray(parsed)) return null;
280
+ return parsed;
281
+ } catch {
282
+ return null;
283
+ }
284
+ }
285
+ function safeWriteJson(filePath, data) {
286
+ fs.mkdirSync(path.dirname(filePath), { recursive: true });
287
+ fs.writeFileSync(filePath, JSON.stringify(data, null, 2));
288
+ }
289
+ function setDeep(obj, keys, value) {
290
+ let node = obj;
291
+ for (const key of keys.slice(0, -1)) {
292
+ const next = node[key];
293
+ if (typeof next !== "object" || next === null || Array.isArray(next)) node[key] = {};
294
+ node = node[key];
295
+ }
296
+ node[keys[keys.length - 1]] = value;
297
+ }
298
+ function parseHexRgbToSignedArgbInt(hex) {
299
+ const cleaned = hex.trim().replace(/^#/, "");
300
+ if (!/^[0-9a-fA-F]{6}$/.test(cleaned)) return null;
301
+ const argbUnsigned = 255 << 24 | Number.parseInt(cleaned, 16);
302
+ return argbUnsigned > 2147483647 ? argbUnsigned - 4294967296 : argbUnsigned;
303
+ }
304
+ function decorateProfile(userDataDir, name, color) {
305
+ const colorInt = parseHexRgbToSignedArgbInt(color);
306
+ const localStatePath = path.join(userDataDir, "Local State");
307
+ const preferencesPath = path.join(userDataDir, "Default", "Preferences");
308
+ const localState = safeReadJson(localStatePath) ?? {};
309
+ setDeep(localState, ["profile", "info_cache", "Default", "name"], name);
310
+ setDeep(localState, ["profile", "info_cache", "Default", "shortcut_name"], name);
311
+ setDeep(localState, ["profile", "info_cache", "Default", "user_name"], name);
312
+ setDeep(localState, ["profile", "info_cache", "Default", "profile_color"], color);
313
+ if (colorInt != null) {
314
+ setDeep(localState, ["profile", "info_cache", "Default", "profile_color_seed"], colorInt);
315
+ setDeep(localState, ["profile", "info_cache", "Default", "profile_highlight_color"], colorInt);
316
+ }
317
+ safeWriteJson(localStatePath, localState);
318
+ const prefs = safeReadJson(preferencesPath) ?? {};
319
+ setDeep(prefs, ["profile", "name"], name);
320
+ setDeep(prefs, ["profile", "profile_color"], color);
321
+ if (colorInt != null) {
322
+ setDeep(prefs, ["autogenerated", "theme", "color"], colorInt);
323
+ setDeep(prefs, ["browser", "theme", "user_color2"], colorInt);
324
+ }
325
+ safeWriteJson(preferencesPath, prefs);
326
+ }
327
+ function ensureCleanExit(userDataDir) {
328
+ const preferencesPath = path.join(userDataDir, "Default", "Preferences");
329
+ const prefs = safeReadJson(preferencesPath) ?? {};
330
+ setDeep(prefs, ["exit_type"], "Normal");
331
+ setDeep(prefs, ["exited_cleanly"], true);
332
+ safeWriteJson(preferencesPath, prefs);
333
+ }
334
+ var DEFAULT_CDP_PORT = 9222;
335
+ var DEFAULT_PROFILE_NAME = "browserclaw";
336
+ var DEFAULT_PROFILE_COLOR = "#FF4500";
337
+ function resolveUserDataDir(profileName) {
338
+ const configDir = process.env.XDG_CONFIG_HOME ?? path.join(os.homedir(), ".config");
339
+ return path.join(configDir, "browserclaw", "profiles", profileName, "user-data");
340
+ }
341
+ async function isChromeReachable(cdpUrl, timeoutMs = 500) {
342
+ const ctrl = new AbortController();
343
+ const t = setTimeout(() => ctrl.abort(), timeoutMs);
344
+ try {
345
+ const res = await fetch(`${cdpUrl.replace(/\/+$/, "")}/json/version`, { signal: ctrl.signal });
346
+ return res.ok;
347
+ } catch {
348
+ return false;
349
+ } finally {
350
+ clearTimeout(t);
351
+ }
352
+ }
353
+ async function getChromeWebSocketUrl(cdpUrl, timeoutMs = 500) {
354
+ const ctrl = new AbortController();
355
+ const t = setTimeout(() => ctrl.abort(), timeoutMs);
356
+ try {
357
+ const res = await fetch(`${cdpUrl.replace(/\/+$/, "")}/json/version`, { signal: ctrl.signal });
358
+ if (!res.ok) return null;
359
+ const data = await res.json();
360
+ return String(data?.webSocketDebuggerUrl ?? "").trim() || null;
361
+ } catch {
362
+ return null;
363
+ } finally {
364
+ clearTimeout(t);
365
+ }
366
+ }
367
+ async function launchChrome(opts = {}) {
368
+ const cdpPort = opts.cdpPort ?? DEFAULT_CDP_PORT;
369
+ await ensurePortAvailable(cdpPort);
370
+ const exe = resolveBrowserExecutable({ executablePath: opts.executablePath });
371
+ if (!exe) throw new Error("No supported browser found (Chrome/Brave/Edge/Chromium). Install one or provide executablePath.");
372
+ const profileName = opts.profileName ?? DEFAULT_PROFILE_NAME;
373
+ const userDataDir = opts.userDataDir ?? resolveUserDataDir(profileName);
374
+ fs.mkdirSync(userDataDir, { recursive: true });
375
+ const spawnChrome = () => {
376
+ const args = [
377
+ `--remote-debugging-port=${cdpPort}`,
378
+ `--user-data-dir=${userDataDir}`,
379
+ "--no-first-run",
380
+ "--no-default-browser-check",
381
+ "--disable-sync",
382
+ "--disable-background-networking",
383
+ "--disable-component-update",
384
+ "--disable-features=Translate,MediaRouter",
385
+ "--disable-session-crashed-bubble",
386
+ "--hide-crash-restore-bubble",
387
+ "--password-store=basic"
388
+ ];
389
+ if (opts.headless) {
390
+ args.push("--headless=new", "--disable-gpu");
391
+ }
392
+ if (opts.noSandbox) {
393
+ args.push("--no-sandbox", "--disable-setuid-sandbox");
394
+ }
395
+ if (process.platform === "linux") args.push("--disable-dev-shm-usage");
396
+ if (opts.chromeArgs?.length) args.push(...opts.chromeArgs);
397
+ args.push("about:blank");
398
+ return spawn(exe.path, args, {
399
+ stdio: "pipe",
400
+ env: { ...process.env, HOME: os.homedir() }
401
+ });
402
+ };
403
+ const startedAt = Date.now();
404
+ const localStatePath = path.join(userDataDir, "Local State");
405
+ const preferencesPath = path.join(userDataDir, "Default", "Preferences");
406
+ if (!fileExists(localStatePath) || !fileExists(preferencesPath)) {
407
+ const bootstrap = spawnChrome();
408
+ const deadline = Date.now() + 1e4;
409
+ while (Date.now() < deadline) {
410
+ if (fileExists(localStatePath) && fileExists(preferencesPath)) break;
411
+ await new Promise((r) => setTimeout(r, 100));
412
+ }
413
+ try {
414
+ bootstrap.kill("SIGTERM");
415
+ } catch {
416
+ }
417
+ const exitDeadline = Date.now() + 5e3;
418
+ while (Date.now() < exitDeadline) {
419
+ if (bootstrap.exitCode != null) break;
420
+ await new Promise((r) => setTimeout(r, 50));
421
+ }
422
+ }
423
+ try {
424
+ decorateProfile(userDataDir, profileName, opts.profileColor ?? DEFAULT_PROFILE_COLOR);
425
+ } catch {
426
+ }
427
+ try {
428
+ ensureCleanExit(userDataDir);
429
+ } catch {
430
+ }
431
+ const proc = spawnChrome();
432
+ const cdpUrl = `http://127.0.0.1:${cdpPort}`;
433
+ const readyDeadline = Date.now() + 15e3;
434
+ while (Date.now() < readyDeadline) {
435
+ if (await isChromeReachable(cdpUrl, 500)) break;
436
+ await new Promise((r) => setTimeout(r, 200));
437
+ }
438
+ if (!await isChromeReachable(cdpUrl, 500)) {
439
+ try {
440
+ proc.kill("SIGKILL");
441
+ } catch {
442
+ }
443
+ throw new Error(`Failed to start Chrome CDP on port ${cdpPort}. Chrome may not have started correctly.`);
444
+ }
445
+ return {
446
+ pid: proc.pid ?? -1,
447
+ exe,
448
+ userDataDir,
449
+ cdpPort,
450
+ startedAt,
451
+ proc
452
+ };
453
+ }
454
+ async function stopChrome(running, timeoutMs = 2500) {
455
+ const proc = running.proc;
456
+ if (proc.killed) return;
457
+ try {
458
+ proc.kill("SIGTERM");
459
+ } catch {
460
+ }
461
+ const start = Date.now();
462
+ while (Date.now() - start < timeoutMs) {
463
+ if (proc.exitCode != null || proc.killed) return;
464
+ if (!await isChromeReachable(`http://127.0.0.1:${running.cdpPort}`, 200)) return;
465
+ await new Promise((r) => setTimeout(r, 100));
466
+ }
467
+ try {
468
+ proc.kill("SIGKILL");
469
+ } catch {
470
+ }
471
+ }
472
+ var cached = null;
473
+ var connecting = null;
474
+ var pageStates = /* @__PURE__ */ new WeakMap();
475
+ var contextStates = /* @__PURE__ */ new WeakMap();
476
+ var observedContexts = /* @__PURE__ */ new WeakSet();
477
+ var observedPages = /* @__PURE__ */ new WeakSet();
478
+ var roleRefsByTarget = /* @__PURE__ */ new Map();
479
+ var MAX_ROLE_REFS_CACHE = 50;
480
+ var MAX_CONSOLE_MESSAGES = 500;
481
+ var MAX_PAGE_ERRORS = 200;
482
+ var MAX_NETWORK_REQUESTS = 500;
483
+ function normalizeCdpUrl(raw) {
484
+ return raw.replace(/\/$/, "");
485
+ }
486
+ function roleRefsKey(cdpUrl, targetId) {
487
+ return `${normalizeCdpUrl(cdpUrl)}::${targetId}`;
488
+ }
489
+ function ensurePageState(page) {
490
+ const existing = pageStates.get(page);
491
+ if (existing) return existing;
492
+ const state = {
493
+ console: [],
494
+ errors: [],
495
+ requests: [],
496
+ requestIds: /* @__PURE__ */ new WeakMap(),
497
+ nextRequestId: 0,
498
+ armIdUpload: 0,
499
+ armIdDialog: 0,
500
+ armIdDownload: 0
501
+ };
502
+ pageStates.set(page, state);
503
+ if (!observedPages.has(page)) {
504
+ observedPages.add(page);
505
+ page.on("console", (msg) => {
506
+ state.console.push({
507
+ type: msg.type(),
508
+ text: msg.text(),
509
+ timestamp: (/* @__PURE__ */ new Date()).toISOString(),
510
+ location: msg.location()
511
+ });
512
+ if (state.console.length > MAX_CONSOLE_MESSAGES) state.console.shift();
513
+ });
514
+ page.on("pageerror", (err) => {
515
+ state.errors.push({
516
+ message: err?.message ? String(err.message) : String(err),
517
+ name: err?.name ? String(err.name) : void 0,
518
+ stack: err?.stack ? String(err.stack) : void 0,
519
+ timestamp: (/* @__PURE__ */ new Date()).toISOString()
520
+ });
521
+ if (state.errors.length > MAX_PAGE_ERRORS) state.errors.shift();
522
+ });
523
+ page.on("request", (req) => {
524
+ state.nextRequestId += 1;
525
+ const id = `r${state.nextRequestId}`;
526
+ state.requestIds.set(req, id);
527
+ state.requests.push({
528
+ id,
529
+ timestamp: (/* @__PURE__ */ new Date()).toISOString(),
530
+ method: req.method(),
531
+ url: req.url(),
532
+ resourceType: req.resourceType()
533
+ });
534
+ if (state.requests.length > MAX_NETWORK_REQUESTS) state.requests.shift();
535
+ });
536
+ page.on("response", (resp) => {
537
+ const req = resp.request();
538
+ const id = state.requestIds.get(req);
539
+ if (!id) return;
540
+ for (let i = state.requests.length - 1; i >= 0; i--) {
541
+ const rec = state.requests[i];
542
+ if (rec && rec.id === id) {
543
+ rec.status = resp.status();
544
+ rec.ok = resp.ok();
545
+ break;
546
+ }
547
+ }
548
+ });
549
+ page.on("requestfailed", (req) => {
550
+ const id = state.requestIds.get(req);
551
+ if (!id) return;
552
+ for (let i = state.requests.length - 1; i >= 0; i--) {
553
+ const rec = state.requests[i];
554
+ if (rec && rec.id === id) {
555
+ rec.failureText = req.failure()?.errorText;
556
+ rec.ok = false;
557
+ break;
558
+ }
559
+ }
560
+ });
561
+ page.on("close", () => {
562
+ pageStates.delete(page);
563
+ observedPages.delete(page);
564
+ });
565
+ }
566
+ return state;
567
+ }
568
+ function ensureContextState(context) {
569
+ const existing = contextStates.get(context);
570
+ if (existing) return existing;
571
+ const state = { traceActive: false };
572
+ contextStates.set(context, state);
573
+ return state;
574
+ }
575
+ function observeContext(context) {
576
+ if (observedContexts.has(context)) return;
577
+ observedContexts.add(context);
578
+ ensureContextState(context);
579
+ for (const page of context.pages()) ensurePageState(page);
580
+ context.on("page", (page) => ensurePageState(page));
581
+ }
582
+ function observeBrowser(browser) {
583
+ for (const context of browser.contexts()) observeContext(context);
584
+ }
585
+ function storeRoleRefsForTarget(opts) {
586
+ const state = ensurePageState(opts.page);
587
+ state.roleRefs = opts.refs;
588
+ state.roleRefsFrameSelector = opts.frameSelector;
589
+ state.roleRefsMode = opts.mode;
590
+ const targetId = opts.targetId?.trim();
591
+ if (!targetId) return;
592
+ roleRefsByTarget.set(roleRefsKey(opts.cdpUrl, targetId), {
593
+ refs: opts.refs,
594
+ ...opts.frameSelector ? { frameSelector: opts.frameSelector } : {},
595
+ ...opts.mode ? { mode: opts.mode } : {}
596
+ });
597
+ while (roleRefsByTarget.size > MAX_ROLE_REFS_CACHE) {
598
+ const first = roleRefsByTarget.keys().next();
599
+ if (first.done) break;
600
+ roleRefsByTarget.delete(first.value);
601
+ }
602
+ }
603
+ function restoreRoleRefsForTarget(opts) {
604
+ const targetId = opts.targetId?.trim() || "";
605
+ if (!targetId) return;
606
+ const cached2 = roleRefsByTarget.get(roleRefsKey(opts.cdpUrl, targetId));
607
+ if (!cached2) return;
608
+ const state = ensurePageState(opts.page);
609
+ if (state.roleRefs) return;
610
+ state.roleRefs = cached2.refs;
611
+ state.roleRefsFrameSelector = cached2.frameSelector;
612
+ state.roleRefsMode = cached2.mode;
613
+ }
614
+ async function connectBrowser(cdpUrl) {
615
+ const normalized = normalizeCdpUrl(cdpUrl);
616
+ if (cached?.cdpUrl === normalized) return cached;
617
+ if (connecting) return await connecting;
618
+ const connectWithRetry = async () => {
619
+ let lastErr;
620
+ for (let attempt = 0; attempt < 3; attempt++) {
621
+ try {
622
+ const timeout = 5e3 + attempt * 2e3;
623
+ const endpoint = await getChromeWebSocketUrl(normalized, timeout).catch(() => null) ?? normalized;
624
+ const browser = await chromium.connectOverCDP(endpoint, { timeout });
625
+ const connected = { browser, cdpUrl: normalized };
626
+ cached = connected;
627
+ observeBrowser(browser);
628
+ browser.on("disconnected", () => {
629
+ if (cached?.browser === browser) cached = null;
630
+ });
631
+ return connected;
632
+ } catch (err) {
633
+ lastErr = err;
634
+ await new Promise((r) => setTimeout(r, 250 + attempt * 250));
635
+ }
636
+ }
637
+ throw lastErr instanceof Error ? lastErr : new Error("CDP connect failed");
638
+ };
639
+ connecting = connectWithRetry().finally(() => {
640
+ connecting = null;
641
+ });
642
+ return await connecting;
643
+ }
644
+ async function disconnectBrowser() {
645
+ const cur = cached;
646
+ cached = null;
647
+ if (cur) await cur.browser.close().catch(() => {
648
+ });
649
+ }
650
+ async function getAllPages(browser) {
651
+ return browser.contexts().flatMap((c) => c.pages());
652
+ }
653
+ async function pageTargetId(page) {
654
+ const session = await page.context().newCDPSession(page);
655
+ try {
656
+ const info = await session.send("Target.getTargetInfo");
657
+ return String(info?.targetInfo?.targetId ?? "").trim() || null;
658
+ } finally {
659
+ await session.detach().catch(() => {
660
+ });
661
+ }
662
+ }
663
+ async function findPageByTargetId(browser, targetId, cdpUrl) {
664
+ const pages = await getAllPages(browser);
665
+ for (const page of pages) {
666
+ const tid = await pageTargetId(page).catch(() => null);
667
+ if (tid && tid === targetId) return page;
668
+ }
669
+ if (cdpUrl) {
670
+ try {
671
+ const listUrl = `${cdpUrl.replace(/\/+$/, "").replace(/^ws:/, "http:").replace(/\/cdp$/, "")}/json/list`;
672
+ const response = await fetch(listUrl);
673
+ if (response.ok) {
674
+ const targets = await response.json();
675
+ const target = targets.find((t) => t.id === targetId);
676
+ if (target) {
677
+ const urlMatch = pages.filter((p) => p.url() === target.url);
678
+ if (urlMatch.length === 1) return urlMatch[0];
679
+ }
680
+ }
681
+ } catch {
682
+ }
683
+ }
684
+ return null;
685
+ }
686
+ async function getPageForTargetId(opts) {
687
+ const { browser } = await connectBrowser(opts.cdpUrl);
688
+ const pages = await getAllPages(browser);
689
+ if (!pages.length) throw new Error("No pages available in the connected browser.");
690
+ const first = pages[0];
691
+ if (!opts.targetId) return first;
692
+ const found = await findPageByTargetId(browser, opts.targetId, opts.cdpUrl);
693
+ if (!found) {
694
+ if (pages.length === 1) return first;
695
+ throw new Error(`Tab not found (targetId: ${opts.targetId}). Use browser.tabs() to list open tabs.`);
696
+ }
697
+ return found;
698
+ }
699
+ function refLocator(page, ref) {
700
+ const normalized = ref.startsWith("@") ? ref.slice(1) : ref.startsWith("ref=") ? ref.slice(4) : ref;
701
+ if (/^e\d+$/.test(normalized)) {
702
+ const state = pageStates.get(page);
703
+ if (state?.roleRefsMode === "aria") {
704
+ return (state.roleRefsFrameSelector ? page.frameLocator(state.roleRefsFrameSelector) : page).locator(`aria-ref=${normalized}`);
705
+ }
706
+ const info = state?.roleRefs?.[normalized];
707
+ if (!info) throw new Error(`Unknown ref "${normalized}". Run a new snapshot and use a ref from that snapshot.`);
708
+ const locAny = state?.roleRefsFrameSelector ? page.frameLocator(state.roleRefsFrameSelector) : page;
709
+ const locator = info.name ? locAny.getByRole(info.role, { name: info.name, exact: true }) : locAny.getByRole(info.role);
710
+ return info.nth !== void 0 ? locator.nth(info.nth) : locator;
711
+ }
712
+ return page.locator(`aria-ref=${normalized}`);
713
+ }
714
+ function toAIFriendlyError(error, selector) {
715
+ const message = error instanceof Error ? error.message : String(error);
716
+ if (message.includes("strict mode violation")) {
717
+ const countMatch = message.match(/resolved to (\d+) elements/);
718
+ const count = countMatch ? countMatch[1] : "multiple";
719
+ return new Error(`Selector "${selector}" matched ${count} elements. Run a new snapshot to get updated refs, or use a different ref.`);
720
+ }
721
+ if ((message.includes("Timeout") || message.includes("waiting for")) && (message.includes("to be visible") || message.includes("not visible"))) {
722
+ return new Error(`Element "${selector}" not found or not visible. Run a new snapshot to see current page elements.`);
723
+ }
724
+ if (message.includes("intercepts pointer events") || message.includes("not visible") || message.includes("not receive pointer events")) {
725
+ return new Error(`Element "${selector}" is not interactable (hidden or covered). Try scrolling it into view, closing overlays, or re-snapshotting.`);
726
+ }
727
+ return error instanceof Error ? error : new Error(message);
728
+ }
729
+ function normalizeTimeoutMs(timeoutMs, fallback) {
730
+ return Math.max(500, Math.min(12e4, timeoutMs ?? fallback));
731
+ }
732
+
733
+ // src/snapshot/ref-map.ts
734
+ var INTERACTIVE_ROLES = /* @__PURE__ */ new Set([
735
+ "button",
736
+ "link",
737
+ "textbox",
738
+ "checkbox",
739
+ "radio",
740
+ "combobox",
741
+ "listbox",
742
+ "menuitem",
743
+ "menuitemcheckbox",
744
+ "menuitemradio",
745
+ "option",
746
+ "searchbox",
747
+ "slider",
748
+ "spinbutton",
749
+ "switch",
750
+ "tab",
751
+ "treeitem"
752
+ ]);
753
+ var CONTENT_ROLES = /* @__PURE__ */ new Set([
754
+ "heading",
755
+ "cell",
756
+ "gridcell",
757
+ "columnheader",
758
+ "rowheader",
759
+ "listitem",
760
+ "article",
761
+ "region",
762
+ "main",
763
+ "navigation"
764
+ ]);
765
+ var STRUCTURAL_ROLES = /* @__PURE__ */ new Set([
766
+ "generic",
767
+ "group",
768
+ "list",
769
+ "table",
770
+ "row",
771
+ "rowgroup",
772
+ "grid",
773
+ "treegrid",
774
+ "menu",
775
+ "menubar",
776
+ "toolbar",
777
+ "tablist",
778
+ "tree",
779
+ "directory",
780
+ "document",
781
+ "application",
782
+ "presentation",
783
+ "none"
784
+ ]);
785
+ function getIndentLevel(line) {
786
+ const match = line.match(/^(\s*)/);
787
+ return match ? Math.floor(match[1].length / 2) : 0;
788
+ }
789
+ function createRoleNameTracker() {
790
+ const counts = /* @__PURE__ */ new Map();
791
+ const refsByKey = /* @__PURE__ */ new Map();
792
+ return {
793
+ counts,
794
+ refsByKey,
795
+ getKey(role, name) {
796
+ return `${role}:${name ?? ""}`;
797
+ },
798
+ getNextIndex(role, name) {
799
+ const key = this.getKey(role, name);
800
+ const current = counts.get(key) ?? 0;
801
+ counts.set(key, current + 1);
802
+ return current;
803
+ },
804
+ trackRef(role, name, ref) {
805
+ const key = this.getKey(role, name);
806
+ const list = refsByKey.get(key) ?? [];
807
+ list.push(ref);
808
+ refsByKey.set(key, list);
809
+ },
810
+ getDuplicateKeys() {
811
+ const out = /* @__PURE__ */ new Set();
812
+ for (const [key, refs] of refsByKey) if (refs.length > 1) out.add(key);
813
+ return out;
814
+ }
815
+ };
816
+ }
817
+ function removeNthFromNonDuplicates(refs, tracker) {
818
+ const duplicates = tracker.getDuplicateKeys();
819
+ for (const [ref, data] of Object.entries(refs)) {
820
+ const key = tracker.getKey(data.role, data.name);
821
+ if (!duplicates.has(key)) delete refs[ref]?.nth;
822
+ }
823
+ }
824
+ function compactTree(tree) {
825
+ const lines = tree.split("\n");
826
+ const result = [];
827
+ for (let i = 0; i < lines.length; i++) {
828
+ const line = lines[i];
829
+ if (line.includes("[ref=")) {
830
+ result.push(line);
831
+ continue;
832
+ }
833
+ if (line.includes(":") && !line.trimEnd().endsWith(":")) {
834
+ result.push(line);
835
+ continue;
836
+ }
837
+ const currentIndent = getIndentLevel(line);
838
+ let hasRelevantChildren = false;
839
+ for (let j = i + 1; j < lines.length; j++) {
840
+ if (getIndentLevel(lines[j]) <= currentIndent) break;
841
+ if (lines[j]?.includes("[ref=")) {
842
+ hasRelevantChildren = true;
843
+ break;
844
+ }
845
+ }
846
+ if (hasRelevantChildren) result.push(line);
847
+ }
848
+ return result.join("\n");
849
+ }
850
+ function buildRoleSnapshotFromAriaSnapshot(ariaSnapshot, options = {}) {
851
+ const lines = ariaSnapshot.split("\n");
852
+ const refs = {};
853
+ const tracker = createRoleNameTracker();
854
+ let counter = 0;
855
+ const nextRef = () => {
856
+ counter++;
857
+ return `e${counter}`;
858
+ };
859
+ if (options.interactive) {
860
+ const result2 = [];
861
+ for (const line of lines) {
862
+ const depth = getIndentLevel(line);
863
+ if (options.maxDepth !== void 0 && depth > options.maxDepth) continue;
864
+ const match = line.match(/^(\s*-\s*)(\w+)(?:\s+"([^"]*)")?(.*)$/);
865
+ if (!match) continue;
866
+ const [, , roleRaw, name, suffix] = match;
867
+ if (roleRaw.startsWith("/")) continue;
868
+ const role = roleRaw.toLowerCase();
869
+ if (!INTERACTIVE_ROLES.has(role)) continue;
870
+ const ref = nextRef();
871
+ const nth = tracker.getNextIndex(role, name);
872
+ tracker.trackRef(role, name, ref);
873
+ refs[ref] = { role, name, nth };
874
+ let enhanced = `- ${roleRaw}`;
875
+ if (name) enhanced += ` "${name}"`;
876
+ enhanced += ` [ref=${ref}]`;
877
+ if (nth > 0) enhanced += ` [nth=${nth}]`;
878
+ if (suffix.includes("[")) enhanced += suffix;
879
+ result2.push(enhanced);
880
+ }
881
+ removeNthFromNonDuplicates(refs, tracker);
882
+ return { snapshot: result2.join("\n") || "(no interactive elements)", refs };
883
+ }
884
+ const result = [];
885
+ for (const line of lines) {
886
+ const depth = getIndentLevel(line);
887
+ if (options.maxDepth !== void 0 && depth > options.maxDepth) continue;
888
+ const match = line.match(/^(\s*-\s*)(\w+)(?:\s+"([^"]*)")?(.*)$/);
889
+ if (!match) {
890
+ if (!options.interactive) result.push(line);
891
+ continue;
892
+ }
893
+ const [, prefix, roleRaw, name, suffix] = match;
894
+ if (roleRaw.startsWith("/")) {
895
+ result.push(line);
896
+ continue;
897
+ }
898
+ const role = roleRaw.toLowerCase();
899
+ const isInteractive = INTERACTIVE_ROLES.has(role);
900
+ const isContent = CONTENT_ROLES.has(role);
901
+ const isStructural = STRUCTURAL_ROLES.has(role);
902
+ if (options.compact && isStructural && !name) continue;
903
+ if (!(isInteractive || isContent && name)) {
904
+ result.push(line);
905
+ continue;
906
+ }
907
+ const ref = nextRef();
908
+ const nth = tracker.getNextIndex(role, name);
909
+ tracker.trackRef(role, name, ref);
910
+ refs[ref] = { role, name, nth };
911
+ let enhanced = `${prefix}${roleRaw}`;
912
+ if (name) enhanced += ` "${name}"`;
913
+ enhanced += ` [ref=${ref}]`;
914
+ if (nth > 0) enhanced += ` [nth=${nth}]`;
915
+ if (suffix) enhanced += suffix;
916
+ result.push(enhanced);
917
+ }
918
+ removeNthFromNonDuplicates(refs, tracker);
919
+ const tree = result.join("\n") || "(empty)";
920
+ return { snapshot: options.compact ? compactTree(tree) : tree, refs };
921
+ }
922
+ function buildRoleSnapshotFromAiSnapshot(aiSnapshot, options = {}) {
923
+ const lines = String(aiSnapshot ?? "").split("\n");
924
+ const refs = {};
925
+ function parseAiSnapshotRef(suffix) {
926
+ const match = suffix.match(/\[ref=(e\d+)\]/i);
927
+ return match ? match[1] : null;
928
+ }
929
+ if (options.interactive) {
930
+ const out2 = [];
931
+ for (const line of lines) {
932
+ const depth = getIndentLevel(line);
933
+ if (options.maxDepth !== void 0 && depth > options.maxDepth) continue;
934
+ const match = line.match(/^(\s*-\s*)(\w+)(?:\s+"([^"]*)")?(.*)$/);
935
+ if (!match) continue;
936
+ const [, , roleRaw, name, suffix] = match;
937
+ if (roleRaw.startsWith("/")) continue;
938
+ const role = roleRaw.toLowerCase();
939
+ if (!INTERACTIVE_ROLES.has(role)) continue;
940
+ const ref = parseAiSnapshotRef(suffix);
941
+ if (!ref) continue;
942
+ refs[ref] = { role, ...name ? { name } : {} };
943
+ out2.push(`- ${roleRaw}${name ? ` "${name}"` : ""}${suffix}`);
944
+ }
945
+ return { snapshot: out2.join("\n") || "(no interactive elements)", refs };
946
+ }
947
+ const out = [];
948
+ for (const line of lines) {
949
+ const depth = getIndentLevel(line);
950
+ if (options.maxDepth !== void 0 && depth > options.maxDepth) continue;
951
+ const match = line.match(/^(\s*-\s*)(\w+)(?:\s+"([^"]*)")?(.*)$/);
952
+ if (!match) {
953
+ out.push(line);
954
+ continue;
955
+ }
956
+ const [, , roleRaw, name, suffix] = match;
957
+ if (roleRaw.startsWith("/")) {
958
+ out.push(line);
959
+ continue;
960
+ }
961
+ const role = roleRaw.toLowerCase();
962
+ const isStructural = STRUCTURAL_ROLES.has(role);
963
+ if (options.compact && isStructural && !name) continue;
964
+ const ref = parseAiSnapshotRef(suffix);
965
+ if (ref) refs[ref] = { role, ...name ? { name } : {} };
966
+ out.push(line);
967
+ }
968
+ const tree = out.join("\n") || "(empty)";
969
+ return { snapshot: options.compact ? compactTree(tree) : tree, refs };
970
+ }
971
+ function getRoleSnapshotStats(snapshot, refs) {
972
+ const interactive = Object.values(refs).filter((r) => INTERACTIVE_ROLES.has(r.role)).length;
973
+ return {
974
+ lines: snapshot.split("\n").length,
975
+ chars: snapshot.length,
976
+ refs: Object.keys(refs).length,
977
+ interactive
978
+ };
979
+ }
980
+
981
+ // src/snapshot/ai-snapshot.ts
982
+ async function snapshotAi(opts) {
983
+ const page = await getPageForTargetId({ cdpUrl: opts.cdpUrl, targetId: opts.targetId });
984
+ ensurePageState(page);
985
+ const maybe = page;
986
+ if (!maybe._snapshotForAI) {
987
+ throw new Error("Playwright _snapshotForAI is not available. Upgrade playwright-core to >= 1.50.");
988
+ }
989
+ const result = await maybe._snapshotForAI({
990
+ timeout: Math.max(500, Math.min(6e4, Math.floor(opts.timeoutMs ?? 5e3))),
991
+ track: "response"
992
+ });
993
+ let snapshot = String(result?.full ?? "");
994
+ const maxChars = opts.maxChars;
995
+ const limit = typeof maxChars === "number" && Number.isFinite(maxChars) && maxChars > 0 ? Math.floor(maxChars) : void 0;
996
+ if (limit && snapshot.length > limit) {
997
+ snapshot = `${snapshot.slice(0, limit)}
998
+
999
+ [...TRUNCATED - page too large]`;
1000
+ }
1001
+ const built = buildRoleSnapshotFromAiSnapshot(snapshot, opts.options);
1002
+ storeRoleRefsForTarget({
1003
+ page,
1004
+ cdpUrl: opts.cdpUrl,
1005
+ targetId: opts.targetId,
1006
+ refs: built.refs,
1007
+ mode: "aria"
1008
+ });
1009
+ return {
1010
+ snapshot: built.snapshot,
1011
+ refs: built.refs,
1012
+ stats: getRoleSnapshotStats(built.snapshot, built.refs)
1013
+ };
1014
+ }
1015
+
1016
+ // src/snapshot/aria-snapshot.ts
1017
+ async function snapshotRole(opts) {
1018
+ const page = await getPageForTargetId({ cdpUrl: opts.cdpUrl, targetId: opts.targetId });
1019
+ ensurePageState(page);
1020
+ const frameSelector = opts.frameSelector?.trim() || "";
1021
+ const selector = opts.selector?.trim() || "";
1022
+ const locator = frameSelector ? selector ? page.frameLocator(frameSelector).locator(selector) : page.frameLocator(frameSelector).locator(":root") : selector ? page.locator(selector) : page.locator(":root");
1023
+ const ariaSnapshot = await locator.ariaSnapshot();
1024
+ const built = buildRoleSnapshotFromAriaSnapshot(String(ariaSnapshot ?? ""), opts.options);
1025
+ storeRoleRefsForTarget({
1026
+ page,
1027
+ cdpUrl: opts.cdpUrl,
1028
+ targetId: opts.targetId,
1029
+ refs: built.refs,
1030
+ frameSelector: frameSelector || void 0,
1031
+ mode: "role"
1032
+ });
1033
+ return {
1034
+ snapshot: built.snapshot,
1035
+ refs: built.refs,
1036
+ stats: getRoleSnapshotStats(built.snapshot, built.refs)
1037
+ };
1038
+ }
1039
+ async function snapshotAria(opts) {
1040
+ const limit = Math.max(1, Math.min(2e3, Math.floor(opts.limit ?? 500)));
1041
+ const page = await getPageForTargetId({ cdpUrl: opts.cdpUrl, targetId: opts.targetId });
1042
+ ensurePageState(page);
1043
+ const session = await page.context().newCDPSession(page);
1044
+ try {
1045
+ await session.send("Accessibility.enable").catch(() => {
1046
+ });
1047
+ const res = await session.send("Accessibility.getFullAXTree");
1048
+ return { nodes: formatAriaNodes(Array.isArray(res?.nodes) ? res.nodes : [], limit) };
1049
+ } finally {
1050
+ await session.detach().catch(() => {
1051
+ });
1052
+ }
1053
+ }
1054
+ function axValue(v) {
1055
+ if (!v || typeof v !== "object") return "";
1056
+ const value = v.value;
1057
+ if (typeof value === "string") return value;
1058
+ if (typeof value === "number" || typeof value === "boolean") return String(value);
1059
+ return "";
1060
+ }
1061
+ function formatAriaNodes(nodes, limit) {
1062
+ const byId = /* @__PURE__ */ new Map();
1063
+ for (const n of nodes) if (n.nodeId) byId.set(n.nodeId, n);
1064
+ const referenced = /* @__PURE__ */ new Set();
1065
+ for (const n of nodes) for (const c of n.childIds ?? []) referenced.add(c);
1066
+ const root = nodes.find((n) => n.nodeId && !referenced.has(n.nodeId)) ?? nodes[0];
1067
+ if (!root?.nodeId) return [];
1068
+ const out = [];
1069
+ const stack = [{ id: root.nodeId, depth: 0 }];
1070
+ while (stack.length && out.length < limit) {
1071
+ const popped = stack.pop();
1072
+ if (!popped) break;
1073
+ const { id, depth } = popped;
1074
+ const n = byId.get(id);
1075
+ if (!n) continue;
1076
+ const role = axValue(n.role);
1077
+ const name = axValue(n.name);
1078
+ const value = axValue(n.value);
1079
+ const description = axValue(n.description);
1080
+ const ref = `ax${out.length + 1}`;
1081
+ out.push({
1082
+ ref,
1083
+ role: role || "unknown",
1084
+ name: name || "",
1085
+ ...value ? { value } : {},
1086
+ ...description ? { description } : {},
1087
+ ...typeof n.backendDOMNodeId === "number" ? { backendDOMNodeId: n.backendDOMNodeId } : {},
1088
+ depth
1089
+ });
1090
+ const children = (n.childIds ?? []).filter((c) => byId.has(c));
1091
+ for (let i = children.length - 1; i >= 0; i--) {
1092
+ if (children[i]) stack.push({ id: children[i], depth: depth + 1 });
1093
+ }
1094
+ }
1095
+ return out;
1096
+ }
1097
+
1098
+ // src/actions/interaction.ts
1099
+ async function clickViaPlaywright(opts) {
1100
+ const page = await getPageForTargetId({ cdpUrl: opts.cdpUrl, targetId: opts.targetId });
1101
+ ensurePageState(page);
1102
+ restoreRoleRefsForTarget({ cdpUrl: opts.cdpUrl, targetId: opts.targetId, page });
1103
+ const locator = refLocator(page, opts.ref);
1104
+ const timeout = Math.max(500, Math.min(6e4, Math.floor(opts.timeoutMs ?? 8e3)));
1105
+ try {
1106
+ if (opts.doubleClick) {
1107
+ await locator.dblclick({ timeout, button: opts.button, modifiers: opts.modifiers });
1108
+ } else {
1109
+ await locator.click({ timeout, button: opts.button, modifiers: opts.modifiers });
1110
+ }
1111
+ } catch (err) {
1112
+ throw toAIFriendlyError(err, opts.ref);
1113
+ }
1114
+ }
1115
+ async function hoverViaPlaywright(opts) {
1116
+ const page = await getPageForTargetId({ cdpUrl: opts.cdpUrl, targetId: opts.targetId });
1117
+ ensurePageState(page);
1118
+ restoreRoleRefsForTarget({ cdpUrl: opts.cdpUrl, targetId: opts.targetId, page });
1119
+ try {
1120
+ await refLocator(page, opts.ref).hover({
1121
+ timeout: Math.max(500, Math.min(6e4, opts.timeoutMs ?? 8e3))
1122
+ });
1123
+ } catch (err) {
1124
+ throw toAIFriendlyError(err, opts.ref);
1125
+ }
1126
+ }
1127
+ async function typeViaPlaywright(opts) {
1128
+ const text = String(opts.text ?? "");
1129
+ const page = await getPageForTargetId({ cdpUrl: opts.cdpUrl, targetId: opts.targetId });
1130
+ ensurePageState(page);
1131
+ restoreRoleRefsForTarget({ cdpUrl: opts.cdpUrl, targetId: opts.targetId, page });
1132
+ const locator = refLocator(page, opts.ref);
1133
+ const timeout = Math.max(500, Math.min(6e4, opts.timeoutMs ?? 8e3));
1134
+ try {
1135
+ if (opts.slowly) {
1136
+ await locator.click({ timeout });
1137
+ await locator.type(text, { timeout, delay: 75 });
1138
+ } else {
1139
+ await locator.fill(text, { timeout });
1140
+ }
1141
+ if (opts.submit) await locator.press("Enter", { timeout });
1142
+ } catch (err) {
1143
+ throw toAIFriendlyError(err, opts.ref);
1144
+ }
1145
+ }
1146
+ async function selectOptionViaPlaywright(opts) {
1147
+ if (!opts.values?.length) throw new Error("values are required");
1148
+ const page = await getPageForTargetId({ cdpUrl: opts.cdpUrl, targetId: opts.targetId });
1149
+ ensurePageState(page);
1150
+ restoreRoleRefsForTarget({ cdpUrl: opts.cdpUrl, targetId: opts.targetId, page });
1151
+ try {
1152
+ await refLocator(page, opts.ref).selectOption(opts.values, {
1153
+ timeout: Math.max(500, Math.min(6e4, opts.timeoutMs ?? 8e3))
1154
+ });
1155
+ } catch (err) {
1156
+ throw toAIFriendlyError(err, opts.ref);
1157
+ }
1158
+ }
1159
+ async function dragViaPlaywright(opts) {
1160
+ const page = await getPageForTargetId({ cdpUrl: opts.cdpUrl, targetId: opts.targetId });
1161
+ ensurePageState(page);
1162
+ restoreRoleRefsForTarget({ cdpUrl: opts.cdpUrl, targetId: opts.targetId, page });
1163
+ try {
1164
+ await refLocator(page, opts.startRef).dragTo(refLocator(page, opts.endRef), {
1165
+ timeout: Math.max(500, Math.min(6e4, opts.timeoutMs ?? 8e3))
1166
+ });
1167
+ } catch (err) {
1168
+ throw toAIFriendlyError(err, `${opts.startRef} -> ${opts.endRef}`);
1169
+ }
1170
+ }
1171
+ async function fillFormViaPlaywright(opts) {
1172
+ const page = await getPageForTargetId({ cdpUrl: opts.cdpUrl, targetId: opts.targetId });
1173
+ ensurePageState(page);
1174
+ restoreRoleRefsForTarget({ cdpUrl: opts.cdpUrl, targetId: opts.targetId, page });
1175
+ const timeout = Math.max(500, Math.min(6e4, opts.timeoutMs ?? 8e3));
1176
+ for (const field of opts.fields) {
1177
+ const ref = field.ref.trim();
1178
+ const type = field.type.trim();
1179
+ const rawValue = field.value;
1180
+ const value = typeof rawValue === "string" ? rawValue : typeof rawValue === "number" || typeof rawValue === "boolean" ? String(rawValue) : "";
1181
+ if (!ref || !type) continue;
1182
+ const locator = refLocator(page, ref);
1183
+ if (type === "checkbox" || type === "radio") {
1184
+ const checked = rawValue === true || rawValue === 1 || rawValue === "1" || rawValue === "true";
1185
+ try {
1186
+ await locator.setChecked(checked, { timeout });
1187
+ } catch (err) {
1188
+ throw toAIFriendlyError(err, ref);
1189
+ }
1190
+ continue;
1191
+ }
1192
+ try {
1193
+ await locator.fill(value, { timeout });
1194
+ } catch (err) {
1195
+ throw toAIFriendlyError(err, ref);
1196
+ }
1197
+ }
1198
+ }
1199
+ async function scrollIntoViewViaPlaywright(opts) {
1200
+ const page = await getPageForTargetId({ cdpUrl: opts.cdpUrl, targetId: opts.targetId });
1201
+ ensurePageState(page);
1202
+ restoreRoleRefsForTarget({ cdpUrl: opts.cdpUrl, targetId: opts.targetId, page });
1203
+ try {
1204
+ await refLocator(page, opts.ref).scrollIntoViewIfNeeded({
1205
+ timeout: normalizeTimeoutMs(opts.timeoutMs, 2e4)
1206
+ });
1207
+ } catch (err) {
1208
+ throw toAIFriendlyError(err, opts.ref);
1209
+ }
1210
+ }
1211
+
1212
+ // src/actions/keyboard.ts
1213
+ async function pressKeyViaPlaywright(opts) {
1214
+ const key = String(opts.key ?? "").trim();
1215
+ if (!key) throw new Error("key is required");
1216
+ const page = await getPageForTargetId({ cdpUrl: opts.cdpUrl, targetId: opts.targetId });
1217
+ ensurePageState(page);
1218
+ await page.keyboard.press(key, { delay: Math.max(0, Math.floor(opts.delayMs ?? 0)) });
1219
+ }
1220
+
1221
+ // src/actions/navigation.ts
1222
+ async function navigateViaPlaywright(opts) {
1223
+ const url = String(opts.url ?? "").trim();
1224
+ if (!url) throw new Error("url is required");
1225
+ const page = await getPageForTargetId({ cdpUrl: opts.cdpUrl, targetId: opts.targetId });
1226
+ ensurePageState(page);
1227
+ await page.goto(url, { timeout: Math.max(1e3, Math.min(12e4, opts.timeoutMs ?? 2e4)) });
1228
+ return { url: page.url() };
1229
+ }
1230
+ async function listPagesViaPlaywright(opts) {
1231
+ const { browser } = await connectBrowser(opts.cdpUrl);
1232
+ const pages = await getAllPages(browser);
1233
+ const results = [];
1234
+ for (const page of pages) {
1235
+ const tid = await pageTargetId(page).catch(() => null);
1236
+ if (tid) results.push({
1237
+ targetId: tid,
1238
+ title: await page.title().catch(() => ""),
1239
+ url: page.url(),
1240
+ type: "page"
1241
+ });
1242
+ }
1243
+ return results;
1244
+ }
1245
+ async function createPageViaPlaywright(opts) {
1246
+ const { browser } = await connectBrowser(opts.cdpUrl);
1247
+ const context = browser.contexts()[0] ?? await browser.newContext();
1248
+ ensureContextState(context);
1249
+ const page = await context.newPage();
1250
+ ensurePageState(page);
1251
+ const targetUrl = (opts.url ?? "").trim() || "about:blank";
1252
+ if (targetUrl !== "about:blank") {
1253
+ await page.goto(targetUrl, { timeout: 3e4 }).catch(() => {
1254
+ });
1255
+ }
1256
+ const tid = await pageTargetId(page).catch(() => null);
1257
+ if (!tid) throw new Error("Failed to get targetId for new page");
1258
+ return {
1259
+ targetId: tid,
1260
+ title: await page.title().catch(() => ""),
1261
+ url: page.url(),
1262
+ type: "page"
1263
+ };
1264
+ }
1265
+ async function closePageByTargetIdViaPlaywright(opts) {
1266
+ const { browser } = await connectBrowser(opts.cdpUrl);
1267
+ const page = await findPageByTargetId(browser, opts.targetId, opts.cdpUrl);
1268
+ if (!page) throw new Error(`Tab not found (targetId: ${opts.targetId}). Use browser.tabs() to list open tabs.`);
1269
+ await page.close();
1270
+ }
1271
+ async function focusPageByTargetIdViaPlaywright(opts) {
1272
+ const { browser } = await connectBrowser(opts.cdpUrl);
1273
+ const page = await findPageByTargetId(browser, opts.targetId, opts.cdpUrl);
1274
+ if (!page) throw new Error(`Tab not found (targetId: ${opts.targetId}). Use browser.tabs() to list open tabs.`);
1275
+ try {
1276
+ await page.bringToFront();
1277
+ } catch (err) {
1278
+ const session = await page.context().newCDPSession(page);
1279
+ try {
1280
+ await session.send("Page.bringToFront");
1281
+ } catch {
1282
+ throw err;
1283
+ } finally {
1284
+ await session.detach().catch(() => {
1285
+ });
1286
+ }
1287
+ }
1288
+ }
1289
+ async function resizeViewportViaPlaywright(opts) {
1290
+ const page = await getPageForTargetId({ cdpUrl: opts.cdpUrl, targetId: opts.targetId });
1291
+ ensurePageState(page);
1292
+ await page.setViewportSize({
1293
+ width: Math.max(1, Math.floor(opts.width)),
1294
+ height: Math.max(1, Math.floor(opts.height))
1295
+ });
1296
+ }
1297
+
1298
+ // src/actions/wait.ts
1299
+ async function waitForViaPlaywright(opts) {
1300
+ const page = await getPageForTargetId({ cdpUrl: opts.cdpUrl, targetId: opts.targetId });
1301
+ ensurePageState(page);
1302
+ const timeout = normalizeTimeoutMs(opts.timeoutMs, 2e4);
1303
+ if (typeof opts.timeMs === "number" && Number.isFinite(opts.timeMs)) {
1304
+ await page.waitForTimeout(Math.max(0, opts.timeMs));
1305
+ }
1306
+ if (opts.text) {
1307
+ await page.getByText(opts.text).first().waitFor({ state: "visible", timeout });
1308
+ }
1309
+ if (opts.textGone) {
1310
+ await page.getByText(opts.textGone).first().waitFor({ state: "hidden", timeout });
1311
+ }
1312
+ if (opts.selector) {
1313
+ const selector = String(opts.selector).trim();
1314
+ if (selector) await page.locator(selector).first().waitFor({ state: "visible", timeout });
1315
+ }
1316
+ if (opts.url) {
1317
+ const url = String(opts.url).trim();
1318
+ if (url) await page.waitForURL(url, { timeout });
1319
+ }
1320
+ if (opts.loadState) {
1321
+ await page.waitForLoadState(opts.loadState, { timeout });
1322
+ }
1323
+ if (opts.fn) {
1324
+ const fn = String(opts.fn).trim();
1325
+ if (fn) await page.waitForFunction(fn, { timeout });
1326
+ }
1327
+ }
1328
+
1329
+ // src/actions/evaluate.ts
1330
+ async function evaluateInAllFramesViaPlaywright(opts) {
1331
+ const fnText = String(opts.fn ?? "").trim();
1332
+ if (!fnText) throw new Error("function is required");
1333
+ const page = await getPageForTargetId({ cdpUrl: opts.cdpUrl, targetId: opts.targetId });
1334
+ const frames = page.frames();
1335
+ const results = [];
1336
+ for (const frame of frames) {
1337
+ try {
1338
+ const result = await frame.evaluate(
1339
+ // eslint-disable-next-line no-eval
1340
+ (fnBody) => {
1341
+ "use strict";
1342
+ try {
1343
+ const candidate = (0, eval)("(" + fnBody + ")");
1344
+ return typeof candidate === "function" ? candidate() : candidate;
1345
+ } catch (err) {
1346
+ throw new Error("Invalid evaluate function: " + (err?.message ?? String(err)));
1347
+ }
1348
+ },
1349
+ fnText
1350
+ );
1351
+ results.push({
1352
+ frameUrl: frame.url(),
1353
+ frameName: frame.name(),
1354
+ result
1355
+ });
1356
+ } catch {
1357
+ }
1358
+ }
1359
+ return results;
1360
+ }
1361
+ async function evaluateViaPlaywright(opts) {
1362
+ const fnText = String(opts.fn ?? "").trim();
1363
+ if (!fnText) throw new Error("function is required");
1364
+ const page = await getPageForTargetId({ cdpUrl: opts.cdpUrl, targetId: opts.targetId });
1365
+ ensurePageState(page);
1366
+ restoreRoleRefsForTarget({ cdpUrl: opts.cdpUrl, targetId: opts.targetId, page });
1367
+ if (opts.ref) {
1368
+ const locator = refLocator(page, opts.ref);
1369
+ return await locator.evaluate(
1370
+ // eslint-disable-next-line no-eval
1371
+ (el, fnBody) => {
1372
+ try {
1373
+ const candidate = (0, eval)("(" + fnBody + ")");
1374
+ return typeof candidate === "function" ? candidate(el) : candidate;
1375
+ } catch (err) {
1376
+ throw new Error("Invalid evaluate function: " + (err?.message ?? String(err)));
1377
+ }
1378
+ },
1379
+ fnText
1380
+ );
1381
+ }
1382
+ return await page.evaluate(
1383
+ // eslint-disable-next-line no-eval
1384
+ (fnBody) => {
1385
+ try {
1386
+ const candidate = (0, eval)("(" + fnBody + ")");
1387
+ return typeof candidate === "function" ? candidate() : candidate;
1388
+ } catch (err) {
1389
+ throw new Error("Invalid evaluate function: " + (err?.message ?? String(err)));
1390
+ }
1391
+ },
1392
+ fnText
1393
+ );
1394
+ }
1395
+
1396
+ // src/capture/screenshot.ts
1397
+ async function takeScreenshotViaPlaywright(opts) {
1398
+ const page = await getPageForTargetId({ cdpUrl: opts.cdpUrl, targetId: opts.targetId });
1399
+ ensurePageState(page);
1400
+ restoreRoleRefsForTarget({ cdpUrl: opts.cdpUrl, targetId: opts.targetId, page });
1401
+ const type = opts.type ?? "png";
1402
+ if (opts.ref) {
1403
+ if (opts.fullPage) throw new Error("fullPage is not supported for element screenshots");
1404
+ return { buffer: await refLocator(page, opts.ref).screenshot({ type }) };
1405
+ }
1406
+ if (opts.element) {
1407
+ if (opts.fullPage) throw new Error("fullPage is not supported for element screenshots");
1408
+ return { buffer: await page.locator(opts.element).first().screenshot({ type }) };
1409
+ }
1410
+ return { buffer: await page.screenshot({ type, fullPage: Boolean(opts.fullPage) }) };
1411
+ }
1412
+
1413
+ // src/capture/pdf.ts
1414
+ async function pdfViaPlaywright(opts) {
1415
+ const page = await getPageForTargetId({ cdpUrl: opts.cdpUrl, targetId: opts.targetId });
1416
+ ensurePageState(page);
1417
+ return { buffer: await page.pdf({ printBackground: true }) };
1418
+ }
1419
+
1420
+ // src/capture/activity.ts
1421
+ function consolePriority(level) {
1422
+ switch (level) {
1423
+ case "error":
1424
+ return 3;
1425
+ case "warning":
1426
+ return 2;
1427
+ case "info":
1428
+ case "log":
1429
+ return 1;
1430
+ case "debug":
1431
+ return 0;
1432
+ default:
1433
+ return 1;
1434
+ }
1435
+ }
1436
+ async function getConsoleMessagesViaPlaywright(opts) {
1437
+ const state = ensurePageState(await getPageForTargetId({ cdpUrl: opts.cdpUrl, targetId: opts.targetId }));
1438
+ if (!opts.level) return [...state.console];
1439
+ const min = consolePriority(opts.level);
1440
+ return state.console.filter((msg) => consolePriority(msg.type) >= min);
1441
+ }
1442
+ async function getPageErrorsViaPlaywright(opts) {
1443
+ const state = ensurePageState(await getPageForTargetId({ cdpUrl: opts.cdpUrl, targetId: opts.targetId }));
1444
+ const errors = [...state.errors];
1445
+ if (opts.clear) state.errors = [];
1446
+ return { errors };
1447
+ }
1448
+ async function getNetworkRequestsViaPlaywright(opts) {
1449
+ const state = ensurePageState(await getPageForTargetId({ cdpUrl: opts.cdpUrl, targetId: opts.targetId }));
1450
+ const raw = [...state.requests];
1451
+ const filter = typeof opts.filter === "string" ? opts.filter.trim() : "";
1452
+ const requests = filter ? raw.filter((r) => r.url.includes(filter)) : raw;
1453
+ if (opts.clear) {
1454
+ state.requests = [];
1455
+ state.requestIds = /* @__PURE__ */ new WeakMap();
1456
+ }
1457
+ return { requests };
1458
+ }
1459
+
1460
+ // src/storage/index.ts
1461
+ async function cookiesGetViaPlaywright(opts) {
1462
+ const page = await getPageForTargetId({ cdpUrl: opts.cdpUrl, targetId: opts.targetId });
1463
+ ensurePageState(page);
1464
+ return { cookies: await page.context().cookies() };
1465
+ }
1466
+ async function cookiesSetViaPlaywright(opts) {
1467
+ const page = await getPageForTargetId({ cdpUrl: opts.cdpUrl, targetId: opts.targetId });
1468
+ ensurePageState(page);
1469
+ const cookie = opts.cookie;
1470
+ if (!cookie.name || cookie.value === void 0) throw new Error("cookie name and value are required");
1471
+ const hasUrl = typeof cookie.url === "string" && cookie.url.trim();
1472
+ const hasDomainPath = typeof cookie.domain === "string" && cookie.domain.trim() && typeof cookie.path === "string" && cookie.path.trim();
1473
+ if (!hasUrl && !hasDomainPath) throw new Error("cookie requires url, or domain+path");
1474
+ await page.context().addCookies([cookie]);
1475
+ }
1476
+ async function cookiesClearViaPlaywright(opts) {
1477
+ const page = await getPageForTargetId({ cdpUrl: opts.cdpUrl, targetId: opts.targetId });
1478
+ ensurePageState(page);
1479
+ await page.context().clearCookies();
1480
+ }
1481
+ async function storageGetViaPlaywright(opts) {
1482
+ const page = await getPageForTargetId({ cdpUrl: opts.cdpUrl, targetId: opts.targetId });
1483
+ ensurePageState(page);
1484
+ return {
1485
+ values: await page.evaluate(
1486
+ ({ kind, key }) => {
1487
+ const store = kind === "session" ? window.sessionStorage : window.localStorage;
1488
+ if (key) {
1489
+ const value = store.getItem(key);
1490
+ return value === null ? {} : { [key]: value };
1491
+ }
1492
+ const out = {};
1493
+ for (let i = 0; i < store.length; i++) {
1494
+ const k = store.key(i);
1495
+ if (!k) continue;
1496
+ const v = store.getItem(k);
1497
+ if (v !== null) out[k] = v;
1498
+ }
1499
+ return out;
1500
+ },
1501
+ { kind: opts.kind, key: opts.key }
1502
+ ) ?? {}
1503
+ };
1504
+ }
1505
+ async function storageSetViaPlaywright(opts) {
1506
+ const key = String(opts.key ?? "");
1507
+ if (!key) throw new Error("key is required");
1508
+ const page = await getPageForTargetId({ cdpUrl: opts.cdpUrl, targetId: opts.targetId });
1509
+ ensurePageState(page);
1510
+ await page.evaluate(
1511
+ ({ kind, key: k, value }) => {
1512
+ (kind === "session" ? window.sessionStorage : window.localStorage).setItem(k, value);
1513
+ },
1514
+ { kind: opts.kind, key, value: String(opts.value ?? "") }
1515
+ );
1516
+ }
1517
+ async function storageClearViaPlaywright(opts) {
1518
+ const page = await getPageForTargetId({ cdpUrl: opts.cdpUrl, targetId: opts.targetId });
1519
+ ensurePageState(page);
1520
+ await page.evaluate(
1521
+ ({ kind }) => {
1522
+ (kind === "session" ? window.sessionStorage : window.localStorage).clear();
1523
+ },
1524
+ { kind: opts.kind }
1525
+ );
1526
+ }
1527
+
1528
+ // src/browser.ts
1529
+ var CrawlPage = class {
1530
+ cdpUrl;
1531
+ targetId;
1532
+ /** @internal */
1533
+ constructor(cdpUrl, targetId) {
1534
+ this.cdpUrl = cdpUrl;
1535
+ this.targetId = targetId;
1536
+ }
1537
+ /** The CDP target ID for this page. Use this to identify the page in multi-tab scenarios. */
1538
+ get id() {
1539
+ return this.targetId;
1540
+ }
1541
+ // ── Snapshot ──────────────────────────────────────────────────
1542
+ /**
1543
+ * Take an AI-readable snapshot of the page.
1544
+ *
1545
+ * Returns a text tree with numbered refs (`e1`, `e2`, ...) that map to
1546
+ * interactive elements. Use these refs with actions like `click()` and `type()`.
1547
+ *
1548
+ * @param opts - Snapshot options (mode, filtering, depth limits)
1549
+ * @returns Snapshot text, ref map, and statistics
1550
+ *
1551
+ * @example
1552
+ * ```ts
1553
+ * // Default snapshot (aria mode)
1554
+ * const { snapshot, refs } = await page.snapshot();
1555
+ *
1556
+ * // Interactive elements only, compact
1557
+ * const result = await page.snapshot({ interactive: true, compact: true });
1558
+ *
1559
+ * // Role-based mode (uses getByRole resolution)
1560
+ * const result = await page.snapshot({ mode: 'role' });
1561
+ * ```
1562
+ */
1563
+ async snapshot(opts) {
1564
+ if (opts?.mode === "role") {
1565
+ return snapshotRole({
1566
+ cdpUrl: this.cdpUrl,
1567
+ targetId: this.targetId,
1568
+ selector: opts?.selector,
1569
+ frameSelector: opts?.frameSelector,
1570
+ options: {
1571
+ interactive: opts?.interactive,
1572
+ compact: opts?.compact,
1573
+ maxDepth: opts?.maxDepth
1574
+ }
1575
+ });
1576
+ }
1577
+ return snapshotAi({
1578
+ cdpUrl: this.cdpUrl,
1579
+ targetId: this.targetId,
1580
+ maxChars: opts?.maxChars,
1581
+ options: {
1582
+ interactive: opts?.interactive,
1583
+ compact: opts?.compact,
1584
+ maxDepth: opts?.maxDepth
1585
+ }
1586
+ });
1587
+ }
1588
+ /**
1589
+ * Take a raw ARIA accessibility tree snapshot via CDP.
1590
+ *
1591
+ * Unlike `snapshot()`, this returns structured node data rather than
1592
+ * an AI-readable text tree. Useful for programmatic accessibility analysis.
1593
+ *
1594
+ * @param opts - Options (limit: max nodes to return, default 500)
1595
+ * @returns Array of accessibility tree nodes
1596
+ */
1597
+ async ariaSnapshot(opts) {
1598
+ return snapshotAria({ cdpUrl: this.cdpUrl, targetId: this.targetId, limit: opts?.limit });
1599
+ }
1600
+ // ── Interactions ─────────────────────────────────────────────
1601
+ /**
1602
+ * Click an element by ref.
1603
+ *
1604
+ * @param ref - Ref ID from a snapshot (e.g. `'e1'`)
1605
+ * @param opts - Click options (double-click, button, modifiers)
1606
+ *
1607
+ * @example
1608
+ * ```ts
1609
+ * await page.click('e1');
1610
+ * await page.click('e2', { doubleClick: true });
1611
+ * await page.click('e3', { button: 'right' });
1612
+ * await page.click('e4', { modifiers: ['Control'] });
1613
+ * ```
1614
+ */
1615
+ async click(ref, opts) {
1616
+ return clickViaPlaywright({
1617
+ cdpUrl: this.cdpUrl,
1618
+ targetId: this.targetId,
1619
+ ref,
1620
+ doubleClick: opts?.doubleClick,
1621
+ button: opts?.button,
1622
+ modifiers: opts?.modifiers,
1623
+ timeoutMs: opts?.timeoutMs
1624
+ });
1625
+ }
1626
+ /**
1627
+ * Type text into an input element by ref.
1628
+ *
1629
+ * By default, uses Playwright's `fill()` for instant input. Use `slowly: true`
1630
+ * to simulate real keystroke typing with a 75ms delay per character.
1631
+ *
1632
+ * @param ref - Ref ID of the input element (e.g. `'e3'`)
1633
+ * @param text - Text to type
1634
+ * @param opts - Type options (submit, slowly)
1635
+ *
1636
+ * @example
1637
+ * ```ts
1638
+ * await page.type('e3', 'hello world');
1639
+ * await page.type('e3', 'slow typing', { slowly: true });
1640
+ * await page.type('e3', 'search query', { submit: true }); // press Enter after
1641
+ * ```
1642
+ */
1643
+ async type(ref, text, opts) {
1644
+ return typeViaPlaywright({
1645
+ cdpUrl: this.cdpUrl,
1646
+ targetId: this.targetId,
1647
+ ref,
1648
+ text,
1649
+ submit: opts?.submit,
1650
+ slowly: opts?.slowly,
1651
+ timeoutMs: opts?.timeoutMs
1652
+ });
1653
+ }
1654
+ /**
1655
+ * Hover over an element by ref.
1656
+ *
1657
+ * @param ref - Ref ID from a snapshot
1658
+ * @param opts - Timeout options
1659
+ */
1660
+ async hover(ref, opts) {
1661
+ return hoverViaPlaywright({
1662
+ cdpUrl: this.cdpUrl,
1663
+ targetId: this.targetId,
1664
+ ref,
1665
+ timeoutMs: opts?.timeoutMs
1666
+ });
1667
+ }
1668
+ /**
1669
+ * Select option(s) in a `<select>` dropdown by ref.
1670
+ *
1671
+ * @param ref - Ref ID of the select element
1672
+ * @param values - One or more option labels/values to select
1673
+ *
1674
+ * @example
1675
+ * ```ts
1676
+ * await page.select('e5', 'Option A');
1677
+ * await page.select('e5', 'Option A', 'Option B'); // multi-select
1678
+ * ```
1679
+ */
1680
+ async select(ref, ...values) {
1681
+ return selectOptionViaPlaywright({
1682
+ cdpUrl: this.cdpUrl,
1683
+ targetId: this.targetId,
1684
+ ref,
1685
+ values
1686
+ });
1687
+ }
1688
+ /**
1689
+ * Drag one element to another.
1690
+ *
1691
+ * @param startRef - Ref ID of the element to drag
1692
+ * @param endRef - Ref ID of the drop target
1693
+ * @param opts - Timeout options
1694
+ */
1695
+ async drag(startRef, endRef, opts) {
1696
+ return dragViaPlaywright({
1697
+ cdpUrl: this.cdpUrl,
1698
+ targetId: this.targetId,
1699
+ startRef,
1700
+ endRef,
1701
+ timeoutMs: opts?.timeoutMs
1702
+ });
1703
+ }
1704
+ /**
1705
+ * Fill multiple form fields at once.
1706
+ *
1707
+ * Supports text inputs, checkboxes, and radio buttons.
1708
+ *
1709
+ * @param fields - Array of form fields to fill
1710
+ *
1711
+ * @example
1712
+ * ```ts
1713
+ * await page.fill([
1714
+ * { ref: 'e2', type: 'text', value: 'Jane Doe' },
1715
+ * { ref: 'e4', type: 'text', value: 'jane@example.com' },
1716
+ * { ref: 'e6', type: 'checkbox', value: true },
1717
+ * ]);
1718
+ * ```
1719
+ */
1720
+ async fill(fields) {
1721
+ return fillFormViaPlaywright({
1722
+ cdpUrl: this.cdpUrl,
1723
+ targetId: this.targetId,
1724
+ fields
1725
+ });
1726
+ }
1727
+ /**
1728
+ * Scroll an element into the visible viewport.
1729
+ *
1730
+ * @param ref - Ref ID of the element to scroll to
1731
+ * @param opts - Timeout options
1732
+ */
1733
+ async scrollIntoView(ref, opts) {
1734
+ return scrollIntoViewViaPlaywright({
1735
+ cdpUrl: this.cdpUrl,
1736
+ targetId: this.targetId,
1737
+ ref,
1738
+ timeoutMs: opts?.timeoutMs
1739
+ });
1740
+ }
1741
+ // ── Keyboard ─────────────────────────────────────────────────
1742
+ /**
1743
+ * Press a keyboard key or key combination.
1744
+ *
1745
+ * Uses Playwright's key names. Supports combinations with `+`.
1746
+ *
1747
+ * @param key - Key to press (e.g. `'Enter'`, `'Tab'`, `'Control+a'`, `'Meta+c'`)
1748
+ * @param opts - Options (delayMs: hold time between keydown and keyup)
1749
+ *
1750
+ * @example
1751
+ * ```ts
1752
+ * await page.press('Enter');
1753
+ * await page.press('Control+a');
1754
+ * await page.press('Meta+Shift+p');
1755
+ * ```
1756
+ */
1757
+ async press(key, opts) {
1758
+ return pressKeyViaPlaywright({
1759
+ cdpUrl: this.cdpUrl,
1760
+ targetId: this.targetId,
1761
+ key,
1762
+ delayMs: opts?.delayMs
1763
+ });
1764
+ }
1765
+ // ── Navigation ───────────────────────────────────────────────
1766
+ /**
1767
+ * Navigate to a URL.
1768
+ *
1769
+ * @param url - The URL to navigate to
1770
+ * @param opts - Timeout options
1771
+ * @returns The final URL after navigation (may differ due to redirects)
1772
+ */
1773
+ async goto(url, opts) {
1774
+ return navigateViaPlaywright({
1775
+ cdpUrl: this.cdpUrl,
1776
+ targetId: this.targetId,
1777
+ url,
1778
+ timeoutMs: opts?.timeoutMs
1779
+ });
1780
+ }
1781
+ // ── Wait ─────────────────────────────────────────────────────
1782
+ /**
1783
+ * Wait for various conditions on the page.
1784
+ *
1785
+ * Multiple conditions can be specified — they are checked in order.
1786
+ *
1787
+ * @param opts - Wait conditions (text, URL, load state, selector, etc.)
1788
+ *
1789
+ * @example
1790
+ * ```ts
1791
+ * await page.waitFor({ loadState: 'networkidle' });
1792
+ * await page.waitFor({ text: 'Welcome back' });
1793
+ * await page.waitFor({ url: '**\/dashboard' });
1794
+ * await page.waitFor({ timeMs: 1000 }); // sleep
1795
+ * ```
1796
+ */
1797
+ async waitFor(opts) {
1798
+ return waitForViaPlaywright({
1799
+ cdpUrl: this.cdpUrl,
1800
+ targetId: this.targetId,
1801
+ ...opts
1802
+ });
1803
+ }
1804
+ // ── Evaluate ─────────────────────────────────────────────────
1805
+ /**
1806
+ * Run JavaScript in the browser page context.
1807
+ *
1808
+ * The function string is evaluated in the browser's sandbox, not in Node.js.
1809
+ * Pass a `ref` to receive the element as the first argument.
1810
+ *
1811
+ * @param fn - JavaScript function body as a string
1812
+ * @param opts - Options (ref: scope evaluation to a specific element)
1813
+ * @returns The return value of the evaluated function
1814
+ *
1815
+ * @example
1816
+ * ```ts
1817
+ * const title = await page.evaluate('() => document.title');
1818
+ * const text = await page.evaluate('(el) => el.textContent', { ref: 'e1' });
1819
+ * const count = await page.evaluate('() => document.querySelectorAll("img").length');
1820
+ * ```
1821
+ */
1822
+ async evaluate(fn, opts) {
1823
+ return evaluateViaPlaywright({
1824
+ cdpUrl: this.cdpUrl,
1825
+ targetId: this.targetId,
1826
+ fn,
1827
+ ref: opts?.ref
1828
+ });
1829
+ }
1830
+ /**
1831
+ * Run JavaScript in ALL frames on the page (including cross-origin iframes).
1832
+ *
1833
+ * Playwright can access cross-origin frames via CDP, bypassing the same-origin policy.
1834
+ * This is essential for filling payment iframes (Stripe, etc.).
1835
+ *
1836
+ * @param fn - JavaScript function body as a string
1837
+ * @returns Array of results from each frame where evaluation succeeded
1838
+ *
1839
+ * @example
1840
+ * ```ts
1841
+ * const results = await page.evaluateInAllFrames(`() => {
1842
+ * const el = document.querySelector('input[name="cardnumber"]');
1843
+ * return el ? 'found' : null;
1844
+ * }`);
1845
+ * ```
1846
+ */
1847
+ async evaluateInAllFrames(fn) {
1848
+ return evaluateInAllFramesViaPlaywright({
1849
+ cdpUrl: this.cdpUrl,
1850
+ targetId: this.targetId,
1851
+ fn
1852
+ });
1853
+ }
1854
+ // ── Capture ──────────────────────────────────────────────────
1855
+ /**
1856
+ * Take a screenshot of the page or a specific element.
1857
+ *
1858
+ * @param opts - Screenshot options (fullPage, ref, element, type)
1859
+ * @returns PNG or JPEG image as a Buffer
1860
+ *
1861
+ * @example
1862
+ * ```ts
1863
+ * const screenshot = await page.screenshot();
1864
+ * const fullPage = await page.screenshot({ fullPage: true });
1865
+ * const element = await page.screenshot({ ref: 'e1' });
1866
+ * ```
1867
+ */
1868
+ async screenshot(opts) {
1869
+ const result = await takeScreenshotViaPlaywright({
1870
+ cdpUrl: this.cdpUrl,
1871
+ targetId: this.targetId,
1872
+ fullPage: opts?.fullPage,
1873
+ ref: opts?.ref,
1874
+ element: opts?.element,
1875
+ type: opts?.type
1876
+ });
1877
+ return result.buffer;
1878
+ }
1879
+ /**
1880
+ * Export the page as a PDF.
1881
+ *
1882
+ * Only works in headless mode.
1883
+ *
1884
+ * @returns PDF document as a Buffer
1885
+ */
1886
+ async pdf() {
1887
+ const result = await pdfViaPlaywright({ cdpUrl: this.cdpUrl, targetId: this.targetId });
1888
+ return result.buffer;
1889
+ }
1890
+ /**
1891
+ * Get console messages captured from the page.
1892
+ *
1893
+ * Messages are buffered automatically. Use `level` to filter by minimum severity.
1894
+ *
1895
+ * @param opts - Filter options (level: `'debug'` | `'log'` | `'info'` | `'warning'` | `'error'`)
1896
+ * @returns Array of captured console messages
1897
+ */
1898
+ async consoleLogs(opts) {
1899
+ return getConsoleMessagesViaPlaywright({
1900
+ cdpUrl: this.cdpUrl,
1901
+ targetId: this.targetId,
1902
+ level: opts?.level
1903
+ });
1904
+ }
1905
+ /**
1906
+ * Get uncaught errors from the page.
1907
+ *
1908
+ * @param opts - Options (clear: reset the error buffer after reading)
1909
+ * @returns Array of captured page errors
1910
+ */
1911
+ async pageErrors(opts) {
1912
+ const result = await getPageErrorsViaPlaywright({
1913
+ cdpUrl: this.cdpUrl,
1914
+ targetId: this.targetId,
1915
+ clear: opts?.clear
1916
+ });
1917
+ return result.errors;
1918
+ }
1919
+ /**
1920
+ * Get network requests captured from the page.
1921
+ *
1922
+ * @param opts - Options (filter: URL substring match, clear: reset the buffer)
1923
+ * @returns Array of captured network requests
1924
+ *
1925
+ * @example
1926
+ * ```ts
1927
+ * const all = await page.networkRequests();
1928
+ * const apiCalls = await page.networkRequests({ filter: '/api/' });
1929
+ * const fresh = await page.networkRequests({ clear: true }); // read and clear
1930
+ * ```
1931
+ */
1932
+ async networkRequests(opts) {
1933
+ const result = await getNetworkRequestsViaPlaywright({
1934
+ cdpUrl: this.cdpUrl,
1935
+ targetId: this.targetId,
1936
+ filter: opts?.filter,
1937
+ clear: opts?.clear
1938
+ });
1939
+ return result.requests;
1940
+ }
1941
+ // ── Viewport ─────────────────────────────────────────────────
1942
+ /**
1943
+ * Resize the browser viewport.
1944
+ *
1945
+ * @param width - Viewport width in pixels
1946
+ * @param height - Viewport height in pixels
1947
+ */
1948
+ async resize(width, height) {
1949
+ return resizeViewportViaPlaywright({
1950
+ cdpUrl: this.cdpUrl,
1951
+ targetId: this.targetId,
1952
+ width,
1953
+ height
1954
+ });
1955
+ }
1956
+ // ── Storage ──────────────────────────────────────────────────
1957
+ /**
1958
+ * Get all cookies for the current browser context.
1959
+ *
1960
+ * @returns Array of cookie objects
1961
+ */
1962
+ async cookies() {
1963
+ const result = await cookiesGetViaPlaywright({ cdpUrl: this.cdpUrl, targetId: this.targetId });
1964
+ return result.cookies;
1965
+ }
1966
+ /**
1967
+ * Set a cookie in the browser context.
1968
+ *
1969
+ * @param cookie - Cookie data (must include `name`, `value`, and either `url` or `domain`+`path`)
1970
+ *
1971
+ * @example
1972
+ * ```ts
1973
+ * await page.setCookie({
1974
+ * name: 'token',
1975
+ * value: 'abc123',
1976
+ * url: 'https://example.com',
1977
+ * });
1978
+ * ```
1979
+ */
1980
+ async setCookie(cookie) {
1981
+ return cookiesSetViaPlaywright({ cdpUrl: this.cdpUrl, targetId: this.targetId, cookie });
1982
+ }
1983
+ /** Clear all cookies in the browser context. */
1984
+ async clearCookies() {
1985
+ return cookiesClearViaPlaywright({ cdpUrl: this.cdpUrl, targetId: this.targetId });
1986
+ }
1987
+ /**
1988
+ * Get values from localStorage or sessionStorage.
1989
+ *
1990
+ * @param kind - `'local'` for localStorage, `'session'` for sessionStorage
1991
+ * @param key - Optional specific key to retrieve (returns all if omitted)
1992
+ * @returns Key-value map of storage entries
1993
+ */
1994
+ async storageGet(kind, key) {
1995
+ const result = await storageGetViaPlaywright({
1996
+ cdpUrl: this.cdpUrl,
1997
+ targetId: this.targetId,
1998
+ kind,
1999
+ key
2000
+ });
2001
+ return result.values;
2002
+ }
2003
+ /**
2004
+ * Set a value in localStorage or sessionStorage.
2005
+ *
2006
+ * @param kind - `'local'` for localStorage, `'session'` for sessionStorage
2007
+ * @param key - Storage key
2008
+ * @param value - Storage value
2009
+ */
2010
+ async storageSet(kind, key, value) {
2011
+ return storageSetViaPlaywright({
2012
+ cdpUrl: this.cdpUrl,
2013
+ targetId: this.targetId,
2014
+ kind,
2015
+ key,
2016
+ value
2017
+ });
2018
+ }
2019
+ /**
2020
+ * Clear all entries in localStorage or sessionStorage.
2021
+ *
2022
+ * @param kind - `'local'` for localStorage, `'session'` for sessionStorage
2023
+ */
2024
+ async storageClear(kind) {
2025
+ return storageClearViaPlaywright({
2026
+ cdpUrl: this.cdpUrl,
2027
+ targetId: this.targetId,
2028
+ kind
2029
+ });
2030
+ }
2031
+ };
2032
+ var BrowserClaw = class _BrowserClaw {
2033
+ cdpUrl;
2034
+ chrome;
2035
+ constructor(cdpUrl, chrome) {
2036
+ this.cdpUrl = cdpUrl;
2037
+ this.chrome = chrome;
2038
+ }
2039
+ /**
2040
+ * Launch a new Chrome instance and connect to it.
2041
+ *
2042
+ * Automatically detects Chrome, Brave, Edge, or Chromium on the system.
2043
+ * Creates a dedicated browser profile to avoid conflicts with your daily browser.
2044
+ *
2045
+ * @param opts - Launch options (headless, executablePath, cdpPort, etc.)
2046
+ * @returns A connected BrowserClaw instance
2047
+ *
2048
+ * @example
2049
+ * ```ts
2050
+ * // Default: visible Chrome window
2051
+ * const browser = await BrowserClaw.launch();
2052
+ *
2053
+ * // Headless mode
2054
+ * const browser = await BrowserClaw.launch({ headless: true });
2055
+ *
2056
+ * // Specific browser
2057
+ * const browser = await BrowserClaw.launch({
2058
+ * executablePath: '/usr/bin/google-chrome',
2059
+ * });
2060
+ * ```
2061
+ */
2062
+ static async launch(opts = {}) {
2063
+ const chrome = await launchChrome(opts);
2064
+ const cdpUrl = `http://127.0.0.1:${chrome.cdpPort}`;
2065
+ return new _BrowserClaw(cdpUrl, chrome);
2066
+ }
2067
+ /**
2068
+ * Connect to an already-running Chrome instance via its CDP endpoint.
2069
+ *
2070
+ * The Chrome instance must have been started with `--remote-debugging-port`.
2071
+ *
2072
+ * @param cdpUrl - CDP endpoint URL (e.g. `'http://localhost:9222'`)
2073
+ * @returns A connected BrowserClaw instance
2074
+ *
2075
+ * @example
2076
+ * ```ts
2077
+ * // Chrome started with: chrome --remote-debugging-port=9222
2078
+ * const browser = await BrowserClaw.connect('http://localhost:9222');
2079
+ * ```
2080
+ */
2081
+ static async connect(cdpUrl) {
2082
+ if (!await isChromeReachable(cdpUrl, 3e3)) {
2083
+ throw new Error(`Cannot connect to Chrome at ${cdpUrl}. Is Chrome running with --remote-debugging-port?`);
2084
+ }
2085
+ await connectBrowser(cdpUrl);
2086
+ return new _BrowserClaw(cdpUrl, null);
2087
+ }
2088
+ /**
2089
+ * Open a URL in a new tab and return the page handle.
2090
+ *
2091
+ * @param url - URL to navigate to
2092
+ * @returns A CrawlPage for the new tab
2093
+ *
2094
+ * @example
2095
+ * ```ts
2096
+ * const page = await browser.open('https://example.com');
2097
+ * const { snapshot, refs } = await page.snapshot();
2098
+ * ```
2099
+ */
2100
+ async open(url) {
2101
+ const tab = await createPageViaPlaywright({ cdpUrl: this.cdpUrl, url });
2102
+ return new CrawlPage(this.cdpUrl, tab.targetId);
2103
+ }
2104
+ /**
2105
+ * Get a CrawlPage handle for the currently active tab.
2106
+ *
2107
+ * @returns CrawlPage for the first/active page
2108
+ */
2109
+ async currentPage() {
2110
+ const { browser } = await connectBrowser(this.cdpUrl);
2111
+ const pages = await getAllPages(browser);
2112
+ if (!pages.length) throw new Error("No pages available");
2113
+ const tid = await pageTargetId(pages[0]).catch(() => null);
2114
+ return new CrawlPage(this.cdpUrl, tid ?? "");
2115
+ }
2116
+ /**
2117
+ * List all open tabs.
2118
+ *
2119
+ * @returns Array of tab information objects
2120
+ */
2121
+ async tabs() {
2122
+ return listPagesViaPlaywright({ cdpUrl: this.cdpUrl });
2123
+ }
2124
+ /**
2125
+ * Bring a tab to the foreground.
2126
+ *
2127
+ * @param targetId - CDP target ID of the tab (from `tabs()` or `page.id`)
2128
+ */
2129
+ async focus(targetId) {
2130
+ return focusPageByTargetIdViaPlaywright({ cdpUrl: this.cdpUrl, targetId });
2131
+ }
2132
+ /**
2133
+ * Close a tab.
2134
+ *
2135
+ * @param targetId - CDP target ID of the tab to close
2136
+ */
2137
+ async close(targetId) {
2138
+ return closePageByTargetIdViaPlaywright({ cdpUrl: this.cdpUrl, targetId });
2139
+ }
2140
+ /**
2141
+ * Get a CrawlPage handle for a specific tab by its target ID.
2142
+ *
2143
+ * Unlike `open()`, this doesn't create a new tab — it wraps an existing one.
2144
+ *
2145
+ * @param targetId - CDP target ID of the tab
2146
+ * @returns CrawlPage for the specified tab
2147
+ */
2148
+ page(targetId) {
2149
+ return new CrawlPage(this.cdpUrl, targetId);
2150
+ }
2151
+ /** The CDP endpoint URL for this browser connection. */
2152
+ get url() {
2153
+ return this.cdpUrl;
2154
+ }
2155
+ /**
2156
+ * Stop the browser and clean up all resources.
2157
+ *
2158
+ * If the browser was launched by `BrowserClaw.launch()`, the Chrome process
2159
+ * will be terminated. If connected via `BrowserClaw.connect()`, only the
2160
+ * Playwright connection is closed.
2161
+ */
2162
+ async stop() {
2163
+ await disconnectBrowser();
2164
+ if (this.chrome) {
2165
+ await stopChrome(this.chrome);
2166
+ this.chrome = null;
2167
+ }
2168
+ }
2169
+ };
2170
+
2171
+ export { BrowserClaw, CrawlPage };
2172
+ //# sourceMappingURL=index.js.map
2173
+ //# sourceMappingURL=index.js.map