browserclaw 0.10.6 → 0.11.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.js CHANGED
@@ -37,9 +37,9 @@ var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__ge
37
37
  mod
38
38
  ));
39
39
 
40
- // ../../../node_modules/ipaddr.js/lib/ipaddr.js
40
+ // node_modules/ipaddr.js/lib/ipaddr.js
41
41
  var require_ipaddr = __commonJS({
42
- "../../../node_modules/ipaddr.js/lib/ipaddr.js"(exports$1, module) {
42
+ "node_modules/ipaddr.js/lib/ipaddr.js"(exports$1, module) {
43
43
  (function(root) {
44
44
  const ipv4Part = "(0?\\d+|0x[a-f0-9]+)";
45
45
  const ipv4Regexes = {
@@ -835,6 +835,10 @@ var CHROMIUM_BUNDLE_IDS = /* @__PURE__ */ new Set([
835
835
  "com.microsoft.EdgeBeta",
836
836
  "com.microsoft.EdgeDev",
837
837
  "com.microsoft.EdgeCanary",
838
+ "com.microsoft.edgemac",
839
+ "com.microsoft.edgemac.beta",
840
+ "com.microsoft.edgemac.dev",
841
+ "com.microsoft.edgemac.canary",
838
842
  "org.chromium.Chromium",
839
843
  "com.vivaldi.Vivaldi",
840
844
  "com.operasoftware.Opera",
@@ -903,12 +907,12 @@ function fileExists(filePath) {
903
907
  return false;
904
908
  }
905
909
  }
906
- function execText(command, args, timeoutMs = 1200) {
910
+ function execText(command, args, timeoutMs = 1200, maxBuffer = 1024 * 1024) {
907
911
  try {
908
912
  const output = execFileSync(command, args, {
909
913
  timeout: timeoutMs,
910
914
  encoding: "utf8",
911
- maxBuffer: 1024 * 1024
915
+ maxBuffer
912
916
  });
913
917
  return output.trim() || null;
914
918
  } catch {
@@ -944,7 +948,12 @@ function detectDefaultBrowserBundleIdMac() {
944
948
  "Library/Preferences/com.apple.LaunchServices/com.apple.launchservices.secure.plist"
945
949
  );
946
950
  if (!fileExists(plistPath)) return null;
947
- const handlersRaw = execText("/usr/bin/plutil", ["-extract", "LSHandlers", "json", "-o", "-", "--", plistPath], 2e3);
951
+ const handlersRaw = execText(
952
+ "/usr/bin/plutil",
953
+ ["-extract", "LSHandlers", "json", "-o", "-", "--", plistPath],
954
+ 2e3,
955
+ 5 * 1024 * 1024
956
+ );
948
957
  if (handlersRaw === null) return null;
949
958
  let handlers;
950
959
  try {
@@ -996,6 +1005,34 @@ function findChromeMac() {
996
1005
  }
997
1006
  ]);
998
1007
  }
1008
+ function splitExecLine(line) {
1009
+ const tokens = [];
1010
+ let current = "";
1011
+ let inQuotes = false;
1012
+ let quoteChar = "";
1013
+ for (const ch of line) {
1014
+ if ((ch === '"' || ch === "'") && (!inQuotes || ch === quoteChar)) {
1015
+ if (inQuotes) {
1016
+ inQuotes = false;
1017
+ quoteChar = "";
1018
+ } else {
1019
+ inQuotes = true;
1020
+ quoteChar = ch;
1021
+ }
1022
+ continue;
1023
+ }
1024
+ if (!inQuotes && /\s/.test(ch)) {
1025
+ if (current) {
1026
+ tokens.push(current);
1027
+ current = "";
1028
+ }
1029
+ continue;
1030
+ }
1031
+ current += ch;
1032
+ }
1033
+ if (current) tokens.push(current);
1034
+ return tokens;
1035
+ }
999
1036
  function detectDefaultChromiumLinux() {
1000
1037
  const desktopId = execText("xdg-settings", ["get", "default-web-browser"]) ?? execText("xdg-mime", ["query", "default", "x-scheme-handler/http"]);
1001
1038
  if (desktopId === null) return null;
@@ -1027,10 +1064,10 @@ function detectDefaultChromiumLinux() {
1027
1064
  } catch {
1028
1065
  }
1029
1066
  if (execLine === null) return null;
1030
- const tokens = execLine.split(/\s+/);
1067
+ const tokens = splitExecLine(execLine);
1031
1068
  let command = null;
1032
1069
  for (const token of tokens) {
1033
- if (!token || token === "env" || token.includes("=") && !token.startsWith("/")) continue;
1070
+ if (!token || token === "env" || token.includes("=") && !token.startsWith("/") && !token.includes("\\")) continue;
1034
1071
  command = token.replace(/^["']|["']$/g, "");
1035
1072
  break;
1036
1073
  }
@@ -1087,6 +1124,49 @@ function findChromeWindows() {
1087
1124
  candidates.push({ kind: "edge", path: j(programFilesX86, "Microsoft", "Edge", "Application", "msedge.exe") });
1088
1125
  return findFirstExe(candidates);
1089
1126
  }
1127
+ function readWindowsProgId() {
1128
+ const output = execText("reg", [
1129
+ "query",
1130
+ "HKCU\\Software\\Microsoft\\Windows\\Shell\\Associations\\UrlAssociations\\http\\UserChoice",
1131
+ "/v",
1132
+ "ProgId"
1133
+ ]);
1134
+ if (output === null) return null;
1135
+ return /ProgId\s+REG_\w+\s+(.+)$/im.exec(output)?.[1]?.trim() ?? null;
1136
+ }
1137
+ function readWindowsCommandForProgId(progId) {
1138
+ const output = execText("reg", [
1139
+ "query",
1140
+ progId === "http" ? "HKCR\\http\\shell\\open\\command" : `HKCR\\${progId}\\shell\\open\\command`,
1141
+ "/ve"
1142
+ ]);
1143
+ if (output === null) return null;
1144
+ return /REG_\w+\s+(.+)$/im.exec(output)?.[1]?.trim() ?? null;
1145
+ }
1146
+ function expandWindowsEnvVars(value) {
1147
+ return value.replace(/%([^%]+)%/g, (_match, name) => {
1148
+ const key = name.trim();
1149
+ return key !== "" ? process.env[key] ?? `%${key}%` : _match;
1150
+ });
1151
+ }
1152
+ function extractWindowsExecutablePath(command) {
1153
+ const quoted = /"([^"]+\.exe)"/i.exec(command);
1154
+ if (quoted?.[1] !== void 0) return quoted[1];
1155
+ const unquoted = /([^\s]+\.exe)/i.exec(command);
1156
+ if (unquoted?.[1] !== void 0) return unquoted[1];
1157
+ return null;
1158
+ }
1159
+ function detectDefaultChromiumWindows() {
1160
+ const progId = readWindowsProgId();
1161
+ const command = (progId !== null ? readWindowsCommandForProgId(progId) : null) ?? readWindowsCommandForProgId("http");
1162
+ if (command === null) return null;
1163
+ const exePath = extractWindowsExecutablePath(expandWindowsEnvVars(command));
1164
+ if (exePath === null) return null;
1165
+ if (!fileExists(exePath)) return null;
1166
+ const exeName = path.win32.basename(exePath).toLowerCase();
1167
+ if (!CHROMIUM_EXE_NAMES.has(exeName)) return null;
1168
+ return { kind: inferKindFromExeName(exeName), path: exePath };
1169
+ }
1090
1170
  function resolveBrowserExecutable(opts) {
1091
1171
  if (opts?.executablePath !== void 0 && opts.executablePath !== "") {
1092
1172
  if (!fileExists(opts.executablePath)) throw new Error(`executablePath not found: ${opts.executablePath}`);
@@ -1095,7 +1175,7 @@ function resolveBrowserExecutable(opts) {
1095
1175
  const platform = process.platform;
1096
1176
  if (platform === "darwin") return detectDefaultChromiumMac() ?? findChromeMac();
1097
1177
  if (platform === "linux") return detectDefaultChromiumLinux() ?? findChromeLinux();
1098
- if (platform === "win32") return findChromeWindows();
1178
+ if (platform === "win32") return detectDefaultChromiumWindows() ?? findChromeWindows();
1099
1179
  return null;
1100
1180
  }
1101
1181
  async function ensurePortAvailable(port) {
@@ -1185,7 +1265,8 @@ function isWebSocketUrl(url) {
1185
1265
  }
1186
1266
  }
1187
1267
  function isLoopbackHost(hostname) {
1188
- return hostname === "localhost" || hostname === "127.0.0.1" || hostname === "::1" || hostname === "[::1]";
1268
+ const h = hostname.replace(/\.+$/, "");
1269
+ return h === "localhost" || h === "127.0.0.1" || h === "::1" || h === "[::1]";
1189
1270
  }
1190
1271
  var PROXY_ENV_KEYS = ["HTTP_PROXY", "HTTPS_PROXY", "ALL_PROXY", "http_proxy", "https_proxy", "all_proxy"];
1191
1272
  function hasProxyEnvConfigured(env = process.env) {
@@ -1377,6 +1458,7 @@ async function launchChrome(opts = {}) {
1377
1458
  const spawnChrome = () => {
1378
1459
  const args = [
1379
1460
  `--remote-debugging-port=${String(cdpPort)}`,
1461
+ "--remote-debugging-address=127.0.0.1",
1380
1462
  `--user-data-dir=${userDataDir}`,
1381
1463
  "--no-first-run",
1382
1464
  "--no-default-browser-check",
@@ -1395,6 +1477,9 @@ async function launchChrome(opts = {}) {
1395
1477
  if (opts.noSandbox === true) {
1396
1478
  args.push("--no-sandbox", "--disable-setuid-sandbox");
1397
1479
  }
1480
+ if (opts.ignoreHTTPSErrors === true) {
1481
+ args.push("--ignore-certificate-errors");
1482
+ }
1398
1483
  if (process.platform === "linux") args.push("--disable-dev-shm-usage");
1399
1484
  const extraArgs = Array.isArray(opts.chromeArgs) ? opts.chromeArgs.filter((a) => typeof a === "string" && a.trim().length > 0) : [];
1400
1485
  if (extraArgs.length) args.push(...extraArgs);
@@ -1715,7 +1800,15 @@ function appendCdpPath2(cdpUrl, cdpPath) {
1715
1800
  }
1716
1801
  }
1717
1802
  async function withPlaywrightPageCdpSession(page, fn) {
1718
- const session = await page.context().newCDPSession(page);
1803
+ const CDP_SESSION_TIMEOUT_MS = 1e4;
1804
+ const session = await Promise.race([
1805
+ page.context().newCDPSession(page),
1806
+ new Promise((_, reject) => {
1807
+ setTimeout(() => {
1808
+ reject(new Error("newCDPSession timed out after 10s"));
1809
+ }, CDP_SESSION_TIMEOUT_MS);
1810
+ })
1811
+ ]);
1719
1812
  try {
1720
1813
  return await fn(session);
1721
1814
  } finally {
@@ -1825,6 +1918,56 @@ function bumpDownloadArmId() {
1825
1918
  nextDownloadArmId += 1;
1826
1919
  return nextDownloadArmId;
1827
1920
  }
1921
+ var BlockedBrowserTargetError = class extends Error {
1922
+ constructor() {
1923
+ super("Browser target is unavailable after SSRF policy blocked its navigation.");
1924
+ this.name = "BlockedBrowserTargetError";
1925
+ }
1926
+ };
1927
+ var blockedTargetsByCdpUrl = /* @__PURE__ */ new Set();
1928
+ var blockedPageRefsByCdpUrl = /* @__PURE__ */ new Map();
1929
+ function blockedTargetKey(cdpUrl, targetId) {
1930
+ return `${normalizeCdpUrl(cdpUrl)}::${targetId}`;
1931
+ }
1932
+ function isBlockedTarget(cdpUrl, targetId) {
1933
+ const normalized = targetId?.trim() ?? "";
1934
+ if (normalized === "") return false;
1935
+ return blockedTargetsByCdpUrl.has(blockedTargetKey(cdpUrl, normalized));
1936
+ }
1937
+ function markTargetBlocked(cdpUrl, targetId) {
1938
+ const normalized = targetId?.trim() ?? "";
1939
+ if (normalized === "") return;
1940
+ blockedTargetsByCdpUrl.add(blockedTargetKey(cdpUrl, normalized));
1941
+ }
1942
+ function clearBlockedTarget(cdpUrl, targetId) {
1943
+ const normalized = targetId?.trim() ?? "";
1944
+ if (normalized === "") return;
1945
+ blockedTargetsByCdpUrl.delete(blockedTargetKey(cdpUrl, normalized));
1946
+ }
1947
+ function hasBlockedTargetsForCdpUrl(cdpUrl) {
1948
+ const prefix = `${normalizeCdpUrl(cdpUrl)}::`;
1949
+ for (const key of blockedTargetsByCdpUrl) {
1950
+ if (key.startsWith(prefix)) return true;
1951
+ }
1952
+ return false;
1953
+ }
1954
+ function blockedPageRefsForCdpUrl(cdpUrl) {
1955
+ const normalized = normalizeCdpUrl(cdpUrl);
1956
+ const existing = blockedPageRefsByCdpUrl.get(normalized);
1957
+ if (existing) return existing;
1958
+ const created = /* @__PURE__ */ new WeakSet();
1959
+ blockedPageRefsByCdpUrl.set(normalized, created);
1960
+ return created;
1961
+ }
1962
+ function isBlockedPageRef(cdpUrl, page) {
1963
+ return blockedPageRefsByCdpUrl.get(normalizeCdpUrl(cdpUrl))?.has(page) ?? false;
1964
+ }
1965
+ function markPageRefBlocked(cdpUrl, page) {
1966
+ blockedPageRefsForCdpUrl(cdpUrl).add(page);
1967
+ }
1968
+ function clearBlockedPageRef(cdpUrl, page) {
1969
+ blockedPageRefsByCdpUrl.get(normalizeCdpUrl(cdpUrl))?.delete(page);
1970
+ }
1828
1971
  function ensureContextState(context) {
1829
1972
  const existing = contextStates.get(context);
1830
1973
  if (existing) return existing;
@@ -2265,11 +2408,43 @@ async function findPageByTargetId(browser, targetId, cdpUrl) {
2265
2408
  if (!resolvedViaCdp && pages.length === 1) return pages[0] ?? null;
2266
2409
  return null;
2267
2410
  }
2411
+ async function partitionAccessiblePages(opts) {
2412
+ const accessible = [];
2413
+ let blockedCount = 0;
2414
+ for (const page of opts.pages) {
2415
+ if (isBlockedPageRef(opts.cdpUrl, page)) {
2416
+ blockedCount += 1;
2417
+ continue;
2418
+ }
2419
+ const targetId = await pageTargetId(page).catch(() => null);
2420
+ if (targetId === null || targetId === "") {
2421
+ if (hasBlockedTargetsForCdpUrl(opts.cdpUrl)) {
2422
+ blockedCount += 1;
2423
+ continue;
2424
+ }
2425
+ accessible.push(page);
2426
+ continue;
2427
+ }
2428
+ if (isBlockedTarget(opts.cdpUrl, targetId)) {
2429
+ blockedCount += 1;
2430
+ continue;
2431
+ }
2432
+ accessible.push(page);
2433
+ }
2434
+ return { accessible, blockedCount };
2435
+ }
2268
2436
  async function getPageForTargetId(opts) {
2437
+ if (opts.targetId !== void 0 && opts.targetId !== "" && isBlockedTarget(opts.cdpUrl, opts.targetId))
2438
+ throw new BlockedBrowserTargetError();
2269
2439
  const { browser } = await connectBrowser(opts.cdpUrl);
2270
2440
  const pages = getAllPages(browser);
2271
2441
  if (!pages.length) throw new Error("No pages available in the connected browser.");
2272
- const first = pages[0];
2442
+ const { accessible, blockedCount } = await partitionAccessiblePages({ cdpUrl: opts.cdpUrl, pages });
2443
+ if (!accessible.length) {
2444
+ if (blockedCount > 0) throw new BlockedBrowserTargetError();
2445
+ throw new Error("No pages available in the connected browser.");
2446
+ }
2447
+ const first = accessible[0];
2273
2448
  if (opts.targetId === void 0 || opts.targetId === "") return first;
2274
2449
  const found = await findPageByTargetId(browser, opts.targetId, opts.cdpUrl);
2275
2450
  if (!found) {
@@ -2278,6 +2453,10 @@ async function getPageForTargetId(opts) {
2278
2453
  `Tab not found (targetId: ${opts.targetId}). Call browser.tabs() to list open tabs.`
2279
2454
  );
2280
2455
  }
2456
+ if (isBlockedPageRef(opts.cdpUrl, found)) throw new BlockedBrowserTargetError();
2457
+ const foundTargetId = await pageTargetId(found).catch(() => null);
2458
+ if (foundTargetId !== null && foundTargetId !== "" && isBlockedTarget(opts.cdpUrl, foundTargetId))
2459
+ throw new BlockedBrowserTargetError();
2281
2460
  return found;
2282
2461
  }
2283
2462
  async function resolvePageByTargetIdOrThrow(opts) {
@@ -2355,7 +2534,14 @@ function toAIFriendlyError(error, selector) {
2355
2534
  `Element "${selector}" is not interactable (hidden or covered). Try scrolling it into view, closing overlays, or re-snapshotting.`
2356
2535
  );
2357
2536
  }
2358
- return error instanceof Error ? error : new Error(message);
2537
+ const timeoutMatch = /Timeout (\d+)ms exceeded/.exec(message);
2538
+ if (timeoutMatch) {
2539
+ return new Error(
2540
+ `Element "${selector}" timed out after ${timeoutMatch[1]}ms \u2014 element may be hidden or not interactable. Run a new snapshot to see current page elements.`
2541
+ );
2542
+ }
2543
+ const cleaned = message.replace(/locator\([^)]*\)\./g, "").replace(/waiting for locator\([^)]*\)/g, "").trim();
2544
+ return new Error(cleaned || message);
2359
2545
  }
2360
2546
  function normalizeTimeoutMs(timeoutMs, fallback, maxMs = 12e4) {
2361
2547
  return Math.max(500, Math.min(maxMs, timeoutMs ?? fallback));
@@ -3053,7 +3239,19 @@ function requiresInspectableBrowserNavigationRedirects(ssrfPolicy) {
3053
3239
 
3054
3240
  // src/actions/interaction.ts
3055
3241
  var MAX_CLICK_DELAY_MS = 5e3;
3056
- var CHECKABLE_ROLES = /* @__PURE__ */ new Set(["menuitemcheckbox", "menuitemradio", "checkbox", "switch"]);
3242
+ var DEFAULT_SCROLL_TIMEOUT_MS = 2e4;
3243
+ var CHECKABLE_ROLES = /* @__PURE__ */ new Set(["menuitemcheckbox", "menuitemradio", "checkbox", "radio", "switch"]);
3244
+ async function setCheckedViaEvaluate(locator, checked) {
3245
+ await locator.evaluate((el, desired) => {
3246
+ const input = el;
3247
+ const desc = Object.getOwnPropertyDescriptor(HTMLInputElement.prototype, "checked");
3248
+ if (desc?.set) desc.set.call(input, desired);
3249
+ else input.checked = desired;
3250
+ input.dispatchEvent(new Event("input", { bubbles: true }));
3251
+ input.dispatchEvent(new Event("change", { bubbles: true }));
3252
+ input.click();
3253
+ }, checked);
3254
+ }
3057
3255
  function resolveLocator(page, resolved) {
3058
3256
  if (resolved.ref !== void 0 && resolved.ref !== "") return refLocator(page, resolved.ref);
3059
3257
  const sel = resolved.selector ?? "";
@@ -3067,11 +3265,29 @@ async function mouseClickViaPlaywright(opts) {
3067
3265
  delay: opts.delayMs
3068
3266
  });
3069
3267
  }
3268
+ async function pressAndHoldViaCdp(opts) {
3269
+ const page = await getPageForTargetId({ cdpUrl: opts.cdpUrl, targetId: opts.targetId });
3270
+ ensurePageState(page);
3271
+ const { x, y } = opts;
3272
+ await withPageScopedCdpClient({
3273
+ cdpUrl: opts.cdpUrl,
3274
+ page,
3275
+ targetId: opts.targetId,
3276
+ fn: async (send) => {
3277
+ await send("Input.dispatchMouseEvent", { type: "mouseMoved", x, y, button: "none" });
3278
+ if (opts.delay !== void 0 && opts.delay !== 0) await new Promise((r) => setTimeout(r, opts.delay));
3279
+ await send("Input.dispatchMouseEvent", { type: "mousePressed", x, y, button: "left", clickCount: 1 });
3280
+ if (opts.holdMs !== void 0 && opts.holdMs !== 0) await new Promise((r) => setTimeout(r, opts.holdMs));
3281
+ await send("Input.dispatchMouseEvent", { type: "mouseReleased", x, y, button: "left", clickCount: 1 });
3282
+ }
3283
+ });
3284
+ }
3070
3285
  async function clickByTextViaPlaywright(opts) {
3071
3286
  const page = await getRestoredPageForTarget(opts);
3072
3287
  const timeout = resolveInteractionTimeoutMs(opts.timeoutMs);
3288
+ const locator = page.getByText(opts.text, { exact: opts.exact }).and(page.locator(":visible")).or(page.getByTitle(opts.text, { exact: opts.exact })).first();
3073
3289
  try {
3074
- await page.getByText(opts.text, { exact: opts.exact }).click({ timeout, button: opts.button, modifiers: opts.modifiers });
3290
+ await locator.click({ timeout, button: opts.button, modifiers: opts.modifiers });
3075
3291
  } catch (err) {
3076
3292
  throw toAIFriendlyError(err, `text="${opts.text}"`);
3077
3293
  }
@@ -3079,13 +3295,12 @@ async function clickByTextViaPlaywright(opts) {
3079
3295
  async function clickByRoleViaPlaywright(opts) {
3080
3296
  const page = await getRestoredPageForTarget(opts);
3081
3297
  const timeout = resolveInteractionTimeoutMs(opts.timeoutMs);
3298
+ const label = `role=${opts.role}${opts.name !== void 0 && opts.name !== "" ? ` name="${opts.name}"` : ""}`;
3299
+ const locator = page.getByRole(opts.role, { name: opts.name }).nth(opts.index ?? 0);
3082
3300
  try {
3083
- await page.getByRole(opts.role, { name: opts.name }).click({ timeout, button: opts.button, modifiers: opts.modifiers });
3301
+ await locator.click({ timeout, button: opts.button, modifiers: opts.modifiers });
3084
3302
  } catch (err) {
3085
- throw toAIFriendlyError(
3086
- err,
3087
- `role=${opts.role}${opts.name !== void 0 && opts.name !== "" ? ` name="${opts.name}"` : ""}`
3088
- );
3303
+ throw toAIFriendlyError(err, label);
3089
3304
  }
3090
3305
  }
3091
3306
  async function clickViaPlaywright(opts) {
@@ -3106,7 +3321,7 @@ async function clickViaPlaywright(opts) {
3106
3321
  try {
3107
3322
  const delayMs = resolveBoundedDelayMs(opts.delayMs, "click delayMs", MAX_CLICK_DELAY_MS);
3108
3323
  if (delayMs > 0) {
3109
- await locator.hover({ timeout });
3324
+ await locator.hover({ timeout, force: opts.force });
3110
3325
  await new Promise((resolve2) => setTimeout(resolve2, delayMs));
3111
3326
  }
3112
3327
  let ariaCheckedBefore;
@@ -3114,9 +3329,9 @@ async function clickViaPlaywright(opts) {
3114
3329
  ariaCheckedBefore = await locator.getAttribute("aria-checked", { timeout }).catch(() => void 0);
3115
3330
  }
3116
3331
  if (opts.doubleClick === true) {
3117
- await locator.dblclick({ timeout, button: opts.button, modifiers: opts.modifiers });
3332
+ await locator.dblclick({ timeout, button: opts.button, modifiers: opts.modifiers, force: opts.force });
3118
3333
  } else {
3119
- await locator.click({ timeout, button: opts.button, modifiers: opts.modifiers });
3334
+ await locator.click({ timeout, button: opts.button, modifiers: opts.modifiers, force: opts.force });
3120
3335
  }
3121
3336
  if (checkableRole && opts.doubleClick !== true && ariaCheckedBefore !== void 0) {
3122
3337
  const POLL_INTERVAL_MS = 50;
@@ -3210,9 +3425,13 @@ async function fillFormViaPlaywright(opts) {
3210
3425
  if (type === "checkbox" || type === "radio") {
3211
3426
  const checked = rawValue === true || rawValue === 1 || rawValue === "1" || rawValue === "true";
3212
3427
  try {
3213
- await locator.setChecked(checked, { timeout });
3214
- } catch (err) {
3215
- throw toAIFriendlyError(err, ref);
3428
+ await locator.setChecked(checked, { timeout, force: true });
3429
+ } catch {
3430
+ try {
3431
+ await setCheckedViaEvaluate(locator, checked);
3432
+ } catch (err) {
3433
+ throw toAIFriendlyError(err, ref);
3434
+ }
3216
3435
  }
3217
3436
  continue;
3218
3437
  }
@@ -3229,7 +3448,13 @@ async function scrollIntoViewViaPlaywright(opts) {
3229
3448
  const label = resolved.ref ?? resolved.selector ?? "";
3230
3449
  const locator = resolveLocator(page, resolved);
3231
3450
  try {
3232
- await locator.scrollIntoViewIfNeeded({ timeout: normalizeTimeoutMs(opts.timeoutMs, 2e4) });
3451
+ await locator.waitFor({
3452
+ state: "attached",
3453
+ timeout: normalizeTimeoutMs(opts.timeoutMs, DEFAULT_SCROLL_TIMEOUT_MS)
3454
+ });
3455
+ await locator.evaluate((el) => {
3456
+ el.scrollIntoView({ block: "center", behavior: "instant" });
3457
+ });
3233
3458
  } catch (err) {
3234
3459
  throw toAIFriendlyError(err, label);
3235
3460
  }
@@ -3359,6 +3584,82 @@ function isRetryableNavigateError(err) {
3359
3584
  const msg = typeof err === "string" ? err.toLowerCase() : err instanceof Error ? err.message.toLowerCase() : "";
3360
3585
  return msg.includes("frame has been detached") || msg.includes("target page, context or browser has been closed");
3361
3586
  }
3587
+ function isPolicyDenyNavigationError(err) {
3588
+ return err instanceof InvalidBrowserNavigationUrlError;
3589
+ }
3590
+ function isTopLevelNavigationRequest(page, request) {
3591
+ if (!request.isNavigationRequest()) return false;
3592
+ try {
3593
+ return request.frame() === page.mainFrame();
3594
+ } catch {
3595
+ return true;
3596
+ }
3597
+ }
3598
+ async function closeBlockedNavigationTarget(opts) {
3599
+ markPageRefBlocked(opts.cdpUrl, opts.page);
3600
+ const resolvedTargetId = await pageTargetId(opts.page).catch(() => null);
3601
+ const fallbackTargetId = opts.targetId?.trim() ?? "";
3602
+ const targetIdToBlock = resolvedTargetId ?? fallbackTargetId;
3603
+ if (targetIdToBlock) markTargetBlocked(opts.cdpUrl, targetIdToBlock);
3604
+ await opts.page.close().catch((e) => {
3605
+ console.warn("[browserclaw] failed to close blocked page", e);
3606
+ });
3607
+ }
3608
+ async function assertPageNavigationCompletedSafely(opts) {
3609
+ const navigationPolicy = withBrowserNavigationPolicy(opts.ssrfPolicy);
3610
+ try {
3611
+ await assertBrowserNavigationRedirectChainAllowed({ request: opts.response?.request(), ...navigationPolicy });
3612
+ await assertBrowserNavigationResultAllowed({ url: opts.page.url(), ...navigationPolicy });
3613
+ } catch (err) {
3614
+ if (isPolicyDenyNavigationError(err))
3615
+ await closeBlockedNavigationTarget({ cdpUrl: opts.cdpUrl, page: opts.page, targetId: opts.targetId });
3616
+ throw err;
3617
+ }
3618
+ }
3619
+ async function gotoPageWithNavigationGuard(opts) {
3620
+ const navigationPolicy = withBrowserNavigationPolicy(opts.ssrfPolicy);
3621
+ const state = { blocked: null };
3622
+ const handler = async (route, request) => {
3623
+ if (state.blocked !== null) {
3624
+ await route.abort().catch((e) => {
3625
+ console.warn("[browserclaw] route abort failed", e);
3626
+ });
3627
+ return;
3628
+ }
3629
+ if (!isTopLevelNavigationRequest(opts.page, request)) {
3630
+ await route.continue();
3631
+ return;
3632
+ }
3633
+ try {
3634
+ await assertBrowserNavigationAllowed({ url: request.url(), ...navigationPolicy });
3635
+ } catch (err) {
3636
+ if (isPolicyDenyNavigationError(err)) {
3637
+ state.blocked = err;
3638
+ await route.abort().catch((e) => {
3639
+ console.warn("[browserclaw] route abort failed", e);
3640
+ });
3641
+ return;
3642
+ }
3643
+ throw err;
3644
+ }
3645
+ await route.continue();
3646
+ };
3647
+ await opts.page.route("**", handler);
3648
+ try {
3649
+ const response = await opts.page.goto(opts.url, { timeout: opts.timeoutMs });
3650
+ if (state.blocked !== null) throw state.blocked;
3651
+ return response;
3652
+ } catch (err) {
3653
+ if (state.blocked !== null) throw state.blocked;
3654
+ throw err;
3655
+ } finally {
3656
+ await opts.page.unroute("**", handler).catch((e) => {
3657
+ console.warn("[browserclaw] route unroute failed", e);
3658
+ });
3659
+ if (state.blocked !== null)
3660
+ await closeBlockedNavigationTarget({ cdpUrl: opts.cdpUrl, page: opts.page, targetId: opts.targetId });
3661
+ }
3662
+ }
3362
3663
  async function navigateViaPlaywright(opts) {
3363
3664
  const url = opts.url.trim();
3364
3665
  if (!url) throw new Error("url is required");
@@ -3367,7 +3668,14 @@ async function navigateViaPlaywright(opts) {
3367
3668
  const timeout = Math.max(1e3, Math.min(12e4, opts.timeoutMs ?? 2e4));
3368
3669
  let page = await getPageForTargetId({ cdpUrl: opts.cdpUrl, targetId: opts.targetId });
3369
3670
  ensurePageState(page);
3370
- const navigate = async () => await page.goto(url, { timeout });
3671
+ const navigate = async () => await gotoPageWithNavigationGuard({
3672
+ cdpUrl: opts.cdpUrl,
3673
+ page,
3674
+ url,
3675
+ timeoutMs: timeout,
3676
+ ssrfPolicy: policy,
3677
+ targetId: opts.targetId
3678
+ });
3371
3679
  let response;
3372
3680
  try {
3373
3681
  response = await navigate();
@@ -3381,21 +3689,23 @@ async function navigateViaPlaywright(opts) {
3381
3689
  ensurePageState(page);
3382
3690
  response = await navigate();
3383
3691
  }
3384
- await assertBrowserNavigationRedirectChainAllowed({
3385
- request: response?.request(),
3386
- ...withBrowserNavigationPolicy(policy)
3692
+ await assertPageNavigationCompletedSafely({
3693
+ cdpUrl: opts.cdpUrl,
3694
+ page,
3695
+ response,
3696
+ ssrfPolicy: policy,
3697
+ targetId: opts.targetId
3387
3698
  });
3388
- const finalUrl = page.url();
3389
- await assertBrowserNavigationResultAllowed({ url: finalUrl, ...withBrowserNavigationPolicy(policy) });
3390
- return { url: finalUrl };
3699
+ return { url: page.url() };
3391
3700
  }
3392
3701
  async function listPagesViaPlaywright(opts) {
3393
3702
  const { browser } = await connectBrowser(opts.cdpUrl);
3394
3703
  const pages = getAllPages(browser);
3395
3704
  const results = [];
3396
3705
  for (const page of pages) {
3706
+ if (isBlockedPageRef(opts.cdpUrl, page)) continue;
3397
3707
  const tid = await pageTargetId(page).catch(() => null);
3398
- if (tid !== null && tid !== "")
3708
+ if (tid !== null && tid !== "" && !isBlockedTarget(opts.cdpUrl, tid))
3399
3709
  results.push({
3400
3710
  targetId: tid,
3401
3711
  title: await page.title().catch(() => ""),
@@ -3411,18 +3721,35 @@ async function createPageViaPlaywright(opts) {
3411
3721
  ensureContextState(context);
3412
3722
  const page = await context.newPage();
3413
3723
  ensurePageState(page);
3724
+ clearBlockedPageRef(opts.cdpUrl, page);
3725
+ const createdTargetId = await pageTargetId(page).catch(() => null);
3726
+ clearBlockedTarget(opts.cdpUrl, createdTargetId ?? void 0);
3414
3727
  const targetUrl = (opts.url ?? "").trim() || "about:blank";
3415
3728
  const policy = opts.allowInternal === true ? { ...opts.ssrfPolicy, dangerouslyAllowPrivateNetwork: true } : opts.ssrfPolicy;
3416
3729
  if (targetUrl !== "about:blank") {
3417
- const navigationPolicy = withBrowserNavigationPolicy(policy);
3418
- await assertBrowserNavigationAllowed({ url: targetUrl, ...navigationPolicy });
3419
- await assertBrowserNavigationRedirectChainAllowed({
3420
- request: (await page.goto(targetUrl, { timeout: 3e4 }).catch(() => null))?.request(),
3421
- ...navigationPolicy
3730
+ await assertBrowserNavigationAllowed({ url: targetUrl, ...withBrowserNavigationPolicy(policy) });
3731
+ let response = null;
3732
+ try {
3733
+ response = await gotoPageWithNavigationGuard({
3734
+ cdpUrl: opts.cdpUrl,
3735
+ page,
3736
+ url: targetUrl,
3737
+ timeoutMs: 3e4,
3738
+ ssrfPolicy: policy,
3739
+ targetId: createdTargetId ?? void 0
3740
+ });
3741
+ } catch (err) {
3742
+ if (isPolicyDenyNavigationError(err) || err instanceof BlockedBrowserTargetError) throw err;
3743
+ }
3744
+ await assertPageNavigationCompletedSafely({
3745
+ cdpUrl: opts.cdpUrl,
3746
+ page,
3747
+ response,
3748
+ ssrfPolicy: policy,
3749
+ targetId: createdTargetId ?? void 0
3422
3750
  });
3423
- await assertBrowserNavigationResultAllowed({ url: page.url(), ...navigationPolicy });
3424
3751
  }
3425
- const tid = await pageTargetId(page).catch(() => null);
3752
+ const tid = createdTargetId ?? await pageTargetId(page).catch(() => null);
3426
3753
  if (tid === null || tid === "") throw new Error("Failed to get targetId for new page");
3427
3754
  return {
3428
3755
  targetId: tid,
@@ -3437,7 +3764,12 @@ async function closePageViaPlaywright(opts) {
3437
3764
  await page.close();
3438
3765
  }
3439
3766
  async function closePageByTargetIdViaPlaywright(opts) {
3440
- await (await resolvePageByTargetIdOrThrow(opts)).close();
3767
+ try {
3768
+ await (await resolvePageByTargetIdOrThrow(opts)).close();
3769
+ } catch (err) {
3770
+ if (err instanceof BrowserTabNotFoundError) return;
3771
+ throw err;
3772
+ }
3441
3773
  }
3442
3774
  async function focusPageByTargetIdViaPlaywright(opts) {
3443
3775
  const page = await resolvePageByTargetIdOrThrow(opts);
@@ -3459,6 +3791,27 @@ async function focusPageByTargetIdViaPlaywright(opts) {
3459
3791
  }
3460
3792
  }
3461
3793
  }
3794
+ async function waitForTabViaPlaywright(opts) {
3795
+ if (opts.urlContains === void 0 && opts.titleContains === void 0)
3796
+ throw new Error("urlContains or titleContains is required");
3797
+ const timeout = Math.max(1e3, Math.min(12e4, opts.timeoutMs ?? 3e4));
3798
+ const start = Date.now();
3799
+ const POLL_INTERVAL_MS = 250;
3800
+ while (Date.now() - start < timeout) {
3801
+ const tabs = await listPagesViaPlaywright({ cdpUrl: opts.cdpUrl });
3802
+ const match = tabs.find((t) => {
3803
+ if (opts.urlContains !== void 0 && !t.url.includes(opts.urlContains)) return false;
3804
+ if (opts.titleContains !== void 0 && !t.title.includes(opts.titleContains)) return false;
3805
+ return true;
3806
+ });
3807
+ if (match) return match;
3808
+ await new Promise((resolve2) => setTimeout(resolve2, POLL_INTERVAL_MS));
3809
+ }
3810
+ const criteria = [];
3811
+ if (opts.urlContains !== void 0) criteria.push(`url contains "${opts.urlContains}"`);
3812
+ if (opts.titleContains !== void 0) criteria.push(`title contains "${opts.titleContains}"`);
3813
+ throw new Error(`Timed out waiting for tab: ${criteria.join(", ")}`);
3814
+ }
3462
3815
  async function resizeViewportViaPlaywright(opts) {
3463
3816
  const page = await getPageForTargetId({ cdpUrl: opts.cdpUrl, targetId: opts.targetId });
3464
3817
  ensurePageState(page);
@@ -3500,9 +3853,13 @@ async function waitForViaPlaywright(opts) {
3500
3853
  if (opts.loadState !== void 0) {
3501
3854
  await page.waitForLoadState(opts.loadState, { timeout });
3502
3855
  }
3503
- if (opts.fn !== void 0 && opts.fn !== "") {
3504
- const fn = opts.fn.trim();
3505
- if (fn !== "") await page.waitForFunction(fn, void 0, { timeout });
3856
+ if (opts.fn !== void 0) {
3857
+ if (typeof opts.fn === "function") {
3858
+ await page.waitForFunction(opts.fn, opts.arg, { timeout });
3859
+ } else {
3860
+ const fn = opts.fn.trim();
3861
+ if (fn !== "") await page.waitForFunction(fn, opts.arg, { timeout });
3862
+ }
3506
3863
  }
3507
3864
  }
3508
3865
 
@@ -3614,6 +3971,7 @@ async function executeSingleAction(action, cdpUrl, targetId, evaluateEnabled, de
3614
3971
  url: action.url,
3615
3972
  loadState: action.loadState,
3616
3973
  fn: action.fn,
3974
+ arg: action.arg,
3617
3975
  timeoutMs: action.timeoutMs
3618
3976
  });
3619
3977
  break;
@@ -4089,6 +4447,40 @@ async function responseBodyViaPlaywright(opts) {
4089
4447
  truncated
4090
4448
  };
4091
4449
  }
4450
+ async function waitForRequestViaPlaywright(opts) {
4451
+ const page = await getPageForTargetId({ cdpUrl: opts.cdpUrl, targetId: opts.targetId });
4452
+ ensurePageState(page);
4453
+ const timeout = normalizeTimeoutMs(opts.timeoutMs, 3e4, 12e4);
4454
+ const pattern = opts.url.trim();
4455
+ if (!pattern) throw new Error("url is required");
4456
+ const upperMethod = opts.method !== void 0 ? opts.method.toUpperCase() : void 0;
4457
+ const response = await page.waitForResponse(
4458
+ (resp) => matchUrlPattern(pattern, resp.url()) && (upperMethod === void 0 || resp.request().method() === upperMethod),
4459
+ { timeout }
4460
+ );
4461
+ const request = response.request();
4462
+ let responseBody;
4463
+ let truncated = false;
4464
+ try {
4465
+ responseBody = await response.text();
4466
+ const maxChars = typeof opts.maxChars === "number" && Number.isFinite(opts.maxChars) ? Math.max(1, Math.min(5e6, Math.floor(opts.maxChars))) : 2e5;
4467
+ if (responseBody.length > maxChars) {
4468
+ responseBody = responseBody.slice(0, maxChars);
4469
+ truncated = true;
4470
+ }
4471
+ } catch (err) {
4472
+ console.warn("[browserclaw] response body unavailable:", err instanceof Error ? err.message : String(err));
4473
+ }
4474
+ return {
4475
+ url: response.url(),
4476
+ method: request.method(),
4477
+ postData: request.postData() ?? void 0,
4478
+ status: response.status(),
4479
+ ok: response.ok(),
4480
+ responseBody,
4481
+ truncated
4482
+ };
4483
+ }
4092
4484
 
4093
4485
  // src/capture/screenshot.ts
4094
4486
  async function takeScreenshotViaPlaywright(opts) {
@@ -4212,6 +4604,13 @@ async function traceStopViaPlaywright(opts) {
4212
4604
  }
4213
4605
 
4214
4606
  // src/snapshot/ref-map.ts
4607
+ function parseStateFromSuffix(suffix) {
4608
+ const state = {};
4609
+ if (/\[disabled\]/i.test(suffix)) state.disabled = true;
4610
+ if (/\[checked\s*=\s*"?mixed"?\]/i.test(suffix)) state.checked = "mixed";
4611
+ else if (/\[checked\]/i.test(suffix)) state.checked = true;
4612
+ return state;
4613
+ }
4215
4614
  var INTERACTIVE_ROLES = /* @__PURE__ */ new Set([
4216
4615
  "button",
4217
4616
  "link",
@@ -4370,7 +4769,8 @@ function buildRoleSnapshotFromAriaSnapshot(ariaSnapshot, options = {}) {
4370
4769
  const ref = nextRef();
4371
4770
  const nth = tracker.getNextIndex(role, name);
4372
4771
  tracker.trackRef(role, name, ref);
4373
- refs[ref] = { role, name, nth };
4772
+ const state = parseStateFromSuffix(suffix);
4773
+ refs[ref] = { role, name, nth, ...state };
4374
4774
  let enhanced = `${prefix}${roleRaw}`;
4375
4775
  if (name !== void 0 && name !== "") enhanced += ` "${name}"`;
4376
4776
  enhanced += ` [ref=${ref}]`;
@@ -4407,7 +4807,8 @@ function buildRoleSnapshotFromAriaSnapshot(ariaSnapshot, options = {}) {
4407
4807
  const ref = nextRef();
4408
4808
  const nth = tracker.getNextIndex(role, name);
4409
4809
  tracker.trackRef(role, name, ref);
4410
- refs[ref] = { role, name, nth };
4810
+ const state = parseStateFromSuffix(suffix);
4811
+ refs[ref] = { role, name, nth, ...state };
4411
4812
  let enhanced = `${prefix}${roleRaw}`;
4412
4813
  if (name !== "") enhanced += ` "${name}"`;
4413
4814
  enhanced += ` [ref=${ref}]`;
@@ -4445,12 +4846,13 @@ function buildRoleSnapshotFromAiSnapshot(aiSnapshot, options = {}) {
4445
4846
  if (!INTERACTIVE_ROLES.has(role)) continue;
4446
4847
  const ref = parseAiSnapshotRef(suffix);
4447
4848
  const prefix = /^(\s*-\s*)/.exec(line)?.[1] ?? "";
4849
+ const state = parseStateFromSuffix(suffix);
4448
4850
  if (ref !== null) {
4449
- refs[ref] = { role, ...name !== void 0 && name !== "" ? { name } : {} };
4851
+ refs[ref] = { role, ...name !== void 0 && name !== "" ? { name } : {}, ...state };
4450
4852
  out2.push(`${prefix}${roleRaw}${name !== void 0 && name !== "" ? ` "${name}"` : ""}${suffix}`);
4451
4853
  } else {
4452
4854
  const generatedRef = nextInteractiveRef();
4453
- refs[generatedRef] = { role, ...name !== void 0 && name !== "" ? { name } : {} };
4855
+ refs[generatedRef] = { role, ...name !== void 0 && name !== "" ? { name } : {}, ...state };
4454
4856
  let enhanced = `${prefix}${roleRaw}`;
4455
4857
  if (name !== void 0 && name !== "") enhanced += ` "${name}"`;
4456
4858
  enhanced += ` [ref=${generatedRef}]`;
@@ -4488,12 +4890,13 @@ function buildRoleSnapshotFromAiSnapshot(aiSnapshot, options = {}) {
4488
4890
  const isStructural = STRUCTURAL_ROLES.has(role);
4489
4891
  if (options.compact === true && isStructural && name === "") continue;
4490
4892
  const ref = parseAiSnapshotRef(suffix);
4893
+ const state = parseStateFromSuffix(suffix);
4491
4894
  if (ref !== null) {
4492
- refs[ref] = { role, ...name !== "" ? { name } : {} };
4895
+ refs[ref] = { role, ...name !== "" ? { name } : {}, ...state };
4493
4896
  out.push(line);
4494
4897
  } else if (INTERACTIVE_ROLES.has(role)) {
4495
4898
  const generatedRef = nextGeneratedRef();
4496
- refs[generatedRef] = { role, ...name !== "" ? { name } : {} };
4899
+ refs[generatedRef] = { role, ...name !== "" ? { name } : {}, ...state };
4497
4900
  let enhanced = `${prefix}${roleRaw}`;
4498
4901
  if (name !== "") enhanced += ` "${name}"`;
4499
4902
  enhanced += ` [ref=${generatedRef}]`;
@@ -4859,7 +5262,8 @@ var CrawlPage = class {
4859
5262
  button: opts?.button,
4860
5263
  modifiers: opts?.modifiers,
4861
5264
  delayMs: opts?.delayMs,
4862
- timeoutMs: opts?.timeoutMs
5265
+ timeoutMs: opts?.timeoutMs,
5266
+ force: opts?.force
4863
5267
  });
4864
5268
  }
4865
5269
  /**
@@ -4885,7 +5289,8 @@ var CrawlPage = class {
4885
5289
  button: opts?.button,
4886
5290
  modifiers: opts?.modifiers,
4887
5291
  delayMs: opts?.delayMs,
4888
- timeoutMs: opts?.timeoutMs
5292
+ timeoutMs: opts?.timeoutMs,
5293
+ force: opts?.force
4889
5294
  });
4890
5295
  }
4891
5296
  /**
@@ -4915,6 +5320,32 @@ var CrawlPage = class {
4915
5320
  delayMs: opts?.delayMs
4916
5321
  });
4917
5322
  }
5323
+ /**
5324
+ * Press and hold at page coordinates using raw CDP events.
5325
+ *
5326
+ * Bypasses Playwright's automation layer by dispatching CDP
5327
+ * `Input.dispatchMouseEvent` directly — useful for anti-bot challenges
5328
+ * that detect automated clicks.
5329
+ *
5330
+ * @param x - X coordinate in CSS pixels
5331
+ * @param y - Y coordinate in CSS pixels
5332
+ * @param opts - Options (delay: ms before press, holdMs: hold duration)
5333
+ *
5334
+ * @example
5335
+ * ```ts
5336
+ * await page.pressAndHold(400, 300, { delay: 150, holdMs: 5000 });
5337
+ * ```
5338
+ */
5339
+ async pressAndHold(x, y, opts) {
5340
+ return pressAndHoldViaCdp({
5341
+ cdpUrl: this.cdpUrl,
5342
+ targetId: this.targetId,
5343
+ x,
5344
+ y,
5345
+ delay: opts?.delay,
5346
+ holdMs: opts?.holdMs
5347
+ });
5348
+ }
4918
5349
  /**
4919
5350
  * Click an element by its visible text content (no snapshot/ref needed).
4920
5351
  *
@@ -4962,6 +5393,7 @@ var CrawlPage = class {
4962
5393
  targetId: this.targetId,
4963
5394
  role,
4964
5395
  name,
5396
+ index: opts?.index,
4965
5397
  button: opts?.button,
4966
5398
  modifiers: opts?.modifiers,
4967
5399
  timeoutMs: opts?.timeoutMs
@@ -5485,6 +5917,35 @@ var CrawlPage = class {
5485
5917
  maxChars: opts?.maxChars
5486
5918
  });
5487
5919
  }
5920
+ /**
5921
+ * Wait for a network request matching a URL pattern and return request + response details.
5922
+ *
5923
+ * Unlike `networkRequests()` which only captures metadata, this method captures
5924
+ * the full request body (POST data) and response body.
5925
+ *
5926
+ * @param url - URL string or pattern to match (supports `*` wildcards and substring matching)
5927
+ * @param opts - Options (method filter, timeoutMs, maxChars for response body)
5928
+ * @returns Request method, postData, response status, and response body
5929
+ *
5930
+ * @example
5931
+ * ```ts
5932
+ * const reqPromise = page.waitForRequest('/api/submit', { method: 'POST' });
5933
+ * await page.click('e5'); // submit a form
5934
+ * const req = await reqPromise;
5935
+ * console.log(req.postData); // form body
5936
+ * console.log(req.status, req.responseBody); // response
5937
+ * ```
5938
+ */
5939
+ async waitForRequest(url, opts) {
5940
+ return waitForRequestViaPlaywright({
5941
+ cdpUrl: this.cdpUrl,
5942
+ targetId: this.targetId,
5943
+ url,
5944
+ method: opts?.method,
5945
+ timeoutMs: opts?.timeoutMs,
5946
+ maxChars: opts?.maxChars
5947
+ });
5948
+ }
5488
5949
  /**
5489
5950
  * Get console messages captured from the page.
5490
5951
  *
@@ -5915,14 +6376,15 @@ var BrowserClaw = class _BrowserClaw {
5915
6376
  *
5916
6377
  * @example
5917
6378
  * ```ts
5918
- * // Default: visible Chrome window
5919
- * const browser = await BrowserClaw.launch();
6379
+ * // Launch and navigate to a URL
6380
+ * const browser = await BrowserClaw.launch({ url: 'https://example.com' });
5920
6381
  *
5921
6382
  * // Headless mode
5922
- * const browser = await BrowserClaw.launch({ headless: true });
6383
+ * const browser = await BrowserClaw.launch({ url: 'https://example.com', headless: true });
5923
6384
  *
5924
6385
  * // Specific browser
5925
6386
  * const browser = await BrowserClaw.launch({
6387
+ * url: 'https://example.com',
5926
6388
  * executablePath: '/usr/bin/google-chrome',
5927
6389
  * });
5928
6390
  * ```
@@ -5931,7 +6393,12 @@ var BrowserClaw = class _BrowserClaw {
5931
6393
  const chrome = await launchChrome(opts);
5932
6394
  const cdpUrl = `http://127.0.0.1:${String(chrome.cdpPort)}`;
5933
6395
  const ssrfPolicy = opts.allowInternal === true ? { ...opts.ssrfPolicy, dangerouslyAllowPrivateNetwork: true } : opts.ssrfPolicy;
5934
- return new _BrowserClaw(cdpUrl, chrome, ssrfPolicy, opts.recordVideo);
6396
+ const browser = new _BrowserClaw(cdpUrl, chrome, ssrfPolicy, opts.recordVideo);
6397
+ if (opts.url !== void 0 && opts.url !== "") {
6398
+ const page = await browser.currentPage();
6399
+ await page.goto(opts.url);
6400
+ }
6401
+ return browser;
5935
6402
  }
5936
6403
  /**
5937
6404
  * Connect to an already-running Chrome instance via its CDP endpoint.
@@ -6007,6 +6474,31 @@ var BrowserClaw = class _BrowserClaw {
6007
6474
  async tabs() {
6008
6475
  return listPagesViaPlaywright({ cdpUrl: this.cdpUrl });
6009
6476
  }
6477
+ /**
6478
+ * Wait for a tab matching the given criteria and return a page handle.
6479
+ *
6480
+ * Polls open tabs until one matches, then focuses it and returns a CrawlPage.
6481
+ *
6482
+ * @param opts - Match criteria (urlContains, titleContains) and timeout
6483
+ * @returns A CrawlPage for the matched tab
6484
+ *
6485
+ * @example
6486
+ * ```ts
6487
+ * await page.click('e5'); // opens a new tab
6488
+ * const appPage = await browser.waitForTab({ urlContains: 'app-web' });
6489
+ * const { snapshot } = await appPage.snapshot();
6490
+ * ```
6491
+ */
6492
+ async waitForTab(opts) {
6493
+ const tab = await waitForTabViaPlaywright({
6494
+ cdpUrl: this.cdpUrl,
6495
+ urlContains: opts.urlContains,
6496
+ titleContains: opts.titleContains,
6497
+ timeoutMs: opts.timeoutMs
6498
+ });
6499
+ await focusPageByTargetIdViaPlaywright({ cdpUrl: this.cdpUrl, targetId: tab.targetId });
6500
+ return new CrawlPage(this.cdpUrl, tab.targetId, this.ssrfPolicy);
6501
+ }
6010
6502
  /**
6011
6503
  * Bring a tab to the foreground.
6012
6504
  *
@@ -6055,6 +6547,6 @@ var BrowserClaw = class _BrowserClaw {
6055
6547
  }
6056
6548
  };
6057
6549
 
6058
- export { BrowserClaw, BrowserTabNotFoundError, CrawlPage, InvalidBrowserNavigationUrlError, STEALTH_SCRIPT, assertBrowserNavigationAllowed, assertBrowserNavigationRedirectChainAllowed, assertBrowserNavigationResultAllowed, assertSafeUploadPaths, batchViaPlaywright, createPinnedLookup, detectChallengeViaPlaywright, ensureContextState, executeSingleAction, forceDisconnectPlaywrightForTarget, getChromeWebSocketUrl, getRestoredPageForTarget, isChromeCdpReady, isChromeReachable, normalizeCdpHttpBaseForJsonEndpoints, parseRoleRef, requireRef, requireRefOrSelector, requiresInspectableBrowserNavigationRedirects, resolveBoundedDelayMs, resolveInteractionTimeoutMs, resolvePageByTargetIdOrThrow, resolvePinnedHostnameWithPolicy, resolveStrictExistingUploadPaths, sanitizeUntrustedFileName, setDialogHandler, waitForChallengeViaPlaywright, withBrowserNavigationPolicy, withPageScopedCdpClient, withPlaywrightPageCdpSession, writeViaSiblingTempPath };
6550
+ export { BrowserClaw, BrowserTabNotFoundError, CrawlPage, InvalidBrowserNavigationUrlError, STEALTH_SCRIPT, assertBrowserNavigationAllowed, assertBrowserNavigationRedirectChainAllowed, assertBrowserNavigationResultAllowed, assertSafeUploadPaths, batchViaPlaywright, createPinnedLookup, detectChallengeViaPlaywright, ensureContextState, executeSingleAction, forceDisconnectPlaywrightForTarget, getChromeWebSocketUrl, getRestoredPageForTarget, isChromeCdpReady, isChromeReachable, normalizeCdpHttpBaseForJsonEndpoints, parseRoleRef, pressAndHoldViaCdp, requireRef, requireRefOrSelector, requiresInspectableBrowserNavigationRedirects, resolveBoundedDelayMs, resolveInteractionTimeoutMs, resolvePageByTargetIdOrThrow, resolvePinnedHostnameWithPolicy, resolveStrictExistingUploadPaths, sanitizeUntrustedFileName, setDialogHandler, waitForChallengeViaPlaywright, withBrowserNavigationPolicy, withPageScopedCdpClient, withPlaywrightPageCdpSession, writeViaSiblingTempPath };
6059
6551
  //# sourceMappingURL=index.js.map
6060
6552
  //# sourceMappingURL=index.js.map