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 +2 -1
- package/dist/index.cjs +137 -45
- package/dist/index.cjs.map +1 -1
- package/dist/index.js +135 -45
- package/dist/index.js.map +1 -1
- package/package.json +2 -2
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.
|
|
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
|
|
1438
|
-
|
|
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
|
-
|
|
1528
|
-
|
|
1529
|
-
|
|
1530
|
-
|
|
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
|
-
|
|
1540
|
-
|
|
1541
|
-
|
|
1542
|
-
|
|
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
|
|
1581
|
-
const
|
|
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
|
-
|
|
1612
|
-
|
|
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 (
|
|
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,
|
|
1630
|
-
|
|
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
|
-
|
|
1735
|
+
connectingByCdpUrl.delete(normalized);
|
|
1644
1736
|
});
|
|
1645
|
-
|
|
1737
|
+
connectingByCdpUrl.set(normalized, promise);
|
|
1646
1738
|
return await promise;
|
|
1647
1739
|
}
|
|
1648
1740
|
async function disconnectBrowser() {
|
|
1649
|
-
if (
|
|
1650
|
-
for (const p of
|
|
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
|
|
1658
|
-
|
|
1659
|
-
|
|
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 =
|
|
1741
|
-
if (!cur
|
|
1742
|
-
|
|
1743
|
-
|
|
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
|
|
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 (
|
|
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
|
);
|