bb-browser 0.8.0 → 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
@@ -232,7 +239,13 @@ async function getJsonVersion(host, port) {
232
239
  function connectWebSocket(url) {
233
240
  return new Promise((resolve2, reject) => {
234
241
  const ws = new WebSocket(url);
235
- ws.once("open", () => resolve2(ws));
242
+ ws.once("open", () => {
243
+ const socket = ws._socket;
244
+ if (socket && typeof socket.unref === "function") {
245
+ socket.unref();
246
+ }
247
+ resolve2(ws);
248
+ });
236
249
  ws.once("error", reject);
237
250
  });
238
251
  }
@@ -500,27 +513,118 @@ async function ensurePageTarget(targetId) {
500
513
  target = targets[targetId] ?? targets.find((item) => Number(item.id) === targetId);
501
514
  } else if (typeof targetId === "string") {
502
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
+ }
503
522
  }
504
523
  target ??= targets[0];
505
524
  connectionState.currentTargetId = target.id;
506
525
  await attachTarget(target.id);
507
526
  return target;
508
527
  }
509
- function parseRef(ref) {
510
- 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
+ }
511
567
  const found = refs[ref];
512
- if (!found?.backendDOMNodeId) {
568
+ if (!found) {
513
569
  throw new Error(`Unknown ref: ${ref}. Run snapshot first.`);
514
570
  }
515
- 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
+ }
516
611
  }
517
612
  function loadBuildDomTreeScript() {
518
613
  const currentDir = path2.dirname(fileURLToPath(import.meta.url));
519
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/
520
622
  path2.resolve(currentDir, "../../../extension/buildDomTree.js"),
623
+ path2.resolve(currentDir, "../../../extension/dist/buildDomTree.js"),
624
+ // dev mode: packages/cli/src/ → ../../extension/
521
625
  path2.resolve(currentDir, "../../extension/buildDomTree.js"),
522
- path2.resolve(currentDir, "../../../packages/extension/buildDomTree.js"),
523
- 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")
524
628
  ];
525
629
  for (const candidate of candidates) {
526
630
  try {
@@ -548,8 +652,34 @@ async function resolveNode(targetId, backendNodeId) {
548
652
  return result.nodeId;
549
653
  }
550
654
  async function focusNode(targetId, backendNodeId) {
551
- const nodeId = await resolveNode(targetId, backendNodeId);
552
- 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 });
553
683
  }
554
684
  async function getNodeBox(targetId, backendNodeId) {
555
685
  const result = await sessionCommand(targetId, "DOM.getBoxModel", {
@@ -583,15 +713,181 @@ async function getAttributeValue(targetId, backendNodeId, attribute) {
583
713
  }
584
714
  async function buildSnapshot(targetId, request) {
585
715
  const script = loadBuildDomTreeScript();
586
- 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, {
587
742
  interactiveOnly: !!request.interactive,
588
743
  compact: !!request.compact,
589
744
  maxDepth: request.maxDepth,
590
745
  selector: request.selector
591
- })}); })()`;
592
- const value = await evaluate(targetId, expression, true);
593
- connectionState?.refsByTarget.set(targetId, value.refs || {});
594
- 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 };
595
891
  }
596
892
  function ok(id, data) {
597
893
  return { id, success: true, data };
@@ -635,10 +931,12 @@ async function dispatchRequest(request) {
635
931
  if (request.tabId === void 0) {
636
932
  const created = await browserCommand("Target.createTarget", { url: request.url });
637
933
  const newTarget = await ensurePageTarget(created.targetId);
638
- return ok(request.id, { url: request.url, tabId: Number(newTarget.id) || void 0 });
934
+ return ok(request.id, { url: request.url, tabId: newTarget.id });
639
935
  }
640
936
  await pageCommand(target.id, "Page.navigate", { url: request.url });
641
- 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 });
642
940
  }
643
941
  case "snapshot": {
644
942
  const snapshotData = await buildSnapshot(target.id, request);
@@ -647,7 +945,7 @@ async function dispatchRequest(request) {
647
945
  case "click":
648
946
  case "hover": {
649
947
  if (!request.ref) return fail(request.id, "Missing ref parameter");
650
- const backendNodeId = parseRef(request.ref);
948
+ const backendNodeId = await parseRef(request.ref);
651
949
  const point = await getNodeBox(target.id, backendNodeId);
652
950
  await sessionCommand(target.id, "Input.dispatchMouseEvent", { type: "mouseMoved", x: point.x, y: point.y, button: "none" });
653
951
  if (request.action === "click") await mouseClick(target.id, point.x, point.y);
@@ -657,18 +955,14 @@ async function dispatchRequest(request) {
657
955
  case "type": {
658
956
  if (!request.ref) return fail(request.id, "Missing ref parameter");
659
957
  if (request.text == null) return fail(request.id, "Missing text parameter");
660
- const backendNodeId = parseRef(request.ref);
661
- await focusNode(target.id, backendNodeId);
662
- if (request.action === "fill") {
663
- await evaluate(target.id, `document.activeElement && ((document.activeElement.value = ''), document.activeElement.dispatchEvent(new Event('input', { bubbles: true })))`);
664
- }
665
- 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");
666
960
  return ok(request.id, { value: request.text });
667
961
  }
668
962
  case "check":
669
963
  case "uncheck": {
670
964
  if (!request.ref) return fail(request.id, "Missing ref parameter");
671
- const backendNodeId = parseRef(request.ref);
965
+ const backendNodeId = await parseRef(request.ref);
672
966
  const desired = request.action === "check";
673
967
  const resolved = await sessionCommand(target.id, "DOM.resolveNode", { backendNodeId });
674
968
  await sessionCommand(target.id, "Runtime.callFunctionOn", {
@@ -679,7 +973,7 @@ async function dispatchRequest(request) {
679
973
  }
680
974
  case "select": {
681
975
  if (!request.ref || request.value == null) return fail(request.id, "Missing ref or value parameter");
682
- const backendNodeId = parseRef(request.ref);
976
+ const backendNodeId = await parseRef(request.ref);
683
977
  const resolved = await sessionCommand(target.id, "DOM.resolveNode", { backendNodeId });
684
978
  await sessionCommand(target.id, "Runtime.callFunctionOn", {
685
979
  objectId: resolved.object.objectId,
@@ -689,7 +983,7 @@ async function dispatchRequest(request) {
689
983
  }
690
984
  case "get": {
691
985
  if (!request.ref || !request.attribute) return fail(request.id, "Missing ref or attribute parameter");
692
- const value = await getAttributeValue(target.id, parseRef(request.ref), request.attribute);
986
+ const value = await getAttributeValue(target.id, await parseRef(request.ref), request.attribute);
693
987
  return ok(request.id, { value });
694
988
  }
695
989
  case "screenshot": {
@@ -698,6 +992,8 @@ async function dispatchRequest(request) {
698
992
  }
699
993
  case "close": {
700
994
  await browserCommand("Target.closeTarget", { targetId: target.id });
995
+ connectionState?.refsByTarget.delete(target.id);
996
+ clearPersistedRefs(target.id);
701
997
  return ok(request.id, {});
702
998
  }
703
999
  case "wait": {
@@ -736,12 +1032,12 @@ async function dispatchRequest(request) {
736
1032
  return ok(request.id, { result });
737
1033
  }
738
1034
  case "tab_list": {
739
- 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 }));
740
1036
  return ok(request.id, { tabs, activeIndex: tabs.findIndex((tab) => tab.active) });
741
1037
  }
742
1038
  case "tab_new": {
743
1039
  const created = await browserCommand("Target.createTarget", { url: request.url ?? "about:blank" });
744
- 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" });
745
1041
  }
746
1042
  case "tab_select": {
747
1043
  const tabs = (await getTargets()).filter((item) => item.type === "page");
@@ -749,14 +1045,16 @@ async function dispatchRequest(request) {
749
1045
  if (!selected) return fail(request.id, "Tab not found");
750
1046
  connectionState.currentTargetId = selected.id;
751
1047
  await attachTarget(selected.id);
752
- 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 });
753
1049
  }
754
1050
  case "tab_close": {
755
1051
  const tabs = (await getTargets()).filter((item) => item.type === "page");
756
1052
  const selected = request.tabId !== void 0 ? tabs.find((item) => item.id === String(request.tabId) || Number(item.id) === request.tabId) : tabs[request.index ?? 0];
757
1053
  if (!selected) return fail(request.id, "Tab not found");
758
1054
  await browserCommand("Target.closeTarget", { targetId: selected.id });
759
- 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 });
760
1058
  }
761
1059
  case "frame": {
762
1060
  if (!request.selector) return fail(request.id, "Missing selector parameter");
@@ -908,7 +1206,7 @@ async function ensureDaemonRunning() {
908
1206
  }
909
1207
 
910
1208
  // packages/cli/src/history-sqlite.ts
911
- import { copyFileSync, existsSync as existsSync3, unlinkSync } from "fs";
1209
+ import { copyFileSync, existsSync as existsSync3, unlinkSync as unlinkSync2 } from "fs";
912
1210
  import { execSync as execSync2 } from "child_process";
913
1211
  import { homedir, tmpdir } from "os";
914
1212
  import { join } from "path";
@@ -966,7 +1264,7 @@ function runHistoryQuery(sql, mapRow) {
966
1264
  return [];
967
1265
  } finally {
968
1266
  try {
969
- unlinkSync(tmpPath);
1267
+ unlinkSync2(tmpPath);
970
1268
  } catch {
971
1269
  }
972
1270
  }
@@ -1957,11 +2255,11 @@ async function getCommand(attribute, ref, options = {}) {
1957
2255
  // packages/cli/src/commands/screenshot.ts
1958
2256
  import fs from "fs";
1959
2257
  import path3 from "path";
1960
- import os2 from "os";
2258
+ import os3 from "os";
1961
2259
  function getDefaultPath() {
1962
2260
  const timestamp = (/* @__PURE__ */ new Date()).toISOString().replace(/[:.]/g, "-");
1963
2261
  const filename = `bb-screenshot-${timestamp}.png`;
1964
- return path3.join(os2.tmpdir(), filename);
2262
+ return path3.join(os3.tmpdir(), filename);
1965
2263
  }
1966
2264
  function saveBase64Image(dataUrl, filePath) {
1967
2265
  const base64Data = dataUrl.replace(/^data:image\/png;base64,/, "");
@@ -2507,6 +2805,9 @@ function parseTabSubcommand(args, rawArgv) {
2507
2805
  return { action: "tab_list" };
2508
2806
  }
2509
2807
  const first = args[0];
2808
+ if (first === "list") {
2809
+ return { action: "tab_list" };
2810
+ }
2510
2811
  if (first === "new") {
2511
2812
  return { action: "tab_new", url: args[1] };
2512
2813
  }
@@ -3035,9 +3336,9 @@ async function fetchCommand(url, options = {}) {
3035
3336
  throw new Error(`Fetch error: ${result.error}`);
3036
3337
  }
3037
3338
  if (options.output) {
3038
- const { writeFileSync } = await import("fs");
3339
+ const { writeFileSync: writeFileSync2 } = await import("fs");
3039
3340
  const content = typeof result.body === "object" ? JSON.stringify(result.body, null, 2) : String(result.body);
3040
- writeFileSync(options.output, content, "utf-8");
3341
+ writeFileSync2(options.output, content, "utf-8");
3041
3342
  console.log(`\u5DF2\u5199\u5165 ${options.output} (${result.status}, ${content.length} bytes)`);
3042
3343
  return;
3043
3344
  }
@@ -3098,7 +3399,7 @@ async function historyCommand(subCommand, options = {}) {
3098
3399
  }
3099
3400
 
3100
3401
  // packages/cli/src/index.ts
3101
- var VERSION = "0.8.0";
3402
+ var VERSION = "0.8.2";
3102
3403
  var HELP_TEXT = `
3103
3404
  bb-browser - AI Agent \u6D4F\u89C8\u5668\u81EA\u52A8\u5316\u5DE5\u5177
3104
3405
 
@@ -3714,5 +4015,5 @@ Full guide: https://github.com/epiral/bb-sites/blob/main/SKILL.md`);
3714
4015
  process.exit(1);
3715
4016
  }
3716
4017
  }
3717
- main();
4018
+ main().then(() => process.exit(0));
3718
4019
  //# sourceMappingURL=cli.js.map