bb-browser 0.7.0 → 0.8.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.
Files changed (3) hide show
  1. package/dist/cli.js +1107 -312
  2. package/dist/cli.js.map +1 -1
  3. package/package.json +1 -1
package/dist/cli.js CHANGED
@@ -1,7 +1,6 @@
1
1
  #!/usr/bin/env node
2
2
  import {
3
3
  COMMAND_TIMEOUT,
4
- DAEMON_BASE_URL,
5
4
  generateId
6
5
  } from "./chunk-DBJBHYC7.js";
7
6
  import {
@@ -10,7 +9,858 @@ import {
10
9
  import "./chunk-D4HDZEJT.js";
11
10
 
12
11
  // packages/cli/src/index.ts
13
- import { fileURLToPath as fileURLToPath2 } from "url";
12
+ import { fileURLToPath as fileURLToPath3 } from "url";
13
+
14
+ // packages/cli/src/cdp-client.ts
15
+ import { readFileSync } from "fs";
16
+ import { request as httpRequest } from "http";
17
+ import { request as httpsRequest } from "https";
18
+ import path2 from "path";
19
+ import { fileURLToPath } from "url";
20
+ import WebSocket from "ws";
21
+
22
+ // packages/cli/src/cdp-discovery.ts
23
+ import { execFile, execSync, spawn } from "child_process";
24
+ import { existsSync } from "fs";
25
+ import { mkdir, readFile, writeFile } from "fs/promises";
26
+ import os from "os";
27
+ import path from "path";
28
+ var DEFAULT_CDP_PORT = 19825;
29
+ var MANAGED_BROWSER_DIR = path.join(os.homedir(), ".bb-browser", "browser");
30
+ var MANAGED_USER_DATA_DIR = path.join(MANAGED_BROWSER_DIR, "user-data");
31
+ var MANAGED_PORT_FILE = path.join(MANAGED_BROWSER_DIR, "cdp-port");
32
+ function execFileAsync(command, args, timeout) {
33
+ return new Promise((resolve2, reject) => {
34
+ execFile(command, args, { encoding: "utf8", timeout }, (error, stdout) => {
35
+ if (error) {
36
+ reject(error);
37
+ return;
38
+ }
39
+ resolve2(stdout.trim());
40
+ });
41
+ });
42
+ }
43
+ function getArgValue(flag) {
44
+ const index = process.argv.indexOf(flag);
45
+ if (index < 0) return void 0;
46
+ return process.argv[index + 1];
47
+ }
48
+ async function tryOpenClaw() {
49
+ try {
50
+ const raw = await execFileAsync("npx", ["openclaw", "browser", "status", "--json"], 5e3);
51
+ const parsed = JSON.parse(raw);
52
+ const port = Number(parsed?.cdpPort);
53
+ if (Number.isInteger(port) && port > 0) {
54
+ return { host: "127.0.0.1", port };
55
+ }
56
+ } catch {
57
+ }
58
+ return null;
59
+ }
60
+ async function canConnect(host, port) {
61
+ try {
62
+ const controller = new AbortController();
63
+ const timeout = setTimeout(() => controller.abort(), 1200);
64
+ const response = await fetch(`http://${host}:${port}/json/version`, { signal: controller.signal });
65
+ clearTimeout(timeout);
66
+ return response.ok;
67
+ } catch {
68
+ return false;
69
+ }
70
+ }
71
+ function findBrowserExecutable() {
72
+ if (process.platform === "darwin") {
73
+ const candidates = [
74
+ "/Applications/Google Chrome.app/Contents/MacOS/Google Chrome",
75
+ "/Applications/Microsoft Edge.app/Contents/MacOS/Microsoft Edge",
76
+ "/Applications/Brave Browser.app/Contents/MacOS/Brave Browser"
77
+ ];
78
+ return candidates.find((candidate) => existsSync(candidate)) ?? null;
79
+ }
80
+ if (process.platform === "linux") {
81
+ const candidates = ["google-chrome", "google-chrome-stable", "chromium-browser", "chromium"];
82
+ for (const candidate of candidates) {
83
+ try {
84
+ const resolved = execSync(`which ${candidate}`, { encoding: "utf8", stdio: ["ignore", "pipe", "ignore"] }).trim();
85
+ if (resolved) {
86
+ return resolved;
87
+ }
88
+ } catch {
89
+ }
90
+ }
91
+ return null;
92
+ }
93
+ if (process.platform === "win32") {
94
+ const candidates = [
95
+ "C:\\Program Files\\Google\\Chrome\\Application\\chrome.exe",
96
+ "C:\\Program Files (x86)\\Google\\Chrome\\Application\\chrome.exe",
97
+ "C:\\Program Files (x86)\\Microsoft\\Edge\\Application\\msedge.exe"
98
+ ];
99
+ return candidates.find((candidate) => existsSync(candidate)) ?? null;
100
+ }
101
+ return null;
102
+ }
103
+ async function launchManagedBrowser(port = DEFAULT_CDP_PORT) {
104
+ const executable = findBrowserExecutable();
105
+ if (!executable) {
106
+ return null;
107
+ }
108
+ await mkdir(MANAGED_USER_DATA_DIR, { recursive: true });
109
+ const defaultProfileDir = path.join(MANAGED_USER_DATA_DIR, "Default");
110
+ const prefsPath = path.join(defaultProfileDir, "Preferences");
111
+ await mkdir(defaultProfileDir, { recursive: true });
112
+ try {
113
+ let prefs = {};
114
+ try {
115
+ prefs = JSON.parse(await readFile(prefsPath, "utf8"));
116
+ } catch {
117
+ }
118
+ if (!prefs.profile?.name || prefs.profile.name !== "bb-browser") {
119
+ prefs.profile = { ...prefs.profile || {}, name: "bb-browser" };
120
+ await writeFile(prefsPath, JSON.stringify(prefs), "utf8");
121
+ }
122
+ } catch {
123
+ }
124
+ const args = [
125
+ `--remote-debugging-port=${port}`,
126
+ `--user-data-dir=${MANAGED_USER_DATA_DIR}`,
127
+ "--no-first-run",
128
+ "--no-default-browser-check",
129
+ "--disable-sync",
130
+ "--disable-background-networking",
131
+ "--disable-component-update",
132
+ "--disable-features=Translate,MediaRouter",
133
+ "--disable-session-crashed-bubble",
134
+ "--hide-crash-restore-bubble",
135
+ "about:blank"
136
+ ];
137
+ try {
138
+ const child = spawn(executable, args, {
139
+ detached: true,
140
+ stdio: "ignore"
141
+ });
142
+ child.unref();
143
+ } catch {
144
+ return null;
145
+ }
146
+ await mkdir(MANAGED_BROWSER_DIR, { recursive: true });
147
+ await writeFile(MANAGED_PORT_FILE, String(port), "utf8");
148
+ const deadline = Date.now() + 8e3;
149
+ while (Date.now() < deadline) {
150
+ if (await canConnect("127.0.0.1", port)) {
151
+ return { host: "127.0.0.1", port };
152
+ }
153
+ await new Promise((resolve2) => setTimeout(resolve2, 250));
154
+ }
155
+ return null;
156
+ }
157
+ async function discoverCdpPort() {
158
+ const explicitPort = Number.parseInt(getArgValue("--port") ?? "", 10);
159
+ if (Number.isInteger(explicitPort) && explicitPort > 0 && await canConnect("127.0.0.1", explicitPort)) {
160
+ return { host: "127.0.0.1", port: explicitPort };
161
+ }
162
+ if (process.argv.includes("--openclaw")) {
163
+ const viaOpenClaw = await tryOpenClaw();
164
+ if (viaOpenClaw && await canConnect(viaOpenClaw.host, viaOpenClaw.port)) {
165
+ return viaOpenClaw;
166
+ }
167
+ }
168
+ const detectedOpenClaw = await tryOpenClaw();
169
+ if (detectedOpenClaw && await canConnect(detectedOpenClaw.host, detectedOpenClaw.port)) {
170
+ return detectedOpenClaw;
171
+ }
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 };
177
+ }
178
+ } catch {
179
+ }
180
+ return launchManagedBrowser();
181
+ }
182
+
183
+ // packages/cli/src/cdp-client.ts
184
+ var connectionState = null;
185
+ var reconnecting = null;
186
+ var networkRequests = /* @__PURE__ */ new Map();
187
+ var networkEnabled = false;
188
+ var consoleMessages = [];
189
+ var consoleEnabled = false;
190
+ var jsErrors = [];
191
+ var errorsEnabled = false;
192
+ var traceRecording = false;
193
+ var traceEvents = [];
194
+ function buildRequestError(error) {
195
+ return error instanceof Error ? error : new Error(String(error));
196
+ }
197
+ function fetchJson(url) {
198
+ return new Promise((resolve2, reject) => {
199
+ const requester = url.startsWith("https:") ? httpsRequest : httpRequest;
200
+ const req = requester(url, { method: "GET" }, (res) => {
201
+ const chunks = [];
202
+ res.on("data", (chunk) => chunks.push(Buffer.from(chunk)));
203
+ res.on("end", () => {
204
+ const raw = Buffer.concat(chunks).toString("utf8");
205
+ if ((res.statusCode ?? 500) >= 400) {
206
+ reject(new Error(`HTTP ${res.statusCode ?? 500}: ${raw}`));
207
+ return;
208
+ }
209
+ try {
210
+ resolve2(JSON.parse(raw));
211
+ } catch (error) {
212
+ reject(error);
213
+ }
214
+ });
215
+ });
216
+ req.on("error", reject);
217
+ req.end();
218
+ });
219
+ }
220
+ async function getJsonList(host, port) {
221
+ const data = await fetchJson(`http://${host}:${port}/json/list`);
222
+ return Array.isArray(data) ? data : [];
223
+ }
224
+ async function getJsonVersion(host, port) {
225
+ const data = await fetchJson(`http://${host}:${port}/json/version`);
226
+ const url = data.webSocketDebuggerUrl;
227
+ if (typeof url !== "string" || !url) {
228
+ throw new Error("CDP endpoint missing webSocketDebuggerUrl");
229
+ }
230
+ return { webSocketDebuggerUrl: url };
231
+ }
232
+ function connectWebSocket(url) {
233
+ return new Promise((resolve2, reject) => {
234
+ const ws = new WebSocket(url);
235
+ ws.once("open", () => resolve2(ws));
236
+ ws.once("error", reject);
237
+ });
238
+ }
239
+ function createState(host, port, browserWsUrl, browserSocket) {
240
+ const state = {
241
+ host,
242
+ port,
243
+ browserWsUrl,
244
+ browserSocket,
245
+ browserPending: /* @__PURE__ */ new Map(),
246
+ nextMessageId: 1,
247
+ sessions: /* @__PURE__ */ new Map(),
248
+ attachedTargets: /* @__PURE__ */ new Map(),
249
+ refsByTarget: /* @__PURE__ */ new Map(),
250
+ activeFrameIdByTarget: /* @__PURE__ */ new Map(),
251
+ dialogHandlers: /* @__PURE__ */ new Map()
252
+ };
253
+ browserSocket.on("message", (raw) => {
254
+ const message = JSON.parse(raw.toString());
255
+ if (typeof message.id === "number") {
256
+ const pending = state.browserPending.get(message.id);
257
+ if (!pending) return;
258
+ state.browserPending.delete(message.id);
259
+ if (message.error) {
260
+ pending.reject(new Error(`${pending.method}: ${message.error.message ?? "Unknown CDP error"}`));
261
+ } else {
262
+ pending.resolve(message.result);
263
+ }
264
+ return;
265
+ }
266
+ if (message.method === "Target.attachedToTarget") {
267
+ const params = message.params;
268
+ const sessionId = params.sessionId;
269
+ const targetInfo = params.targetInfo;
270
+ if (typeof sessionId === "string" && typeof targetInfo?.targetId === "string") {
271
+ state.sessions.set(targetInfo.targetId, sessionId);
272
+ state.attachedTargets.set(sessionId, targetInfo.targetId);
273
+ }
274
+ return;
275
+ }
276
+ if (message.method === "Target.detachedFromTarget") {
277
+ const params = message.params;
278
+ const sessionId = params.sessionId;
279
+ if (typeof sessionId === "string") {
280
+ const targetId = state.attachedTargets.get(sessionId);
281
+ if (targetId) {
282
+ state.sessions.delete(targetId);
283
+ state.attachedTargets.delete(sessionId);
284
+ state.activeFrameIdByTarget.delete(targetId);
285
+ state.dialogHandlers.delete(targetId);
286
+ }
287
+ }
288
+ return;
289
+ }
290
+ if (message.method === "Target.receivedMessageFromTarget") {
291
+ const params = message.params;
292
+ const sessionId = params.sessionId;
293
+ const messageText = params.message;
294
+ if (typeof sessionId === "string" && typeof messageText === "string") {
295
+ const targetId = state.attachedTargets.get(sessionId);
296
+ if (targetId) {
297
+ handleSessionEvent(targetId, JSON.parse(messageText)).catch(() => {
298
+ });
299
+ }
300
+ }
301
+ return;
302
+ }
303
+ if (typeof message.sessionId === "string" && typeof message.method === "string") {
304
+ const targetId = state.attachedTargets.get(message.sessionId);
305
+ if (targetId) {
306
+ handleSessionEvent(targetId, message).catch(() => {
307
+ });
308
+ }
309
+ }
310
+ });
311
+ browserSocket.on("close", () => {
312
+ if (connectionState === state) {
313
+ connectionState = null;
314
+ }
315
+ for (const pending of state.browserPending.values()) {
316
+ pending.reject(new Error("CDP connection closed"));
317
+ }
318
+ state.browserPending.clear();
319
+ });
320
+ browserSocket.on("error", () => {
321
+ });
322
+ return state;
323
+ }
324
+ async function browserCommand(method, params = {}) {
325
+ const state = connectionState;
326
+ if (!state) throw new Error("CDP connection not initialized");
327
+ const id = state.nextMessageId++;
328
+ const payload = JSON.stringify({ id, method, params });
329
+ const promise = new Promise((resolve2, reject) => {
330
+ state.browserPending.set(id, { resolve: resolve2, reject, method });
331
+ });
332
+ state.browserSocket.send(payload);
333
+ return promise;
334
+ }
335
+ async function sessionCommand(targetId, method, params = {}) {
336
+ const state = connectionState;
337
+ if (!state) throw new Error("CDP connection not initialized");
338
+ const sessionId = state.sessions.get(targetId) ?? await attachTarget(targetId);
339
+ const id = state.nextMessageId++;
340
+ const payload = JSON.stringify({ id, method, params, sessionId });
341
+ return new Promise((resolve2, reject) => {
342
+ const check = (raw) => {
343
+ const msg = JSON.parse(raw.toString());
344
+ if (msg.id === id && msg.sessionId === sessionId) {
345
+ state.browserSocket.off("message", check);
346
+ if (msg.error) reject(new Error(`${method}: ${msg.error.message ?? "Unknown CDP error"}`));
347
+ else resolve2(msg.result);
348
+ }
349
+ };
350
+ state.browserSocket.on("message", check);
351
+ state.browserSocket.send(payload);
352
+ });
353
+ }
354
+ function getActiveFrameId(targetId) {
355
+ const frameId = connectionState?.activeFrameIdByTarget.get(targetId);
356
+ return frameId ?? void 0;
357
+ }
358
+ async function pageCommand(targetId, method, params = {}) {
359
+ const frameId = getActiveFrameId(targetId);
360
+ return sessionCommand(targetId, method, frameId ? { ...params, frameId } : params);
361
+ }
362
+ function normalizeHeaders(headers) {
363
+ if (!headers || typeof headers !== "object") return void 0;
364
+ return Object.fromEntries(Object.entries(headers).map(([key, value]) => [key, String(value)]));
365
+ }
366
+ async function handleSessionEvent(targetId, event) {
367
+ const method = event.method;
368
+ const params = event.params ?? {};
369
+ if (typeof method !== "string") return;
370
+ if (method === "Page.javascriptDialogOpening") {
371
+ const handler = connectionState?.dialogHandlers.get(targetId);
372
+ if (handler) {
373
+ await sessionCommand(targetId, "Page.handleJavaScriptDialog", {
374
+ accept: handler.accept,
375
+ ...handler.promptText !== void 0 ? { promptText: handler.promptText } : {}
376
+ });
377
+ }
378
+ return;
379
+ }
380
+ if (method === "Network.requestWillBeSent") {
381
+ const requestId = typeof params.requestId === "string" ? params.requestId : void 0;
382
+ const request = params.request;
383
+ if (!requestId || !request) return;
384
+ networkRequests.set(requestId, {
385
+ requestId,
386
+ url: String(request.url ?? ""),
387
+ method: String(request.method ?? "GET"),
388
+ type: String(params.type ?? "Other"),
389
+ timestamp: Math.round(Number(params.timestamp ?? Date.now()) * 1e3),
390
+ requestHeaders: normalizeHeaders(request.headers),
391
+ requestBody: typeof request.postData === "string" ? request.postData : void 0
392
+ });
393
+ return;
394
+ }
395
+ if (method === "Network.responseReceived") {
396
+ const requestId = typeof params.requestId === "string" ? params.requestId : void 0;
397
+ const response = params.response;
398
+ if (!requestId || !response) return;
399
+ const existing = networkRequests.get(requestId);
400
+ if (!existing) return;
401
+ existing.status = typeof response.status === "number" ? response.status : void 0;
402
+ existing.statusText = typeof response.statusText === "string" ? response.statusText : void 0;
403
+ existing.responseHeaders = normalizeHeaders(response.headers);
404
+ existing.mimeType = typeof response.mimeType === "string" ? response.mimeType : void 0;
405
+ networkRequests.set(requestId, existing);
406
+ return;
407
+ }
408
+ if (method === "Network.loadingFailed") {
409
+ const requestId = typeof params.requestId === "string" ? params.requestId : void 0;
410
+ if (!requestId) return;
411
+ const existing = networkRequests.get(requestId);
412
+ if (!existing) return;
413
+ existing.failed = true;
414
+ existing.failureReason = typeof params.errorText === "string" ? params.errorText : "Unknown error";
415
+ networkRequests.set(requestId, existing);
416
+ return;
417
+ }
418
+ if (method === "Runtime.consoleAPICalled") {
419
+ const type = String(params.type ?? "log");
420
+ const args = Array.isArray(params.args) ? params.args : [];
421
+ const text = args.map((arg) => {
422
+ if (typeof arg.value === "string") return arg.value;
423
+ if (arg.value !== void 0) return String(arg.value);
424
+ if (typeof arg.description === "string") return arg.description;
425
+ return "";
426
+ }).filter(Boolean).join(" ");
427
+ const stack = params.stackTrace;
428
+ const firstCallFrame = Array.isArray(stack?.callFrames) ? stack?.callFrames[0] : void 0;
429
+ consoleMessages.push({
430
+ type: ["log", "info", "warn", "error", "debug"].includes(type) ? type : "log",
431
+ text,
432
+ timestamp: Math.round(Number(params.timestamp ?? Date.now())),
433
+ url: typeof firstCallFrame?.url === "string" ? firstCallFrame.url : void 0,
434
+ lineNumber: typeof firstCallFrame?.lineNumber === "number" ? firstCallFrame.lineNumber : void 0
435
+ });
436
+ return;
437
+ }
438
+ if (method === "Runtime.exceptionThrown") {
439
+ const details = params.exceptionDetails;
440
+ if (!details) return;
441
+ const exception = details.exception;
442
+ const stackTrace = details.stackTrace;
443
+ const callFrames = Array.isArray(stackTrace?.callFrames) ? stackTrace.callFrames : [];
444
+ jsErrors.push({
445
+ message: typeof exception?.description === "string" ? exception.description : String(details.text ?? "JavaScript exception"),
446
+ url: typeof details.url === "string" ? details.url : typeof callFrames[0]?.url === "string" ? String(callFrames[0].url) : void 0,
447
+ lineNumber: typeof details.lineNumber === "number" ? details.lineNumber : void 0,
448
+ columnNumber: typeof details.columnNumber === "number" ? details.columnNumber : void 0,
449
+ stackTrace: callFrames.length > 0 ? callFrames.map((frame) => `${String(frame.functionName ?? "<anonymous>")} (${String(frame.url ?? "")}:${String(frame.lineNumber ?? 0)}:${String(frame.columnNumber ?? 0)})`).join("\n") : void 0,
450
+ timestamp: Date.now()
451
+ });
452
+ }
453
+ }
454
+ async function ensureNetworkMonitoring(targetId) {
455
+ if (networkEnabled) return;
456
+ await sessionCommand(targetId, "Network.enable");
457
+ networkEnabled = true;
458
+ }
459
+ async function ensureConsoleMonitoring(targetId) {
460
+ if (consoleEnabled && errorsEnabled) return;
461
+ await sessionCommand(targetId, "Runtime.enable");
462
+ consoleEnabled = true;
463
+ errorsEnabled = true;
464
+ }
465
+ async function attachTarget(targetId) {
466
+ const result = await browserCommand("Target.attachToTarget", {
467
+ targetId,
468
+ flatten: true
469
+ });
470
+ connectionState?.sessions.set(targetId, result.sessionId);
471
+ connectionState?.attachedTargets.set(result.sessionId, targetId);
472
+ connectionState?.activeFrameIdByTarget.set(targetId, connectionState?.activeFrameIdByTarget.get(targetId) ?? null);
473
+ await sessionCommand(targetId, "Page.enable");
474
+ await sessionCommand(targetId, "Runtime.enable");
475
+ await sessionCommand(targetId, "DOM.enable");
476
+ await sessionCommand(targetId, "Accessibility.enable");
477
+ return result.sessionId;
478
+ }
479
+ async function getTargets() {
480
+ const state = connectionState;
481
+ if (!state) throw new Error("CDP connection not initialized");
482
+ try {
483
+ const result = await browserCommand("Target.getTargets");
484
+ return (result.targetInfos || []).map((target) => ({
485
+ id: target.targetId,
486
+ type: target.type,
487
+ title: target.title,
488
+ url: target.url,
489
+ webSocketDebuggerUrl: ""
490
+ }));
491
+ } catch {
492
+ return getJsonList(state.host, state.port);
493
+ }
494
+ }
495
+ async function ensurePageTarget(targetId) {
496
+ const targets = (await getTargets()).filter((target2) => target2.type === "page");
497
+ if (targets.length === 0) throw new Error("No page target found");
498
+ let target;
499
+ if (typeof targetId === "number") {
500
+ target = targets[targetId] ?? targets.find((item) => Number(item.id) === targetId);
501
+ } else if (typeof targetId === "string") {
502
+ target = targets.find((item) => item.id === targetId);
503
+ }
504
+ target ??= targets[0];
505
+ connectionState.currentTargetId = target.id;
506
+ await attachTarget(target.id);
507
+ return target;
508
+ }
509
+ function parseRef(ref) {
510
+ const refs = connectionState?.refsByTarget.get(connectionState.currentTargetId ?? "") ?? {};
511
+ const found = refs[ref];
512
+ if (!found?.backendDOMNodeId) {
513
+ throw new Error(`Unknown ref: ${ref}. Run snapshot first.`);
514
+ }
515
+ return found.backendDOMNodeId;
516
+ }
517
+ function loadBuildDomTreeScript() {
518
+ const currentDir = path2.dirname(fileURLToPath(import.meta.url));
519
+ const candidates = [
520
+ path2.resolve(currentDir, "../../../extension/buildDomTree.js"),
521
+ path2.resolve(currentDir, "../../extension/buildDomTree.js"),
522
+ path2.resolve(currentDir, "../../../packages/extension/buildDomTree.js"),
523
+ path2.resolve(currentDir, "../../../extension/dist/buildDomTree.js")
524
+ ];
525
+ for (const candidate of candidates) {
526
+ try {
527
+ return readFileSync(candidate, "utf8");
528
+ } catch {
529
+ }
530
+ }
531
+ throw new Error("Cannot find buildDomTree.js");
532
+ }
533
+ async function evaluate(targetId, expression, returnByValue = true) {
534
+ const result = await sessionCommand(targetId, "Runtime.evaluate", {
535
+ expression,
536
+ awaitPromise: true,
537
+ returnByValue
538
+ });
539
+ if (result.exceptionDetails) {
540
+ throw new Error(result.exceptionDetails.text || "Runtime.evaluate failed");
541
+ }
542
+ return result.result.value ?? result.result;
543
+ }
544
+ async function resolveNode(targetId, backendNodeId) {
545
+ const result = await sessionCommand(targetId, "DOM.pushNodesByBackendIdsToFrontend", {
546
+ backendNodeIds: [backendNodeId]
547
+ });
548
+ return result.nodeId;
549
+ }
550
+ async function focusNode(targetId, backendNodeId) {
551
+ const nodeId = await resolveNode(targetId, backendNodeId);
552
+ await sessionCommand(targetId, "DOM.focus", { nodeId });
553
+ }
554
+ async function getNodeBox(targetId, backendNodeId) {
555
+ const result = await sessionCommand(targetId, "DOM.getBoxModel", {
556
+ backendNodeId
557
+ });
558
+ const quad = result.model.content.length >= 8 ? result.model.content : result.model.border;
559
+ const xs = [quad[0], quad[2], quad[4], quad[6]];
560
+ const ys = [quad[1], quad[3], quad[5], quad[7]];
561
+ return {
562
+ x: xs.reduce((a, b) => a + b, 0) / xs.length,
563
+ y: ys.reduce((a, b) => a + b, 0) / ys.length
564
+ };
565
+ }
566
+ async function mouseClick(targetId, x, y) {
567
+ await sessionCommand(targetId, "Input.dispatchMouseEvent", { type: "mouseMoved", x, y, button: "none" });
568
+ await sessionCommand(targetId, "Input.dispatchMouseEvent", { type: "mousePressed", x, y, button: "left", clickCount: 1 });
569
+ await sessionCommand(targetId, "Input.dispatchMouseEvent", { type: "mouseReleased", x, y, button: "left", clickCount: 1 });
570
+ }
571
+ async function getAttributeValue(targetId, backendNodeId, attribute) {
572
+ const nodeId = await resolveNode(targetId, backendNodeId);
573
+ if (attribute === "text") {
574
+ return evaluate(targetId, `(() => { const n = this; return n.innerText ?? n.textContent ?? ''; }).call(document.querySelector('[data-bb-node-id="${nodeId}"]'))`);
575
+ }
576
+ const result = await sessionCommand(targetId, "DOM.resolveNode", { backendNodeId });
577
+ const call = await sessionCommand(targetId, "Runtime.callFunctionOn", {
578
+ objectId: result.object.objectId,
579
+ functionDeclaration: `function() { if (${JSON.stringify(attribute)} === 'url') return this.href || this.src || location.href; if (${JSON.stringify(attribute)} === 'title') return document.title; return this.getAttribute(${JSON.stringify(attribute)}) || ''; }`,
580
+ returnByValue: true
581
+ });
582
+ return String(call.result.value ?? "");
583
+ }
584
+ async function buildSnapshot(targetId, request) {
585
+ const script = loadBuildDomTreeScript();
586
+ const expression = `(() => { ${script}; return (typeof buildDomTree === 'function' ? buildDomTree : globalThis.buildDomTree)(${JSON.stringify({
587
+ interactiveOnly: !!request.interactive,
588
+ compact: !!request.compact,
589
+ maxDepth: request.maxDepth,
590
+ selector: request.selector
591
+ })}); })()`;
592
+ const value = await evaluate(targetId, expression, true);
593
+ connectionState?.refsByTarget.set(targetId, value.refs || {});
594
+ return value;
595
+ }
596
+ function ok(id, data) {
597
+ return { id, success: true, data };
598
+ }
599
+ function fail(id, error) {
600
+ return { id, success: false, error: buildRequestError(error).message };
601
+ }
602
+ async function ensureCdpConnection() {
603
+ if (connectionState) return;
604
+ if (reconnecting) return reconnecting;
605
+ reconnecting = (async () => {
606
+ const discovered = await discoverCdpPort();
607
+ if (!discovered) {
608
+ throw new Error("No browser connection found");
609
+ }
610
+ const version = await getJsonVersion(discovered.host, discovered.port);
611
+ const wsUrl = version.webSocketDebuggerUrl;
612
+ const socket = await connectWebSocket(wsUrl);
613
+ connectionState = createState(discovered.host, discovered.port, wsUrl, socket);
614
+ })();
615
+ try {
616
+ await reconnecting;
617
+ } finally {
618
+ reconnecting = null;
619
+ }
620
+ }
621
+ async function sendCommand(request) {
622
+ try {
623
+ await ensureCdpConnection();
624
+ const timeout = new Promise((_, reject) => setTimeout(() => reject(new Error("\u8BF7\u6C42\u8D85\u65F6")), COMMAND_TIMEOUT));
625
+ return await Promise.race([dispatchRequest(request), timeout]);
626
+ } catch (error) {
627
+ return fail(request.id, error);
628
+ }
629
+ }
630
+ async function dispatchRequest(request) {
631
+ const target = await ensurePageTarget(request.tabId);
632
+ switch (request.action) {
633
+ case "open": {
634
+ if (!request.url) return fail(request.id, "Missing url parameter");
635
+ if (request.tabId === void 0) {
636
+ const created = await browserCommand("Target.createTarget", { url: request.url });
637
+ const newTarget = await ensurePageTarget(created.targetId);
638
+ return ok(request.id, { url: request.url, tabId: Number(newTarget.id) || void 0 });
639
+ }
640
+ 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 });
642
+ }
643
+ case "snapshot": {
644
+ const snapshotData = await buildSnapshot(target.id, request);
645
+ return ok(request.id, { title: target.title, url: target.url, snapshotData });
646
+ }
647
+ case "click":
648
+ case "hover": {
649
+ if (!request.ref) return fail(request.id, "Missing ref parameter");
650
+ const backendNodeId = parseRef(request.ref);
651
+ const point = await getNodeBox(target.id, backendNodeId);
652
+ await sessionCommand(target.id, "Input.dispatchMouseEvent", { type: "mouseMoved", x: point.x, y: point.y, button: "none" });
653
+ if (request.action === "click") await mouseClick(target.id, point.x, point.y);
654
+ return ok(request.id, {});
655
+ }
656
+ case "fill":
657
+ case "type": {
658
+ if (!request.ref) return fail(request.id, "Missing ref parameter");
659
+ 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 });
666
+ return ok(request.id, { value: request.text });
667
+ }
668
+ case "check":
669
+ case "uncheck": {
670
+ if (!request.ref) return fail(request.id, "Missing ref parameter");
671
+ const backendNodeId = parseRef(request.ref);
672
+ const desired = request.action === "check";
673
+ const resolved = await sessionCommand(target.id, "DOM.resolveNode", { backendNodeId });
674
+ await sessionCommand(target.id, "Runtime.callFunctionOn", {
675
+ objectId: resolved.object.objectId,
676
+ functionDeclaration: `function() { this.checked = ${desired}; this.dispatchEvent(new Event('input', { bubbles: true })); this.dispatchEvent(new Event('change', { bubbles: true })); }`
677
+ });
678
+ return ok(request.id, {});
679
+ }
680
+ case "select": {
681
+ if (!request.ref || request.value == null) return fail(request.id, "Missing ref or value parameter");
682
+ const backendNodeId = parseRef(request.ref);
683
+ const resolved = await sessionCommand(target.id, "DOM.resolveNode", { backendNodeId });
684
+ await sessionCommand(target.id, "Runtime.callFunctionOn", {
685
+ objectId: resolved.object.objectId,
686
+ functionDeclaration: `function() { this.value = ${JSON.stringify(request.value)}; this.dispatchEvent(new Event('input', { bubbles: true })); this.dispatchEvent(new Event('change', { bubbles: true })); }`
687
+ });
688
+ return ok(request.id, { value: request.value });
689
+ }
690
+ case "get": {
691
+ 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);
693
+ return ok(request.id, { value });
694
+ }
695
+ case "screenshot": {
696
+ const result = await sessionCommand(target.id, "Page.captureScreenshot", { format: "png", fromSurface: true });
697
+ return ok(request.id, { dataUrl: `data:image/png;base64,${result.data}` });
698
+ }
699
+ case "close": {
700
+ await browserCommand("Target.closeTarget", { targetId: target.id });
701
+ return ok(request.id, {});
702
+ }
703
+ case "wait": {
704
+ await new Promise((resolve2) => setTimeout(resolve2, request.ms ?? 1e3));
705
+ return ok(request.id, {});
706
+ }
707
+ case "press": {
708
+ if (!request.key) return fail(request.id, "Missing key parameter");
709
+ await sessionCommand(target.id, "Input.dispatchKeyEvent", { type: "keyDown", key: request.key });
710
+ if (request.key.length === 1) {
711
+ await sessionCommand(target.id, "Input.dispatchKeyEvent", { type: "char", text: request.key, key: request.key });
712
+ }
713
+ await sessionCommand(target.id, "Input.dispatchKeyEvent", { type: "keyUp", key: request.key });
714
+ return ok(request.id, {});
715
+ }
716
+ case "scroll": {
717
+ const deltaY = request.direction === "up" ? -(request.pixels ?? 300) : request.pixels ?? 300;
718
+ await sessionCommand(target.id, "Input.dispatchMouseEvent", { type: "mouseWheel", x: 0, y: 0, deltaX: 0, deltaY });
719
+ return ok(request.id, {});
720
+ }
721
+ case "back": {
722
+ await evaluate(target.id, "history.back(); undefined");
723
+ return ok(request.id, {});
724
+ }
725
+ case "forward": {
726
+ await evaluate(target.id, "history.forward(); undefined");
727
+ return ok(request.id, {});
728
+ }
729
+ case "refresh": {
730
+ await sessionCommand(target.id, "Page.reload", { ignoreCache: false });
731
+ return ok(request.id, {});
732
+ }
733
+ case "eval": {
734
+ if (!request.script) return fail(request.id, "Missing script parameter");
735
+ const result = await evaluate(target.id, request.script, true);
736
+ return ok(request.id, { result });
737
+ }
738
+ 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 }));
740
+ return ok(request.id, { tabs, activeIndex: tabs.findIndex((tab) => tab.active) });
741
+ }
742
+ case "tab_new": {
743
+ 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" });
745
+ }
746
+ case "tab_select": {
747
+ const tabs = (await getTargets()).filter((item) => item.type === "page");
748
+ const selected = request.tabId !== void 0 ? tabs.find((item) => item.id === String(request.tabId) || Number(item.id) === request.tabId) : tabs[request.index ?? 0];
749
+ if (!selected) return fail(request.id, "Tab not found");
750
+ connectionState.currentTargetId = selected.id;
751
+ await attachTarget(selected.id);
752
+ return ok(request.id, { tabId: Number(selected.id) || void 0, url: selected.url, title: selected.title });
753
+ }
754
+ case "tab_close": {
755
+ const tabs = (await getTargets()).filter((item) => item.type === "page");
756
+ const selected = request.tabId !== void 0 ? tabs.find((item) => item.id === String(request.tabId) || Number(item.id) === request.tabId) : tabs[request.index ?? 0];
757
+ if (!selected) return fail(request.id, "Tab not found");
758
+ await browserCommand("Target.closeTarget", { targetId: selected.id });
759
+ return ok(request.id, { tabId: Number(selected.id) || void 0 });
760
+ }
761
+ case "frame": {
762
+ if (!request.selector) return fail(request.id, "Missing selector parameter");
763
+ const document = await pageCommand(target.id, "DOM.getDocument", {});
764
+ const node = await pageCommand(target.id, "DOM.querySelector", { nodeId: document.root.nodeId, selector: request.selector });
765
+ if (!node.nodeId) return fail(request.id, `\u627E\u4E0D\u5230 iframe: ${request.selector}`);
766
+ const described = await pageCommand(target.id, "DOM.describeNode", { nodeId: node.nodeId });
767
+ const frameId = described.node.frameId;
768
+ const nodeName = String(described.node.nodeName ?? "").toLowerCase();
769
+ if (!frameId) return fail(request.id, `\u65E0\u6CD5\u83B7\u53D6 iframe frameId: ${request.selector}`);
770
+ if (nodeName && nodeName !== "iframe" && nodeName !== "frame") return fail(request.id, `\u5143\u7D20\u4E0D\u662F iframe: ${nodeName}`);
771
+ connectionState?.activeFrameIdByTarget.set(target.id, frameId);
772
+ const attributes = described.node.attributes ?? [];
773
+ const attrMap = {};
774
+ for (let i = 0; i < attributes.length; i += 2) attrMap[String(attributes[i])] = String(attributes[i + 1] ?? "");
775
+ return ok(request.id, { frameInfo: { selector: request.selector, name: attrMap.name ?? "", url: attrMap.src ?? "", frameId } });
776
+ }
777
+ case "frame_main": {
778
+ connectionState?.activeFrameIdByTarget.set(target.id, null);
779
+ return ok(request.id, { frameInfo: { frameId: 0 } });
780
+ }
781
+ case "dialog": {
782
+ connectionState?.dialogHandlers.set(target.id, { accept: request.dialogResponse !== "dismiss", ...request.promptText !== void 0 ? { promptText: request.promptText } : {} });
783
+ await sessionCommand(target.id, "Page.enable");
784
+ return ok(request.id, { dialog: { armed: true, response: request.dialogResponse ?? "accept" } });
785
+ }
786
+ case "network": {
787
+ const subCommand = request.networkCommand ?? "requests";
788
+ switch (subCommand) {
789
+ case "requests": {
790
+ await ensureNetworkMonitoring(target.id);
791
+ const requests = Array.from(networkRequests.values()).filter((item) => !request.filter || item.url.includes(request.filter));
792
+ if (request.withBody) {
793
+ await Promise.all(requests.map(async (item) => {
794
+ if (item.failed || item.responseBody !== void 0 || item.bodyError !== void 0) return;
795
+ try {
796
+ const body = await sessionCommand(target.id, "Network.getResponseBody", { requestId: item.requestId });
797
+ item.responseBody = body.body;
798
+ item.responseBodyBase64 = body.base64Encoded;
799
+ } catch (error) {
800
+ item.bodyError = error instanceof Error ? error.message : String(error);
801
+ }
802
+ }));
803
+ }
804
+ return ok(request.id, { networkRequests: requests });
805
+ }
806
+ case "route":
807
+ return ok(request.id, { routeCount: 0 });
808
+ case "unroute":
809
+ return ok(request.id, { routeCount: 0 });
810
+ case "clear":
811
+ networkRequests.clear();
812
+ return ok(request.id, {});
813
+ default:
814
+ return fail(request.id, `Unknown network subcommand: ${subCommand}`);
815
+ }
816
+ }
817
+ case "console": {
818
+ const subCommand = request.consoleCommand ?? "get";
819
+ await ensureConsoleMonitoring(target.id);
820
+ switch (subCommand) {
821
+ case "get":
822
+ return ok(request.id, { consoleMessages: consoleMessages.filter((item) => !request.filter || item.text.includes(request.filter)) });
823
+ case "clear":
824
+ consoleMessages.length = 0;
825
+ return ok(request.id, {});
826
+ default:
827
+ return fail(request.id, `Unknown console subcommand: ${subCommand}`);
828
+ }
829
+ }
830
+ case "errors": {
831
+ const subCommand = request.errorsCommand ?? "get";
832
+ await ensureConsoleMonitoring(target.id);
833
+ switch (subCommand) {
834
+ case "get":
835
+ return ok(request.id, { jsErrors: jsErrors.filter((item) => !request.filter || item.message.includes(request.filter) || item.url?.includes(request.filter)) });
836
+ case "clear":
837
+ jsErrors.length = 0;
838
+ return ok(request.id, {});
839
+ default:
840
+ return fail(request.id, `Unknown errors subcommand: ${subCommand}`);
841
+ }
842
+ }
843
+ case "trace": {
844
+ const subCommand = request.traceCommand ?? "status";
845
+ switch (subCommand) {
846
+ case "start":
847
+ traceRecording = true;
848
+ traceEvents.length = 0;
849
+ return ok(request.id, { traceStatus: { recording: true, eventCount: 0 } });
850
+ case "stop": {
851
+ traceRecording = false;
852
+ return ok(request.id, { traceEvents: [...traceEvents], traceStatus: { recording: false, eventCount: traceEvents.length } });
853
+ }
854
+ case "status":
855
+ return ok(request.id, { traceStatus: { recording: traceRecording, eventCount: traceEvents.length } });
856
+ default:
857
+ return fail(request.id, `Unknown trace subcommand: ${subCommand}`);
858
+ }
859
+ }
860
+ default:
861
+ return fail(request.id, `Action not yet supported in direct CDP mode: ${request.action}`);
862
+ }
863
+ }
14
864
 
15
865
  // packages/cli/src/client.ts
16
866
  var jqExpression;
@@ -30,184 +880,190 @@ function handleJqResponse(response) {
30
880
  printJqResults(response);
31
881
  }
32
882
  }
33
- async function sendCommand(request) {
34
- const controller = new AbortController();
35
- const timeoutId = setTimeout(() => controller.abort(), COMMAND_TIMEOUT);
36
- try {
37
- const res = await fetch(`${DAEMON_BASE_URL}/command`, {
38
- method: "POST",
39
- headers: {
40
- "Content-Type": "application/json"
41
- },
42
- body: JSON.stringify(request),
43
- signal: controller.signal
44
- });
45
- clearTimeout(timeoutId);
46
- if (!res.ok) {
47
- if (res.status === 408) {
48
- return {
49
- id: request.id,
50
- success: false,
51
- error: "\u547D\u4EE4\u6267\u884C\u8D85\u65F6"
52
- };
53
- }
54
- if (res.status === 503) {
55
- return {
56
- id: request.id,
57
- success: false,
58
- error: [
59
- "Chrome extension not connected.",
60
- "",
61
- "1. Download extension: https://github.com/epiral/bb-browser/releases/latest",
62
- "2. Unzip the downloaded file",
63
- "3. Open chrome://extensions/ \u2192 Enable Developer Mode",
64
- '4. Click "Load unpacked" \u2192 select the unzipped folder'
65
- ].join("\n")
66
- };
67
- }
68
- return {
69
- id: request.id,
70
- success: false,
71
- error: `HTTP \u9519\u8BEF: ${res.status} ${res.statusText}`
72
- };
73
- }
74
- const response = await res.json();
75
- return response;
76
- } catch (error) {
77
- clearTimeout(timeoutId);
78
- if (error instanceof Error) {
79
- if (error.name === "AbortError") {
80
- return {
81
- id: request.id,
82
- success: false,
83
- error: "\u8BF7\u6C42\u8D85\u65F6"
84
- };
85
- }
86
- if (error.message.includes("fetch failed") || error.message.includes("ECONNREFUSED")) {
87
- throw new Error([
88
- "Cannot connect to daemon.",
89
- "",
90
- "Start the daemon first:",
91
- " bb-browser daemon",
92
- "",
93
- "Then load the Chrome extension:",
94
- " chrome://extensions/ \u2192 Developer Mode \u2192 Load unpacked \u2192 node_modules/bb-browser/extension/"
95
- ].join("\n"));
96
- }
97
- throw error;
98
- }
99
- throw error;
100
- }
883
+ async function sendCommand2(request) {
884
+ return sendCommand(request);
101
885
  }
102
886
 
103
887
  // packages/cli/src/daemon-manager.ts
104
- import { spawn } from "child_process";
105
- import { existsSync } from "fs";
106
- import { fileURLToPath } from "url";
888
+ import { fileURLToPath as fileURLToPath2 } from "url";
107
889
  import { dirname, resolve } from "path";
108
- function getDaemonPath() {
109
- const currentFile = fileURLToPath(import.meta.url);
110
- const currentDir = dirname(currentFile);
111
- const sameDirPath = resolve(currentDir, "daemon.js");
112
- if (existsSync(sameDirPath)) {
113
- return sameDirPath;
114
- }
115
- return resolve(currentDir, "../../daemon/dist/index.js");
116
- }
117
- var DAEMON_START_TIMEOUT = 5e3;
118
- var POLL_INTERVAL = 200;
890
+ import { existsSync as existsSync2 } from "fs";
119
891
  async function isDaemonRunning() {
892
+ return await discoverCdpPort() !== null;
893
+ }
894
+ async function ensureDaemonRunning() {
120
895
  try {
121
- const controller = new AbortController();
122
- const timeoutId = setTimeout(() => controller.abort(), 2e3);
123
- const response = await fetch(`${DAEMON_BASE_URL}/status`, {
124
- signal: controller.signal
125
- });
126
- clearTimeout(timeoutId);
127
- return response.ok;
128
- } catch {
129
- return false;
896
+ await ensureCdpConnection();
897
+ } catch (error) {
898
+ if (error instanceof Error && error.message.includes("No browser connection found")) {
899
+ throw new Error([
900
+ "bb-browser: Could not start browser.",
901
+ "",
902
+ "Make sure Chrome is installed, then try again.",
903
+ "Or specify a CDP port manually: bb-browser --port 9222"
904
+ ].join("\n"));
905
+ }
906
+ throw error;
130
907
  }
131
908
  }
132
- async function isExtensionConnected() {
133
- try {
134
- const controller = new AbortController();
135
- const timeoutId = setTimeout(() => controller.abort(), 2e3);
136
- const response = await fetch(`${DAEMON_BASE_URL}/status`, {
137
- signal: controller.signal
138
- });
139
- clearTimeout(timeoutId);
140
- if (!response.ok) return false;
141
- const data = await response.json();
142
- return !!data.extensionConnected;
143
- } catch {
144
- return false;
909
+
910
+ // packages/cli/src/history-sqlite.ts
911
+ import { copyFileSync, existsSync as existsSync3, unlinkSync } from "fs";
912
+ import { execSync as execSync2 } from "child_process";
913
+ import { homedir, tmpdir } from "os";
914
+ import { join } from "path";
915
+ function getHistoryPathCandidates() {
916
+ const home = homedir();
917
+ const localAppData = process.env.LOCALAPPDATA || "";
918
+ const candidates = [
919
+ join(home, "Library/Application Support/Google/Chrome/Default/History"),
920
+ join(home, "Library/Application Support/Microsoft Edge/Default/History"),
921
+ join(home, "Library/Application Support/BraveSoftware/Brave-Browser/Default/History"),
922
+ join(home, "Library/Application Support/Arc/User Data/Default/History"),
923
+ join(home, ".config/google-chrome/Default/History")
924
+ ];
925
+ if (localAppData) {
926
+ candidates.push(
927
+ join(localAppData, "Google/Chrome/User Data/Default/History"),
928
+ join(localAppData, "Microsoft/Edge/User Data/Default/History")
929
+ );
145
930
  }
931
+ return candidates;
146
932
  }
147
- async function waitForDaemon(timeoutMs) {
148
- const startTime = Date.now();
149
- while (Date.now() - startTime < timeoutMs) {
150
- if (await isDaemonRunning()) {
151
- return true;
933
+ function findHistoryPath() {
934
+ for (const historyPath of getHistoryPathCandidates()) {
935
+ if (existsSync3(historyPath)) {
936
+ return historyPath;
152
937
  }
153
- await new Promise((resolve2) => setTimeout(resolve2, POLL_INTERVAL));
154
938
  }
155
- return false;
939
+ return null;
156
940
  }
157
- function spawnDaemon() {
158
- const daemonPath = getDaemonPath();
159
- const daemonProcess = spawn(process.execPath, [daemonPath], {
160
- detached: true,
161
- stdio: "ignore",
162
- env: { ...process.env }
163
- });
164
- daemonProcess.unref();
941
+ function sqlEscape(value) {
942
+ return value.replace(/'/g, "''");
165
943
  }
166
- async function ensureDaemonRunning() {
167
- if (await isDaemonRunning()) {
168
- return;
169
- }
170
- spawnDaemon();
171
- const ready = await waitForDaemon(DAEMON_START_TIMEOUT);
172
- if (!ready) {
173
- throw new Error(
174
- "\u65E0\u6CD5\u542F\u52A8 Daemon\u3002\u8BF7\u624B\u52A8\u8FD0\u884C bb-browser daemon \u6216 bb-daemon \u542F\u52A8\u670D\u52A1"
175
- );
176
- }
177
- const extStart = Date.now();
178
- while (Date.now() - extStart < 1e4) {
179
- if (await isExtensionConnected()) return;
180
- await new Promise((r) => setTimeout(r, POLL_INTERVAL));
944
+ function buildTimeWhere(days) {
945
+ if (!days || days <= 0) {
946
+ return "";
181
947
  }
948
+ return `last_visit_time > (strftime('%s', 'now') - ${Math.floor(days)}*86400) * 1000000 + 11644473600000000`;
182
949
  }
183
- async function stopDaemon() {
950
+ function runHistoryQuery(sql, mapRow) {
951
+ const historyPath = findHistoryPath();
952
+ if (!historyPath) {
953
+ return [];
954
+ }
955
+ const tmpPath = join(tmpdir(), `bb-history-${Date.now()}-${Math.random().toString(36).slice(2)}.db`);
184
956
  try {
185
- const controller = new AbortController();
186
- const timeoutId = setTimeout(() => controller.abort(), 5e3);
187
- const response = await fetch(`${DAEMON_BASE_URL}/shutdown`, {
188
- method: "POST",
189
- signal: controller.signal
957
+ copyFileSync(historyPath, tmpPath);
958
+ const escapedTmpPath = tmpPath.replace(/"/g, '\\"');
959
+ const escapedSql = sql.replace(/"/g, '\\"');
960
+ const output = execSync2(`sqlite3 -separator $'\\t' "${escapedTmpPath}" "${escapedSql}"`, {
961
+ encoding: "utf-8",
962
+ stdio: ["pipe", "pipe", "pipe"]
190
963
  });
191
- clearTimeout(timeoutId);
192
- return response.ok;
964
+ return output.split("\n").filter(Boolean).map((line) => mapRow(line.split(" "))).filter((item) => item !== null);
193
965
  } catch {
194
- return false;
966
+ return [];
967
+ } finally {
968
+ try {
969
+ unlinkSync(tmpPath);
970
+ } catch {
971
+ }
195
972
  }
196
973
  }
974
+ function searchHistory(query, days) {
975
+ const conditions = [];
976
+ const trimmedQuery = query?.trim();
977
+ if (trimmedQuery) {
978
+ const escapedQuery = sqlEscape(trimmedQuery);
979
+ conditions.push(`(url LIKE '%${escapedQuery}%' OR title LIKE '%${escapedQuery}%')`);
980
+ }
981
+ const timeWhere = buildTimeWhere(days);
982
+ if (timeWhere) {
983
+ conditions.push(timeWhere);
984
+ }
985
+ const whereClause = conditions.length > 0 ? `WHERE ${conditions.join(" AND ")}` : "";
986
+ const sql = `
987
+ SELECT
988
+ url,
989
+ REPLACE(IFNULL(title, ''), char(9), ' '),
990
+ IFNULL(visit_count, 0),
991
+ IFNULL(last_visit_time, 0)
992
+ FROM urls
993
+ ${whereClause}
994
+ ORDER BY last_visit_time DESC
995
+ LIMIT 100;
996
+ `.trim();
997
+ return runHistoryQuery(sql, (row) => {
998
+ if (row.length < 4) {
999
+ return null;
1000
+ }
1001
+ const chromeTimestamp = Number(row[3]) || 0;
1002
+ return {
1003
+ url: row[0] || "",
1004
+ title: row[1] || "",
1005
+ visitCount: Number(row[2]) || 0,
1006
+ lastVisitTime: chromeTimestamp > 0 ? chromeTimestamp / 1e6 - 11644473600 : 0
1007
+ };
1008
+ });
1009
+ }
1010
+ function getHistoryDomains(days) {
1011
+ const timeWhere = buildTimeWhere(days);
1012
+ const whereClause = timeWhere ? `WHERE ${timeWhere}` : "";
1013
+ const sql = `
1014
+ SELECT
1015
+ domain,
1016
+ SUM(visit_count) AS visits,
1017
+ GROUP_CONCAT(title, char(31)) AS titles
1018
+ FROM (
1019
+ SELECT
1020
+ CASE
1021
+ WHEN instr(url, '//') > 0 AND instr(substr(url, instr(url, '//') + 2), '/') > 0
1022
+ THEN substr(
1023
+ substr(url, instr(url, '//') + 2),
1024
+ 1,
1025
+ instr(substr(url, instr(url, '//') + 2), '/') - 1
1026
+ )
1027
+ WHEN instr(url, '//') > 0 THEN substr(url, instr(url, '//') + 2)
1028
+ WHEN instr(url, '/') > 0 THEN substr(url, 1, instr(url, '/') - 1)
1029
+ ELSE url
1030
+ END AS domain,
1031
+ IFNULL(visit_count, 0) AS visit_count,
1032
+ REPLACE(IFNULL(title, ''), char(31), ' ') AS title
1033
+ FROM urls
1034
+ ${whereClause}
1035
+ )
1036
+ WHERE domain != ''
1037
+ GROUP BY domain
1038
+ ORDER BY visits DESC
1039
+ LIMIT 50;
1040
+ `.trim();
1041
+ return runHistoryQuery(sql, (row) => {
1042
+ if (row.length < 3) {
1043
+ return null;
1044
+ }
1045
+ const titles = row[2] ? Array.from(new Set(row[2].split(String.fromCharCode(31)).map((title) => title.trim()).filter(Boolean))).slice(0, 10) : [];
1046
+ return {
1047
+ domain: row[0] || "",
1048
+ visits: Number(row[1]) || 0,
1049
+ titles
1050
+ };
1051
+ });
1052
+ }
197
1053
 
198
1054
  // packages/cli/src/commands/site.ts
199
- import { readFileSync, readdirSync, existsSync as existsSync2, mkdirSync } from "fs";
200
- import { join, relative } from "path";
201
- import { homedir } from "os";
202
- import { execSync } from "child_process";
203
- var BB_DIR = join(homedir(), ".bb-browser");
204
- var LOCAL_SITES_DIR = join(BB_DIR, "sites");
205
- var COMMUNITY_SITES_DIR = join(BB_DIR, "bb-sites");
1055
+ import { readFileSync as readFileSync2, readdirSync, existsSync as existsSync4, mkdirSync } from "fs";
1056
+ import { join as join2, relative } from "path";
1057
+ import { homedir as homedir2 } from "os";
1058
+ import { execSync as execSync3 } from "child_process";
1059
+ var BB_DIR = join2(homedir2(), ".bb-browser");
1060
+ var LOCAL_SITES_DIR = join2(BB_DIR, "sites");
1061
+ var COMMUNITY_SITES_DIR = join2(BB_DIR, "bb-sites");
206
1062
  var COMMUNITY_REPO = "https://github.com/epiral/bb-sites.git";
207
1063
  function checkCliUpdate() {
208
1064
  try {
209
- const current = execSync("bb-browser --version", { timeout: 3e3, stdio: ["pipe", "pipe", "pipe"] }).toString().trim();
210
- const latest = execSync("npm view bb-browser version", { timeout: 5e3, stdio: ["pipe", "pipe", "pipe"] }).toString().trim();
1065
+ const current = execSync3("bb-browser --version", { timeout: 3e3, stdio: ["pipe", "pipe", "pipe"] }).toString().trim();
1066
+ const latest = execSync3("npm view bb-browser version", { timeout: 5e3, stdio: ["pipe", "pipe", "pipe"] }).toString().trim();
211
1067
  if (latest && current && latest !== current && latest.localeCompare(current, void 0, { numeric: true }) > 0) {
212
1068
  console.log(`
213
1069
  \u{1F4E6} bb-browser ${latest} available (current: ${current}). Run: npm install -g bb-browser`);
@@ -218,7 +1074,7 @@ function checkCliUpdate() {
218
1074
  function parseSiteMeta(filePath, source) {
219
1075
  let content;
220
1076
  try {
221
- content = readFileSync(filePath, "utf-8");
1077
+ content = readFileSync2(filePath, "utf-8");
222
1078
  } catch {
223
1079
  return null;
224
1080
  }
@@ -278,7 +1134,7 @@ function parseSiteMeta(filePath, source) {
278
1134
  return meta;
279
1135
  }
280
1136
  function scanSites(dir, source) {
281
- if (!existsSync2(dir)) return [];
1137
+ if (!existsSync4(dir)) return [];
282
1138
  const sites = [];
283
1139
  function walk(currentDir) {
284
1140
  let entries;
@@ -288,7 +1144,7 @@ function scanSites(dir, source) {
288
1144
  return;
289
1145
  }
290
1146
  for (const entry of entries) {
291
- const fullPath = join(currentDir, entry.name);
1147
+ const fullPath = join2(currentDir, entry.name);
292
1148
  if (entry.isDirectory() && !entry.name.startsWith(".")) {
293
1149
  walk(fullPath);
294
1150
  } else if (entry.isFile() && entry.name.endsWith(".js")) {
@@ -392,10 +1248,10 @@ function siteSearch(query, options) {
392
1248
  }
393
1249
  function siteUpdate() {
394
1250
  mkdirSync(BB_DIR, { recursive: true });
395
- if (existsSync2(join(COMMUNITY_SITES_DIR, ".git"))) {
1251
+ if (existsSync4(join2(COMMUNITY_SITES_DIR, ".git"))) {
396
1252
  console.log("\u66F4\u65B0\u793E\u533A site adapter \u5E93...");
397
1253
  try {
398
- execSync("git pull --ff-only", { cwd: COMMUNITY_SITES_DIR, stdio: "pipe" });
1254
+ execSync3("git pull --ff-only", { cwd: COMMUNITY_SITES_DIR, stdio: "pipe" });
399
1255
  console.log("\u66F4\u65B0\u5B8C\u6210\u3002");
400
1256
  console.log("");
401
1257
  console.log("\u{1F4A1} \u8FD0\u884C bb-browser site recommend \u770B\u770B\u54EA\u4E9B\u548C\u4F60\u7684\u6D4F\u89C8\u4E60\u60EF\u5339\u914D");
@@ -407,7 +1263,7 @@ function siteUpdate() {
407
1263
  } else {
408
1264
  console.log(`\u514B\u9686\u793E\u533A adapter \u5E93: ${COMMUNITY_REPO}`);
409
1265
  try {
410
- execSync(`git clone ${COMMUNITY_REPO} ${COMMUNITY_SITES_DIR}`, { stdio: "pipe" });
1266
+ execSync3(`git clone ${COMMUNITY_REPO} ${COMMUNITY_SITES_DIR}`, { stdio: "pipe" });
411
1267
  console.log("\u514B\u9686\u5B8C\u6210\u3002");
412
1268
  console.log("");
413
1269
  console.log("\u{1F4A1} \u8FD0\u884C bb-browser site recommend \u770B\u770B\u54EA\u4E9B\u548C\u4F60\u7684\u6D4F\u89C8\u4E60\u60EF\u5339\u914D");
@@ -466,16 +1322,7 @@ function siteInfo(name, options) {
466
1322
  }
467
1323
  async function siteRecommend(options) {
468
1324
  const days = options.days ?? 30;
469
- const response = await sendCommand({
470
- id: generateId(),
471
- action: "history",
472
- historyCommand: "domains",
473
- ms: days
474
- });
475
- if (!response.success) {
476
- throw new Error(response.error || "History command failed");
477
- }
478
- const historyDomains = response.data?.historyDomains || [];
1325
+ const historyDomains = getHistoryDomains(days);
479
1326
  const sites = getAllSites();
480
1327
  const sitesByDomain = /* @__PURE__ */ new Map();
481
1328
  for (const site of sites) {
@@ -593,7 +1440,7 @@ async function siteRun(name, args, options) {
593
1440
  process.exit(1);
594
1441
  }
595
1442
  }
596
- const jsContent = readFileSync(site.filePath, "utf-8");
1443
+ const jsContent = readFileSync2(site.filePath, "utf-8");
597
1444
  const jsBody = jsContent.replace(/\/\*\s*@meta[\s\S]*?\*\//, "").trim();
598
1445
  const argsJson = JSON.stringify(argMap);
599
1446
  const script = `(${jsBody})(${argsJson})`;
@@ -653,7 +1500,7 @@ async function siteRun(name, args, options) {
653
1500
  let targetTabId = options.tabId;
654
1501
  if (!targetTabId && site.domain) {
655
1502
  const listReq = { id: generateId(), action: "tab_list" };
656
- const listResp = await sendCommand(listReq);
1503
+ const listResp = await sendCommand2(listReq);
657
1504
  if (listResp.success && listResp.data?.tabs) {
658
1505
  const matchingTab = listResp.data.tabs.find(
659
1506
  (tab) => matchTabOrigin(tab.url, site.domain)
@@ -663,7 +1510,7 @@ async function siteRun(name, args, options) {
663
1510
  }
664
1511
  }
665
1512
  if (!targetTabId) {
666
- const newResp = await sendCommand({
1513
+ const newResp = await sendCommand2({
667
1514
  id: generateId(),
668
1515
  action: "tab_new",
669
1516
  url: `https://${site.domain}`
@@ -673,7 +1520,7 @@ async function siteRun(name, args, options) {
673
1520
  }
674
1521
  }
675
1522
  const evalReq = { id: generateId(), action: "eval", script, tabId: targetTabId };
676
- const evalResp = await sendCommand(evalReq);
1523
+ const evalResp = await sendCommand2(evalReq);
677
1524
  if (!evalResp.success) {
678
1525
  const hint = site.domain ? `Open https://${site.domain} in your browser, make sure you are logged in, then retry.` : void 0;
679
1526
  if (options.json) {
@@ -808,10 +1655,10 @@ async function siteCommand(args, options = {}) {
808
1655
  silentUpdate();
809
1656
  }
810
1657
  function silentUpdate() {
811
- const gitDir = join(COMMUNITY_SITES_DIR, ".git");
812
- if (!existsSync2(gitDir)) return;
813
- import("child_process").then(({ spawn: spawn3 }) => {
814
- const child = spawn3("git", ["pull", "--ff-only"], {
1658
+ const gitDir = join2(COMMUNITY_SITES_DIR, ".git");
1659
+ if (!existsSync4(gitDir)) return;
1660
+ import("child_process").then(({ spawn: spawn2 }) => {
1661
+ const child = spawn2("git", ["pull", "--ff-only"], {
815
1662
  cwd: COMMUNITY_SITES_DIR,
816
1663
  stdio: "ignore",
817
1664
  detached: true
@@ -847,7 +1694,7 @@ async function openCommand(url, options = {}) {
847
1694
  request.tabId = tabId;
848
1695
  }
849
1696
  }
850
- const response = await sendCommand(request);
1697
+ const response = await sendCommand2(request);
851
1698
  if (options.json) {
852
1699
  console.log(JSON.stringify(response, null, 2));
853
1700
  } else {
@@ -883,7 +1730,7 @@ async function snapshotCommand(options = {}) {
883
1730
  selector: options.selector,
884
1731
  tabId: options.tabId
885
1732
  };
886
- const response = await sendCommand(request);
1733
+ const response = await sendCommand2(request);
887
1734
  if (options.json) {
888
1735
  console.log(JSON.stringify(response, null, 2));
889
1736
  } else {
@@ -902,7 +1749,7 @@ async function snapshotCommand(options = {}) {
902
1749
  }
903
1750
 
904
1751
  // packages/cli/src/commands/click.ts
905
- function parseRef(ref) {
1752
+ function parseRef2(ref) {
906
1753
  return ref.startsWith("@") ? ref.slice(1) : ref;
907
1754
  }
908
1755
  async function clickCommand(ref, options = {}) {
@@ -910,14 +1757,14 @@ async function clickCommand(ref, options = {}) {
910
1757
  throw new Error("\u7F3A\u5C11 ref \u53C2\u6570");
911
1758
  }
912
1759
  await ensureDaemonRunning();
913
- const parsedRef = parseRef(ref);
1760
+ const parsedRef = parseRef2(ref);
914
1761
  const request = {
915
1762
  id: generateId(),
916
1763
  action: "click",
917
1764
  ref: parsedRef,
918
1765
  tabId: options.tabId
919
1766
  };
920
- const response = await sendCommand(request);
1767
+ const response = await sendCommand2(request);
921
1768
  if (options.json) {
922
1769
  console.log(JSON.stringify(response, null, 2));
923
1770
  } else {
@@ -937,7 +1784,7 @@ async function clickCommand(ref, options = {}) {
937
1784
  }
938
1785
 
939
1786
  // packages/cli/src/commands/hover.ts
940
- function parseRef2(ref) {
1787
+ function parseRef3(ref) {
941
1788
  return ref.startsWith("@") ? ref.slice(1) : ref;
942
1789
  }
943
1790
  async function hoverCommand(ref, options = {}) {
@@ -945,14 +1792,14 @@ async function hoverCommand(ref, options = {}) {
945
1792
  throw new Error("\u7F3A\u5C11 ref \u53C2\u6570");
946
1793
  }
947
1794
  await ensureDaemonRunning();
948
- const parsedRef = parseRef2(ref);
1795
+ const parsedRef = parseRef3(ref);
949
1796
  const request = {
950
1797
  id: generateId(),
951
1798
  action: "hover",
952
1799
  ref: parsedRef,
953
1800
  tabId: options.tabId
954
1801
  };
955
- const response = await sendCommand(request);
1802
+ const response = await sendCommand2(request);
956
1803
  if (options.json) {
957
1804
  console.log(JSON.stringify(response, null, 2));
958
1805
  } else {
@@ -972,7 +1819,7 @@ async function hoverCommand(ref, options = {}) {
972
1819
  }
973
1820
 
974
1821
  // packages/cli/src/commands/fill.ts
975
- function parseRef3(ref) {
1822
+ function parseRef4(ref) {
976
1823
  return ref.startsWith("@") ? ref.slice(1) : ref;
977
1824
  }
978
1825
  async function fillCommand(ref, text, options = {}) {
@@ -983,7 +1830,7 @@ async function fillCommand(ref, text, options = {}) {
983
1830
  throw new Error("\u7F3A\u5C11 text \u53C2\u6570");
984
1831
  }
985
1832
  await ensureDaemonRunning();
986
- const parsedRef = parseRef3(ref);
1833
+ const parsedRef = parseRef4(ref);
987
1834
  const request = {
988
1835
  id: generateId(),
989
1836
  action: "fill",
@@ -991,7 +1838,7 @@ async function fillCommand(ref, text, options = {}) {
991
1838
  text,
992
1839
  tabId: options.tabId
993
1840
  };
994
- const response = await sendCommand(request);
1841
+ const response = await sendCommand2(request);
995
1842
  if (options.json) {
996
1843
  console.log(JSON.stringify(response, null, 2));
997
1844
  } else {
@@ -1012,7 +1859,7 @@ async function fillCommand(ref, text, options = {}) {
1012
1859
  }
1013
1860
 
1014
1861
  // packages/cli/src/commands/type.ts
1015
- function parseRef4(ref) {
1862
+ function parseRef5(ref) {
1016
1863
  return ref.startsWith("@") ? ref.slice(1) : ref;
1017
1864
  }
1018
1865
  async function typeCommand(ref, text, options = {}) {
@@ -1023,7 +1870,7 @@ async function typeCommand(ref, text, options = {}) {
1023
1870
  throw new Error("\u7F3A\u5C11 text \u53C2\u6570");
1024
1871
  }
1025
1872
  await ensureDaemonRunning();
1026
- const parsedRef = parseRef4(ref);
1873
+ const parsedRef = parseRef5(ref);
1027
1874
  const request = {
1028
1875
  id: generateId(),
1029
1876
  action: "type",
@@ -1031,7 +1878,7 @@ async function typeCommand(ref, text, options = {}) {
1031
1878
  text,
1032
1879
  tabId: options.tabId
1033
1880
  };
1034
- const response = await sendCommand(request);
1881
+ const response = await sendCommand2(request);
1035
1882
  if (options.json) {
1036
1883
  console.log(JSON.stringify(response, null, 2));
1037
1884
  } else {
@@ -1059,7 +1906,7 @@ async function closeCommand(options = {}) {
1059
1906
  action: "close",
1060
1907
  tabId: options.tabId
1061
1908
  };
1062
- const response = await sendCommand(request);
1909
+ const response = await sendCommand2(request);
1063
1910
  if (options.json) {
1064
1911
  console.log(JSON.stringify(response, null, 2));
1065
1912
  } else {
@@ -1078,7 +1925,7 @@ async function closeCommand(options = {}) {
1078
1925
  }
1079
1926
 
1080
1927
  // packages/cli/src/commands/get.ts
1081
- function parseRef5(ref) {
1928
+ function parseRef6(ref) {
1082
1929
  return ref.startsWith("@") ? ref.slice(1) : ref;
1083
1930
  }
1084
1931
  async function getCommand(attribute, ref, options = {}) {
@@ -1090,10 +1937,10 @@ async function getCommand(attribute, ref, options = {}) {
1090
1937
  id: generateId(),
1091
1938
  action: "get",
1092
1939
  attribute,
1093
- ref: ref ? parseRef5(ref) : void 0,
1940
+ ref: ref ? parseRef6(ref) : void 0,
1094
1941
  tabId: options.tabId
1095
1942
  };
1096
- const response = await sendCommand(request);
1943
+ const response = await sendCommand2(request);
1097
1944
  if (options.json) {
1098
1945
  console.log(JSON.stringify(response, null, 2));
1099
1946
  } else {
@@ -1109,17 +1956,17 @@ async function getCommand(attribute, ref, options = {}) {
1109
1956
 
1110
1957
  // packages/cli/src/commands/screenshot.ts
1111
1958
  import fs from "fs";
1112
- import path from "path";
1113
- import os from "os";
1959
+ import path3 from "path";
1960
+ import os2 from "os";
1114
1961
  function getDefaultPath() {
1115
1962
  const timestamp = (/* @__PURE__ */ new Date()).toISOString().replace(/[:.]/g, "-");
1116
1963
  const filename = `bb-screenshot-${timestamp}.png`;
1117
- return path.join(os.tmpdir(), filename);
1964
+ return path3.join(os2.tmpdir(), filename);
1118
1965
  }
1119
1966
  function saveBase64Image(dataUrl, filePath) {
1120
1967
  const base64Data = dataUrl.replace(/^data:image\/png;base64,/, "");
1121
1968
  const buffer = Buffer.from(base64Data, "base64");
1122
- const dir = path.dirname(filePath);
1969
+ const dir = path3.dirname(filePath);
1123
1970
  if (!fs.existsSync(dir)) {
1124
1971
  fs.mkdirSync(dir, { recursive: true });
1125
1972
  }
@@ -1127,13 +1974,13 @@ function saveBase64Image(dataUrl, filePath) {
1127
1974
  }
1128
1975
  async function screenshotCommand(outputPath, options = {}) {
1129
1976
  await ensureDaemonRunning();
1130
- const filePath = outputPath ? path.resolve(outputPath) : getDefaultPath();
1977
+ const filePath = outputPath ? path3.resolve(outputPath) : getDefaultPath();
1131
1978
  const request = {
1132
1979
  id: generateId(),
1133
1980
  action: "screenshot",
1134
1981
  tabId: options.tabId
1135
1982
  };
1136
- const response = await sendCommand(request);
1983
+ const response = await sendCommand2(request);
1137
1984
  if (response.success && response.data?.dataUrl) {
1138
1985
  const dataUrl = response.data.dataUrl;
1139
1986
  saveBase64Image(dataUrl, filePath);
@@ -1160,7 +2007,7 @@ async function screenshotCommand(outputPath, options = {}) {
1160
2007
  function isTimeWait(target) {
1161
2008
  return /^\d+$/.test(target);
1162
2009
  }
1163
- function parseRef6(ref) {
2010
+ function parseRef7(ref) {
1164
2011
  return ref.startsWith("@") ? ref.slice(1) : ref;
1165
2012
  }
1166
2013
  async function waitCommand(target, options = {}) {
@@ -1179,7 +2026,7 @@ async function waitCommand(target, options = {}) {
1179
2026
  tabId: options.tabId
1180
2027
  };
1181
2028
  } else {
1182
- const ref = parseRef6(target);
2029
+ const ref = parseRef7(target);
1183
2030
  request = {
1184
2031
  id: generateId(),
1185
2032
  action: "wait",
@@ -1188,7 +2035,7 @@ async function waitCommand(target, options = {}) {
1188
2035
  tabId: options.tabId
1189
2036
  };
1190
2037
  }
1191
- const response = await sendCommand(request);
2038
+ const response = await sendCommand2(request);
1192
2039
  if (options.json) {
1193
2040
  console.log(JSON.stringify(response, null, 2));
1194
2041
  } else {
@@ -1196,7 +2043,7 @@ async function waitCommand(target, options = {}) {
1196
2043
  if (isTimeWait(target)) {
1197
2044
  console.log(`\u5DF2\u7B49\u5F85 ${target}ms`);
1198
2045
  } else {
1199
- console.log(`\u5143\u7D20 @${parseRef6(target)} \u5DF2\u51FA\u73B0`);
2046
+ console.log(`\u5143\u7D20 @${parseRef7(target)} \u5DF2\u51FA\u73B0`);
1200
2047
  }
1201
2048
  } else {
1202
2049
  console.error(`\u9519\u8BEF: ${response.error}`);
@@ -1236,7 +2083,7 @@ async function pressCommand(keyString, options = {}) {
1236
2083
  modifiers,
1237
2084
  tabId: options.tabId
1238
2085
  };
1239
- const response = await sendCommand(request);
2086
+ const response = await sendCommand2(request);
1240
2087
  if (options.json) {
1241
2088
  console.log(JSON.stringify(response, null, 2));
1242
2089
  } else {
@@ -1277,7 +2124,7 @@ async function scrollCommand(direction, pixels, options = {}) {
1277
2124
  pixels: pixelValue,
1278
2125
  tabId: options.tabId
1279
2126
  };
1280
- const response = await sendCommand(request);
2127
+ const response = await sendCommand2(request);
1281
2128
  if (options.json) {
1282
2129
  console.log(JSON.stringify(response, null, 2));
1283
2130
  } else {
@@ -1291,76 +2138,17 @@ async function scrollCommand(direction, pixels, options = {}) {
1291
2138
  }
1292
2139
 
1293
2140
  // packages/cli/src/commands/daemon.ts
1294
- import { spawn as spawn2 } from "child_process";
1295
- async function daemonCommand(options = {}) {
1296
- if (await isDaemonRunning()) {
1297
- if (options.json) {
1298
- console.log(JSON.stringify({ success: false, error: "Daemon \u5DF2\u5728\u8FD0\u884C" }));
1299
- } else {
1300
- console.log("Daemon \u5DF2\u5728\u8FD0\u884C");
1301
- }
1302
- return;
1303
- }
1304
- const daemonPath = getDaemonPath();
1305
- const args = [daemonPath];
1306
- if (options.host) {
1307
- args.push("--host", options.host);
1308
- }
1309
- if (options.json) {
1310
- console.log(JSON.stringify({ success: true, message: "Daemon \u542F\u52A8\u4E2D..." }));
1311
- } else {
1312
- console.log("Daemon \u542F\u52A8\u4E2D...");
1313
- }
1314
- await new Promise((resolve2, reject) => {
1315
- const child = spawn2(process.execPath, args, {
1316
- stdio: "inherit"
1317
- });
1318
- child.on("exit", (code) => {
1319
- if (code && code !== 0) {
1320
- reject(new Error(`Daemon exited with code ${code}`));
1321
- } else {
1322
- resolve2();
1323
- }
1324
- });
1325
- child.on("error", reject);
1326
- });
1327
- }
1328
- async function stopCommand(options = {}) {
1329
- if (!await isDaemonRunning()) {
1330
- if (options.json) {
1331
- console.log(JSON.stringify({ success: false, error: "Daemon \u672A\u8FD0\u884C" }));
1332
- } else {
1333
- console.log("Daemon \u672A\u8FD0\u884C");
1334
- }
1335
- return;
1336
- }
1337
- const stopped = await stopDaemon();
1338
- if (stopped) {
1339
- if (options.json) {
1340
- console.log(JSON.stringify({ success: true, message: "Daemon \u5DF2\u505C\u6B62" }));
1341
- } else {
1342
- console.log("Daemon \u5DF2\u505C\u6B62");
1343
- }
1344
- } else {
1345
- if (options.json) {
1346
- console.log(JSON.stringify({ success: false, error: "\u65E0\u6CD5\u505C\u6B62 Daemon" }));
1347
- } else {
1348
- console.error("\u65E0\u6CD5\u505C\u6B62 Daemon");
1349
- }
1350
- process.exit(1);
1351
- }
1352
- }
1353
2141
  async function statusCommand(options = {}) {
1354
2142
  const running = await isDaemonRunning();
1355
2143
  if (options.json) {
1356
2144
  console.log(JSON.stringify({ running }));
1357
2145
  } else {
1358
- console.log(running ? "Daemon \u8FD0\u884C\u4E2D" : "Daemon \u672A\u8FD0\u884C");
2146
+ console.log(running ? "\u6D4F\u89C8\u5668\u8FD0\u884C\u4E2D" : "\u6D4F\u89C8\u5668\u672A\u8FD0\u884C");
1359
2147
  }
1360
2148
  }
1361
2149
 
1362
2150
  // packages/cli/src/commands/reload.ts
1363
- import WebSocket from "ws";
2151
+ import WebSocket2 from "ws";
1364
2152
  var EXTENSION_NAME = "bb-browser";
1365
2153
  async function reloadCommand(options = {}) {
1366
2154
  const port = options.port || 9222;
@@ -1377,7 +2165,7 @@ async function reloadCommand(options = {}) {
1377
2165
  throw new Error("\u8BF7\u5148\u6253\u5F00 chrome://extensions \u9875\u9762");
1378
2166
  }
1379
2167
  const result = await new Promise((resolve2, reject) => {
1380
- const ws = new WebSocket(extPage.webSocketDebuggerUrl);
2168
+ const ws = new WebSocket2(extPage.webSocketDebuggerUrl);
1381
2169
  let resolved = false;
1382
2170
  const timeout = setTimeout(() => {
1383
2171
  if (!resolved) {
@@ -1474,7 +2262,7 @@ async function backCommand(options = {}) {
1474
2262
  action: "back",
1475
2263
  tabId: options.tabId
1476
2264
  };
1477
- const response = await sendCommand(request);
2265
+ const response = await sendCommand2(request);
1478
2266
  if (options.json) {
1479
2267
  console.log(JSON.stringify(response, null, 2));
1480
2268
  } else {
@@ -1498,7 +2286,7 @@ async function forwardCommand(options = {}) {
1498
2286
  action: "forward",
1499
2287
  tabId: options.tabId
1500
2288
  };
1501
- const response = await sendCommand(request);
2289
+ const response = await sendCommand2(request);
1502
2290
  if (options.json) {
1503
2291
  console.log(JSON.stringify(response, null, 2));
1504
2292
  } else {
@@ -1522,7 +2310,7 @@ async function refreshCommand(options = {}) {
1522
2310
  action: "refresh",
1523
2311
  tabId: options.tabId
1524
2312
  };
1525
- const response = await sendCommand(request);
2313
+ const response = await sendCommand2(request);
1526
2314
  if (options.json) {
1527
2315
  console.log(JSON.stringify(response, null, 2));
1528
2316
  } else {
@@ -1541,7 +2329,7 @@ async function refreshCommand(options = {}) {
1541
2329
  }
1542
2330
 
1543
2331
  // packages/cli/src/commands/check.ts
1544
- function parseRef7(ref) {
2332
+ function parseRef8(ref) {
1545
2333
  return ref.startsWith("@") ? ref.slice(1) : ref;
1546
2334
  }
1547
2335
  async function checkCommand(ref, options = {}) {
@@ -1549,14 +2337,14 @@ async function checkCommand(ref, options = {}) {
1549
2337
  throw new Error("\u7F3A\u5C11 ref \u53C2\u6570");
1550
2338
  }
1551
2339
  await ensureDaemonRunning();
1552
- const parsedRef = parseRef7(ref);
2340
+ const parsedRef = parseRef8(ref);
1553
2341
  const request = {
1554
2342
  id: generateId(),
1555
2343
  action: "check",
1556
2344
  ref: parsedRef,
1557
2345
  tabId: options.tabId
1558
2346
  };
1559
- const response = await sendCommand(request);
2347
+ const response = await sendCommand2(request);
1560
2348
  if (options.json) {
1561
2349
  console.log(JSON.stringify(response, null, 2));
1562
2350
  } else {
@@ -1588,14 +2376,14 @@ async function uncheckCommand(ref, options = {}) {
1588
2376
  throw new Error("\u7F3A\u5C11 ref \u53C2\u6570");
1589
2377
  }
1590
2378
  await ensureDaemonRunning();
1591
- const parsedRef = parseRef7(ref);
2379
+ const parsedRef = parseRef8(ref);
1592
2380
  const request = {
1593
2381
  id: generateId(),
1594
2382
  action: "uncheck",
1595
2383
  ref: parsedRef,
1596
2384
  tabId: options.tabId
1597
2385
  };
1598
- const response = await sendCommand(request);
2386
+ const response = await sendCommand2(request);
1599
2387
  if (options.json) {
1600
2388
  console.log(JSON.stringify(response, null, 2));
1601
2389
  } else {
@@ -1624,7 +2412,7 @@ async function uncheckCommand(ref, options = {}) {
1624
2412
  }
1625
2413
 
1626
2414
  // packages/cli/src/commands/select.ts
1627
- function parseRef8(ref) {
2415
+ function parseRef9(ref) {
1628
2416
  return ref.startsWith("@") ? ref.slice(1) : ref;
1629
2417
  }
1630
2418
  async function selectCommand(ref, value, options = {}) {
@@ -1635,7 +2423,7 @@ async function selectCommand(ref, value, options = {}) {
1635
2423
  throw new Error("\u7F3A\u5C11 value \u53C2\u6570");
1636
2424
  }
1637
2425
  await ensureDaemonRunning();
1638
- const parsedRef = parseRef8(ref);
2426
+ const parsedRef = parseRef9(ref);
1639
2427
  const request = {
1640
2428
  id: generateId(),
1641
2429
  action: "select",
@@ -1643,7 +2431,7 @@ async function selectCommand(ref, value, options = {}) {
1643
2431
  value,
1644
2432
  tabId: options.tabId
1645
2433
  };
1646
- const response = await sendCommand(request);
2434
+ const response = await sendCommand2(request);
1647
2435
  if (options.json) {
1648
2436
  console.log(JSON.stringify(response, null, 2));
1649
2437
  } else {
@@ -1681,7 +2469,7 @@ async function evalCommand(script, options = {}) {
1681
2469
  script,
1682
2470
  tabId: options.tabId
1683
2471
  };
1684
- const response = await sendCommand(request);
2472
+ const response = await sendCommand2(request);
1685
2473
  if (options.json) {
1686
2474
  console.log(JSON.stringify(response, null, 2));
1687
2475
  } else {
@@ -1768,7 +2556,7 @@ async function tabCommand(args, options = {}) {
1768
2556
  index: parsed.index,
1769
2557
  tabId: parsed.tabId
1770
2558
  };
1771
- const response = await sendCommand(request);
2559
+ const response = await sendCommand2(request);
1772
2560
  if (options.json) {
1773
2561
  console.log(JSON.stringify(response, null, 2));
1774
2562
  } else {
@@ -1817,7 +2605,7 @@ async function frameCommand(selector, options = {}) {
1817
2605
  selector,
1818
2606
  tabId: options.tabId
1819
2607
  };
1820
- const response = await sendCommand(request);
2608
+ const response = await sendCommand2(request);
1821
2609
  if (options.json) {
1822
2610
  console.log(JSON.stringify(response, null, 2));
1823
2611
  } else {
@@ -1841,7 +2629,7 @@ async function frameMainCommand(options = {}) {
1841
2629
  action: "frame_main",
1842
2630
  tabId: options.tabId
1843
2631
  };
1844
- const response = await sendCommand(request);
2632
+ const response = await sendCommand2(request);
1845
2633
  if (options.json) {
1846
2634
  console.log(JSON.stringify(response, null, 2));
1847
2635
  } else {
@@ -1867,7 +2655,7 @@ async function dialogCommand(subCommand, promptText, options = {}) {
1867
2655
  promptText: subCommand === "accept" ? promptText : void 0,
1868
2656
  tabId: options.tabId
1869
2657
  };
1870
- const response = await sendCommand(request);
2658
+ const response = await sendCommand2(request);
1871
2659
  if (options.json) {
1872
2660
  console.log(JSON.stringify(response, null, 2));
1873
2661
  } else {
@@ -1888,7 +2676,7 @@ async function dialogCommand(subCommand, promptText, options = {}) {
1888
2676
 
1889
2677
  // packages/cli/src/commands/network.ts
1890
2678
  async function networkCommand(subCommand, urlOrFilter, options = {}) {
1891
- const response = await sendCommand({
2679
+ const response = await sendCommand2({
1892
2680
  id: crypto.randomUUID(),
1893
2681
  action: "network",
1894
2682
  networkCommand: subCommand,
@@ -1975,7 +2763,7 @@ async function networkCommand(subCommand, urlOrFilter, options = {}) {
1975
2763
 
1976
2764
  // packages/cli/src/commands/console.ts
1977
2765
  async function consoleCommand(options = {}) {
1978
- const response = await sendCommand({
2766
+ const response = await sendCommand2({
1979
2767
  id: crypto.randomUUID(),
1980
2768
  action: "console",
1981
2769
  consoleCommand: options.clear ? "clear" : "get",
@@ -2020,7 +2808,7 @@ async function consoleCommand(options = {}) {
2020
2808
 
2021
2809
  // packages/cli/src/commands/errors.ts
2022
2810
  async function errorsCommand(options = {}) {
2023
- const response = await sendCommand({
2811
+ const response = await sendCommand2({
2024
2812
  id: crypto.randomUUID(),
2025
2813
  action: "errors",
2026
2814
  errorsCommand: options.clear ? "clear" : "get",
@@ -2060,7 +2848,7 @@ async function errorsCommand(options = {}) {
2060
2848
 
2061
2849
  // packages/cli/src/commands/trace.ts
2062
2850
  async function traceCommand(subCommand, options = {}) {
2063
- const response = await sendCommand({
2851
+ const response = await sendCommand2({
2064
2852
  id: crypto.randomUUID(),
2065
2853
  action: "trace",
2066
2854
  traceCommand: subCommand,
@@ -2150,7 +2938,7 @@ function matchTabOrigin2(tabUrl, targetHostname) {
2150
2938
  }
2151
2939
  async function ensureTabForOrigin(origin, hostname) {
2152
2940
  const listReq = { id: generateId(), action: "tab_list" };
2153
- const listResp = await sendCommand(listReq);
2941
+ const listResp = await sendCommand2(listReq);
2154
2942
  if (listResp.success && listResp.data?.tabs) {
2155
2943
  const matchingTab = listResp.data.tabs.find(
2156
2944
  (tab) => matchTabOrigin2(tab.url, hostname)
@@ -2159,7 +2947,7 @@ async function ensureTabForOrigin(origin, hostname) {
2159
2947
  return matchingTab.tabId;
2160
2948
  }
2161
2949
  }
2162
- const newResp = await sendCommand({ id: generateId(), action: "tab_new", url: origin });
2950
+ const newResp = await sendCommand2({ id: generateId(), action: "tab_new", url: origin });
2163
2951
  if (!newResp.success) {
2164
2952
  throw new Error(`\u65E0\u6CD5\u6253\u5F00 ${origin}: ${newResp.error}`);
2165
2953
  }
@@ -2228,7 +3016,7 @@ async function fetchCommand(url, options = {}) {
2228
3016
  }
2229
3017
  const script = buildFetchScript(url, options);
2230
3018
  const evalReq = { id: generateId(), action: "eval", script, tabId: targetTabId };
2231
- const evalResp = await sendCommand(evalReq);
3019
+ const evalResp = await sendCommand2(evalReq);
2232
3020
  if (!evalResp.success) {
2233
3021
  throw new Error(`Fetch \u5931\u8D25: ${evalResp.error}`);
2234
3022
  }
@@ -2262,21 +3050,16 @@ async function fetchCommand(url, options = {}) {
2262
3050
 
2263
3051
  // packages/cli/src/commands/history.ts
2264
3052
  async function historyCommand(subCommand, options = {}) {
2265
- const response = await sendCommand({
2266
- id: crypto.randomUUID(),
2267
- action: "history",
2268
- historyCommand: subCommand,
2269
- text: options.query,
2270
- ms: options.days
2271
- });
3053
+ const days = options.days || 30;
3054
+ const data = subCommand === "search" ? { historyItems: searchHistory(options.query, days) } : { historyDomains: getHistoryDomains(days) };
2272
3055
  if (options.json) {
2273
- console.log(JSON.stringify(response));
3056
+ console.log(JSON.stringify({
3057
+ id: crypto.randomUUID(),
3058
+ success: true,
3059
+ data
3060
+ }));
2274
3061
  return;
2275
3062
  }
2276
- if (!response.success) {
2277
- throw new Error(response.error || "History command failed");
2278
- }
2279
- const data = response.data;
2280
3063
  switch (subCommand) {
2281
3064
  case "search": {
2282
3065
  const items = data?.historyItems || [];
@@ -2315,10 +3098,13 @@ async function historyCommand(subCommand, options = {}) {
2315
3098
  }
2316
3099
 
2317
3100
  // packages/cli/src/index.ts
2318
- var VERSION = "0.7.0";
3101
+ var VERSION = "0.8.0";
2319
3102
  var HELP_TEXT = `
2320
3103
  bb-browser - AI Agent \u6D4F\u89C8\u5668\u81EA\u52A8\u5316\u5DE5\u5177
2321
3104
 
3105
+ \u5B89\u88C5\uFF1A
3106
+ npm install -g bb-browser
3107
+
2322
3108
  \u63D0\u793A\uFF1A\u5927\u591A\u6570\u6570\u636E\u83B7\u53D6\u4EFB\u52A1\u8BF7\u76F4\u63A5\u4F7F\u7528 site \u547D\u4EE4\uFF0C\u65E0\u9700\u624B\u52A8\u64CD\u4F5C\u6D4F\u89C8\u5668\uFF1A
2323
3109
  bb-browser site list \u67E5\u770B\u6240\u6709\u53EF\u7528\u547D\u4EE4
2324
3110
  bb-browser site twitter/search "AI" \u793A\u4F8B\uFF1A\u641C\u7D22\u63A8\u6587
@@ -2356,6 +3142,7 @@ bb-browser - AI Agent \u6D4F\u89C8\u5668\u81EA\u52A8\u5316\u5DE5\u5177
2356
3142
 
2357
3143
  \u6807\u7B7E\u9875\uFF1A
2358
3144
  tab [list|new|close|<n>] \u7BA1\u7406\u6807\u7B7E\u9875
3145
+ status \u67E5\u770B\u53D7\u7BA1\u6D4F\u89C8\u5668\u72B6\u6001
2359
3146
 
2360
3147
  \u5BFC\u822A\uFF1A
2361
3148
  back / forward / refresh \u540E\u9000 / \u524D\u8FDB / \u5237\u65B0
@@ -2369,6 +3156,8 @@ bb-browser - AI Agent \u6D4F\u89C8\u5668\u81EA\u52A8\u5316\u5DE5\u5177
2369
3156
 
2370
3157
  \u9009\u9879\uFF1A
2371
3158
  --json \u4EE5 JSON \u683C\u5F0F\u8F93\u51FA
3159
+ --port <n> \u6307\u5B9A Chrome CDP \u7AEF\u53E3
3160
+ --openclaw \u4F18\u5148\u590D\u7528 OpenClaw \u6D4F\u89C8\u5668\u5B9E\u4F8B
2372
3161
  --jq <expr> \u5BF9 JSON \u8F93\u51FA\u5E94\u7528 jq \u8FC7\u6EE4\uFF08\u76F4\u63A5\u4F5C\u7528\u4E8E\u6570\u636E\uFF0C\u8DF3\u8FC7 id/success \u4FE1\u5C01\uFF09
2373
3162
  -i, --interactive \u53EA\u8F93\u51FA\u53EF\u4EA4\u4E92\u5143\u7D20\uFF08snapshot \u547D\u4EE4\uFF09
2374
3163
  -c, --compact \u79FB\u9664\u7A7A\u7ED3\u6784\u8282\u70B9\uFF08snapshot \u547D\u4EE4\uFF09
@@ -2409,6 +3198,12 @@ function parseArgs(argv) {
2409
3198
  }
2410
3199
  } else if (arg === "--openclaw") {
2411
3200
  result.flags.openclaw = true;
3201
+ } else if (arg === "--port") {
3202
+ skipNext = true;
3203
+ const nextIdx = args.indexOf(arg) + 1;
3204
+ if (nextIdx < args.length) {
3205
+ result.flags.port = parseInt(args[nextIdx], 10);
3206
+ }
2412
3207
  } else if (arg === "--help" || arg === "-h") {
2413
3208
  result.flags.help = true;
2414
3209
  } else if (arg === "--version" || arg === "-v") {
@@ -2458,9 +3253,9 @@ async function main() {
2458
3253
  return;
2459
3254
  }
2460
3255
  if (process.argv.includes("--mcp")) {
2461
- const mcpPath = fileURLToPath2(new URL("./mcp.js", import.meta.url));
2462
- const { spawn: spawn3 } = await import("child_process");
2463
- const child = spawn3(process.execPath, [mcpPath], { stdio: "inherit" });
3256
+ const mcpPath = fileURLToPath3(new URL("./mcp.js", import.meta.url));
3257
+ const { spawn: spawn2 } = await import("child_process");
3258
+ const child = spawn2(process.execPath, [mcpPath], { stdio: "inherit" });
2464
3259
  child.on("exit", (code) => process.exit(code ?? 0));
2465
3260
  return;
2466
3261
  }
@@ -2818,9 +3613,9 @@ async function main() {
2818
3613
  break;
2819
3614
  }
2820
3615
  case "star": {
2821
- const { execSync: execSync2 } = await import("child_process");
3616
+ const { execSync: execSync4 } = await import("child_process");
2822
3617
  try {
2823
- execSync2("gh auth status", { stdio: "pipe" });
3618
+ execSync4("gh auth status", { stdio: "pipe" });
2824
3619
  } catch {
2825
3620
  console.error("\u9700\u8981\u5148\u5B89\u88C5\u5E76\u767B\u5F55 GitHub CLI: https://cli.github.com");
2826
3621
  console.error(" brew install gh && gh auth login");
@@ -2829,7 +3624,7 @@ async function main() {
2829
3624
  const repos = ["epiral/bb-browser", "epiral/bb-sites"];
2830
3625
  for (const repo of repos) {
2831
3626
  try {
2832
- execSync2(`gh api user/starred/${repo} -X PUT`, { stdio: "pipe" });
3627
+ execSync4(`gh api user/starred/${repo} -X PUT`, { stdio: "pipe" });
2833
3628
  console.log(`\u2B50 Starred ${repo}`);
2834
3629
  } catch {
2835
3630
  console.log(`Already starred or failed: ${repo}`);