browserclaw 0.2.4 → 0.2.6

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,7 +1,9 @@
1
- # browserclaw
1
+ <h2 align="center">🦞 BrowserClaw — Standalone OpenClaw browser module</h1>
2
2
 
3
- [![npm version](https://img.shields.io/npm/v/browserclaw.svg)](https://www.npmjs.com/package/browserclaw)
4
- [![License: MIT](https://img.shields.io/badge/License-MIT-blue.svg)](./LICENSE)
3
+ <p align="center">
4
+ <a href="https://www.npmjs.com/package/browserclaw"><img src="https://img.shields.io/npm/v/browserclaw.svg" alt="npm version" /></a>
5
+ <a href="./LICENSE"><img src="https://img.shields.io/badge/License-MIT-blue.svg" alt="License: MIT" /></a>
6
+ </p>
5
7
 
6
8
  Extracted and refined from [OpenClaw](https://github.com/openclaw/openclaw)'s browser automation module. A standalone, typed library for AI-friendly browser control with **snapshot + ref targeting** — no CSS selectors, no XPath, no vision, just numbered refs that map to interactive elements.
7
9
 
package/dist/index.cjs CHANGED
@@ -408,7 +408,8 @@ async function launchChrome(opts = {}) {
408
408
  args.push("--no-sandbox", "--disable-setuid-sandbox");
409
409
  }
410
410
  if (process.platform === "linux") args.push("--disable-dev-shm-usage");
411
- if (opts.chromeArgs?.length) args.push(...opts.chromeArgs);
411
+ const extraArgs = Array.isArray(opts.chromeArgs) ? opts.chromeArgs.filter((a) => typeof a === "string" && a.trim().length > 0) : [];
412
+ if (extraArgs.length) args.push(...extraArgs);
412
413
  args.push("about:blank");
413
414
  return child_process.spawn(exe.path, args, {
414
415
  stdio: "pipe",
@@ -701,10 +702,20 @@ async function pageTargetId(page) {
701
702
  }
702
703
  async function findPageByTargetId(browser, targetId, cdpUrl) {
703
704
  const pages = await getAllPages(browser);
705
+ let resolvedViaCdp = false;
704
706
  for (const page of pages) {
705
- const tid = await pageTargetId(page).catch(() => null);
707
+ let tid = null;
708
+ try {
709
+ tid = await pageTargetId(page);
710
+ resolvedViaCdp = true;
711
+ } catch {
712
+ tid = null;
713
+ }
706
714
  if (tid && tid === targetId) return page;
707
715
  }
716
+ if (!resolvedViaCdp && pages.length === 1) {
717
+ return pages[0];
718
+ }
708
719
  if (cdpUrl) {
709
720
  try {
710
721
  const listUrl = `${cdpUrl.replace(/\/+$/, "").replace(/^ws:/, "http:").replace(/\/cdp$/, "")}/json/list`;
@@ -835,6 +846,27 @@ function getIndentLevel(line) {
835
846
  const match = line.match(/^(\s*)/);
836
847
  return match ? Math.floor(match[1].length / 2) : 0;
837
848
  }
849
+ function matchInteractiveSnapshotLine(line, options) {
850
+ const depth = getIndentLevel(line);
851
+ if (options.maxDepth !== void 0 && depth > options.maxDepth) {
852
+ return null;
853
+ }
854
+ const match = line.match(/^(\s*-\s*)(\w+)(?:\s+"([^"]*)")?(.*)$/);
855
+ if (!match) {
856
+ return null;
857
+ }
858
+ const [, , roleRaw, name, suffix] = match;
859
+ if (roleRaw.startsWith("/")) {
860
+ return null;
861
+ }
862
+ const role = roleRaw.toLowerCase();
863
+ return {
864
+ roleRaw,
865
+ role,
866
+ ...name ? { name } : {},
867
+ suffix
868
+ };
869
+ }
838
870
  function createRoleNameTracker() {
839
871
  const counts = /* @__PURE__ */ new Map();
840
872
  const refsByKey = /* @__PURE__ */ new Map();
@@ -908,14 +940,11 @@ function buildRoleSnapshotFromAriaSnapshot(ariaSnapshot, options = {}) {
908
940
  if (options.interactive) {
909
941
  const result2 = [];
910
942
  for (const line of lines) {
911
- const depth = getIndentLevel(line);
912
- if (options.maxDepth !== void 0 && depth > options.maxDepth) continue;
913
- const match = line.match(/^(\s*-\s*)(\w+)(?:\s+"([^"]*)")?(.*)$/);
914
- if (!match) continue;
915
- const [, prefix, roleRaw, name, suffix] = match;
916
- if (roleRaw.startsWith("/")) continue;
917
- const role = roleRaw.toLowerCase();
943
+ const parsed = matchInteractiveSnapshotLine(line, options);
944
+ if (!parsed) continue;
945
+ const { roleRaw, role, name, suffix } = parsed;
918
946
  if (!INTERACTIVE_ROLES.has(role)) continue;
947
+ const prefix = line.match(/^(\s*-\s*)/)?.[1] ?? "";
919
948
  const ref = nextRef();
920
949
  const nth = tracker.getNextIndex(role, name);
921
950
  tracker.trackRef(role, name, ref);
@@ -978,16 +1007,13 @@ function buildRoleSnapshotFromAiSnapshot(aiSnapshot, options = {}) {
978
1007
  if (options.interactive) {
979
1008
  const out2 = [];
980
1009
  for (const line of lines) {
981
- const depth = getIndentLevel(line);
982
- if (options.maxDepth !== void 0 && depth > options.maxDepth) continue;
983
- const match = line.match(/^(\s*-\s*)(\w+)(?:\s+"([^"]*)")?(.*)$/);
984
- if (!match) continue;
985
- const [, prefix, roleRaw, name, suffix] = match;
986
- if (roleRaw.startsWith("/")) continue;
987
- const role = roleRaw.toLowerCase();
1010
+ const parsed = matchInteractiveSnapshotLine(line, options);
1011
+ if (!parsed) continue;
1012
+ const { roleRaw, role, name, suffix } = parsed;
988
1013
  if (!INTERACTIVE_ROLES.has(role)) continue;
989
1014
  const ref = parseAiSnapshotRef(suffix);
990
1015
  if (!ref) continue;
1016
+ const prefix = line.match(/^(\s*-\s*)/)?.[1] ?? "";
991
1017
  refs[ref] = { role, ...name ? { name } : {} };
992
1018
  out2.push(`${prefix}${roleRaw}${name ? ` "${name}"` : ""}${suffix}`);
993
1019
  }
@@ -1380,6 +1406,60 @@ function assertSafeOutputPath(path2, allowedRoots) {
1380
1406
  }
1381
1407
  }
1382
1408
  }
1409
+ function expandIPv6(ip) {
1410
+ let normalized = ip;
1411
+ const v4Match = normalized.match(/^(.+:)(\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3})$/);
1412
+ if (v4Match) {
1413
+ const octets = v4Match[2].split(".").map(Number);
1414
+ if (octets.some((o) => o > 255)) return null;
1415
+ const hexHi = (octets[0] << 8 | octets[1]).toString(16).padStart(4, "0");
1416
+ const hexLo = (octets[2] << 8 | octets[3]).toString(16).padStart(4, "0");
1417
+ normalized = v4Match[1] + hexHi + ":" + hexLo;
1418
+ }
1419
+ const halves = normalized.split("::");
1420
+ if (halves.length > 2) return null;
1421
+ if (halves.length === 2) {
1422
+ const left = halves[0] !== "" ? halves[0].split(":") : [];
1423
+ const right = halves[1] !== "" ? halves[1].split(":") : [];
1424
+ const needed = 8 - left.length - right.length;
1425
+ if (needed < 0) return null;
1426
+ const groups2 = [...left, ...Array(needed).fill("0"), ...right];
1427
+ if (groups2.length !== 8) return null;
1428
+ return groups2.map((g) => g.padStart(4, "0")).join(":");
1429
+ }
1430
+ const groups = normalized.split(":");
1431
+ if (groups.length !== 8) return null;
1432
+ return groups.map((g) => g.padStart(4, "0")).join(":");
1433
+ }
1434
+ function hexToIPv4(hiHex, loHex) {
1435
+ const hi = parseInt(hiHex, 16);
1436
+ const lo = parseInt(loHex, 16);
1437
+ return `${hi >> 8 & 255}.${hi & 255}.${lo >> 8 & 255}.${lo & 255}`;
1438
+ }
1439
+ function extractEmbeddedIPv4(lower) {
1440
+ if (lower.startsWith("::ffff:")) {
1441
+ return lower.slice(7);
1442
+ }
1443
+ const expanded = expandIPv6(lower);
1444
+ if (expanded === null) return "";
1445
+ const groups = expanded.split(":");
1446
+ if (groups.length !== 8) return "";
1447
+ if (groups[0] === "0064" && groups[1] === "ff9b" && groups[2] === "0000" && groups[3] === "0000" && groups[4] === "0000" && groups[5] === "0000") {
1448
+ return hexToIPv4(groups[6], groups[7]);
1449
+ }
1450
+ if (groups[0] === "0064" && groups[1] === "ff9b" && groups[2] === "0001") {
1451
+ return hexToIPv4(groups[6], groups[7]);
1452
+ }
1453
+ if (groups[0] === "2002") {
1454
+ return hexToIPv4(groups[1], groups[2]);
1455
+ }
1456
+ if (groups[0] === "2001" && groups[1] === "0000") {
1457
+ const hiXored = (parseInt(groups[6], 16) ^ 65535).toString(16).padStart(4, "0");
1458
+ const loXored = (parseInt(groups[7], 16) ^ 65535).toString(16).padStart(4, "0");
1459
+ return hexToIPv4(hiXored, loXored);
1460
+ }
1461
+ return null;
1462
+ }
1383
1463
  function isInternalIP(ip) {
1384
1464
  if (/^127\./.test(ip)) return true;
1385
1465
  if (/^10\./.test(ip)) return true;
@@ -1392,9 +1472,10 @@ function isInternalIP(ip) {
1392
1472
  if (lower === "::1") return true;
1393
1473
  if (lower.startsWith("fe80:")) return true;
1394
1474
  if (lower.startsWith("fc") || lower.startsWith("fd")) return true;
1395
- if (lower.startsWith("::ffff:")) {
1396
- const v4 = lower.replace(/^::ffff:/, "");
1397
- return isInternalIP(v4);
1475
+ const embedded = extractEmbeddedIPv4(lower);
1476
+ if (embedded !== null) {
1477
+ if (embedded === "") return true;
1478
+ return isInternalIP(embedded);
1398
1479
  }
1399
1480
  return false;
1400
1481
  }