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