browserclaw 0.5.1 → 0.5.2

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.d.cts CHANGED
@@ -227,6 +227,8 @@ interface ClickOptions {
227
227
  button?: 'left' | 'right' | 'middle';
228
228
  /** Modifier keys to hold during click */
229
229
  modifiers?: ('Alt' | 'Control' | 'ControlOrMeta' | 'Meta' | 'Shift')[];
230
+ /** Delay in ms between hover and click (hovers first, waits, then clicks). Max: `5000` */
231
+ delayMs?: number;
230
232
  /** Timeout in milliseconds. Default: `8000` */
231
233
  timeoutMs?: number;
232
234
  }
@@ -1238,8 +1240,8 @@ declare function requiresInspectableBrowserNavigationRedirects(ssrfPolicy?: Ssrf
1238
1240
  declare function ensureContextState(context: BrowserContext): ContextState;
1239
1241
  /**
1240
1242
  * Force-disconnect a Playwright browser connection for a given CDP target.
1241
- * Clears the connection cache, sends Runtime.terminateExecution via CDP
1242
- * session to kill stuck evals, and closes the browser.
1243
+ * Clears the connection cache, sends Runtime.terminateExecution via raw CDP
1244
+ * websocket to kill stuck evals (bypassing Playwright), and closes the browser.
1243
1245
  */
1244
1246
  declare function forceDisconnectPlaywrightForTarget(opts: {
1245
1247
  cdpUrl: string;
package/dist/index.d.ts CHANGED
@@ -227,6 +227,8 @@ interface ClickOptions {
227
227
  button?: 'left' | 'right' | 'middle';
228
228
  /** Modifier keys to hold during click */
229
229
  modifiers?: ('Alt' | 'Control' | 'ControlOrMeta' | 'Meta' | 'Shift')[];
230
+ /** Delay in ms between hover and click (hovers first, waits, then clicks). Max: `5000` */
231
+ delayMs?: number;
230
232
  /** Timeout in milliseconds. Default: `8000` */
231
233
  timeoutMs?: number;
232
234
  }
@@ -1238,8 +1240,8 @@ declare function requiresInspectableBrowserNavigationRedirects(ssrfPolicy?: Ssrf
1238
1240
  declare function ensureContextState(context: BrowserContext): ContextState;
1239
1241
  /**
1240
1242
  * Force-disconnect a Playwright browser connection for a given CDP target.
1241
- * Clears the connection cache, sends Runtime.terminateExecution via CDP
1242
- * session to kill stuck evals, and closes the browser.
1243
+ * Clears the connection cache, sends Runtime.terminateExecution via raw CDP
1244
+ * websocket to kill stuck evals (bypassing Playwright), and closes the browser.
1243
1245
  */
1244
1246
  declare function forceDisconnectPlaywrightForTarget(opts: {
1245
1247
  cdpUrl: string;
package/dist/index.js CHANGED
@@ -813,6 +813,7 @@ async function connectBrowser(cdpUrl, authToken) {
813
813
  return connected;
814
814
  } catch (err) {
815
815
  lastErr = err;
816
+ if ((err instanceof Error ? err.message : String(err)).includes("rate limit")) break;
816
817
  await new Promise((r) => setTimeout(r, 250 + attempt * 250));
817
818
  }
818
819
  }
@@ -838,6 +839,55 @@ async function disconnectBrowser() {
838
839
  if (cur) await cur.browser.close().catch(() => {
839
840
  });
840
841
  }
842
+ async function tryTerminateExecutionViaCdp(cdpUrl, targetId) {
843
+ const httpBase = normalizeCdpHttpBaseForJsonEndpoints(cdpUrl);
844
+ const ctrl = new AbortController();
845
+ const t = setTimeout(() => ctrl.abort(), 2e3);
846
+ let targets;
847
+ try {
848
+ const res = await fetch(`${httpBase}/json/list`, { signal: ctrl.signal });
849
+ if (!res.ok) return;
850
+ targets = await res.json();
851
+ } catch {
852
+ return;
853
+ } finally {
854
+ clearTimeout(t);
855
+ }
856
+ if (!Array.isArray(targets)) return;
857
+ const target = targets.find((entry) => String(entry?.id ?? "").trim() === targetId);
858
+ const wsUrl = String(target?.webSocketDebuggerUrl ?? "").trim();
859
+ if (!wsUrl) return;
860
+ await new Promise((resolve2) => {
861
+ let done = false;
862
+ const finish = () => {
863
+ if (done) return;
864
+ done = true;
865
+ clearTimeout(timer);
866
+ try {
867
+ ws.close();
868
+ } catch {
869
+ }
870
+ resolve2();
871
+ };
872
+ const timer = setTimeout(finish, 2e3);
873
+ let ws;
874
+ try {
875
+ ws = new WebSocket(wsUrl);
876
+ } catch {
877
+ finish();
878
+ return;
879
+ }
880
+ ws.onopen = () => {
881
+ try {
882
+ ws.send(JSON.stringify({ id: 1, method: "Runtime.terminateExecution" }));
883
+ } catch {
884
+ }
885
+ setTimeout(finish, 300);
886
+ };
887
+ ws.onerror = () => finish();
888
+ ws.onclose = () => finish();
889
+ });
890
+ }
841
891
  async function forceDisconnectPlaywrightForTarget(opts) {
842
892
  const normalized = normalizeCdpUrl(opts.cdpUrl);
843
893
  const cur = cached;
@@ -846,23 +896,8 @@ async function forceDisconnectPlaywrightForTarget(opts) {
846
896
  connectingByUrl.delete(normalized);
847
897
  const targetId = opts.targetId?.trim() || "";
848
898
  if (targetId) {
849
- try {
850
- const pages = await getAllPages(cur.browser);
851
- for (const page of pages) {
852
- const tid = await pageTargetId(page).catch(() => null);
853
- if (tid === targetId) {
854
- const session = await page.context().newCDPSession(page);
855
- try {
856
- await session.send("Runtime.terminateExecution");
857
- } finally {
858
- await session.detach().catch(() => {
859
- });
860
- }
861
- break;
862
- }
863
- }
864
- } catch {
865
- }
899
+ await tryTerminateExecutionViaCdp(normalized, targetId).catch(() => {
900
+ });
866
901
  }
867
902
  cur.browser.close().catch(() => {
868
903
  });
@@ -1284,6 +1319,35 @@ async function snapshotRole(opts) {
1284
1319
  const page = await getPageForTargetId({ cdpUrl: opts.cdpUrl, targetId: opts.targetId });
1285
1320
  ensurePageState(page);
1286
1321
  const sourceUrl = page.url();
1322
+ if (opts.refsMode === "aria") {
1323
+ if (opts.selector?.trim() || opts.frameSelector?.trim()) {
1324
+ throw new Error("refs=aria does not support selector/frame snapshots yet.");
1325
+ }
1326
+ const maybe = page;
1327
+ if (!maybe._snapshotForAI) {
1328
+ throw new Error("refs=aria requires Playwright _snapshotForAI support.");
1329
+ }
1330
+ const result = await maybe._snapshotForAI({ timeout: 5e3, track: "response" });
1331
+ const built2 = buildRoleSnapshotFromAiSnapshot(String(result?.full ?? ""), opts.options);
1332
+ storeRoleRefsForTarget({
1333
+ page,
1334
+ cdpUrl: opts.cdpUrl,
1335
+ targetId: opts.targetId,
1336
+ refs: built2.refs,
1337
+ mode: "aria"
1338
+ });
1339
+ return {
1340
+ snapshot: built2.snapshot,
1341
+ refs: built2.refs,
1342
+ stats: getRoleSnapshotStats(built2.snapshot, built2.refs),
1343
+ untrusted: true,
1344
+ contentMeta: {
1345
+ sourceUrl,
1346
+ contentType: "browser-snapshot",
1347
+ capturedAt: (/* @__PURE__ */ new Date()).toISOString()
1348
+ }
1349
+ };
1350
+ }
1287
1351
  const frameSelector = opts.frameSelector?.trim() || "";
1288
1352
  const selector = opts.selector?.trim() || "";
1289
1353
  const locator = frameSelector ? selector ? page.frameLocator(frameSelector).locator(selector) : page.frameLocator(frameSelector).locator(":root") : selector ? page.locator(selector) : page.locator(":root");
@@ -1295,7 +1359,7 @@ async function snapshotRole(opts) {
1295
1359
  targetId: opts.targetId,
1296
1360
  refs: built.refs,
1297
1361
  frameSelector: frameSelector || void 0,
1298
- mode: opts.refsMode ?? "role"
1362
+ mode: "role"
1299
1363
  });
1300
1364
  return {
1301
1365
  snapshot: built.snapshot,
@@ -1383,7 +1447,7 @@ var InvalidBrowserNavigationUrlError = class extends Error {
1383
1447
  }
1384
1448
  };
1385
1449
  function withBrowserNavigationPolicy(ssrfPolicy) {
1386
- return { ssrfPolicy };
1450
+ return ssrfPolicy ? { ssrfPolicy } : {};
1387
1451
  }
1388
1452
  var NETWORK_NAVIGATION_PROTOCOLS = /* @__PURE__ */ new Set(["http:", "https:"]);
1389
1453
  var SAFE_NON_NETWORK_URLS = /* @__PURE__ */ new Set(["about:blank"]);
@@ -1496,6 +1560,14 @@ function extractEmbeddedIpv4FromIpv6(v6, opts) {
1496
1560
  return true;
1497
1561
  }
1498
1562
  }
1563
+ if (parts[0] === 0 && parts[1] === 0 && parts[2] === 0 && parts[3] === 0 && parts[4] === 65535 && parts[5] === 0) {
1564
+ const ip4str = `${parts[6] >> 8 & 255}.${parts[6] & 255}.${parts[7] >> 8 & 255}.${parts[7] & 255}`;
1565
+ try {
1566
+ return isBlockedSpecialUseIpv4Address(ipaddr.IPv4.parse(ip4str), opts);
1567
+ } catch {
1568
+ return true;
1569
+ }
1570
+ }
1499
1571
  if (parts[0] === 0 && parts[1] === 0 && parts[2] === 0 && parts[3] === 0 && parts[4] === 0 && parts[5] === 0) {
1500
1572
  const ip4str = `${parts[6] >> 8 & 255}.${parts[6] & 255}.${parts[7] >> 8 & 255}.${parts[7] & 255}`;
1501
1573
  try {
@@ -1812,6 +1884,13 @@ function requiresInspectableBrowserNavigationRedirects(ssrfPolicy) {
1812
1884
  }
1813
1885
 
1814
1886
  // src/actions/interaction.ts
1887
+ var MAX_CLICK_DELAY_MS = 5e3;
1888
+ function resolveBoundedDelayMs(value, label, maxMs) {
1889
+ const normalized = Math.floor(value ?? 0);
1890
+ if (!Number.isFinite(normalized) || normalized < 0) throw new Error(`${label} must be >= 0`);
1891
+ if (normalized > maxMs) throw new Error(`${label} exceeds maximum of ${maxMs}ms`);
1892
+ return normalized;
1893
+ }
1815
1894
  async function clickViaPlaywright(opts) {
1816
1895
  const page = await getPageForTargetId({ cdpUrl: opts.cdpUrl, targetId: opts.targetId });
1817
1896
  ensurePageState(page);
@@ -1819,6 +1898,11 @@ async function clickViaPlaywright(opts) {
1819
1898
  const locator = refLocator(page, opts.ref);
1820
1899
  const timeout = normalizeTimeoutMs(opts.timeoutMs, 8e3, 6e4);
1821
1900
  try {
1901
+ const delayMs = resolveBoundedDelayMs(opts.delayMs, "click delayMs", MAX_CLICK_DELAY_MS);
1902
+ if (delayMs > 0) {
1903
+ await locator.hover({ timeout });
1904
+ await new Promise((resolve2) => setTimeout(resolve2, delayMs));
1905
+ }
1822
1906
  if (opts.doubleClick) {
1823
1907
  await locator.dblclick({ timeout, button: opts.button, modifiers: opts.modifiers });
1824
1908
  } else {
@@ -2129,12 +2213,14 @@ async function resizeViewportViaPlaywright(opts) {
2129
2213
  }
2130
2214
 
2131
2215
  // src/actions/wait.ts
2216
+ var MAX_WAIT_TIME_MS = 3e4;
2132
2217
  async function waitForViaPlaywright(opts) {
2133
2218
  const page = await getPageForTargetId({ cdpUrl: opts.cdpUrl, targetId: opts.targetId });
2134
2219
  ensurePageState(page);
2135
2220
  const timeout = normalizeTimeoutMs(opts.timeoutMs, 2e4);
2136
2221
  if (typeof opts.timeMs === "number" && Number.isFinite(opts.timeMs)) {
2137
- await page.waitForTimeout(Math.max(0, opts.timeMs));
2222
+ const bounded = Math.max(0, Math.min(MAX_WAIT_TIME_MS, Math.floor(opts.timeMs)));
2223
+ await page.waitForTimeout(bounded);
2138
2224
  }
2139
2225
  if (opts.text) {
2140
2226
  await page.getByText(opts.text).first().waitFor({ state: "visible", timeout });
@@ -2823,6 +2909,7 @@ var CrawlPage = class {
2823
2909
  doubleClick: opts?.doubleClick,
2824
2910
  button: opts?.button,
2825
2911
  modifiers: opts?.modifiers,
2912
+ delayMs: opts?.delayMs,
2826
2913
  timeoutMs: opts?.timeoutMs
2827
2914
  });
2828
2915
  }