bb-browser 0.8.3 → 0.9.0

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
@@ -6,10 +6,13 @@ import {
6
6
  import {
7
7
  applyJq
8
8
  } from "./chunk-AHGAQEFO.js";
9
+ import {
10
+ parseOpenClawJson
11
+ } from "./chunk-FSL4RNI6.js";
9
12
  import "./chunk-D4HDZEJT.js";
10
13
 
11
14
  // packages/cli/src/index.ts
12
- import { fileURLToPath as fileURLToPath3 } from "url";
15
+ import { fileURLToPath as fileURLToPath4 } from "url";
13
16
 
14
17
  // packages/cli/src/cdp-client.ts
15
18
  import { readFileSync, unlinkSync, writeFileSync } from "fs";
@@ -31,13 +34,13 @@ var MANAGED_BROWSER_DIR = path.join(os.homedir(), ".bb-browser", "browser");
31
34
  var MANAGED_USER_DATA_DIR = path.join(MANAGED_BROWSER_DIR, "user-data");
32
35
  var MANAGED_PORT_FILE = path.join(MANAGED_BROWSER_DIR, "cdp-port");
33
36
  function execFileAsync(command, args, timeout) {
34
- return new Promise((resolve2, reject) => {
37
+ return new Promise((resolve3, reject) => {
35
38
  execFile(command, args, { encoding: "utf8", timeout }, (error, stdout) => {
36
39
  if (error) {
37
40
  reject(error);
38
41
  return;
39
42
  }
40
- resolve2(stdout.trim());
43
+ resolve3(stdout.trim());
41
44
  });
42
45
  });
43
46
  }
@@ -49,7 +52,7 @@ function getArgValue(flag) {
49
52
  async function tryOpenClaw() {
50
53
  try {
51
54
  const raw = await execFileAsync("npx", ["openclaw", "browser", "status", "--json"], 5e3);
52
- const parsed = JSON.parse(raw);
55
+ const parsed = parseOpenClawJson(raw);
53
56
  const port = Number(parsed?.cdpPort);
54
57
  if (Number.isInteger(port) && port > 0) {
55
58
  return { host: "127.0.0.1", port };
@@ -105,6 +108,18 @@ function findBrowserExecutable() {
105
108
  }
106
109
  return null;
107
110
  }
111
+ async function isManagedBrowserRunning() {
112
+ try {
113
+ const rawPort = await readFile(MANAGED_PORT_FILE, "utf8");
114
+ const port = Number.parseInt(rawPort.trim(), 10);
115
+ if (!Number.isInteger(port) || port <= 0) {
116
+ return false;
117
+ }
118
+ return await canConnect("127.0.0.1", port);
119
+ } catch {
120
+ return false;
121
+ }
122
+ }
108
123
  async function launchManagedBrowser(port = DEFAULT_CDP_PORT) {
109
124
  const executable = findBrowserExecutable();
110
125
  if (!executable) {
@@ -155,7 +170,7 @@ async function launchManagedBrowser(port = DEFAULT_CDP_PORT) {
155
170
  if (await canConnect("127.0.0.1", port)) {
156
171
  return { host: "127.0.0.1", port };
157
172
  }
158
- await new Promise((resolve2) => setTimeout(resolve2, 250));
173
+ await new Promise((resolve3) => setTimeout(resolve3, 250));
159
174
  }
160
175
  return null;
161
176
  }
@@ -202,11 +217,35 @@ var jsErrors = [];
202
217
  var errorsEnabled = false;
203
218
  var traceRecording = false;
204
219
  var traceEvents = [];
220
+ function getContextFilePath(host, port) {
221
+ const safeHost = host.replace(/[^a-zA-Z0-9_.-]/g, "_");
222
+ return path2.join(os2.tmpdir(), `bb-browser-cdp-context-${safeHost}-${port}.json`);
223
+ }
224
+ function loadPersistedCurrentTargetId(host, port) {
225
+ try {
226
+ const data = JSON.parse(readFileSync(getContextFilePath(host, port), "utf-8"));
227
+ return typeof data.currentTargetId === "string" && data.currentTargetId ? data.currentTargetId : void 0;
228
+ } catch {
229
+ return void 0;
230
+ }
231
+ }
232
+ function persistCurrentTargetId(host, port, currentTargetId) {
233
+ try {
234
+ writeFileSync(getContextFilePath(host, port), JSON.stringify({ currentTargetId }));
235
+ } catch {
236
+ }
237
+ }
238
+ function setCurrentTargetId(targetId) {
239
+ const state = connectionState;
240
+ if (!state) return;
241
+ state.currentTargetId = targetId;
242
+ persistCurrentTargetId(state.host, state.port, targetId);
243
+ }
205
244
  function buildRequestError(error) {
206
245
  return error instanceof Error ? error : new Error(String(error));
207
246
  }
208
247
  function fetchJson(url) {
209
- return new Promise((resolve2, reject) => {
248
+ return new Promise((resolve3, reject) => {
210
249
  const requester = url.startsWith("https:") ? httpsRequest : httpRequest;
211
250
  const req = requester(url, { method: "GET" }, (res) => {
212
251
  const chunks = [];
@@ -218,7 +257,7 @@ function fetchJson(url) {
218
257
  return;
219
258
  }
220
259
  try {
221
- resolve2(JSON.parse(raw));
260
+ resolve3(JSON.parse(raw));
222
261
  } catch (error) {
223
262
  reject(error);
224
263
  }
@@ -241,14 +280,14 @@ async function getJsonVersion(host, port) {
241
280
  return { webSocketDebuggerUrl: url };
242
281
  }
243
282
  function connectWebSocket(url) {
244
- return new Promise((resolve2, reject) => {
283
+ return new Promise((resolve3, reject) => {
245
284
  const ws = new WebSocket(url);
246
285
  ws.once("open", () => {
247
286
  const socket = ws._socket;
248
287
  if (socket && typeof socket.unref === "function") {
249
288
  socket.unref();
250
289
  }
251
- resolve2(ws);
290
+ resolve3(ws);
252
291
  });
253
292
  ws.once("error", reject);
254
293
  });
@@ -264,6 +303,7 @@ function createState(host, port, browserWsUrl, browserSocket) {
264
303
  sessions: /* @__PURE__ */ new Map(),
265
304
  attachedTargets: /* @__PURE__ */ new Map(),
266
305
  refsByTarget: /* @__PURE__ */ new Map(),
306
+ currentTargetId: loadPersistedCurrentTargetId(host, port),
267
307
  activeFrameIdByTarget: /* @__PURE__ */ new Map(),
268
308
  dialogHandlers: /* @__PURE__ */ new Map()
269
309
  };
@@ -300,6 +340,10 @@ function createState(host, port, browserWsUrl, browserSocket) {
300
340
  state.attachedTargets.delete(sessionId);
301
341
  state.activeFrameIdByTarget.delete(targetId);
302
342
  state.dialogHandlers.delete(targetId);
343
+ if (state.currentTargetId === targetId) {
344
+ state.currentTargetId = void 0;
345
+ persistCurrentTargetId(state.host, state.port, void 0);
346
+ }
303
347
  }
304
348
  }
305
349
  return;
@@ -343,8 +387,8 @@ async function browserCommand(method, params = {}) {
343
387
  if (!state) throw new Error("CDP connection not initialized");
344
388
  const id = state.nextMessageId++;
345
389
  const payload = JSON.stringify({ id, method, params });
346
- const promise = new Promise((resolve2, reject) => {
347
- state.browserPending.set(id, { resolve: resolve2, reject, method });
390
+ const promise = new Promise((resolve3, reject) => {
391
+ state.browserPending.set(id, { resolve: resolve3, reject, method });
348
392
  });
349
393
  state.browserSocket.send(payload);
350
394
  return promise;
@@ -355,13 +399,13 @@ async function sessionCommand(targetId, method, params = {}) {
355
399
  const sessionId = state.sessions.get(targetId) ?? await attachTarget(targetId);
356
400
  const id = state.nextMessageId++;
357
401
  const payload = JSON.stringify({ id, method, params, sessionId });
358
- return new Promise((resolve2, reject) => {
402
+ return new Promise((resolve3, reject) => {
359
403
  const check = (raw) => {
360
404
  const msg = JSON.parse(raw.toString());
361
405
  if (msg.id === id && msg.sessionId === sessionId) {
362
406
  state.browserSocket.off("message", check);
363
407
  if (msg.error) reject(new Error(`${method}: ${msg.error.message ?? "Unknown CDP error"}`));
364
- else resolve2(msg.result);
408
+ else resolve3(msg.result);
365
409
  }
366
410
  };
367
411
  state.browserSocket.on("message", check);
@@ -512,6 +556,7 @@ async function getTargets() {
512
556
  async function ensurePageTarget(targetId) {
513
557
  const targets = (await getTargets()).filter((target2) => target2.type === "page");
514
558
  if (targets.length === 0) throw new Error("No page target found");
559
+ const persistedTargetId = targetId === void 0 ? connectionState?.currentTargetId : void 0;
515
560
  let target;
516
561
  if (typeof targetId === "number") {
517
562
  target = targets[targetId] ?? targets.find((item) => Number(item.id) === targetId);
@@ -523,9 +568,11 @@ async function ensurePageTarget(targetId) {
523
568
  target = targets[numericTargetId] ?? targets.find((item) => Number(item.id) === numericTargetId);
524
569
  }
525
570
  }
571
+ } else if (persistedTargetId) {
572
+ target = targets.find((item) => item.id === persistedTargetId);
526
573
  }
527
574
  target ??= targets[0];
528
- connectionState.currentTargetId = target.id;
575
+ setCurrentTargetId(target.id);
529
576
  await attachTarget(target.id);
530
577
  return target;
531
578
  }
@@ -642,19 +689,16 @@ async function evaluate(targetId, expression, returnByValue = true) {
642
689
  const result = await sessionCommand(targetId, "Runtime.evaluate", {
643
690
  expression,
644
691
  awaitPromise: true,
645
- returnByValue
692
+ returnByValue,
693
+ replMode: true
646
694
  });
647
695
  if (result.exceptionDetails) {
648
- throw new Error(result.exceptionDetails.text || "Runtime.evaluate failed");
696
+ throw new Error(
697
+ result.exceptionDetails.exception?.description || result.exceptionDetails.text || "Runtime.evaluate failed"
698
+ );
649
699
  }
650
700
  return result.result.value ?? result.result;
651
701
  }
652
- async function resolveNode(targetId, backendNodeId) {
653
- const result = await sessionCommand(targetId, "DOM.pushNodesByBackendIdsToFrontend", {
654
- backendNodeIds: [backendNodeId]
655
- });
656
- return result.nodeId;
657
- }
658
702
  async function focusNode(targetId, backendNodeId) {
659
703
  await sessionCommand(targetId, "DOM.focus", { backendNodeId });
660
704
  }
@@ -662,40 +706,77 @@ async function insertTextIntoNode(targetId, backendNodeId, text, clearFirst) {
662
706
  const resolved = await sessionCommand(targetId, "DOM.resolveNode", { backendNodeId });
663
707
  await sessionCommand(targetId, "Runtime.callFunctionOn", {
664
708
  objectId: resolved.object.objectId,
665
- functionDeclaration: `function(value, clearFirst) {
709
+ functionDeclaration: `function(clearFirst) {
710
+ if (typeof this.scrollIntoView === 'function') {
711
+ this.scrollIntoView({ behavior: 'auto', block: 'center', inline: 'center' });
712
+ }
666
713
  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 }));
714
+ if (this instanceof HTMLInputElement || this instanceof HTMLTextAreaElement) {
715
+ if (clearFirst) {
716
+ this.value = '';
717
+ this.dispatchEvent(new Event('input', { bubbles: true }));
718
+ }
719
+ if (typeof this.setSelectionRange === 'function') {
720
+ const end = this.value.length;
721
+ this.setSelectionRange(end, end);
722
+ }
723
+ return true;
724
+ }
725
+ if (this instanceof HTMLElement && this.isContentEditable) {
726
+ if (clearFirst) {
727
+ this.textContent = '';
728
+ this.dispatchEvent(new Event('input', { bubbles: true }));
729
+ }
730
+ const selection = window.getSelection();
731
+ if (selection) {
732
+ const range = document.createRange();
733
+ range.selectNodeContents(this);
734
+ range.collapse(false);
735
+ selection.removeAllRanges();
736
+ selection.addRange(range);
737
+ }
675
738
  return true;
676
739
  }
677
740
  return false;
678
741
  }`,
679
742
  arguments: [
680
- { value: text },
681
743
  { value: clearFirst }
682
744
  ],
683
745
  returnByValue: true
684
746
  });
685
- await focusNode(targetId, backendNodeId);
686
- await sessionCommand(targetId, "Input.insertText", { text });
747
+ if (text) {
748
+ await focusNode(targetId, backendNodeId);
749
+ await sessionCommand(targetId, "Input.insertText", { text });
750
+ }
687
751
  }
688
- async function getNodeBox(targetId, backendNodeId) {
689
- const result = await sessionCommand(targetId, "DOM.getBoxModel", {
690
- backendNodeId
752
+ async function getInteractablePoint(targetId, backendNodeId) {
753
+ const resolved = await sessionCommand(targetId, "DOM.resolveNode", { backendNodeId });
754
+ const call = await sessionCommand(targetId, "Runtime.callFunctionOn", {
755
+ objectId: resolved.object.objectId,
756
+ functionDeclaration: `function() {
757
+ if (!(this instanceof Element)) {
758
+ throw new Error('Ref does not resolve to an element');
759
+ }
760
+ this.scrollIntoView({ behavior: 'auto', block: 'center', inline: 'center' });
761
+ const rect = this.getBoundingClientRect();
762
+ if (!rect || rect.width <= 0 || rect.height <= 0) {
763
+ throw new Error('Element is not visible');
764
+ }
765
+ return {
766
+ x: rect.left + rect.width / 2,
767
+ y: rect.top + rect.height / 2,
768
+ };
769
+ }`,
770
+ returnByValue: true
691
771
  });
692
- const quad = result.model.content.length >= 8 ? result.model.content : result.model.border;
693
- const xs = [quad[0], quad[2], quad[4], quad[6]];
694
- const ys = [quad[1], quad[3], quad[5], quad[7]];
695
- return {
696
- x: xs.reduce((a, b) => a + b, 0) / xs.length,
697
- y: ys.reduce((a, b) => a + b, 0) / ys.length
698
- };
772
+ if (call.exceptionDetails) {
773
+ throw new Error(call.exceptionDetails.text || "Failed to resolve element point");
774
+ }
775
+ const point = call.result.value;
776
+ if (!point || typeof point.x !== "number" || typeof point.y !== "number" || !Number.isFinite(point.x) || !Number.isFinite(point.y)) {
777
+ throw new Error("Failed to resolve element point");
778
+ }
779
+ return point;
699
780
  }
700
781
  async function mouseClick(targetId, x, y) {
701
782
  await sessionCommand(targetId, "Input.dispatchMouseEvent", { type: "mouseMoved", x, y, button: "none" });
@@ -703,9 +784,14 @@ async function mouseClick(targetId, x, y) {
703
784
  await sessionCommand(targetId, "Input.dispatchMouseEvent", { type: "mouseReleased", x, y, button: "left", clickCount: 1 });
704
785
  }
705
786
  async function getAttributeValue(targetId, backendNodeId, attribute) {
706
- const nodeId = await resolveNode(targetId, backendNodeId);
707
787
  if (attribute === "text") {
708
- return evaluate(targetId, `(() => { const n = this; return n.innerText ?? n.textContent ?? ''; }).call(document.querySelector('[data-bb-node-id="${nodeId}"]'))`);
788
+ const resolved = await sessionCommand(targetId, "DOM.resolveNode", { backendNodeId });
789
+ const call2 = await sessionCommand(targetId, "Runtime.callFunctionOn", {
790
+ objectId: resolved.object.objectId,
791
+ functionDeclaration: `function() { return (this instanceof HTMLElement ? this.innerText : this.textContent || '').trim(); }`,
792
+ returnByValue: true
793
+ });
794
+ return String(call2.result.value ?? "");
709
795
  }
710
796
  const result = await sessionCommand(targetId, "DOM.resolveNode", { backendNodeId });
711
797
  const call = await sessionCommand(targetId, "Runtime.callFunctionOn", {
@@ -933,7 +1019,7 @@ async function dispatchRequest(request) {
933
1019
  case "open": {
934
1020
  if (!request.url) return fail(request.id, "Missing url parameter");
935
1021
  if (request.tabId === void 0) {
936
- const created = await browserCommand("Target.createTarget", { url: request.url });
1022
+ const created = await browserCommand("Target.createTarget", { url: request.url, background: true });
937
1023
  const newTarget = await ensurePageTarget(created.targetId);
938
1024
  return ok(request.id, { url: request.url, tabId: newTarget.id });
939
1025
  }
@@ -950,7 +1036,7 @@ async function dispatchRequest(request) {
950
1036
  case "hover": {
951
1037
  if (!request.ref) return fail(request.id, "Missing ref parameter");
952
1038
  const backendNodeId = await parseRef(request.ref);
953
- const point = await getNodeBox(target.id, backendNodeId);
1039
+ const point = await getInteractablePoint(target.id, backendNodeId);
954
1040
  await sessionCommand(target.id, "Input.dispatchMouseEvent", { type: "mouseMoved", x: point.x, y: point.y, button: "none" });
955
1041
  if (request.action === "click") await mouseClick(target.id, point.x, point.y);
956
1042
  return ok(request.id, {});
@@ -986,7 +1072,14 @@ async function dispatchRequest(request) {
986
1072
  return ok(request.id, { value: request.value });
987
1073
  }
988
1074
  case "get": {
989
- if (!request.ref || !request.attribute) return fail(request.id, "Missing ref or attribute parameter");
1075
+ if (!request.attribute) return fail(request.id, "Missing attribute parameter");
1076
+ if (request.attribute === "url" && !request.ref) {
1077
+ return ok(request.id, { value: await evaluate(target.id, "location.href", true) });
1078
+ }
1079
+ if (request.attribute === "title" && !request.ref) {
1080
+ return ok(request.id, { value: await evaluate(target.id, "document.title", true) });
1081
+ }
1082
+ if (!request.ref) return fail(request.id, "Missing ref parameter");
990
1083
  const value = await getAttributeValue(target.id, await parseRef(request.ref), request.attribute);
991
1084
  return ok(request.id, { value });
992
1085
  }
@@ -1001,7 +1094,7 @@ async function dispatchRequest(request) {
1001
1094
  return ok(request.id, {});
1002
1095
  }
1003
1096
  case "wait": {
1004
- await new Promise((resolve2) => setTimeout(resolve2, request.ms ?? 1e3));
1097
+ await new Promise((resolve3) => setTimeout(resolve3, request.ms ?? 1e3));
1005
1098
  return ok(request.id, {});
1006
1099
  }
1007
1100
  case "press": {
@@ -1040,14 +1133,14 @@ async function dispatchRequest(request) {
1040
1133
  return ok(request.id, { tabs, activeIndex: tabs.findIndex((tab) => tab.active) });
1041
1134
  }
1042
1135
  case "tab_new": {
1043
- const created = await browserCommand("Target.createTarget", { url: request.url ?? "about:blank" });
1136
+ const created = await browserCommand("Target.createTarget", { url: request.url ?? "about:blank", background: true });
1044
1137
  return ok(request.id, { tabId: created.targetId, url: request.url ?? "about:blank" });
1045
1138
  }
1046
1139
  case "tab_select": {
1047
1140
  const tabs = (await getTargets()).filter((item) => item.type === "page");
1048
1141
  const selected = request.tabId !== void 0 ? tabs.find((item) => item.id === String(request.tabId) || Number(item.id) === request.tabId) : tabs[request.index ?? 0];
1049
1142
  if (!selected) return fail(request.id, "Tab not found");
1050
- connectionState.currentTargetId = selected.id;
1143
+ setCurrentTargetId(selected.id);
1051
1144
  await attachTarget(selected.id);
1052
1145
  return ok(request.id, { tabId: selected.id, url: selected.url, title: selected.title });
1053
1146
  }
@@ -1057,6 +1150,9 @@ async function dispatchRequest(request) {
1057
1150
  if (!selected) return fail(request.id, "Tab not found");
1058
1151
  await browserCommand("Target.closeTarget", { targetId: selected.id });
1059
1152
  connectionState?.refsByTarget.delete(selected.id);
1153
+ if (connectionState?.currentTargetId === selected.id) {
1154
+ setCurrentTargetId(void 0);
1155
+ }
1060
1156
  clearPersistedRefs(selected.id);
1061
1157
  return ok(request.id, { tabId: selected.id });
1062
1158
  }
@@ -1164,7 +1260,167 @@ async function dispatchRequest(request) {
1164
1260
  }
1165
1261
  }
1166
1262
 
1263
+ // packages/cli/src/monitor-manager.ts
1264
+ import { spawn as spawn2 } from "child_process";
1265
+ import { mkdir as mkdir2, readFile as readFile2, writeFile as writeFile2, unlink } from "fs/promises";
1266
+ import { request as httpRequest2 } from "http";
1267
+ import { randomBytes } from "crypto";
1268
+ import { fileURLToPath as fileURLToPath2 } from "url";
1269
+ import { dirname, resolve } from "path";
1270
+ import { existsSync as existsSync2 } from "fs";
1271
+ import os3 from "os";
1272
+ import path3 from "path";
1273
+ var MONITOR_DIR = path3.join(os3.homedir(), ".bb-browser");
1274
+ var PID_FILE = path3.join(MONITOR_DIR, "monitor.pid");
1275
+ var PORT_FILE = path3.join(MONITOR_DIR, "monitor.port");
1276
+ var TOKEN_FILE = path3.join(MONITOR_DIR, "monitor.token");
1277
+ var DEFAULT_MONITOR_PORT = 19826;
1278
+ function httpJson(method, url, token, body) {
1279
+ return new Promise((resolve3, reject) => {
1280
+ const parsed = new URL(url);
1281
+ const payload = body !== void 0 ? JSON.stringify(body) : void 0;
1282
+ const req = httpRequest2(
1283
+ {
1284
+ hostname: parsed.hostname,
1285
+ port: parsed.port,
1286
+ path: parsed.pathname,
1287
+ method,
1288
+ headers: {
1289
+ Authorization: `Bearer ${token}`,
1290
+ ...payload ? { "Content-Type": "application/json", "Content-Length": Buffer.byteLength(payload) } : {}
1291
+ },
1292
+ timeout: 5e3
1293
+ },
1294
+ (res) => {
1295
+ const chunks = [];
1296
+ res.on("data", (chunk) => chunks.push(Buffer.from(chunk)));
1297
+ res.on("end", () => {
1298
+ const raw = Buffer.concat(chunks).toString("utf8");
1299
+ if ((res.statusCode ?? 500) >= 400) {
1300
+ reject(new Error(`Monitor HTTP ${res.statusCode}: ${raw}`));
1301
+ return;
1302
+ }
1303
+ try {
1304
+ resolve3(JSON.parse(raw));
1305
+ } catch {
1306
+ reject(new Error(`Invalid JSON from monitor: ${raw}`));
1307
+ }
1308
+ });
1309
+ }
1310
+ );
1311
+ req.on("error", reject);
1312
+ req.on("timeout", () => {
1313
+ req.destroy();
1314
+ reject(new Error("Monitor request timed out"));
1315
+ });
1316
+ if (payload) req.write(payload);
1317
+ req.end();
1318
+ });
1319
+ }
1320
+ async function readPortFile() {
1321
+ try {
1322
+ const raw = await readFile2(PORT_FILE, "utf8");
1323
+ const port = Number.parseInt(raw.trim(), 10);
1324
+ return Number.isInteger(port) && port > 0 ? port : null;
1325
+ } catch {
1326
+ return null;
1327
+ }
1328
+ }
1329
+ async function readTokenFile() {
1330
+ try {
1331
+ return (await readFile2(TOKEN_FILE, "utf8")).trim();
1332
+ } catch {
1333
+ return null;
1334
+ }
1335
+ }
1336
+ async function ensureMonitorRunning() {
1337
+ const existingPort = await readPortFile();
1338
+ const existingToken = await readTokenFile();
1339
+ if (existingPort && existingToken) {
1340
+ try {
1341
+ const status = await httpJson(
1342
+ "GET",
1343
+ `http://127.0.0.1:${existingPort}/status`,
1344
+ existingToken
1345
+ );
1346
+ if (status.running) {
1347
+ return { port: existingPort, token: existingToken };
1348
+ }
1349
+ } catch {
1350
+ }
1351
+ }
1352
+ const cdp = await discoverCdpPort();
1353
+ if (!cdp) {
1354
+ throw new Error("Cannot start monitor: no browser connection found");
1355
+ }
1356
+ const token = randomBytes(32).toString("hex");
1357
+ const monitorPort = DEFAULT_MONITOR_PORT;
1358
+ const monitorScript = findMonitorScript();
1359
+ await mkdir2(MONITOR_DIR, { recursive: true });
1360
+ await writeFile2(TOKEN_FILE, token, { mode: 384 });
1361
+ const child = spawn2(process.execPath, [
1362
+ monitorScript,
1363
+ "--cdp-host",
1364
+ cdp.host,
1365
+ "--cdp-port",
1366
+ String(cdp.port),
1367
+ "--monitor-port",
1368
+ String(monitorPort),
1369
+ "--token",
1370
+ token
1371
+ ], {
1372
+ detached: true,
1373
+ stdio: "ignore"
1374
+ });
1375
+ child.unref();
1376
+ const deadline = Date.now() + 5e3;
1377
+ while (Date.now() < deadline) {
1378
+ await new Promise((r) => setTimeout(r, 200));
1379
+ try {
1380
+ const status = await httpJson(
1381
+ "GET",
1382
+ `http://127.0.0.1:${monitorPort}/status`,
1383
+ token
1384
+ );
1385
+ if (status.running) {
1386
+ return { port: monitorPort, token };
1387
+ }
1388
+ } catch {
1389
+ }
1390
+ }
1391
+ throw new Error("Monitor process did not start in time");
1392
+ }
1393
+ async function monitorCommand(request) {
1394
+ const { port, token } = await ensureMonitorRunning();
1395
+ return httpJson(
1396
+ "POST",
1397
+ `http://127.0.0.1:${port}/command`,
1398
+ token,
1399
+ request
1400
+ );
1401
+ }
1402
+ function findMonitorScript() {
1403
+ const currentFile = fileURLToPath2(import.meta.url);
1404
+ const currentDir = dirname(currentFile);
1405
+ const candidates = [
1406
+ // Built output (tsup puts it next to cli.js)
1407
+ resolve(currentDir, "cdp-monitor.js"),
1408
+ // Development: packages/cli/src -> packages/cli/dist
1409
+ resolve(currentDir, "../dist/cdp-monitor.js"),
1410
+ // Monorepo root dist
1411
+ resolve(currentDir, "../../dist/cdp-monitor.js"),
1412
+ resolve(currentDir, "../../../dist/cdp-monitor.js")
1413
+ ];
1414
+ for (const candidate of candidates) {
1415
+ if (existsSync2(candidate)) {
1416
+ return candidate;
1417
+ }
1418
+ }
1419
+ return candidates[0];
1420
+ }
1421
+
1167
1422
  // packages/cli/src/client.ts
1423
+ var MONITOR_ACTIONS = /* @__PURE__ */ new Set(["network", "console", "errors", "trace"]);
1168
1424
  var jqExpression;
1169
1425
  function setJqExpression(expression) {
1170
1426
  jqExpression = expression;
@@ -1183,13 +1439,23 @@ function handleJqResponse(response) {
1183
1439
  }
1184
1440
  }
1185
1441
  async function sendCommand2(request) {
1442
+ if (MONITOR_ACTIONS.has(request.action)) {
1443
+ try {
1444
+ return await monitorCommand(request);
1445
+ } catch {
1446
+ return sendCommand(request);
1447
+ }
1448
+ }
1186
1449
  return sendCommand(request);
1187
1450
  }
1188
1451
 
1189
1452
  // packages/cli/src/daemon-manager.ts
1190
- import { fileURLToPath as fileURLToPath2 } from "url";
1191
- import { dirname, resolve } from "path";
1192
- import { existsSync as existsSync2 } from "fs";
1453
+ import { fileURLToPath as fileURLToPath3 } from "url";
1454
+ import { dirname as dirname2, resolve as resolve2 } from "path";
1455
+ import { existsSync as existsSync3 } from "fs";
1456
+ async function isDaemonRunning() {
1457
+ return await isManagedBrowserRunning();
1458
+ }
1193
1459
  async function ensureDaemonRunning() {
1194
1460
  try {
1195
1461
  await ensureCdpConnection();
@@ -1207,7 +1473,7 @@ async function ensureDaemonRunning() {
1207
1473
  }
1208
1474
 
1209
1475
  // packages/cli/src/history-sqlite.ts
1210
- import { copyFileSync, existsSync as existsSync3, unlinkSync as unlinkSync2 } from "fs";
1476
+ import { copyFileSync, existsSync as existsSync4, unlinkSync as unlinkSync2 } from "fs";
1211
1477
  import { execSync as execSync2 } from "child_process";
1212
1478
  import { homedir, tmpdir } from "os";
1213
1479
  import { join } from "path";
@@ -1231,7 +1497,7 @@ function getHistoryPathCandidates() {
1231
1497
  }
1232
1498
  function findHistoryPath() {
1233
1499
  for (const historyPath of getHistoryPathCandidates()) {
1234
- if (existsSync3(historyPath)) {
1500
+ if (existsSync4(historyPath)) {
1235
1501
  return historyPath;
1236
1502
  }
1237
1503
  }
@@ -1351,7 +1617,7 @@ function getHistoryDomains(days) {
1351
1617
  }
1352
1618
 
1353
1619
  // packages/cli/src/commands/site.ts
1354
- import { readFileSync as readFileSync2, readdirSync, existsSync as existsSync4, mkdirSync } from "fs";
1620
+ import { readFileSync as readFileSync2, readdirSync, existsSync as existsSync5, mkdirSync } from "fs";
1355
1621
  import { join as join2, relative } from "path";
1356
1622
  import { homedir as homedir2 } from "os";
1357
1623
  import { execSync as execSync3 } from "child_process";
@@ -1370,6 +1636,10 @@ function checkCliUpdate() {
1370
1636
  } catch {
1371
1637
  }
1372
1638
  }
1639
+ function exitJsonError(error, extra = {}) {
1640
+ console.log(JSON.stringify({ success: false, error, ...extra }, null, 2));
1641
+ process.exit(1);
1642
+ }
1373
1643
  function parseSiteMeta(filePath, source) {
1374
1644
  let content;
1375
1645
  try {
@@ -1433,7 +1703,7 @@ function parseSiteMeta(filePath, source) {
1433
1703
  return meta;
1434
1704
  }
1435
1705
  function scanSites(dir, source) {
1436
- if (!existsSync4(dir)) return [];
1706
+ if (!existsSync5(dir)) return [];
1437
1707
  const sites = [];
1438
1708
  function walk(currentDir) {
1439
1709
  let entries;
@@ -1487,6 +1757,10 @@ function matchTabOrigin(tabUrl, domain) {
1487
1757
  function siteList(options) {
1488
1758
  const sites = getAllSites();
1489
1759
  if (sites.length === 0) {
1760
+ if (options.json) {
1761
+ console.log("[]");
1762
+ return;
1763
+ }
1490
1764
  console.log("\u672A\u627E\u5230\u4EFB\u4F55 site adapter\u3002");
1491
1765
  console.log(" \u5B89\u88C5\u793E\u533A adapter: bb-browser site update");
1492
1766
  console.log(` \u79C1\u6709 adapter \u76EE\u5F55: ${LOCAL_SITES_DIR}`);
@@ -1527,6 +1801,10 @@ function siteSearch(query, options) {
1527
1801
  (s) => s.name.toLowerCase().includes(q) || s.description.toLowerCase().includes(q) || s.domain.toLowerCase().includes(q)
1528
1802
  );
1529
1803
  if (matches.length === 0) {
1804
+ if (options.json) {
1805
+ console.log("[]");
1806
+ return;
1807
+ }
1530
1808
  console.log(`\u672A\u627E\u5230\u5339\u914D "${query}" \u7684 adapter\u3002`);
1531
1809
  console.log(" \u67E5\u770B\u6240\u6709: bb-browser site list");
1532
1810
  return;
@@ -1545,34 +1823,63 @@ function siteSearch(query, options) {
1545
1823
  console.log(`${s.name.padEnd(24)} ${s.description}${src}`);
1546
1824
  }
1547
1825
  }
1548
- function siteUpdate() {
1826
+ function siteUpdate(options = {}) {
1549
1827
  mkdirSync(BB_DIR, { recursive: true });
1550
- if (existsSync4(join2(COMMUNITY_SITES_DIR, ".git"))) {
1551
- console.log("\u66F4\u65B0\u793E\u533A site adapter \u5E93...");
1828
+ const updateMode = existsSync5(join2(COMMUNITY_SITES_DIR, ".git")) ? "pull" : "clone";
1829
+ if (updateMode === "pull") {
1830
+ if (!options.json) {
1831
+ console.log("\u66F4\u65B0\u793E\u533A site adapter \u5E93...");
1832
+ }
1552
1833
  try {
1553
1834
  execSync3("git pull --ff-only", { cwd: COMMUNITY_SITES_DIR, stdio: "pipe" });
1554
- console.log("\u66F4\u65B0\u5B8C\u6210\u3002");
1555
- console.log("");
1556
- console.log("\u{1F4A1} \u8FD0\u884C bb-browser site recommend \u770B\u770B\u54EA\u4E9B\u548C\u4F60\u7684\u6D4F\u89C8\u4E60\u60EF\u5339\u914D");
1835
+ if (!options.json) {
1836
+ console.log("\u66F4\u65B0\u5B8C\u6210\u3002");
1837
+ console.log("");
1838
+ console.log("\u{1F4A1} \u8FD0\u884C bb-browser site recommend \u770B\u770B\u54EA\u4E9B\u548C\u4F60\u7684\u6D4F\u89C8\u4E60\u60EF\u5339\u914D");
1839
+ }
1557
1840
  } catch (e) {
1841
+ const message = e instanceof Error ? e.message : String(e);
1842
+ const manualAction = "cd ~/.bb-browser/bb-sites && git pull";
1843
+ if (options.json) {
1844
+ exitJsonError(`\u66F4\u65B0\u5931\u8D25: ${message}`, { action: manualAction, updateMode });
1845
+ }
1558
1846
  console.error(`\u66F4\u65B0\u5931\u8D25: ${e instanceof Error ? e.message : e}`);
1559
1847
  console.error(" \u624B\u52A8\u4FEE\u590D: cd ~/.bb-browser/bb-sites && git pull");
1560
1848
  process.exit(1);
1561
1849
  }
1562
1850
  } else {
1563
- console.log(`\u514B\u9686\u793E\u533A adapter \u5E93: ${COMMUNITY_REPO}`);
1851
+ if (!options.json) {
1852
+ console.log(`\u514B\u9686\u793E\u533A adapter \u5E93: ${COMMUNITY_REPO}`);
1853
+ }
1564
1854
  try {
1565
1855
  execSync3(`git clone ${COMMUNITY_REPO} ${COMMUNITY_SITES_DIR}`, { stdio: "pipe" });
1566
- console.log("\u514B\u9686\u5B8C\u6210\u3002");
1567
- console.log("");
1568
- console.log("\u{1F4A1} \u8FD0\u884C bb-browser site recommend \u770B\u770B\u54EA\u4E9B\u548C\u4F60\u7684\u6D4F\u89C8\u4E60\u60EF\u5339\u914D");
1856
+ if (!options.json) {
1857
+ console.log("\u514B\u9686\u5B8C\u6210\u3002");
1858
+ console.log("");
1859
+ console.log("\u{1F4A1} \u8FD0\u884C bb-browser site recommend \u770B\u770B\u54EA\u4E9B\u548C\u4F60\u7684\u6D4F\u89C8\u4E60\u60EF\u5339\u914D");
1860
+ }
1569
1861
  } catch (e) {
1862
+ const message = e instanceof Error ? e.message : String(e);
1863
+ const manualAction = `git clone ${COMMUNITY_REPO} ~/.bb-browser/bb-sites`;
1864
+ if (options.json) {
1865
+ exitJsonError(`\u514B\u9686\u5931\u8D25: ${message}`, { action: manualAction, updateMode });
1866
+ }
1570
1867
  console.error(`\u514B\u9686\u5931\u8D25: ${e instanceof Error ? e.message : e}`);
1571
1868
  console.error(` \u624B\u52A8\u4FEE\u590D: git clone ${COMMUNITY_REPO} ~/.bb-browser/bb-sites`);
1572
1869
  process.exit(1);
1573
1870
  }
1574
1871
  }
1575
1872
  const sites = scanSites(COMMUNITY_SITES_DIR, "community");
1873
+ if (options.json) {
1874
+ console.log(JSON.stringify({
1875
+ success: true,
1876
+ updateMode,
1877
+ communityRepo: COMMUNITY_REPO,
1878
+ communityDir: COMMUNITY_SITES_DIR,
1879
+ siteCount: sites.length
1880
+ }, null, 2));
1881
+ return;
1882
+ }
1576
1883
  console.log(`\u5DF2\u5B89\u88C5 ${sites.length} \u4E2A\u793E\u533A adapter\u3002`);
1577
1884
  console.log(`\u2B50 Like bb-browser? \u2192 bb-browser star`);
1578
1885
  checkCliUpdate();
@@ -1583,6 +1890,9 @@ function findSiteByName(name) {
1583
1890
  function siteInfo(name, options) {
1584
1891
  const site = findSiteByName(name);
1585
1892
  if (!site) {
1893
+ if (options.json) {
1894
+ exitJsonError(`adapter "${name}" not found`, { action: "bb-browser site list" });
1895
+ }
1586
1896
  console.error(`[error] site info: adapter "${name}" not found.`);
1587
1897
  console.error(" Try: bb-browser site list");
1588
1898
  process.exit(1);
@@ -1695,6 +2005,12 @@ async function siteRun(name, args, options) {
1695
2005
  const site = sites.find((s) => s.name === name);
1696
2006
  if (!site) {
1697
2007
  const fuzzy = sites.filter((s) => s.name.includes(name));
2008
+ if (options.json) {
2009
+ exitJsonError(`site "${name}" not found`, {
2010
+ suggestions: fuzzy.slice(0, 5).map((s) => s.name),
2011
+ action: fuzzy.length > 0 ? void 0 : "bb-browser site update"
2012
+ });
2013
+ }
1698
2014
  console.error(`[error] site: "${name}" not found.`);
1699
2015
  if (fuzzy.length > 0) {
1700
2016
  console.error(" Did you mean:");
@@ -1729,11 +2045,17 @@ async function siteRun(name, args, options) {
1729
2045
  }
1730
2046
  for (const [argName, argDef] of Object.entries(site.args)) {
1731
2047
  if (argDef.required && !argMap[argName]) {
1732
- console.error(`[error] site ${name}: missing required argument "${argName}".`);
1733
2048
  const usage = argNames.map((a) => {
1734
2049
  const def = site.args[a];
1735
2050
  return def.required ? `<${a}>` : `[${a}]`;
1736
2051
  }).join(" ");
2052
+ if (options.json) {
2053
+ exitJsonError(`missing required argument "${argName}"`, {
2054
+ usage: `bb-browser site ${name} ${usage}`,
2055
+ example: site.example
2056
+ });
2057
+ }
2058
+ console.error(`[error] site ${name}: missing required argument "${argName}".`);
1737
2059
  console.error(` Usage: bb-browser site ${name} ${usage}`);
1738
2060
  if (site.example) console.error(` Example: ${site.example}`);
1739
2061
  process.exit(1);
@@ -1744,7 +2066,7 @@ async function siteRun(name, args, options) {
1744
2066
  const argsJson = JSON.stringify(argMap);
1745
2067
  const script = `(${jsBody})(${argsJson})`;
1746
2068
  if (options.openclaw) {
1747
- const { ocGetTabs, ocFindTabByDomain, ocOpenTab, ocEvaluate } = await import("./openclaw-bridge-P3G4KGYM.js");
2069
+ const { ocGetTabs, ocFindTabByDomain, ocOpenTab, ocEvaluate } = await import("./openclaw-bridge-HBJH6UFO.js");
1748
2070
  let targetId;
1749
2071
  if (site.domain) {
1750
2072
  const tabs = ocGetTabs();
@@ -1753,7 +2075,7 @@ async function siteRun(name, args, options) {
1753
2075
  targetId = existing.targetId;
1754
2076
  } else {
1755
2077
  targetId = ocOpenTab(`https://${site.domain}`);
1756
- await new Promise((resolve2) => setTimeout(resolve2, 3e3));
2078
+ await new Promise((resolve3) => setTimeout(resolve3, 3e3));
1757
2079
  }
1758
2080
  } else {
1759
2081
  const tabs = ocGetTabs();
@@ -1815,7 +2137,7 @@ async function siteRun(name, args, options) {
1815
2137
  url: `https://${site.domain}`
1816
2138
  });
1817
2139
  targetTabId = newResp.data?.tabId;
1818
- await new Promise((resolve2) => setTimeout(resolve2, 3e3));
2140
+ await new Promise((resolve3) => setTimeout(resolve3, 3e3));
1819
2141
  }
1820
2142
  }
1821
2143
  const evalReq = { id: generateId(), action: "eval", script, tabId: targetTabId };
@@ -1929,7 +2251,7 @@ async function siteCommand(args, options = {}) {
1929
2251
  await siteRecommend(options);
1930
2252
  break;
1931
2253
  case "update":
1932
- siteUpdate();
2254
+ siteUpdate(options);
1933
2255
  break;
1934
2256
  case "run":
1935
2257
  if (!args[1]) {
@@ -1955,9 +2277,9 @@ async function siteCommand(args, options = {}) {
1955
2277
  }
1956
2278
  function silentUpdate() {
1957
2279
  const gitDir = join2(COMMUNITY_SITES_DIR, ".git");
1958
- if (!existsSync4(gitDir)) return;
1959
- import("child_process").then(({ spawn: spawn2 }) => {
1960
- const child = spawn2("git", ["pull", "--ff-only"], {
2280
+ if (!existsSync5(gitDir)) return;
2281
+ import("child_process").then(({ spawn: spawn3 }) => {
2282
+ const child = spawn3("git", ["pull", "--ff-only"], {
1961
2283
  cwd: COMMUNITY_SITES_DIR,
1962
2284
  stdio: "ignore",
1963
2285
  detached: true
@@ -2255,17 +2577,17 @@ async function getCommand(attribute, ref, options = {}) {
2255
2577
 
2256
2578
  // packages/cli/src/commands/screenshot.ts
2257
2579
  import fs from "fs";
2258
- import path3 from "path";
2259
- import os3 from "os";
2580
+ import path4 from "path";
2581
+ import os4 from "os";
2260
2582
  function getDefaultPath() {
2261
2583
  const timestamp = (/* @__PURE__ */ new Date()).toISOString().replace(/[:.]/g, "-");
2262
2584
  const filename = `bb-screenshot-${timestamp}.png`;
2263
- return path3.join(os3.tmpdir(), filename);
2585
+ return path4.join(os4.tmpdir(), filename);
2264
2586
  }
2265
2587
  function saveBase64Image(dataUrl, filePath) {
2266
2588
  const base64Data = dataUrl.replace(/^data:image\/png;base64,/, "");
2267
2589
  const buffer = Buffer.from(base64Data, "base64");
2268
- const dir = path3.dirname(filePath);
2590
+ const dir = path4.dirname(filePath);
2269
2591
  if (!fs.existsSync(dir)) {
2270
2592
  fs.mkdirSync(dir, { recursive: true });
2271
2593
  }
@@ -2273,7 +2595,7 @@ function saveBase64Image(dataUrl, filePath) {
2273
2595
  }
2274
2596
  async function screenshotCommand(outputPath, options = {}) {
2275
2597
  await ensureDaemonRunning();
2276
- const filePath = outputPath ? path3.resolve(outputPath) : getDefaultPath();
2598
+ const filePath = outputPath ? path4.resolve(outputPath) : getDefaultPath();
2277
2599
  const request = {
2278
2600
  id: generateId(),
2279
2601
  action: "screenshot",
@@ -3136,7 +3458,7 @@ async function ensureTabForOrigin(origin, hostname) {
3136
3458
  if (!newResp.success) {
3137
3459
  throw new Error(`\u65E0\u6CD5\u6253\u5F00 ${origin}: ${newResp.error}`);
3138
3460
  }
3139
- await new Promise((resolve2) => setTimeout(resolve2, 3e3));
3461
+ await new Promise((resolve3) => setTimeout(resolve3, 3e3));
3140
3462
  return newResp.data?.tabId;
3141
3463
  }
3142
3464
  function buildFetchScript(url, options) {
@@ -3282,8 +3604,18 @@ async function historyCommand(subCommand, options = {}) {
3282
3604
  }
3283
3605
  }
3284
3606
 
3607
+ // packages/cli/src/commands/daemon.ts
3608
+ async function statusCommand(options = {}) {
3609
+ const running = await isDaemonRunning();
3610
+ if (options.json) {
3611
+ console.log(JSON.stringify({ running }));
3612
+ } else {
3613
+ console.log(running ? "\u6D4F\u89C8\u5668\u8FD0\u884C\u4E2D" : "\u6D4F\u89C8\u5668\u672A\u8FD0\u884C");
3614
+ }
3615
+ }
3616
+
3285
3617
  // packages/cli/src/index.ts
3286
- var VERSION = "0.8.2";
3618
+ var VERSION = "0.9.0";
3287
3619
  var HELP_TEXT = `
3288
3620
  bb-browser - AI Agent \u6D4F\u89C8\u5668\u81EA\u52A8\u5316\u5DE5\u5177
3289
3621
 
@@ -3438,9 +3770,9 @@ async function main() {
3438
3770
  return;
3439
3771
  }
3440
3772
  if (process.argv.includes("--mcp")) {
3441
- const mcpPath = fileURLToPath3(new URL("./mcp.js", import.meta.url));
3442
- const { spawn: spawn2 } = await import("child_process");
3443
- const child = spawn2(process.execPath, [mcpPath], { stdio: "inherit" });
3773
+ const mcpPath = fileURLToPath4(new URL("./mcp.js", import.meta.url));
3774
+ const { spawn: spawn3 } = await import("child_process");
3775
+ const child = spawn3(process.execPath, [mcpPath], { stdio: "inherit" });
3444
3776
  child.on("exit", (code) => process.exit(code ?? 0));
3445
3777
  return;
3446
3778
  }
@@ -3663,6 +3995,10 @@ async function main() {
3663
3995
  await tabCommand(parsed.args, { json: parsed.flags.json });
3664
3996
  break;
3665
3997
  }
3998
+ case "status": {
3999
+ await statusCommand({ json: parsed.flags.json });
4000
+ break;
4001
+ }
3666
4002
  case "frame": {
3667
4003
  const selectorOrMain = parsed.args[0];
3668
4004
  if (!selectorOrMain) {