browserclaw 0.5.7 → 0.6.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/index.js CHANGED
@@ -1,15 +1,15 @@
1
- import os from 'os';
2
- import path, { posix, win32, resolve, dirname, join, basename, relative, sep, normalize } from 'path';
3
- import fs from 'fs';
4
- import net from 'net';
5
- import { spawn, execFileSync } from 'child_process';
6
- import { devices, chromium } from 'playwright-core';
7
1
  import http from 'http';
8
2
  import https from 'https';
9
- import { lookup as lookup$1 } from 'dns/promises';
10
- import { lookup } from 'dns';
11
- import { realpath, rename, rm, lstat } from 'fs/promises';
3
+ import { devices, chromium } from 'playwright-core';
4
+ import { spawn, execFileSync } from 'child_process';
5
+ import fs from 'fs';
6
+ import net from 'net';
7
+ import os from 'os';
8
+ import path, { posix, win32, resolve, dirname, join, basename, relative, sep, normalize } from 'path';
12
9
  import { randomUUID } from 'crypto';
10
+ import { lookup } from 'dns';
11
+ import { lookup as lookup$1 } from 'dns/promises';
12
+ import { lstat, realpath, rename, rm } from 'fs/promises';
13
13
 
14
14
  var __create = Object.create;
15
15
  var __defProp = Object.defineProperty;
@@ -910,7 +910,7 @@ function execText(command, args, timeoutMs = 1200) {
910
910
  encoding: "utf8",
911
911
  maxBuffer: 1024 * 1024
912
912
  });
913
- return String(output ?? "").trim() || null;
913
+ return output.trim() || null;
914
914
  } catch {
915
915
  return null;
916
916
  }
@@ -921,7 +921,8 @@ function inferKindFromIdentifier(identifier) {
921
921
  if (id.includes("edge")) return "edge";
922
922
  if (id.includes("chromium")) return "chromium";
923
923
  if (id.includes("canary")) return "canary";
924
- if (id.includes("opera") || id.includes("vivaldi") || id.includes("yandex") || id.includes("thebrowser")) return "chromium";
924
+ if (id.includes("opera") || id.includes("vivaldi") || id.includes("yandex") || id.includes("thebrowser"))
925
+ return "chromium";
925
926
  return "chrome";
926
927
  }
927
928
  function inferKindFromExeName(name) {
@@ -938,24 +939,29 @@ function findFirstExe(candidates) {
938
939
  return null;
939
940
  }
940
941
  function detectDefaultBrowserBundleIdMac() {
941
- const plistPath = path.join(os.homedir(), "Library/Preferences/com.apple.LaunchServices/com.apple.launchservices.secure.plist");
942
+ const plistPath = path.join(
943
+ os.homedir(),
944
+ "Library/Preferences/com.apple.LaunchServices/com.apple.launchservices.secure.plist"
945
+ );
942
946
  if (!fileExists(plistPath)) return null;
943
947
  const handlersRaw = execText("/usr/bin/plutil", ["-extract", "LSHandlers", "json", "-o", "-", "--", plistPath], 2e3);
944
- if (!handlersRaw) return null;
948
+ if (handlersRaw === null) return null;
945
949
  let handlers;
946
950
  try {
947
- handlers = JSON.parse(handlersRaw);
951
+ const parsed = JSON.parse(handlersRaw);
952
+ if (!Array.isArray(parsed)) return null;
953
+ handlers = parsed;
948
954
  } catch {
949
955
  return null;
950
956
  }
951
- if (!Array.isArray(handlers)) return null;
952
957
  const resolveScheme = (scheme) => {
953
958
  let candidate = null;
954
959
  for (const entry of handlers) {
955
- if (!entry || typeof entry !== "object") continue;
956
- if (entry.LSHandlerURLScheme !== scheme) continue;
957
- const role = typeof entry.LSHandlerRoleAll === "string" && entry.LSHandlerRoleAll || typeof entry.LSHandlerRoleViewer === "string" && entry.LSHandlerRoleViewer || null;
958
- if (role) candidate = role;
960
+ if (entry === null || entry === void 0 || typeof entry !== "object") continue;
961
+ const rec = entry;
962
+ if (rec.LSHandlerURLScheme !== scheme) continue;
963
+ const role = (typeof rec.LSHandlerRoleAll === "string" ? rec.LSHandlerRoleAll : null) ?? (typeof rec.LSHandlerRoleViewer === "string" ? rec.LSHandlerRoleViewer : null) ?? null;
964
+ if (role !== null) candidate = role;
959
965
  }
960
966
  return candidate;
961
967
  };
@@ -963,12 +969,12 @@ function detectDefaultBrowserBundleIdMac() {
963
969
  }
964
970
  function detectDefaultChromiumMac() {
965
971
  const bundleId = detectDefaultBrowserBundleIdMac();
966
- if (!bundleId || !CHROMIUM_BUNDLE_IDS.has(bundleId)) return null;
972
+ if (bundleId === null || !CHROMIUM_BUNDLE_IDS.has(bundleId)) return null;
967
973
  const appPathRaw = execText("/usr/bin/osascript", ["-e", `POSIX path of (path to application id "${bundleId}")`]);
968
- if (!appPathRaw) return null;
974
+ if (appPathRaw === null) return null;
969
975
  const appPath = appPathRaw.trim().replace(/\/$/, "");
970
976
  const exeName = execText("/usr/bin/defaults", ["read", path.join(appPath, "Contents", "Info"), "CFBundleExecutable"]);
971
- if (!exeName) return null;
977
+ if (exeName === null) return null;
972
978
  const exePath = path.join(appPath, "Contents", "MacOS", exeName.trim());
973
979
  if (!fileExists(exePath)) return null;
974
980
  return { kind: inferKindFromIdentifier(bundleId), path: exePath };
@@ -984,12 +990,15 @@ function findChromeMac() {
984
990
  { kind: "chromium", path: "/Applications/Chromium.app/Contents/MacOS/Chromium" },
985
991
  { kind: "chromium", path: path.join(os.homedir(), "Applications/Chromium.app/Contents/MacOS/Chromium") },
986
992
  { kind: "canary", path: "/Applications/Google Chrome Canary.app/Contents/MacOS/Google Chrome Canary" },
987
- { kind: "canary", path: path.join(os.homedir(), "Applications/Google Chrome Canary.app/Contents/MacOS/Google Chrome Canary") }
993
+ {
994
+ kind: "canary",
995
+ path: path.join(os.homedir(), "Applications/Google Chrome Canary.app/Contents/MacOS/Google Chrome Canary")
996
+ }
988
997
  ]);
989
998
  }
990
999
  function detectDefaultChromiumLinux() {
991
- const desktopId = execText("xdg-settings", ["get", "default-web-browser"]) || execText("xdg-mime", ["query", "default", "x-scheme-handler/http"]);
992
- if (!desktopId) return null;
1000
+ const desktopId = execText("xdg-settings", ["get", "default-web-browser"]) ?? execText("xdg-mime", ["query", "default", "x-scheme-handler/http"]);
1001
+ if (desktopId === null) return null;
993
1002
  const trimmed = desktopId.trim();
994
1003
  if (!CHROMIUM_DESKTOP_IDS.has(trimmed)) return null;
995
1004
  const searchDirs = [
@@ -1006,17 +1015,18 @@ function detectDefaultChromiumLinux() {
1006
1015
  break;
1007
1016
  }
1008
1017
  }
1009
- if (!desktopPath) return null;
1018
+ if (desktopPath === null) return null;
1010
1019
  let execLine = null;
1011
1020
  try {
1012
1021
  const lines = fs.readFileSync(desktopPath, "utf8").split(/\r?\n/);
1013
- for (const line of lines) if (line.startsWith("Exec=")) {
1014
- execLine = line.slice(5).trim();
1015
- break;
1016
- }
1022
+ for (const line of lines)
1023
+ if (line.startsWith("Exec=")) {
1024
+ execLine = line.slice(5).trim();
1025
+ break;
1026
+ }
1017
1027
  } catch {
1018
1028
  }
1019
- if (!execLine) return null;
1029
+ if (execLine === null) return null;
1020
1030
  const tokens = execLine.split(/\s+/);
1021
1031
  let command = null;
1022
1032
  for (const token of tokens) {
@@ -1024,9 +1034,9 @@ function detectDefaultChromiumLinux() {
1024
1034
  command = token.replace(/^["']|["']$/g, "");
1025
1035
  break;
1026
1036
  }
1027
- if (!command) return null;
1037
+ if (command === null) return null;
1028
1038
  const resolved = command.startsWith("/") ? command : execText("which", [command], 800)?.trim() ?? null;
1029
- if (!resolved) return null;
1039
+ if (resolved === null || resolved === "") return null;
1030
1040
  const exeName = path.posix.basename(resolved).toLowerCase();
1031
1041
  if (!CHROMIUM_EXE_NAMES.has(exeName)) return null;
1032
1042
  return { kind: inferKindFromExeName(exeName), path: resolved };
@@ -1055,21 +1065,30 @@ function findChromeWindows() {
1055
1065
  const candidates = [];
1056
1066
  if (localAppData) {
1057
1067
  candidates.push({ kind: "chrome", path: j(localAppData, "Google", "Chrome", "Application", "chrome.exe") });
1058
- candidates.push({ kind: "brave", path: j(localAppData, "BraveSoftware", "Brave-Browser", "Application", "brave.exe") });
1068
+ candidates.push({
1069
+ kind: "brave",
1070
+ path: j(localAppData, "BraveSoftware", "Brave-Browser", "Application", "brave.exe")
1071
+ });
1059
1072
  candidates.push({ kind: "edge", path: j(localAppData, "Microsoft", "Edge", "Application", "msedge.exe") });
1060
1073
  candidates.push({ kind: "chromium", path: j(localAppData, "Chromium", "Application", "chrome.exe") });
1061
1074
  candidates.push({ kind: "canary", path: j(localAppData, "Google", "Chrome SxS", "Application", "chrome.exe") });
1062
1075
  }
1063
1076
  candidates.push({ kind: "chrome", path: j(programFiles, "Google", "Chrome", "Application", "chrome.exe") });
1064
1077
  candidates.push({ kind: "chrome", path: j(programFilesX86, "Google", "Chrome", "Application", "chrome.exe") });
1065
- candidates.push({ kind: "brave", path: j(programFiles, "BraveSoftware", "Brave-Browser", "Application", "brave.exe") });
1066
- candidates.push({ kind: "brave", path: j(programFilesX86, "BraveSoftware", "Brave-Browser", "Application", "brave.exe") });
1078
+ candidates.push({
1079
+ kind: "brave",
1080
+ path: j(programFiles, "BraveSoftware", "Brave-Browser", "Application", "brave.exe")
1081
+ });
1082
+ candidates.push({
1083
+ kind: "brave",
1084
+ path: j(programFilesX86, "BraveSoftware", "Brave-Browser", "Application", "brave.exe")
1085
+ });
1067
1086
  candidates.push({ kind: "edge", path: j(programFiles, "Microsoft", "Edge", "Application", "msedge.exe") });
1068
1087
  candidates.push({ kind: "edge", path: j(programFilesX86, "Microsoft", "Edge", "Application", "msedge.exe") });
1069
1088
  return findFirstExe(candidates);
1070
1089
  }
1071
1090
  function resolveBrowserExecutable(opts) {
1072
- if (opts?.executablePath) {
1091
+ if (opts?.executablePath !== void 0 && opts.executablePath !== "") {
1073
1092
  if (!fileExists(opts.executablePath)) throw new Error(`executablePath not found: ${opts.executablePath}`);
1074
1093
  return { kind: "custom", path: opts.executablePath };
1075
1094
  }
@@ -1082,10 +1101,12 @@ function resolveBrowserExecutable(opts) {
1082
1101
  async function ensurePortAvailable(port) {
1083
1102
  await new Promise((resolve2, reject) => {
1084
1103
  const tester = net.createServer().once("error", (err) => {
1085
- if (err.code === "EADDRINUSE") reject(new Error(`Port ${port} is already in use`));
1104
+ if (err.code === "EADDRINUSE") reject(new Error(`Port ${String(port)} is already in use`));
1086
1105
  else reject(err);
1087
1106
  }).once("listening", () => {
1088
- tester.close(() => resolve2());
1107
+ tester.close(() => {
1108
+ resolve2();
1109
+ });
1089
1110
  }).listen(port);
1090
1111
  });
1091
1112
  }
@@ -1167,7 +1188,7 @@ function isLoopbackHost(hostname) {
1167
1188
  return hostname === "localhost" || hostname === "127.0.0.1" || hostname === "::1" || hostname === "[::1]";
1168
1189
  }
1169
1190
  function hasProxyEnvConfigured() {
1170
- return !!(process.env.HTTP_PROXY || process.env.HTTPS_PROXY || process.env.http_proxy || process.env.https_proxy);
1191
+ return (process.env.HTTP_PROXY ?? process.env.HTTPS_PROXY ?? process.env.http_proxy ?? process.env.https_proxy ?? "") !== "";
1171
1192
  }
1172
1193
  function normalizeCdpWsUrl(wsUrl, cdpUrl) {
1173
1194
  const ws = new URL(wsUrl);
@@ -1219,7 +1240,12 @@ async function canOpenWebSocket(url, timeoutMs) {
1219
1240
  }
1220
1241
  resolve2(value);
1221
1242
  };
1222
- const timer = setTimeout(() => finish(false), Math.max(50, timeoutMs + 25));
1243
+ const timer = setTimeout(
1244
+ () => {
1245
+ finish(false);
1246
+ },
1247
+ Math.max(50, timeoutMs + 25)
1248
+ );
1223
1249
  let ws;
1224
1250
  try {
1225
1251
  ws = new WebSocket(url);
@@ -1227,21 +1253,27 @@ async function canOpenWebSocket(url, timeoutMs) {
1227
1253
  finish(false);
1228
1254
  return;
1229
1255
  }
1230
- ws.onopen = () => finish(true);
1231
- ws.onerror = () => finish(false);
1256
+ ws.onopen = () => {
1257
+ finish(true);
1258
+ };
1259
+ ws.onerror = () => {
1260
+ finish(false);
1261
+ };
1232
1262
  });
1233
1263
  }
1234
1264
  async function fetchChromeVersion(cdpUrl, timeoutMs = 500, authToken) {
1235
1265
  const ctrl = new AbortController();
1236
- const t = setTimeout(() => ctrl.abort(), timeoutMs);
1266
+ const t = setTimeout(() => {
1267
+ ctrl.abort();
1268
+ }, timeoutMs);
1237
1269
  try {
1238
1270
  const httpBase = isWebSocketUrl(cdpUrl) ? normalizeCdpHttpBaseForJsonEndpoints(cdpUrl) : cdpUrl;
1239
1271
  const headers = {};
1240
- if (authToken) headers["Authorization"] = `Bearer ${authToken}`;
1272
+ if (authToken !== void 0 && authToken !== "") headers.Authorization = `Bearer ${authToken}`;
1241
1273
  const res = await fetch(appendCdpPath(httpBase, "/json/version"), { signal: ctrl.signal, headers });
1242
1274
  if (!res.ok) return null;
1243
1275
  const data = await res.json();
1244
- if (!data || typeof data !== "object") return null;
1276
+ if (data === null || data === void 0 || typeof data !== "object") return null;
1245
1277
  return data;
1246
1278
  } catch {
1247
1279
  return null;
@@ -1257,13 +1289,14 @@ async function isChromeReachable(cdpUrl, timeoutMs = 500, authToken) {
1257
1289
  async function getChromeWebSocketUrl(cdpUrl, timeoutMs = 500, authToken) {
1258
1290
  if (isWebSocketUrl(cdpUrl)) return cdpUrl;
1259
1291
  const version = await fetchChromeVersion(cdpUrl, timeoutMs, authToken);
1260
- const wsUrl = String(version?.webSocketDebuggerUrl ?? "").trim();
1261
- if (!wsUrl) return null;
1292
+ const rawWsUrl = version?.webSocketDebuggerUrl;
1293
+ const wsUrl = typeof rawWsUrl === "string" ? rawWsUrl.trim() : "";
1294
+ if (wsUrl === "") return null;
1262
1295
  return normalizeCdpWsUrl(wsUrl, cdpUrl);
1263
1296
  }
1264
1297
  async function isChromeCdpReady(cdpUrl, timeoutMs = 500, handshakeTimeoutMs = 800) {
1265
1298
  const wsUrl = await getChromeWebSocketUrl(cdpUrl, timeoutMs);
1266
- if (!wsUrl) return false;
1299
+ if (wsUrl === null) return false;
1267
1300
  return await canRunCdpHealthCommand(wsUrl, handshakeTimeoutMs);
1268
1301
  }
1269
1302
  async function canRunCdpHealthCommand(wsUrl, timeoutMs = 800) {
@@ -1279,7 +1312,12 @@ async function canRunCdpHealthCommand(wsUrl, timeoutMs = 800) {
1279
1312
  }
1280
1313
  resolve2(value);
1281
1314
  };
1282
- const timer = setTimeout(() => finish(false), Math.max(50, timeoutMs + 25));
1315
+ const timer = setTimeout(
1316
+ () => {
1317
+ finish(false);
1318
+ },
1319
+ Math.max(50, timeoutMs + 25)
1320
+ );
1283
1321
  let ws;
1284
1322
  try {
1285
1323
  ws = new WebSocket(wsUrl);
@@ -1297,26 +1335,33 @@ async function canRunCdpHealthCommand(wsUrl, timeoutMs = 800) {
1297
1335
  ws.onmessage = (event) => {
1298
1336
  try {
1299
1337
  const parsed = JSON.parse(String(event.data));
1300
- if (parsed?.id !== 1) return;
1301
- finish(Boolean(parsed.result && typeof parsed.result === "object"));
1338
+ if (typeof parsed !== "object" || parsed === null) return;
1339
+ const msg = parsed;
1340
+ if (msg.id !== 1) return;
1341
+ finish(typeof msg.result === "object" && msg.result !== null);
1302
1342
  } catch {
1303
1343
  }
1304
1344
  };
1305
- ws.onerror = () => finish(false);
1306
- ws.onclose = () => finish(false);
1345
+ ws.onerror = () => {
1346
+ finish(false);
1347
+ };
1348
+ ws.onclose = () => {
1349
+ finish(false);
1350
+ };
1307
1351
  });
1308
1352
  }
1309
1353
  async function launchChrome(opts = {}) {
1310
1354
  const cdpPort = opts.cdpPort ?? DEFAULT_CDP_PORT;
1311
1355
  await ensurePortAvailable(cdpPort);
1312
1356
  const exe = resolveBrowserExecutable({ executablePath: opts.executablePath });
1313
- if (!exe) throw new Error("No supported browser found (Chrome/Brave/Edge/Chromium). Install one or provide executablePath.");
1357
+ if (!exe)
1358
+ throw new Error("No supported browser found (Chrome/Brave/Edge/Chromium). Install one or provide executablePath.");
1314
1359
  const profileName = opts.profileName ?? DEFAULT_PROFILE_NAME;
1315
1360
  const userDataDir = opts.userDataDir ?? resolveUserDataDir(profileName);
1316
1361
  fs.mkdirSync(userDataDir, { recursive: true });
1317
1362
  const spawnChrome = () => {
1318
1363
  const args = [
1319
- `--remote-debugging-port=${cdpPort}`,
1364
+ `--remote-debugging-port=${String(cdpPort)}`,
1320
1365
  `--user-data-dir=${userDataDir}`,
1321
1366
  "--no-first-run",
1322
1367
  "--no-default-browser-check",
@@ -1329,10 +1374,10 @@ async function launchChrome(opts = {}) {
1329
1374
  "--hide-crash-restore-bubble",
1330
1375
  "--password-store=basic"
1331
1376
  ];
1332
- if (opts.headless) {
1377
+ if (opts.headless === true) {
1333
1378
  args.push("--headless=new", "--disable-gpu");
1334
1379
  }
1335
- if (opts.noSandbox) {
1380
+ if (opts.noSandbox === true) {
1336
1381
  args.push("--no-sandbox", "--disable-setuid-sandbox");
1337
1382
  }
1338
1383
  if (process.platform === "linux") args.push("--disable-dev-shm-usage");
@@ -1379,12 +1424,12 @@ async function launchChrome(opts = {}) {
1379
1424
  } catch {
1380
1425
  }
1381
1426
  const proc = spawnChrome();
1382
- const cdpUrl = `http://127.0.0.1:${cdpPort}`;
1427
+ const cdpUrl = `http://127.0.0.1:${String(cdpPort)}`;
1383
1428
  const stderrChunks = [];
1384
1429
  const onStderr = (chunk) => {
1385
1430
  stderrChunks.push(chunk);
1386
1431
  };
1387
- proc.stderr?.on("data", onStderr);
1432
+ proc.stderr.on("data", onStderr);
1388
1433
  const readyDeadline = Date.now() + 15e3;
1389
1434
  while (Date.now() < readyDeadline) {
1390
1435
  if (await isChromeReachable(cdpUrl, 500)) break;
@@ -1395,14 +1440,14 @@ async function launchChrome(opts = {}) {
1395
1440
  const stderrHint = stderrOutput ? `
1396
1441
  Chrome stderr:
1397
1442
  ${stderrOutput.slice(0, 2e3)}` : "";
1398
- const sandboxHint = process.platform === "linux" && !opts.noSandbox ? "\nHint: If running in a container or as root, try setting noSandbox: true." : "";
1443
+ const sandboxHint = process.platform === "linux" && opts.noSandbox !== true ? "\nHint: If running in a container or as root, try setting noSandbox: true." : "";
1399
1444
  try {
1400
1445
  proc.kill("SIGKILL");
1401
1446
  } catch {
1402
1447
  }
1403
- throw new Error(`Failed to start Chrome CDP on port ${cdpPort}.${sandboxHint}${stderrHint}`);
1448
+ throw new Error(`Failed to start Chrome CDP on port ${String(cdpPort)}.${sandboxHint}${stderrHint}`);
1404
1449
  }
1405
- proc.stderr?.off("data", onStderr);
1450
+ proc.stderr.off("data", onStderr);
1406
1451
  stderrChunks.length = 0;
1407
1452
  return {
1408
1453
  pid: proc.pid ?? -1,
@@ -1415,14 +1460,14 @@ ${stderrOutput.slice(0, 2e3)}` : "";
1415
1460
  }
1416
1461
  async function stopChrome(running, timeoutMs = 2500) {
1417
1462
  const proc = running.proc;
1418
- if (proc.exitCode != null) return;
1463
+ if (proc.exitCode !== null) return;
1419
1464
  try {
1420
1465
  proc.kill("SIGTERM");
1421
1466
  } catch {
1422
1467
  }
1423
1468
  const start = Date.now();
1424
1469
  while (Date.now() - start < timeoutMs) {
1425
- if (proc.exitCode != null) return;
1470
+ if (proc.exitCode !== null) return;
1426
1471
  await new Promise((r) => setTimeout(r, 100));
1427
1472
  }
1428
1473
  try {
@@ -1430,9 +1475,55 @@ async function stopChrome(running, timeoutMs = 2500) {
1430
1475
  } catch {
1431
1476
  }
1432
1477
  }
1478
+
1479
+ // src/connection.ts
1480
+ var BrowserTabNotFoundError = class extends Error {
1481
+ constructor(message = "Tab not found") {
1482
+ super(message);
1483
+ this.name = "BrowserTabNotFoundError";
1484
+ }
1485
+ };
1486
+ async function fetchJsonForCdp(url, timeoutMs) {
1487
+ const ctrl = new AbortController();
1488
+ const t = setTimeout(() => {
1489
+ ctrl.abort();
1490
+ }, timeoutMs);
1491
+ try {
1492
+ const res = await fetch(url, { signal: ctrl.signal });
1493
+ if (!res.ok) return null;
1494
+ return await res.json();
1495
+ } catch {
1496
+ return null;
1497
+ } finally {
1498
+ clearTimeout(t);
1499
+ }
1500
+ }
1501
+ function appendCdpPath2(cdpUrl, cdpPath) {
1502
+ try {
1503
+ const url = new URL(cdpUrl);
1504
+ url.pathname = `${url.pathname.replace(/\/$/, "")}${cdpPath.startsWith("/") ? cdpPath : `/${cdpPath}`}`;
1505
+ return url.toString();
1506
+ } catch {
1507
+ return `${cdpUrl.replace(/\/$/, "")}${cdpPath}`;
1508
+ }
1509
+ }
1510
+ async function withPlaywrightPageCdpSession(page, fn) {
1511
+ const session = await page.context().newCDPSession(page);
1512
+ try {
1513
+ return await fn(session);
1514
+ } finally {
1515
+ await session.detach().catch(() => {
1516
+ });
1517
+ }
1518
+ }
1519
+ async function withPageScopedCdpClient(opts) {
1520
+ return await withPlaywrightPageCdpSession(opts.page, async (session) => {
1521
+ return await opts.fn((method, params) => session.send(method, params));
1522
+ });
1523
+ }
1433
1524
  var LOOPBACK_ENTRIES = "localhost,127.0.0.1,[::1]";
1434
1525
  function noProxyAlreadyCoversLocalhost() {
1435
- const current = process.env.NO_PROXY || process.env.no_proxy || "";
1526
+ const current = process.env.NO_PROXY ?? process.env.no_proxy ?? "";
1436
1527
  return current.includes("localhost") && current.includes("127.0.0.1") && current.includes("[::1]");
1437
1528
  }
1438
1529
  function isLoopbackCdpUrl(url) {
@@ -1450,7 +1541,7 @@ var NoProxyLeaseManager = class {
1450
1541
  if (this.leaseCount === 0 && !noProxyAlreadyCoversLocalhost()) {
1451
1542
  const noProxy = process.env.NO_PROXY;
1452
1543
  const noProxyLower = process.env.no_proxy;
1453
- const current = noProxy || noProxyLower || "";
1544
+ const current = noProxy ?? noProxyLower ?? "";
1454
1545
  const applied = current ? `${current},${LOOPBACK_ENTRIES}` : LOOPBACK_ENTRIES;
1455
1546
  process.env.NO_PROXY = applied;
1456
1547
  process.env.no_proxy = applied;
@@ -1495,9 +1586,12 @@ function getHeadersWithAuth(endpoint, baseHeaders = {}) {
1495
1586
  const headers = { ...baseHeaders };
1496
1587
  try {
1497
1588
  const parsed = new URL(endpoint);
1498
- if (parsed.username && parsed.password) {
1499
- const credentials = Buffer.from(`${decodeURIComponent(parsed.username)}:${decodeURIComponent(parsed.password)}`).toString("base64");
1500
- headers["Authorization"] = `Basic ${credentials}`;
1589
+ if (Object.keys(headers).some((k) => k.toLowerCase() === "authorization")) return headers;
1590
+ if (parsed.username || parsed.password) {
1591
+ const credentials = Buffer.from(
1592
+ `${decodeURIComponent(parsed.username)}:${decodeURIComponent(parsed.password)}`
1593
+ ).toString("base64");
1594
+ headers.Authorization = `Basic ${credentials}`;
1501
1595
  }
1502
1596
  } catch {
1503
1597
  }
@@ -1545,7 +1639,7 @@ function roleRefsKey(cdpUrl, targetId) {
1545
1639
  function findNetworkRequestById(state, id) {
1546
1640
  for (let i = state.requests.length - 1; i >= 0; i--) {
1547
1641
  const candidate = state.requests[i];
1548
- if (candidate && candidate.id === id) return candidate;
1642
+ if (candidate.id === id) return candidate;
1549
1643
  }
1550
1644
  return void 0;
1551
1645
  }
@@ -1576,16 +1670,16 @@ function ensurePageState(page) {
1576
1670
  });
1577
1671
  page.on("pageerror", (err) => {
1578
1672
  state.errors.push({
1579
- message: err?.message ? String(err.message) : String(err),
1580
- name: err?.name ? String(err.name) : void 0,
1581
- stack: err?.stack ? String(err.stack) : void 0,
1673
+ message: err.message !== "" ? err.message : String(err),
1674
+ name: err.name !== "" ? err.name : void 0,
1675
+ stack: err.stack !== void 0 && err.stack !== "" ? err.stack : void 0,
1582
1676
  timestamp: (/* @__PURE__ */ new Date()).toISOString()
1583
1677
  });
1584
1678
  if (state.errors.length > MAX_PAGE_ERRORS) state.errors.shift();
1585
1679
  });
1586
1680
  page.on("request", (req) => {
1587
1681
  state.nextRequestId += 1;
1588
- const id = `r${state.nextRequestId}`;
1682
+ const id = `r${String(state.nextRequestId)}`;
1589
1683
  state.requestIds.set(req, id);
1590
1684
  state.requests.push({
1591
1685
  id,
@@ -1599,7 +1693,7 @@ function ensurePageState(page) {
1599
1693
  page.on("response", (resp) => {
1600
1694
  const req = resp.request();
1601
1695
  const id = state.requestIds.get(req);
1602
- if (!id) return;
1696
+ if (id === void 0) return;
1603
1697
  const rec = findNetworkRequestById(state, id);
1604
1698
  if (rec) {
1605
1699
  rec.status = resp.status();
@@ -1608,7 +1702,7 @@ function ensurePageState(page) {
1608
1702
  });
1609
1703
  page.on("requestfailed", (req) => {
1610
1704
  const id = state.requestIds.get(req);
1611
- if (!id) return;
1705
+ if (id === void 0) return;
1612
1706
  const rec = findNetworkRequestById(state, id);
1613
1707
  if (rec) {
1614
1708
  rec.failureText = req.failure()?.errorText;
@@ -1625,7 +1719,8 @@ function ensurePageState(page) {
1625
1719
  var STEALTH_SCRIPT = `Object.defineProperty(navigator, 'webdriver', { get: () => undefined })`;
1626
1720
  function applyStealthToPage(page) {
1627
1721
  page.evaluate(STEALTH_SCRIPT).catch((e) => {
1628
- if (process.env.DEBUG) console.warn("[browserclaw] stealth evaluate failed:", e.message);
1722
+ if (process.env.DEBUG !== void 0 && process.env.DEBUG !== "")
1723
+ console.warn("[browserclaw] stealth evaluate failed:", e instanceof Error ? e.message : String(e));
1629
1724
  });
1630
1725
  }
1631
1726
  function observeContext(context) {
@@ -1633,7 +1728,8 @@ function observeContext(context) {
1633
1728
  observedContexts.add(context);
1634
1729
  ensureContextState(context);
1635
1730
  context.addInitScript(STEALTH_SCRIPT).catch((e) => {
1636
- if (process.env.DEBUG) console.warn("[browserclaw] stealth initScript failed:", e.message);
1731
+ if (process.env.DEBUG !== void 0 && process.env.DEBUG !== "")
1732
+ console.warn("[browserclaw] stealth initScript failed:", e instanceof Error ? e.message : String(e));
1637
1733
  });
1638
1734
  for (const page of context.pages()) {
1639
1735
  ensurePageState(page);
@@ -1649,15 +1745,15 @@ function observeBrowser(browser) {
1649
1745
  }
1650
1746
  function rememberRoleRefsForTarget(opts) {
1651
1747
  const targetId = opts.targetId.trim();
1652
- if (!targetId) return;
1748
+ if (targetId === "") return;
1653
1749
  roleRefsByTarget.set(roleRefsKey(opts.cdpUrl, targetId), {
1654
1750
  refs: opts.refs,
1655
- ...opts.frameSelector ? { frameSelector: opts.frameSelector } : {},
1656
- ...opts.mode ? { mode: opts.mode } : {}
1751
+ ...opts.frameSelector !== void 0 && opts.frameSelector !== "" ? { frameSelector: opts.frameSelector } : {},
1752
+ ...opts.mode !== void 0 ? { mode: opts.mode } : {}
1657
1753
  });
1658
1754
  while (roleRefsByTarget.size > MAX_ROLE_REFS_CACHE) {
1659
1755
  const first = roleRefsByTarget.keys().next();
1660
- if (first.done) break;
1756
+ if (first.done === true) break;
1661
1757
  roleRefsByTarget.delete(first.value);
1662
1758
  }
1663
1759
  }
@@ -1666,7 +1762,7 @@ function storeRoleRefsForTarget(opts) {
1666
1762
  state.roleRefs = opts.refs;
1667
1763
  state.roleRefsFrameSelector = opts.frameSelector;
1668
1764
  state.roleRefsMode = opts.mode;
1669
- if (!opts.targetId?.trim()) return;
1765
+ if (opts.targetId === void 0 || opts.targetId.trim() === "") return;
1670
1766
  rememberRoleRefsForTarget({
1671
1767
  cdpUrl: opts.cdpUrl,
1672
1768
  targetId: opts.targetId,
@@ -1676,8 +1772,8 @@ function storeRoleRefsForTarget(opts) {
1676
1772
  });
1677
1773
  }
1678
1774
  function restoreRoleRefsForTarget(opts) {
1679
- const targetId = opts.targetId?.trim() || "";
1680
- if (!targetId) return;
1775
+ const targetId = opts.targetId?.trim() ?? "";
1776
+ if (targetId === "") return;
1681
1777
  const entry = roleRefsByTarget.get(roleRefsKey(opts.cdpUrl, targetId));
1682
1778
  if (!entry) return;
1683
1779
  const state = ensurePageState(opts.page);
@@ -1699,12 +1795,18 @@ async function connectBrowser(cdpUrl, authToken) {
1699
1795
  const timeout = 5e3 + attempt * 2e3;
1700
1796
  const endpoint = await getChromeWebSocketUrl(normalized, timeout, authToken).catch(() => null) ?? normalized;
1701
1797
  const headers = getHeadersWithAuth(endpoint);
1702
- if (authToken && !headers["Authorization"]) headers["Authorization"] = `Bearer ${authToken}`;
1703
- const browser = await withNoProxyForCdpUrl(endpoint, () => chromium.connectOverCDP(endpoint, { timeout, headers }));
1798
+ if (authToken !== void 0 && authToken !== "" && !headers.Authorization)
1799
+ headers.Authorization = `Bearer ${authToken}`;
1800
+ const browser = await withNoProxyForCdpUrl(
1801
+ endpoint,
1802
+ () => chromium.connectOverCDP(endpoint, { timeout, headers })
1803
+ );
1704
1804
  const onDisconnected = () => {
1705
- if (cachedByCdpUrl.get(normalized)?.browser === browser) cachedByCdpUrl.delete(normalized);
1706
- for (const key of roleRefsByTarget.keys()) {
1707
- if (key.startsWith(normalized + "::")) roleRefsByTarget.delete(key);
1805
+ if (cachedByCdpUrl.get(normalized)?.browser === browser) {
1806
+ cachedByCdpUrl.delete(normalized);
1807
+ for (const key of roleRefsByTarget.keys()) {
1808
+ if (key.startsWith(normalized + "::")) roleRefsByTarget.delete(key);
1809
+ }
1708
1810
  }
1709
1811
  };
1710
1812
  const connected = { browser, cdpUrl: normalized, onDisconnected };
@@ -1752,7 +1854,9 @@ function cdpSocketNeedsAttach(wsUrl) {
1752
1854
  async function tryTerminateExecutionViaCdp(cdpUrl, targetId) {
1753
1855
  const httpBase = normalizeCdpHttpBaseForJsonEndpoints(cdpUrl);
1754
1856
  const ctrl = new AbortController();
1755
- const t = setTimeout(() => ctrl.abort(), 2e3);
1857
+ const t = setTimeout(() => {
1858
+ ctrl.abort();
1859
+ }, 2e3);
1756
1860
  let targets;
1757
1861
  try {
1758
1862
  const res = await fetch(`${httpBase}/json/list`, { signal: ctrl.signal });
@@ -1764,9 +1868,12 @@ async function tryTerminateExecutionViaCdp(cdpUrl, targetId) {
1764
1868
  clearTimeout(t);
1765
1869
  }
1766
1870
  if (!Array.isArray(targets)) return;
1767
- const target = targets.find((entry) => String(entry?.id ?? "").trim() === targetId);
1768
- const wsUrlRaw = String(target?.webSocketDebuggerUrl ?? "").trim();
1769
- if (!wsUrlRaw) return;
1871
+ const target = targets.find((entry) => {
1872
+ const e = entry;
1873
+ return (e.id ?? "").trim() === targetId;
1874
+ });
1875
+ const wsUrlRaw = (target?.webSocketDebuggerUrl ?? "").trim();
1876
+ if (wsUrlRaw === "") return;
1770
1877
  const wsUrl = normalizeCdpWsUrl(wsUrlRaw, httpBase);
1771
1878
  const needsAttach = cdpSocketNeedsAttach(wsUrl);
1772
1879
  await new Promise((resolve2) => {
@@ -1802,10 +1909,18 @@ async function tryTerminateExecutionViaCdp(cdpUrl, targetId) {
1802
1909
  if (!needsAttach) return;
1803
1910
  try {
1804
1911
  const msg = JSON.parse(String(event.data));
1805
- if (msg.id && msg.result?.sessionId) {
1806
- ws.send(JSON.stringify({ id: nextId++, sessionId: msg.result.sessionId, method: "Runtime.terminateExecution" }));
1912
+ const result = msg.result;
1913
+ if (msg.id !== void 0 && result?.sessionId !== void 0) {
1914
+ const sessionId = result.sessionId;
1915
+ ws.send(JSON.stringify({ id: nextId++, sessionId, method: "Runtime.terminateExecution" }));
1807
1916
  try {
1808
- ws.send(JSON.stringify({ id: nextId++, method: "Target.detachFromTarget", params: { sessionId: msg.result.sessionId } }));
1917
+ ws.send(
1918
+ JSON.stringify({
1919
+ id: nextId++,
1920
+ method: "Target.detachFromTarget",
1921
+ params: { sessionId }
1922
+ })
1923
+ );
1809
1924
  } catch {
1810
1925
  }
1811
1926
  setTimeout(finish, 300);
@@ -1813,8 +1928,12 @@ async function tryTerminateExecutionViaCdp(cdpUrl, targetId) {
1813
1928
  } catch {
1814
1929
  }
1815
1930
  };
1816
- ws.onerror = () => finish();
1817
- ws.onclose = () => finish();
1931
+ ws.onerror = () => {
1932
+ finish();
1933
+ };
1934
+ ws.onclose = () => {
1935
+ finish();
1936
+ };
1818
1937
  });
1819
1938
  }
1820
1939
  async function forceDisconnectPlaywrightForTarget(opts) {
@@ -1826,15 +1945,15 @@ async function forceDisconnectPlaywrightForTarget(opts) {
1826
1945
  if (cur.onDisconnected && typeof cur.browser.off === "function") {
1827
1946
  cur.browser.off("disconnected", cur.onDisconnected);
1828
1947
  }
1829
- const targetId = opts.targetId?.trim() || "";
1830
- if (targetId) {
1948
+ const targetId = opts.targetId?.trim() ?? "";
1949
+ if (targetId !== "") {
1831
1950
  await tryTerminateExecutionViaCdp(normalized, targetId).catch(() => {
1832
1951
  });
1833
1952
  }
1834
1953
  cur.browser.close().catch(() => {
1835
1954
  });
1836
1955
  }
1837
- async function getAllPages(browser) {
1956
+ function getAllPages(browser) {
1838
1957
  return browser.contexts().flatMap((c) => c.pages());
1839
1958
  }
1840
1959
  async function pageTargetId(page) {
@@ -1842,14 +1961,36 @@ async function pageTargetId(page) {
1842
1961
  try {
1843
1962
  const info = await session.send("Target.getTargetInfo");
1844
1963
  const targetInfo = info.targetInfo;
1845
- return String(targetInfo?.targetId ?? "").trim() || null;
1964
+ return (targetInfo?.targetId ?? "").trim() || null;
1846
1965
  } finally {
1847
1966
  await session.detach().catch(() => {
1848
1967
  });
1849
1968
  }
1850
1969
  }
1970
+ function matchPageByTargetList(pages, targets, targetId) {
1971
+ const target = targets.find((entry) => entry.id === targetId);
1972
+ if (!target) return null;
1973
+ const urlMatch = pages.filter((page) => page.url() === target.url);
1974
+ if (urlMatch.length === 1) return urlMatch[0] ?? null;
1975
+ if (urlMatch.length > 1) {
1976
+ const sameUrlTargets = targets.filter((entry) => entry.url === target.url);
1977
+ if (sameUrlTargets.length === urlMatch.length) {
1978
+ const idx = sameUrlTargets.findIndex((entry) => entry.id === targetId);
1979
+ if (idx >= 0 && idx < urlMatch.length) return urlMatch[idx] ?? null;
1980
+ }
1981
+ }
1982
+ return null;
1983
+ }
1984
+ async function findPageByTargetIdViaTargetList(pages, targetId, cdpUrl) {
1985
+ const targets = await fetchJsonForCdp(
1986
+ appendCdpPath2(normalizeCdpHttpBaseForJsonEndpoints(cdpUrl), "/json/list"),
1987
+ 2e3
1988
+ );
1989
+ if (!Array.isArray(targets)) return null;
1990
+ return matchPageByTargetList(pages, targets, targetId);
1991
+ }
1851
1992
  async function findPageByTargetId(browser, targetId, cdpUrl) {
1852
- const pages = await getAllPages(browser);
1993
+ const pages = getAllPages(browser);
1853
1994
  let resolvedViaCdp = false;
1854
1995
  for (const page of pages) {
1855
1996
  let tid = null;
@@ -1859,66 +2000,84 @@ async function findPageByTargetId(browser, targetId, cdpUrl) {
1859
2000
  } catch {
1860
2001
  tid = null;
1861
2002
  }
1862
- if (tid && tid === targetId) return page;
2003
+ if (tid !== null && tid !== "" && tid === targetId) return page;
1863
2004
  }
1864
- if (cdpUrl) {
2005
+ if (cdpUrl !== void 0 && cdpUrl !== "") {
1865
2006
  try {
1866
- const listUrl = `${normalizeCdpHttpBaseForJsonEndpoints(cdpUrl)}/json/list`;
1867
- const headers = {};
1868
- const ctrl = new AbortController();
1869
- const t = setTimeout(() => ctrl.abort(), 2e3);
1870
- try {
1871
- const response = await fetch(listUrl, { headers, signal: ctrl.signal });
1872
- if (response.ok) {
1873
- const targets = await response.json();
1874
- const target = targets.find((entry) => entry.id === targetId);
1875
- if (target) {
1876
- const urlMatch = pages.filter((p) => p.url() === target.url);
1877
- if (urlMatch.length === 1) return urlMatch[0];
1878
- if (urlMatch.length > 1) {
1879
- const sameUrlTargets = targets.filter((entry) => entry.url === target.url);
1880
- if (sameUrlTargets.length === urlMatch.length) {
1881
- const idx = sameUrlTargets.findIndex((entry) => entry.id === targetId);
1882
- if (idx >= 0 && idx < urlMatch.length) return urlMatch[idx];
1883
- }
1884
- }
1885
- }
1886
- }
1887
- } finally {
1888
- clearTimeout(t);
1889
- }
2007
+ return await findPageByTargetIdViaTargetList(pages, targetId, cdpUrl);
1890
2008
  } catch {
1891
2009
  }
1892
2010
  }
1893
- if (!resolvedViaCdp && pages.length === 1) return pages[0];
2011
+ if (!resolvedViaCdp && pages.length === 1) return pages[0] ?? null;
1894
2012
  return null;
1895
2013
  }
1896
2014
  async function getPageForTargetId(opts) {
1897
2015
  const { browser } = await connectBrowser(opts.cdpUrl);
1898
- const pages = await getAllPages(browser);
2016
+ const pages = getAllPages(browser);
1899
2017
  if (!pages.length) throw new Error("No pages available in the connected browser.");
1900
2018
  const first = pages[0];
1901
- if (!opts.targetId) return first;
2019
+ if (opts.targetId === void 0 || opts.targetId === "") return first;
1902
2020
  const found = await findPageByTargetId(browser, opts.targetId, opts.cdpUrl);
1903
2021
  if (!found) {
1904
2022
  if (pages.length === 1) return first;
1905
- throw new Error(`Tab not found (targetId: ${opts.targetId}). Call browser.tabs() to list open tabs.`);
2023
+ throw new BrowserTabNotFoundError(
2024
+ `Tab not found (targetId: ${opts.targetId}). Call browser.tabs() to list open tabs.`
2025
+ );
1906
2026
  }
1907
2027
  return found;
1908
2028
  }
2029
+ async function resolvePageByTargetIdOrThrow(opts) {
2030
+ const { browser } = await connectBrowser(opts.cdpUrl);
2031
+ const page = await findPageByTargetId(browser, opts.targetId, opts.cdpUrl);
2032
+ if (!page) throw new BrowserTabNotFoundError();
2033
+ return page;
2034
+ }
2035
+ function parseRoleRef(raw) {
2036
+ const trimmed = raw.trim();
2037
+ if (!trimmed) return null;
2038
+ const normalized = trimmed.startsWith("@") ? trimmed.slice(1) : trimmed.startsWith("ref=") ? trimmed.slice(4) : trimmed;
2039
+ return /^e\d+$/.test(normalized) ? normalized : null;
2040
+ }
2041
+ function requireRef(value) {
2042
+ const raw = typeof value === "string" ? value.trim() : "";
2043
+ const ref = (raw ? parseRoleRef(raw) : null) ?? (raw.startsWith("@") ? raw.slice(1) : raw);
2044
+ if (!ref) throw new Error("ref is required");
2045
+ return ref;
2046
+ }
2047
+ function requireRefOrSelector(ref, selector) {
2048
+ const trimmedRef = typeof ref === "string" ? ref.trim() : "";
2049
+ const trimmedSelector = typeof selector === "string" ? selector.trim() : "";
2050
+ if (!trimmedRef && !trimmedSelector) throw new Error("ref or selector is required");
2051
+ return { ref: trimmedRef || void 0, selector: trimmedSelector || void 0 };
2052
+ }
2053
+ function resolveInteractionTimeoutMs(timeoutMs) {
2054
+ return Math.max(500, Math.min(6e4, Math.floor(timeoutMs ?? 8e3)));
2055
+ }
2056
+ function resolveBoundedDelayMs(value, label, maxMs) {
2057
+ const normalized = Math.floor(value ?? 0);
2058
+ if (!Number.isFinite(normalized) || normalized < 0) throw new Error(`${label} must be >= 0`);
2059
+ if (normalized > maxMs) throw new Error(`${label} exceeds maximum of ${String(maxMs)}ms`);
2060
+ return normalized;
2061
+ }
2062
+ async function getRestoredPageForTarget(opts) {
2063
+ const page = await getPageForTargetId(opts);
2064
+ ensurePageState(page);
2065
+ restoreRoleRefsForTarget({ cdpUrl: opts.cdpUrl, targetId: opts.targetId, page });
2066
+ return page;
2067
+ }
1909
2068
  function refLocator(page, ref) {
1910
2069
  const normalized = ref.startsWith("@") ? ref.slice(1) : ref.startsWith("ref=") ? ref.slice(4) : ref;
1911
- if (!normalized.trim()) throw new Error("ref is required");
2070
+ if (normalized.trim() === "") throw new Error("ref is required");
1912
2071
  if (/^e\d+$/.test(normalized)) {
1913
2072
  const state = pageStates.get(page);
1914
2073
  if (state?.roleRefsMode === "aria") {
1915
- return (state.roleRefsFrameSelector ? page.frameLocator(state.roleRefsFrameSelector) : page).locator(`aria-ref=${normalized}`);
2074
+ return (state.roleRefsFrameSelector !== void 0 && state.roleRefsFrameSelector !== "" ? page.frameLocator(state.roleRefsFrameSelector) : page).locator(`aria-ref=${normalized}`);
1916
2075
  }
1917
2076
  const info = state?.roleRefs?.[normalized];
1918
2077
  if (!info) throw new Error(`Unknown ref "${normalized}". Run a new snapshot and use a ref from that snapshot.`);
1919
- const locAny = state?.roleRefsFrameSelector ? page.frameLocator(state.roleRefsFrameSelector) : page;
2078
+ const locAny = state.roleRefsFrameSelector !== void 0 && state.roleRefsFrameSelector !== "" ? page.frameLocator(state.roleRefsFrameSelector) : page;
1920
2079
  const role = info.role;
1921
- const locator = info.name ? locAny.getByRole(role, { name: info.name, exact: true }) : locAny.getByRole(role);
2080
+ const locator = info.name !== void 0 && info.name !== "" ? locAny.getByRole(role, { name: info.name, exact: true }) : locAny.getByRole(role);
1922
2081
  return info.nth !== void 0 ? locator.nth(info.nth) : locator;
1923
2082
  }
1924
2083
  return page.locator(`aria-ref=${normalized}`);
@@ -1926,15 +2085,21 @@ function refLocator(page, ref) {
1926
2085
  function toAIFriendlyError(error, selector) {
1927
2086
  const message = error instanceof Error ? error.message : String(error);
1928
2087
  if (message.includes("strict mode violation")) {
1929
- const countMatch = message.match(/resolved to (\d+) elements/);
2088
+ const countMatch = /resolved to (\d+) elements/.exec(message);
1930
2089
  const count = countMatch ? countMatch[1] : "multiple";
1931
- return new Error(`Selector "${selector}" matched ${count} elements. Run a new snapshot to get updated refs, or use a different ref.`);
2090
+ return new Error(
2091
+ `Selector "${selector}" matched ${count} elements. Run a new snapshot to get updated refs, or use a different ref.`
2092
+ );
1932
2093
  }
1933
2094
  if ((message.includes("Timeout") || message.includes("waiting for")) && (message.includes("to be visible") || message.includes("not visible"))) {
1934
- return new Error(`Element "${selector}" not found or not visible. Run a new snapshot to see current page elements.`);
2095
+ return new Error(
2096
+ `Element "${selector}" not found or not visible. Run a new snapshot to see current page elements.`
2097
+ );
1935
2098
  }
1936
2099
  if (message.includes("intercepts pointer events") || message.includes("not visible") || message.includes("not receive pointer events")) {
1937
- return new Error(`Element "${selector}" is not interactable (hidden or covered). Try scrolling it into view, closing overlays, or re-snapshotting.`);
2100
+ return new Error(
2101
+ `Element "${selector}" is not interactable (hidden or covered). Try scrolling it into view, closing overlays, or re-snapshotting.`
2102
+ );
1938
2103
  }
1939
2104
  return error instanceof Error ? error : new Error(message);
1940
2105
  }
@@ -1942,438 +2107,151 @@ function normalizeTimeoutMs(timeoutMs, fallback, maxMs = 12e4) {
1942
2107
  return Math.max(500, Math.min(maxMs, timeoutMs ?? fallback));
1943
2108
  }
1944
2109
 
1945
- // src/snapshot/ref-map.ts
1946
- var INTERACTIVE_ROLES = /* @__PURE__ */ new Set([
1947
- "button",
1948
- "link",
1949
- "textbox",
1950
- "checkbox",
1951
- "radio",
1952
- "combobox",
1953
- "listbox",
1954
- "menuitem",
1955
- "menuitemcheckbox",
1956
- "menuitemradio",
1957
- "option",
1958
- "searchbox",
1959
- "slider",
1960
- "spinbutton",
1961
- "switch",
1962
- "tab",
1963
- "treeitem"
1964
- ]);
1965
- var CONTENT_ROLES = /* @__PURE__ */ new Set([
1966
- "heading",
1967
- "cell",
1968
- "gridcell",
1969
- "columnheader",
1970
- "rowheader",
1971
- "listitem",
1972
- "article",
1973
- "region",
1974
- "main",
1975
- "navigation"
1976
- ]);
1977
- var STRUCTURAL_ROLES = /* @__PURE__ */ new Set([
1978
- "generic",
1979
- "group",
1980
- "list",
1981
- "table",
1982
- "row",
1983
- "rowgroup",
1984
- "grid",
1985
- "treegrid",
1986
- "menu",
1987
- "menubar",
1988
- "toolbar",
1989
- "tablist",
1990
- "tree",
1991
- "directory",
1992
- "document",
1993
- "application",
1994
- "presentation",
1995
- "none"
1996
- ]);
1997
- function getIndentLevel(line) {
1998
- const match = line.match(/^(\s*)/);
1999
- return match ? Math.floor(match[1].length / 2) : 0;
2000
- }
2001
- function matchInteractiveSnapshotLine(line, options) {
2002
- const depth = getIndentLevel(line);
2003
- if (options.maxDepth !== void 0 && depth > options.maxDepth) {
2004
- return null;
2005
- }
2006
- const match = line.match(/^(\s*-\s*)(\w+)(?:\s+"([^"]*)")?(.*)$/);
2007
- if (!match) {
2008
- return null;
2009
- }
2010
- const [, , roleRaw, name, suffix] = match;
2011
- if (roleRaw.startsWith("/")) {
2012
- return null;
2013
- }
2014
- const role = roleRaw.toLowerCase();
2015
- return {
2016
- roleRaw,
2017
- role,
2018
- ...name ? { name } : {},
2019
- suffix
2020
- };
2021
- }
2022
- function createRoleNameTracker() {
2023
- const counts = /* @__PURE__ */ new Map();
2024
- const refsByKey = /* @__PURE__ */ new Map();
2025
- return {
2026
- counts,
2027
- refsByKey,
2028
- getKey(role, name) {
2029
- return `${role}:${name ?? ""}`;
2030
- },
2031
- getNextIndex(role, name) {
2032
- const key = this.getKey(role, name);
2033
- const current = counts.get(key) ?? 0;
2034
- counts.set(key, current + 1);
2035
- return current;
2036
- },
2037
- trackRef(role, name, ref) {
2038
- const key = this.getKey(role, name);
2039
- const list = refsByKey.get(key) ?? [];
2040
- list.push(ref);
2041
- refsByKey.set(key, list);
2042
- },
2043
- getDuplicateKeys() {
2044
- const out = /* @__PURE__ */ new Set();
2045
- for (const [key, refs] of refsByKey) if (refs.length > 1) out.add(key);
2046
- return out;
2110
+ // src/actions/evaluate.ts
2111
+ async function evaluateInAllFramesViaPlaywright(opts) {
2112
+ const fnText = opts.fn.trim();
2113
+ if (!fnText) throw new Error("function is required");
2114
+ const page = await getPageForTargetId({ cdpUrl: opts.cdpUrl, targetId: opts.targetId });
2115
+ const frames = page.frames();
2116
+ const results = [];
2117
+ for (const frame of frames) {
2118
+ try {
2119
+ const result = await frame.evaluate((fnBody) => {
2120
+ "use strict";
2121
+ try {
2122
+ const candidate = (0, eval)("(" + fnBody + ")");
2123
+ return typeof candidate === "function" ? candidate() : candidate;
2124
+ } catch (err) {
2125
+ throw new Error("Invalid evaluate function: " + (err instanceof Error ? err.message : String(err)));
2126
+ }
2127
+ }, fnText);
2128
+ results.push({
2129
+ frameUrl: frame.url(),
2130
+ frameName: frame.name(),
2131
+ result
2132
+ });
2133
+ } catch {
2047
2134
  }
2048
- };
2135
+ }
2136
+ return results;
2049
2137
  }
2050
- function removeNthFromNonDuplicates(refs, tracker) {
2051
- const duplicates = tracker.getDuplicateKeys();
2052
- for (const [ref, data] of Object.entries(refs)) {
2053
- const key = tracker.getKey(data.role, data.name);
2054
- if (!duplicates.has(key)) delete refs[ref]?.nth;
2138
+ async function awaitEvalWithAbort(evalPromise, abortPromise) {
2139
+ if (!abortPromise) return await evalPromise;
2140
+ try {
2141
+ return await Promise.race([evalPromise, abortPromise]);
2142
+ } catch (err) {
2143
+ evalPromise.catch(() => {
2144
+ });
2145
+ throw err;
2055
2146
  }
2056
2147
  }
2057
- function compactTree(tree) {
2058
- const lines = tree.split("\n");
2059
- const result = [];
2060
- for (let i = 0; i < lines.length; i++) {
2061
- const line = lines[i];
2062
- if (line.includes("[ref=")) {
2063
- result.push(line);
2064
- continue;
2065
- }
2066
- if (line.includes(":") && !line.trimEnd().endsWith(":")) {
2067
- result.push(line);
2068
- continue;
2069
- }
2070
- const currentIndent = getIndentLevel(line);
2071
- let hasRelevantChildren = false;
2072
- for (let j = i + 1; j < lines.length; j++) {
2073
- if (getIndentLevel(lines[j]) <= currentIndent) break;
2074
- if (lines[j]?.includes("[ref=")) {
2075
- hasRelevantChildren = true;
2076
- break;
2077
- }
2078
- }
2079
- if (hasRelevantChildren) result.push(line);
2080
- }
2081
- return result.join("\n");
2082
- }
2083
- function buildRoleSnapshotFromAriaSnapshot(ariaSnapshot, options = {}) {
2084
- const lines = ariaSnapshot.split("\n");
2085
- const refs = {};
2086
- const tracker = createRoleNameTracker();
2087
- let counter = 0;
2088
- const nextRef = () => {
2089
- counter++;
2090
- return `e${counter}`;
2091
- };
2092
- if (options.interactive) {
2093
- const result2 = [];
2094
- for (const line of lines) {
2095
- const parsed = matchInteractiveSnapshotLine(line, options);
2096
- if (!parsed) continue;
2097
- const { roleRaw, role, name, suffix } = parsed;
2098
- if (!INTERACTIVE_ROLES.has(role)) continue;
2099
- const prefix = line.match(/^(\s*-\s*)/)?.[1] ?? "";
2100
- const ref = nextRef();
2101
- const nth = tracker.getNextIndex(role, name);
2102
- tracker.trackRef(role, name, ref);
2103
- refs[ref] = { role, name, nth };
2104
- let enhanced = `${prefix}${roleRaw}`;
2105
- if (name) enhanced += ` "${name}"`;
2106
- enhanced += ` [ref=${ref}]`;
2107
- if (nth > 0) enhanced += ` [nth=${nth}]`;
2108
- if (suffix.includes("[")) enhanced += suffix;
2109
- result2.push(enhanced);
2110
- }
2111
- removeNthFromNonDuplicates(refs, tracker);
2112
- return { snapshot: result2.join("\n") || "(no interactive elements)", refs };
2113
- }
2114
- const result = [];
2115
- for (const line of lines) {
2116
- const depth = getIndentLevel(line);
2117
- if (options.maxDepth !== void 0 && depth > options.maxDepth) continue;
2118
- const match = line.match(/^(\s*-\s*)(\w+)(?:\s+"([^"]*)")?(.*)$/);
2119
- if (!match) {
2120
- result.push(line);
2121
- continue;
2122
- }
2123
- const [, prefix, roleRaw, name, suffix] = match;
2124
- if (roleRaw.startsWith("/")) {
2125
- result.push(line);
2126
- continue;
2127
- }
2128
- const role = roleRaw.toLowerCase();
2129
- const isInteractive = INTERACTIVE_ROLES.has(role);
2130
- const isContent = CONTENT_ROLES.has(role);
2131
- const isStructural = STRUCTURAL_ROLES.has(role);
2132
- if (options.compact && isStructural && !name) continue;
2133
- if (!(isInteractive || isContent && name)) {
2134
- result.push(line);
2135
- continue;
2136
- }
2137
- const ref = nextRef();
2138
- const nth = tracker.getNextIndex(role, name);
2139
- tracker.trackRef(role, name, ref);
2140
- refs[ref] = { role, name, nth };
2141
- let enhanced = `${prefix}${roleRaw}`;
2142
- if (name) enhanced += ` "${name}"`;
2143
- enhanced += ` [ref=${ref}]`;
2144
- if (nth > 0) enhanced += ` [nth=${nth}]`;
2145
- if (suffix) enhanced += suffix;
2146
- result.push(enhanced);
2147
- }
2148
- removeNthFromNonDuplicates(refs, tracker);
2149
- const tree = result.join("\n") || "(empty)";
2150
- return { snapshot: options.compact ? compactTree(tree) : tree, refs };
2151
- }
2152
- function buildRoleSnapshotFromAiSnapshot(aiSnapshot, options = {}) {
2153
- const lines = String(aiSnapshot ?? "").split("\n");
2154
- const refs = {};
2155
- function parseAiSnapshotRef(suffix) {
2156
- const match = suffix.match(/\[ref=(e\d+)\]/i);
2157
- return match ? match[1] : null;
2158
- }
2159
- if (options.interactive) {
2160
- const out2 = [];
2161
- for (const line of lines) {
2162
- const parsed = matchInteractiveSnapshotLine(line, options);
2163
- if (!parsed) continue;
2164
- const { roleRaw, role, name, suffix } = parsed;
2165
- if (!INTERACTIVE_ROLES.has(role)) continue;
2166
- const ref = parseAiSnapshotRef(suffix);
2167
- if (!ref) continue;
2168
- const prefix = line.match(/^(\s*-\s*)/)?.[1] ?? "";
2169
- refs[ref] = { role, ...name ? { name } : {} };
2170
- out2.push(`${prefix}${roleRaw}${name ? ` "${name}"` : ""}${suffix}`);
2148
+ var BROWSER_EVALUATOR = new Function(
2149
+ "args",
2150
+ `
2151
+ "use strict";
2152
+ var fnBody = args.fnBody, timeoutMs = args.timeoutMs;
2153
+ try {
2154
+ var candidate = eval("(" + fnBody + ")");
2155
+ var result = typeof candidate === "function" ? candidate() : candidate;
2156
+ if (result && typeof result.then === "function") {
2157
+ return Promise.race([
2158
+ result,
2159
+ new Promise(function(_, reject) {
2160
+ setTimeout(function() { reject(new Error("evaluate timed out after " + timeoutMs + "ms")); }, timeoutMs);
2161
+ })
2162
+ ]);
2171
2163
  }
2172
- return { snapshot: out2.join("\n") || "(no interactive elements)", refs };
2164
+ return result;
2165
+ } catch (err) {
2166
+ throw new Error("Invalid evaluate function: " + (err && err.message ? err.message : String(err)));
2173
2167
  }
2174
- const out = [];
2175
- for (const line of lines) {
2176
- const depth = getIndentLevel(line);
2177
- if (options.maxDepth !== void 0 && depth > options.maxDepth) continue;
2178
- const match = line.match(/^(\s*-\s*)(\w+)(?:\s+"([^"]*)")?(.*)$/);
2179
- if (!match) {
2180
- out.push(line);
2181
- continue;
2182
- }
2183
- const [, , roleRaw, name, suffix] = match;
2184
- if (roleRaw.startsWith("/")) {
2185
- out.push(line);
2186
- continue;
2168
+ `
2169
+ );
2170
+ var ELEMENT_EVALUATOR = new Function(
2171
+ "el",
2172
+ "args",
2173
+ `
2174
+ "use strict";
2175
+ var fnBody = args.fnBody, timeoutMs = args.timeoutMs;
2176
+ try {
2177
+ var candidate = eval("(" + fnBody + ")");
2178
+ var result = typeof candidate === "function" ? candidate(el) : candidate;
2179
+ if (result && typeof result.then === "function") {
2180
+ return Promise.race([
2181
+ result,
2182
+ new Promise(function(_, reject) {
2183
+ setTimeout(function() { reject(new Error("evaluate timed out after " + timeoutMs + "ms")); }, timeoutMs);
2184
+ })
2185
+ ]);
2187
2186
  }
2188
- const role = roleRaw.toLowerCase();
2189
- const isStructural = STRUCTURAL_ROLES.has(role);
2190
- if (options.compact && isStructural && !name) continue;
2191
- const ref = parseAiSnapshotRef(suffix);
2192
- if (ref) refs[ref] = { role, ...name ? { name } : {} };
2193
- out.push(line);
2194
- }
2195
- const tree = out.join("\n") || "(empty)";
2196
- return { snapshot: options.compact ? compactTree(tree) : tree, refs };
2197
- }
2198
- function getRoleSnapshotStats(snapshot, refs) {
2199
- const interactive = Object.values(refs).filter((r) => INTERACTIVE_ROLES.has(r.role)).length;
2200
- return {
2201
- lines: snapshot.split("\n").length,
2202
- chars: snapshot.length,
2203
- refs: Object.keys(refs).length,
2204
- interactive
2205
- };
2206
- }
2207
-
2208
- // src/snapshot/ai-snapshot.ts
2209
- async function snapshotAi(opts) {
2210
- const page = await getPageForTargetId({ cdpUrl: opts.cdpUrl, targetId: opts.targetId });
2211
- ensurePageState(page);
2212
- const maybe = page;
2213
- if (!maybe._snapshotForAI) {
2214
- throw new Error("Playwright _snapshotForAI is not available. Upgrade playwright-core to >= 1.50.");
2215
- }
2216
- const sourceUrl = page.url();
2217
- const result = await maybe._snapshotForAI({
2218
- timeout: normalizeTimeoutMs(opts.timeoutMs, 5e3, 6e4),
2219
- track: "response"
2220
- });
2221
- let snapshot = String(result?.full ?? "");
2222
- const maxChars = opts.maxChars;
2223
- const limit = typeof maxChars === "number" && Number.isFinite(maxChars) && maxChars > 0 ? Math.floor(maxChars) : void 0;
2224
- if (limit && snapshot.length > limit) {
2225
- const lastNewline = snapshot.lastIndexOf("\n", limit);
2226
- const cutoff = lastNewline > 0 ? lastNewline : limit;
2227
- snapshot = `${snapshot.slice(0, cutoff)}
2228
-
2229
- [...TRUNCATED - page too large]`;
2187
+ return result;
2188
+ } catch (err) {
2189
+ throw new Error("Invalid evaluate function: " + (err && err.message ? err.message : String(err)));
2230
2190
  }
2231
- const built = buildRoleSnapshotFromAiSnapshot(snapshot, opts.options);
2232
- storeRoleRefsForTarget({
2233
- page,
2234
- cdpUrl: opts.cdpUrl,
2235
- targetId: opts.targetId,
2236
- refs: built.refs,
2237
- mode: "aria"
2238
- });
2239
- return {
2240
- snapshot: built.snapshot,
2241
- refs: built.refs,
2242
- stats: getRoleSnapshotStats(built.snapshot, built.refs),
2243
- untrusted: true,
2244
- contentMeta: {
2245
- sourceUrl,
2246
- contentType: "browser-snapshot",
2247
- capturedAt: (/* @__PURE__ */ new Date()).toISOString()
2248
- }
2249
- };
2250
- }
2251
-
2252
- // src/snapshot/aria-snapshot.ts
2253
- async function snapshotRole(opts) {
2191
+ `
2192
+ );
2193
+ async function evaluateViaPlaywright(opts) {
2194
+ const fnText = opts.fn.trim();
2195
+ if (!fnText) throw new Error("function is required");
2254
2196
  const page = await getPageForTargetId({ cdpUrl: opts.cdpUrl, targetId: opts.targetId });
2255
2197
  ensurePageState(page);
2256
- const sourceUrl = page.url();
2257
- if (opts.refsMode === "aria") {
2258
- if (opts.selector?.trim() || opts.frameSelector?.trim()) {
2259
- throw new Error("refs=aria does not support selector/frame snapshots yet.");
2260
- }
2261
- const maybe = page;
2262
- if (!maybe._snapshotForAI) {
2263
- throw new Error("refs=aria requires Playwright _snapshotForAI support.");
2264
- }
2265
- const result = await maybe._snapshotForAI({ timeout: 5e3, track: "response" });
2266
- const built2 = buildRoleSnapshotFromAiSnapshot(String(result?.full ?? ""), opts.options);
2267
- storeRoleRefsForTarget({
2268
- page,
2269
- cdpUrl: opts.cdpUrl,
2270
- targetId: opts.targetId,
2271
- refs: built2.refs,
2272
- mode: "aria"
2198
+ restoreRoleRefsForTarget({ cdpUrl: opts.cdpUrl, targetId: opts.targetId, page });
2199
+ const outerTimeout = normalizeTimeoutMs(opts.timeoutMs, 2e4);
2200
+ let evaluateTimeout = Math.max(1e3, Math.min(12e4, outerTimeout - 500));
2201
+ evaluateTimeout = Math.min(evaluateTimeout, outerTimeout);
2202
+ const signal = opts.signal;
2203
+ let abortListener;
2204
+ let abortReject;
2205
+ let abortPromise;
2206
+ if (signal !== void 0) {
2207
+ abortPromise = new Promise((_, reject) => {
2208
+ abortReject = reject;
2209
+ });
2210
+ abortPromise.catch(() => {
2273
2211
  });
2274
- return {
2275
- snapshot: built2.snapshot,
2276
- refs: built2.refs,
2277
- stats: getRoleSnapshotStats(built2.snapshot, built2.refs),
2278
- untrusted: true,
2279
- contentMeta: {
2280
- sourceUrl,
2281
- contentType: "browser-snapshot",
2282
- capturedAt: (/* @__PURE__ */ new Date()).toISOString()
2283
- }
2284
- };
2285
2212
  }
2286
- const frameSelector = opts.frameSelector?.trim() || "";
2287
- const selector = opts.selector?.trim() || "";
2288
- const locator = frameSelector ? selector ? page.frameLocator(frameSelector).locator(selector) : page.frameLocator(frameSelector).locator(":root") : selector ? page.locator(selector) : page.locator(":root");
2289
- const ariaSnapshot = await locator.ariaSnapshot({ timeout: normalizeTimeoutMs(opts.timeoutMs, 5e3) });
2290
- const built = buildRoleSnapshotFromAriaSnapshot(String(ariaSnapshot ?? ""), opts.options);
2291
- storeRoleRefsForTarget({
2292
- page,
2293
- cdpUrl: opts.cdpUrl,
2294
- targetId: opts.targetId,
2295
- refs: built.refs,
2296
- frameSelector: frameSelector || void 0,
2297
- mode: "role"
2298
- });
2299
- return {
2300
- snapshot: built.snapshot,
2301
- refs: built.refs,
2302
- stats: getRoleSnapshotStats(built.snapshot, built.refs),
2303
- untrusted: true,
2304
- contentMeta: {
2305
- sourceUrl,
2306
- contentType: "browser-snapshot",
2307
- capturedAt: (/* @__PURE__ */ new Date()).toISOString()
2213
+ if (signal !== void 0) {
2214
+ const disconnect = () => {
2215
+ forceDisconnectPlaywrightForTarget({
2216
+ cdpUrl: opts.cdpUrl,
2217
+ targetId: opts.targetId}).catch(() => {
2218
+ });
2219
+ };
2220
+ if (signal.aborted) {
2221
+ disconnect();
2222
+ throw signal.reason ?? new Error("aborted");
2308
2223
  }
2309
- };
2310
- }
2311
- async function snapshotAria(opts) {
2312
- const limit = Math.max(1, Math.min(2e3, Math.floor(opts.limit ?? 500)));
2313
- const page = await getPageForTargetId({ cdpUrl: opts.cdpUrl, targetId: opts.targetId });
2314
- ensurePageState(page);
2315
- const sourceUrl = page.url();
2316
- const session = await page.context().newCDPSession(page);
2317
- try {
2318
- await session.send("Accessibility.enable").catch(() => {
2319
- });
2320
- const res = await session.send("Accessibility.getFullAXTree");
2321
- return {
2322
- nodes: formatAriaNodes(Array.isArray(res?.nodes) ? res.nodes : [], limit),
2323
- untrusted: true,
2324
- contentMeta: {
2325
- sourceUrl,
2326
- contentType: "browser-aria-tree",
2327
- capturedAt: (/* @__PURE__ */ new Date()).toISOString()
2328
- }
2224
+ abortListener = () => {
2225
+ disconnect();
2226
+ abortReject?.(signal.reason ?? new Error("aborted"));
2329
2227
  };
2330
- } finally {
2331
- await session.detach().catch(() => {
2332
- });
2228
+ signal.addEventListener("abort", abortListener, { once: true });
2229
+ if (signal.aborted) {
2230
+ abortListener();
2231
+ throw signal.reason ?? new Error("aborted");
2232
+ }
2333
2233
  }
2334
- }
2335
- function axValue(v) {
2336
- if (!v || typeof v !== "object") return "";
2337
- const value = v.value;
2338
- if (typeof value === "string") return value;
2339
- if (typeof value === "number" || typeof value === "boolean") return String(value);
2340
- return "";
2341
- }
2342
- function formatAriaNodes(nodes, limit) {
2343
- const byId = /* @__PURE__ */ new Map();
2344
- for (const n of nodes) if (n.nodeId) byId.set(n.nodeId, n);
2345
- const referenced = /* @__PURE__ */ new Set();
2346
- for (const n of nodes) for (const c of n.childIds ?? []) referenced.add(c);
2347
- const root = nodes.find((n) => n.nodeId && !referenced.has(n.nodeId)) ?? nodes[0];
2348
- if (!root?.nodeId) return [];
2349
- const out = [];
2350
- const stack = [{ id: root.nodeId, depth: 0 }];
2351
- while (stack.length && out.length < limit) {
2352
- const popped = stack.pop();
2353
- if (!popped) break;
2354
- const { id, depth } = popped;
2355
- const n = byId.get(id);
2356
- if (!n) continue;
2357
- const role = axValue(n.role);
2358
- const name = axValue(n.name);
2359
- const value = axValue(n.value);
2360
- const description = axValue(n.description);
2361
- const ref = `ax${out.length + 1}`;
2362
- out.push({
2363
- ref,
2364
- role: role || "unknown",
2365
- name: name || "",
2366
- ...value ? { value } : {},
2367
- ...description ? { description } : {},
2368
- ...typeof n.backendDOMNodeId === "number" ? { backendDOMNodeId: n.backendDOMNodeId } : {},
2369
- depth
2370
- });
2371
- const children = (n.childIds ?? []).filter((c) => byId.has(c));
2372
- for (let i = children.length - 1; i >= 0; i--) {
2373
- if (children[i]) stack.push({ id: children[i], depth: depth + 1 });
2234
+ try {
2235
+ if (opts.ref !== void 0 && opts.ref !== "") {
2236
+ const locator = refLocator(page, opts.ref);
2237
+ return await awaitEvalWithAbort(
2238
+ locator.evaluate(ELEMENT_EVALUATOR, {
2239
+ fnBody: fnText,
2240
+ timeoutMs: evaluateTimeout
2241
+ }),
2242
+ abortPromise
2243
+ );
2374
2244
  }
2245
+ return await awaitEvalWithAbort(
2246
+ page.evaluate(BROWSER_EVALUATOR, {
2247
+ fnBody: fnText,
2248
+ timeoutMs: evaluateTimeout
2249
+ }),
2250
+ abortPromise
2251
+ );
2252
+ } finally {
2253
+ if (signal && abortListener) signal.removeEventListener("abort", abortListener);
2375
2254
  }
2376
- return out;
2377
2255
  }
2378
2256
 
2379
2257
  // src/security.ts
@@ -2390,11 +2268,7 @@ function withBrowserNavigationPolicy(ssrfPolicy) {
2390
2268
  var NETWORK_NAVIGATION_PROTOCOLS = /* @__PURE__ */ new Set(["http:", "https:"]);
2391
2269
  var SAFE_NON_NETWORK_URLS = /* @__PURE__ */ new Set(["about:blank"]);
2392
2270
  var PROXY_ENV_KEYS = ["HTTP_PROXY", "HTTPS_PROXY", "ALL_PROXY", "http_proxy", "https_proxy", "all_proxy"];
2393
- var BLOCKED_HOSTNAMES = /* @__PURE__ */ new Set([
2394
- "localhost",
2395
- "localhost.localdomain",
2396
- "metadata.google.internal"
2397
- ]);
2271
+ var BLOCKED_HOSTNAMES = /* @__PURE__ */ new Set(["localhost", "localhost.localdomain", "metadata.google.internal"]);
2398
2272
  function isAllowedNonNetworkNavigationUrl(parsed) {
2399
2273
  return SAFE_NON_NETWORK_URLS.has(parsed.href);
2400
2274
  }
@@ -2409,7 +2283,7 @@ function hasProxyEnvConfigured2(env = process.env) {
2409
2283
  return false;
2410
2284
  }
2411
2285
  function normalizeHostname(hostname) {
2412
- let h = String(hostname ?? "").trim().toLowerCase();
2286
+ let h = hostname.trim().toLowerCase();
2413
2287
  if (h.startsWith("[") && h.endsWith("]")) h = h.slice(1, -1);
2414
2288
  if (h.endsWith(".")) h = h.slice(0, -1);
2415
2289
  return h;
@@ -2428,13 +2302,7 @@ var BLOCKED_IPV4_RANGES = /* @__PURE__ */ new Set([
2428
2302
  "private",
2429
2303
  "reserved"
2430
2304
  ]);
2431
- var BLOCKED_IPV6_RANGES = /* @__PURE__ */ new Set([
2432
- "unspecified",
2433
- "loopback",
2434
- "linkLocal",
2435
- "uniqueLocal",
2436
- "multicast"
2437
- ]);
2305
+ var BLOCKED_IPV6_RANGES = /* @__PURE__ */ new Set(["unspecified", "loopback", "linkLocal", "uniqueLocal", "multicast"]);
2438
2306
  var RFC2544_BENCHMARK_PREFIX = [ipaddr.IPv4.parse("198.18.0.0"), 15];
2439
2307
  var EMBEDDED_IPV4_SENTINEL_RULES = [
2440
2308
  // IPv4-compatible (::a.b.c.d)
@@ -2483,12 +2351,12 @@ function parseIpv6WithEmbeddedIpv4(raw) {
2483
2351
  }
2484
2352
  function normalizeIpParseInput(raw) {
2485
2353
  const trimmed = raw?.trim();
2486
- if (!trimmed) return;
2354
+ if (trimmed === void 0 || trimmed === "") return;
2487
2355
  return stripIpv6Brackets(trimmed);
2488
2356
  }
2489
2357
  function parseCanonicalIpAddress(raw) {
2490
2358
  const normalized = normalizeIpParseInput(raw);
2491
- if (!normalized) return;
2359
+ if (normalized === void 0) return;
2492
2360
  if (ipaddr.IPv4.isValid(normalized)) {
2493
2361
  if (!ipaddr.IPv4.isValidFourPartDecimal(normalized)) return;
2494
2362
  return ipaddr.IPv4.parse(normalized);
@@ -2498,20 +2366,20 @@ function parseCanonicalIpAddress(raw) {
2498
2366
  }
2499
2367
  function parseLooseIpAddress(raw) {
2500
2368
  const normalized = normalizeIpParseInput(raw);
2501
- if (!normalized) return;
2369
+ if (normalized === void 0) return;
2502
2370
  if (ipaddr.isValid(normalized)) return ipaddr.parse(normalized);
2503
2371
  return parseIpv6WithEmbeddedIpv4(normalized);
2504
2372
  }
2505
2373
  function isCanonicalDottedDecimalIPv4(raw) {
2506
- const trimmed = raw?.trim();
2507
- if (!trimmed) return false;
2374
+ const trimmed = raw.trim();
2375
+ if (trimmed === "") return false;
2508
2376
  const normalized = stripIpv6Brackets(trimmed);
2509
2377
  if (!normalized) return false;
2510
2378
  return ipaddr.IPv4.isValidFourPartDecimal(normalized);
2511
2379
  }
2512
2380
  function isLegacyIpv4Literal(raw) {
2513
- const trimmed = raw?.trim();
2514
- if (!trimmed) return false;
2381
+ const trimmed = raw.trim();
2382
+ if (trimmed === "") return false;
2515
2383
  const normalized = stripIpv6Brackets(trimmed);
2516
2384
  if (!normalized || normalized.includes(":")) return false;
2517
2385
  if (isCanonicalDottedDecimalIPv4(normalized)) return false;
@@ -2537,12 +2405,7 @@ function isBlockedSpecialUseIpv6Address(address) {
2537
2405
  return (address.parts[0] & 65472) === 65216;
2538
2406
  }
2539
2407
  function decodeIpv4FromHextets(high, low) {
2540
- const octets = [
2541
- high >>> 8 & 255,
2542
- high & 255,
2543
- low >>> 8 & 255,
2544
- low & 255
2545
- ];
2408
+ const octets = [high >>> 8 & 255, high & 255, low >>> 8 & 255, low & 255];
2546
2409
  return ipaddr.IPv4.parse(octets.join("."));
2547
2410
  }
2548
2411
  function extractEmbeddedIpv4FromIpv6(address) {
@@ -2589,9 +2452,7 @@ function normalizeHostnameSet(values) {
2589
2452
  function normalizeHostnameAllowlist(values) {
2590
2453
  if (!values || values.length === 0) return [];
2591
2454
  return Array.from(
2592
- new Set(
2593
- values.map((v) => normalizeHostname(v)).filter((v) => v !== "*" && v !== "*." && v.length > 0)
2594
- )
2455
+ new Set(values.map((v) => normalizeHostname(v)).filter((v) => v !== "*" && v !== "*." && v.length > 0))
2595
2456
  );
2596
2457
  }
2597
2458
  function isHostnameAllowedByPattern(hostname, pattern) {
@@ -2626,19 +2487,25 @@ function createPinnedLookup(params) {
2626
2487
  family: address.includes(":") ? 6 : 4
2627
2488
  }));
2628
2489
  let index = 0;
2629
- return ((host, options, callback) => {
2630
- const cb = typeof options === "function" ? options : callback;
2631
- if (!cb) return;
2632
- const normalized = normalizeHostname(host);
2633
- if (!normalized || normalized !== normalizedHost) {
2634
- if (typeof options === "function" || options === void 0) return fallback(host, cb);
2635
- return fallback(host, options, cb);
2490
+ return ((_host, ...rest) => {
2491
+ const second = rest[0];
2492
+ const third = rest[1];
2493
+ const cb = typeof second === "function" ? second : typeof third === "function" ? third : void 0;
2494
+ if (cb === void 0) return;
2495
+ const normalized = normalizeHostname(_host);
2496
+ if (normalized === "" || normalized !== normalizedHost) {
2497
+ if (typeof second === "function" || second === void 0) {
2498
+ fallback(_host, cb);
2499
+ return;
2500
+ }
2501
+ fallback(_host, second, cb);
2502
+ return;
2636
2503
  }
2637
- const opts = typeof options === "object" && options !== null ? options : {};
2638
- const requestedFamily = typeof options === "number" ? options : typeof opts.family === "number" ? opts.family : 0;
2504
+ const opts = typeof second === "object" ? second : {};
2505
+ const requestedFamily = typeof second === "number" ? second : typeof opts.family === "number" ? opts.family : 0;
2639
2506
  const candidates = requestedFamily === 4 || requestedFamily === 6 ? records.filter((entry) => entry.family === requestedFamily) : records;
2640
2507
  const usable = candidates.length > 0 ? candidates : records;
2641
- if (opts.all) {
2508
+ if (opts.all === true) {
2642
2509
  cb(null, usable);
2643
2510
  return;
2644
2511
  }
@@ -2656,9 +2523,7 @@ async function resolvePinnedHostnameWithPolicy(hostname, params = {}) {
2656
2523
  const isExplicitlyAllowed = allowedHostnames.has(normalized);
2657
2524
  const skipPrivateNetworkChecks = allowPrivateNetwork || isExplicitlyAllowed;
2658
2525
  if (!matchesHostnameAllowlist(normalized, hostnameAllowlist)) {
2659
- throw new InvalidBrowserNavigationUrlError(
2660
- `Navigation blocked: hostname "${hostname}" is not in the allowlist.`
2661
- );
2526
+ throw new InvalidBrowserNavigationUrlError(`Navigation blocked: hostname "${hostname}" is not in the allowlist.`);
2662
2527
  }
2663
2528
  if (!skipPrivateNetworkChecks) {
2664
2529
  if (isBlockedHostnameOrIp(normalized, params.policy)) {
@@ -2676,7 +2541,7 @@ async function resolvePinnedHostnameWithPolicy(hostname, params = {}) {
2676
2541
  `Navigation to internal/loopback address blocked: unable to resolve "${hostname}". ssrfPolicy.dangerouslyAllowPrivateNetwork is false (strict mode).`
2677
2542
  );
2678
2543
  }
2679
- if (!results || results.length === 0) {
2544
+ if (results.length === 0) {
2680
2545
  throw new InvalidBrowserNavigationUrlError(
2681
2546
  `Navigation to internal/loopback address blocked: unable to resolve "${hostname}". ssrfPolicy.dangerouslyAllowPrivateNetwork is false (strict mode).`
2682
2547
  );
@@ -2703,8 +2568,8 @@ async function resolvePinnedHostnameWithPolicy(hostname, params = {}) {
2703
2568
  };
2704
2569
  }
2705
2570
  async function assertBrowserNavigationAllowed(opts) {
2706
- const rawUrl = String(opts.url ?? "").trim();
2707
- if (!rawUrl) throw new InvalidBrowserNavigationUrlError("url is required");
2571
+ const rawUrl = opts.url.trim();
2572
+ if (rawUrl === "") throw new InvalidBrowserNavigationUrlError("url is required");
2708
2573
  let parsed;
2709
2574
  try {
2710
2575
  parsed = new URL(rawUrl);
@@ -2733,7 +2598,7 @@ async function assertSafeOutputPath(path2, allowedRoots) {
2733
2598
  if (normalized.includes("..")) {
2734
2599
  throw new Error(`Unsafe output path: directory traversal detected in "${path2}".`);
2735
2600
  }
2736
- if (allowedRoots?.length) {
2601
+ if (allowedRoots !== void 0 && allowedRoots.length > 0) {
2737
2602
  const resolved = resolve(normalized);
2738
2603
  let parentReal;
2739
2604
  try {
@@ -2782,9 +2647,17 @@ async function assertSafeUploadPaths(paths) {
2782
2647
  }
2783
2648
  }
2784
2649
  }
2650
+ async function resolveStrictExistingUploadPaths(params) {
2651
+ try {
2652
+ await assertSafeUploadPaths(params.requestedPaths);
2653
+ return { ok: true, paths: params.requestedPaths };
2654
+ } catch (err) {
2655
+ return { ok: false, error: err instanceof Error ? err.message : String(err) };
2656
+ }
2657
+ }
2785
2658
  function sanitizeUntrustedFileName(fileName, fallbackName) {
2786
- const trimmed = String(fileName ?? "").trim();
2787
- if (!trimmed) return fallbackName;
2659
+ const trimmed = fileName.trim();
2660
+ if (trimmed === "") return fallbackName;
2788
2661
  let base = posix.basename(trimmed);
2789
2662
  base = win32.basename(base);
2790
2663
  let cleaned = "";
@@ -2818,16 +2691,17 @@ async function writeViaSiblingTempPath(params) {
2818
2691
  await rename(tempPath, targetPath);
2819
2692
  renameSucceeded = true;
2820
2693
  } finally {
2821
- if (!renameSucceeded) await rm(tempPath, { force: true }).catch(() => {
2822
- });
2694
+ if (!renameSucceeded)
2695
+ await rm(tempPath, { force: true }).catch(() => {
2696
+ });
2823
2697
  }
2824
2698
  }
2825
2699
  function isAbsolute(p) {
2826
2700
  return p.startsWith("/") || /^[a-zA-Z]:/.test(p);
2827
2701
  }
2828
2702
  async function assertBrowserNavigationResultAllowed(opts) {
2829
- const rawUrl = String(opts.url ?? "").trim();
2830
- if (!rawUrl) return;
2703
+ const rawUrl = opts.url.trim();
2704
+ if (rawUrl === "") return;
2831
2705
  let parsed;
2832
2706
  try {
2833
2707
  parsed = new URL(rawUrl);
@@ -2855,47 +2729,61 @@ function requiresInspectableBrowserNavigationRedirects(ssrfPolicy) {
2855
2729
 
2856
2730
  // src/actions/interaction.ts
2857
2731
  var MAX_CLICK_DELAY_MS = 5e3;
2858
- function resolveBoundedDelayMs(value, label, maxMs) {
2859
- const normalized = Math.floor(value ?? 0);
2860
- if (!Number.isFinite(normalized) || normalized < 0) throw new Error(`${label} must be >= 0`);
2861
- if (normalized > maxMs) throw new Error(`${label} exceeds maximum of ${maxMs}ms`);
2862
- return normalized;
2863
- }
2864
- function resolveInteractionTimeoutMs(timeoutMs) {
2865
- return Math.max(500, Math.min(6e4, Math.floor(timeoutMs ?? 8e3)));
2866
- }
2867
- function requireRefOrSelector(ref, selector) {
2868
- const trimmedRef = typeof ref === "string" ? ref.trim() : "";
2869
- const trimmedSelector = typeof selector === "string" ? selector.trim() : "";
2870
- if (!trimmedRef && !trimmedSelector) throw new Error("ref or selector is required");
2871
- return { ref: trimmedRef || void 0, selector: trimmedSelector || void 0 };
2872
- }
2732
+ var CHECKABLE_ROLES = /* @__PURE__ */ new Set(["menuitemcheckbox", "menuitemradio", "checkbox", "switch"]);
2873
2733
  function resolveLocator(page, resolved) {
2874
- return resolved.ref ? refLocator(page, resolved.ref) : page.locator(resolved.selector);
2875
- }
2876
- async function getRestoredPageForTarget(opts) {
2877
- const page = await getPageForTargetId({ cdpUrl: opts.cdpUrl, targetId: opts.targetId });
2878
- ensurePageState(page);
2879
- restoreRoleRefsForTarget({ cdpUrl: opts.cdpUrl, targetId: opts.targetId, page });
2880
- return page;
2734
+ if (resolved.ref !== void 0 && resolved.ref !== "") return refLocator(page, resolved.ref);
2735
+ const sel = resolved.selector ?? "";
2736
+ return page.locator(sel);
2881
2737
  }
2882
2738
  async function clickViaPlaywright(opts) {
2883
2739
  const resolved = requireRefOrSelector(opts.ref, opts.selector);
2884
2740
  const page = await getRestoredPageForTarget(opts);
2885
- const label = resolved.ref ?? resolved.selector;
2741
+ const label = resolved.ref ?? resolved.selector ?? "";
2886
2742
  const locator = resolveLocator(page, resolved);
2887
2743
  const timeout = resolveInteractionTimeoutMs(opts.timeoutMs);
2744
+ let checkableRole = false;
2745
+ if (resolved.ref !== void 0 && resolved.ref !== "") {
2746
+ const refId = parseRoleRef(resolved.ref);
2747
+ if (refId !== null) {
2748
+ const state = ensurePageState(page);
2749
+ const info = state.roleRefs?.[refId];
2750
+ if (info && CHECKABLE_ROLES.has(info.role)) checkableRole = true;
2751
+ }
2752
+ }
2888
2753
  try {
2889
2754
  const delayMs = resolveBoundedDelayMs(opts.delayMs, "click delayMs", MAX_CLICK_DELAY_MS);
2890
2755
  if (delayMs > 0) {
2891
2756
  await locator.hover({ timeout });
2892
2757
  await new Promise((resolve2) => setTimeout(resolve2, delayMs));
2893
2758
  }
2894
- if (opts.doubleClick) {
2759
+ let ariaCheckedBefore;
2760
+ if (checkableRole && opts.doubleClick !== true) {
2761
+ ariaCheckedBefore = await locator.getAttribute("aria-checked", { timeout }).catch(() => void 0);
2762
+ }
2763
+ if (opts.doubleClick === true) {
2895
2764
  await locator.dblclick({ timeout, button: opts.button, modifiers: opts.modifiers });
2896
2765
  } else {
2897
2766
  await locator.click({ timeout, button: opts.button, modifiers: opts.modifiers });
2898
2767
  }
2768
+ if (checkableRole && opts.doubleClick !== true && ariaCheckedBefore !== void 0) {
2769
+ const POLL_INTERVAL_MS = 50;
2770
+ const POLL_TIMEOUT_MS = 500;
2771
+ let changed = false;
2772
+ for (let elapsed = 0; elapsed < POLL_TIMEOUT_MS; elapsed += POLL_INTERVAL_MS) {
2773
+ const current = await locator.getAttribute("aria-checked", { timeout }).catch(() => void 0);
2774
+ if (current === void 0 || current !== ariaCheckedBefore) {
2775
+ changed = true;
2776
+ break;
2777
+ }
2778
+ await new Promise((resolve2) => setTimeout(resolve2, POLL_INTERVAL_MS));
2779
+ }
2780
+ if (!changed) {
2781
+ await locator.evaluate((el) => {
2782
+ el.click();
2783
+ }).catch(() => {
2784
+ });
2785
+ }
2786
+ }
2899
2787
  } catch (err) {
2900
2788
  throw toAIFriendlyError(err, label);
2901
2789
  }
@@ -2903,7 +2791,7 @@ async function clickViaPlaywright(opts) {
2903
2791
  async function hoverViaPlaywright(opts) {
2904
2792
  const resolved = requireRefOrSelector(opts.ref, opts.selector);
2905
2793
  const page = await getRestoredPageForTarget(opts);
2906
- const label = resolved.ref ?? resolved.selector;
2794
+ const label = resolved.ref ?? resolved.selector ?? "";
2907
2795
  const locator = resolveLocator(page, resolved);
2908
2796
  try {
2909
2797
  await locator.hover({ timeout: resolveInteractionTimeoutMs(opts.timeoutMs) });
@@ -2913,28 +2801,28 @@ async function hoverViaPlaywright(opts) {
2913
2801
  }
2914
2802
  async function typeViaPlaywright(opts) {
2915
2803
  const resolved = requireRefOrSelector(opts.ref, opts.selector);
2916
- const text = String(opts.text ?? "");
2804
+ const text = opts.text;
2917
2805
  const page = await getRestoredPageForTarget(opts);
2918
- const label = resolved.ref ?? resolved.selector;
2806
+ const label = resolved.ref ?? resolved.selector ?? "";
2919
2807
  const locator = resolveLocator(page, resolved);
2920
2808
  const timeout = resolveInteractionTimeoutMs(opts.timeoutMs);
2921
2809
  try {
2922
- if (opts.slowly) {
2810
+ if (opts.slowly === true) {
2923
2811
  await locator.click({ timeout });
2924
2812
  await locator.pressSequentially(text, { timeout, delay: 75 });
2925
2813
  } else {
2926
2814
  await locator.fill(text, { timeout });
2927
2815
  }
2928
- if (opts.submit) await locator.press("Enter", { timeout });
2816
+ if (opts.submit === true) await locator.press("Enter", { timeout });
2929
2817
  } catch (err) {
2930
2818
  throw toAIFriendlyError(err, label);
2931
2819
  }
2932
2820
  }
2933
2821
  async function selectOptionViaPlaywright(opts) {
2934
2822
  const resolved = requireRefOrSelector(opts.ref, opts.selector);
2935
- if (!opts.values?.length) throw new Error("values are required");
2823
+ if (opts.values.length === 0) throw new Error("values are required");
2936
2824
  const page = await getRestoredPageForTarget(opts);
2937
- const label = resolved.ref ?? resolved.selector;
2825
+ const label = resolved.ref ?? resolved.selector ?? "";
2938
2826
  const locator = resolveLocator(page, resolved);
2939
2827
  try {
2940
2828
  await locator.selectOption(opts.values, { timeout: resolveInteractionTimeoutMs(opts.timeoutMs) });
@@ -2948,8 +2836,8 @@ async function dragViaPlaywright(opts) {
2948
2836
  const page = await getRestoredPageForTarget(opts);
2949
2837
  const startLocator = resolveLocator(page, resolvedStart);
2950
2838
  const endLocator = resolveLocator(page, resolvedEnd);
2951
- const startLabel = resolvedStart.ref ?? resolvedStart.selector;
2952
- const endLabel = resolvedEnd.ref ?? resolvedEnd.selector;
2839
+ const startLabel = resolvedStart.ref ?? resolvedStart.selector ?? "";
2840
+ const endLabel = resolvedEnd.ref ?? resolvedEnd.selector ?? "";
2953
2841
  try {
2954
2842
  await startLocator.dragTo(endLocator, { timeout: resolveInteractionTimeoutMs(opts.timeoutMs) });
2955
2843
  } catch (err) {
@@ -2985,7 +2873,7 @@ async function fillFormViaPlaywright(opts) {
2985
2873
  async function scrollIntoViewViaPlaywright(opts) {
2986
2874
  const resolved = requireRefOrSelector(opts.ref, opts.selector);
2987
2875
  const page = await getRestoredPageForTarget(opts);
2988
- const label = resolved.ref ?? resolved.selector;
2876
+ const label = resolved.ref ?? resolved.selector ?? "";
2989
2877
  const locator = resolveLocator(page, resolved);
2990
2878
  try {
2991
2879
  await locator.scrollIntoViewIfNeeded({ timeout: normalizeTimeoutMs(opts.timeoutMs, 2e4) });
@@ -2995,10 +2883,11 @@ async function scrollIntoViewViaPlaywright(opts) {
2995
2883
  }
2996
2884
  async function highlightViaPlaywright(opts) {
2997
2885
  const page = await getRestoredPageForTarget(opts);
2886
+ const ref = requireRef(opts.ref);
2998
2887
  try {
2999
- await refLocator(page, opts.ref).highlight();
2888
+ await refLocator(page, ref).highlight();
3000
2889
  } catch (err) {
3001
- throw toAIFriendlyError(err, opts.ref);
2890
+ throw toAIFriendlyError(err, ref);
3002
2891
  }
3003
2892
  }
3004
2893
  async function setInputFilesViaPlaywright(opts) {
@@ -3009,9 +2898,14 @@ async function setInputFilesViaPlaywright(opts) {
3009
2898
  if (inputRef && element) throw new Error("ref and element are mutually exclusive");
3010
2899
  if (!inputRef && !element) throw new Error("Either ref or element is required for setInputFiles");
3011
2900
  const locator = inputRef ? refLocator(page, inputRef) : page.locator(element).first();
3012
- await assertSafeUploadPaths(opts.paths);
2901
+ const uploadPathsResult = await resolveStrictExistingUploadPaths({
2902
+ requestedPaths: opts.paths,
2903
+ scopeLabel: "uploads directory"
2904
+ });
2905
+ if (!uploadPathsResult.ok) throw new Error(uploadPathsResult.error);
2906
+ const resolvedPaths = uploadPathsResult.paths;
3013
2907
  try {
3014
- await locator.setInputFiles(opts.paths);
2908
+ await locator.setInputFiles(resolvedPaths);
3015
2909
  } catch (err) {
3016
2910
  throw toAIFriendlyError(err, inputRef || element);
3017
2911
  }
@@ -3047,26 +2941,28 @@ async function armFileUploadViaPlaywright(opts) {
3047
2941
  const armId = state.armIdUpload;
3048
2942
  page.waitForEvent("filechooser", { timeout }).then(async (fileChooser) => {
3049
2943
  if (state.armIdUpload !== armId) return;
3050
- if (!opts.paths?.length) {
2944
+ if (opts.paths === void 0 || opts.paths.length === 0) {
3051
2945
  try {
3052
2946
  await page.keyboard.press("Escape");
3053
2947
  } catch {
3054
2948
  }
3055
2949
  return;
3056
2950
  }
3057
- try {
3058
- await assertSafeUploadPaths(opts.paths);
3059
- } catch {
2951
+ const uploadPathsResult = await resolveStrictExistingUploadPaths({
2952
+ requestedPaths: opts.paths,
2953
+ scopeLabel: "uploads directory"
2954
+ });
2955
+ if (!uploadPathsResult.ok) {
3060
2956
  try {
3061
2957
  await page.keyboard.press("Escape");
3062
2958
  } catch {
3063
2959
  }
3064
2960
  return;
3065
2961
  }
3066
- await fileChooser.setFiles(opts.paths);
2962
+ await fileChooser.setFiles(uploadPathsResult.paths);
3067
2963
  try {
3068
2964
  const input = typeof fileChooser.element === "function" ? await Promise.resolve(fileChooser.element()) : null;
3069
- if (input) {
2965
+ if (input !== null) {
3070
2966
  await input.evaluate((el) => {
3071
2967
  el.dispatchEvent(new Event("input", { bubbles: true }));
3072
2968
  el.dispatchEvent(new Event("change", { bubbles: true }));
@@ -3080,7 +2976,7 @@ async function armFileUploadViaPlaywright(opts) {
3080
2976
 
3081
2977
  // src/actions/keyboard.ts
3082
2978
  async function pressKeyViaPlaywright(opts) {
3083
- const key = String(opts.key ?? "").trim();
2979
+ const key = opts.key.trim();
3084
2980
  if (!key) throw new Error("key is required");
3085
2981
  const page = await getPageForTargetId({ cdpUrl: opts.cdpUrl, targetId: opts.targetId });
3086
2982
  ensurePageState(page);
@@ -3093,9 +2989,9 @@ function isRetryableNavigateError(err) {
3093
2989
  return msg.includes("frame has been detached") || msg.includes("target page, context or browser has been closed");
3094
2990
  }
3095
2991
  async function navigateViaPlaywright(opts) {
3096
- const url = String(opts.url ?? "").trim();
2992
+ const url = opts.url.trim();
3097
2993
  if (!url) throw new Error("url is required");
3098
- const policy = opts.allowInternal ? { ...opts.ssrfPolicy, dangerouslyAllowPrivateNetwork: true } : opts.ssrfPolicy;
2994
+ const policy = opts.allowInternal === true ? { ...opts.ssrfPolicy, dangerouslyAllowPrivateNetwork: true } : opts.ssrfPolicy;
3099
2995
  await assertBrowserNavigationAllowed({ url, ...withBrowserNavigationPolicy(policy) });
3100
2996
  const timeout = Math.max(1e3, Math.min(12e4, opts.timeoutMs ?? 2e4));
3101
2997
  let page = await getPageForTargetId({ cdpUrl: opts.cdpUrl, targetId: opts.targetId });
@@ -3114,45 +3010,49 @@ async function navigateViaPlaywright(opts) {
3114
3010
  ensurePageState(page);
3115
3011
  response = await navigate();
3116
3012
  }
3117
- await assertBrowserNavigationRedirectChainAllowed({ request: response?.request(), ...withBrowserNavigationPolicy(policy) });
3013
+ await assertBrowserNavigationRedirectChainAllowed({
3014
+ request: response?.request(),
3015
+ ...withBrowserNavigationPolicy(policy)
3016
+ });
3118
3017
  const finalUrl = page.url();
3119
3018
  await assertBrowserNavigationResultAllowed({ url: finalUrl, ...withBrowserNavigationPolicy(policy) });
3120
3019
  return { url: finalUrl };
3121
3020
  }
3122
3021
  async function listPagesViaPlaywright(opts) {
3123
3022
  const { browser } = await connectBrowser(opts.cdpUrl);
3124
- const pages = await getAllPages(browser);
3023
+ const pages = getAllPages(browser);
3125
3024
  const results = [];
3126
3025
  for (const page of pages) {
3127
3026
  const tid = await pageTargetId(page).catch(() => null);
3128
- if (tid) results.push({
3129
- targetId: tid,
3130
- title: await page.title().catch(() => ""),
3131
- url: page.url(),
3132
- type: "page"
3133
- });
3027
+ if (tid !== null && tid !== "")
3028
+ results.push({
3029
+ targetId: tid,
3030
+ title: await page.title().catch(() => ""),
3031
+ url: page.url(),
3032
+ type: "page"
3033
+ });
3134
3034
  }
3135
3035
  return results;
3136
3036
  }
3137
3037
  async function createPageViaPlaywright(opts) {
3138
- const targetUrl = (opts.url ?? "").trim() || "about:blank";
3139
- const policy = opts.allowInternal ? { ...opts.ssrfPolicy, dangerouslyAllowPrivateNetwork: true } : opts.ssrfPolicy;
3140
- if (targetUrl !== "about:blank") {
3141
- await assertBrowserNavigationAllowed({ url: targetUrl, ssrfPolicy: policy });
3142
- }
3143
3038
  const { browser } = await connectBrowser(opts.cdpUrl);
3144
3039
  const context = browser.contexts()[0] ?? await browser.newContext();
3145
3040
  ensureContextState(context);
3146
3041
  const page = await context.newPage();
3147
3042
  ensurePageState(page);
3043
+ const targetUrl = (opts.url ?? "").trim() || "about:blank";
3044
+ const policy = opts.allowInternal === true ? { ...opts.ssrfPolicy, dangerouslyAllowPrivateNetwork: true } : opts.ssrfPolicy;
3148
3045
  if (targetUrl !== "about:blank") {
3149
3046
  const navigationPolicy = withBrowserNavigationPolicy(policy);
3150
- const response = await page.goto(targetUrl, { timeout: 3e4 }).catch(() => null);
3151
- await assertBrowserNavigationRedirectChainAllowed({ request: response?.request(), ...navigationPolicy });
3152
- await assertBrowserNavigationResultAllowed({ url: page.url(), ssrfPolicy: policy });
3047
+ await assertBrowserNavigationAllowed({ url: targetUrl, ...navigationPolicy });
3048
+ await assertBrowserNavigationRedirectChainAllowed({
3049
+ request: (await page.goto(targetUrl, { timeout: 3e4 }).catch(() => null))?.request(),
3050
+ ...navigationPolicy
3051
+ });
3052
+ await assertBrowserNavigationResultAllowed({ url: page.url(), ...navigationPolicy });
3153
3053
  }
3154
3054
  const tid = await pageTargetId(page).catch(() => null);
3155
- if (!tid) throw new Error("Failed to get targetId for new page");
3055
+ if (tid === null || tid === "") throw new Error("Failed to get targetId for new page");
3156
3056
  return {
3157
3057
  targetId: tid,
3158
3058
  title: await page.title().catch(() => ""),
@@ -3160,213 +3060,240 @@ async function createPageViaPlaywright(opts) {
3160
3060
  type: "page"
3161
3061
  };
3162
3062
  }
3163
- async function closePageByTargetIdViaPlaywright(opts) {
3164
- const { browser } = await connectBrowser(opts.cdpUrl);
3165
- const page = await findPageByTargetId(browser, opts.targetId, opts.cdpUrl);
3166
- if (!page) throw new Error(`Tab not found (targetId: ${opts.targetId}). Use browser.tabs() to list open tabs.`);
3167
- await page.close();
3168
- }
3169
- async function focusPageByTargetIdViaPlaywright(opts) {
3170
- const { browser } = await connectBrowser(opts.cdpUrl);
3171
- const page = await findPageByTargetId(browser, opts.targetId, opts.cdpUrl);
3172
- if (!page) throw new Error(`Tab not found (targetId: ${opts.targetId}). Use browser.tabs() to list open tabs.`);
3173
- try {
3174
- await page.bringToFront();
3175
- } catch (err) {
3176
- const session = await page.context().newCDPSession(page);
3177
- try {
3178
- await session.send("Page.bringToFront");
3179
- } catch {
3180
- throw err;
3181
- } finally {
3182
- await session.detach().catch(() => {
3183
- });
3184
- }
3185
- }
3186
- }
3187
- async function resizeViewportViaPlaywright(opts) {
3063
+ async function closePageViaPlaywright(opts) {
3188
3064
  const page = await getPageForTargetId({ cdpUrl: opts.cdpUrl, targetId: opts.targetId });
3189
3065
  ensurePageState(page);
3190
- await page.setViewportSize({
3191
- width: Math.max(1, Math.floor(opts.width)),
3192
- height: Math.max(1, Math.floor(opts.height))
3193
- });
3194
- }
3195
-
3196
- // src/actions/wait.ts
3197
- var MAX_WAIT_TIME_MS = 3e4;
3198
- function resolveBoundedDelayMs2(value, label, maxMs) {
3199
- const normalized = Math.floor(value ?? 0);
3200
- if (!Number.isFinite(normalized) || normalized < 0) throw new Error(`${label} must be >= 0`);
3201
- if (normalized > maxMs) throw new Error(`${label} exceeds maximum of ${maxMs}ms`);
3202
- return normalized;
3203
- }
3204
- async function waitForViaPlaywright(opts) {
3205
- const page = await getPageForTargetId({ cdpUrl: opts.cdpUrl, targetId: opts.targetId });
3206
- ensurePageState(page);
3207
- const timeout = normalizeTimeoutMs(opts.timeoutMs, 2e4);
3208
- if (typeof opts.timeMs === "number" && Number.isFinite(opts.timeMs)) {
3209
- await page.waitForTimeout(resolveBoundedDelayMs2(opts.timeMs, "wait timeMs", MAX_WAIT_TIME_MS));
3210
- }
3211
- if (opts.text) {
3212
- await page.getByText(opts.text).first().waitFor({ state: "visible", timeout });
3213
- }
3214
- if (opts.textGone) {
3215
- await page.getByText(opts.textGone).first().waitFor({ state: "hidden", timeout });
3216
- }
3217
- if (opts.selector) {
3218
- const selector = String(opts.selector).trim();
3219
- if (selector) await page.locator(selector).first().waitFor({ state: "visible", timeout });
3220
- }
3221
- if (opts.url) {
3222
- const url = String(opts.url).trim();
3223
- if (url) await page.waitForURL(url, { timeout });
3224
- }
3225
- if (opts.loadState) {
3226
- await page.waitForLoadState(opts.loadState, { timeout });
3227
- }
3228
- if (opts.fn) {
3229
- const fn = String(opts.fn).trim();
3230
- if (fn) await page.waitForFunction(fn, void 0, { timeout });
3231
- }
3232
- }
3233
-
3234
- // src/actions/evaluate.ts
3235
- async function evaluateInAllFramesViaPlaywright(opts) {
3236
- const fnText = String(opts.fn ?? "").trim();
3237
- if (!fnText) throw new Error("function is required");
3238
- const page = await getPageForTargetId({ cdpUrl: opts.cdpUrl, targetId: opts.targetId });
3239
- const frames = page.frames();
3240
- const results = [];
3241
- for (const frame of frames) {
3242
- try {
3243
- const result = await frame.evaluate(
3244
- // eslint-disable-next-line no-eval
3245
- (fnBody) => {
3246
- "use strict";
3247
- try {
3248
- const candidate = (0, eval)("(" + fnBody + ")");
3249
- return typeof candidate === "function" ? candidate() : candidate;
3250
- } catch (err) {
3251
- throw new Error("Invalid evaluate function: " + (err instanceof Error ? err.message : String(err)));
3252
- }
3253
- },
3254
- fnText
3255
- );
3256
- results.push({
3257
- frameUrl: frame.url(),
3258
- frameName: frame.name(),
3259
- result
3260
- });
3261
- } catch {
3262
- }
3263
- }
3264
- return results;
3066
+ await page.close();
3265
3067
  }
3266
- async function awaitEvalWithAbort(evalPromise, abortPromise) {
3267
- if (!abortPromise) return await evalPromise;
3268
- try {
3269
- return await Promise.race([evalPromise, abortPromise]);
3270
- } catch (err) {
3271
- evalPromise.catch(() => {
3272
- });
3273
- throw err;
3274
- }
3068
+ async function closePageByTargetIdViaPlaywright(opts) {
3069
+ await (await resolvePageByTargetIdOrThrow(opts)).close();
3275
3070
  }
3276
- var BROWSER_EVALUATOR = new Function("args", `
3277
- "use strict";
3278
- var fnBody = args.fnBody, timeoutMs = args.timeoutMs;
3279
- try {
3280
- var candidate = eval("(" + fnBody + ")");
3281
- var result = typeof candidate === "function" ? candidate() : candidate;
3282
- if (result && typeof result.then === "function") {
3283
- return Promise.race([
3284
- result,
3285
- new Promise(function(_, reject) {
3286
- setTimeout(function() { reject(new Error("evaluate timed out after " + timeoutMs + "ms")); }, timeoutMs);
3287
- })
3288
- ]);
3289
- }
3290
- return result;
3291
- } catch (err) {
3292
- throw new Error("Invalid evaluate function: " + (err && err.message ? err.message : String(err)));
3293
- }
3294
- `);
3295
- var ELEMENT_EVALUATOR = new Function("el", "args", `
3296
- "use strict";
3297
- var fnBody = args.fnBody, timeoutMs = args.timeoutMs;
3071
+ async function focusPageByTargetIdViaPlaywright(opts) {
3072
+ const page = await resolvePageByTargetIdOrThrow(opts);
3298
3073
  try {
3299
- var candidate = eval("(" + fnBody + ")");
3300
- var result = typeof candidate === "function" ? candidate(el) : candidate;
3301
- if (result && typeof result.then === "function") {
3302
- return Promise.race([
3303
- result,
3304
- new Promise(function(_, reject) {
3305
- setTimeout(function() { reject(new Error("evaluate timed out after " + timeoutMs + "ms")); }, timeoutMs);
3306
- })
3307
- ]);
3308
- }
3309
- return result;
3074
+ await page.bringToFront();
3310
3075
  } catch (err) {
3311
- throw new Error("Invalid evaluate function: " + (err && err.message ? err.message : String(err)));
3076
+ try {
3077
+ await withPageScopedCdpClient({
3078
+ cdpUrl: opts.cdpUrl,
3079
+ page,
3080
+ targetId: opts.targetId,
3081
+ fn: async (send) => {
3082
+ await send("Page.bringToFront");
3083
+ }
3084
+ });
3085
+ return;
3086
+ } catch {
3087
+ throw err;
3088
+ }
3312
3089
  }
3313
- `);
3314
- async function evaluateViaPlaywright(opts) {
3315
- const fnText = String(opts.fn ?? "").trim();
3316
- if (!fnText) throw new Error("function is required");
3090
+ }
3091
+ async function resizeViewportViaPlaywright(opts) {
3317
3092
  const page = await getPageForTargetId({ cdpUrl: opts.cdpUrl, targetId: opts.targetId });
3318
3093
  ensurePageState(page);
3319
- restoreRoleRefsForTarget({ cdpUrl: opts.cdpUrl, targetId: opts.targetId, page });
3320
- const outerTimeout = normalizeTimeoutMs(opts.timeoutMs, 2e4);
3321
- let evaluateTimeout = Math.max(1e3, Math.min(12e4, outerTimeout - 500));
3322
- evaluateTimeout = Math.min(evaluateTimeout, outerTimeout);
3323
- const signal = opts.signal;
3324
- let abortListener;
3325
- let abortReject;
3326
- let abortPromise;
3327
- if (signal) {
3328
- abortPromise = new Promise((_, reject) => {
3329
- abortReject = reject;
3330
- });
3331
- abortPromise.catch(() => {
3332
- });
3094
+ await page.setViewportSize({
3095
+ width: Math.max(1, Math.floor(opts.width)),
3096
+ height: Math.max(1, Math.floor(opts.height))
3097
+ });
3098
+ }
3099
+
3100
+ // src/actions/wait.ts
3101
+ var MAX_WAIT_TIME_MS = 3e4;
3102
+ function resolveBoundedDelayMs2(value, label, maxMs) {
3103
+ const normalized = Math.floor(value ?? 0);
3104
+ if (!Number.isFinite(normalized) || normalized < 0) throw new Error(`${label} must be >= 0`);
3105
+ if (normalized > maxMs) throw new Error(`${label} exceeds maximum of ${String(maxMs)}ms`);
3106
+ return normalized;
3107
+ }
3108
+ async function waitForViaPlaywright(opts) {
3109
+ const page = await getPageForTargetId({ cdpUrl: opts.cdpUrl, targetId: opts.targetId });
3110
+ ensurePageState(page);
3111
+ const timeout = normalizeTimeoutMs(opts.timeoutMs, 2e4);
3112
+ if (typeof opts.timeMs === "number" && Number.isFinite(opts.timeMs)) {
3113
+ await page.waitForTimeout(resolveBoundedDelayMs2(opts.timeMs, "wait timeMs", MAX_WAIT_TIME_MS));
3333
3114
  }
3334
- if (signal) {
3335
- const disconnect = () => {
3336
- forceDisconnectPlaywrightForTarget({
3337
- cdpUrl: opts.cdpUrl,
3338
- targetId: opts.targetId}).catch(() => {
3115
+ if (opts.text !== void 0 && opts.text !== "") {
3116
+ await page.getByText(opts.text).first().waitFor({ state: "visible", timeout });
3117
+ }
3118
+ if (opts.textGone !== void 0 && opts.textGone !== "") {
3119
+ await page.getByText(opts.textGone).first().waitFor({ state: "hidden", timeout });
3120
+ }
3121
+ if (opts.selector !== void 0 && opts.selector !== "") {
3122
+ const selector = opts.selector.trim();
3123
+ if (selector !== "") await page.locator(selector).first().waitFor({ state: "visible", timeout });
3124
+ }
3125
+ if (opts.url !== void 0 && opts.url !== "") {
3126
+ const url = opts.url.trim();
3127
+ if (url !== "") await page.waitForURL(url, { timeout });
3128
+ }
3129
+ if (opts.loadState !== void 0) {
3130
+ await page.waitForLoadState(opts.loadState, { timeout });
3131
+ }
3132
+ if (opts.fn !== void 0 && opts.fn !== "") {
3133
+ const fn = opts.fn.trim();
3134
+ if (fn !== "") await page.waitForFunction(fn, void 0, { timeout });
3135
+ }
3136
+ }
3137
+
3138
+ // src/actions/batch.ts
3139
+ var MAX_BATCH_DEPTH = 5;
3140
+ var MAX_BATCH_ACTIONS = 100;
3141
+ async function executeSingleAction(action, cdpUrl, targetId, evaluateEnabled, depth = 0) {
3142
+ if (depth > MAX_BATCH_DEPTH) throw new Error(`Batch nesting depth exceeds maximum of ${String(MAX_BATCH_DEPTH)}`);
3143
+ const effectiveTargetId = action.targetId ?? targetId;
3144
+ switch (action.kind) {
3145
+ case "click":
3146
+ await clickViaPlaywright({
3147
+ cdpUrl,
3148
+ targetId: effectiveTargetId,
3149
+ ref: action.ref,
3150
+ selector: action.selector,
3151
+ doubleClick: action.doubleClick,
3152
+ button: action.button,
3153
+ modifiers: action.modifiers,
3154
+ delayMs: action.delayMs,
3155
+ timeoutMs: action.timeoutMs
3339
3156
  });
3340
- };
3341
- if (signal.aborted) {
3342
- disconnect();
3343
- throw signal.reason ?? new Error("aborted");
3344
- }
3345
- abortListener = () => {
3346
- disconnect();
3347
- abortReject?.(signal.reason ?? new Error("aborted"));
3348
- };
3349
- signal.addEventListener("abort", abortListener, { once: true });
3350
- if (signal.aborted) {
3351
- abortListener();
3352
- throw signal.reason ?? new Error("aborted");
3353
- }
3157
+ break;
3158
+ case "type":
3159
+ await typeViaPlaywright({
3160
+ cdpUrl,
3161
+ targetId: effectiveTargetId,
3162
+ ref: action.ref,
3163
+ selector: action.selector,
3164
+ text: action.text,
3165
+ submit: action.submit,
3166
+ slowly: action.slowly,
3167
+ timeoutMs: action.timeoutMs
3168
+ });
3169
+ break;
3170
+ case "press":
3171
+ await pressKeyViaPlaywright({
3172
+ cdpUrl,
3173
+ targetId: effectiveTargetId,
3174
+ key: action.key,
3175
+ delayMs: action.delayMs
3176
+ });
3177
+ break;
3178
+ case "hover":
3179
+ await hoverViaPlaywright({
3180
+ cdpUrl,
3181
+ targetId: effectiveTargetId,
3182
+ ref: action.ref,
3183
+ selector: action.selector,
3184
+ timeoutMs: action.timeoutMs
3185
+ });
3186
+ break;
3187
+ case "scrollIntoView":
3188
+ await scrollIntoViewViaPlaywright({
3189
+ cdpUrl,
3190
+ targetId: effectiveTargetId,
3191
+ ref: action.ref,
3192
+ selector: action.selector,
3193
+ timeoutMs: action.timeoutMs
3194
+ });
3195
+ break;
3196
+ case "drag":
3197
+ await dragViaPlaywright({
3198
+ cdpUrl,
3199
+ targetId: effectiveTargetId,
3200
+ startRef: action.startRef,
3201
+ startSelector: action.startSelector,
3202
+ endRef: action.endRef,
3203
+ endSelector: action.endSelector,
3204
+ timeoutMs: action.timeoutMs
3205
+ });
3206
+ break;
3207
+ case "select":
3208
+ await selectOptionViaPlaywright({
3209
+ cdpUrl,
3210
+ targetId: effectiveTargetId,
3211
+ ref: action.ref,
3212
+ selector: action.selector,
3213
+ values: action.values,
3214
+ timeoutMs: action.timeoutMs
3215
+ });
3216
+ break;
3217
+ case "fill":
3218
+ await fillFormViaPlaywright({
3219
+ cdpUrl,
3220
+ targetId: effectiveTargetId,
3221
+ fields: action.fields,
3222
+ timeoutMs: action.timeoutMs
3223
+ });
3224
+ break;
3225
+ case "resize":
3226
+ await resizeViewportViaPlaywright({
3227
+ cdpUrl,
3228
+ targetId: effectiveTargetId,
3229
+ width: action.width,
3230
+ height: action.height
3231
+ });
3232
+ break;
3233
+ case "wait":
3234
+ if (action.fn !== void 0 && action.fn !== "" && !evaluateEnabled)
3235
+ throw new Error("wait --fn is disabled by config (browser.evaluateEnabled=false)");
3236
+ await waitForViaPlaywright({
3237
+ cdpUrl,
3238
+ targetId: effectiveTargetId,
3239
+ timeMs: action.timeMs,
3240
+ text: action.text,
3241
+ textGone: action.textGone,
3242
+ selector: action.selector,
3243
+ url: action.url,
3244
+ loadState: action.loadState,
3245
+ fn: action.fn,
3246
+ timeoutMs: action.timeoutMs
3247
+ });
3248
+ break;
3249
+ case "evaluate":
3250
+ if (!evaluateEnabled) throw new Error("act:evaluate is disabled by config (browser.evaluateEnabled=false)");
3251
+ await evaluateViaPlaywright({
3252
+ cdpUrl,
3253
+ targetId: effectiveTargetId,
3254
+ fn: action.fn,
3255
+ ref: action.ref,
3256
+ timeoutMs: action.timeoutMs
3257
+ });
3258
+ break;
3259
+ case "close":
3260
+ await closePageViaPlaywright({
3261
+ cdpUrl,
3262
+ targetId: effectiveTargetId
3263
+ });
3264
+ break;
3265
+ case "batch":
3266
+ await batchViaPlaywright({
3267
+ cdpUrl,
3268
+ targetId: effectiveTargetId,
3269
+ actions: action.actions,
3270
+ stopOnError: action.stopOnError,
3271
+ evaluateEnabled,
3272
+ depth: depth + 1
3273
+ });
3274
+ break;
3275
+ default:
3276
+ throw new Error(`Unsupported batch action kind: ${String(action.kind)}`);
3354
3277
  }
3355
- try {
3356
- if (opts.ref) {
3357
- const locator = refLocator(page, opts.ref);
3358
- return await awaitEvalWithAbort(
3359
- locator.evaluate(ELEMENT_EVALUATOR, { fnBody: fnText, timeoutMs: evaluateTimeout }),
3360
- abortPromise
3361
- );
3278
+ }
3279
+ async function batchViaPlaywright(opts) {
3280
+ const depth = opts.depth ?? 0;
3281
+ if (depth > MAX_BATCH_DEPTH) throw new Error(`Batch nesting depth exceeds maximum of ${String(MAX_BATCH_DEPTH)}`);
3282
+ if (opts.actions.length > MAX_BATCH_ACTIONS)
3283
+ throw new Error(`Batch exceeds maximum of ${String(MAX_BATCH_ACTIONS)} actions`);
3284
+ const results = [];
3285
+ const evaluateEnabled = opts.evaluateEnabled !== false;
3286
+ for (const action of opts.actions) {
3287
+ try {
3288
+ await executeSingleAction(action, opts.cdpUrl, opts.targetId, evaluateEnabled, depth);
3289
+ results.push({ ok: true });
3290
+ } catch (err) {
3291
+ const message = err instanceof Error ? err.message : String(err);
3292
+ results.push({ ok: false, error: message });
3293
+ if (opts.stopOnError !== false) break;
3362
3294
  }
3363
- return await awaitEvalWithAbort(
3364
- page.evaluate(BROWSER_EVALUATOR, { fnBody: fnText, timeoutMs: evaluateTimeout }),
3365
- abortPromise
3366
- );
3367
- } finally {
3368
- if (signal && abortListener) signal.removeEventListener("abort", abortListener);
3369
3295
  }
3296
+ return { results };
3370
3297
  }
3371
3298
  function createPageDownloadWaiter(page, timeoutMs) {
3372
3299
  let done = false;
@@ -3401,379 +3328,856 @@ function createPageDownloadWaiter(page, timeoutMs) {
3401
3328
  done = true;
3402
3329
  cleanup();
3403
3330
  }
3404
- };
3331
+ };
3332
+ }
3333
+ async function saveDownloadPayload(download, outPath) {
3334
+ await writeViaSiblingTempPath({
3335
+ rootDir: dirname(outPath),
3336
+ targetPath: outPath,
3337
+ writeTemp: async (tempPath) => {
3338
+ await download.saveAs(tempPath);
3339
+ }
3340
+ });
3341
+ return {
3342
+ url: download.url(),
3343
+ suggestedFilename: download.suggestedFilename(),
3344
+ path: outPath
3345
+ };
3346
+ }
3347
+ async function awaitDownloadPayload(params) {
3348
+ try {
3349
+ const download = await params.waiter.promise;
3350
+ if (params.state.armIdDownload !== params.armId) throw new Error("Download was superseded by another waiter");
3351
+ return await saveDownloadPayload(download, params.outPath);
3352
+ } catch (err) {
3353
+ params.waiter.cancel();
3354
+ throw err;
3355
+ }
3356
+ }
3357
+ async function downloadViaPlaywright(opts) {
3358
+ await assertSafeOutputPath(opts.path, opts.allowedOutputRoots);
3359
+ const page = await getPageForTargetId({ cdpUrl: opts.cdpUrl, targetId: opts.targetId });
3360
+ const state = ensurePageState(page);
3361
+ restoreRoleRefsForTarget({ cdpUrl: opts.cdpUrl, targetId: opts.targetId, page });
3362
+ const timeout = normalizeTimeoutMs(opts.timeoutMs, 12e4);
3363
+ const outPath = opts.path.trim();
3364
+ if (!outPath) throw new Error("path is required");
3365
+ state.armIdDownload = bumpDownloadArmId();
3366
+ const armId = state.armIdDownload;
3367
+ const waiter = createPageDownloadWaiter(page, timeout);
3368
+ try {
3369
+ const locator = refLocator(page, opts.ref);
3370
+ try {
3371
+ await locator.click({ timeout });
3372
+ } catch (err) {
3373
+ throw toAIFriendlyError(err, opts.ref);
3374
+ }
3375
+ return await awaitDownloadPayload({ waiter, state, armId, outPath });
3376
+ } catch (err) {
3377
+ waiter.cancel();
3378
+ throw err;
3379
+ }
3380
+ }
3381
+ async function waitForDownloadViaPlaywright(opts) {
3382
+ const page = await getPageForTargetId({ cdpUrl: opts.cdpUrl, targetId: opts.targetId });
3383
+ const state = ensurePageState(page);
3384
+ const timeout = normalizeTimeoutMs(opts.timeoutMs, 12e4);
3385
+ state.armIdDownload = bumpDownloadArmId();
3386
+ const armId = state.armIdDownload;
3387
+ const waiter = createPageDownloadWaiter(page, timeout);
3388
+ try {
3389
+ const download = await waiter.promise;
3390
+ if (state.armIdDownload !== armId) throw new Error("Download was superseded by another waiter");
3391
+ const savePath = opts.path ?? download.suggestedFilename();
3392
+ await assertSafeOutputPath(savePath, opts.allowedOutputRoots);
3393
+ return await saveDownloadPayload(download, savePath);
3394
+ } catch (err) {
3395
+ waiter.cancel();
3396
+ throw err;
3397
+ }
3398
+ }
3399
+ async function emulateMediaViaPlaywright(opts) {
3400
+ const page = await getPageForTargetId({ cdpUrl: opts.cdpUrl, targetId: opts.targetId });
3401
+ ensurePageState(page);
3402
+ await page.emulateMedia({ colorScheme: opts.colorScheme });
3403
+ }
3404
+ async function setDeviceViaPlaywright(opts) {
3405
+ const name = opts.name.trim();
3406
+ if (!name) throw new Error("device name is required");
3407
+ const device = devices[name];
3408
+ if (device === void 0) {
3409
+ throw new Error(`Unknown device "${name}".`);
3410
+ }
3411
+ const page = await getPageForTargetId({ cdpUrl: opts.cdpUrl, targetId: opts.targetId });
3412
+ ensurePageState(page);
3413
+ if (device.viewport !== null) {
3414
+ await page.setViewportSize({
3415
+ width: device.viewport.width,
3416
+ height: device.viewport.height
3417
+ });
3418
+ }
3419
+ await withPageScopedCdpClient({
3420
+ cdpUrl: opts.cdpUrl,
3421
+ page,
3422
+ targetId: opts.targetId,
3423
+ fn: async (send) => {
3424
+ const locale = device.locale;
3425
+ if (device.userAgent !== "" || locale !== void 0 && locale !== "") {
3426
+ await send("Emulation.setUserAgentOverride", {
3427
+ userAgent: device.userAgent,
3428
+ acceptLanguage: locale
3429
+ });
3430
+ }
3431
+ if (device.viewport !== null) {
3432
+ await send("Emulation.setDeviceMetricsOverride", {
3433
+ mobile: device.isMobile,
3434
+ width: device.viewport.width,
3435
+ height: device.viewport.height,
3436
+ deviceScaleFactor: device.deviceScaleFactor,
3437
+ screenWidth: device.viewport.width,
3438
+ screenHeight: device.viewport.height
3439
+ });
3440
+ }
3441
+ if (device.hasTouch) {
3442
+ await send("Emulation.setTouchEmulationEnabled", { enabled: true });
3443
+ }
3444
+ }
3445
+ });
3446
+ }
3447
+ async function setExtraHTTPHeadersViaPlaywright(opts) {
3448
+ const page = await getPageForTargetId({ cdpUrl: opts.cdpUrl, targetId: opts.targetId });
3449
+ ensurePageState(page);
3450
+ await page.context().setExtraHTTPHeaders(opts.headers);
3451
+ }
3452
+ async function setGeolocationViaPlaywright(opts) {
3453
+ const page = await getPageForTargetId({ cdpUrl: opts.cdpUrl, targetId: opts.targetId });
3454
+ ensurePageState(page);
3455
+ const context = page.context();
3456
+ if (opts.clear === true) {
3457
+ await context.setGeolocation(null);
3458
+ await context.clearPermissions().catch(() => {
3459
+ });
3460
+ return;
3461
+ }
3462
+ if (typeof opts.latitude !== "number" || typeof opts.longitude !== "number") {
3463
+ throw new Error("latitude and longitude are required (or set clear=true)");
3464
+ }
3465
+ await context.setGeolocation({
3466
+ latitude: opts.latitude,
3467
+ longitude: opts.longitude,
3468
+ accuracy: typeof opts.accuracy === "number" ? opts.accuracy : void 0
3469
+ });
3470
+ const origin = (opts.origin !== void 0 && opts.origin !== "" ? opts.origin.trim() : "") || (() => {
3471
+ try {
3472
+ return new URL(page.url()).origin;
3473
+ } catch {
3474
+ return "";
3475
+ }
3476
+ })();
3477
+ if (origin !== "")
3478
+ await context.grantPermissions(["geolocation"], { origin }).catch(() => {
3479
+ });
3480
+ }
3481
+ async function setHttpCredentialsViaPlaywright(opts) {
3482
+ const page = await getPageForTargetId({ cdpUrl: opts.cdpUrl, targetId: opts.targetId });
3483
+ ensurePageState(page);
3484
+ if (opts.clear === true) {
3485
+ await page.context().setHTTPCredentials(null);
3486
+ return;
3487
+ }
3488
+ const username = opts.username ?? "";
3489
+ const password = opts.password ?? "";
3490
+ if (!username) throw new Error("username is required (or set clear=true)");
3491
+ await page.context().setHTTPCredentials({ username, password });
3492
+ }
3493
+ async function setLocaleViaPlaywright(opts) {
3494
+ const page = await getPageForTargetId({ cdpUrl: opts.cdpUrl, targetId: opts.targetId });
3495
+ ensurePageState(page);
3496
+ const locale = opts.locale.trim();
3497
+ if (!locale) throw new Error("locale is required");
3498
+ await withPageScopedCdpClient({
3499
+ cdpUrl: opts.cdpUrl,
3500
+ page,
3501
+ targetId: opts.targetId,
3502
+ fn: async (send) => {
3503
+ try {
3504
+ await send("Emulation.setLocaleOverride", { locale });
3505
+ } catch (err) {
3506
+ if (String(err).includes("Another locale override is already in effect")) return;
3507
+ throw err;
3508
+ }
3509
+ }
3510
+ });
3405
3511
  }
3406
- async function saveDownloadPayload(download, outPath) {
3407
- await writeViaSiblingTempPath({
3408
- rootDir: dirname(outPath),
3409
- targetPath: outPath,
3410
- writeTemp: async (tempPath) => {
3411
- await download.saveAs(tempPath);
3512
+ async function setOfflineViaPlaywright(opts) {
3513
+ const page = await getPageForTargetId({ cdpUrl: opts.cdpUrl, targetId: opts.targetId });
3514
+ ensurePageState(page);
3515
+ await page.context().setOffline(opts.offline);
3516
+ }
3517
+ async function setTimezoneViaPlaywright(opts) {
3518
+ const page = await getPageForTargetId({ cdpUrl: opts.cdpUrl, targetId: opts.targetId });
3519
+ ensurePageState(page);
3520
+ const timezoneId = opts.timezoneId.trim();
3521
+ if (!timezoneId) throw new Error("timezoneId is required");
3522
+ await withPageScopedCdpClient({
3523
+ cdpUrl: opts.cdpUrl,
3524
+ page,
3525
+ targetId: opts.targetId,
3526
+ fn: async (send) => {
3527
+ try {
3528
+ await send("Emulation.setTimezoneOverride", { timezoneId });
3529
+ } catch (err) {
3530
+ const msg = String(err);
3531
+ if (msg.includes("Timezone override is already in effect")) return;
3532
+ if (msg.includes("Invalid timezone")) throw new Error(`Invalid timezone ID: ${timezoneId}`, { cause: err });
3533
+ throw err;
3534
+ }
3412
3535
  }
3413
3536
  });
3537
+ }
3538
+
3539
+ // src/capture/activity.ts
3540
+ function consolePriority(level) {
3541
+ switch (level) {
3542
+ case "error":
3543
+ return 3;
3544
+ case "warning":
3545
+ case "warn":
3546
+ return 2;
3547
+ case "info":
3548
+ case "log":
3549
+ return 1;
3550
+ case "debug":
3551
+ return 0;
3552
+ default:
3553
+ return 1;
3554
+ }
3555
+ }
3556
+ async function getConsoleMessagesViaPlaywright(opts) {
3557
+ const state = ensurePageState(await getPageForTargetId({ cdpUrl: opts.cdpUrl, targetId: opts.targetId }));
3558
+ const messages = opts.level !== void 0 && opts.level !== "" ? state.console.filter((msg) => consolePriority(msg.type) >= consolePriority(opts.level ?? "")) : [...state.console];
3559
+ if (opts.clear === true) state.console = [];
3560
+ return messages;
3561
+ }
3562
+ async function getPageErrorsViaPlaywright(opts) {
3563
+ const state = ensurePageState(await getPageForTargetId({ cdpUrl: opts.cdpUrl, targetId: opts.targetId }));
3564
+ const errors = [...state.errors];
3565
+ if (opts.clear === true) state.errors = [];
3566
+ return { errors };
3567
+ }
3568
+ async function getNetworkRequestsViaPlaywright(opts) {
3569
+ const state = ensurePageState(await getPageForTargetId({ cdpUrl: opts.cdpUrl, targetId: opts.targetId }));
3570
+ const raw = [...state.requests];
3571
+ const filter = typeof opts.filter === "string" ? opts.filter.trim() : "";
3572
+ const requests = filter ? raw.filter((r) => r.url.includes(filter)) : raw;
3573
+ if (opts.clear === true) {
3574
+ state.requests = [];
3575
+ state.requestIds = /* @__PURE__ */ new WeakMap();
3576
+ }
3577
+ return { requests };
3578
+ }
3579
+
3580
+ // src/capture/pdf.ts
3581
+ async function pdfViaPlaywright(opts) {
3582
+ const page = await getPageForTargetId({ cdpUrl: opts.cdpUrl, targetId: opts.targetId });
3583
+ ensurePageState(page);
3584
+ return { buffer: await page.pdf({ printBackground: true }) };
3585
+ }
3586
+
3587
+ // src/capture/response.ts
3588
+ function matchUrlPattern(pattern, url) {
3589
+ if (!pattern || !url) return false;
3590
+ if (pattern === url) return true;
3591
+ if (pattern.includes("*")) {
3592
+ const escaped = pattern.replace(/[.+^${}()|[\]\\]/g, "\\$&").replace(/\*/g, ".*");
3593
+ try {
3594
+ return new RegExp(`^${escaped}$`).test(url);
3595
+ } catch {
3596
+ return false;
3597
+ }
3598
+ }
3599
+ return url.includes(pattern);
3600
+ }
3601
+ async function responseBodyViaPlaywright(opts) {
3602
+ const page = await getPageForTargetId({ cdpUrl: opts.cdpUrl, targetId: opts.targetId });
3603
+ ensurePageState(page);
3604
+ const timeout = normalizeTimeoutMs(opts.timeoutMs, 3e4, 12e4);
3605
+ const pattern = opts.url.trim();
3606
+ if (!pattern) throw new Error("url is required");
3607
+ const response = await page.waitForResponse((resp) => matchUrlPattern(pattern, resp.url()), { timeout });
3608
+ let body = await response.text();
3609
+ let truncated = false;
3610
+ const maxChars = typeof opts.maxChars === "number" && Number.isFinite(opts.maxChars) ? Math.max(1, Math.min(5e6, Math.floor(opts.maxChars))) : 2e5;
3611
+ if (body.length > maxChars) {
3612
+ body = body.slice(0, maxChars);
3613
+ truncated = true;
3614
+ }
3615
+ const headers = {};
3616
+ const allHeaders = response.headers();
3617
+ for (const [key, value] of Object.entries(allHeaders)) {
3618
+ headers[key] = value;
3619
+ }
3414
3620
  return {
3415
- url: download.url(),
3416
- suggestedFilename: download.suggestedFilename(),
3417
- path: outPath
3621
+ url: response.url(),
3622
+ status: response.status(),
3623
+ headers,
3624
+ body,
3625
+ truncated
3418
3626
  };
3419
3627
  }
3420
- async function awaitDownloadPayload(params) {
3421
- try {
3422
- const download = await params.waiter.promise;
3423
- if (params.state.armIdDownload !== params.armId) throw new Error("Download was superseded by another waiter");
3424
- return await saveDownloadPayload(download, params.outPath);
3425
- } catch (err) {
3426
- params.waiter.cancel();
3427
- throw err;
3628
+
3629
+ // src/capture/screenshot.ts
3630
+ async function takeScreenshotViaPlaywright(opts) {
3631
+ const page = await getPageForTargetId({ cdpUrl: opts.cdpUrl, targetId: opts.targetId });
3632
+ ensurePageState(page);
3633
+ restoreRoleRefsForTarget({ cdpUrl: opts.cdpUrl, targetId: opts.targetId, page });
3634
+ const type = opts.type ?? "png";
3635
+ if (opts.ref !== void 0 && opts.ref !== "") {
3636
+ if (opts.fullPage === true) throw new Error("fullPage is not supported for element screenshots");
3637
+ return { buffer: await refLocator(page, opts.ref).screenshot({ type }) };
3428
3638
  }
3639
+ if (opts.element !== void 0 && opts.element !== "") {
3640
+ if (opts.fullPage === true) throw new Error("fullPage is not supported for element screenshots");
3641
+ return { buffer: await page.locator(opts.element).first().screenshot({ type }) };
3642
+ }
3643
+ return { buffer: await page.screenshot({ type, fullPage: Boolean(opts.fullPage) }) };
3429
3644
  }
3430
- async function downloadViaPlaywright(opts) {
3431
- await assertSafeOutputPath(opts.path, opts.allowedOutputRoots);
3645
+ async function screenshotWithLabelsViaPlaywright(opts) {
3432
3646
  const page = await getPageForTargetId({ cdpUrl: opts.cdpUrl, targetId: opts.targetId });
3433
- const state = ensurePageState(page);
3647
+ ensurePageState(page);
3434
3648
  restoreRoleRefsForTarget({ cdpUrl: opts.cdpUrl, targetId: opts.targetId, page });
3435
- const timeout = normalizeTimeoutMs(opts.timeoutMs, 12e4);
3436
- const outPath = String(opts.path ?? "").trim();
3437
- if (!outPath) throw new Error("path is required");
3438
- state.armIdDownload = bumpDownloadArmId();
3439
- const armId = state.armIdDownload;
3440
- const waiter = createPageDownloadWaiter(page, timeout);
3441
- try {
3442
- const locator = refLocator(page, opts.ref);
3649
+ const maxLabels = typeof opts.maxLabels === "number" && Number.isFinite(opts.maxLabels) ? Math.max(1, Math.floor(opts.maxLabels)) : 150;
3650
+ const type = opts.type ?? "png";
3651
+ const refs = opts.refs.slice(0, maxLabels);
3652
+ const skipped = opts.refs.slice(maxLabels);
3653
+ const viewport = await page.evaluate(() => ({
3654
+ width: window.innerWidth || 0,
3655
+ height: window.innerHeight || 0
3656
+ }));
3657
+ const labels = [];
3658
+ for (let i = 0; i < refs.length; i++) {
3659
+ const ref = refs[i];
3443
3660
  try {
3444
- await locator.click({ timeout });
3445
- } catch (err) {
3446
- throw toAIFriendlyError(err, opts.ref);
3661
+ const locator = refLocator(page, ref);
3662
+ const box = await locator.boundingBox({ timeout: 2e3 });
3663
+ if (!box) {
3664
+ skipped.push(ref);
3665
+ continue;
3666
+ }
3667
+ const x1 = box.x + box.width;
3668
+ const y1 = box.y + box.height;
3669
+ if (x1 < 0 || box.x > viewport.width || y1 < 0 || box.y > viewport.height) {
3670
+ skipped.push(ref);
3671
+ continue;
3672
+ }
3673
+ labels.push({ ref, index: i + 1, box });
3674
+ } catch {
3675
+ skipped.push(ref);
3447
3676
  }
3448
- return await awaitDownloadPayload({ waiter, state, armId, outPath });
3449
- } catch (err) {
3450
- waiter.cancel();
3451
- throw err;
3677
+ }
3678
+ try {
3679
+ if (labels.length > 0) {
3680
+ await page.evaluate(
3681
+ (labelData) => {
3682
+ document.querySelectorAll("[data-browserclaw-labels]").forEach((el) => {
3683
+ el.remove();
3684
+ });
3685
+ const container = document.createElement("div");
3686
+ container.setAttribute("data-browserclaw-labels", "1");
3687
+ container.style.cssText = "position:fixed;top:0;left:0;width:100%;height:100%;pointer-events:none;z-index:2147483647;";
3688
+ for (const { index, box } of labelData) {
3689
+ const border = document.createElement("div");
3690
+ border.style.cssText = `position:absolute;left:${String(box.x)}px;top:${String(box.y)}px;width:${String(box.width)}px;height:${String(box.height)}px;border:2px solid #FF4500;box-sizing:border-box;`;
3691
+ container.appendChild(border);
3692
+ const badge = document.createElement("div");
3693
+ badge.textContent = String(index);
3694
+ badge.style.cssText = `position:absolute;left:${String(box.x)}px;top:${String(Math.max(0, box.y - 18))}px;background:#FF4500;color:#fff;font:bold 12px/16px monospace;padding:0 4px;border-radius:2px;`;
3695
+ container.appendChild(badge);
3696
+ }
3697
+ document.documentElement.appendChild(container);
3698
+ },
3699
+ labels.map((l) => ({ index: l.index, box: l.box }))
3700
+ );
3701
+ }
3702
+ return {
3703
+ buffer: await page.screenshot({ type }),
3704
+ labels,
3705
+ skipped
3706
+ };
3707
+ } finally {
3708
+ await page.evaluate(() => {
3709
+ document.querySelectorAll("[data-browserclaw-labels]").forEach((el) => {
3710
+ el.remove();
3711
+ });
3712
+ }).catch(() => {
3713
+ });
3452
3714
  }
3453
3715
  }
3454
- async function waitForDownloadViaPlaywright(opts) {
3716
+ async function traceStartViaPlaywright(opts) {
3455
3717
  const page = await getPageForTargetId({ cdpUrl: opts.cdpUrl, targetId: opts.targetId });
3456
- const state = ensurePageState(page);
3457
- const timeout = normalizeTimeoutMs(opts.timeoutMs, 12e4);
3458
- state.armIdDownload = bumpDownloadArmId();
3459
- const armId = state.armIdDownload;
3460
- const waiter = createPageDownloadWaiter(page, timeout);
3461
- try {
3462
- const download = await waiter.promise;
3463
- if (state.armIdDownload !== armId) throw new Error("Download was superseded by another waiter");
3464
- const savePath = opts.path ?? download.suggestedFilename();
3465
- await assertSafeOutputPath(savePath, opts.allowedOutputRoots);
3466
- return await saveDownloadPayload(download, savePath);
3467
- } catch (err) {
3468
- waiter.cancel();
3469
- throw err;
3718
+ ensurePageState(page);
3719
+ const context = page.context();
3720
+ const ctxState = ensureContextState(context);
3721
+ if (ctxState.traceActive) {
3722
+ throw new Error("Trace already running. Stop the current trace before starting a new one.");
3470
3723
  }
3724
+ await context.tracing.start({
3725
+ screenshots: opts.screenshots ?? true,
3726
+ snapshots: opts.snapshots ?? true,
3727
+ sources: opts.sources ?? false
3728
+ });
3729
+ ctxState.traceActive = true;
3471
3730
  }
3472
- async function emulateMediaViaPlaywright(opts) {
3731
+ async function traceStopViaPlaywright(opts) {
3732
+ await assertSafeOutputPath(opts.path, opts.allowedOutputRoots);
3473
3733
  const page = await getPageForTargetId({ cdpUrl: opts.cdpUrl, targetId: opts.targetId });
3474
3734
  ensurePageState(page);
3475
- await page.emulateMedia({ colorScheme: opts.colorScheme });
3735
+ const context = page.context();
3736
+ const ctxState = ensureContextState(context);
3737
+ if (!ctxState.traceActive) {
3738
+ throw new Error("No active trace. Start a trace before stopping it.");
3739
+ }
3740
+ await writeViaSiblingTempPath({
3741
+ rootDir: dirname(opts.path),
3742
+ targetPath: opts.path,
3743
+ writeTemp: async (tempPath) => {
3744
+ await context.tracing.stop({ path: tempPath });
3745
+ }
3746
+ });
3747
+ ctxState.traceActive = false;
3748
+ }
3749
+
3750
+ // src/snapshot/ref-map.ts
3751
+ var INTERACTIVE_ROLES = /* @__PURE__ */ new Set([
3752
+ "button",
3753
+ "link",
3754
+ "textbox",
3755
+ "checkbox",
3756
+ "radio",
3757
+ "combobox",
3758
+ "listbox",
3759
+ "menuitem",
3760
+ "menuitemcheckbox",
3761
+ "menuitemradio",
3762
+ "option",
3763
+ "searchbox",
3764
+ "slider",
3765
+ "spinbutton",
3766
+ "switch",
3767
+ "tab",
3768
+ "treeitem"
3769
+ ]);
3770
+ var CONTENT_ROLES = /* @__PURE__ */ new Set([
3771
+ "heading",
3772
+ "cell",
3773
+ "gridcell",
3774
+ "columnheader",
3775
+ "rowheader",
3776
+ "listitem",
3777
+ "article",
3778
+ "region",
3779
+ "main",
3780
+ "navigation"
3781
+ ]);
3782
+ var STRUCTURAL_ROLES = /* @__PURE__ */ new Set([
3783
+ "generic",
3784
+ "group",
3785
+ "list",
3786
+ "table",
3787
+ "row",
3788
+ "rowgroup",
3789
+ "grid",
3790
+ "treegrid",
3791
+ "menu",
3792
+ "menubar",
3793
+ "toolbar",
3794
+ "tablist",
3795
+ "tree",
3796
+ "directory",
3797
+ "document",
3798
+ "application",
3799
+ "presentation",
3800
+ "none"
3801
+ ]);
3802
+ function getIndentLevel(line) {
3803
+ const match = /^(\s*)/.exec(line);
3804
+ return match ? Math.floor(match[1].length / 2) : 0;
3476
3805
  }
3477
- async function setDeviceViaPlaywright(opts) {
3478
- const name = String(opts.name ?? "").trim();
3479
- if (!name) throw new Error("device name is required");
3480
- const device = devices[name];
3481
- if (!device) {
3482
- throw new Error(`Unknown device "${name}".`);
3806
+ function matchInteractiveSnapshotLine(line, options) {
3807
+ const depth = getIndentLevel(line);
3808
+ if (options.maxDepth !== void 0 && depth > options.maxDepth) {
3809
+ return null;
3483
3810
  }
3484
- const page = await getPageForTargetId({ cdpUrl: opts.cdpUrl, targetId: opts.targetId });
3485
- ensurePageState(page);
3486
- if (device.viewport) {
3487
- await page.setViewportSize({
3488
- width: device.viewport.width,
3489
- height: device.viewport.height
3490
- });
3811
+ const match = /^(\s*-\s*)(\w+)(?:\s+"([^"]*)")?(.*)$/.exec(line);
3812
+ if (!match) {
3813
+ return null;
3491
3814
  }
3492
- const session = await page.context().newCDPSession(page);
3493
- try {
3494
- const locale = device.locale;
3495
- if (device.userAgent || locale) {
3496
- await session.send("Emulation.setUserAgentOverride", {
3497
- userAgent: device.userAgent ?? "",
3498
- acceptLanguage: locale ?? void 0
3499
- });
3500
- }
3501
- if (device.viewport) {
3502
- await session.send("Emulation.setDeviceMetricsOverride", {
3503
- mobile: Boolean(device.isMobile),
3504
- width: device.viewport.width,
3505
- height: device.viewport.height,
3506
- deviceScaleFactor: device.deviceScaleFactor ?? 1,
3507
- screenWidth: device.viewport.width,
3508
- screenHeight: device.viewport.height
3509
- });
3510
- }
3511
- if (device.hasTouch) {
3512
- await session.send("Emulation.setTouchEmulationEnabled", { enabled: true });
3513
- }
3514
- } finally {
3515
- await session.detach().catch(() => {
3516
- });
3815
+ const [, , roleRaw, name, suffix] = match;
3816
+ if (roleRaw.startsWith("/")) {
3817
+ return null;
3517
3818
  }
3819
+ const role = roleRaw.toLowerCase();
3820
+ return {
3821
+ roleRaw,
3822
+ role,
3823
+ ...name ? { name } : {},
3824
+ suffix
3825
+ };
3518
3826
  }
3519
- async function setExtraHTTPHeadersViaPlaywright(opts) {
3520
- const page = await getPageForTargetId({ cdpUrl: opts.cdpUrl, targetId: opts.targetId });
3521
- ensurePageState(page);
3522
- await page.context().setExtraHTTPHeaders(opts.headers);
3523
- }
3524
- async function setGeolocationViaPlaywright(opts) {
3525
- const page = await getPageForTargetId({ cdpUrl: opts.cdpUrl, targetId: opts.targetId });
3526
- ensurePageState(page);
3527
- const context = page.context();
3528
- if (opts.clear) {
3529
- await context.setGeolocation(null);
3530
- await context.clearPermissions().catch(() => {
3531
- });
3532
- return;
3533
- }
3534
- if (typeof opts.latitude !== "number" || typeof opts.longitude !== "number") {
3535
- throw new Error("latitude and longitude are required (or set clear=true)");
3536
- }
3537
- await context.setGeolocation({
3538
- latitude: opts.latitude,
3539
- longitude: opts.longitude,
3540
- accuracy: typeof opts.accuracy === "number" ? opts.accuracy : void 0
3541
- });
3542
- const origin = opts.origin?.trim() || (() => {
3543
- try {
3544
- return new URL(page.url()).origin;
3545
- } catch {
3546
- return "";
3827
+ function createRoleNameTracker() {
3828
+ const counts = /* @__PURE__ */ new Map();
3829
+ const refsByKey = /* @__PURE__ */ new Map();
3830
+ return {
3831
+ counts,
3832
+ refsByKey,
3833
+ getKey(role, name) {
3834
+ return `${role}:${name ?? ""}`;
3835
+ },
3836
+ getNextIndex(role, name) {
3837
+ const key = this.getKey(role, name);
3838
+ const current = counts.get(key) ?? 0;
3839
+ counts.set(key, current + 1);
3840
+ return current;
3841
+ },
3842
+ trackRef(role, name, ref) {
3843
+ const key = this.getKey(role, name);
3844
+ const list = refsByKey.get(key) ?? [];
3845
+ list.push(ref);
3846
+ refsByKey.set(key, list);
3847
+ },
3848
+ getDuplicateKeys() {
3849
+ const out = /* @__PURE__ */ new Set();
3850
+ for (const [key, refs] of refsByKey) if (refs.length > 1) out.add(key);
3851
+ return out;
3547
3852
  }
3548
- })();
3549
- if (origin) await context.grantPermissions(["geolocation"], { origin }).catch(() => {
3550
- });
3853
+ };
3551
3854
  }
3552
- async function setHttpCredentialsViaPlaywright(opts) {
3553
- const page = await getPageForTargetId({ cdpUrl: opts.cdpUrl, targetId: opts.targetId });
3554
- ensurePageState(page);
3555
- if (opts.clear) {
3556
- await page.context().setHTTPCredentials(null);
3557
- return;
3855
+ function removeNthFromNonDuplicates(refs, tracker) {
3856
+ const duplicates = tracker.getDuplicateKeys();
3857
+ for (const [ref, data] of Object.entries(refs)) {
3858
+ const key = tracker.getKey(data.role, data.name);
3859
+ if (!duplicates.has(key)) delete refs[ref].nth;
3558
3860
  }
3559
- const username = String(opts.username ?? "");
3560
- const password = String(opts.password ?? "");
3561
- if (!username) throw new Error("username is required (or set clear=true)");
3562
- await page.context().setHTTPCredentials({ username, password });
3563
3861
  }
3564
- async function setLocaleViaPlaywright(opts) {
3565
- const page = await getPageForTargetId({ cdpUrl: opts.cdpUrl, targetId: opts.targetId });
3566
- ensurePageState(page);
3567
- const locale = String(opts.locale ?? "").trim();
3568
- if (!locale) throw new Error("locale is required");
3569
- const session = await page.context().newCDPSession(page);
3570
- try {
3571
- try {
3572
- await session.send("Emulation.setLocaleOverride", { locale });
3573
- } catch (err) {
3574
- if (String(err).includes("Another locale override is already in effect")) return;
3575
- throw err;
3862
+ function compactTree(tree) {
3863
+ const lines = tree.split("\n");
3864
+ const result = [];
3865
+ for (let i = 0; i < lines.length; i++) {
3866
+ const line = lines[i];
3867
+ if (line.includes("[ref=")) {
3868
+ result.push(line);
3869
+ continue;
3576
3870
  }
3577
- } finally {
3578
- await session.detach().catch(() => {
3579
- });
3580
- }
3581
- }
3582
- async function setOfflineViaPlaywright(opts) {
3583
- const page = await getPageForTargetId({ cdpUrl: opts.cdpUrl, targetId: opts.targetId });
3584
- ensurePageState(page);
3585
- await page.context().setOffline(Boolean(opts.offline));
3586
- }
3587
- async function setTimezoneViaPlaywright(opts) {
3588
- const page = await getPageForTargetId({ cdpUrl: opts.cdpUrl, targetId: opts.targetId });
3589
- ensurePageState(page);
3590
- const timezoneId = String(opts.timezoneId ?? "").trim();
3591
- if (!timezoneId) throw new Error("timezoneId is required");
3592
- const session = await page.context().newCDPSession(page);
3593
- try {
3594
- try {
3595
- await session.send("Emulation.setTimezoneOverride", { timezoneId });
3596
- } catch (err) {
3597
- const msg = String(err);
3598
- if (msg.includes("Timezone override is already in effect")) return;
3599
- if (msg.includes("Invalid timezone")) throw new Error(`Invalid timezone ID: ${timezoneId}`, { cause: err });
3600
- throw err;
3871
+ if (line.includes(":") && !line.trimEnd().endsWith(":")) {
3872
+ result.push(line);
3873
+ continue;
3601
3874
  }
3602
- } finally {
3603
- await session.detach().catch(() => {
3604
- });
3875
+ const currentIndent = getIndentLevel(line);
3876
+ let hasRelevantChildren = false;
3877
+ for (let j = i + 1; j < lines.length; j++) {
3878
+ if (getIndentLevel(lines[j]) <= currentIndent) break;
3879
+ if (lines[j]?.includes("[ref=")) {
3880
+ hasRelevantChildren = true;
3881
+ break;
3882
+ }
3883
+ }
3884
+ if (hasRelevantChildren) result.push(line);
3605
3885
  }
3886
+ return result.join("\n");
3606
3887
  }
3607
-
3608
- // src/capture/screenshot.ts
3609
- async function takeScreenshotViaPlaywright(opts) {
3610
- const page = await getPageForTargetId({ cdpUrl: opts.cdpUrl, targetId: opts.targetId });
3611
- ensurePageState(page);
3612
- restoreRoleRefsForTarget({ cdpUrl: opts.cdpUrl, targetId: opts.targetId, page });
3613
- const type = opts.type ?? "png";
3614
- if (opts.ref) {
3615
- if (opts.fullPage) throw new Error("fullPage is not supported for element screenshots");
3616
- return { buffer: await refLocator(page, opts.ref).screenshot({ type }) };
3888
+ function buildRoleSnapshotFromAriaSnapshot(ariaSnapshot, options = {}) {
3889
+ const lines = ariaSnapshot.split("\n");
3890
+ const refs = {};
3891
+ const tracker = createRoleNameTracker();
3892
+ let counter = 0;
3893
+ const nextRef = () => {
3894
+ counter++;
3895
+ return `e${String(counter)}`;
3896
+ };
3897
+ if (options.interactive === true) {
3898
+ const result2 = [];
3899
+ for (const line of lines) {
3900
+ const parsed = matchInteractiveSnapshotLine(line, options);
3901
+ if (!parsed) continue;
3902
+ const { roleRaw, role, name, suffix } = parsed;
3903
+ if (!INTERACTIVE_ROLES.has(role)) continue;
3904
+ const prefix = /^(\s*-\s*)/.exec(line)?.[1] ?? "";
3905
+ const ref = nextRef();
3906
+ const nth = tracker.getNextIndex(role, name);
3907
+ tracker.trackRef(role, name, ref);
3908
+ refs[ref] = { role, name, nth };
3909
+ let enhanced = `${prefix}${roleRaw}`;
3910
+ if (name !== void 0 && name !== "") enhanced += ` "${name}"`;
3911
+ enhanced += ` [ref=${ref}]`;
3912
+ if (nth > 0) enhanced += ` [nth=${String(nth)}]`;
3913
+ if (suffix.includes("[")) enhanced += suffix;
3914
+ result2.push(enhanced);
3915
+ }
3916
+ removeNthFromNonDuplicates(refs, tracker);
3917
+ return { snapshot: result2.join("\n") || "(no interactive elements)", refs };
3617
3918
  }
3618
- if (opts.element) {
3619
- if (opts.fullPage) throw new Error("fullPage is not supported for element screenshots");
3620
- return { buffer: await page.locator(opts.element).first().screenshot({ type }) };
3919
+ const result = [];
3920
+ for (const line of lines) {
3921
+ const depth = getIndentLevel(line);
3922
+ if (options.maxDepth !== void 0 && depth > options.maxDepth) continue;
3923
+ const match = /^(\s*-\s*)(\w+)(?:\s+"([^"]*)")?(.*)$/.exec(line);
3924
+ if (!match) {
3925
+ result.push(line);
3926
+ continue;
3927
+ }
3928
+ const [, prefix, roleRaw, name, suffix] = match;
3929
+ if (roleRaw.startsWith("/")) {
3930
+ result.push(line);
3931
+ continue;
3932
+ }
3933
+ const role = roleRaw.toLowerCase();
3934
+ const isInteractive = INTERACTIVE_ROLES.has(role);
3935
+ const isContent = CONTENT_ROLES.has(role);
3936
+ const isStructural = STRUCTURAL_ROLES.has(role);
3937
+ if (options.compact === true && isStructural && name === "") continue;
3938
+ if (!(isInteractive || isContent && name !== "")) {
3939
+ result.push(line);
3940
+ continue;
3941
+ }
3942
+ const ref = nextRef();
3943
+ const nth = tracker.getNextIndex(role, name);
3944
+ tracker.trackRef(role, name, ref);
3945
+ refs[ref] = { role, name, nth };
3946
+ let enhanced = `${prefix}${roleRaw}`;
3947
+ if (name !== "") enhanced += ` "${name}"`;
3948
+ enhanced += ` [ref=${ref}]`;
3949
+ if (nth > 0) enhanced += ` [nth=${String(nth)}]`;
3950
+ if (suffix !== "") enhanced += suffix;
3951
+ result.push(enhanced);
3621
3952
  }
3622
- return { buffer: await page.screenshot({ type, fullPage: Boolean(opts.fullPage) }) };
3953
+ removeNthFromNonDuplicates(refs, tracker);
3954
+ const tree = result.join("\n") || "(empty)";
3955
+ return { snapshot: options.compact === true ? compactTree(tree) : tree, refs };
3623
3956
  }
3624
- async function screenshotWithLabelsViaPlaywright(opts) {
3625
- const page = await getPageForTargetId({ cdpUrl: opts.cdpUrl, targetId: opts.targetId });
3626
- ensurePageState(page);
3627
- restoreRoleRefsForTarget({ cdpUrl: opts.cdpUrl, targetId: opts.targetId, page });
3628
- const maxLabels = opts.maxLabels ?? 50;
3629
- const type = opts.type ?? "png";
3630
- const refs = opts.refs.slice(0, maxLabels);
3631
- const skipped = opts.refs.slice(maxLabels);
3632
- const labels = [];
3633
- for (let i = 0; i < refs.length; i++) {
3634
- const ref = refs[i];
3635
- try {
3636
- const locator = refLocator(page, ref);
3637
- const box = await locator.boundingBox({ timeout: 2e3 });
3638
- if (box) {
3639
- labels.push({ ref, index: i + 1, box });
3640
- } else {
3641
- skipped.push(ref);
3642
- }
3643
- } catch {
3644
- skipped.push(ref);
3957
+ function buildRoleSnapshotFromAiSnapshot(aiSnapshot, options = {}) {
3958
+ const lines = aiSnapshot.split("\n");
3959
+ const refs = {};
3960
+ function parseAiSnapshotRef(suffix) {
3961
+ const match = /\[ref=(e\d+)\]/i.exec(suffix);
3962
+ return match ? match[1] : null;
3963
+ }
3964
+ if (options.interactive === true) {
3965
+ const out2 = [];
3966
+ for (const line of lines) {
3967
+ const parsed = matchInteractiveSnapshotLine(line, options);
3968
+ if (!parsed) continue;
3969
+ const { roleRaw, role, name, suffix } = parsed;
3970
+ if (!INTERACTIVE_ROLES.has(role)) continue;
3971
+ const ref = parseAiSnapshotRef(suffix);
3972
+ if (ref === null) continue;
3973
+ const prefix = /^(\s*-\s*)/.exec(line)?.[1] ?? "";
3974
+ refs[ref] = { role, ...name !== void 0 && name !== "" ? { name } : {} };
3975
+ out2.push(`${prefix}${roleRaw}${name !== void 0 && name !== "" ? ` "${name}"` : ""}${suffix}`);
3645
3976
  }
3977
+ return { snapshot: out2.join("\n") || "(no interactive elements)", refs };
3646
3978
  }
3647
- await page.evaluate((labelData) => {
3648
- const container = document.createElement("div");
3649
- container.id = "__browserclaw_labels__";
3650
- container.style.cssText = "position:fixed;top:0;left:0;width:100%;height:100%;pointer-events:none;z-index:2147483647;";
3651
- for (const { index, box } of labelData) {
3652
- const border = document.createElement("div");
3653
- border.style.cssText = `position:absolute;left:${box.x}px;top:${box.y}px;width:${box.width}px;height:${box.height}px;border:2px solid #FF4500;box-sizing:border-box;`;
3654
- container.appendChild(border);
3655
- const badge = document.createElement("div");
3656
- badge.textContent = String(index);
3657
- badge.style.cssText = `position:absolute;left:${box.x}px;top:${Math.max(0, box.y - 18)}px;background:#FF4500;color:#fff;font:bold 12px/16px monospace;padding:0 4px;border-radius:2px;`;
3658
- container.appendChild(badge);
3979
+ const out = [];
3980
+ for (const line of lines) {
3981
+ const depth = getIndentLevel(line);
3982
+ if (options.maxDepth !== void 0 && depth > options.maxDepth) continue;
3983
+ const match = /^(\s*-\s*)(\w+)(?:\s+"([^"]*)")?(.*)$/.exec(line);
3984
+ if (!match) {
3985
+ out.push(line);
3986
+ continue;
3659
3987
  }
3660
- document.body.appendChild(container);
3661
- }, labels.map((l) => ({ index: l.index, box: l.box })));
3662
- const buffer = await page.screenshot({ type });
3663
- await page.evaluate(() => {
3664
- const el = document.getElementById("__browserclaw_labels__");
3665
- if (el) el.remove();
3666
- }).catch(() => {
3667
- });
3668
- return { buffer, labels, skipped };
3988
+ const [, , roleRaw, name, suffix] = match;
3989
+ if (roleRaw.startsWith("/")) {
3990
+ out.push(line);
3991
+ continue;
3992
+ }
3993
+ const role = roleRaw.toLowerCase();
3994
+ const isStructural = STRUCTURAL_ROLES.has(role);
3995
+ if (options.compact === true && isStructural && name === "") continue;
3996
+ const ref = parseAiSnapshotRef(suffix);
3997
+ if (ref !== null) refs[ref] = { role, ...name !== "" ? { name } : {} };
3998
+ out.push(line);
3999
+ }
4000
+ const tree = out.join("\n") || "(empty)";
4001
+ return { snapshot: options.compact === true ? compactTree(tree) : tree, refs };
3669
4002
  }
3670
-
3671
- // src/capture/pdf.ts
3672
- async function pdfViaPlaywright(opts) {
3673
- const page = await getPageForTargetId({ cdpUrl: opts.cdpUrl, targetId: opts.targetId });
3674
- ensurePageState(page);
3675
- return { buffer: await page.pdf({ printBackground: true }) };
4003
+ function getRoleSnapshotStats(snapshot, refs) {
4004
+ const interactive = Object.values(refs).filter((r) => INTERACTIVE_ROLES.has(r.role)).length;
4005
+ return {
4006
+ lines: snapshot.split("\n").length,
4007
+ chars: snapshot.length,
4008
+ refs: Object.keys(refs).length,
4009
+ interactive
4010
+ };
3676
4011
  }
3677
- async function traceStartViaPlaywright(opts) {
4012
+
4013
+ // src/snapshot/ai-snapshot.ts
4014
+ async function snapshotAi(opts) {
3678
4015
  const page = await getPageForTargetId({ cdpUrl: opts.cdpUrl, targetId: opts.targetId });
3679
4016
  ensurePageState(page);
3680
- const context = page.context();
3681
- const ctxState = ensureContextState(context);
3682
- if (ctxState.traceActive) {
3683
- throw new Error("Trace already running. Stop the current trace before starting a new one.");
4017
+ const maybe = page;
4018
+ if (!maybe._snapshotForAI) {
4019
+ throw new Error("Playwright _snapshotForAI is not available. Upgrade playwright-core to >= 1.50.");
3684
4020
  }
3685
- await context.tracing.start({
3686
- screenshots: opts.screenshots ?? true,
3687
- snapshots: opts.snapshots ?? true,
3688
- sources: opts.sources ?? false
4021
+ const sourceUrl = page.url();
4022
+ const result = await maybe._snapshotForAI({
4023
+ timeout: normalizeTimeoutMs(opts.timeoutMs, 5e3, 6e4),
4024
+ track: "response"
3689
4025
  });
3690
- ctxState.traceActive = true;
3691
- }
3692
- async function traceStopViaPlaywright(opts) {
3693
- await assertSafeOutputPath(opts.path, opts.allowedOutputRoots);
3694
- const page = await getPageForTargetId({ cdpUrl: opts.cdpUrl, targetId: opts.targetId });
3695
- ensurePageState(page);
3696
- const context = page.context();
3697
- const ctxState = ensureContextState(context);
3698
- if (!ctxState.traceActive) {
3699
- throw new Error("No active trace. Start a trace before stopping it.");
4026
+ let snapshot = String(result.full);
4027
+ const maxChars = opts.maxChars;
4028
+ const limit = typeof maxChars === "number" && Number.isFinite(maxChars) && maxChars > 0 ? Math.floor(maxChars) : void 0;
4029
+ let truncated = false;
4030
+ if (limit !== void 0 && snapshot.length > limit) {
4031
+ const lastNewline = snapshot.lastIndexOf("\n", limit);
4032
+ const cutoff = lastNewline > 0 ? lastNewline : limit;
4033
+ snapshot = `${snapshot.slice(0, cutoff)}
4034
+
4035
+ [...TRUNCATED - page too large]`;
4036
+ truncated = true;
3700
4037
  }
3701
- await writeViaSiblingTempPath({
3702
- rootDir: dirname(opts.path),
3703
- targetPath: opts.path,
3704
- writeTemp: async (tempPath) => {
3705
- await context.tracing.stop({ path: tempPath });
3706
- }
4038
+ const built = buildRoleSnapshotFromAiSnapshot(snapshot, opts.options);
4039
+ storeRoleRefsForTarget({
4040
+ page,
4041
+ cdpUrl: opts.cdpUrl,
4042
+ targetId: opts.targetId,
4043
+ refs: built.refs,
4044
+ mode: "aria"
3707
4045
  });
3708
- ctxState.traceActive = false;
4046
+ return {
4047
+ snapshot: built.snapshot,
4048
+ refs: built.refs,
4049
+ stats: getRoleSnapshotStats(built.snapshot, built.refs),
4050
+ ...truncated ? { truncated } : {},
4051
+ untrusted: true,
4052
+ contentMeta: {
4053
+ sourceUrl,
4054
+ contentType: "browser-snapshot",
4055
+ capturedAt: (/* @__PURE__ */ new Date()).toISOString()
4056
+ }
4057
+ };
3709
4058
  }
3710
4059
 
3711
- // src/capture/response.ts
3712
- async function responseBodyViaPlaywright(opts) {
4060
+ // src/snapshot/aria-snapshot.ts
4061
+ async function snapshotRole(opts) {
3713
4062
  const page = await getPageForTargetId({ cdpUrl: opts.cdpUrl, targetId: opts.targetId });
3714
4063
  ensurePageState(page);
3715
- const timeout = normalizeTimeoutMs(opts.timeoutMs, 3e4, 12e4);
3716
- const response = await page.waitForResponse(opts.url, { timeout });
3717
- let body = await response.text();
3718
- let truncated = false;
3719
- const maxChars = typeof opts.maxChars === "number" && Number.isFinite(opts.maxChars) ? Math.max(1, Math.min(5e6, Math.floor(opts.maxChars))) : void 0;
3720
- if (maxChars !== void 0 && body.length > maxChars) {
3721
- body = body.slice(0, maxChars);
3722
- truncated = true;
3723
- }
3724
- const headers = {};
3725
- const allHeaders = response.headers();
3726
- for (const [key, value] of Object.entries(allHeaders)) {
3727
- headers[key] = value;
4064
+ const sourceUrl = page.url();
4065
+ if (opts.refsMode === "aria") {
4066
+ if (opts.selector !== void 0 && opts.selector.trim() !== "" || opts.frameSelector !== void 0 && opts.frameSelector.trim() !== "") {
4067
+ throw new Error("refs=aria does not support selector/frame snapshots yet.");
4068
+ }
4069
+ const maybe = page;
4070
+ if (!maybe._snapshotForAI) {
4071
+ throw new Error("refs=aria requires Playwright _snapshotForAI support.");
4072
+ }
4073
+ const result = await maybe._snapshotForAI({ timeout: 5e3, track: "response" });
4074
+ const built2 = buildRoleSnapshotFromAiSnapshot(String(result.full), opts.options);
4075
+ storeRoleRefsForTarget({
4076
+ page,
4077
+ cdpUrl: opts.cdpUrl,
4078
+ targetId: opts.targetId,
4079
+ refs: built2.refs,
4080
+ mode: "aria"
4081
+ });
4082
+ return {
4083
+ snapshot: built2.snapshot,
4084
+ refs: built2.refs,
4085
+ stats: getRoleSnapshotStats(built2.snapshot, built2.refs),
4086
+ untrusted: true,
4087
+ contentMeta: {
4088
+ sourceUrl,
4089
+ contentType: "browser-snapshot",
4090
+ capturedAt: (/* @__PURE__ */ new Date()).toISOString()
4091
+ }
4092
+ };
3728
4093
  }
4094
+ const frameSelector = opts.frameSelector?.trim() ?? "";
4095
+ const selector = opts.selector?.trim() ?? "";
4096
+ const locator = frameSelector ? selector ? page.frameLocator(frameSelector).locator(selector) : page.frameLocator(frameSelector).locator(":root") : selector ? page.locator(selector) : page.locator(":root");
4097
+ const ariaSnapshot = await locator.ariaSnapshot({ timeout: normalizeTimeoutMs(opts.timeoutMs, 5e3) });
4098
+ const built = buildRoleSnapshotFromAriaSnapshot(ariaSnapshot, opts.options);
4099
+ storeRoleRefsForTarget({
4100
+ page,
4101
+ cdpUrl: opts.cdpUrl,
4102
+ targetId: opts.targetId,
4103
+ refs: built.refs,
4104
+ frameSelector: frameSelector !== "" ? frameSelector : void 0,
4105
+ mode: "role"
4106
+ });
3729
4107
  return {
3730
- url: response.url(),
3731
- status: response.status(),
3732
- headers,
3733
- body,
3734
- truncated
4108
+ snapshot: built.snapshot,
4109
+ refs: built.refs,
4110
+ stats: getRoleSnapshotStats(built.snapshot, built.refs),
4111
+ untrusted: true,
4112
+ contentMeta: {
4113
+ sourceUrl,
4114
+ contentType: "browser-snapshot",
4115
+ capturedAt: (/* @__PURE__ */ new Date()).toISOString()
4116
+ }
3735
4117
  };
3736
4118
  }
3737
-
3738
- // src/capture/activity.ts
3739
- function consolePriority(level) {
3740
- switch (level) {
3741
- case "error":
3742
- return 3;
3743
- case "warning":
3744
- case "warn":
3745
- return 2;
3746
- case "info":
3747
- case "log":
3748
- return 1;
3749
- case "debug":
3750
- return 0;
3751
- default:
3752
- return 1;
3753
- }
3754
- }
3755
- async function getConsoleMessagesViaPlaywright(opts) {
3756
- const state = ensurePageState(await getPageForTargetId({ cdpUrl: opts.cdpUrl, targetId: opts.targetId }));
3757
- const messages = opts.level ? state.console.filter((msg) => consolePriority(msg.type) >= consolePriority(opts.level)) : [...state.console];
3758
- if (opts.clear) state.console = [];
3759
- return messages;
4119
+ async function snapshotAria(opts) {
4120
+ const limit = Math.max(1, Math.min(2e3, Math.floor(opts.limit ?? 500)));
4121
+ const page = await getPageForTargetId({ cdpUrl: opts.cdpUrl, targetId: opts.targetId });
4122
+ ensurePageState(page);
4123
+ const sourceUrl = page.url();
4124
+ const res = await withPlaywrightPageCdpSession(page, async (session) => {
4125
+ await session.send("Accessibility.enable").catch(() => {
4126
+ });
4127
+ return await session.send("Accessibility.getFullAXTree");
4128
+ });
4129
+ return {
4130
+ nodes: formatAriaNodes(Array.isArray(res.nodes) ? res.nodes : [], limit),
4131
+ untrusted: true,
4132
+ contentMeta: {
4133
+ sourceUrl,
4134
+ contentType: "browser-aria-tree",
4135
+ capturedAt: (/* @__PURE__ */ new Date()).toISOString()
4136
+ }
4137
+ };
3760
4138
  }
3761
- async function getPageErrorsViaPlaywright(opts) {
3762
- const state = ensurePageState(await getPageForTargetId({ cdpUrl: opts.cdpUrl, targetId: opts.targetId }));
3763
- const errors = [...state.errors];
3764
- if (opts.clear) state.errors = [];
3765
- return { errors };
4139
+ function axValue(v) {
4140
+ if (!v || typeof v !== "object") return "";
4141
+ const value = v.value;
4142
+ if (typeof value === "string") return value;
4143
+ if (typeof value === "number" || typeof value === "boolean") return String(value);
4144
+ return "";
3766
4145
  }
3767
- async function getNetworkRequestsViaPlaywright(opts) {
3768
- const state = ensurePageState(await getPageForTargetId({ cdpUrl: opts.cdpUrl, targetId: opts.targetId }));
3769
- const raw = [...state.requests];
3770
- const filter = typeof opts.filter === "string" ? opts.filter.trim() : "";
3771
- const requests = filter ? raw.filter((r) => r.url.includes(filter)) : raw;
3772
- if (opts.clear) {
3773
- state.requests = [];
3774
- state.requestIds = /* @__PURE__ */ new WeakMap();
4146
+ function formatAriaNodes(nodes, limit) {
4147
+ const byId = /* @__PURE__ */ new Map();
4148
+ for (const n of nodes) if (n.nodeId) byId.set(n.nodeId, n);
4149
+ const referenced = /* @__PURE__ */ new Set();
4150
+ for (const n of nodes) for (const c of n.childIds ?? []) referenced.add(c);
4151
+ const root = nodes.find((n) => n.nodeId !== "" && !referenced.has(n.nodeId)) ?? nodes[0];
4152
+ if (root.nodeId === "") return [];
4153
+ const out = [];
4154
+ const stack = [{ id: root.nodeId, depth: 0 }];
4155
+ while (stack.length && out.length < limit) {
4156
+ const popped = stack.pop();
4157
+ if (!popped) break;
4158
+ const { id, depth } = popped;
4159
+ const n = byId.get(id);
4160
+ if (!n) continue;
4161
+ const role = axValue(n.role);
4162
+ const name = axValue(n.name);
4163
+ const value = axValue(n.value);
4164
+ const description = axValue(n.description);
4165
+ const ref = `ax${String(out.length + 1)}`;
4166
+ out.push({
4167
+ ref,
4168
+ role: role || "unknown",
4169
+ name: name || "",
4170
+ ...value ? { value } : {},
4171
+ ...description ? { description } : {},
4172
+ ...typeof n.backendDOMNodeId === "number" ? { backendDOMNodeId: n.backendDOMNodeId } : {},
4173
+ depth
4174
+ });
4175
+ const children = (n.childIds ?? []).filter((c) => byId.has(c));
4176
+ for (let i = children.length - 1; i >= 0; i--) {
4177
+ if (children[i]) stack.push({ id: children[i], depth: depth + 1 });
4178
+ }
3775
4179
  }
3776
- return { requests };
4180
+ return out;
3777
4181
  }
3778
4182
 
3779
4183
  // src/storage/index.ts
@@ -3786,9 +4190,9 @@ async function cookiesSetViaPlaywright(opts) {
3786
4190
  const page = await getPageForTargetId({ cdpUrl: opts.cdpUrl, targetId: opts.targetId });
3787
4191
  ensurePageState(page);
3788
4192
  const cookie = opts.cookie;
3789
- if (!cookie.name || cookie.value === void 0) throw new Error("cookie name and value are required");
3790
- const hasUrl = typeof cookie.url === "string" && cookie.url.trim();
3791
- const hasDomainPath = typeof cookie.domain === "string" && cookie.domain.trim() && typeof cookie.path === "string" && cookie.path.trim();
4193
+ if (cookie.name === "") throw new Error("cookie name and value are required");
4194
+ const hasUrl = typeof cookie.url === "string" && cookie.url.trim() !== "";
4195
+ const hasDomainPath = typeof cookie.domain === "string" && cookie.domain.trim() !== "" && typeof cookie.path === "string" && cookie.path.trim() !== "";
3792
4196
  if (!hasUrl && !hasDomainPath) throw new Error("cookie requires url, or domain+path");
3793
4197
  await page.context().addCookies([cookie]);
3794
4198
  }
@@ -3804,33 +4208,33 @@ async function storageGetViaPlaywright(opts) {
3804
4208
  values: await page.evaluate(
3805
4209
  ({ kind, key }) => {
3806
4210
  const store = kind === "session" ? window.sessionStorage : window.localStorage;
3807
- if (key) {
4211
+ if (key !== void 0 && key !== "") {
3808
4212
  const value = store.getItem(key);
3809
4213
  return value === null ? {} : { [key]: value };
3810
4214
  }
3811
4215
  const out = {};
3812
4216
  for (let i = 0; i < store.length; i++) {
3813
4217
  const k = store.key(i);
3814
- if (!k) continue;
4218
+ if (k === null || k === "") continue;
3815
4219
  const v = store.getItem(k);
3816
4220
  if (v !== null) out[k] = v;
3817
4221
  }
3818
4222
  return out;
3819
4223
  },
3820
4224
  { kind: opts.kind, key: opts.key }
3821
- ) ?? {}
4225
+ )
3822
4226
  };
3823
4227
  }
3824
4228
  async function storageSetViaPlaywright(opts) {
3825
- const key = String(opts.key ?? "");
3826
- if (!key) throw new Error("key is required");
4229
+ const key = opts.key;
4230
+ if (key === "") throw new Error("key is required");
3827
4231
  const page = await getPageForTargetId({ cdpUrl: opts.cdpUrl, targetId: opts.targetId });
3828
4232
  ensurePageState(page);
3829
4233
  await page.evaluate(
3830
4234
  ({ kind, key: k, value }) => {
3831
4235
  (kind === "session" ? window.sessionStorage : window.localStorage).setItem(k, value);
3832
4236
  },
3833
- { kind: opts.kind, key, value: String(opts.value ?? "") }
4237
+ { kind: opts.kind, key, value: opts.value }
3834
4238
  );
3835
4239
  }
3836
4240
  async function storageClearViaPlaywright(opts) {
@@ -3897,8 +4301,10 @@ var CrawlPage = class {
3897
4301
  }
3898
4302
  });
3899
4303
  }
3900
- if (opts?.selector || opts?.frameSelector) {
3901
- throw new Error('selector and frameSelector are only supported in role mode. Use { mode: "role" } or omit these options.');
4304
+ if (opts?.selector !== void 0 && opts.selector !== "" || opts?.frameSelector !== void 0 && opts.frameSelector !== "") {
4305
+ throw new Error(
4306
+ 'selector and frameSelector are only supported in role mode. Use { mode: "role" } or omit these options.'
4307
+ );
3902
4308
  }
3903
4309
  return snapshotAi({
3904
4310
  cdpUrl: this.cdpUrl,
@@ -4137,6 +4543,22 @@ var CrawlPage = class {
4137
4543
  timeoutMs: opts?.timeoutMs
4138
4544
  });
4139
4545
  }
4546
+ /**
4547
+ * Execute multiple browser actions in sequence.
4548
+ *
4549
+ * @param actions - Array of actions to execute
4550
+ * @param opts - Options (stopOnError: stop on first failure, default true)
4551
+ * @returns Array of per-action results
4552
+ */
4553
+ async batch(actions, opts) {
4554
+ return batchViaPlaywright({
4555
+ cdpUrl: this.cdpUrl,
4556
+ targetId: this.targetId,
4557
+ actions,
4558
+ stopOnError: opts?.stopOnError,
4559
+ evaluateEnabled: opts?.evaluateEnabled
4560
+ });
4561
+ }
4140
4562
  // ── Keyboard ─────────────────────────────────────────────────
4141
4563
  /**
4142
4564
  * Press a keyboard key or key combination.
@@ -4753,8 +5175,8 @@ var BrowserClaw = class _BrowserClaw {
4753
5175
  */
4754
5176
  static async launch(opts = {}) {
4755
5177
  const chrome = await launchChrome(opts);
4756
- const cdpUrl = `http://127.0.0.1:${chrome.cdpPort}`;
4757
- const ssrfPolicy = opts.allowInternal ? { ...opts.ssrfPolicy, dangerouslyAllowPrivateNetwork: true } : opts.ssrfPolicy;
5178
+ const cdpUrl = `http://127.0.0.1:${String(chrome.cdpPort)}`;
5179
+ const ssrfPolicy = opts.allowInternal === true ? { ...opts.ssrfPolicy, dangerouslyAllowPrivateNetwork: true } : opts.ssrfPolicy;
4758
5180
  return new _BrowserClaw(cdpUrl, chrome, ssrfPolicy);
4759
5181
  }
4760
5182
  /**
@@ -4776,7 +5198,7 @@ var BrowserClaw = class _BrowserClaw {
4776
5198
  throw new Error(`Cannot connect to Chrome at ${cdpUrl}. Is Chrome running with --remote-debugging-port?`);
4777
5199
  }
4778
5200
  await connectBrowser(cdpUrl, opts?.authToken);
4779
- const ssrfPolicy = opts?.allowInternal ? { ...opts.ssrfPolicy, dangerouslyAllowPrivateNetwork: true } : opts?.ssrfPolicy;
5201
+ const ssrfPolicy = opts?.allowInternal === true ? { ...opts.ssrfPolicy, dangerouslyAllowPrivateNetwork: true } : opts?.ssrfPolicy;
4780
5202
  return new _BrowserClaw(cdpUrl, null, ssrfPolicy);
4781
5203
  }
4782
5204
  /**
@@ -4802,10 +5224,10 @@ var BrowserClaw = class _BrowserClaw {
4802
5224
  */
4803
5225
  async currentPage() {
4804
5226
  const { browser } = await connectBrowser(this.cdpUrl);
4805
- const pages = await getAllPages(browser);
5227
+ const pages = getAllPages(browser);
4806
5228
  if (!pages.length) throw new Error("No pages available. Use browser.open(url) to create a tab.");
4807
5229
  const tid = await pageTargetId(pages[0]).catch(() => null);
4808
- if (!tid) throw new Error("Failed to get targetId for the current page.");
5230
+ if (tid === null || tid === "") throw new Error("Failed to get targetId for the current page.");
4809
5231
  return new CrawlPage(this.cdpUrl, tid, this.ssrfPolicy);
4810
5232
  }
4811
5233
  /**
@@ -4863,6 +5285,6 @@ var BrowserClaw = class _BrowserClaw {
4863
5285
  }
4864
5286
  };
4865
5287
 
4866
- export { BrowserClaw, CrawlPage, InvalidBrowserNavigationUrlError, assertBrowserNavigationAllowed, assertBrowserNavigationRedirectChainAllowed, assertBrowserNavigationResultAllowed, createPinnedLookup, ensureContextState, forceDisconnectPlaywrightForTarget, getChromeWebSocketUrl, isChromeCdpReady, isChromeReachable, normalizeCdpHttpBaseForJsonEndpoints, requiresInspectableBrowserNavigationRedirects, resolvePinnedHostnameWithPolicy, sanitizeUntrustedFileName, withBrowserNavigationPolicy, writeViaSiblingTempPath };
5288
+ export { BrowserClaw, BrowserTabNotFoundError, CrawlPage, InvalidBrowserNavigationUrlError, assertBrowserNavigationAllowed, assertBrowserNavigationRedirectChainAllowed, assertBrowserNavigationResultAllowed, assertSafeUploadPaths, batchViaPlaywright, createPinnedLookup, ensureContextState, executeSingleAction, forceDisconnectPlaywrightForTarget, getChromeWebSocketUrl, getRestoredPageForTarget, isChromeCdpReady, isChromeReachable, normalizeCdpHttpBaseForJsonEndpoints, parseRoleRef, requireRef, requireRefOrSelector, requiresInspectableBrowserNavigationRedirects, resolveBoundedDelayMs, resolveInteractionTimeoutMs, resolvePageByTargetIdOrThrow, resolvePinnedHostnameWithPolicy, resolveStrictExistingUploadPaths, sanitizeUntrustedFileName, withBrowserNavigationPolicy, withPageScopedCdpClient, withPlaywrightPageCdpSession, writeViaSiblingTempPath };
4867
5289
  //# sourceMappingURL=index.js.map
4868
5290
  //# sourceMappingURL=index.js.map