browserclaw 0.5.6 → 0.5.7

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/README.md CHANGED
@@ -1,6 +1,7 @@
1
1
  <h2 align="center">🦞 BrowserClaw — Standalone OpenClaw browser module</h2>
2
2
 
3
3
  <p align="center">
4
+ <a href="https://browserclaw.agent"><img src="https://img.shields.io/badge/Live-browserclaw.agent-orange" alt="Live" /></a>
4
5
  <a href="https://www.npmjs.com/package/browserclaw"><img src="https://img.shields.io/npm/v/browserclaw.svg" alt="npm version" /></a>
5
6
  <a href="./LICENSE"><img src="https://img.shields.io/badge/License-MIT-blue.svg" alt="License: MIT" /></a>
6
7
  <a href="https://www.npmjs.com/package/browserclaw"><img src="https://img.shields.io/npm/dw/browserclaw" alt="npm downloads" /></a>
@@ -85,7 +86,7 @@ When you're running the same multi-step workflow hundreds of times — filling f
85
86
 
86
87
  [browserclaw.org](https://browserclaw.org) is an open-source playground where you can type a prompt and watch an AI agent use browserclaw in a real browser — live. No setup, no API keys, just a text box and a browser stream.
87
88
 
88
- Want to run it yourself? The source is at [github.com/idan-rubin/browserclaw.org](https://github.com/idan-rubin/browserclaw.org) — spin it up with Docker or Node.js. Supports Groq, Gemini, and OpenAI out of the box.
89
+ Want to run it yourself? The source is at [github.com/idan-rubin/browserclaw.agent](https://github.com/idan-rubin/browserclaw.agent) — spin it up with Docker or Node.js. Supports Groq, Gemini, OpenAI, and Anthropic out of the box.
89
90
 
90
91
  ## Install
91
92
 
package/dist/index.cjs CHANGED
@@ -6,6 +6,8 @@ var fs = require('fs');
6
6
  var net = require('net');
7
7
  var child_process = require('child_process');
8
8
  var playwrightCore = require('playwright-core');
9
+ var http = require('http');
10
+ var https = require('https');
9
11
  var promises = require('dns/promises');
10
12
  var dns = require('dns');
11
13
  var promises$1 = require('fs/promises');
@@ -17,6 +19,8 @@ var os__default = /*#__PURE__*/_interopDefault(os);
17
19
  var path__default = /*#__PURE__*/_interopDefault(path);
18
20
  var fs__default = /*#__PURE__*/_interopDefault(fs);
19
21
  var net__default = /*#__PURE__*/_interopDefault(net);
22
+ var http__default = /*#__PURE__*/_interopDefault(http);
23
+ var https__default = /*#__PURE__*/_interopDefault(https);
20
24
 
21
25
  var __create = Object.create;
22
26
  var __defProp = Object.defineProperty;
@@ -1173,6 +1177,9 @@ function isWebSocketUrl(url) {
1173
1177
  function isLoopbackHost(hostname) {
1174
1178
  return hostname === "localhost" || hostname === "127.0.0.1" || hostname === "::1" || hostname === "[::1]";
1175
1179
  }
1180
+ function hasProxyEnvConfigured() {
1181
+ return !!(process.env.HTTP_PROXY || process.env.HTTPS_PROXY || process.env.http_proxy || process.env.https_proxy);
1182
+ }
1176
1183
  function normalizeCdpWsUrl(wsUrl, cdpUrl) {
1177
1184
  const ws = new URL(wsUrl);
1178
1185
  const cdp = new URL(cdpUrl);
@@ -1434,8 +1441,81 @@ async function stopChrome(running, timeoutMs = 2500) {
1434
1441
  } catch {
1435
1442
  }
1436
1443
  }
1437
- var cached = null;
1438
- var connectingByUrl = /* @__PURE__ */ new Map();
1444
+ var LOOPBACK_ENTRIES = "localhost,127.0.0.1,[::1]";
1445
+ function noProxyAlreadyCoversLocalhost() {
1446
+ const current = process.env.NO_PROXY || process.env.no_proxy || "";
1447
+ return current.includes("localhost") && current.includes("127.0.0.1") && current.includes("[::1]");
1448
+ }
1449
+ function isLoopbackCdpUrl(url) {
1450
+ try {
1451
+ return isLoopbackHost(new URL(url).hostname);
1452
+ } catch {
1453
+ return false;
1454
+ }
1455
+ }
1456
+ var NoProxyLeaseManager = class {
1457
+ leaseCount = 0;
1458
+ snapshot = null;
1459
+ acquire(url) {
1460
+ if (!isLoopbackCdpUrl(url) || !hasProxyEnvConfigured()) return null;
1461
+ if (this.leaseCount === 0 && !noProxyAlreadyCoversLocalhost()) {
1462
+ const noProxy = process.env.NO_PROXY;
1463
+ const noProxyLower = process.env.no_proxy;
1464
+ const current = noProxy || noProxyLower || "";
1465
+ const applied = current ? `${current},${LOOPBACK_ENTRIES}` : LOOPBACK_ENTRIES;
1466
+ process.env.NO_PROXY = applied;
1467
+ process.env.no_proxy = applied;
1468
+ this.snapshot = { noProxy, noProxyLower, applied };
1469
+ }
1470
+ this.leaseCount += 1;
1471
+ let released = false;
1472
+ return () => {
1473
+ if (released) return;
1474
+ released = true;
1475
+ this.release();
1476
+ };
1477
+ }
1478
+ release() {
1479
+ if (this.leaseCount <= 0) return;
1480
+ this.leaseCount -= 1;
1481
+ if (this.leaseCount > 0 || !this.snapshot) return;
1482
+ const { noProxy, noProxyLower, applied } = this.snapshot;
1483
+ const currentNoProxy = process.env.NO_PROXY;
1484
+ const currentNoProxyLower = process.env.no_proxy;
1485
+ if (currentNoProxy === applied && (currentNoProxyLower === applied || currentNoProxyLower === void 0)) {
1486
+ if (noProxy !== void 0) process.env.NO_PROXY = noProxy;
1487
+ else delete process.env.NO_PROXY;
1488
+ if (noProxyLower !== void 0) process.env.no_proxy = noProxyLower;
1489
+ else delete process.env.no_proxy;
1490
+ }
1491
+ this.snapshot = null;
1492
+ }
1493
+ };
1494
+ var noProxyLeaseManager = new NoProxyLeaseManager();
1495
+ async function withNoProxyForCdpUrl(url, fn) {
1496
+ const release = noProxyLeaseManager.acquire(url);
1497
+ try {
1498
+ return await fn();
1499
+ } finally {
1500
+ release?.();
1501
+ }
1502
+ }
1503
+ new http__default.default.Agent();
1504
+ new https__default.default.Agent();
1505
+ function getHeadersWithAuth(endpoint, baseHeaders = {}) {
1506
+ const headers = { ...baseHeaders };
1507
+ try {
1508
+ const parsed = new URL(endpoint);
1509
+ if (parsed.username && parsed.password) {
1510
+ const credentials = Buffer.from(`${decodeURIComponent(parsed.username)}:${decodeURIComponent(parsed.password)}`).toString("base64");
1511
+ headers["Authorization"] = `Basic ${credentials}`;
1512
+ }
1513
+ } catch {
1514
+ }
1515
+ return headers;
1516
+ }
1517
+ var cachedByCdpUrl = /* @__PURE__ */ new Map();
1518
+ var connectingByCdpUrl = /* @__PURE__ */ new Map();
1439
1519
  var pageStates = /* @__PURE__ */ new WeakMap();
1440
1520
  var contextStates = /* @__PURE__ */ new WeakMap();
1441
1521
  var observedContexts = /* @__PURE__ */ new WeakSet();
@@ -1473,6 +1553,13 @@ function normalizeCdpUrl(raw) {
1473
1553
  function roleRefsKey(cdpUrl, targetId) {
1474
1554
  return `${normalizeCdpUrl(cdpUrl)}::${targetId}`;
1475
1555
  }
1556
+ function findNetworkRequestById(state, id) {
1557
+ for (let i = state.requests.length - 1; i >= 0; i--) {
1558
+ const candidate = state.requests[i];
1559
+ if (candidate && candidate.id === id) return candidate;
1560
+ }
1561
+ return void 0;
1562
+ }
1476
1563
  function ensurePageState(page) {
1477
1564
  const existing = pageStates.get(page);
1478
1565
  if (existing) return existing;
@@ -1524,25 +1611,19 @@ function ensurePageState(page) {
1524
1611
  const req = resp.request();
1525
1612
  const id = state.requestIds.get(req);
1526
1613
  if (!id) return;
1527
- for (let i = state.requests.length - 1; i >= 0; i--) {
1528
- const rec = state.requests[i];
1529
- if (rec && rec.id === id) {
1530
- rec.status = resp.status();
1531
- rec.ok = resp.ok();
1532
- break;
1533
- }
1614
+ const rec = findNetworkRequestById(state, id);
1615
+ if (rec) {
1616
+ rec.status = resp.status();
1617
+ rec.ok = resp.ok();
1534
1618
  }
1535
1619
  });
1536
1620
  page.on("requestfailed", (req) => {
1537
1621
  const id = state.requestIds.get(req);
1538
1622
  if (!id) return;
1539
- for (let i = state.requests.length - 1; i >= 0; i--) {
1540
- const rec = state.requests[i];
1541
- if (rec && rec.id === id) {
1542
- rec.failureText = req.failure()?.errorText;
1543
- rec.ok = false;
1544
- break;
1545
- }
1623
+ const rec = findNetworkRequestById(state, id);
1624
+ if (rec) {
1625
+ rec.failureText = req.failure()?.errorText;
1626
+ rec.ok = false;
1546
1627
  }
1547
1628
  });
1548
1629
  page.on("close", () => {
@@ -1577,12 +1658,8 @@ function observeContext(context) {
1577
1658
  function observeBrowser(browser) {
1578
1659
  for (const context of browser.contexts()) observeContext(context);
1579
1660
  }
1580
- function storeRoleRefsForTarget(opts) {
1581
- const state = ensurePageState(opts.page);
1582
- state.roleRefs = opts.refs;
1583
- state.roleRefsFrameSelector = opts.frameSelector;
1584
- state.roleRefsMode = opts.mode;
1585
- const targetId = opts.targetId?.trim();
1661
+ function rememberRoleRefsForTarget(opts) {
1662
+ const targetId = opts.targetId.trim();
1586
1663
  if (!targetId) return;
1587
1664
  roleRefsByTarget.set(roleRefsKey(opts.cdpUrl, targetId), {
1588
1665
  refs: opts.refs,
@@ -1595,6 +1672,20 @@ function storeRoleRefsForTarget(opts) {
1595
1672
  roleRefsByTarget.delete(first.value);
1596
1673
  }
1597
1674
  }
1675
+ function storeRoleRefsForTarget(opts) {
1676
+ const state = ensurePageState(opts.page);
1677
+ state.roleRefs = opts.refs;
1678
+ state.roleRefsFrameSelector = opts.frameSelector;
1679
+ state.roleRefsMode = opts.mode;
1680
+ if (!opts.targetId?.trim()) return;
1681
+ rememberRoleRefsForTarget({
1682
+ cdpUrl: opts.cdpUrl,
1683
+ targetId: opts.targetId,
1684
+ refs: opts.refs,
1685
+ frameSelector: opts.frameSelector,
1686
+ mode: opts.mode
1687
+ });
1688
+ }
1598
1689
  function restoreRoleRefsForTarget(opts) {
1599
1690
  const targetId = opts.targetId?.trim() || "";
1600
1691
  if (!targetId) return;
@@ -1608,8 +1699,9 @@ function restoreRoleRefsForTarget(opts) {
1608
1699
  }
1609
1700
  async function connectBrowser(cdpUrl, authToken) {
1610
1701
  const normalized = normalizeCdpUrl(cdpUrl);
1611
- if (cached?.cdpUrl === normalized) return cached;
1612
- const existing = connectingByUrl.get(normalized);
1702
+ const existing_cached = cachedByCdpUrl.get(normalized);
1703
+ if (existing_cached) return existing_cached;
1704
+ const existing = connectingByCdpUrl.get(normalized);
1613
1705
  if (existing) return await existing;
1614
1706
  const connectWithRetry = async () => {
1615
1707
  let lastErr;
@@ -1617,17 +1709,17 @@ async function connectBrowser(cdpUrl, authToken) {
1617
1709
  try {
1618
1710
  const timeout = 5e3 + attempt * 2e3;
1619
1711
  const endpoint = await getChromeWebSocketUrl(normalized, timeout, authToken).catch(() => null) ?? normalized;
1620
- const headers = {};
1621
- if (authToken) headers["Authorization"] = `Bearer ${authToken}`;
1622
- const browser = await playwrightCore.chromium.connectOverCDP(endpoint, { timeout, headers });
1712
+ const headers = getHeadersWithAuth(endpoint);
1713
+ if (authToken && !headers["Authorization"]) headers["Authorization"] = `Bearer ${authToken}`;
1714
+ const browser = await withNoProxyForCdpUrl(endpoint, () => playwrightCore.chromium.connectOverCDP(endpoint, { timeout, headers }));
1623
1715
  const onDisconnected = () => {
1624
- if (cached?.browser === browser) cached = null;
1716
+ if (cachedByCdpUrl.get(normalized)?.browser === browser) cachedByCdpUrl.delete(normalized);
1625
1717
  for (const key of roleRefsByTarget.keys()) {
1626
1718
  if (key.startsWith(normalized + "::")) roleRefsByTarget.delete(key);
1627
1719
  }
1628
1720
  };
1629
- const connected = { browser, cdpUrl: normalized, authToken, onDisconnected };
1630
- cached = connected;
1721
+ const connected = { browser, cdpUrl: normalized, onDisconnected };
1722
+ cachedByCdpUrl.set(normalized, connected);
1631
1723
  observeBrowser(browser);
1632
1724
  browser.on("disconnected", onDisconnected);
1633
1725
  return connected;
@@ -1640,24 +1732,25 @@ async function connectBrowser(cdpUrl, authToken) {
1640
1732
  throw lastErr instanceof Error ? lastErr : new Error("CDP connect failed");
1641
1733
  };
1642
1734
  const promise = connectWithRetry().finally(() => {
1643
- connectingByUrl.delete(normalized);
1735
+ connectingByCdpUrl.delete(normalized);
1644
1736
  });
1645
- connectingByUrl.set(normalized, promise);
1737
+ connectingByCdpUrl.set(normalized, promise);
1646
1738
  return await promise;
1647
1739
  }
1648
1740
  async function disconnectBrowser() {
1649
- if (connectingByUrl.size) {
1650
- for (const p of connectingByUrl.values()) {
1741
+ if (connectingByCdpUrl.size) {
1742
+ for (const p of connectingByCdpUrl.values()) {
1651
1743
  try {
1652
1744
  await p;
1653
1745
  } catch {
1654
1746
  }
1655
1747
  }
1656
1748
  }
1657
- const cur = cached;
1658
- cached = null;
1659
- if (cur) await cur.browser.close().catch(() => {
1660
- });
1749
+ for (const cur of cachedByCdpUrl.values()) {
1750
+ await cur.browser.close().catch(() => {
1751
+ });
1752
+ }
1753
+ cachedByCdpUrl.clear();
1661
1754
  }
1662
1755
  function cdpSocketNeedsAttach(wsUrl) {
1663
1756
  try {
@@ -1737,10 +1830,10 @@ async function tryTerminateExecutionViaCdp(cdpUrl, targetId) {
1737
1830
  }
1738
1831
  async function forceDisconnectPlaywrightForTarget(opts) {
1739
1832
  const normalized = normalizeCdpUrl(opts.cdpUrl);
1740
- const cur = cached;
1741
- if (!cur || cur.cdpUrl !== normalized) return;
1742
- cached = null;
1743
- connectingByUrl.delete(normalized);
1833
+ const cur = cachedByCdpUrl.get(normalized);
1834
+ if (!cur) return;
1835
+ cachedByCdpUrl.delete(normalized);
1836
+ connectingByCdpUrl.delete(normalized);
1744
1837
  if (cur.onDisconnected && typeof cur.browser.off === "function") {
1745
1838
  cur.browser.off("disconnected", cur.onDisconnected);
1746
1839
  }
@@ -1783,7 +1876,6 @@ async function findPageByTargetId(browser, targetId, cdpUrl) {
1783
1876
  try {
1784
1877
  const listUrl = `${normalizeCdpHttpBaseForJsonEndpoints(cdpUrl)}/json/list`;
1785
1878
  const headers = {};
1786
- if (cached?.authToken) headers["Authorization"] = `Bearer ${cached.authToken}`;
1787
1879
  const ctrl = new AbortController();
1788
1880
  const t = setTimeout(() => ctrl.abort(), 2e3);
1789
1881
  try {
@@ -2320,7 +2412,7 @@ function isAllowedNonNetworkNavigationUrl(parsed) {
2320
2412
  function isPrivateNetworkAllowedByPolicy(policy) {
2321
2413
  return policy?.dangerouslyAllowPrivateNetwork === true || policy?.allowPrivateNetwork === true;
2322
2414
  }
2323
- function hasProxyEnvConfigured(env = process.env) {
2415
+ function hasProxyEnvConfigured2(env = process.env) {
2324
2416
  for (const key of PROXY_ENV_KEYS) {
2325
2417
  const value = env[key];
2326
2418
  if (typeof value === "string" && value.trim().length > 0) return true;
@@ -2634,7 +2726,7 @@ async function assertBrowserNavigationAllowed(opts) {
2634
2726
  if (isAllowedNonNetworkNavigationUrl(parsed)) return;
2635
2727
  throw new InvalidBrowserNavigationUrlError(`Navigation blocked: unsupported protocol "${parsed.protocol}"`);
2636
2728
  }
2637
- if (hasProxyEnvConfigured() && !isPrivateNetworkAllowedByPolicy(opts.ssrfPolicy)) {
2729
+ if (hasProxyEnvConfigured2() && !isPrivateNetworkAllowedByPolicy(opts.ssrfPolicy)) {
2638
2730
  throw new InvalidBrowserNavigationUrlError(
2639
2731
  "Navigation blocked: strict browser SSRF policy cannot be enforced while env proxy variables are set"
2640
2732
  );