bb-browser 0.8.1 → 0.8.3

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/cli.js CHANGED
@@ -12,9 +12,10 @@ import "./chunk-D4HDZEJT.js";
12
12
  import { fileURLToPath as fileURLToPath3 } from "url";
13
13
 
14
14
  // packages/cli/src/cdp-client.ts
15
- import { readFileSync } from "fs";
15
+ import { readFileSync, unlinkSync, writeFileSync } from "fs";
16
16
  import { request as httpRequest } from "http";
17
17
  import { request as httpsRequest } from "https";
18
+ import os2 from "os";
18
19
  import path2 from "path";
19
20
  import { fileURLToPath } from "url";
20
21
  import WebSocket from "ws";
@@ -72,6 +73,10 @@ function findBrowserExecutable() {
72
73
  if (process.platform === "darwin") {
73
74
  const candidates = [
74
75
  "/Applications/Google Chrome.app/Contents/MacOS/Google Chrome",
76
+ "/Applications/Google Chrome Dev.app/Contents/MacOS/Google Chrome Dev",
77
+ "/Applications/Google Chrome Canary.app/Contents/MacOS/Google Chrome Canary",
78
+ "/Applications/Google Chrome Beta.app/Contents/MacOS/Google Chrome Beta",
79
+ "/Applications/Arc.app/Contents/MacOS/Arc",
75
80
  "/Applications/Microsoft Edge.app/Contents/MacOS/Microsoft Edge",
76
81
  "/Applications/Brave Browser.app/Contents/MacOS/Brave Browser"
77
82
  ];
@@ -159,25 +164,31 @@ async function discoverCdpPort() {
159
164
  if (Number.isInteger(explicitPort) && explicitPort > 0 && await canConnect("127.0.0.1", explicitPort)) {
160
165
  return { host: "127.0.0.1", port: explicitPort };
161
166
  }
167
+ try {
168
+ const rawPort = await readFile(MANAGED_PORT_FILE, "utf8");
169
+ const managedPort = Number.parseInt(rawPort.trim(), 10);
170
+ if (Number.isInteger(managedPort) && managedPort > 0 && await canConnect("127.0.0.1", managedPort)) {
171
+ return { host: "127.0.0.1", port: managedPort };
172
+ }
173
+ } catch {
174
+ }
162
175
  if (process.argv.includes("--openclaw")) {
163
176
  const viaOpenClaw = await tryOpenClaw();
164
177
  if (viaOpenClaw && await canConnect(viaOpenClaw.host, viaOpenClaw.port)) {
165
178
  return viaOpenClaw;
166
179
  }
167
180
  }
168
- const detectedOpenClaw = await tryOpenClaw();
169
- if (detectedOpenClaw && await canConnect(detectedOpenClaw.host, detectedOpenClaw.port)) {
170
- return detectedOpenClaw;
181
+ const launched = await launchManagedBrowser();
182
+ if (launched) {
183
+ return launched;
171
184
  }
172
- try {
173
- const rawPort = await readFile(MANAGED_PORT_FILE, "utf8");
174
- const managedPort = Number.parseInt(rawPort.trim(), 10);
175
- if (Number.isInteger(managedPort) && managedPort > 0 && await canConnect("127.0.0.1", managedPort)) {
176
- return { host: "127.0.0.1", port: managedPort };
185
+ if (!process.argv.includes("--openclaw")) {
186
+ const detectedOpenClaw = await tryOpenClaw();
187
+ if (detectedOpenClaw && await canConnect(detectedOpenClaw.host, detectedOpenClaw.port)) {
188
+ return detectedOpenClaw;
177
189
  }
178
- } catch {
179
190
  }
180
- return launchManagedBrowser();
191
+ return null;
181
192
  }
182
193
 
183
194
  // packages/cli/src/cdp-client.ts
@@ -233,7 +244,10 @@ function connectWebSocket(url) {
233
244
  return new Promise((resolve2, reject) => {
234
245
  const ws = new WebSocket(url);
235
246
  ws.once("open", () => {
236
- ws._socket?.unref?.();
247
+ const socket = ws._socket;
248
+ if (socket && typeof socket.unref === "function") {
249
+ socket.unref();
250
+ }
237
251
  resolve2(ws);
238
252
  });
239
253
  ws.once("error", reject);
@@ -503,27 +517,118 @@ async function ensurePageTarget(targetId) {
503
517
  target = targets[targetId] ?? targets.find((item) => Number(item.id) === targetId);
504
518
  } else if (typeof targetId === "string") {
505
519
  target = targets.find((item) => item.id === targetId);
520
+ if (!target) {
521
+ const numericTargetId = Number(targetId);
522
+ if (!Number.isNaN(numericTargetId)) {
523
+ target = targets[numericTargetId] ?? targets.find((item) => Number(item.id) === numericTargetId);
524
+ }
525
+ }
506
526
  }
507
527
  target ??= targets[0];
508
528
  connectionState.currentTargetId = target.id;
509
529
  await attachTarget(target.id);
510
530
  return target;
511
531
  }
512
- function parseRef(ref) {
513
- const refs = connectionState?.refsByTarget.get(connectionState.currentTargetId ?? "") ?? {};
532
+ async function resolveBackendNodeIdByXPath(targetId, xpath) {
533
+ await sessionCommand(targetId, "DOM.getDocument", { depth: 0 });
534
+ const search = await sessionCommand(targetId, "DOM.performSearch", {
535
+ query: xpath,
536
+ includeUserAgentShadowDOM: true
537
+ });
538
+ try {
539
+ if (!search.resultCount) {
540
+ throw new Error(`Unknown ref xpath: ${xpath}`);
541
+ }
542
+ const { nodeIds } = await sessionCommand(targetId, "DOM.getSearchResults", {
543
+ searchId: search.searchId,
544
+ fromIndex: 0,
545
+ toIndex: search.resultCount
546
+ });
547
+ for (const nodeId of nodeIds) {
548
+ const described = await sessionCommand(targetId, "DOM.describeNode", {
549
+ nodeId
550
+ });
551
+ if (described.node.backendNodeId) {
552
+ return described.node.backendNodeId;
553
+ }
554
+ }
555
+ throw new Error(`XPath resolved but no backend node id found: ${xpath}`);
556
+ } finally {
557
+ await sessionCommand(targetId, "DOM.discardSearchResults", { searchId: search.searchId }).catch(() => {
558
+ });
559
+ }
560
+ }
561
+ async function parseRef(ref) {
562
+ const targetId = connectionState?.currentTargetId ?? "";
563
+ let refs = connectionState?.refsByTarget.get(targetId) ?? {};
564
+ if (!refs[ref] && targetId) {
565
+ const persistedRefs = loadPersistedRefs(targetId);
566
+ if (persistedRefs) {
567
+ connectionState?.refsByTarget.set(targetId, persistedRefs);
568
+ refs = persistedRefs;
569
+ }
570
+ }
514
571
  const found = refs[ref];
515
- if (!found?.backendDOMNodeId) {
572
+ if (!found) {
516
573
  throw new Error(`Unknown ref: ${ref}. Run snapshot first.`);
517
574
  }
518
- return found.backendDOMNodeId;
575
+ if (found.backendDOMNodeId) {
576
+ return found.backendDOMNodeId;
577
+ }
578
+ if (targetId && found.xpath) {
579
+ const backendDOMNodeId = await resolveBackendNodeIdByXPath(targetId, found.xpath);
580
+ found.backendDOMNodeId = backendDOMNodeId;
581
+ connectionState?.refsByTarget.set(targetId, refs);
582
+ const pageUrl = await evaluate(targetId, "location.href", true).catch(() => void 0);
583
+ if (pageUrl) {
584
+ persistRefs(targetId, pageUrl, refs);
585
+ }
586
+ return backendDOMNodeId;
587
+ }
588
+ throw new Error(`Unknown ref: ${ref}. Run snapshot first.`);
589
+ }
590
+ function getRefsFilePath(targetId) {
591
+ return path2.join(os2.tmpdir(), `bb-browser-refs-${targetId}.json`);
592
+ }
593
+ function loadPersistedRefs(targetId, expectedUrl) {
594
+ try {
595
+ const data = JSON.parse(readFileSync(getRefsFilePath(targetId), "utf-8"));
596
+ if (data.targetId !== targetId) return null;
597
+ if (expectedUrl !== void 0 && data.url !== expectedUrl) return null;
598
+ if (!data.refs || typeof data.refs !== "object") return null;
599
+ return data.refs;
600
+ } catch {
601
+ return null;
602
+ }
603
+ }
604
+ function persistRefs(targetId, url, refs) {
605
+ try {
606
+ writeFileSync(getRefsFilePath(targetId), JSON.stringify({ targetId, url, timestamp: Date.now(), refs }));
607
+ } catch {
608
+ }
609
+ }
610
+ function clearPersistedRefs(targetId) {
611
+ try {
612
+ unlinkSync(getRefsFilePath(targetId));
613
+ } catch {
614
+ }
519
615
  }
520
616
  function loadBuildDomTreeScript() {
521
617
  const currentDir = path2.dirname(fileURLToPath(import.meta.url));
522
618
  const candidates = [
619
+ path2.resolve(currentDir, "./extension/buildDomTree.js"),
620
+ // npm installed: dist/cli.js → ../extension/buildDomTree.js
621
+ path2.resolve(currentDir, "../extension/buildDomTree.js"),
622
+ path2.resolve(currentDir, "../extension/dist/buildDomTree.js"),
623
+ path2.resolve(currentDir, "../packages/extension/public/buildDomTree.js"),
624
+ path2.resolve(currentDir, "../packages/extension/dist/buildDomTree.js"),
625
+ // dev mode: packages/cli/dist/ → ../../../extension/
523
626
  path2.resolve(currentDir, "../../../extension/buildDomTree.js"),
627
+ path2.resolve(currentDir, "../../../extension/dist/buildDomTree.js"),
628
+ // dev mode: packages/cli/src/ → ../../extension/
524
629
  path2.resolve(currentDir, "../../extension/buildDomTree.js"),
525
- path2.resolve(currentDir, "../../../packages/extension/buildDomTree.js"),
526
- path2.resolve(currentDir, "../../../extension/dist/buildDomTree.js")
630
+ path2.resolve(currentDir, "../../../packages/extension/dist/buildDomTree.js"),
631
+ path2.resolve(currentDir, "../../../packages/extension/public/buildDomTree.js")
527
632
  ];
528
633
  for (const candidate of candidates) {
529
634
  try {
@@ -551,8 +656,34 @@ async function resolveNode(targetId, backendNodeId) {
551
656
  return result.nodeId;
552
657
  }
553
658
  async function focusNode(targetId, backendNodeId) {
554
- const nodeId = await resolveNode(targetId, backendNodeId);
555
- await sessionCommand(targetId, "DOM.focus", { nodeId });
659
+ await sessionCommand(targetId, "DOM.focus", { backendNodeId });
660
+ }
661
+ async function insertTextIntoNode(targetId, backendNodeId, text, clearFirst) {
662
+ const resolved = await sessionCommand(targetId, "DOM.resolveNode", { backendNodeId });
663
+ await sessionCommand(targetId, "Runtime.callFunctionOn", {
664
+ objectId: resolved.object.objectId,
665
+ functionDeclaration: `function(value, clearFirst) {
666
+ if (typeof this.focus === 'function') this.focus();
667
+ if (clearFirst && ('value' in this)) {
668
+ this.value = '';
669
+ this.dispatchEvent(new Event('input', { bubbles: true }));
670
+ }
671
+ if ('value' in this) {
672
+ this.value = clearFirst ? value : String(this.value ?? '') + value;
673
+ this.dispatchEvent(new Event('input', { bubbles: true }));
674
+ this.dispatchEvent(new Event('change', { bubbles: true }));
675
+ return true;
676
+ }
677
+ return false;
678
+ }`,
679
+ arguments: [
680
+ { value: text },
681
+ { value: clearFirst }
682
+ ],
683
+ returnByValue: true
684
+ });
685
+ await focusNode(targetId, backendNodeId);
686
+ await sessionCommand(targetId, "Input.insertText", { text });
556
687
  }
557
688
  async function getNodeBox(targetId, backendNodeId) {
558
689
  const result = await sessionCommand(targetId, "DOM.getBoxModel", {
@@ -586,15 +717,181 @@ async function getAttributeValue(targetId, backendNodeId, attribute) {
586
717
  }
587
718
  async function buildSnapshot(targetId, request) {
588
719
  const script = loadBuildDomTreeScript();
589
- const expression = `(() => { ${script}; return (typeof buildDomTree === 'function' ? buildDomTree : globalThis.buildDomTree)(${JSON.stringify({
720
+ const buildArgs = {
721
+ showHighlightElements: true,
722
+ focusHighlightIndex: -1,
723
+ viewportExpansion: -1,
724
+ debugMode: false,
725
+ startId: 0,
726
+ startHighlightIndex: 0
727
+ };
728
+ const expression = `(() => { ${script}; const fn = globalThis.buildDomTree ?? (typeof window !== 'undefined' ? window.buildDomTree : undefined); if (typeof fn !== 'function') { throw new Error('buildDomTree is not available after script injection'); } return fn(${JSON.stringify({
729
+ ...buildArgs
730
+ })}); })()`;
731
+ const value = await evaluate(targetId, expression, true);
732
+ if (!value || !value.map || !value.rootId) {
733
+ const title = await evaluate(targetId, "document.title", true);
734
+ const pageUrl2 = await evaluate(targetId, "location.href", true);
735
+ const fallbackSnapshot = {
736
+ title,
737
+ url: pageUrl2,
738
+ lines: [title || pageUrl2],
739
+ refs: {}
740
+ };
741
+ connectionState?.refsByTarget.set(targetId, {});
742
+ persistRefs(targetId, pageUrl2, {});
743
+ return fallbackSnapshot;
744
+ }
745
+ const snapshot = convertBuildDomTreeResult(value, {
590
746
  interactiveOnly: !!request.interactive,
591
747
  compact: !!request.compact,
592
748
  maxDepth: request.maxDepth,
593
749
  selector: request.selector
594
- })}); })()`;
595
- const value = await evaluate(targetId, expression, true);
596
- connectionState?.refsByTarget.set(targetId, value.refs || {});
597
- return value;
750
+ });
751
+ const pageUrl = await evaluate(targetId, "location.href", true);
752
+ connectionState?.refsByTarget.set(targetId, snapshot.refs || {});
753
+ persistRefs(targetId, pageUrl, snapshot.refs || {});
754
+ return snapshot;
755
+ }
756
+ function convertBuildDomTreeResult(result, options) {
757
+ const { interactiveOnly, compact, maxDepth, selector } = options;
758
+ const { rootId, map } = result;
759
+ const refs = {};
760
+ const lines = [];
761
+ const getRole = (node) => {
762
+ const tagName = node.tagName.toLowerCase();
763
+ const role = node.attributes?.role;
764
+ if (role) return role;
765
+ const type = node.attributes?.type?.toLowerCase() || "text";
766
+ const inputRoleMap = {
767
+ text: "textbox",
768
+ password: "textbox",
769
+ email: "textbox",
770
+ url: "textbox",
771
+ tel: "textbox",
772
+ search: "searchbox",
773
+ number: "spinbutton",
774
+ range: "slider",
775
+ checkbox: "checkbox",
776
+ radio: "radio",
777
+ button: "button",
778
+ submit: "button",
779
+ reset: "button",
780
+ file: "button"
781
+ };
782
+ const roleMap = {
783
+ a: "link",
784
+ button: "button",
785
+ input: inputRoleMap[type] || "textbox",
786
+ select: "combobox",
787
+ textarea: "textbox",
788
+ img: "image",
789
+ nav: "navigation",
790
+ main: "main",
791
+ header: "banner",
792
+ footer: "contentinfo",
793
+ aside: "complementary",
794
+ form: "form",
795
+ table: "table",
796
+ ul: "list",
797
+ ol: "list",
798
+ li: "listitem",
799
+ h1: "heading",
800
+ h2: "heading",
801
+ h3: "heading",
802
+ h4: "heading",
803
+ h5: "heading",
804
+ h6: "heading",
805
+ dialog: "dialog",
806
+ article: "article",
807
+ section: "region",
808
+ label: "label",
809
+ details: "group",
810
+ summary: "button"
811
+ };
812
+ return roleMap[tagName] || tagName;
813
+ };
814
+ const collectTextContent = (node, nodeMap, depthLimit = 5) => {
815
+ const texts = [];
816
+ const visit = (nodeId, depth) => {
817
+ if (depth > depthLimit) return;
818
+ const currentNode = nodeMap[nodeId];
819
+ if (!currentNode) return;
820
+ if ("type" in currentNode && currentNode.type === "TEXT_NODE") {
821
+ const text = currentNode.text.trim();
822
+ if (text) texts.push(text);
823
+ return;
824
+ }
825
+ for (const childId of currentNode.children || []) visit(childId, depth + 1);
826
+ };
827
+ for (const childId of node.children || []) visit(childId, 0);
828
+ return texts.join(" ").trim();
829
+ };
830
+ const getName = (node) => {
831
+ const attrs = node.attributes || {};
832
+ return attrs["aria-label"] || attrs.title || attrs.placeholder || attrs.alt || attrs.value || collectTextContent(node, map) || attrs.name || void 0;
833
+ };
834
+ const truncateText = (text, length = 50) => text.length <= length ? text : `${text.slice(0, length - 3)}...`;
835
+ const selectorText = selector?.trim().toLowerCase();
836
+ const matchesSelector = (node, role, name) => {
837
+ if (!selectorText) return true;
838
+ const haystack = [node.tagName, role, name, node.xpath || "", ...Object.values(node.attributes || {})].join(" ").toLowerCase();
839
+ return haystack.includes(selectorText);
840
+ };
841
+ if (interactiveOnly) {
842
+ const interactiveNodes = Object.entries(map).filter(([, node]) => !("type" in node) && node.highlightIndex !== void 0 && node.highlightIndex !== null).map(([id, node]) => ({ id, node })).sort((a, b) => (a.node.highlightIndex ?? 0) - (b.node.highlightIndex ?? 0));
843
+ for (const { node } of interactiveNodes) {
844
+ const refId = String(node.highlightIndex);
845
+ const role = getRole(node);
846
+ const name = getName(node);
847
+ if (!matchesSelector(node, role, name)) continue;
848
+ let line = `${role} [ref=${refId}]`;
849
+ if (name) line += ` ${JSON.stringify(truncateText(name))}`;
850
+ lines.push(line);
851
+ refs[refId] = {
852
+ xpath: node.xpath || "",
853
+ role,
854
+ name,
855
+ tagName: node.tagName.toLowerCase()
856
+ };
857
+ }
858
+ return { snapshot: lines.join("\n"), refs };
859
+ }
860
+ const walk = (nodeId, depth) => {
861
+ if (maxDepth !== void 0 && depth > maxDepth) return;
862
+ const node = map[nodeId];
863
+ if (!node) return;
864
+ if ("type" in node && node.type === "TEXT_NODE") {
865
+ const text = node.text.trim();
866
+ if (!text) return;
867
+ lines.push(`${" ".repeat(depth)}- text ${JSON.stringify(truncateText(text, compact ? 80 : 120))}`);
868
+ return;
869
+ }
870
+ const role = getRole(node);
871
+ const name = getName(node);
872
+ if (!matchesSelector(node, role, name)) {
873
+ for (const childId of node.children || []) walk(childId, depth + 1);
874
+ return;
875
+ }
876
+ const indent = " ".repeat(depth);
877
+ const refId = node.highlightIndex !== void 0 && node.highlightIndex !== null ? String(node.highlightIndex) : null;
878
+ let line = `${indent}- ${role}`;
879
+ if (refId) line += ` [ref=${refId}]`;
880
+ if (name) line += ` ${JSON.stringify(truncateText(name, compact ? 50 : 80))}`;
881
+ if (!compact) line += ` <${node.tagName.toLowerCase()}>`;
882
+ lines.push(line);
883
+ if (refId) {
884
+ refs[refId] = {
885
+ xpath: node.xpath || "",
886
+ role,
887
+ name,
888
+ tagName: node.tagName.toLowerCase()
889
+ };
890
+ }
891
+ for (const childId of node.children || []) walk(childId, depth + 1);
892
+ };
893
+ walk(rootId, 0);
894
+ return { snapshot: lines.join("\n"), refs };
598
895
  }
599
896
  function ok(id, data) {
600
897
  return { id, success: true, data };
@@ -638,10 +935,12 @@ async function dispatchRequest(request) {
638
935
  if (request.tabId === void 0) {
639
936
  const created = await browserCommand("Target.createTarget", { url: request.url });
640
937
  const newTarget = await ensurePageTarget(created.targetId);
641
- return ok(request.id, { url: request.url, tabId: Number(newTarget.id) || void 0 });
938
+ return ok(request.id, { url: request.url, tabId: newTarget.id });
642
939
  }
643
940
  await pageCommand(target.id, "Page.navigate", { url: request.url });
644
- return ok(request.id, { url: request.url, title: target.title, tabId: Number(target.id) || void 0 });
941
+ connectionState?.refsByTarget.delete(target.id);
942
+ clearPersistedRefs(target.id);
943
+ return ok(request.id, { url: request.url, title: target.title, tabId: target.id });
645
944
  }
646
945
  case "snapshot": {
647
946
  const snapshotData = await buildSnapshot(target.id, request);
@@ -650,7 +949,7 @@ async function dispatchRequest(request) {
650
949
  case "click":
651
950
  case "hover": {
652
951
  if (!request.ref) return fail(request.id, "Missing ref parameter");
653
- const backendNodeId = parseRef(request.ref);
952
+ const backendNodeId = await parseRef(request.ref);
654
953
  const point = await getNodeBox(target.id, backendNodeId);
655
954
  await sessionCommand(target.id, "Input.dispatchMouseEvent", { type: "mouseMoved", x: point.x, y: point.y, button: "none" });
656
955
  if (request.action === "click") await mouseClick(target.id, point.x, point.y);
@@ -660,18 +959,14 @@ async function dispatchRequest(request) {
660
959
  case "type": {
661
960
  if (!request.ref) return fail(request.id, "Missing ref parameter");
662
961
  if (request.text == null) return fail(request.id, "Missing text parameter");
663
- const backendNodeId = parseRef(request.ref);
664
- await focusNode(target.id, backendNodeId);
665
- if (request.action === "fill") {
666
- await evaluate(target.id, `document.activeElement && ((document.activeElement.value = ''), document.activeElement.dispatchEvent(new Event('input', { bubbles: true })))`);
667
- }
668
- await sessionCommand(target.id, "Input.insertText", { text: request.text });
962
+ const backendNodeId = await parseRef(request.ref);
963
+ await insertTextIntoNode(target.id, backendNodeId, request.text, request.action === "fill");
669
964
  return ok(request.id, { value: request.text });
670
965
  }
671
966
  case "check":
672
967
  case "uncheck": {
673
968
  if (!request.ref) return fail(request.id, "Missing ref parameter");
674
- const backendNodeId = parseRef(request.ref);
969
+ const backendNodeId = await parseRef(request.ref);
675
970
  const desired = request.action === "check";
676
971
  const resolved = await sessionCommand(target.id, "DOM.resolveNode", { backendNodeId });
677
972
  await sessionCommand(target.id, "Runtime.callFunctionOn", {
@@ -682,7 +977,7 @@ async function dispatchRequest(request) {
682
977
  }
683
978
  case "select": {
684
979
  if (!request.ref || request.value == null) return fail(request.id, "Missing ref or value parameter");
685
- const backendNodeId = parseRef(request.ref);
980
+ const backendNodeId = await parseRef(request.ref);
686
981
  const resolved = await sessionCommand(target.id, "DOM.resolveNode", { backendNodeId });
687
982
  await sessionCommand(target.id, "Runtime.callFunctionOn", {
688
983
  objectId: resolved.object.objectId,
@@ -692,7 +987,7 @@ async function dispatchRequest(request) {
692
987
  }
693
988
  case "get": {
694
989
  if (!request.ref || !request.attribute) return fail(request.id, "Missing ref or attribute parameter");
695
- const value = await getAttributeValue(target.id, parseRef(request.ref), request.attribute);
990
+ const value = await getAttributeValue(target.id, await parseRef(request.ref), request.attribute);
696
991
  return ok(request.id, { value });
697
992
  }
698
993
  case "screenshot": {
@@ -701,6 +996,8 @@ async function dispatchRequest(request) {
701
996
  }
702
997
  case "close": {
703
998
  await browserCommand("Target.closeTarget", { targetId: target.id });
999
+ connectionState?.refsByTarget.delete(target.id);
1000
+ clearPersistedRefs(target.id);
704
1001
  return ok(request.id, {});
705
1002
  }
706
1003
  case "wait": {
@@ -739,12 +1036,12 @@ async function dispatchRequest(request) {
739
1036
  return ok(request.id, { result });
740
1037
  }
741
1038
  case "tab_list": {
742
- const tabs = (await getTargets()).filter((item) => item.type === "page").map((item, index) => ({ index, url: item.url, title: item.title, active: item.id === connectionState?.currentTargetId || !connectionState?.currentTargetId && index === 0, tabId: Number(item.id) || index }));
1039
+ const tabs = (await getTargets()).filter((item) => item.type === "page").map((item, index) => ({ index, url: item.url, title: item.title, active: item.id === connectionState?.currentTargetId || !connectionState?.currentTargetId && index === 0, tabId: item.id }));
743
1040
  return ok(request.id, { tabs, activeIndex: tabs.findIndex((tab) => tab.active) });
744
1041
  }
745
1042
  case "tab_new": {
746
1043
  const created = await browserCommand("Target.createTarget", { url: request.url ?? "about:blank" });
747
- return ok(request.id, { tabId: Number(created.targetId) || void 0, url: request.url ?? "about:blank" });
1044
+ return ok(request.id, { tabId: created.targetId, url: request.url ?? "about:blank" });
748
1045
  }
749
1046
  case "tab_select": {
750
1047
  const tabs = (await getTargets()).filter((item) => item.type === "page");
@@ -752,14 +1049,16 @@ async function dispatchRequest(request) {
752
1049
  if (!selected) return fail(request.id, "Tab not found");
753
1050
  connectionState.currentTargetId = selected.id;
754
1051
  await attachTarget(selected.id);
755
- return ok(request.id, { tabId: Number(selected.id) || void 0, url: selected.url, title: selected.title });
1052
+ return ok(request.id, { tabId: selected.id, url: selected.url, title: selected.title });
756
1053
  }
757
1054
  case "tab_close": {
758
1055
  const tabs = (await getTargets()).filter((item) => item.type === "page");
759
1056
  const selected = request.tabId !== void 0 ? tabs.find((item) => item.id === String(request.tabId) || Number(item.id) === request.tabId) : tabs[request.index ?? 0];
760
1057
  if (!selected) return fail(request.id, "Tab not found");
761
1058
  await browserCommand("Target.closeTarget", { targetId: selected.id });
762
- return ok(request.id, { tabId: Number(selected.id) || void 0 });
1059
+ connectionState?.refsByTarget.delete(selected.id);
1060
+ clearPersistedRefs(selected.id);
1061
+ return ok(request.id, { tabId: selected.id });
763
1062
  }
764
1063
  case "frame": {
765
1064
  if (!request.selector) return fail(request.id, "Missing selector parameter");
@@ -891,9 +1190,6 @@ async function sendCommand2(request) {
891
1190
  import { fileURLToPath as fileURLToPath2 } from "url";
892
1191
  import { dirname, resolve } from "path";
893
1192
  import { existsSync as existsSync2 } from "fs";
894
- async function isDaemonRunning() {
895
- return await discoverCdpPort() !== null;
896
- }
897
1193
  async function ensureDaemonRunning() {
898
1194
  try {
899
1195
  await ensureCdpConnection();
@@ -911,7 +1207,7 @@ async function ensureDaemonRunning() {
911
1207
  }
912
1208
 
913
1209
  // packages/cli/src/history-sqlite.ts
914
- import { copyFileSync, existsSync as existsSync3, unlinkSync } from "fs";
1210
+ import { copyFileSync, existsSync as existsSync3, unlinkSync as unlinkSync2 } from "fs";
915
1211
  import { execSync as execSync2 } from "child_process";
916
1212
  import { homedir, tmpdir } from "os";
917
1213
  import { join } from "path";
@@ -969,7 +1265,7 @@ function runHistoryQuery(sql, mapRow) {
969
1265
  return [];
970
1266
  } finally {
971
1267
  try {
972
- unlinkSync(tmpPath);
1268
+ unlinkSync2(tmpPath);
973
1269
  } catch {
974
1270
  }
975
1271
  }
@@ -1960,11 +2256,11 @@ async function getCommand(attribute, ref, options = {}) {
1960
2256
  // packages/cli/src/commands/screenshot.ts
1961
2257
  import fs from "fs";
1962
2258
  import path3 from "path";
1963
- import os2 from "os";
2259
+ import os3 from "os";
1964
2260
  function getDefaultPath() {
1965
2261
  const timestamp = (/* @__PURE__ */ new Date()).toISOString().replace(/[:.]/g, "-");
1966
2262
  const filename = `bb-screenshot-${timestamp}.png`;
1967
- return path3.join(os2.tmpdir(), filename);
2263
+ return path3.join(os3.tmpdir(), filename);
1968
2264
  }
1969
2265
  function saveBase64Image(dataUrl, filePath) {
1970
2266
  const base64Data = dataUrl.replace(/^data:image\/png;base64,/, "");
@@ -2140,123 +2436,6 @@ async function scrollCommand(direction, pixels, options = {}) {
2140
2436
  }
2141
2437
  }
2142
2438
 
2143
- // packages/cli/src/commands/daemon.ts
2144
- async function statusCommand(options = {}) {
2145
- const running = await isDaemonRunning();
2146
- if (options.json) {
2147
- console.log(JSON.stringify({ running }));
2148
- } else {
2149
- console.log(running ? "\u6D4F\u89C8\u5668\u8FD0\u884C\u4E2D" : "\u6D4F\u89C8\u5668\u672A\u8FD0\u884C");
2150
- }
2151
- }
2152
-
2153
- // packages/cli/src/commands/reload.ts
2154
- import WebSocket2 from "ws";
2155
- var EXTENSION_NAME = "bb-browser";
2156
- async function reloadCommand(options = {}) {
2157
- const port = options.port || 9222;
2158
- try {
2159
- const listRes = await fetch(`http://127.0.0.1:${port}/json/list`);
2160
- if (!listRes.ok) {
2161
- throw new Error(`CDP \u672A\u542F\u7528\u3002\u8BF7\u7528 --remote-debugging-port=${port} \u542F\u52A8 Chrome`);
2162
- }
2163
- const list = await listRes.json();
2164
- const extPage = list.find(
2165
- (t) => t.type === "page" && t.url.includes("chrome://extensions")
2166
- );
2167
- if (!extPage) {
2168
- throw new Error("\u8BF7\u5148\u6253\u5F00 chrome://extensions \u9875\u9762");
2169
- }
2170
- const result = await new Promise((resolve2, reject) => {
2171
- const ws = new WebSocket2(extPage.webSocketDebuggerUrl);
2172
- let resolved = false;
2173
- const timeout = setTimeout(() => {
2174
- if (!resolved) {
2175
- resolved = true;
2176
- ws.close();
2177
- reject(new Error("CDP \u8FDE\u63A5\u8D85\u65F6"));
2178
- }
2179
- }, 1e4);
2180
- ws.on("open", () => {
2181
- const script = `
2182
- (async function() {
2183
- if (!chrome || !chrome.developerPrivate) {
2184
- return { error: 'developerPrivate API not available' };
2185
- }
2186
-
2187
- try {
2188
- const exts = await chrome.developerPrivate.getExtensionsInfo();
2189
- const bbExt = exts.find(e => e.name === '${EXTENSION_NAME}');
2190
-
2191
- if (!bbExt) {
2192
- return { error: '${EXTENSION_NAME} \u6269\u5C55\u672A\u5B89\u88C5' };
2193
- }
2194
-
2195
- if (bbExt.state !== 'ENABLED') {
2196
- return { error: '${EXTENSION_NAME} \u6269\u5C55\u5DF2\u7981\u7528' };
2197
- }
2198
-
2199
- await chrome.developerPrivate.reload(bbExt.id, {failQuietly: true});
2200
- return { success: true, extensionId: bbExt.id };
2201
- } catch (e) {
2202
- return { error: e.message };
2203
- }
2204
- })()
2205
- `;
2206
- ws.send(JSON.stringify({
2207
- id: 1,
2208
- method: "Runtime.evaluate",
2209
- params: {
2210
- expression: script,
2211
- awaitPromise: true,
2212
- returnByValue: true
2213
- }
2214
- }));
2215
- });
2216
- ws.on("message", (data) => {
2217
- const msg = JSON.parse(data.toString());
2218
- if (msg.id === 1) {
2219
- clearTimeout(timeout);
2220
- resolved = true;
2221
- ws.close();
2222
- const value = msg.result?.result?.value;
2223
- if (value?.success) {
2224
- resolve2({
2225
- success: true,
2226
- message: "\u6269\u5C55\u5DF2\u91CD\u8F7D",
2227
- extensionId: value.extensionId
2228
- });
2229
- } else if (value?.error) {
2230
- reject(new Error(value.error));
2231
- } else {
2232
- reject(new Error(`\u91CD\u8F7D\u5931\u8D25: ${JSON.stringify(value)}`));
2233
- }
2234
- }
2235
- });
2236
- ws.on("error", (err) => {
2237
- clearTimeout(timeout);
2238
- if (!resolved) {
2239
- resolved = true;
2240
- reject(new Error(`CDP \u8FDE\u63A5\u5931\u8D25: ${err.message}`));
2241
- }
2242
- });
2243
- });
2244
- if (options.json) {
2245
- console.log(JSON.stringify(result));
2246
- } else {
2247
- console.log(`${result.message} (${result.extensionId})`);
2248
- }
2249
- } catch (error) {
2250
- const message = error instanceof Error ? error.message : String(error);
2251
- if (options.json) {
2252
- console.log(JSON.stringify({ success: false, error: message }));
2253
- } else {
2254
- console.error(`\u9519\u8BEF: ${message}`);
2255
- }
2256
- process.exit(1);
2257
- }
2258
- }
2259
-
2260
2439
  // packages/cli/src/commands/nav.ts
2261
2440
  async function backCommand(options = {}) {
2262
2441
  await ensureDaemonRunning();
@@ -2510,6 +2689,9 @@ function parseTabSubcommand(args, rawArgv) {
2510
2689
  return { action: "tab_list" };
2511
2690
  }
2512
2691
  const first = args[0];
2692
+ if (first === "list") {
2693
+ return { action: "tab_list" };
2694
+ }
2513
2695
  if (first === "new") {
2514
2696
  return { action: "tab_new", url: args[1] };
2515
2697
  }
@@ -3038,9 +3220,9 @@ async function fetchCommand(url, options = {}) {
3038
3220
  throw new Error(`Fetch error: ${result.error}`);
3039
3221
  }
3040
3222
  if (options.output) {
3041
- const { writeFileSync } = await import("fs");
3223
+ const { writeFileSync: writeFileSync2 } = await import("fs");
3042
3224
  const content = typeof result.body === "object" ? JSON.stringify(result.body, null, 2) : String(result.body);
3043
- writeFileSync(options.output, content, "utf-8");
3225
+ writeFileSync2(options.output, content, "utf-8");
3044
3226
  console.log(`\u5DF2\u5199\u5165 ${options.output} (${result.status}, ${content.length} bytes)`);
3045
3227
  return;
3046
3228
  }
@@ -3101,7 +3283,7 @@ async function historyCommand(subCommand, options = {}) {
3101
3283
  }
3102
3284
 
3103
3285
  // packages/cli/src/index.ts
3104
- var VERSION = "0.8.0";
3286
+ var VERSION = "0.8.2";
3105
3287
  var HELP_TEXT = `
3106
3288
  bb-browser - AI Agent \u6D4F\u89C8\u5668\u81EA\u52A8\u5316\u5DE5\u5177
3107
3289
 
@@ -3419,24 +3601,6 @@ async function main() {
3419
3601
  break;
3420
3602
  }
3421
3603
  case "daemon":
3422
- case "start": {
3423
- const hostIdx = process.argv.findIndex((a) => a === "--host");
3424
- const host = hostIdx >= 0 ? process.argv[hostIdx + 1] : void 0;
3425
- await daemonCommand({ json: parsed.flags.json, host });
3426
- break;
3427
- }
3428
- case "stop": {
3429
- await stopCommand({ json: parsed.flags.json });
3430
- break;
3431
- }
3432
- case "status": {
3433
- await statusCommand({ json: parsed.flags.json });
3434
- break;
3435
- }
3436
- case "reload": {
3437
- await reloadCommand({ json: parsed.flags.json });
3438
- break;
3439
- }
3440
3604
  case "close": {
3441
3605
  await closeCommand({ json: parsed.flags.json, tabId: globalTabId });
3442
3606
  break;