bb-browser 0.6.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.
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 {
@@ -9,6 +8,860 @@ import {
9
8
  } from "./chunk-AHGAQEFO.js";
10
9
  import "./chunk-D4HDZEJT.js";
11
10
 
11
+ // packages/cli/src/index.ts
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
+ }
864
+
12
865
  // packages/cli/src/client.ts
13
866
  var jqExpression;
14
867
  function setJqExpression(expression) {
@@ -27,184 +880,201 @@ function handleJqResponse(response) {
27
880
  printJqResults(response);
28
881
  }
29
882
  }
30
- async function sendCommand(request) {
31
- const controller = new AbortController();
32
- const timeoutId = setTimeout(() => controller.abort(), COMMAND_TIMEOUT);
33
- try {
34
- const res = await fetch(`${DAEMON_BASE_URL}/command`, {
35
- method: "POST",
36
- headers: {
37
- "Content-Type": "application/json"
38
- },
39
- body: JSON.stringify(request),
40
- signal: controller.signal
41
- });
42
- clearTimeout(timeoutId);
43
- if (!res.ok) {
44
- if (res.status === 408) {
45
- return {
46
- id: request.id,
47
- success: false,
48
- error: "\u547D\u4EE4\u6267\u884C\u8D85\u65F6"
49
- };
50
- }
51
- if (res.status === 503) {
52
- return {
53
- id: request.id,
54
- success: false,
55
- error: [
56
- "Chrome extension not connected.",
57
- "",
58
- "1. Download extension: https://github.com/epiral/bb-browser/releases/latest",
59
- "2. Unzip the downloaded file",
60
- "3. Open chrome://extensions/ \u2192 Enable Developer Mode",
61
- '4. Click "Load unpacked" \u2192 select the unzipped folder'
62
- ].join("\n")
63
- };
64
- }
65
- return {
66
- id: request.id,
67
- success: false,
68
- error: `HTTP \u9519\u8BEF: ${res.status} ${res.statusText}`
69
- };
70
- }
71
- const response = await res.json();
72
- return response;
73
- } catch (error) {
74
- clearTimeout(timeoutId);
75
- if (error instanceof Error) {
76
- if (error.name === "AbortError") {
77
- return {
78
- id: request.id,
79
- success: false,
80
- error: "\u8BF7\u6C42\u8D85\u65F6"
81
- };
82
- }
83
- if (error.message.includes("fetch failed") || error.message.includes("ECONNREFUSED")) {
84
- throw new Error([
85
- "Cannot connect to daemon.",
86
- "",
87
- "Start the daemon first:",
88
- " bb-browser daemon",
89
- "",
90
- "Then load the Chrome extension:",
91
- " chrome://extensions/ \u2192 Developer Mode \u2192 Load unpacked \u2192 node_modules/bb-browser/extension/"
92
- ].join("\n"));
93
- }
94
- throw error;
95
- }
96
- throw error;
97
- }
883
+ async function sendCommand2(request) {
884
+ return sendCommand(request);
98
885
  }
99
886
 
100
887
  // packages/cli/src/daemon-manager.ts
101
- import { spawn } from "child_process";
102
- import { existsSync } from "fs";
103
- import { fileURLToPath } from "url";
888
+ import { fileURLToPath as fileURLToPath2 } from "url";
104
889
  import { dirname, resolve } from "path";
105
- function getDaemonPath() {
106
- const currentFile = fileURLToPath(import.meta.url);
107
- const currentDir = dirname(currentFile);
108
- const sameDirPath = resolve(currentDir, "daemon.js");
109
- if (existsSync(sameDirPath)) {
110
- return sameDirPath;
111
- }
112
- return resolve(currentDir, "../../daemon/dist/index.js");
113
- }
114
- var DAEMON_START_TIMEOUT = 5e3;
115
- var POLL_INTERVAL = 200;
890
+ import { existsSync as existsSync2 } from "fs";
116
891
  async function isDaemonRunning() {
892
+ return await discoverCdpPort() !== null;
893
+ }
894
+ async function ensureDaemonRunning() {
117
895
  try {
118
- const controller = new AbortController();
119
- const timeoutId = setTimeout(() => controller.abort(), 2e3);
120
- const response = await fetch(`${DAEMON_BASE_URL}/status`, {
121
- signal: controller.signal
122
- });
123
- clearTimeout(timeoutId);
124
- return response.ok;
125
- } catch {
126
- 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;
127
907
  }
128
908
  }
129
- async function isExtensionConnected() {
130
- try {
131
- const controller = new AbortController();
132
- const timeoutId = setTimeout(() => controller.abort(), 2e3);
133
- const response = await fetch(`${DAEMON_BASE_URL}/status`, {
134
- signal: controller.signal
135
- });
136
- clearTimeout(timeoutId);
137
- if (!response.ok) return false;
138
- const data = await response.json();
139
- return !!data.extensionConnected;
140
- } catch {
141
- 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
+ );
142
930
  }
931
+ return candidates;
143
932
  }
144
- async function waitForDaemon(timeoutMs) {
145
- const startTime = Date.now();
146
- while (Date.now() - startTime < timeoutMs) {
147
- if (await isDaemonRunning()) {
148
- return true;
933
+ function findHistoryPath() {
934
+ for (const historyPath of getHistoryPathCandidates()) {
935
+ if (existsSync3(historyPath)) {
936
+ return historyPath;
149
937
  }
150
- await new Promise((resolve2) => setTimeout(resolve2, POLL_INTERVAL));
151
938
  }
152
- return false;
939
+ return null;
153
940
  }
154
- function spawnDaemon() {
155
- const daemonPath = getDaemonPath();
156
- const daemonProcess = spawn(process.execPath, [daemonPath], {
157
- detached: true,
158
- stdio: "ignore",
159
- env: { ...process.env }
160
- });
161
- daemonProcess.unref();
941
+ function sqlEscape(value) {
942
+ return value.replace(/'/g, "''");
162
943
  }
163
- async function ensureDaemonRunning() {
164
- if (await isDaemonRunning()) {
165
- return;
166
- }
167
- spawnDaemon();
168
- const ready = await waitForDaemon(DAEMON_START_TIMEOUT);
169
- if (!ready) {
170
- throw new Error(
171
- "\u65E0\u6CD5\u542F\u52A8 Daemon\u3002\u8BF7\u624B\u52A8\u8FD0\u884C bb-browser daemon \u6216 bb-daemon \u542F\u52A8\u670D\u52A1"
172
- );
173
- }
174
- const extStart = Date.now();
175
- while (Date.now() - extStart < 1e4) {
176
- if (await isExtensionConnected()) return;
177
- await new Promise((r) => setTimeout(r, POLL_INTERVAL));
944
+ function buildTimeWhere(days) {
945
+ if (!days || days <= 0) {
946
+ return "";
178
947
  }
948
+ return `last_visit_time > (strftime('%s', 'now') - ${Math.floor(days)}*86400) * 1000000 + 11644473600000000`;
179
949
  }
180
- 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`);
181
956
  try {
182
- const controller = new AbortController();
183
- const timeoutId = setTimeout(() => controller.abort(), 5e3);
184
- const response = await fetch(`${DAEMON_BASE_URL}/shutdown`, {
185
- method: "POST",
186
- 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"]
187
963
  });
188
- clearTimeout(timeoutId);
189
- return response.ok;
964
+ return output.split("\n").filter(Boolean).map((line) => mapRow(line.split(" "))).filter((item) => item !== null);
190
965
  } catch {
191
- return false;
966
+ return [];
967
+ } finally {
968
+ try {
969
+ unlinkSync(tmpPath);
970
+ } catch {
971
+ }
192
972
  }
193
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
+ }
194
1053
 
195
1054
  // packages/cli/src/commands/site.ts
196
- import { readFileSync, readdirSync, existsSync as existsSync2, mkdirSync } from "fs";
197
- import { join, relative } from "path";
198
- import { homedir } from "os";
199
- import { execSync } from "child_process";
200
- var BB_DIR = join(homedir(), ".bb-browser");
201
- var LOCAL_SITES_DIR = join(BB_DIR, "sites");
202
- 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");
203
1062
  var COMMUNITY_REPO = "https://github.com/epiral/bb-sites.git";
1063
+ function checkCliUpdate() {
1064
+ try {
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();
1067
+ if (latest && current && latest !== current && latest.localeCompare(current, void 0, { numeric: true }) > 0) {
1068
+ console.log(`
1069
+ \u{1F4E6} bb-browser ${latest} available (current: ${current}). Run: npm install -g bb-browser`);
1070
+ }
1071
+ } catch {
1072
+ }
1073
+ }
204
1074
  function parseSiteMeta(filePath, source) {
205
1075
  let content;
206
1076
  try {
207
- content = readFileSync(filePath, "utf-8");
1077
+ content = readFileSync2(filePath, "utf-8");
208
1078
  } catch {
209
1079
  return null;
210
1080
  }
@@ -264,7 +1134,7 @@ function parseSiteMeta(filePath, source) {
264
1134
  return meta;
265
1135
  }
266
1136
  function scanSites(dir, source) {
267
- if (!existsSync2(dir)) return [];
1137
+ if (!existsSync4(dir)) return [];
268
1138
  const sites = [];
269
1139
  function walk(currentDir) {
270
1140
  let entries;
@@ -274,7 +1144,7 @@ function scanSites(dir, source) {
274
1144
  return;
275
1145
  }
276
1146
  for (const entry of entries) {
277
- const fullPath = join(currentDir, entry.name);
1147
+ const fullPath = join2(currentDir, entry.name);
278
1148
  if (entry.isDirectory() && !entry.name.startsWith(".")) {
279
1149
  walk(fullPath);
280
1150
  } else if (entry.isFile() && entry.name.endsWith(".js")) {
@@ -378,10 +1248,10 @@ function siteSearch(query, options) {
378
1248
  }
379
1249
  function siteUpdate() {
380
1250
  mkdirSync(BB_DIR, { recursive: true });
381
- if (existsSync2(join(COMMUNITY_SITES_DIR, ".git"))) {
1251
+ if (existsSync4(join2(COMMUNITY_SITES_DIR, ".git"))) {
382
1252
  console.log("\u66F4\u65B0\u793E\u533A site adapter \u5E93...");
383
1253
  try {
384
- execSync("git pull --ff-only", { cwd: COMMUNITY_SITES_DIR, stdio: "pipe" });
1254
+ execSync3("git pull --ff-only", { cwd: COMMUNITY_SITES_DIR, stdio: "pipe" });
385
1255
  console.log("\u66F4\u65B0\u5B8C\u6210\u3002");
386
1256
  console.log("");
387
1257
  console.log("\u{1F4A1} \u8FD0\u884C bb-browser site recommend \u770B\u770B\u54EA\u4E9B\u548C\u4F60\u7684\u6D4F\u89C8\u4E60\u60EF\u5339\u914D");
@@ -393,7 +1263,7 @@ function siteUpdate() {
393
1263
  } else {
394
1264
  console.log(`\u514B\u9686\u793E\u533A adapter \u5E93: ${COMMUNITY_REPO}`);
395
1265
  try {
396
- execSync(`git clone ${COMMUNITY_REPO} ${COMMUNITY_SITES_DIR}`, { stdio: "pipe" });
1266
+ execSync3(`git clone ${COMMUNITY_REPO} ${COMMUNITY_SITES_DIR}`, { stdio: "pipe" });
397
1267
  console.log("\u514B\u9686\u5B8C\u6210\u3002");
398
1268
  console.log("");
399
1269
  console.log("\u{1F4A1} \u8FD0\u884C bb-browser site recommend \u770B\u770B\u54EA\u4E9B\u548C\u4F60\u7684\u6D4F\u89C8\u4E60\u60EF\u5339\u914D");
@@ -405,6 +1275,8 @@ function siteUpdate() {
405
1275
  }
406
1276
  const sites = scanSites(COMMUNITY_SITES_DIR, "community");
407
1277
  console.log(`\u5DF2\u5B89\u88C5 ${sites.length} \u4E2A\u793E\u533A adapter\u3002`);
1278
+ console.log(`\u2B50 Like bb-browser? \u2192 bb-browser star`);
1279
+ checkCliUpdate();
408
1280
  }
409
1281
  function findSiteByName(name) {
410
1282
  return getAllSites().find((site) => site.name === name);
@@ -450,16 +1322,7 @@ function siteInfo(name, options) {
450
1322
  }
451
1323
  async function siteRecommend(options) {
452
1324
  const days = options.days ?? 30;
453
- const response = await sendCommand({
454
- id: generateId(),
455
- action: "history",
456
- historyCommand: "domains",
457
- ms: days
458
- });
459
- if (!response.success) {
460
- throw new Error(response.error || "History command failed");
461
- }
462
- const historyDomains = response.data?.historyDomains || [];
1325
+ const historyDomains = getHistoryDomains(days);
463
1326
  const sites = getAllSites();
464
1327
  const sitesByDomain = /* @__PURE__ */ new Map();
465
1328
  for (const site of sites) {
@@ -577,7 +1440,7 @@ async function siteRun(name, args, options) {
577
1440
  process.exit(1);
578
1441
  }
579
1442
  }
580
- const jsContent = readFileSync(site.filePath, "utf-8");
1443
+ const jsContent = readFileSync2(site.filePath, "utf-8");
581
1444
  const jsBody = jsContent.replace(/\/\*\s*@meta[\s\S]*?\*\//, "").trim();
582
1445
  const argsJson = JSON.stringify(argMap);
583
1446
  const script = `(${jsBody})(${argsJson})`;
@@ -637,7 +1500,7 @@ async function siteRun(name, args, options) {
637
1500
  let targetTabId = options.tabId;
638
1501
  if (!targetTabId && site.domain) {
639
1502
  const listReq = { id: generateId(), action: "tab_list" };
640
- const listResp = await sendCommand(listReq);
1503
+ const listResp = await sendCommand2(listReq);
641
1504
  if (listResp.success && listResp.data?.tabs) {
642
1505
  const matchingTab = listResp.data.tabs.find(
643
1506
  (tab) => matchTabOrigin(tab.url, site.domain)
@@ -647,7 +1510,7 @@ async function siteRun(name, args, options) {
647
1510
  }
648
1511
  }
649
1512
  if (!targetTabId) {
650
- const newResp = await sendCommand({
1513
+ const newResp = await sendCommand2({
651
1514
  id: generateId(),
652
1515
  action: "tab_new",
653
1516
  url: `https://${site.domain}`
@@ -657,7 +1520,7 @@ async function siteRun(name, args, options) {
657
1520
  }
658
1521
  }
659
1522
  const evalReq = { id: generateId(), action: "eval", script, tabId: targetTabId };
660
- const evalResp = await sendCommand(evalReq);
1523
+ const evalResp = await sendCommand2(evalReq);
661
1524
  if (!evalResp.success) {
662
1525
  const hint = site.domain ? `Open https://${site.domain} in your browser, make sure you are logged in, then retry.` : void 0;
663
1526
  if (options.json) {
@@ -792,10 +1655,10 @@ async function siteCommand(args, options = {}) {
792
1655
  silentUpdate();
793
1656
  }
794
1657
  function silentUpdate() {
795
- const gitDir = join(COMMUNITY_SITES_DIR, ".git");
796
- if (!existsSync2(gitDir)) return;
797
- import("child_process").then(({ spawn: spawn3 }) => {
798
- 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"], {
799
1662
  cwd: COMMUNITY_SITES_DIR,
800
1663
  stdio: "ignore",
801
1664
  detached: true
@@ -831,7 +1694,7 @@ async function openCommand(url, options = {}) {
831
1694
  request.tabId = tabId;
832
1695
  }
833
1696
  }
834
- const response = await sendCommand(request);
1697
+ const response = await sendCommand2(request);
835
1698
  if (options.json) {
836
1699
  console.log(JSON.stringify(response, null, 2));
837
1700
  } else {
@@ -867,7 +1730,7 @@ async function snapshotCommand(options = {}) {
867
1730
  selector: options.selector,
868
1731
  tabId: options.tabId
869
1732
  };
870
- const response = await sendCommand(request);
1733
+ const response = await sendCommand2(request);
871
1734
  if (options.json) {
872
1735
  console.log(JSON.stringify(response, null, 2));
873
1736
  } else {
@@ -886,7 +1749,7 @@ async function snapshotCommand(options = {}) {
886
1749
  }
887
1750
 
888
1751
  // packages/cli/src/commands/click.ts
889
- function parseRef(ref) {
1752
+ function parseRef2(ref) {
890
1753
  return ref.startsWith("@") ? ref.slice(1) : ref;
891
1754
  }
892
1755
  async function clickCommand(ref, options = {}) {
@@ -894,14 +1757,14 @@ async function clickCommand(ref, options = {}) {
894
1757
  throw new Error("\u7F3A\u5C11 ref \u53C2\u6570");
895
1758
  }
896
1759
  await ensureDaemonRunning();
897
- const parsedRef = parseRef(ref);
1760
+ const parsedRef = parseRef2(ref);
898
1761
  const request = {
899
1762
  id: generateId(),
900
1763
  action: "click",
901
1764
  ref: parsedRef,
902
1765
  tabId: options.tabId
903
1766
  };
904
- const response = await sendCommand(request);
1767
+ const response = await sendCommand2(request);
905
1768
  if (options.json) {
906
1769
  console.log(JSON.stringify(response, null, 2));
907
1770
  } else {
@@ -921,7 +1784,7 @@ async function clickCommand(ref, options = {}) {
921
1784
  }
922
1785
 
923
1786
  // packages/cli/src/commands/hover.ts
924
- function parseRef2(ref) {
1787
+ function parseRef3(ref) {
925
1788
  return ref.startsWith("@") ? ref.slice(1) : ref;
926
1789
  }
927
1790
  async function hoverCommand(ref, options = {}) {
@@ -929,14 +1792,14 @@ async function hoverCommand(ref, options = {}) {
929
1792
  throw new Error("\u7F3A\u5C11 ref \u53C2\u6570");
930
1793
  }
931
1794
  await ensureDaemonRunning();
932
- const parsedRef = parseRef2(ref);
1795
+ const parsedRef = parseRef3(ref);
933
1796
  const request = {
934
1797
  id: generateId(),
935
1798
  action: "hover",
936
1799
  ref: parsedRef,
937
1800
  tabId: options.tabId
938
1801
  };
939
- const response = await sendCommand(request);
1802
+ const response = await sendCommand2(request);
940
1803
  if (options.json) {
941
1804
  console.log(JSON.stringify(response, null, 2));
942
1805
  } else {
@@ -956,7 +1819,7 @@ async function hoverCommand(ref, options = {}) {
956
1819
  }
957
1820
 
958
1821
  // packages/cli/src/commands/fill.ts
959
- function parseRef3(ref) {
1822
+ function parseRef4(ref) {
960
1823
  return ref.startsWith("@") ? ref.slice(1) : ref;
961
1824
  }
962
1825
  async function fillCommand(ref, text, options = {}) {
@@ -967,7 +1830,7 @@ async function fillCommand(ref, text, options = {}) {
967
1830
  throw new Error("\u7F3A\u5C11 text \u53C2\u6570");
968
1831
  }
969
1832
  await ensureDaemonRunning();
970
- const parsedRef = parseRef3(ref);
1833
+ const parsedRef = parseRef4(ref);
971
1834
  const request = {
972
1835
  id: generateId(),
973
1836
  action: "fill",
@@ -975,7 +1838,7 @@ async function fillCommand(ref, text, options = {}) {
975
1838
  text,
976
1839
  tabId: options.tabId
977
1840
  };
978
- const response = await sendCommand(request);
1841
+ const response = await sendCommand2(request);
979
1842
  if (options.json) {
980
1843
  console.log(JSON.stringify(response, null, 2));
981
1844
  } else {
@@ -996,7 +1859,7 @@ async function fillCommand(ref, text, options = {}) {
996
1859
  }
997
1860
 
998
1861
  // packages/cli/src/commands/type.ts
999
- function parseRef4(ref) {
1862
+ function parseRef5(ref) {
1000
1863
  return ref.startsWith("@") ? ref.slice(1) : ref;
1001
1864
  }
1002
1865
  async function typeCommand(ref, text, options = {}) {
@@ -1007,7 +1870,7 @@ async function typeCommand(ref, text, options = {}) {
1007
1870
  throw new Error("\u7F3A\u5C11 text \u53C2\u6570");
1008
1871
  }
1009
1872
  await ensureDaemonRunning();
1010
- const parsedRef = parseRef4(ref);
1873
+ const parsedRef = parseRef5(ref);
1011
1874
  const request = {
1012
1875
  id: generateId(),
1013
1876
  action: "type",
@@ -1015,7 +1878,7 @@ async function typeCommand(ref, text, options = {}) {
1015
1878
  text,
1016
1879
  tabId: options.tabId
1017
1880
  };
1018
- const response = await sendCommand(request);
1881
+ const response = await sendCommand2(request);
1019
1882
  if (options.json) {
1020
1883
  console.log(JSON.stringify(response, null, 2));
1021
1884
  } else {
@@ -1043,7 +1906,7 @@ async function closeCommand(options = {}) {
1043
1906
  action: "close",
1044
1907
  tabId: options.tabId
1045
1908
  };
1046
- const response = await sendCommand(request);
1909
+ const response = await sendCommand2(request);
1047
1910
  if (options.json) {
1048
1911
  console.log(JSON.stringify(response, null, 2));
1049
1912
  } else {
@@ -1062,7 +1925,7 @@ async function closeCommand(options = {}) {
1062
1925
  }
1063
1926
 
1064
1927
  // packages/cli/src/commands/get.ts
1065
- function parseRef5(ref) {
1928
+ function parseRef6(ref) {
1066
1929
  return ref.startsWith("@") ? ref.slice(1) : ref;
1067
1930
  }
1068
1931
  async function getCommand(attribute, ref, options = {}) {
@@ -1074,10 +1937,10 @@ async function getCommand(attribute, ref, options = {}) {
1074
1937
  id: generateId(),
1075
1938
  action: "get",
1076
1939
  attribute,
1077
- ref: ref ? parseRef5(ref) : void 0,
1940
+ ref: ref ? parseRef6(ref) : void 0,
1078
1941
  tabId: options.tabId
1079
1942
  };
1080
- const response = await sendCommand(request);
1943
+ const response = await sendCommand2(request);
1081
1944
  if (options.json) {
1082
1945
  console.log(JSON.stringify(response, null, 2));
1083
1946
  } else {
@@ -1093,17 +1956,17 @@ async function getCommand(attribute, ref, options = {}) {
1093
1956
 
1094
1957
  // packages/cli/src/commands/screenshot.ts
1095
1958
  import fs from "fs";
1096
- import path from "path";
1097
- import os from "os";
1959
+ import path3 from "path";
1960
+ import os2 from "os";
1098
1961
  function getDefaultPath() {
1099
1962
  const timestamp = (/* @__PURE__ */ new Date()).toISOString().replace(/[:.]/g, "-");
1100
1963
  const filename = `bb-screenshot-${timestamp}.png`;
1101
- return path.join(os.tmpdir(), filename);
1964
+ return path3.join(os2.tmpdir(), filename);
1102
1965
  }
1103
1966
  function saveBase64Image(dataUrl, filePath) {
1104
1967
  const base64Data = dataUrl.replace(/^data:image\/png;base64,/, "");
1105
1968
  const buffer = Buffer.from(base64Data, "base64");
1106
- const dir = path.dirname(filePath);
1969
+ const dir = path3.dirname(filePath);
1107
1970
  if (!fs.existsSync(dir)) {
1108
1971
  fs.mkdirSync(dir, { recursive: true });
1109
1972
  }
@@ -1111,13 +1974,13 @@ function saveBase64Image(dataUrl, filePath) {
1111
1974
  }
1112
1975
  async function screenshotCommand(outputPath, options = {}) {
1113
1976
  await ensureDaemonRunning();
1114
- const filePath = outputPath ? path.resolve(outputPath) : getDefaultPath();
1977
+ const filePath = outputPath ? path3.resolve(outputPath) : getDefaultPath();
1115
1978
  const request = {
1116
1979
  id: generateId(),
1117
1980
  action: "screenshot",
1118
1981
  tabId: options.tabId
1119
1982
  };
1120
- const response = await sendCommand(request);
1983
+ const response = await sendCommand2(request);
1121
1984
  if (response.success && response.data?.dataUrl) {
1122
1985
  const dataUrl = response.data.dataUrl;
1123
1986
  saveBase64Image(dataUrl, filePath);
@@ -1144,7 +2007,7 @@ async function screenshotCommand(outputPath, options = {}) {
1144
2007
  function isTimeWait(target) {
1145
2008
  return /^\d+$/.test(target);
1146
2009
  }
1147
- function parseRef6(ref) {
2010
+ function parseRef7(ref) {
1148
2011
  return ref.startsWith("@") ? ref.slice(1) : ref;
1149
2012
  }
1150
2013
  async function waitCommand(target, options = {}) {
@@ -1163,7 +2026,7 @@ async function waitCommand(target, options = {}) {
1163
2026
  tabId: options.tabId
1164
2027
  };
1165
2028
  } else {
1166
- const ref = parseRef6(target);
2029
+ const ref = parseRef7(target);
1167
2030
  request = {
1168
2031
  id: generateId(),
1169
2032
  action: "wait",
@@ -1172,7 +2035,7 @@ async function waitCommand(target, options = {}) {
1172
2035
  tabId: options.tabId
1173
2036
  };
1174
2037
  }
1175
- const response = await sendCommand(request);
2038
+ const response = await sendCommand2(request);
1176
2039
  if (options.json) {
1177
2040
  console.log(JSON.stringify(response, null, 2));
1178
2041
  } else {
@@ -1180,7 +2043,7 @@ async function waitCommand(target, options = {}) {
1180
2043
  if (isTimeWait(target)) {
1181
2044
  console.log(`\u5DF2\u7B49\u5F85 ${target}ms`);
1182
2045
  } else {
1183
- console.log(`\u5143\u7D20 @${parseRef6(target)} \u5DF2\u51FA\u73B0`);
2046
+ console.log(`\u5143\u7D20 @${parseRef7(target)} \u5DF2\u51FA\u73B0`);
1184
2047
  }
1185
2048
  } else {
1186
2049
  console.error(`\u9519\u8BEF: ${response.error}`);
@@ -1220,7 +2083,7 @@ async function pressCommand(keyString, options = {}) {
1220
2083
  modifiers,
1221
2084
  tabId: options.tabId
1222
2085
  };
1223
- const response = await sendCommand(request);
2086
+ const response = await sendCommand2(request);
1224
2087
  if (options.json) {
1225
2088
  console.log(JSON.stringify(response, null, 2));
1226
2089
  } else {
@@ -1261,7 +2124,7 @@ async function scrollCommand(direction, pixels, options = {}) {
1261
2124
  pixels: pixelValue,
1262
2125
  tabId: options.tabId
1263
2126
  };
1264
- const response = await sendCommand(request);
2127
+ const response = await sendCommand2(request);
1265
2128
  if (options.json) {
1266
2129
  console.log(JSON.stringify(response, null, 2));
1267
2130
  } else {
@@ -1275,76 +2138,17 @@ async function scrollCommand(direction, pixels, options = {}) {
1275
2138
  }
1276
2139
 
1277
2140
  // packages/cli/src/commands/daemon.ts
1278
- import { spawn as spawn2 } from "child_process";
1279
- async function daemonCommand(options = {}) {
1280
- if (await isDaemonRunning()) {
1281
- if (options.json) {
1282
- console.log(JSON.stringify({ success: false, error: "Daemon \u5DF2\u5728\u8FD0\u884C" }));
1283
- } else {
1284
- console.log("Daemon \u5DF2\u5728\u8FD0\u884C");
1285
- }
1286
- return;
1287
- }
1288
- const daemonPath = getDaemonPath();
1289
- const args = [daemonPath];
1290
- if (options.host) {
1291
- args.push("--host", options.host);
1292
- }
1293
- if (options.json) {
1294
- console.log(JSON.stringify({ success: true, message: "Daemon \u542F\u52A8\u4E2D..." }));
1295
- } else {
1296
- console.log("Daemon \u542F\u52A8\u4E2D...");
1297
- }
1298
- await new Promise((resolve2, reject) => {
1299
- const child = spawn2(process.execPath, args, {
1300
- stdio: "inherit"
1301
- });
1302
- child.on("exit", (code) => {
1303
- if (code && code !== 0) {
1304
- reject(new Error(`Daemon exited with code ${code}`));
1305
- } else {
1306
- resolve2();
1307
- }
1308
- });
1309
- child.on("error", reject);
1310
- });
1311
- }
1312
- async function stopCommand(options = {}) {
1313
- if (!await isDaemonRunning()) {
1314
- if (options.json) {
1315
- console.log(JSON.stringify({ success: false, error: "Daemon \u672A\u8FD0\u884C" }));
1316
- } else {
1317
- console.log("Daemon \u672A\u8FD0\u884C");
1318
- }
1319
- return;
1320
- }
1321
- const stopped = await stopDaemon();
1322
- if (stopped) {
1323
- if (options.json) {
1324
- console.log(JSON.stringify({ success: true, message: "Daemon \u5DF2\u505C\u6B62" }));
1325
- } else {
1326
- console.log("Daemon \u5DF2\u505C\u6B62");
1327
- }
1328
- } else {
1329
- if (options.json) {
1330
- console.log(JSON.stringify({ success: false, error: "\u65E0\u6CD5\u505C\u6B62 Daemon" }));
1331
- } else {
1332
- console.error("\u65E0\u6CD5\u505C\u6B62 Daemon");
1333
- }
1334
- process.exit(1);
1335
- }
1336
- }
1337
2141
  async function statusCommand(options = {}) {
1338
2142
  const running = await isDaemonRunning();
1339
2143
  if (options.json) {
1340
2144
  console.log(JSON.stringify({ running }));
1341
2145
  } else {
1342
- 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");
1343
2147
  }
1344
2148
  }
1345
2149
 
1346
2150
  // packages/cli/src/commands/reload.ts
1347
- import WebSocket from "ws";
2151
+ import WebSocket2 from "ws";
1348
2152
  var EXTENSION_NAME = "bb-browser";
1349
2153
  async function reloadCommand(options = {}) {
1350
2154
  const port = options.port || 9222;
@@ -1361,7 +2165,7 @@ async function reloadCommand(options = {}) {
1361
2165
  throw new Error("\u8BF7\u5148\u6253\u5F00 chrome://extensions \u9875\u9762");
1362
2166
  }
1363
2167
  const result = await new Promise((resolve2, reject) => {
1364
- const ws = new WebSocket(extPage.webSocketDebuggerUrl);
2168
+ const ws = new WebSocket2(extPage.webSocketDebuggerUrl);
1365
2169
  let resolved = false;
1366
2170
  const timeout = setTimeout(() => {
1367
2171
  if (!resolved) {
@@ -1458,7 +2262,7 @@ async function backCommand(options = {}) {
1458
2262
  action: "back",
1459
2263
  tabId: options.tabId
1460
2264
  };
1461
- const response = await sendCommand(request);
2265
+ const response = await sendCommand2(request);
1462
2266
  if (options.json) {
1463
2267
  console.log(JSON.stringify(response, null, 2));
1464
2268
  } else {
@@ -1482,7 +2286,7 @@ async function forwardCommand(options = {}) {
1482
2286
  action: "forward",
1483
2287
  tabId: options.tabId
1484
2288
  };
1485
- const response = await sendCommand(request);
2289
+ const response = await sendCommand2(request);
1486
2290
  if (options.json) {
1487
2291
  console.log(JSON.stringify(response, null, 2));
1488
2292
  } else {
@@ -1506,7 +2310,7 @@ async function refreshCommand(options = {}) {
1506
2310
  action: "refresh",
1507
2311
  tabId: options.tabId
1508
2312
  };
1509
- const response = await sendCommand(request);
2313
+ const response = await sendCommand2(request);
1510
2314
  if (options.json) {
1511
2315
  console.log(JSON.stringify(response, null, 2));
1512
2316
  } else {
@@ -1525,7 +2329,7 @@ async function refreshCommand(options = {}) {
1525
2329
  }
1526
2330
 
1527
2331
  // packages/cli/src/commands/check.ts
1528
- function parseRef7(ref) {
2332
+ function parseRef8(ref) {
1529
2333
  return ref.startsWith("@") ? ref.slice(1) : ref;
1530
2334
  }
1531
2335
  async function checkCommand(ref, options = {}) {
@@ -1533,14 +2337,14 @@ async function checkCommand(ref, options = {}) {
1533
2337
  throw new Error("\u7F3A\u5C11 ref \u53C2\u6570");
1534
2338
  }
1535
2339
  await ensureDaemonRunning();
1536
- const parsedRef = parseRef7(ref);
2340
+ const parsedRef = parseRef8(ref);
1537
2341
  const request = {
1538
2342
  id: generateId(),
1539
2343
  action: "check",
1540
2344
  ref: parsedRef,
1541
2345
  tabId: options.tabId
1542
2346
  };
1543
- const response = await sendCommand(request);
2347
+ const response = await sendCommand2(request);
1544
2348
  if (options.json) {
1545
2349
  console.log(JSON.stringify(response, null, 2));
1546
2350
  } else {
@@ -1572,14 +2376,14 @@ async function uncheckCommand(ref, options = {}) {
1572
2376
  throw new Error("\u7F3A\u5C11 ref \u53C2\u6570");
1573
2377
  }
1574
2378
  await ensureDaemonRunning();
1575
- const parsedRef = parseRef7(ref);
2379
+ const parsedRef = parseRef8(ref);
1576
2380
  const request = {
1577
2381
  id: generateId(),
1578
2382
  action: "uncheck",
1579
2383
  ref: parsedRef,
1580
2384
  tabId: options.tabId
1581
2385
  };
1582
- const response = await sendCommand(request);
2386
+ const response = await sendCommand2(request);
1583
2387
  if (options.json) {
1584
2388
  console.log(JSON.stringify(response, null, 2));
1585
2389
  } else {
@@ -1608,7 +2412,7 @@ async function uncheckCommand(ref, options = {}) {
1608
2412
  }
1609
2413
 
1610
2414
  // packages/cli/src/commands/select.ts
1611
- function parseRef8(ref) {
2415
+ function parseRef9(ref) {
1612
2416
  return ref.startsWith("@") ? ref.slice(1) : ref;
1613
2417
  }
1614
2418
  async function selectCommand(ref, value, options = {}) {
@@ -1619,7 +2423,7 @@ async function selectCommand(ref, value, options = {}) {
1619
2423
  throw new Error("\u7F3A\u5C11 value \u53C2\u6570");
1620
2424
  }
1621
2425
  await ensureDaemonRunning();
1622
- const parsedRef = parseRef8(ref);
2426
+ const parsedRef = parseRef9(ref);
1623
2427
  const request = {
1624
2428
  id: generateId(),
1625
2429
  action: "select",
@@ -1627,7 +2431,7 @@ async function selectCommand(ref, value, options = {}) {
1627
2431
  value,
1628
2432
  tabId: options.tabId
1629
2433
  };
1630
- const response = await sendCommand(request);
2434
+ const response = await sendCommand2(request);
1631
2435
  if (options.json) {
1632
2436
  console.log(JSON.stringify(response, null, 2));
1633
2437
  } else {
@@ -1665,7 +2469,7 @@ async function evalCommand(script, options = {}) {
1665
2469
  script,
1666
2470
  tabId: options.tabId
1667
2471
  };
1668
- const response = await sendCommand(request);
2472
+ const response = await sendCommand2(request);
1669
2473
  if (options.json) {
1670
2474
  console.log(JSON.stringify(response, null, 2));
1671
2475
  } else {
@@ -1752,7 +2556,7 @@ async function tabCommand(args, options = {}) {
1752
2556
  index: parsed.index,
1753
2557
  tabId: parsed.tabId
1754
2558
  };
1755
- const response = await sendCommand(request);
2559
+ const response = await sendCommand2(request);
1756
2560
  if (options.json) {
1757
2561
  console.log(JSON.stringify(response, null, 2));
1758
2562
  } else {
@@ -1801,7 +2605,7 @@ async function frameCommand(selector, options = {}) {
1801
2605
  selector,
1802
2606
  tabId: options.tabId
1803
2607
  };
1804
- const response = await sendCommand(request);
2608
+ const response = await sendCommand2(request);
1805
2609
  if (options.json) {
1806
2610
  console.log(JSON.stringify(response, null, 2));
1807
2611
  } else {
@@ -1825,7 +2629,7 @@ async function frameMainCommand(options = {}) {
1825
2629
  action: "frame_main",
1826
2630
  tabId: options.tabId
1827
2631
  };
1828
- const response = await sendCommand(request);
2632
+ const response = await sendCommand2(request);
1829
2633
  if (options.json) {
1830
2634
  console.log(JSON.stringify(response, null, 2));
1831
2635
  } else {
@@ -1851,7 +2655,7 @@ async function dialogCommand(subCommand, promptText, options = {}) {
1851
2655
  promptText: subCommand === "accept" ? promptText : void 0,
1852
2656
  tabId: options.tabId
1853
2657
  };
1854
- const response = await sendCommand(request);
2658
+ const response = await sendCommand2(request);
1855
2659
  if (options.json) {
1856
2660
  console.log(JSON.stringify(response, null, 2));
1857
2661
  } else {
@@ -1872,7 +2676,7 @@ async function dialogCommand(subCommand, promptText, options = {}) {
1872
2676
 
1873
2677
  // packages/cli/src/commands/network.ts
1874
2678
  async function networkCommand(subCommand, urlOrFilter, options = {}) {
1875
- const response = await sendCommand({
2679
+ const response = await sendCommand2({
1876
2680
  id: crypto.randomUUID(),
1877
2681
  action: "network",
1878
2682
  networkCommand: subCommand,
@@ -1959,7 +2763,7 @@ async function networkCommand(subCommand, urlOrFilter, options = {}) {
1959
2763
 
1960
2764
  // packages/cli/src/commands/console.ts
1961
2765
  async function consoleCommand(options = {}) {
1962
- const response = await sendCommand({
2766
+ const response = await sendCommand2({
1963
2767
  id: crypto.randomUUID(),
1964
2768
  action: "console",
1965
2769
  consoleCommand: options.clear ? "clear" : "get",
@@ -2004,7 +2808,7 @@ async function consoleCommand(options = {}) {
2004
2808
 
2005
2809
  // packages/cli/src/commands/errors.ts
2006
2810
  async function errorsCommand(options = {}) {
2007
- const response = await sendCommand({
2811
+ const response = await sendCommand2({
2008
2812
  id: crypto.randomUUID(),
2009
2813
  action: "errors",
2010
2814
  errorsCommand: options.clear ? "clear" : "get",
@@ -2044,7 +2848,7 @@ async function errorsCommand(options = {}) {
2044
2848
 
2045
2849
  // packages/cli/src/commands/trace.ts
2046
2850
  async function traceCommand(subCommand, options = {}) {
2047
- const response = await sendCommand({
2851
+ const response = await sendCommand2({
2048
2852
  id: crypto.randomUUID(),
2049
2853
  action: "trace",
2050
2854
  traceCommand: subCommand,
@@ -2134,7 +2938,7 @@ function matchTabOrigin2(tabUrl, targetHostname) {
2134
2938
  }
2135
2939
  async function ensureTabForOrigin(origin, hostname) {
2136
2940
  const listReq = { id: generateId(), action: "tab_list" };
2137
- const listResp = await sendCommand(listReq);
2941
+ const listResp = await sendCommand2(listReq);
2138
2942
  if (listResp.success && listResp.data?.tabs) {
2139
2943
  const matchingTab = listResp.data.tabs.find(
2140
2944
  (tab) => matchTabOrigin2(tab.url, hostname)
@@ -2143,7 +2947,7 @@ async function ensureTabForOrigin(origin, hostname) {
2143
2947
  return matchingTab.tabId;
2144
2948
  }
2145
2949
  }
2146
- const newResp = await sendCommand({ id: generateId(), action: "tab_new", url: origin });
2950
+ const newResp = await sendCommand2({ id: generateId(), action: "tab_new", url: origin });
2147
2951
  if (!newResp.success) {
2148
2952
  throw new Error(`\u65E0\u6CD5\u6253\u5F00 ${origin}: ${newResp.error}`);
2149
2953
  }
@@ -2212,7 +3016,7 @@ async function fetchCommand(url, options = {}) {
2212
3016
  }
2213
3017
  const script = buildFetchScript(url, options);
2214
3018
  const evalReq = { id: generateId(), action: "eval", script, tabId: targetTabId };
2215
- const evalResp = await sendCommand(evalReq);
3019
+ const evalResp = await sendCommand2(evalReq);
2216
3020
  if (!evalResp.success) {
2217
3021
  throw new Error(`Fetch \u5931\u8D25: ${evalResp.error}`);
2218
3022
  }
@@ -2246,21 +3050,16 @@ async function fetchCommand(url, options = {}) {
2246
3050
 
2247
3051
  // packages/cli/src/commands/history.ts
2248
3052
  async function historyCommand(subCommand, options = {}) {
2249
- const response = await sendCommand({
2250
- id: crypto.randomUUID(),
2251
- action: "history",
2252
- historyCommand: subCommand,
2253
- text: options.query,
2254
- ms: options.days
2255
- });
3053
+ const days = options.days || 30;
3054
+ const data = subCommand === "search" ? { historyItems: searchHistory(options.query, days) } : { historyDomains: getHistoryDomains(days) };
2256
3055
  if (options.json) {
2257
- console.log(JSON.stringify(response));
3056
+ console.log(JSON.stringify({
3057
+ id: crypto.randomUUID(),
3058
+ success: true,
3059
+ data
3060
+ }));
2258
3061
  return;
2259
3062
  }
2260
- if (!response.success) {
2261
- throw new Error(response.error || "History command failed");
2262
- }
2263
- const data = response.data;
2264
3063
  switch (subCommand) {
2265
3064
  case "search": {
2266
3065
  const items = data?.historyItems || [];
@@ -2299,10 +3098,13 @@ async function historyCommand(subCommand, options = {}) {
2299
3098
  }
2300
3099
 
2301
3100
  // packages/cli/src/index.ts
2302
- var VERSION = "0.3.0";
3101
+ var VERSION = "0.8.0";
2303
3102
  var HELP_TEXT = `
2304
3103
  bb-browser - AI Agent \u6D4F\u89C8\u5668\u81EA\u52A8\u5316\u5DE5\u5177
2305
3104
 
3105
+ \u5B89\u88C5\uFF1A
3106
+ npm install -g bb-browser
3107
+
2306
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
2307
3109
  bb-browser site list \u67E5\u770B\u6240\u6709\u53EF\u7528\u547D\u4EE4
2308
3110
  bb-browser site twitter/search "AI" \u793A\u4F8B\uFF1A\u641C\u7D22\u63A8\u6587
@@ -2318,6 +3120,7 @@ bb-browser - AI Agent \u6D4F\u89C8\u5668\u81EA\u52A8\u5316\u5DE5\u5177
2318
3120
  site <name> [args] \u8FD0\u884C adapter
2319
3121
  site update \u66F4\u65B0\u793E\u533A adapter \u5E93
2320
3122
  guide \u5982\u4F55\u628A\u4EFB\u4F55\u7F51\u7AD9\u53D8\u6210 adapter
3123
+ star \u2B50 Star bb-browser on GitHub
2321
3124
 
2322
3125
  \u6D4F\u89C8\u5668\u64CD\u4F5C\uFF1A
2323
3126
  open <url> [--tab] \u6253\u5F00 URL
@@ -2339,6 +3142,7 @@ bb-browser - AI Agent \u6D4F\u89C8\u5668\u81EA\u52A8\u5316\u5DE5\u5177
2339
3142
 
2340
3143
  \u6807\u7B7E\u9875\uFF1A
2341
3144
  tab [list|new|close|<n>] \u7BA1\u7406\u6807\u7B7E\u9875
3145
+ status \u67E5\u770B\u53D7\u7BA1\u6D4F\u89C8\u5668\u72B6\u6001
2342
3146
 
2343
3147
  \u5BFC\u822A\uFF1A
2344
3148
  back / forward / refresh \u540E\u9000 / \u524D\u8FDB / \u5237\u65B0
@@ -2352,6 +3156,8 @@ bb-browser - AI Agent \u6D4F\u89C8\u5668\u81EA\u52A8\u5316\u5DE5\u5177
2352
3156
 
2353
3157
  \u9009\u9879\uFF1A
2354
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
2355
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
2356
3162
  -i, --interactive \u53EA\u8F93\u51FA\u53EF\u4EA4\u4E92\u5143\u7D20\uFF08snapshot \u547D\u4EE4\uFF09
2357
3163
  -c, --compact \u79FB\u9664\u7A7A\u7ED3\u6784\u8282\u70B9\uFF08snapshot \u547D\u4EE4\uFF09
@@ -2392,6 +3198,12 @@ function parseArgs(argv) {
2392
3198
  }
2393
3199
  } else if (arg === "--openclaw") {
2394
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
+ }
2395
3207
  } else if (arg === "--help" || arg === "-h") {
2396
3208
  result.flags.help = true;
2397
3209
  } else if (arg === "--version" || arg === "-v") {
@@ -2441,9 +3253,9 @@ async function main() {
2441
3253
  return;
2442
3254
  }
2443
3255
  if (process.argv.includes("--mcp")) {
2444
- const mcpPath = new URL("./mcp.js", import.meta.url).pathname;
2445
- const { spawn: spawn3 } = await import("child_process");
2446
- 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" });
2447
3259
  child.on("exit", (code) => process.exit(code ?? 0));
2448
3260
  return;
2449
3261
  }
@@ -2800,6 +3612,27 @@ async function main() {
2800
3612
  });
2801
3613
  break;
2802
3614
  }
3615
+ case "star": {
3616
+ const { execSync: execSync4 } = await import("child_process");
3617
+ try {
3618
+ execSync4("gh auth status", { stdio: "pipe" });
3619
+ } catch {
3620
+ console.error("\u9700\u8981\u5148\u5B89\u88C5\u5E76\u767B\u5F55 GitHub CLI: https://cli.github.com");
3621
+ console.error(" brew install gh && gh auth login");
3622
+ process.exit(1);
3623
+ }
3624
+ const repos = ["epiral/bb-browser", "epiral/bb-sites"];
3625
+ for (const repo of repos) {
3626
+ try {
3627
+ execSync4(`gh api user/starred/${repo} -X PUT`, { stdio: "pipe" });
3628
+ console.log(`\u2B50 Starred ${repo}`);
3629
+ } catch {
3630
+ console.log(`Already starred or failed: ${repo}`);
3631
+ }
3632
+ }
3633
+ console.log("\nThanks for your support! \u{1F64F}");
3634
+ break;
3635
+ }
2803
3636
  case "guide": {
2804
3637
  console.log(`How to turn any website into a bb-browser site adapter
2805
3638
  =======================================================