browserclaw 0.11.2 → 0.11.4

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
@@ -1319,7 +1319,10 @@ function normalizeCdpHttpBaseForJsonEndpoints(cdpUrl) {
1319
1319
  url.pathname = url.pathname.replace(/\/cdp$/, "");
1320
1320
  return url.toString().replace(/\/$/, "");
1321
1321
  } catch {
1322
- return cdpUrl.replace(/^ws:/, "http:").replace(/^wss:/, "https:").replace(/\/devtools\/browser\/.*$/, "").replace(/\/cdp$/, "").replace(/\/$/, "");
1322
+ let normalized = cdpUrl.replace(/^ws:/, "http:").replace(/^wss:/, "https:");
1323
+ const dtIdx = normalized.indexOf("/devtools/browser/");
1324
+ if (dtIdx >= 0) normalized = normalized.slice(0, dtIdx);
1325
+ return normalized.replace(/\/cdp$/, "").replace(/\/$/, "");
1323
1326
  }
1324
1327
  }
1325
1328
  function appendCdpPath(cdpUrl, cdpPath) {
@@ -1782,205 +1785,25 @@ var STEALTH_SCRIPT = `(function() {
1782
1785
  });
1783
1786
  })()`;
1784
1787
 
1785
- // src/connection.ts
1786
- var BrowserTabNotFoundError = class extends Error {
1787
- constructor(message = "Tab not found") {
1788
- super(message);
1789
- this.name = "BrowserTabNotFoundError";
1790
- }
1791
- };
1792
- async function fetchJsonForCdp(url, timeoutMs) {
1793
- const ctrl = new AbortController();
1794
- const t = setTimeout(() => {
1795
- ctrl.abort();
1796
- }, timeoutMs);
1797
- try {
1798
- const res = await fetch(url, { signal: ctrl.signal });
1799
- if (!res.ok) return null;
1800
- return await res.json();
1801
- } catch {
1802
- return null;
1803
- } finally {
1804
- clearTimeout(t);
1805
- }
1806
- }
1807
- function appendCdpPath2(cdpUrl, cdpPath) {
1808
- try {
1809
- const url = new URL(cdpUrl);
1810
- url.pathname = `${url.pathname.replace(/\/$/, "")}${cdpPath.startsWith("/") ? cdpPath : `/${cdpPath}`}`;
1811
- return url.toString();
1812
- } catch {
1813
- return `${cdpUrl.replace(/\/$/, "")}${cdpPath}`;
1814
- }
1815
- }
1816
- async function withPlaywrightPageCdpSession(page, fn) {
1817
- const CDP_SESSION_TIMEOUT_MS = 1e4;
1818
- const session = await Promise.race([
1819
- page.context().newCDPSession(page),
1820
- new Promise((_, reject) => {
1821
- setTimeout(() => {
1822
- reject(new Error("newCDPSession timed out after 10s"));
1823
- }, CDP_SESSION_TIMEOUT_MS);
1824
- })
1825
- ]);
1826
- try {
1827
- return await fn(session);
1828
- } finally {
1829
- await session.detach().catch(() => {
1830
- });
1831
- }
1832
- }
1833
- async function withPageScopedCdpClient(opts) {
1834
- return await withPlaywrightPageCdpSession(opts.page, async (session) => {
1835
- return await opts.fn((method, params) => session.send(method, params));
1836
- });
1837
- }
1838
- var LOOPBACK_ENTRIES = "localhost,127.0.0.1,[::1]";
1839
- function noProxyAlreadyCoversLocalhost() {
1840
- const current = process.env.NO_PROXY ?? process.env.no_proxy ?? "";
1841
- return current.includes("localhost") && current.includes("127.0.0.1") && current.includes("[::1]");
1842
- }
1843
- function isLoopbackCdpUrl(url) {
1844
- try {
1845
- return isLoopbackHost(new URL(url).hostname);
1846
- } catch {
1847
- return false;
1848
- }
1849
- }
1850
- var NoProxyLeaseManager = class {
1851
- leaseCount = 0;
1852
- snapshot = null;
1853
- acquire(url) {
1854
- if (!isLoopbackCdpUrl(url) || !hasProxyEnvConfigured()) return null;
1855
- if (this.leaseCount === 0 && !noProxyAlreadyCoversLocalhost()) {
1856
- const noProxy = process.env.NO_PROXY;
1857
- const noProxyLower = process.env.no_proxy;
1858
- const current = noProxy ?? noProxyLower ?? "";
1859
- const applied = current ? `${current},${LOOPBACK_ENTRIES}` : LOOPBACK_ENTRIES;
1860
- process.env.NO_PROXY = applied;
1861
- process.env.no_proxy = applied;
1862
- this.snapshot = { noProxy, noProxyLower, applied };
1863
- }
1864
- this.leaseCount += 1;
1865
- let released = false;
1866
- return () => {
1867
- if (released) return;
1868
- released = true;
1869
- this.release();
1870
- };
1871
- }
1872
- release() {
1873
- if (this.leaseCount <= 0) return;
1874
- this.leaseCount -= 1;
1875
- if (this.leaseCount > 0 || !this.snapshot) return;
1876
- const { noProxy, noProxyLower, applied } = this.snapshot;
1877
- const currentNoProxy = process.env.NO_PROXY;
1878
- const currentNoProxyLower = process.env.no_proxy;
1879
- if (currentNoProxy === applied && (currentNoProxyLower === applied || currentNoProxyLower === void 0)) {
1880
- if (noProxy !== void 0) process.env.NO_PROXY = noProxy;
1881
- else delete process.env.NO_PROXY;
1882
- if (noProxyLower !== void 0) process.env.no_proxy = noProxyLower;
1883
- else delete process.env.no_proxy;
1884
- }
1885
- this.snapshot = null;
1886
- }
1887
- };
1888
- var noProxyLeaseManager = new NoProxyLeaseManager();
1889
- async function withNoProxyForCdpUrl(url, fn) {
1890
- const release = noProxyLeaseManager.acquire(url);
1891
- try {
1892
- return await fn();
1893
- } finally {
1894
- release?.();
1895
- }
1896
- }
1897
- new http__default.default.Agent();
1898
- new https__default.default.Agent();
1899
- function getHeadersWithAuth(endpoint, baseHeaders = {}) {
1900
- const headers = { ...baseHeaders };
1901
- try {
1902
- const parsed = new URL(endpoint);
1903
- if (Object.keys(headers).some((k) => k.toLowerCase() === "authorization")) return headers;
1904
- if (parsed.username || parsed.password) {
1905
- const credentials = Buffer.from(
1906
- `${decodeURIComponent(parsed.username)}:${decodeURIComponent(parsed.password)}`
1907
- ).toString("base64");
1908
- headers.Authorization = `Basic ${credentials}`;
1909
- }
1910
- } catch {
1911
- }
1912
- return headers;
1913
- }
1914
- var cachedByCdpUrl = /* @__PURE__ */ new Map();
1915
- var connectingByCdpUrl = /* @__PURE__ */ new Map();
1788
+ // src/page-utils.ts
1789
+ var MAX_CONSOLE_MESSAGES = 500;
1790
+ var MAX_PAGE_ERRORS = 200;
1791
+ var MAX_NETWORK_REQUESTS = 500;
1916
1792
  var pageStates = /* @__PURE__ */ new WeakMap();
1917
1793
  var contextStates = /* @__PURE__ */ new WeakMap();
1918
1794
  var observedContexts = /* @__PURE__ */ new WeakSet();
1919
1795
  var observedPages = /* @__PURE__ */ new WeakSet();
1920
- var nextUploadArmId = 0;
1921
- var nextDialogArmId = 0;
1922
- var nextDownloadArmId = 0;
1923
- function bumpUploadArmId() {
1924
- nextUploadArmId += 1;
1925
- return nextUploadArmId;
1926
- }
1927
- function bumpDialogArmId() {
1928
- nextDialogArmId += 1;
1929
- return nextDialogArmId;
1930
- }
1931
- function bumpDownloadArmId() {
1932
- nextDownloadArmId += 1;
1933
- return nextDownloadArmId;
1934
- }
1935
- var BlockedBrowserTargetError = class extends Error {
1936
- constructor() {
1937
- super("Browser target is unavailable after SSRF policy blocked its navigation.");
1938
- this.name = "BlockedBrowserTargetError";
1939
- }
1940
- };
1941
- var blockedTargetsByCdpUrl = /* @__PURE__ */ new Set();
1942
- var blockedPageRefsByCdpUrl = /* @__PURE__ */ new Map();
1943
- function blockedTargetKey(cdpUrl, targetId) {
1944
- return `${normalizeCdpUrl(cdpUrl)}::${targetId}`;
1796
+ function bumpUploadArmId(state) {
1797
+ state.nextArmIdUpload += 1;
1798
+ return state.nextArmIdUpload;
1945
1799
  }
1946
- function isBlockedTarget(cdpUrl, targetId) {
1947
- const normalized = targetId?.trim() ?? "";
1948
- if (normalized === "") return false;
1949
- return blockedTargetsByCdpUrl.has(blockedTargetKey(cdpUrl, normalized));
1800
+ function bumpDialogArmId(state) {
1801
+ state.nextArmIdDialog += 1;
1802
+ return state.nextArmIdDialog;
1950
1803
  }
1951
- function markTargetBlocked(cdpUrl, targetId) {
1952
- const normalized = targetId?.trim() ?? "";
1953
- if (normalized === "") return;
1954
- blockedTargetsByCdpUrl.add(blockedTargetKey(cdpUrl, normalized));
1955
- }
1956
- function clearBlockedTarget(cdpUrl, targetId) {
1957
- const normalized = targetId?.trim() ?? "";
1958
- if (normalized === "") return;
1959
- blockedTargetsByCdpUrl.delete(blockedTargetKey(cdpUrl, normalized));
1960
- }
1961
- function hasBlockedTargetsForCdpUrl(cdpUrl) {
1962
- const prefix = `${normalizeCdpUrl(cdpUrl)}::`;
1963
- for (const key of blockedTargetsByCdpUrl) {
1964
- if (key.startsWith(prefix)) return true;
1965
- }
1966
- return false;
1967
- }
1968
- function blockedPageRefsForCdpUrl(cdpUrl) {
1969
- const normalized = normalizeCdpUrl(cdpUrl);
1970
- const existing = blockedPageRefsByCdpUrl.get(normalized);
1971
- if (existing) return existing;
1972
- const created = /* @__PURE__ */ new WeakSet();
1973
- blockedPageRefsByCdpUrl.set(normalized, created);
1974
- return created;
1975
- }
1976
- function isBlockedPageRef(cdpUrl, page) {
1977
- return blockedPageRefsByCdpUrl.get(normalizeCdpUrl(cdpUrl))?.has(page) ?? false;
1978
- }
1979
- function markPageRefBlocked(cdpUrl, page) {
1980
- blockedPageRefsForCdpUrl(cdpUrl).add(page);
1981
- }
1982
- function clearBlockedPageRef(cdpUrl, page) {
1983
- blockedPageRefsByCdpUrl.get(normalizeCdpUrl(cdpUrl))?.delete(page);
1804
+ function bumpDownloadArmId(state) {
1805
+ state.nextArmIdDownload += 1;
1806
+ return state.nextArmIdDownload;
1984
1807
  }
1985
1808
  function ensureContextState(context) {
1986
1809
  const existing = contextStates.get(context);
@@ -1989,16 +1812,8 @@ function ensureContextState(context) {
1989
1812
  contextStates.set(context, state);
1990
1813
  return state;
1991
1814
  }
1992
- var roleRefsByTarget = /* @__PURE__ */ new Map();
1993
- var MAX_ROLE_REFS_CACHE = 50;
1994
- var MAX_CONSOLE_MESSAGES = 500;
1995
- var MAX_PAGE_ERRORS = 200;
1996
- var MAX_NETWORK_REQUESTS = 500;
1997
- function normalizeCdpUrl(raw) {
1998
- return raw.replace(/\/$/, "");
1999
- }
2000
- function roleRefsKey(cdpUrl, targetId) {
2001
- return `${normalizeCdpUrl(cdpUrl)}::${targetId}`;
1815
+ function getPageState(page) {
1816
+ return pageStates.get(page);
2002
1817
  }
2003
1818
  function findNetworkRequestById(state, id) {
2004
1819
  for (let i = state.requests.length - 1; i >= 0; i--) {
@@ -2018,7 +1833,10 @@ function ensurePageState(page) {
2018
1833
  nextRequestId: 0,
2019
1834
  armIdUpload: 0,
2020
1835
  armIdDialog: 0,
2021
- armIdDownload: 0
1836
+ armIdDownload: 0,
1837
+ nextArmIdUpload: 0,
1838
+ nextArmIdDialog: 0,
1839
+ nextArmIdDownload: 0
2022
1840
  };
2023
1841
  pageStates.set(page, state);
2024
1842
  if (!observedPages.has(page)) {
@@ -2121,10 +1939,9 @@ function ensurePageState(page) {
2121
1939
  }
2122
1940
  return state;
2123
1941
  }
2124
- async function setDialogHandler(opts) {
2125
- const page = await getPageForTargetId({ cdpUrl: opts.cdpUrl, targetId: opts.targetId });
1942
+ function setDialogHandlerOnPage(page, handler) {
2126
1943
  const state = ensurePageState(page);
2127
- state.dialogHandler = opts.handler;
1944
+ state.dialogHandler = handler;
2128
1945
  }
2129
1946
  function applyStealthToPage(page) {
2130
1947
  page.evaluate(STEALTH_SCRIPT).catch((e) => {
@@ -2152,6 +1969,47 @@ function observeContext(context) {
2152
1969
  function observeBrowser(browser) {
2153
1970
  for (const context of browser.contexts()) observeContext(context);
2154
1971
  }
1972
+ function toAIFriendlyError(error, selector) {
1973
+ const message = error instanceof Error ? error.message : String(error);
1974
+ if (message.includes("strict mode violation")) {
1975
+ const countMatch = /resolved to (\d+) elements/.exec(message);
1976
+ const count = countMatch ? countMatch[1] : "multiple";
1977
+ return new Error(
1978
+ `Selector "${selector}" matched ${count} elements. Run a new snapshot to get updated refs, or use a different ref.`
1979
+ );
1980
+ }
1981
+ if ((message.includes("Timeout") || message.includes("waiting for")) && (message.includes("to be visible") || message.includes("not visible"))) {
1982
+ return new Error(
1983
+ `Element "${selector}" not found or not visible. Run a new snapshot to see current page elements.`
1984
+ );
1985
+ }
1986
+ if (message.includes("intercepts pointer events") || message.includes("not visible") || message.includes("not receive pointer events")) {
1987
+ return new Error(
1988
+ `Element "${selector}" is not interactable (hidden or covered). Try scrolling it into view, closing overlays, or re-snapshotting.`
1989
+ );
1990
+ }
1991
+ const timeoutMatch = /Timeout (\d+)ms exceeded/.exec(message);
1992
+ if (timeoutMatch) {
1993
+ return new Error(
1994
+ `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.`
1995
+ );
1996
+ }
1997
+ const cleaned = message.replace(/locator\([^)]*\)\./g, "").replace(/waiting for locator\([^)]*\)/g, "").trim();
1998
+ return new Error(cleaned || message);
1999
+ }
2000
+ function normalizeTimeoutMs(timeoutMs, fallback, maxMs = 12e4) {
2001
+ return Math.max(500, Math.min(maxMs, timeoutMs ?? fallback));
2002
+ }
2003
+
2004
+ // src/ref-resolver.ts
2005
+ var roleRefsByTarget = /* @__PURE__ */ new Map();
2006
+ var MAX_ROLE_REFS_CACHE = 50;
2007
+ function normalizeCdpUrl(raw) {
2008
+ return raw.replace(/\/$/, "");
2009
+ }
2010
+ function roleRefsKey(cdpUrl, targetId) {
2011
+ return `${normalizeCdpUrl(cdpUrl)}::${targetId}`;
2012
+ }
2155
2013
  function rememberRoleRefsForTarget(opts) {
2156
2014
  const targetId = opts.targetId.trim();
2157
2015
  if (targetId === "") return;
@@ -2191,6 +2049,232 @@ function restoreRoleRefsForTarget(opts) {
2191
2049
  state.roleRefsFrameSelector = entry.frameSelector;
2192
2050
  state.roleRefsMode = entry.mode;
2193
2051
  }
2052
+ function clearRoleRefsForCdpUrl(cdpUrl) {
2053
+ const normalized = normalizeCdpUrl(cdpUrl);
2054
+ for (const key of roleRefsByTarget.keys()) {
2055
+ if (key.startsWith(normalized + "::")) roleRefsByTarget.delete(key);
2056
+ }
2057
+ }
2058
+ function parseRoleRef(raw) {
2059
+ const trimmed = raw.trim();
2060
+ if (!trimmed) return null;
2061
+ const normalized = trimmed.startsWith("@") ? trimmed.slice(1) : trimmed.startsWith("ref=") ? trimmed.slice(4) : trimmed;
2062
+ return /^e\d+$/.test(normalized) ? normalized : null;
2063
+ }
2064
+ function requireRef(value) {
2065
+ const raw = typeof value === "string" ? value.trim() : "";
2066
+ const ref = (raw ? parseRoleRef(raw) : null) ?? (raw.startsWith("@") ? raw.slice(1) : raw);
2067
+ if (!ref) throw new Error("ref is required");
2068
+ return ref;
2069
+ }
2070
+ function requireRefOrSelector(ref, selector) {
2071
+ const trimmedRef = typeof ref === "string" ? ref.trim() : "";
2072
+ const trimmedSelector = typeof selector === "string" ? selector.trim() : "";
2073
+ if (!trimmedRef && !trimmedSelector) throw new Error("ref or selector is required");
2074
+ return { ref: trimmedRef || void 0, selector: trimmedSelector || void 0 };
2075
+ }
2076
+ function resolveInteractionTimeoutMs(timeoutMs) {
2077
+ return Math.max(500, Math.min(6e4, Math.floor(timeoutMs ?? 8e3)));
2078
+ }
2079
+ function resolveBoundedDelayMs(value, label, maxMs) {
2080
+ const normalized = Math.floor(value ?? 0);
2081
+ if (!Number.isFinite(normalized) || normalized < 0) throw new Error(`${label} must be >= 0`);
2082
+ if (normalized > maxMs) throw new Error(`${label} exceeds maximum of ${String(maxMs)}ms`);
2083
+ return normalized;
2084
+ }
2085
+ function refLocator(page, ref) {
2086
+ const normalized = ref.startsWith("@") ? ref.slice(1) : ref.startsWith("ref=") ? ref.slice(4) : ref;
2087
+ if (normalized.trim() === "") throw new Error("ref is required");
2088
+ if (/^e\d+$/.test(normalized)) {
2089
+ const state = getPageState(page);
2090
+ if (state?.roleRefsMode === "aria") {
2091
+ return (state.roleRefsFrameSelector !== void 0 && state.roleRefsFrameSelector !== "" ? page.frameLocator(state.roleRefsFrameSelector) : page).locator(`aria-ref=${normalized}`);
2092
+ }
2093
+ const info = state?.roleRefs?.[normalized];
2094
+ if (!info) throw new Error(`Unknown ref "${normalized}". Run a new snapshot and use a ref from that snapshot.`);
2095
+ const locAny = state.roleRefsFrameSelector !== void 0 && state.roleRefsFrameSelector !== "" ? page.frameLocator(state.roleRefsFrameSelector) : page;
2096
+ const role = info.role;
2097
+ const locator = info.name !== void 0 && info.name !== "" ? locAny.getByRole(role, { name: info.name, exact: true }) : locAny.getByRole(role);
2098
+ return info.nth !== void 0 ? locator.nth(info.nth) : locator;
2099
+ }
2100
+ return page.locator(`aria-ref=${normalized}`);
2101
+ }
2102
+
2103
+ // src/connection.ts
2104
+ var BrowserTabNotFoundError = class extends Error {
2105
+ constructor(message = "Tab not found") {
2106
+ super(message);
2107
+ this.name = "BrowserTabNotFoundError";
2108
+ }
2109
+ };
2110
+ async function fetchJsonForCdp(url, timeoutMs) {
2111
+ const ctrl = new AbortController();
2112
+ const t = setTimeout(() => {
2113
+ ctrl.abort();
2114
+ }, timeoutMs);
2115
+ try {
2116
+ const res = await fetch(url, { signal: ctrl.signal });
2117
+ if (!res.ok) return null;
2118
+ return await res.json();
2119
+ } catch {
2120
+ return null;
2121
+ } finally {
2122
+ clearTimeout(t);
2123
+ }
2124
+ }
2125
+ function appendCdpPath2(cdpUrl, cdpPath) {
2126
+ try {
2127
+ const url = new URL(cdpUrl);
2128
+ url.pathname = `${url.pathname.replace(/\/$/, "")}${cdpPath.startsWith("/") ? cdpPath : `/${cdpPath}`}`;
2129
+ return url.toString();
2130
+ } catch {
2131
+ return `${cdpUrl.replace(/\/$/, "")}${cdpPath}`;
2132
+ }
2133
+ }
2134
+ async function withPlaywrightPageCdpSession(page, fn) {
2135
+ const CDP_SESSION_TIMEOUT_MS = 1e4;
2136
+ const session = await Promise.race([
2137
+ page.context().newCDPSession(page),
2138
+ new Promise((_, reject) => {
2139
+ setTimeout(() => {
2140
+ reject(new Error("newCDPSession timed out after 10s"));
2141
+ }, CDP_SESSION_TIMEOUT_MS);
2142
+ })
2143
+ ]);
2144
+ try {
2145
+ return await fn(session);
2146
+ } finally {
2147
+ await session.detach().catch(() => {
2148
+ });
2149
+ }
2150
+ }
2151
+ async function withPageScopedCdpClient(opts) {
2152
+ return await withPlaywrightPageCdpSession(opts.page, async (session) => {
2153
+ return await opts.fn((method, params) => session.send(method, params));
2154
+ });
2155
+ }
2156
+ var LOOPBACK_ENTRIES = "localhost,127.0.0.1,[::1]";
2157
+ function noProxyAlreadyCoversLocalhost() {
2158
+ const current = process.env.NO_PROXY ?? process.env.no_proxy ?? "";
2159
+ return current.includes("localhost") && current.includes("127.0.0.1") && current.includes("[::1]");
2160
+ }
2161
+ function isLoopbackCdpUrl(url) {
2162
+ try {
2163
+ return isLoopbackHost(new URL(url).hostname);
2164
+ } catch {
2165
+ return false;
2166
+ }
2167
+ }
2168
+ var envMutexPromise = Promise.resolve();
2169
+ async function withNoProxyForCdpUrl(url, fn) {
2170
+ if (!isLoopbackCdpUrl(url) || !hasProxyEnvConfigured()) return fn();
2171
+ const prev = envMutexPromise;
2172
+ let release = () => {
2173
+ };
2174
+ envMutexPromise = new Promise((r) => {
2175
+ release = r;
2176
+ });
2177
+ await prev;
2178
+ if (noProxyAlreadyCoversLocalhost()) {
2179
+ try {
2180
+ return await fn();
2181
+ } finally {
2182
+ release();
2183
+ }
2184
+ }
2185
+ const savedNoProxy = process.env.NO_PROXY;
2186
+ const savedNoProxyLower = process.env.no_proxy;
2187
+ const current = savedNoProxy ?? savedNoProxyLower ?? "";
2188
+ const applied = current ? `${current},${LOOPBACK_ENTRIES}` : LOOPBACK_ENTRIES;
2189
+ process.env.NO_PROXY = applied;
2190
+ process.env.no_proxy = applied;
2191
+ try {
2192
+ return await fn();
2193
+ } finally {
2194
+ if (process.env.NO_PROXY === applied) {
2195
+ if (savedNoProxy !== void 0) process.env.NO_PROXY = savedNoProxy;
2196
+ else delete process.env.NO_PROXY;
2197
+ }
2198
+ if (process.env.no_proxy === applied) {
2199
+ if (savedNoProxyLower !== void 0) process.env.no_proxy = savedNoProxyLower;
2200
+ else delete process.env.no_proxy;
2201
+ }
2202
+ release();
2203
+ }
2204
+ }
2205
+ new http__default.default.Agent();
2206
+ new https__default.default.Agent();
2207
+ function getHeadersWithAuth(endpoint, baseHeaders = {}) {
2208
+ const headers = { ...baseHeaders };
2209
+ try {
2210
+ const parsed = new URL(endpoint);
2211
+ if (Object.keys(headers).some((k) => k.toLowerCase() === "authorization")) return headers;
2212
+ if (parsed.username || parsed.password) {
2213
+ const credentials = Buffer.from(
2214
+ `${decodeURIComponent(parsed.username)}:${decodeURIComponent(parsed.password)}`
2215
+ ).toString("base64");
2216
+ headers.Authorization = `Basic ${credentials}`;
2217
+ }
2218
+ } catch {
2219
+ }
2220
+ return headers;
2221
+ }
2222
+ var cachedByCdpUrl = /* @__PURE__ */ new Map();
2223
+ var connectingByCdpUrl = /* @__PURE__ */ new Map();
2224
+ var BlockedBrowserTargetError = class extends Error {
2225
+ constructor() {
2226
+ super("Browser target is unavailable after SSRF policy blocked its navigation.");
2227
+ this.name = "BlockedBrowserTargetError";
2228
+ }
2229
+ };
2230
+ var blockedTargetsByCdpUrl = /* @__PURE__ */ new Set();
2231
+ var blockedPageRefsByCdpUrl = /* @__PURE__ */ new Map();
2232
+ function blockedTargetKey(cdpUrl, targetId) {
2233
+ return `${normalizeCdpUrl(cdpUrl)}::${targetId}`;
2234
+ }
2235
+ function isBlockedTarget(cdpUrl, targetId) {
2236
+ const normalized = targetId?.trim() ?? "";
2237
+ if (normalized === "") return false;
2238
+ return blockedTargetsByCdpUrl.has(blockedTargetKey(cdpUrl, normalized));
2239
+ }
2240
+ function markTargetBlocked(cdpUrl, targetId) {
2241
+ const normalized = targetId?.trim() ?? "";
2242
+ if (normalized === "") return;
2243
+ blockedTargetsByCdpUrl.add(blockedTargetKey(cdpUrl, normalized));
2244
+ }
2245
+ function clearBlockedTarget(cdpUrl, targetId) {
2246
+ const normalized = targetId?.trim() ?? "";
2247
+ if (normalized === "") return;
2248
+ blockedTargetsByCdpUrl.delete(blockedTargetKey(cdpUrl, normalized));
2249
+ }
2250
+ function hasBlockedTargetsForCdpUrl(cdpUrl) {
2251
+ const prefix = `${normalizeCdpUrl(cdpUrl)}::`;
2252
+ for (const key of blockedTargetsByCdpUrl) {
2253
+ if (key.startsWith(prefix)) return true;
2254
+ }
2255
+ return false;
2256
+ }
2257
+ function blockedPageRefsForCdpUrl(cdpUrl) {
2258
+ const normalized = normalizeCdpUrl(cdpUrl);
2259
+ const existing = blockedPageRefsByCdpUrl.get(normalized);
2260
+ if (existing) return existing;
2261
+ const created = /* @__PURE__ */ new WeakSet();
2262
+ blockedPageRefsByCdpUrl.set(normalized, created);
2263
+ return created;
2264
+ }
2265
+ function isBlockedPageRef(cdpUrl, page) {
2266
+ return blockedPageRefsByCdpUrl.get(normalizeCdpUrl(cdpUrl))?.has(page) ?? false;
2267
+ }
2268
+ function markPageRefBlocked(cdpUrl, page) {
2269
+ blockedPageRefsForCdpUrl(cdpUrl).add(page);
2270
+ }
2271
+ function clearBlockedPageRef(cdpUrl, page) {
2272
+ blockedPageRefsByCdpUrl.get(normalizeCdpUrl(cdpUrl))?.delete(page);
2273
+ }
2274
+ async function setDialogHandler(opts) {
2275
+ const page = await getPageForTargetId({ cdpUrl: opts.cdpUrl, targetId: opts.targetId });
2276
+ setDialogHandlerOnPage(page, opts.handler);
2277
+ }
2194
2278
  async function connectBrowser(cdpUrl, authToken) {
2195
2279
  const normalized = normalizeCdpUrl(cdpUrl);
2196
2280
  const existing_cached = cachedByCdpUrl.get(normalized);
@@ -2213,9 +2297,7 @@ async function connectBrowser(cdpUrl, authToken) {
2213
2297
  const onDisconnected = () => {
2214
2298
  if (cachedByCdpUrl.get(normalized)?.browser === browser) {
2215
2299
  cachedByCdpUrl.delete(normalized);
2216
- for (const key of roleRefsByTarget.keys()) {
2217
- if (key.startsWith(normalized + "::")) roleRefsByTarget.delete(key);
2218
- }
2300
+ clearRoleRefsForCdpUrl(normalized);
2219
2301
  }
2220
2302
  };
2221
2303
  const connected = { browser, cdpUrl: normalized, onDisconnected };
@@ -2347,7 +2429,7 @@ async function tryTerminateExecutionViaCdp(cdpUrl, targetId) {
2347
2429
  };
2348
2430
  });
2349
2431
  }
2350
- async function forceDisconnectPlaywrightForTarget(opts) {
2432
+ async function forceDisconnectPlaywrightConnection(opts) {
2351
2433
  const normalized = normalizeCdpUrl(opts.cdpUrl);
2352
2434
  const cur = cachedByCdpUrl.get(normalized);
2353
2435
  if (!cur) return;
@@ -2364,6 +2446,7 @@ async function forceDisconnectPlaywrightForTarget(opts) {
2364
2446
  cur.browser.close().catch(() => {
2365
2447
  });
2366
2448
  }
2449
+ var forceDisconnectPlaywrightForTarget = forceDisconnectPlaywrightConnection;
2367
2450
  function getAllPages(browser) {
2368
2451
  return browser.contexts().flatMap((c) => c.pages());
2369
2452
  }
@@ -2479,87 +2562,12 @@ async function resolvePageByTargetIdOrThrow(opts) {
2479
2562
  if (!page) throw new BrowserTabNotFoundError();
2480
2563
  return page;
2481
2564
  }
2482
- function parseRoleRef(raw) {
2483
- const trimmed = raw.trim();
2484
- if (!trimmed) return null;
2485
- const normalized = trimmed.startsWith("@") ? trimmed.slice(1) : trimmed.startsWith("ref=") ? trimmed.slice(4) : trimmed;
2486
- return /^e\d+$/.test(normalized) ? normalized : null;
2487
- }
2488
- function requireRef(value) {
2489
- const raw = typeof value === "string" ? value.trim() : "";
2490
- const ref = (raw ? parseRoleRef(raw) : null) ?? (raw.startsWith("@") ? raw.slice(1) : raw);
2491
- if (!ref) throw new Error("ref is required");
2492
- return ref;
2493
- }
2494
- function requireRefOrSelector(ref, selector) {
2495
- const trimmedRef = typeof ref === "string" ? ref.trim() : "";
2496
- const trimmedSelector = typeof selector === "string" ? selector.trim() : "";
2497
- if (!trimmedRef && !trimmedSelector) throw new Error("ref or selector is required");
2498
- return { ref: trimmedRef || void 0, selector: trimmedSelector || void 0 };
2499
- }
2500
- function resolveInteractionTimeoutMs(timeoutMs) {
2501
- return Math.max(500, Math.min(6e4, Math.floor(timeoutMs ?? 8e3)));
2502
- }
2503
- function resolveBoundedDelayMs(value, label, maxMs) {
2504
- const normalized = Math.floor(value ?? 0);
2505
- if (!Number.isFinite(normalized) || normalized < 0) throw new Error(`${label} must be >= 0`);
2506
- if (normalized > maxMs) throw new Error(`${label} exceeds maximum of ${String(maxMs)}ms`);
2507
- return normalized;
2508
- }
2509
2565
  async function getRestoredPageForTarget(opts) {
2510
2566
  const page = await getPageForTargetId(opts);
2511
2567
  ensurePageState(page);
2512
2568
  restoreRoleRefsForTarget({ cdpUrl: opts.cdpUrl, targetId: opts.targetId, page });
2513
2569
  return page;
2514
2570
  }
2515
- function refLocator(page, ref) {
2516
- const normalized = ref.startsWith("@") ? ref.slice(1) : ref.startsWith("ref=") ? ref.slice(4) : ref;
2517
- if (normalized.trim() === "") throw new Error("ref is required");
2518
- if (/^e\d+$/.test(normalized)) {
2519
- const state = pageStates.get(page);
2520
- if (state?.roleRefsMode === "aria") {
2521
- return (state.roleRefsFrameSelector !== void 0 && state.roleRefsFrameSelector !== "" ? page.frameLocator(state.roleRefsFrameSelector) : page).locator(`aria-ref=${normalized}`);
2522
- }
2523
- const info = state?.roleRefs?.[normalized];
2524
- if (!info) throw new Error(`Unknown ref "${normalized}". Run a new snapshot and use a ref from that snapshot.`);
2525
- const locAny = state.roleRefsFrameSelector !== void 0 && state.roleRefsFrameSelector !== "" ? page.frameLocator(state.roleRefsFrameSelector) : page;
2526
- const role = info.role;
2527
- const locator = info.name !== void 0 && info.name !== "" ? locAny.getByRole(role, { name: info.name, exact: true }) : locAny.getByRole(role);
2528
- return info.nth !== void 0 ? locator.nth(info.nth) : locator;
2529
- }
2530
- return page.locator(`aria-ref=${normalized}`);
2531
- }
2532
- function toAIFriendlyError(error, selector) {
2533
- const message = error instanceof Error ? error.message : String(error);
2534
- if (message.includes("strict mode violation")) {
2535
- const countMatch = /resolved to (\d+) elements/.exec(message);
2536
- const count = countMatch ? countMatch[1] : "multiple";
2537
- return new Error(
2538
- `Selector "${selector}" matched ${count} elements. Run a new snapshot to get updated refs, or use a different ref.`
2539
- );
2540
- }
2541
- if ((message.includes("Timeout") || message.includes("waiting for")) && (message.includes("to be visible") || message.includes("not visible"))) {
2542
- return new Error(
2543
- `Element "${selector}" not found or not visible. Run a new snapshot to see current page elements.`
2544
- );
2545
- }
2546
- if (message.includes("intercepts pointer events") || message.includes("not visible") || message.includes("not receive pointer events")) {
2547
- return new Error(
2548
- `Element "${selector}" is not interactable (hidden or covered). Try scrolling it into view, closing overlays, or re-snapshotting.`
2549
- );
2550
- }
2551
- const timeoutMatch = /Timeout (\d+)ms exceeded/.exec(message);
2552
- if (timeoutMatch) {
2553
- return new Error(
2554
- `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.`
2555
- );
2556
- }
2557
- const cleaned = message.replace(/locator\([^)]*\)\./g, "").replace(/waiting for locator\([^)]*\)/g, "").trim();
2558
- return new Error(cleaned || message);
2559
- }
2560
- function normalizeTimeoutMs(timeoutMs, fallback, maxMs = 12e4) {
2561
- return Math.max(500, Math.min(maxMs, timeoutMs ?? fallback));
2562
- }
2563
2571
 
2564
2572
  // src/actions/evaluate.ts
2565
2573
  async function evaluateInAllFramesViaPlaywright(opts) {
@@ -2584,7 +2592,8 @@ async function evaluateInAllFramesViaPlaywright(opts) {
2584
2592
  frameName: frame.name(),
2585
2593
  result
2586
2594
  });
2587
- } catch {
2595
+ } catch (err) {
2596
+ console.warn("[browserclaw] frame evaluate failed:", err instanceof Error ? err.message : String(err));
2588
2597
  }
2589
2598
  }
2590
2599
  return results;
@@ -2670,7 +2679,7 @@ async function evaluateViaPlaywright(opts) {
2670
2679
  }
2671
2680
  if (signal !== void 0) {
2672
2681
  const disconnect = () => {
2673
- forceDisconnectPlaywrightForTarget({
2682
+ forceDisconnectPlaywrightConnection({
2674
2683
  cdpUrl: opts.cdpUrl,
2675
2684
  targetId: opts.targetId}).catch(() => {
2676
2685
  });
@@ -2737,7 +2746,6 @@ function withBrowserNavigationPolicy(ssrfPolicy) {
2737
2746
  }
2738
2747
  var NETWORK_NAVIGATION_PROTOCOLS = /* @__PURE__ */ new Set(["http:", "https:"]);
2739
2748
  var SAFE_NON_NETWORK_URLS = /* @__PURE__ */ new Set(["about:blank"]);
2740
- var PROXY_ENV_KEYS2 = ["HTTP_PROXY", "HTTPS_PROXY", "ALL_PROXY", "http_proxy", "https_proxy", "all_proxy"];
2741
2749
  var BLOCKED_HOSTNAMES = /* @__PURE__ */ new Set(["localhost", "localhost.localdomain", "metadata.google.internal"]);
2742
2750
  function isAllowedNonNetworkNavigationUrl(parsed) {
2743
2751
  return SAFE_NON_NETWORK_URLS.has(parsed.href);
@@ -2745,13 +2753,6 @@ function isAllowedNonNetworkNavigationUrl(parsed) {
2745
2753
  function isPrivateNetworkAllowedByPolicy(policy) {
2746
2754
  return policy?.dangerouslyAllowPrivateNetwork === true || policy?.allowPrivateNetwork === true;
2747
2755
  }
2748
- function hasProxyEnvConfigured2(env = process.env) {
2749
- for (const key of PROXY_ENV_KEYS2) {
2750
- const value = env[key];
2751
- if (typeof value === "string" && value.trim().length > 0) return true;
2752
- }
2753
- return false;
2754
- }
2755
2756
  function normalizeHostname(hostname) {
2756
2757
  let h = hostname.trim().toLowerCase();
2757
2758
  if (h.startsWith("[") && h.endsWith("]")) h = h.slice(1, -1);
@@ -3062,7 +3063,7 @@ async function assertBrowserNavigationAllowed(opts) {
3062
3063
  if (isAllowedNonNetworkNavigationUrl(parsed)) return;
3063
3064
  throw new InvalidBrowserNavigationUrlError(`Navigation blocked: unsupported protocol "${parsed.protocol}"`);
3064
3065
  }
3065
- if (hasProxyEnvConfigured2() && !isPrivateNetworkAllowedByPolicy(opts.ssrfPolicy)) {
3066
+ if (hasProxyEnvConfigured() && !isPrivateNetworkAllowedByPolicy(opts.ssrfPolicy)) {
3066
3067
  throw new InvalidBrowserNavigationUrlError(
3067
3068
  "Navigation blocked: strict browser SSRF policy cannot be enforced while env proxy variables are set"
3068
3069
  );
@@ -3299,7 +3300,7 @@ async function pressAndHoldViaCdp(opts) {
3299
3300
  async function clickByTextViaPlaywright(opts) {
3300
3301
  const page = await getRestoredPageForTarget(opts);
3301
3302
  const timeout = resolveInteractionTimeoutMs(opts.timeoutMs);
3302
- const locator = page.getByText(opts.text, { exact: opts.exact }).and(page.locator(":visible")).or(page.getByTitle(opts.text, { exact: opts.exact })).first();
3303
+ const locator = page.getByText(opts.text, { exact: opts.exact }).or(page.getByTitle(opts.text, { exact: opts.exact })).and(page.locator(":visible")).first();
3303
3304
  try {
3304
3305
  await locator.click({ timeout, button: opts.button, modifiers: opts.modifiers });
3305
3306
  } catch (err) {
@@ -3517,7 +3518,7 @@ async function armDialogViaPlaywright(opts) {
3517
3518
  const page = await getPageForTargetId({ cdpUrl: opts.cdpUrl, targetId: opts.targetId });
3518
3519
  const state = ensurePageState(page);
3519
3520
  const timeout = normalizeTimeoutMs(opts.timeoutMs, 12e4);
3520
- state.armIdDialog = bumpDialogArmId();
3521
+ state.armIdDialog = bumpDialogArmId(state);
3521
3522
  const armId = state.armIdDialog;
3522
3523
  page.waitForEvent("dialog", { timeout }).then(async (dialog) => {
3523
3524
  if (state.armIdDialog !== armId) return;
@@ -3535,7 +3536,7 @@ async function armFileUploadViaPlaywright(opts) {
3535
3536
  const page = await getPageForTargetId({ cdpUrl: opts.cdpUrl, targetId: opts.targetId });
3536
3537
  const state = ensurePageState(page);
3537
3538
  const timeout = normalizeTimeoutMs(opts.timeoutMs, 12e4);
3538
- state.armIdUpload = bumpUploadArmId();
3539
+ state.armIdUpload = bumpUploadArmId(state);
3539
3540
  const armId = state.armIdUpload;
3540
3541
  page.waitForEvent("filechooser", { timeout }).then(async (fileChooser) => {
3541
3542
  if (state.armIdUpload !== armId) return;
@@ -3695,7 +3696,7 @@ async function navigateViaPlaywright(opts) {
3695
3696
  response = await navigate();
3696
3697
  } catch (err) {
3697
3698
  if (!isRetryableNavigateError(err)) throw err;
3698
- await forceDisconnectPlaywrightForTarget({
3699
+ await forceDisconnectPlaywrightConnection({
3699
3700
  cdpUrl: opts.cdpUrl,
3700
3701
  targetId: opts.targetId}).catch(() => {
3701
3702
  });
@@ -3837,18 +3838,12 @@ async function resizeViewportViaPlaywright(opts) {
3837
3838
 
3838
3839
  // src/actions/wait.ts
3839
3840
  var MAX_WAIT_TIME_MS = 3e4;
3840
- function resolveBoundedDelayMs2(value, label, maxMs) {
3841
- const normalized = Math.floor(value ?? 0);
3842
- if (!Number.isFinite(normalized) || normalized < 0) throw new Error(`${label} must be >= 0`);
3843
- if (normalized > maxMs) throw new Error(`${label} exceeds maximum of ${String(maxMs)}ms`);
3844
- return normalized;
3845
- }
3846
3841
  async function waitForViaPlaywright(opts) {
3847
3842
  const page = await getPageForTargetId({ cdpUrl: opts.cdpUrl, targetId: opts.targetId });
3848
3843
  ensurePageState(page);
3849
3844
  const timeout = normalizeTimeoutMs(opts.timeoutMs, 2e4);
3850
3845
  if (typeof opts.timeMs === "number" && Number.isFinite(opts.timeMs)) {
3851
- await page.waitForTimeout(resolveBoundedDelayMs2(opts.timeMs, "wait timeMs", MAX_WAIT_TIME_MS));
3846
+ await page.waitForTimeout(resolveBoundedDelayMs(opts.timeMs, "wait timeMs", MAX_WAIT_TIME_MS));
3852
3847
  }
3853
3848
  if (opts.text !== void 0 && opts.text !== "") {
3854
3849
  await page.waitForFunction((text) => (document.body?.innerText ?? "").includes(text), opts.text, { timeout });
@@ -4105,7 +4100,7 @@ async function downloadViaPlaywright(opts) {
4105
4100
  const timeout = normalizeTimeoutMs(opts.timeoutMs, 12e4);
4106
4101
  const outPath = opts.path.trim();
4107
4102
  if (!outPath) throw new Error("path is required");
4108
- state.armIdDownload = bumpDownloadArmId();
4103
+ state.armIdDownload = bumpDownloadArmId(state);
4109
4104
  const armId = state.armIdDownload;
4110
4105
  const waiter = createPageDownloadWaiter(page, timeout);
4111
4106
  try {
@@ -4125,7 +4120,7 @@ async function waitForDownloadViaPlaywright(opts) {
4125
4120
  const page = await getPageForTargetId({ cdpUrl: opts.cdpUrl, targetId: opts.targetId });
4126
4121
  const state = ensurePageState(page);
4127
4122
  const timeout = normalizeTimeoutMs(opts.timeoutMs, 12e4);
4128
- state.armIdDownload = bumpDownloadArmId();
4123
+ state.armIdDownload = bumpDownloadArmId(state);
4129
4124
  const armId = state.armIdDownload;
4130
4125
  const waiter = createPageDownloadWaiter(page, timeout);
4131
4126
  try {
@@ -5067,6 +5062,7 @@ function axValue(v) {
5067
5062
  return "";
5068
5063
  }
5069
5064
  function formatAriaNodes(nodes, limit) {
5065
+ if (nodes.length === 0) return [];
5070
5066
  const byId = /* @__PURE__ */ new Map();
5071
5067
  for (const n of nodes) if (n.nodeId) byId.set(n.nodeId, n);
5072
5068
  const referenced = /* @__PURE__ */ new Set();
@@ -6575,6 +6571,7 @@ exports.createPinnedLookup = createPinnedLookup;
6575
6571
  exports.detectChallengeViaPlaywright = detectChallengeViaPlaywright;
6576
6572
  exports.ensureContextState = ensureContextState;
6577
6573
  exports.executeSingleAction = executeSingleAction;
6574
+ exports.forceDisconnectPlaywrightConnection = forceDisconnectPlaywrightConnection;
6578
6575
  exports.forceDisconnectPlaywrightForTarget = forceDisconnectPlaywrightForTarget;
6579
6576
  exports.getChromeWebSocketUrl = getChromeWebSocketUrl;
6580
6577
  exports.getRestoredPageForTarget = getRestoredPageForTarget;