bb-browser 0.8.1 → 0.8.2

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";
@@ -159,25 +160,31 @@ async function discoverCdpPort() {
159
160
  if (Number.isInteger(explicitPort) && explicitPort > 0 && await canConnect("127.0.0.1", explicitPort)) {
160
161
  return { host: "127.0.0.1", port: explicitPort };
161
162
  }
163
+ try {
164
+ const rawPort = await readFile(MANAGED_PORT_FILE, "utf8");
165
+ const managedPort = Number.parseInt(rawPort.trim(), 10);
166
+ if (Number.isInteger(managedPort) && managedPort > 0 && await canConnect("127.0.0.1", managedPort)) {
167
+ return { host: "127.0.0.1", port: managedPort };
168
+ }
169
+ } catch {
170
+ }
162
171
  if (process.argv.includes("--openclaw")) {
163
172
  const viaOpenClaw = await tryOpenClaw();
164
173
  if (viaOpenClaw && await canConnect(viaOpenClaw.host, viaOpenClaw.port)) {
165
174
  return viaOpenClaw;
166
175
  }
167
176
  }
168
- const detectedOpenClaw = await tryOpenClaw();
169
- if (detectedOpenClaw && await canConnect(detectedOpenClaw.host, detectedOpenClaw.port)) {
170
- return detectedOpenClaw;
177
+ const launched = await launchManagedBrowser();
178
+ if (launched) {
179
+ return launched;
171
180
  }
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 };
181
+ if (!process.argv.includes("--openclaw")) {
182
+ const detectedOpenClaw = await tryOpenClaw();
183
+ if (detectedOpenClaw && await canConnect(detectedOpenClaw.host, detectedOpenClaw.port)) {
184
+ return detectedOpenClaw;
177
185
  }
178
- } catch {
179
186
  }
180
- return launchManagedBrowser();
187
+ return null;
181
188
  }
182
189
 
183
190
  // packages/cli/src/cdp-client.ts
@@ -233,7 +240,10 @@ function connectWebSocket(url) {
233
240
  return new Promise((resolve2, reject) => {
234
241
  const ws = new WebSocket(url);
235
242
  ws.once("open", () => {
236
- ws._socket?.unref?.();
243
+ const socket = ws._socket;
244
+ if (socket && typeof socket.unref === "function") {
245
+ socket.unref();
246
+ }
237
247
  resolve2(ws);
238
248
  });
239
249
  ws.once("error", reject);
@@ -503,27 +513,118 @@ async function ensurePageTarget(targetId) {
503
513
  target = targets[targetId] ?? targets.find((item) => Number(item.id) === targetId);
504
514
  } else if (typeof targetId === "string") {
505
515
  target = targets.find((item) => item.id === targetId);
516
+ if (!target) {
517
+ const numericTargetId = Number(targetId);
518
+ if (!Number.isNaN(numericTargetId)) {
519
+ target = targets[numericTargetId] ?? targets.find((item) => Number(item.id) === numericTargetId);
520
+ }
521
+ }
506
522
  }
507
523
  target ??= targets[0];
508
524
  connectionState.currentTargetId = target.id;
509
525
  await attachTarget(target.id);
510
526
  return target;
511
527
  }
512
- function parseRef(ref) {
513
- const refs = connectionState?.refsByTarget.get(connectionState.currentTargetId ?? "") ?? {};
528
+ async function resolveBackendNodeIdByXPath(targetId, xpath) {
529
+ await sessionCommand(targetId, "DOM.getDocument", { depth: 0 });
530
+ const search = await sessionCommand(targetId, "DOM.performSearch", {
531
+ query: xpath,
532
+ includeUserAgentShadowDOM: true
533
+ });
534
+ try {
535
+ if (!search.resultCount) {
536
+ throw new Error(`Unknown ref xpath: ${xpath}`);
537
+ }
538
+ const { nodeIds } = await sessionCommand(targetId, "DOM.getSearchResults", {
539
+ searchId: search.searchId,
540
+ fromIndex: 0,
541
+ toIndex: search.resultCount
542
+ });
543
+ for (const nodeId of nodeIds) {
544
+ const described = await sessionCommand(targetId, "DOM.describeNode", {
545
+ nodeId
546
+ });
547
+ if (described.node.backendNodeId) {
548
+ return described.node.backendNodeId;
549
+ }
550
+ }
551
+ throw new Error(`XPath resolved but no backend node id found: ${xpath}`);
552
+ } finally {
553
+ await sessionCommand(targetId, "DOM.discardSearchResults", { searchId: search.searchId }).catch(() => {
554
+ });
555
+ }
556
+ }
557
+ async function parseRef(ref) {
558
+ const targetId = connectionState?.currentTargetId ?? "";
559
+ let refs = connectionState?.refsByTarget.get(targetId) ?? {};
560
+ if (!refs[ref] && targetId) {
561
+ const persistedRefs = loadPersistedRefs(targetId);
562
+ if (persistedRefs) {
563
+ connectionState?.refsByTarget.set(targetId, persistedRefs);
564
+ refs = persistedRefs;
565
+ }
566
+ }
514
567
  const found = refs[ref];
515
- if (!found?.backendDOMNodeId) {
568
+ if (!found) {
516
569
  throw new Error(`Unknown ref: ${ref}. Run snapshot first.`);
517
570
  }
518
- return found.backendDOMNodeId;
571
+ if (found.backendDOMNodeId) {
572
+ return found.backendDOMNodeId;
573
+ }
574
+ if (targetId && found.xpath) {
575
+ const backendDOMNodeId = await resolveBackendNodeIdByXPath(targetId, found.xpath);
576
+ found.backendDOMNodeId = backendDOMNodeId;
577
+ connectionState?.refsByTarget.set(targetId, refs);
578
+ const pageUrl = await evaluate(targetId, "location.href", true).catch(() => void 0);
579
+ if (pageUrl) {
580
+ persistRefs(targetId, pageUrl, refs);
581
+ }
582
+ return backendDOMNodeId;
583
+ }
584
+ throw new Error(`Unknown ref: ${ref}. Run snapshot first.`);
585
+ }
586
+ function getRefsFilePath(targetId) {
587
+ return path2.join(os2.tmpdir(), `bb-browser-refs-${targetId}.json`);
588
+ }
589
+ function loadPersistedRefs(targetId, expectedUrl) {
590
+ try {
591
+ const data = JSON.parse(readFileSync(getRefsFilePath(targetId), "utf-8"));
592
+ if (data.targetId !== targetId) return null;
593
+ if (expectedUrl !== void 0 && data.url !== expectedUrl) return null;
594
+ if (!data.refs || typeof data.refs !== "object") return null;
595
+ return data.refs;
596
+ } catch {
597
+ return null;
598
+ }
599
+ }
600
+ function persistRefs(targetId, url, refs) {
601
+ try {
602
+ writeFileSync(getRefsFilePath(targetId), JSON.stringify({ targetId, url, timestamp: Date.now(), refs }));
603
+ } catch {
604
+ }
605
+ }
606
+ function clearPersistedRefs(targetId) {
607
+ try {
608
+ unlinkSync(getRefsFilePath(targetId));
609
+ } catch {
610
+ }
519
611
  }
520
612
  function loadBuildDomTreeScript() {
521
613
  const currentDir = path2.dirname(fileURLToPath(import.meta.url));
522
614
  const candidates = [
615
+ path2.resolve(currentDir, "./extension/buildDomTree.js"),
616
+ // npm installed: dist/cli.js → ../extension/buildDomTree.js
617
+ path2.resolve(currentDir, "../extension/buildDomTree.js"),
618
+ path2.resolve(currentDir, "../extension/dist/buildDomTree.js"),
619
+ path2.resolve(currentDir, "../packages/extension/public/buildDomTree.js"),
620
+ path2.resolve(currentDir, "../packages/extension/dist/buildDomTree.js"),
621
+ // dev mode: packages/cli/dist/ → ../../../extension/
523
622
  path2.resolve(currentDir, "../../../extension/buildDomTree.js"),
623
+ path2.resolve(currentDir, "../../../extension/dist/buildDomTree.js"),
624
+ // dev mode: packages/cli/src/ → ../../extension/
524
625
  path2.resolve(currentDir, "../../extension/buildDomTree.js"),
525
- path2.resolve(currentDir, "../../../packages/extension/buildDomTree.js"),
526
- path2.resolve(currentDir, "../../../extension/dist/buildDomTree.js")
626
+ path2.resolve(currentDir, "../../../packages/extension/dist/buildDomTree.js"),
627
+ path2.resolve(currentDir, "../../../packages/extension/public/buildDomTree.js")
527
628
  ];
528
629
  for (const candidate of candidates) {
529
630
  try {
@@ -551,8 +652,34 @@ async function resolveNode(targetId, backendNodeId) {
551
652
  return result.nodeId;
552
653
  }
553
654
  async function focusNode(targetId, backendNodeId) {
554
- const nodeId = await resolveNode(targetId, backendNodeId);
555
- await sessionCommand(targetId, "DOM.focus", { nodeId });
655
+ await sessionCommand(targetId, "DOM.focus", { backendNodeId });
656
+ }
657
+ async function insertTextIntoNode(targetId, backendNodeId, text, clearFirst) {
658
+ const resolved = await sessionCommand(targetId, "DOM.resolveNode", { backendNodeId });
659
+ await sessionCommand(targetId, "Runtime.callFunctionOn", {
660
+ objectId: resolved.object.objectId,
661
+ functionDeclaration: `function(value, clearFirst) {
662
+ if (typeof this.focus === 'function') this.focus();
663
+ if (clearFirst && ('value' in this)) {
664
+ this.value = '';
665
+ this.dispatchEvent(new Event('input', { bubbles: true }));
666
+ }
667
+ if ('value' in this) {
668
+ this.value = clearFirst ? value : String(this.value ?? '') + value;
669
+ this.dispatchEvent(new Event('input', { bubbles: true }));
670
+ this.dispatchEvent(new Event('change', { bubbles: true }));
671
+ return true;
672
+ }
673
+ return false;
674
+ }`,
675
+ arguments: [
676
+ { value: text },
677
+ { value: clearFirst }
678
+ ],
679
+ returnByValue: true
680
+ });
681
+ await focusNode(targetId, backendNodeId);
682
+ await sessionCommand(targetId, "Input.insertText", { text });
556
683
  }
557
684
  async function getNodeBox(targetId, backendNodeId) {
558
685
  const result = await sessionCommand(targetId, "DOM.getBoxModel", {
@@ -586,15 +713,181 @@ async function getAttributeValue(targetId, backendNodeId, attribute) {
586
713
  }
587
714
  async function buildSnapshot(targetId, request) {
588
715
  const script = loadBuildDomTreeScript();
589
- const expression = `(() => { ${script}; return (typeof buildDomTree === 'function' ? buildDomTree : globalThis.buildDomTree)(${JSON.stringify({
716
+ const buildArgs = {
717
+ showHighlightElements: true,
718
+ focusHighlightIndex: -1,
719
+ viewportExpansion: -1,
720
+ debugMode: false,
721
+ startId: 0,
722
+ startHighlightIndex: 0
723
+ };
724
+ 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({
725
+ ...buildArgs
726
+ })}); })()`;
727
+ const value = await evaluate(targetId, expression, true);
728
+ if (!value || !value.map || !value.rootId) {
729
+ const title = await evaluate(targetId, "document.title", true);
730
+ const pageUrl2 = await evaluate(targetId, "location.href", true);
731
+ const fallbackSnapshot = {
732
+ title,
733
+ url: pageUrl2,
734
+ lines: [title || pageUrl2],
735
+ refs: {}
736
+ };
737
+ connectionState?.refsByTarget.set(targetId, {});
738
+ persistRefs(targetId, pageUrl2, {});
739
+ return fallbackSnapshot;
740
+ }
741
+ const snapshot = convertBuildDomTreeResult(value, {
590
742
  interactiveOnly: !!request.interactive,
591
743
  compact: !!request.compact,
592
744
  maxDepth: request.maxDepth,
593
745
  selector: request.selector
594
- })}); })()`;
595
- const value = await evaluate(targetId, expression, true);
596
- connectionState?.refsByTarget.set(targetId, value.refs || {});
597
- return value;
746
+ });
747
+ const pageUrl = await evaluate(targetId, "location.href", true);
748
+ connectionState?.refsByTarget.set(targetId, snapshot.refs || {});
749
+ persistRefs(targetId, pageUrl, snapshot.refs || {});
750
+ return snapshot;
751
+ }
752
+ function convertBuildDomTreeResult(result, options) {
753
+ const { interactiveOnly, compact, maxDepth, selector } = options;
754
+ const { rootId, map } = result;
755
+ const refs = {};
756
+ const lines = [];
757
+ const getRole = (node) => {
758
+ const tagName = node.tagName.toLowerCase();
759
+ const role = node.attributes?.role;
760
+ if (role) return role;
761
+ const type = node.attributes?.type?.toLowerCase() || "text";
762
+ const inputRoleMap = {
763
+ text: "textbox",
764
+ password: "textbox",
765
+ email: "textbox",
766
+ url: "textbox",
767
+ tel: "textbox",
768
+ search: "searchbox",
769
+ number: "spinbutton",
770
+ range: "slider",
771
+ checkbox: "checkbox",
772
+ radio: "radio",
773
+ button: "button",
774
+ submit: "button",
775
+ reset: "button",
776
+ file: "button"
777
+ };
778
+ const roleMap = {
779
+ a: "link",
780
+ button: "button",
781
+ input: inputRoleMap[type] || "textbox",
782
+ select: "combobox",
783
+ textarea: "textbox",
784
+ img: "image",
785
+ nav: "navigation",
786
+ main: "main",
787
+ header: "banner",
788
+ footer: "contentinfo",
789
+ aside: "complementary",
790
+ form: "form",
791
+ table: "table",
792
+ ul: "list",
793
+ ol: "list",
794
+ li: "listitem",
795
+ h1: "heading",
796
+ h2: "heading",
797
+ h3: "heading",
798
+ h4: "heading",
799
+ h5: "heading",
800
+ h6: "heading",
801
+ dialog: "dialog",
802
+ article: "article",
803
+ section: "region",
804
+ label: "label",
805
+ details: "group",
806
+ summary: "button"
807
+ };
808
+ return roleMap[tagName] || tagName;
809
+ };
810
+ const collectTextContent = (node, nodeMap, depthLimit = 5) => {
811
+ const texts = [];
812
+ const visit = (nodeId, depth) => {
813
+ if (depth > depthLimit) return;
814
+ const currentNode = nodeMap[nodeId];
815
+ if (!currentNode) return;
816
+ if ("type" in currentNode && currentNode.type === "TEXT_NODE") {
817
+ const text = currentNode.text.trim();
818
+ if (text) texts.push(text);
819
+ return;
820
+ }
821
+ for (const childId of currentNode.children || []) visit(childId, depth + 1);
822
+ };
823
+ for (const childId of node.children || []) visit(childId, 0);
824
+ return texts.join(" ").trim();
825
+ };
826
+ const getName = (node) => {
827
+ const attrs = node.attributes || {};
828
+ return attrs["aria-label"] || attrs.title || attrs.placeholder || attrs.alt || attrs.value || collectTextContent(node, map) || attrs.name || void 0;
829
+ };
830
+ const truncateText = (text, length = 50) => text.length <= length ? text : `${text.slice(0, length - 3)}...`;
831
+ const selectorText = selector?.trim().toLowerCase();
832
+ const matchesSelector = (node, role, name) => {
833
+ if (!selectorText) return true;
834
+ const haystack = [node.tagName, role, name, node.xpath || "", ...Object.values(node.attributes || {})].join(" ").toLowerCase();
835
+ return haystack.includes(selectorText);
836
+ };
837
+ if (interactiveOnly) {
838
+ 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));
839
+ for (const { node } of interactiveNodes) {
840
+ const refId = String(node.highlightIndex);
841
+ const role = getRole(node);
842
+ const name = getName(node);
843
+ if (!matchesSelector(node, role, name)) continue;
844
+ let line = `${role} [ref=${refId}]`;
845
+ if (name) line += ` ${JSON.stringify(truncateText(name))}`;
846
+ lines.push(line);
847
+ refs[refId] = {
848
+ xpath: node.xpath || "",
849
+ role,
850
+ name,
851
+ tagName: node.tagName.toLowerCase()
852
+ };
853
+ }
854
+ return { snapshot: lines.join("\n"), refs };
855
+ }
856
+ const walk = (nodeId, depth) => {
857
+ if (maxDepth !== void 0 && depth > maxDepth) return;
858
+ const node = map[nodeId];
859
+ if (!node) return;
860
+ if ("type" in node && node.type === "TEXT_NODE") {
861
+ const text = node.text.trim();
862
+ if (!text) return;
863
+ lines.push(`${" ".repeat(depth)}- text ${JSON.stringify(truncateText(text, compact ? 80 : 120))}`);
864
+ return;
865
+ }
866
+ const role = getRole(node);
867
+ const name = getName(node);
868
+ if (!matchesSelector(node, role, name)) {
869
+ for (const childId of node.children || []) walk(childId, depth + 1);
870
+ return;
871
+ }
872
+ const indent = " ".repeat(depth);
873
+ const refId = node.highlightIndex !== void 0 && node.highlightIndex !== null ? String(node.highlightIndex) : null;
874
+ let line = `${indent}- ${role}`;
875
+ if (refId) line += ` [ref=${refId}]`;
876
+ if (name) line += ` ${JSON.stringify(truncateText(name, compact ? 50 : 80))}`;
877
+ if (!compact) line += ` <${node.tagName.toLowerCase()}>`;
878
+ lines.push(line);
879
+ if (refId) {
880
+ refs[refId] = {
881
+ xpath: node.xpath || "",
882
+ role,
883
+ name,
884
+ tagName: node.tagName.toLowerCase()
885
+ };
886
+ }
887
+ for (const childId of node.children || []) walk(childId, depth + 1);
888
+ };
889
+ walk(rootId, 0);
890
+ return { snapshot: lines.join("\n"), refs };
598
891
  }
599
892
  function ok(id, data) {
600
893
  return { id, success: true, data };
@@ -638,10 +931,12 @@ async function dispatchRequest(request) {
638
931
  if (request.tabId === void 0) {
639
932
  const created = await browserCommand("Target.createTarget", { url: request.url });
640
933
  const newTarget = await ensurePageTarget(created.targetId);
641
- return ok(request.id, { url: request.url, tabId: Number(newTarget.id) || void 0 });
934
+ return ok(request.id, { url: request.url, tabId: newTarget.id });
642
935
  }
643
936
  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 });
937
+ connectionState?.refsByTarget.delete(target.id);
938
+ clearPersistedRefs(target.id);
939
+ return ok(request.id, { url: request.url, title: target.title, tabId: target.id });
645
940
  }
646
941
  case "snapshot": {
647
942
  const snapshotData = await buildSnapshot(target.id, request);
@@ -650,7 +945,7 @@ async function dispatchRequest(request) {
650
945
  case "click":
651
946
  case "hover": {
652
947
  if (!request.ref) return fail(request.id, "Missing ref parameter");
653
- const backendNodeId = parseRef(request.ref);
948
+ const backendNodeId = await parseRef(request.ref);
654
949
  const point = await getNodeBox(target.id, backendNodeId);
655
950
  await sessionCommand(target.id, "Input.dispatchMouseEvent", { type: "mouseMoved", x: point.x, y: point.y, button: "none" });
656
951
  if (request.action === "click") await mouseClick(target.id, point.x, point.y);
@@ -660,18 +955,14 @@ async function dispatchRequest(request) {
660
955
  case "type": {
661
956
  if (!request.ref) return fail(request.id, "Missing ref parameter");
662
957
  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 });
958
+ const backendNodeId = await parseRef(request.ref);
959
+ await insertTextIntoNode(target.id, backendNodeId, request.text, request.action === "fill");
669
960
  return ok(request.id, { value: request.text });
670
961
  }
671
962
  case "check":
672
963
  case "uncheck": {
673
964
  if (!request.ref) return fail(request.id, "Missing ref parameter");
674
- const backendNodeId = parseRef(request.ref);
965
+ const backendNodeId = await parseRef(request.ref);
675
966
  const desired = request.action === "check";
676
967
  const resolved = await sessionCommand(target.id, "DOM.resolveNode", { backendNodeId });
677
968
  await sessionCommand(target.id, "Runtime.callFunctionOn", {
@@ -682,7 +973,7 @@ async function dispatchRequest(request) {
682
973
  }
683
974
  case "select": {
684
975
  if (!request.ref || request.value == null) return fail(request.id, "Missing ref or value parameter");
685
- const backendNodeId = parseRef(request.ref);
976
+ const backendNodeId = await parseRef(request.ref);
686
977
  const resolved = await sessionCommand(target.id, "DOM.resolveNode", { backendNodeId });
687
978
  await sessionCommand(target.id, "Runtime.callFunctionOn", {
688
979
  objectId: resolved.object.objectId,
@@ -692,7 +983,7 @@ async function dispatchRequest(request) {
692
983
  }
693
984
  case "get": {
694
985
  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);
986
+ const value = await getAttributeValue(target.id, await parseRef(request.ref), request.attribute);
696
987
  return ok(request.id, { value });
697
988
  }
698
989
  case "screenshot": {
@@ -701,6 +992,8 @@ async function dispatchRequest(request) {
701
992
  }
702
993
  case "close": {
703
994
  await browserCommand("Target.closeTarget", { targetId: target.id });
995
+ connectionState?.refsByTarget.delete(target.id);
996
+ clearPersistedRefs(target.id);
704
997
  return ok(request.id, {});
705
998
  }
706
999
  case "wait": {
@@ -739,12 +1032,12 @@ async function dispatchRequest(request) {
739
1032
  return ok(request.id, { result });
740
1033
  }
741
1034
  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 }));
1035
+ 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
1036
  return ok(request.id, { tabs, activeIndex: tabs.findIndex((tab) => tab.active) });
744
1037
  }
745
1038
  case "tab_new": {
746
1039
  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" });
1040
+ return ok(request.id, { tabId: created.targetId, url: request.url ?? "about:blank" });
748
1041
  }
749
1042
  case "tab_select": {
750
1043
  const tabs = (await getTargets()).filter((item) => item.type === "page");
@@ -752,14 +1045,16 @@ async function dispatchRequest(request) {
752
1045
  if (!selected) return fail(request.id, "Tab not found");
753
1046
  connectionState.currentTargetId = selected.id;
754
1047
  await attachTarget(selected.id);
755
- return ok(request.id, { tabId: Number(selected.id) || void 0, url: selected.url, title: selected.title });
1048
+ return ok(request.id, { tabId: selected.id, url: selected.url, title: selected.title });
756
1049
  }
757
1050
  case "tab_close": {
758
1051
  const tabs = (await getTargets()).filter((item) => item.type === "page");
759
1052
  const selected = request.tabId !== void 0 ? tabs.find((item) => item.id === String(request.tabId) || Number(item.id) === request.tabId) : tabs[request.index ?? 0];
760
1053
  if (!selected) return fail(request.id, "Tab not found");
761
1054
  await browserCommand("Target.closeTarget", { targetId: selected.id });
762
- return ok(request.id, { tabId: Number(selected.id) || void 0 });
1055
+ connectionState?.refsByTarget.delete(selected.id);
1056
+ clearPersistedRefs(selected.id);
1057
+ return ok(request.id, { tabId: selected.id });
763
1058
  }
764
1059
  case "frame": {
765
1060
  if (!request.selector) return fail(request.id, "Missing selector parameter");
@@ -911,7 +1206,7 @@ async function ensureDaemonRunning() {
911
1206
  }
912
1207
 
913
1208
  // packages/cli/src/history-sqlite.ts
914
- import { copyFileSync, existsSync as existsSync3, unlinkSync } from "fs";
1209
+ import { copyFileSync, existsSync as existsSync3, unlinkSync as unlinkSync2 } from "fs";
915
1210
  import { execSync as execSync2 } from "child_process";
916
1211
  import { homedir, tmpdir } from "os";
917
1212
  import { join } from "path";
@@ -969,7 +1264,7 @@ function runHistoryQuery(sql, mapRow) {
969
1264
  return [];
970
1265
  } finally {
971
1266
  try {
972
- unlinkSync(tmpPath);
1267
+ unlinkSync2(tmpPath);
973
1268
  } catch {
974
1269
  }
975
1270
  }
@@ -1960,11 +2255,11 @@ async function getCommand(attribute, ref, options = {}) {
1960
2255
  // packages/cli/src/commands/screenshot.ts
1961
2256
  import fs from "fs";
1962
2257
  import path3 from "path";
1963
- import os2 from "os";
2258
+ import os3 from "os";
1964
2259
  function getDefaultPath() {
1965
2260
  const timestamp = (/* @__PURE__ */ new Date()).toISOString().replace(/[:.]/g, "-");
1966
2261
  const filename = `bb-screenshot-${timestamp}.png`;
1967
- return path3.join(os2.tmpdir(), filename);
2262
+ return path3.join(os3.tmpdir(), filename);
1968
2263
  }
1969
2264
  function saveBase64Image(dataUrl, filePath) {
1970
2265
  const base64Data = dataUrl.replace(/^data:image\/png;base64,/, "");
@@ -2510,6 +2805,9 @@ function parseTabSubcommand(args, rawArgv) {
2510
2805
  return { action: "tab_list" };
2511
2806
  }
2512
2807
  const first = args[0];
2808
+ if (first === "list") {
2809
+ return { action: "tab_list" };
2810
+ }
2513
2811
  if (first === "new") {
2514
2812
  return { action: "tab_new", url: args[1] };
2515
2813
  }
@@ -3038,9 +3336,9 @@ async function fetchCommand(url, options = {}) {
3038
3336
  throw new Error(`Fetch error: ${result.error}`);
3039
3337
  }
3040
3338
  if (options.output) {
3041
- const { writeFileSync } = await import("fs");
3339
+ const { writeFileSync: writeFileSync2 } = await import("fs");
3042
3340
  const content = typeof result.body === "object" ? JSON.stringify(result.body, null, 2) : String(result.body);
3043
- writeFileSync(options.output, content, "utf-8");
3341
+ writeFileSync2(options.output, content, "utf-8");
3044
3342
  console.log(`\u5DF2\u5199\u5165 ${options.output} (${result.status}, ${content.length} bytes)`);
3045
3343
  return;
3046
3344
  }
@@ -3101,7 +3399,7 @@ async function historyCommand(subCommand, options = {}) {
3101
3399
  }
3102
3400
 
3103
3401
  // packages/cli/src/index.ts
3104
- var VERSION = "0.8.0";
3402
+ var VERSION = "0.8.2";
3105
3403
  var HELP_TEXT = `
3106
3404
  bb-browser - AI Agent \u6D4F\u89C8\u5668\u81EA\u52A8\u5316\u5DE5\u5177
3107
3405