@nntoan/bb-browser 0.13.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 ADDED
@@ -0,0 +1,4228 @@
1
+ #!/usr/bin/env node
2
+ import {
3
+ COMMAND_TIMEOUT,
4
+ generateId
5
+ } from "./chunk-XYKHDJST.js";
6
+ import {
7
+ applyJq
8
+ } from "./chunk-GG6E44JO.js";
9
+ import {
10
+ parseOpenClawJson
11
+ } from "./chunk-FSL4RNI6.js";
12
+ import "./chunk-D4HDZEJT.js";
13
+
14
+ // packages/cli/src/index.ts
15
+ import { fileURLToPath as fileURLToPath4 } from "url";
16
+
17
+ // packages/cli/src/cdp-client.ts
18
+ import { readFileSync, unlinkSync, writeFileSync } from "fs";
19
+ import { request as httpRequest } from "http";
20
+ import { request as httpsRequest } from "https";
21
+ import os2 from "os";
22
+ import path2 from "path";
23
+ import { fileURLToPath } from "url";
24
+ import WebSocket from "ws";
25
+
26
+ // packages/cli/src/cdp-discovery.ts
27
+ import { execFile, execSync, spawn } from "child_process";
28
+ import { existsSync } from "fs";
29
+ import { mkdir, readFile, writeFile } from "fs/promises";
30
+ import os from "os";
31
+ import path from "path";
32
+ var DEFAULT_CDP_PORT = 19825;
33
+ var MANAGED_BROWSER_DIR = path.join(os.homedir(), ".bb-browser", "browser");
34
+ var MANAGED_USER_DATA_DIR = path.join(MANAGED_BROWSER_DIR, "user-data");
35
+ var MANAGED_PORT_FILE = path.join(MANAGED_BROWSER_DIR, "cdp-port");
36
+ function execFileAsync(command, args, timeout) {
37
+ return new Promise((resolve3, reject) => {
38
+ execFile(command, args, { encoding: "utf8", timeout }, (error, stdout) => {
39
+ if (error) {
40
+ reject(error);
41
+ return;
42
+ }
43
+ resolve3(stdout.trim());
44
+ });
45
+ });
46
+ }
47
+ function getArgValue(flag) {
48
+ const index = process.argv.indexOf(flag);
49
+ if (index < 0) return void 0;
50
+ return process.argv[index + 1];
51
+ }
52
+ async function tryOpenClaw() {
53
+ try {
54
+ const raw = await execFileAsync("npx", ["openclaw", "browser", "status", "--json"], 5e3);
55
+ const parsed = parseOpenClawJson(raw);
56
+ const port = Number(parsed?.cdpPort);
57
+ if (Number.isInteger(port) && port > 0) {
58
+ return { host: "127.0.0.1", port };
59
+ }
60
+ } catch {
61
+ }
62
+ return null;
63
+ }
64
+ async function canConnect(host, port) {
65
+ try {
66
+ const controller = new AbortController();
67
+ const timeout = setTimeout(() => controller.abort(), 1200);
68
+ const response = await fetch(`http://${host}:${port}/json/version`, { signal: controller.signal });
69
+ clearTimeout(timeout);
70
+ return response.ok;
71
+ } catch {
72
+ return false;
73
+ }
74
+ }
75
+ function findBrowserExecutable() {
76
+ if (process.platform === "darwin") {
77
+ const candidates = [
78
+ "/Applications/Google Chrome.app/Contents/MacOS/Google Chrome",
79
+ "/Applications/Google Chrome Dev.app/Contents/MacOS/Google Chrome Dev",
80
+ "/Applications/Google Chrome Canary.app/Contents/MacOS/Google Chrome Canary",
81
+ "/Applications/Google Chrome Beta.app/Contents/MacOS/Google Chrome Beta",
82
+ "/Applications/Arc.app/Contents/MacOS/Arc",
83
+ "/Applications/Microsoft Edge.app/Contents/MacOS/Microsoft Edge",
84
+ "/Applications/Brave Browser.app/Contents/MacOS/Brave Browser"
85
+ ];
86
+ return candidates.find((candidate) => existsSync(candidate)) ?? null;
87
+ }
88
+ if (process.platform === "linux") {
89
+ const candidates = ["google-chrome", "google-chrome-stable", "chromium-browser", "chromium"];
90
+ for (const candidate of candidates) {
91
+ try {
92
+ const resolved = execSync(`which ${candidate}`, { encoding: "utf8", stdio: ["ignore", "pipe", "ignore"] }).trim();
93
+ if (resolved) {
94
+ return resolved;
95
+ }
96
+ } catch {
97
+ }
98
+ }
99
+ return null;
100
+ }
101
+ if (process.platform === "win32") {
102
+ const localAppData = process.env.LOCALAPPDATA ?? "";
103
+ const candidates = [
104
+ "C:\\Program Files\\Google\\Chrome\\Application\\chrome.exe",
105
+ "C:\\Program Files (x86)\\Google\\Chrome\\Application\\chrome.exe",
106
+ ...localAppData ? [
107
+ `${localAppData}\\Google\\Chrome Dev\\Application\\chrome.exe`,
108
+ `${localAppData}\\Google\\Chrome SxS\\Application\\chrome.exe`,
109
+ `${localAppData}\\Google\\Chrome Beta\\Application\\chrome.exe`
110
+ ] : [],
111
+ "C:\\Program Files (x86)\\Microsoft\\Edge\\Application\\msedge.exe",
112
+ "C:\\Program Files\\Microsoft\\Edge\\Application\\msedge.exe",
113
+ "C:\\Program Files\\BraveSoftware\\Brave-Browser\\Application\\brave.exe"
114
+ ];
115
+ return candidates.find((candidate) => existsSync(candidate)) ?? null;
116
+ }
117
+ return null;
118
+ }
119
+ async function isManagedBrowserRunning() {
120
+ try {
121
+ const rawPort = await readFile(MANAGED_PORT_FILE, "utf8");
122
+ const port = Number.parseInt(rawPort.trim(), 10);
123
+ if (!Number.isInteger(port) || port <= 0) {
124
+ return false;
125
+ }
126
+ return await canConnect("127.0.0.1", port);
127
+ } catch {
128
+ return false;
129
+ }
130
+ }
131
+ async function launchManagedBrowser(port = DEFAULT_CDP_PORT) {
132
+ const executable = findBrowserExecutable();
133
+ if (!executable) {
134
+ return null;
135
+ }
136
+ await mkdir(MANAGED_USER_DATA_DIR, { recursive: true });
137
+ const defaultProfileDir = path.join(MANAGED_USER_DATA_DIR, "Default");
138
+ const prefsPath = path.join(defaultProfileDir, "Preferences");
139
+ await mkdir(defaultProfileDir, { recursive: true });
140
+ try {
141
+ let prefs = {};
142
+ try {
143
+ prefs = JSON.parse(await readFile(prefsPath, "utf8"));
144
+ } catch {
145
+ }
146
+ if (!prefs.profile?.name || prefs.profile.name !== "bb-browser") {
147
+ prefs.profile = { ...prefs.profile || {}, name: "bb-browser" };
148
+ await writeFile(prefsPath, JSON.stringify(prefs), "utf8");
149
+ }
150
+ } catch {
151
+ }
152
+ const args = [
153
+ `--remote-debugging-port=${port}`,
154
+ `--user-data-dir=${MANAGED_USER_DATA_DIR}`,
155
+ "--no-first-run",
156
+ "--no-default-browser-check",
157
+ "--disable-sync",
158
+ "--disable-background-networking",
159
+ "--disable-component-update",
160
+ "--disable-features=Translate,MediaRouter",
161
+ "--disable-session-crashed-bubble",
162
+ "--hide-crash-restore-bubble",
163
+ "about:blank"
164
+ ];
165
+ try {
166
+ const child = spawn(executable, args, {
167
+ detached: true,
168
+ stdio: "ignore"
169
+ });
170
+ child.unref();
171
+ } catch {
172
+ return null;
173
+ }
174
+ await mkdir(MANAGED_BROWSER_DIR, { recursive: true });
175
+ await writeFile(MANAGED_PORT_FILE, String(port), "utf8");
176
+ const deadline = Date.now() + 8e3;
177
+ while (Date.now() < deadline) {
178
+ if (await canConnect("127.0.0.1", port)) {
179
+ return { host: "127.0.0.1", port };
180
+ }
181
+ await new Promise((resolve3) => setTimeout(resolve3, 250));
182
+ }
183
+ return null;
184
+ }
185
+ async function discoverCdpPort() {
186
+ const explicitPort = Number.parseInt(getArgValue("--port") ?? "", 10);
187
+ if (Number.isInteger(explicitPort) && explicitPort > 0 && await canConnect("127.0.0.1", explicitPort)) {
188
+ return { host: "127.0.0.1", port: explicitPort };
189
+ }
190
+ try {
191
+ const rawPort = await readFile(MANAGED_PORT_FILE, "utf8");
192
+ const managedPort = Number.parseInt(rawPort.trim(), 10);
193
+ if (Number.isInteger(managedPort) && managedPort > 0 && await canConnect("127.0.0.1", managedPort)) {
194
+ return { host: "127.0.0.1", port: managedPort };
195
+ }
196
+ } catch {
197
+ }
198
+ if (process.argv.includes("--openclaw")) {
199
+ const viaOpenClaw = await tryOpenClaw();
200
+ if (viaOpenClaw && await canConnect(viaOpenClaw.host, viaOpenClaw.port)) {
201
+ return viaOpenClaw;
202
+ }
203
+ }
204
+ const launched = await launchManagedBrowser();
205
+ if (launched) {
206
+ return launched;
207
+ }
208
+ if (!process.argv.includes("--openclaw")) {
209
+ const detectedOpenClaw = await tryOpenClaw();
210
+ if (detectedOpenClaw && await canConnect(detectedOpenClaw.host, detectedOpenClaw.port)) {
211
+ return detectedOpenClaw;
212
+ }
213
+ }
214
+ return null;
215
+ }
216
+
217
+ // packages/cli/src/cdp-client.ts
218
+ var connectionState = null;
219
+ var reconnecting = null;
220
+ var networkRequests = /* @__PURE__ */ new Map();
221
+ var networkEnabled = false;
222
+ var consoleMessages = [];
223
+ var consoleEnabled = false;
224
+ var jsErrors = [];
225
+ var errorsEnabled = false;
226
+ var traceRecording = false;
227
+ var traceEvents = [];
228
+ function getContextFilePath(host, port) {
229
+ const safeHost = host.replace(/[^a-zA-Z0-9_.-]/g, "_");
230
+ return path2.join(os2.tmpdir(), `bb-browser-cdp-context-${safeHost}-${port}.json`);
231
+ }
232
+ function loadPersistedCurrentTargetId(host, port) {
233
+ try {
234
+ const data = JSON.parse(readFileSync(getContextFilePath(host, port), "utf-8"));
235
+ return typeof data.currentTargetId === "string" && data.currentTargetId ? data.currentTargetId : void 0;
236
+ } catch {
237
+ return void 0;
238
+ }
239
+ }
240
+ function persistCurrentTargetId(host, port, currentTargetId) {
241
+ try {
242
+ writeFileSync(getContextFilePath(host, port), JSON.stringify({ currentTargetId }));
243
+ } catch {
244
+ }
245
+ }
246
+ function setCurrentTargetId(targetId) {
247
+ const state = connectionState;
248
+ if (!state) return;
249
+ state.currentTargetId = targetId;
250
+ persistCurrentTargetId(state.host, state.port, targetId);
251
+ }
252
+ function buildRequestError(error) {
253
+ return error instanceof Error ? error : new Error(String(error));
254
+ }
255
+ function fetchJson(url) {
256
+ return new Promise((resolve3, reject) => {
257
+ const requester = url.startsWith("https:") ? httpsRequest : httpRequest;
258
+ const req = requester(url, { method: "GET" }, (res) => {
259
+ const chunks = [];
260
+ res.on("data", (chunk) => chunks.push(Buffer.from(chunk)));
261
+ res.on("end", () => {
262
+ const raw = Buffer.concat(chunks).toString("utf8");
263
+ if ((res.statusCode ?? 500) >= 400) {
264
+ reject(new Error(`HTTP ${res.statusCode ?? 500}: ${raw}`));
265
+ return;
266
+ }
267
+ try {
268
+ resolve3(JSON.parse(raw));
269
+ } catch (error) {
270
+ reject(error);
271
+ }
272
+ });
273
+ });
274
+ req.on("error", reject);
275
+ req.end();
276
+ });
277
+ }
278
+ async function getJsonList(host, port) {
279
+ const data = await fetchJson(`http://${host}:${port}/json/list`);
280
+ return Array.isArray(data) ? data : [];
281
+ }
282
+ async function getJsonVersion(host, port) {
283
+ const data = await fetchJson(`http://${host}:${port}/json/version`);
284
+ const url = data.webSocketDebuggerUrl;
285
+ if (typeof url !== "string" || !url) {
286
+ throw new Error("CDP endpoint missing webSocketDebuggerUrl");
287
+ }
288
+ return { webSocketDebuggerUrl: url };
289
+ }
290
+ function connectWebSocket(url) {
291
+ return new Promise((resolve3, reject) => {
292
+ const ws = new WebSocket(url);
293
+ ws.once("open", () => {
294
+ const socket = ws._socket;
295
+ if (socket && typeof socket.unref === "function") {
296
+ socket.unref();
297
+ }
298
+ resolve3(ws);
299
+ });
300
+ ws.once("error", reject);
301
+ });
302
+ }
303
+ function createState(host, port, browserWsUrl, browserSocket) {
304
+ const state = {
305
+ host,
306
+ port,
307
+ browserWsUrl,
308
+ browserSocket,
309
+ browserPending: /* @__PURE__ */ new Map(),
310
+ nextMessageId: 1,
311
+ sessions: /* @__PURE__ */ new Map(),
312
+ attachedTargets: /* @__PURE__ */ new Map(),
313
+ refsByTarget: /* @__PURE__ */ new Map(),
314
+ currentTargetId: loadPersistedCurrentTargetId(host, port),
315
+ activeFrameIdByTarget: /* @__PURE__ */ new Map(),
316
+ dialogHandlers: /* @__PURE__ */ new Map()
317
+ };
318
+ browserSocket.on("message", (raw) => {
319
+ const message = JSON.parse(raw.toString());
320
+ if (typeof message.id === "number") {
321
+ const pending = state.browserPending.get(message.id);
322
+ if (!pending) return;
323
+ state.browserPending.delete(message.id);
324
+ if (message.error) {
325
+ pending.reject(new Error(`${pending.method}: ${message.error.message ?? "Unknown CDP error"}`));
326
+ } else {
327
+ pending.resolve(message.result);
328
+ }
329
+ return;
330
+ }
331
+ if (message.method === "Target.attachedToTarget") {
332
+ const params = message.params;
333
+ const sessionId = params.sessionId;
334
+ const targetInfo = params.targetInfo;
335
+ if (typeof sessionId === "string" && typeof targetInfo?.targetId === "string") {
336
+ state.sessions.set(targetInfo.targetId, sessionId);
337
+ state.attachedTargets.set(sessionId, targetInfo.targetId);
338
+ }
339
+ return;
340
+ }
341
+ if (message.method === "Target.detachedFromTarget") {
342
+ const params = message.params;
343
+ const sessionId = params.sessionId;
344
+ if (typeof sessionId === "string") {
345
+ const targetId = state.attachedTargets.get(sessionId);
346
+ if (targetId) {
347
+ state.sessions.delete(targetId);
348
+ state.attachedTargets.delete(sessionId);
349
+ state.activeFrameIdByTarget.delete(targetId);
350
+ state.dialogHandlers.delete(targetId);
351
+ if (state.currentTargetId === targetId) {
352
+ state.currentTargetId = void 0;
353
+ persistCurrentTargetId(state.host, state.port, void 0);
354
+ }
355
+ }
356
+ }
357
+ return;
358
+ }
359
+ if (message.method === "Target.receivedMessageFromTarget") {
360
+ const params = message.params;
361
+ const sessionId = params.sessionId;
362
+ const messageText = params.message;
363
+ if (typeof sessionId === "string" && typeof messageText === "string") {
364
+ const targetId = state.attachedTargets.get(sessionId);
365
+ if (targetId) {
366
+ handleSessionEvent(targetId, JSON.parse(messageText)).catch(() => {
367
+ });
368
+ }
369
+ }
370
+ return;
371
+ }
372
+ if (typeof message.sessionId === "string" && typeof message.method === "string") {
373
+ const targetId = state.attachedTargets.get(message.sessionId);
374
+ if (targetId) {
375
+ handleSessionEvent(targetId, message).catch(() => {
376
+ });
377
+ }
378
+ }
379
+ });
380
+ browserSocket.on("close", () => {
381
+ if (connectionState === state) {
382
+ connectionState = null;
383
+ }
384
+ for (const pending of state.browserPending.values()) {
385
+ pending.reject(new Error("CDP connection closed"));
386
+ }
387
+ state.browserPending.clear();
388
+ });
389
+ browserSocket.on("error", () => {
390
+ });
391
+ return state;
392
+ }
393
+ async function browserCommand(method, params = {}) {
394
+ const state = connectionState;
395
+ if (!state) throw new Error("CDP connection not initialized");
396
+ const id = state.nextMessageId++;
397
+ const payload = JSON.stringify({ id, method, params });
398
+ const promise = new Promise((resolve3, reject) => {
399
+ state.browserPending.set(id, { resolve: resolve3, reject, method });
400
+ });
401
+ state.browserSocket.send(payload);
402
+ return promise;
403
+ }
404
+ async function sessionCommand(targetId, method, params = {}) {
405
+ const state = connectionState;
406
+ if (!state) throw new Error("CDP connection not initialized");
407
+ const sessionId = state.sessions.get(targetId) ?? await attachTarget(targetId);
408
+ const id = state.nextMessageId++;
409
+ const payload = JSON.stringify({ id, method, params, sessionId });
410
+ return new Promise((resolve3, reject) => {
411
+ const check = (raw) => {
412
+ const msg = JSON.parse(raw.toString());
413
+ if (msg.id === id && msg.sessionId === sessionId) {
414
+ state.browserSocket.off("message", check);
415
+ if (msg.error) reject(new Error(`${method}: ${msg.error.message ?? "Unknown CDP error"}`));
416
+ else resolve3(msg.result);
417
+ }
418
+ };
419
+ state.browserSocket.on("message", check);
420
+ state.browserSocket.send(payload);
421
+ });
422
+ }
423
+ function getActiveFrameId(targetId) {
424
+ const frameId = connectionState?.activeFrameIdByTarget.get(targetId);
425
+ return frameId ?? void 0;
426
+ }
427
+ async function pageCommand(targetId, method, params = {}) {
428
+ const frameId = getActiveFrameId(targetId);
429
+ return sessionCommand(targetId, method, frameId ? { ...params, frameId } : params);
430
+ }
431
+ function normalizeHeaders(headers) {
432
+ if (!headers || typeof headers !== "object") return void 0;
433
+ return Object.fromEntries(Object.entries(headers).map(([key, value]) => [key, String(value)]));
434
+ }
435
+ async function handleSessionEvent(targetId, event) {
436
+ const method = event.method;
437
+ const params = event.params ?? {};
438
+ if (typeof method !== "string") return;
439
+ if (method === "Page.javascriptDialogOpening") {
440
+ const handler = connectionState?.dialogHandlers.get(targetId);
441
+ if (handler) {
442
+ await sessionCommand(targetId, "Page.handleJavaScriptDialog", {
443
+ accept: handler.accept,
444
+ ...handler.promptText !== void 0 ? { promptText: handler.promptText } : {}
445
+ });
446
+ }
447
+ return;
448
+ }
449
+ if (method === "Network.requestWillBeSent") {
450
+ const requestId = typeof params.requestId === "string" ? params.requestId : void 0;
451
+ const request = params.request;
452
+ if (!requestId || !request) return;
453
+ networkRequests.set(requestId, {
454
+ requestId,
455
+ url: String(request.url ?? ""),
456
+ method: String(request.method ?? "GET"),
457
+ type: String(params.type ?? "Other"),
458
+ timestamp: Math.round(Number(params.timestamp ?? Date.now()) * 1e3),
459
+ requestHeaders: normalizeHeaders(request.headers),
460
+ requestBody: typeof request.postData === "string" ? request.postData : void 0
461
+ });
462
+ return;
463
+ }
464
+ if (method === "Network.responseReceived") {
465
+ const requestId = typeof params.requestId === "string" ? params.requestId : void 0;
466
+ const response = params.response;
467
+ if (!requestId || !response) return;
468
+ const existing = networkRequests.get(requestId);
469
+ if (!existing) return;
470
+ existing.status = typeof response.status === "number" ? response.status : void 0;
471
+ existing.statusText = typeof response.statusText === "string" ? response.statusText : void 0;
472
+ existing.responseHeaders = normalizeHeaders(response.headers);
473
+ existing.mimeType = typeof response.mimeType === "string" ? response.mimeType : void 0;
474
+ networkRequests.set(requestId, existing);
475
+ return;
476
+ }
477
+ if (method === "Network.loadingFailed") {
478
+ const requestId = typeof params.requestId === "string" ? params.requestId : void 0;
479
+ if (!requestId) return;
480
+ const existing = networkRequests.get(requestId);
481
+ if (!existing) return;
482
+ existing.failed = true;
483
+ existing.failureReason = typeof params.errorText === "string" ? params.errorText : "Unknown error";
484
+ networkRequests.set(requestId, existing);
485
+ return;
486
+ }
487
+ if (method === "Runtime.consoleAPICalled") {
488
+ const type = String(params.type ?? "log");
489
+ const args = Array.isArray(params.args) ? params.args : [];
490
+ const text = args.map((arg) => {
491
+ if (typeof arg.value === "string") return arg.value;
492
+ if (arg.value !== void 0) return String(arg.value);
493
+ if (typeof arg.description === "string") return arg.description;
494
+ return "";
495
+ }).filter(Boolean).join(" ");
496
+ const stack = params.stackTrace;
497
+ const firstCallFrame = Array.isArray(stack?.callFrames) ? stack?.callFrames[0] : void 0;
498
+ consoleMessages.push({
499
+ type: ["log", "info", "warn", "error", "debug"].includes(type) ? type : "log",
500
+ text,
501
+ timestamp: Math.round(Number(params.timestamp ?? Date.now())),
502
+ url: typeof firstCallFrame?.url === "string" ? firstCallFrame.url : void 0,
503
+ lineNumber: typeof firstCallFrame?.lineNumber === "number" ? firstCallFrame.lineNumber : void 0
504
+ });
505
+ return;
506
+ }
507
+ if (method === "Runtime.exceptionThrown") {
508
+ const details = params.exceptionDetails;
509
+ if (!details) return;
510
+ const exception = details.exception;
511
+ const stackTrace = details.stackTrace;
512
+ const callFrames = Array.isArray(stackTrace?.callFrames) ? stackTrace.callFrames : [];
513
+ jsErrors.push({
514
+ message: typeof exception?.description === "string" ? exception.description : String(details.text ?? "JavaScript exception"),
515
+ url: typeof details.url === "string" ? details.url : typeof callFrames[0]?.url === "string" ? String(callFrames[0].url) : void 0,
516
+ lineNumber: typeof details.lineNumber === "number" ? details.lineNumber : void 0,
517
+ columnNumber: typeof details.columnNumber === "number" ? details.columnNumber : void 0,
518
+ 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,
519
+ timestamp: Date.now()
520
+ });
521
+ }
522
+ }
523
+ async function ensureNetworkMonitoring(targetId) {
524
+ if (networkEnabled) return;
525
+ await sessionCommand(targetId, "Network.enable");
526
+ networkEnabled = true;
527
+ }
528
+ async function ensureConsoleMonitoring(targetId) {
529
+ if (consoleEnabled && errorsEnabled) return;
530
+ await sessionCommand(targetId, "Runtime.enable");
531
+ consoleEnabled = true;
532
+ errorsEnabled = true;
533
+ }
534
+ async function attachTarget(targetId) {
535
+ const result = await browserCommand("Target.attachToTarget", {
536
+ targetId,
537
+ flatten: true
538
+ });
539
+ connectionState?.sessions.set(targetId, result.sessionId);
540
+ connectionState?.attachedTargets.set(result.sessionId, targetId);
541
+ connectionState?.activeFrameIdByTarget.set(targetId, connectionState?.activeFrameIdByTarget.get(targetId) ?? null);
542
+ await sessionCommand(targetId, "Page.enable");
543
+ await sessionCommand(targetId, "Runtime.enable");
544
+ await sessionCommand(targetId, "DOM.enable");
545
+ await sessionCommand(targetId, "Accessibility.enable");
546
+ return result.sessionId;
547
+ }
548
+ async function getTargets() {
549
+ const state = connectionState;
550
+ if (!state) throw new Error("CDP connection not initialized");
551
+ try {
552
+ const result = await browserCommand("Target.getTargets");
553
+ return (result.targetInfos || []).map((target) => ({
554
+ id: target.targetId,
555
+ type: target.type,
556
+ title: target.title,
557
+ url: target.url,
558
+ webSocketDebuggerUrl: ""
559
+ }));
560
+ } catch {
561
+ return getJsonList(state.host, state.port);
562
+ }
563
+ }
564
+ async function ensurePageTarget(targetId) {
565
+ const targets = (await getTargets()).filter((target2) => target2.type === "page");
566
+ if (targets.length === 0) throw new Error("No page target found");
567
+ const persistedTargetId = targetId === void 0 ? connectionState?.currentTargetId : void 0;
568
+ let target;
569
+ if (typeof targetId === "number") {
570
+ target = targets[targetId] ?? targets.find((item) => Number(item.id) === targetId);
571
+ } else if (typeof targetId === "string") {
572
+ target = targets.find((item) => item.id === targetId);
573
+ if (!target) {
574
+ const numericTargetId = Number(targetId);
575
+ if (!Number.isNaN(numericTargetId)) {
576
+ target = targets[numericTargetId] ?? targets.find((item) => Number(item.id) === numericTargetId);
577
+ }
578
+ }
579
+ } else if (persistedTargetId) {
580
+ target = targets.find((item) => item.id === persistedTargetId);
581
+ }
582
+ target ??= targets[0];
583
+ setCurrentTargetId(target.id);
584
+ await attachTarget(target.id);
585
+ return target;
586
+ }
587
+ async function resolveBackendNodeIdByXPath(targetId, xpath) {
588
+ await sessionCommand(targetId, "DOM.getDocument", { depth: 0 });
589
+ const search = await sessionCommand(targetId, "DOM.performSearch", {
590
+ query: xpath,
591
+ includeUserAgentShadowDOM: true
592
+ });
593
+ try {
594
+ if (!search.resultCount) {
595
+ throw new Error(`Unknown ref xpath: ${xpath}`);
596
+ }
597
+ const { nodeIds } = await sessionCommand(targetId, "DOM.getSearchResults", {
598
+ searchId: search.searchId,
599
+ fromIndex: 0,
600
+ toIndex: search.resultCount
601
+ });
602
+ for (const nodeId of nodeIds) {
603
+ const described = await sessionCommand(targetId, "DOM.describeNode", {
604
+ nodeId
605
+ });
606
+ if (described.node.backendNodeId) {
607
+ return described.node.backendNodeId;
608
+ }
609
+ }
610
+ throw new Error(`XPath resolved but no backend node id found: ${xpath}`);
611
+ } finally {
612
+ await sessionCommand(targetId, "DOM.discardSearchResults", { searchId: search.searchId }).catch(() => {
613
+ });
614
+ }
615
+ }
616
+ async function parseRef(ref) {
617
+ const targetId = connectionState?.currentTargetId ?? "";
618
+ let refs = connectionState?.refsByTarget.get(targetId) ?? {};
619
+ if (!refs[ref] && targetId) {
620
+ const persistedRefs = loadPersistedRefs(targetId);
621
+ if (persistedRefs) {
622
+ connectionState?.refsByTarget.set(targetId, persistedRefs);
623
+ refs = persistedRefs;
624
+ }
625
+ }
626
+ const found = refs[ref];
627
+ if (!found) {
628
+ throw new Error(`Unknown ref: ${ref}. Run snapshot first.`);
629
+ }
630
+ if (found.backendDOMNodeId) {
631
+ return found.backendDOMNodeId;
632
+ }
633
+ if (targetId && found.xpath) {
634
+ const backendDOMNodeId = await resolveBackendNodeIdByXPath(targetId, found.xpath);
635
+ found.backendDOMNodeId = backendDOMNodeId;
636
+ connectionState?.refsByTarget.set(targetId, refs);
637
+ const pageUrl = await evaluate(targetId, "location.href", true).catch(() => void 0);
638
+ if (pageUrl) {
639
+ persistRefs(targetId, pageUrl, refs);
640
+ }
641
+ return backendDOMNodeId;
642
+ }
643
+ throw new Error(`Unknown ref: ${ref}. Run snapshot first.`);
644
+ }
645
+ function getRefsFilePath(targetId) {
646
+ return path2.join(os2.tmpdir(), `bb-browser-refs-${targetId}.json`);
647
+ }
648
+ function loadPersistedRefs(targetId, expectedUrl) {
649
+ try {
650
+ const data = JSON.parse(readFileSync(getRefsFilePath(targetId), "utf-8"));
651
+ if (data.targetId !== targetId) return null;
652
+ if (expectedUrl !== void 0 && data.url !== expectedUrl) return null;
653
+ if (!data.refs || typeof data.refs !== "object") return null;
654
+ return data.refs;
655
+ } catch {
656
+ return null;
657
+ }
658
+ }
659
+ function persistRefs(targetId, url, refs) {
660
+ try {
661
+ writeFileSync(getRefsFilePath(targetId), JSON.stringify({ targetId, url, timestamp: Date.now(), refs }));
662
+ } catch {
663
+ }
664
+ }
665
+ function clearPersistedRefs(targetId) {
666
+ try {
667
+ unlinkSync(getRefsFilePath(targetId));
668
+ } catch {
669
+ }
670
+ }
671
+ function loadBuildDomTreeScript() {
672
+ const currentDir = path2.dirname(fileURLToPath(import.meta.url));
673
+ const candidates = [
674
+ path2.resolve(currentDir, "./extension/buildDomTree.js"),
675
+ // npm installed: dist/cli.js → ../extension/buildDomTree.js
676
+ path2.resolve(currentDir, "../extension/buildDomTree.js"),
677
+ path2.resolve(currentDir, "../extension/dist/buildDomTree.js"),
678
+ path2.resolve(currentDir, "../packages/extension/public/buildDomTree.js"),
679
+ path2.resolve(currentDir, "../packages/extension/dist/buildDomTree.js"),
680
+ // dev mode: packages/cli/dist/ → ../../../extension/
681
+ path2.resolve(currentDir, "../../../extension/buildDomTree.js"),
682
+ path2.resolve(currentDir, "../../../extension/dist/buildDomTree.js"),
683
+ // dev mode: packages/cli/src/ → ../../extension/
684
+ path2.resolve(currentDir, "../../extension/buildDomTree.js"),
685
+ path2.resolve(currentDir, "../../../packages/extension/dist/buildDomTree.js"),
686
+ path2.resolve(currentDir, "../../../packages/extension/public/buildDomTree.js")
687
+ ];
688
+ for (const candidate of candidates) {
689
+ try {
690
+ return readFileSync(candidate, "utf8");
691
+ } catch {
692
+ }
693
+ }
694
+ throw new Error("Cannot find buildDomTree.js");
695
+ }
696
+ async function evaluate(targetId, expression, returnByValue = true) {
697
+ const result = await sessionCommand(targetId, "Runtime.evaluate", {
698
+ expression,
699
+ awaitPromise: true,
700
+ returnByValue
701
+ });
702
+ if (result.exceptionDetails) {
703
+ throw new Error(
704
+ result.exceptionDetails.exception?.description || result.exceptionDetails.text || "Runtime.evaluate failed"
705
+ );
706
+ }
707
+ return result.result.value ?? result.result;
708
+ }
709
+ async function focusNode(targetId, backendNodeId) {
710
+ await sessionCommand(targetId, "DOM.focus", { backendNodeId });
711
+ }
712
+ async function insertTextIntoNode(targetId, backendNodeId, text, clearFirst) {
713
+ const resolved = await sessionCommand(targetId, "DOM.resolveNode", { backendNodeId });
714
+ await sessionCommand(targetId, "Runtime.callFunctionOn", {
715
+ objectId: resolved.object.objectId,
716
+ functionDeclaration: `function(clearFirst) {
717
+ if (typeof this.scrollIntoView === 'function') {
718
+ this.scrollIntoView({ behavior: 'auto', block: 'center', inline: 'center' });
719
+ }
720
+ if (typeof this.focus === 'function') this.focus();
721
+ if (this instanceof HTMLInputElement || this instanceof HTMLTextAreaElement) {
722
+ if (clearFirst) {
723
+ this.value = '';
724
+ this.dispatchEvent(new Event('input', { bubbles: true }));
725
+ }
726
+ if (typeof this.setSelectionRange === 'function') {
727
+ const end = this.value.length;
728
+ this.setSelectionRange(end, end);
729
+ }
730
+ return true;
731
+ }
732
+ if (this instanceof HTMLElement && this.isContentEditable) {
733
+ if (clearFirst) {
734
+ this.textContent = '';
735
+ this.dispatchEvent(new Event('input', { bubbles: true }));
736
+ }
737
+ const selection = window.getSelection();
738
+ if (selection) {
739
+ const range = document.createRange();
740
+ range.selectNodeContents(this);
741
+ range.collapse(false);
742
+ selection.removeAllRanges();
743
+ selection.addRange(range);
744
+ }
745
+ return true;
746
+ }
747
+ return false;
748
+ }`,
749
+ arguments: [
750
+ { value: clearFirst }
751
+ ],
752
+ returnByValue: true
753
+ });
754
+ if (text) {
755
+ await focusNode(targetId, backendNodeId);
756
+ await sessionCommand(targetId, "Input.insertText", { text });
757
+ }
758
+ }
759
+ async function getInteractablePoint(targetId, backendNodeId) {
760
+ const resolved = await sessionCommand(targetId, "DOM.resolveNode", { backendNodeId });
761
+ const call = await sessionCommand(targetId, "Runtime.callFunctionOn", {
762
+ objectId: resolved.object.objectId,
763
+ functionDeclaration: `function() {
764
+ if (!(this instanceof Element)) {
765
+ throw new Error('Ref does not resolve to an element');
766
+ }
767
+ this.scrollIntoView({ behavior: 'auto', block: 'center', inline: 'center' });
768
+ const rect = this.getBoundingClientRect();
769
+ if (!rect || rect.width <= 0 || rect.height <= 0) {
770
+ throw new Error('Element is not visible');
771
+ }
772
+ return {
773
+ x: rect.left + rect.width / 2,
774
+ y: rect.top + rect.height / 2,
775
+ };
776
+ }`,
777
+ returnByValue: true
778
+ });
779
+ if (call.exceptionDetails) {
780
+ throw new Error(call.exceptionDetails.text || "Failed to resolve element point");
781
+ }
782
+ const point = call.result.value;
783
+ if (!point || typeof point.x !== "number" || typeof point.y !== "number" || !Number.isFinite(point.x) || !Number.isFinite(point.y)) {
784
+ throw new Error("Failed to resolve element point");
785
+ }
786
+ return point;
787
+ }
788
+ async function mouseClick(targetId, x, y) {
789
+ await sessionCommand(targetId, "Input.dispatchMouseEvent", { type: "mouseMoved", x, y, button: "none" });
790
+ await sessionCommand(targetId, "Input.dispatchMouseEvent", { type: "mousePressed", x, y, button: "left", clickCount: 1 });
791
+ await sessionCommand(targetId, "Input.dispatchMouseEvent", { type: "mouseReleased", x, y, button: "left", clickCount: 1 });
792
+ }
793
+ async function getAttributeValue(targetId, backendNodeId, attribute) {
794
+ if (attribute === "text") {
795
+ const resolved = await sessionCommand(targetId, "DOM.resolveNode", { backendNodeId });
796
+ const call2 = await sessionCommand(targetId, "Runtime.callFunctionOn", {
797
+ objectId: resolved.object.objectId,
798
+ functionDeclaration: `function() { return (this instanceof HTMLElement ? this.innerText : this.textContent || '').trim(); }`,
799
+ returnByValue: true
800
+ });
801
+ return String(call2.result.value ?? "");
802
+ }
803
+ const result = await sessionCommand(targetId, "DOM.resolveNode", { backendNodeId });
804
+ const call = await sessionCommand(targetId, "Runtime.callFunctionOn", {
805
+ objectId: result.object.objectId,
806
+ 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)}) || ''; }`,
807
+ returnByValue: true
808
+ });
809
+ return String(call.result.value ?? "");
810
+ }
811
+ async function buildSnapshot(targetId, request) {
812
+ const script = loadBuildDomTreeScript();
813
+ const buildArgs = {
814
+ showHighlightElements: true,
815
+ focusHighlightIndex: -1,
816
+ viewportExpansion: -1,
817
+ debugMode: false,
818
+ startId: 0,
819
+ startHighlightIndex: 0
820
+ };
821
+ const expression = `(() => { ${script}; const fn = globalThis.buildDomTree ?? (typeof window !== 'undefined' ? window.buildDomTree : undefined); if (typeof fn !== 'function') { throw new Error('buildDomTree is not available after script injection'); } return fn(${JSON.stringify({
822
+ ...buildArgs
823
+ })}); })()`;
824
+ const value = await evaluate(targetId, expression, true);
825
+ if (!value || !value.map || !value.rootId) {
826
+ const title = await evaluate(targetId, "document.title", true);
827
+ const pageUrl2 = await evaluate(targetId, "location.href", true);
828
+ const fallbackSnapshot = {
829
+ title,
830
+ url: pageUrl2,
831
+ lines: [title || pageUrl2],
832
+ refs: {}
833
+ };
834
+ connectionState?.refsByTarget.set(targetId, {});
835
+ persistRefs(targetId, pageUrl2, {});
836
+ return fallbackSnapshot;
837
+ }
838
+ const snapshot = convertBuildDomTreeResult(value, {
839
+ interactiveOnly: !!request.interactive,
840
+ compact: !!request.compact,
841
+ maxDepth: request.maxDepth,
842
+ selector: request.selector
843
+ });
844
+ const pageUrl = await evaluate(targetId, "location.href", true);
845
+ connectionState?.refsByTarget.set(targetId, snapshot.refs || {});
846
+ persistRefs(targetId, pageUrl, snapshot.refs || {});
847
+ return snapshot;
848
+ }
849
+ function convertBuildDomTreeResult(result, options) {
850
+ const { interactiveOnly, compact, maxDepth, selector } = options;
851
+ const { rootId, map } = result;
852
+ const refs = {};
853
+ const lines = [];
854
+ const getRole = (node) => {
855
+ const tagName = node.tagName.toLowerCase();
856
+ const role = node.attributes?.role;
857
+ if (role) return role;
858
+ const type = node.attributes?.type?.toLowerCase() || "text";
859
+ const inputRoleMap = {
860
+ text: "textbox",
861
+ password: "textbox",
862
+ email: "textbox",
863
+ url: "textbox",
864
+ tel: "textbox",
865
+ search: "searchbox",
866
+ number: "spinbutton",
867
+ range: "slider",
868
+ checkbox: "checkbox",
869
+ radio: "radio",
870
+ button: "button",
871
+ submit: "button",
872
+ reset: "button",
873
+ file: "button"
874
+ };
875
+ const roleMap = {
876
+ a: "link",
877
+ button: "button",
878
+ input: inputRoleMap[type] || "textbox",
879
+ select: "combobox",
880
+ textarea: "textbox",
881
+ img: "image",
882
+ nav: "navigation",
883
+ main: "main",
884
+ header: "banner",
885
+ footer: "contentinfo",
886
+ aside: "complementary",
887
+ form: "form",
888
+ table: "table",
889
+ ul: "list",
890
+ ol: "list",
891
+ li: "listitem",
892
+ h1: "heading",
893
+ h2: "heading",
894
+ h3: "heading",
895
+ h4: "heading",
896
+ h5: "heading",
897
+ h6: "heading",
898
+ dialog: "dialog",
899
+ article: "article",
900
+ section: "region",
901
+ label: "label",
902
+ details: "group",
903
+ summary: "button"
904
+ };
905
+ return roleMap[tagName] || tagName;
906
+ };
907
+ const collectTextContent = (node, nodeMap, depthLimit = 5) => {
908
+ const texts = [];
909
+ const visit = (nodeId, depth) => {
910
+ if (depth > depthLimit) return;
911
+ const currentNode = nodeMap[nodeId];
912
+ if (!currentNode) return;
913
+ if ("type" in currentNode && currentNode.type === "TEXT_NODE") {
914
+ const text = currentNode.text.trim();
915
+ if (text) texts.push(text);
916
+ return;
917
+ }
918
+ for (const childId of currentNode.children || []) visit(childId, depth + 1);
919
+ };
920
+ for (const childId of node.children || []) visit(childId, 0);
921
+ return texts.join(" ").trim();
922
+ };
923
+ const getName = (node) => {
924
+ const attrs = node.attributes || {};
925
+ return attrs["aria-label"] || attrs.title || attrs.placeholder || attrs.alt || attrs.value || collectTextContent(node, map) || attrs.name || void 0;
926
+ };
927
+ const truncateText = (text, length = 50) => text.length <= length ? text : `${text.slice(0, length - 3)}...`;
928
+ const selectorText = selector?.trim().toLowerCase();
929
+ const matchesSelector = (node, role, name) => {
930
+ if (!selectorText) return true;
931
+ const haystack = [node.tagName, role, name, node.xpath || "", ...Object.values(node.attributes || {})].join(" ").toLowerCase();
932
+ return haystack.includes(selectorText);
933
+ };
934
+ if (interactiveOnly) {
935
+ const interactiveNodes = Object.entries(map).filter(([, node]) => !("type" in node) && node.highlightIndex !== void 0 && node.highlightIndex !== null).map(([id, node]) => ({ id, node })).sort((a, b) => (a.node.highlightIndex ?? 0) - (b.node.highlightIndex ?? 0));
936
+ for (const { node } of interactiveNodes) {
937
+ const refId = String(node.highlightIndex);
938
+ const role = getRole(node);
939
+ const name = getName(node);
940
+ if (!matchesSelector(node, role, name)) continue;
941
+ let line = `${role} [ref=${refId}]`;
942
+ if (name) line += ` ${JSON.stringify(truncateText(name))}`;
943
+ lines.push(line);
944
+ refs[refId] = {
945
+ xpath: node.xpath || "",
946
+ role,
947
+ name,
948
+ tagName: node.tagName.toLowerCase()
949
+ };
950
+ }
951
+ return { snapshot: lines.join("\n"), refs };
952
+ }
953
+ const walk = (nodeId, depth) => {
954
+ if (maxDepth !== void 0 && depth > maxDepth) return;
955
+ const node = map[nodeId];
956
+ if (!node) return;
957
+ if ("type" in node && node.type === "TEXT_NODE") {
958
+ const text = node.text.trim();
959
+ if (!text) return;
960
+ lines.push(`${" ".repeat(depth)}- text ${JSON.stringify(truncateText(text, compact ? 80 : 120))}`);
961
+ return;
962
+ }
963
+ const role = getRole(node);
964
+ const name = getName(node);
965
+ if (!matchesSelector(node, role, name)) {
966
+ for (const childId of node.children || []) walk(childId, depth + 1);
967
+ return;
968
+ }
969
+ const indent = " ".repeat(depth);
970
+ const refId = node.highlightIndex !== void 0 && node.highlightIndex !== null ? String(node.highlightIndex) : null;
971
+ let line = `${indent}- ${role}`;
972
+ if (refId) line += ` [ref=${refId}]`;
973
+ if (name) line += ` ${JSON.stringify(truncateText(name, compact ? 50 : 80))}`;
974
+ if (!compact) line += ` <${node.tagName.toLowerCase()}>`;
975
+ lines.push(line);
976
+ if (refId) {
977
+ refs[refId] = {
978
+ xpath: node.xpath || "",
979
+ role,
980
+ name,
981
+ tagName: node.tagName.toLowerCase()
982
+ };
983
+ }
984
+ for (const childId of node.children || []) walk(childId, depth + 1);
985
+ };
986
+ walk(rootId, 0);
987
+ return { snapshot: lines.join("\n"), refs };
988
+ }
989
+ function ok(id, data) {
990
+ return { id, success: true, data };
991
+ }
992
+ function fail(id, error) {
993
+ return { id, success: false, error: buildRequestError(error).message };
994
+ }
995
+ async function ensureCdpConnection() {
996
+ if (connectionState) return;
997
+ if (reconnecting) return reconnecting;
998
+ reconnecting = (async () => {
999
+ const discovered = await discoverCdpPort();
1000
+ if (!discovered) {
1001
+ throw new Error("No browser connection found");
1002
+ }
1003
+ const version = await getJsonVersion(discovered.host, discovered.port);
1004
+ const wsUrl = version.webSocketDebuggerUrl;
1005
+ const socket = await connectWebSocket(wsUrl);
1006
+ connectionState = createState(discovered.host, discovered.port, wsUrl, socket);
1007
+ })();
1008
+ try {
1009
+ await reconnecting;
1010
+ } finally {
1011
+ reconnecting = null;
1012
+ }
1013
+ }
1014
+ async function sendCommand(request) {
1015
+ try {
1016
+ await ensureCdpConnection();
1017
+ const timeout = new Promise((_, reject) => setTimeout(() => reject(new Error("Request timed out")), COMMAND_TIMEOUT));
1018
+ return await Promise.race([dispatchRequest(request), timeout]);
1019
+ } catch (error) {
1020
+ return fail(request.id, error);
1021
+ }
1022
+ }
1023
+ async function dispatchRequest(request) {
1024
+ const target = await ensurePageTarget(request.tabId);
1025
+ switch (request.action) {
1026
+ case "open": {
1027
+ if (!request.url) return fail(request.id, "Missing url parameter");
1028
+ if (request.tabId === void 0) {
1029
+ const created = await browserCommand("Target.createTarget", { url: request.url, background: true });
1030
+ const newTarget = await ensurePageTarget(created.targetId);
1031
+ return ok(request.id, { url: request.url, tabId: newTarget.id });
1032
+ }
1033
+ await pageCommand(target.id, "Page.navigate", { url: request.url });
1034
+ connectionState?.refsByTarget.delete(target.id);
1035
+ clearPersistedRefs(target.id);
1036
+ return ok(request.id, { url: request.url, title: target.title, tabId: target.id });
1037
+ }
1038
+ case "snapshot": {
1039
+ const snapshotData = await buildSnapshot(target.id, request);
1040
+ return ok(request.id, { title: target.title, url: target.url, snapshotData });
1041
+ }
1042
+ case "click":
1043
+ case "hover": {
1044
+ if (!request.ref) return fail(request.id, "Missing ref parameter");
1045
+ const backendNodeId = await parseRef(request.ref);
1046
+ const point = await getInteractablePoint(target.id, backendNodeId);
1047
+ await sessionCommand(target.id, "Input.dispatchMouseEvent", { type: "mouseMoved", x: point.x, y: point.y, button: "none" });
1048
+ if (request.action === "click") await mouseClick(target.id, point.x, point.y);
1049
+ return ok(request.id, {});
1050
+ }
1051
+ case "fill":
1052
+ case "type": {
1053
+ if (!request.ref) return fail(request.id, "Missing ref parameter");
1054
+ if (request.text == null) return fail(request.id, "Missing text parameter");
1055
+ const backendNodeId = await parseRef(request.ref);
1056
+ await insertTextIntoNode(target.id, backendNodeId, request.text, request.action === "fill");
1057
+ return ok(request.id, { value: request.text });
1058
+ }
1059
+ case "check":
1060
+ case "uncheck": {
1061
+ if (!request.ref) return fail(request.id, "Missing ref parameter");
1062
+ const backendNodeId = await parseRef(request.ref);
1063
+ const desired = request.action === "check";
1064
+ const resolved = await sessionCommand(target.id, "DOM.resolveNode", { backendNodeId });
1065
+ await sessionCommand(target.id, "Runtime.callFunctionOn", {
1066
+ objectId: resolved.object.objectId,
1067
+ functionDeclaration: `function() { this.checked = ${desired}; this.dispatchEvent(new Event('input', { bubbles: true })); this.dispatchEvent(new Event('change', { bubbles: true })); }`
1068
+ });
1069
+ return ok(request.id, {});
1070
+ }
1071
+ case "select": {
1072
+ if (!request.ref || request.value == null) return fail(request.id, "Missing ref or value parameter");
1073
+ const backendNodeId = await parseRef(request.ref);
1074
+ const resolved = await sessionCommand(target.id, "DOM.resolveNode", { backendNodeId });
1075
+ await sessionCommand(target.id, "Runtime.callFunctionOn", {
1076
+ objectId: resolved.object.objectId,
1077
+ functionDeclaration: `function() { this.value = ${JSON.stringify(request.value)}; this.dispatchEvent(new Event('input', { bubbles: true })); this.dispatchEvent(new Event('change', { bubbles: true })); }`
1078
+ });
1079
+ return ok(request.id, { value: request.value });
1080
+ }
1081
+ case "get": {
1082
+ if (!request.attribute) return fail(request.id, "Missing attribute parameter");
1083
+ if (request.attribute === "url" && !request.ref) {
1084
+ return ok(request.id, { value: await evaluate(target.id, "location.href", true) });
1085
+ }
1086
+ if (request.attribute === "title" && !request.ref) {
1087
+ return ok(request.id, { value: await evaluate(target.id, "document.title", true) });
1088
+ }
1089
+ if (!request.ref) return fail(request.id, "Missing ref parameter");
1090
+ const value = await getAttributeValue(target.id, await parseRef(request.ref), request.attribute);
1091
+ return ok(request.id, { value });
1092
+ }
1093
+ case "screenshot": {
1094
+ const result = await sessionCommand(target.id, "Page.captureScreenshot", { format: "png", fromSurface: true });
1095
+ return ok(request.id, { dataUrl: `data:image/png;base64,${result.data}` });
1096
+ }
1097
+ case "close": {
1098
+ await browserCommand("Target.closeTarget", { targetId: target.id });
1099
+ connectionState?.refsByTarget.delete(target.id);
1100
+ clearPersistedRefs(target.id);
1101
+ return ok(request.id, {});
1102
+ }
1103
+ case "wait": {
1104
+ await new Promise((resolve3) => setTimeout(resolve3, request.ms ?? 1e3));
1105
+ return ok(request.id, {});
1106
+ }
1107
+ case "press": {
1108
+ if (!request.key) return fail(request.id, "Missing key parameter");
1109
+ await sessionCommand(target.id, "Input.dispatchKeyEvent", { type: "keyDown", key: request.key });
1110
+ if (request.key.length === 1) {
1111
+ await sessionCommand(target.id, "Input.dispatchKeyEvent", { type: "char", text: request.key, key: request.key });
1112
+ }
1113
+ await sessionCommand(target.id, "Input.dispatchKeyEvent", { type: "keyUp", key: request.key });
1114
+ return ok(request.id, {});
1115
+ }
1116
+ case "scroll": {
1117
+ const deltaY = request.direction === "up" ? -(request.pixels ?? 300) : request.pixels ?? 300;
1118
+ await sessionCommand(target.id, "Input.dispatchMouseEvent", { type: "mouseWheel", x: 0, y: 0, deltaX: 0, deltaY });
1119
+ return ok(request.id, {});
1120
+ }
1121
+ case "back": {
1122
+ await evaluate(target.id, "history.back(); undefined");
1123
+ return ok(request.id, {});
1124
+ }
1125
+ case "forward": {
1126
+ await evaluate(target.id, "history.forward(); undefined");
1127
+ return ok(request.id, {});
1128
+ }
1129
+ case "refresh": {
1130
+ await sessionCommand(target.id, "Page.reload", { ignoreCache: false });
1131
+ return ok(request.id, {});
1132
+ }
1133
+ case "eval": {
1134
+ if (!request.script) return fail(request.id, "Missing script parameter");
1135
+ const result = await evaluate(target.id, request.script, true);
1136
+ return ok(request.id, { result });
1137
+ }
1138
+ case "tab_list": {
1139
+ const tabs = (await getTargets()).filter((item) => item.type === "page").map((item, index) => ({ index, url: item.url, title: item.title, active: item.id === connectionState?.currentTargetId || !connectionState?.currentTargetId && index === 0, tabId: item.id }));
1140
+ return ok(request.id, { tabs, activeIndex: tabs.findIndex((tab) => tab.active) });
1141
+ }
1142
+ case "tab_new": {
1143
+ const created = await browserCommand("Target.createTarget", { url: request.url ?? "about:blank", background: true });
1144
+ return ok(request.id, { tabId: created.targetId, url: request.url ?? "about:blank" });
1145
+ }
1146
+ case "tab_select": {
1147
+ const tabs = (await getTargets()).filter((item) => item.type === "page");
1148
+ const selected = request.tabId !== void 0 ? tabs.find((item) => item.id === String(request.tabId) || Number(item.id) === request.tabId) : tabs[request.index ?? 0];
1149
+ if (!selected) return fail(request.id, "Tab not found");
1150
+ setCurrentTargetId(selected.id);
1151
+ await attachTarget(selected.id);
1152
+ return ok(request.id, { tabId: selected.id, url: selected.url, title: selected.title });
1153
+ }
1154
+ case "tab_close": {
1155
+ const tabs = (await getTargets()).filter((item) => item.type === "page");
1156
+ const selected = request.tabId !== void 0 ? tabs.find((item) => item.id === String(request.tabId) || Number(item.id) === request.tabId) : tabs[request.index ?? 0];
1157
+ if (!selected) return fail(request.id, "Tab not found");
1158
+ await browserCommand("Target.closeTarget", { targetId: selected.id });
1159
+ connectionState?.refsByTarget.delete(selected.id);
1160
+ if (connectionState?.currentTargetId === selected.id) {
1161
+ setCurrentTargetId(void 0);
1162
+ }
1163
+ clearPersistedRefs(selected.id);
1164
+ return ok(request.id, { tabId: selected.id });
1165
+ }
1166
+ case "frame": {
1167
+ if (!request.selector) return fail(request.id, "Missing selector parameter");
1168
+ const document = await pageCommand(target.id, "DOM.getDocument", {});
1169
+ const node = await pageCommand(target.id, "DOM.querySelector", { nodeId: document.root.nodeId, selector: request.selector });
1170
+ if (!node.nodeId) return fail(request.id, `iframe not found: ${request.selector}`);
1171
+ const described = await pageCommand(target.id, "DOM.describeNode", { nodeId: node.nodeId });
1172
+ const frameId = described.node.frameId;
1173
+ const nodeName = String(described.node.nodeName ?? "").toLowerCase();
1174
+ if (!frameId) return fail(request.id, `Unable to get iframe frameId: ${request.selector}`);
1175
+ if (nodeName && nodeName !== "iframe" && nodeName !== "frame") return fail(request.id, `Element is not an iframe: ${nodeName}`);
1176
+ connectionState?.activeFrameIdByTarget.set(target.id, frameId);
1177
+ const attributes = described.node.attributes ?? [];
1178
+ const attrMap = {};
1179
+ for (let i = 0; i < attributes.length; i += 2) attrMap[String(attributes[i])] = String(attributes[i + 1] ?? "");
1180
+ return ok(request.id, { frameInfo: { selector: request.selector, name: attrMap.name ?? "", url: attrMap.src ?? "", frameId } });
1181
+ }
1182
+ case "frame_main": {
1183
+ connectionState?.activeFrameIdByTarget.set(target.id, null);
1184
+ return ok(request.id, { frameInfo: { frameId: 0 } });
1185
+ }
1186
+ case "dialog": {
1187
+ connectionState?.dialogHandlers.set(target.id, { accept: request.dialogResponse !== "dismiss", ...request.promptText !== void 0 ? { promptText: request.promptText } : {} });
1188
+ await sessionCommand(target.id, "Page.enable");
1189
+ return ok(request.id, { dialog: { armed: true, response: request.dialogResponse ?? "accept" } });
1190
+ }
1191
+ case "network": {
1192
+ const subCommand = request.networkCommand ?? "requests";
1193
+ switch (subCommand) {
1194
+ case "requests": {
1195
+ await ensureNetworkMonitoring(target.id);
1196
+ const requests = Array.from(networkRequests.values()).filter((item) => !request.filter || item.url.includes(request.filter));
1197
+ if (request.withBody) {
1198
+ await Promise.all(requests.map(async (item) => {
1199
+ if (item.failed || item.responseBody !== void 0 || item.bodyError !== void 0) return;
1200
+ try {
1201
+ const body = await sessionCommand(target.id, "Network.getResponseBody", { requestId: item.requestId });
1202
+ item.responseBody = body.body;
1203
+ item.responseBodyBase64 = body.base64Encoded;
1204
+ } catch (error) {
1205
+ item.bodyError = error instanceof Error ? error.message : String(error);
1206
+ }
1207
+ }));
1208
+ }
1209
+ return ok(request.id, { networkRequests: requests });
1210
+ }
1211
+ case "route":
1212
+ return ok(request.id, { routeCount: 0 });
1213
+ case "unroute":
1214
+ return ok(request.id, { routeCount: 0 });
1215
+ case "clear":
1216
+ networkRequests.clear();
1217
+ return ok(request.id, {});
1218
+ default:
1219
+ return fail(request.id, `Unknown network subcommand: ${subCommand}`);
1220
+ }
1221
+ }
1222
+ case "console": {
1223
+ const subCommand = request.consoleCommand ?? "get";
1224
+ await ensureConsoleMonitoring(target.id);
1225
+ switch (subCommand) {
1226
+ case "get":
1227
+ return ok(request.id, { consoleMessages: consoleMessages.filter((item) => !request.filter || item.text.includes(request.filter)) });
1228
+ case "clear":
1229
+ consoleMessages.length = 0;
1230
+ return ok(request.id, {});
1231
+ default:
1232
+ return fail(request.id, `Unknown console subcommand: ${subCommand}`);
1233
+ }
1234
+ }
1235
+ case "errors": {
1236
+ const subCommand = request.errorsCommand ?? "get";
1237
+ await ensureConsoleMonitoring(target.id);
1238
+ switch (subCommand) {
1239
+ case "get":
1240
+ return ok(request.id, { jsErrors: jsErrors.filter((item) => !request.filter || item.message.includes(request.filter) || item.url?.includes(request.filter)) });
1241
+ case "clear":
1242
+ jsErrors.length = 0;
1243
+ return ok(request.id, {});
1244
+ default:
1245
+ return fail(request.id, `Unknown errors subcommand: ${subCommand}`);
1246
+ }
1247
+ }
1248
+ case "trace": {
1249
+ const subCommand = request.traceCommand ?? "status";
1250
+ switch (subCommand) {
1251
+ case "start":
1252
+ traceRecording = true;
1253
+ traceEvents.length = 0;
1254
+ return ok(request.id, { traceStatus: { recording: true, eventCount: 0 } });
1255
+ case "stop": {
1256
+ traceRecording = false;
1257
+ return ok(request.id, { traceEvents: [...traceEvents], traceStatus: { recording: false, eventCount: traceEvents.length } });
1258
+ }
1259
+ case "status":
1260
+ return ok(request.id, { traceStatus: { recording: traceRecording, eventCount: traceEvents.length } });
1261
+ default:
1262
+ return fail(request.id, `Unknown trace subcommand: ${subCommand}`);
1263
+ }
1264
+ }
1265
+ default:
1266
+ return fail(request.id, `Action not yet supported in direct CDP mode: ${request.action}`);
1267
+ }
1268
+ }
1269
+
1270
+ // packages/cli/src/monitor-manager.ts
1271
+ import { spawn as spawn2 } from "child_process";
1272
+ import { mkdir as mkdir2, readFile as readFile2, writeFile as writeFile2, unlink } from "fs/promises";
1273
+ import { request as httpRequest2 } from "http";
1274
+ import { randomBytes } from "crypto";
1275
+ import { fileURLToPath as fileURLToPath2 } from "url";
1276
+ import { dirname, resolve } from "path";
1277
+ import { existsSync as existsSync2 } from "fs";
1278
+ import os3 from "os";
1279
+ import path3 from "path";
1280
+ var MONITOR_DIR = path3.join(os3.homedir(), ".bb-browser");
1281
+ var PID_FILE = path3.join(MONITOR_DIR, "monitor.pid");
1282
+ var PORT_FILE = path3.join(MONITOR_DIR, "monitor.port");
1283
+ var TOKEN_FILE = path3.join(MONITOR_DIR, "monitor.token");
1284
+ var DEFAULT_MONITOR_PORT = 19826;
1285
+ function httpJson(method, url, token, body) {
1286
+ return new Promise((resolve3, reject) => {
1287
+ const parsed = new URL(url);
1288
+ const payload = body !== void 0 ? JSON.stringify(body) : void 0;
1289
+ const req = httpRequest2(
1290
+ {
1291
+ hostname: parsed.hostname,
1292
+ port: parsed.port,
1293
+ path: parsed.pathname,
1294
+ method,
1295
+ headers: {
1296
+ Authorization: `Bearer ${token}`,
1297
+ ...payload ? { "Content-Type": "application/json", "Content-Length": Buffer.byteLength(payload) } : {}
1298
+ },
1299
+ timeout: 5e3
1300
+ },
1301
+ (res) => {
1302
+ const chunks = [];
1303
+ res.on("data", (chunk) => chunks.push(Buffer.from(chunk)));
1304
+ res.on("end", () => {
1305
+ const raw = Buffer.concat(chunks).toString("utf8");
1306
+ if ((res.statusCode ?? 500) >= 400) {
1307
+ reject(new Error(`Monitor HTTP ${res.statusCode}: ${raw}`));
1308
+ return;
1309
+ }
1310
+ try {
1311
+ resolve3(JSON.parse(raw));
1312
+ } catch {
1313
+ reject(new Error(`Invalid JSON from monitor: ${raw}`));
1314
+ }
1315
+ });
1316
+ }
1317
+ );
1318
+ req.on("error", reject);
1319
+ req.on("timeout", () => {
1320
+ req.destroy();
1321
+ reject(new Error("Monitor request timed out"));
1322
+ });
1323
+ if (payload) req.write(payload);
1324
+ req.end();
1325
+ });
1326
+ }
1327
+ async function readPortFile() {
1328
+ try {
1329
+ const raw = await readFile2(PORT_FILE, "utf8");
1330
+ const port = Number.parseInt(raw.trim(), 10);
1331
+ return Number.isInteger(port) && port > 0 ? port : null;
1332
+ } catch {
1333
+ return null;
1334
+ }
1335
+ }
1336
+ async function readTokenFile() {
1337
+ try {
1338
+ return (await readFile2(TOKEN_FILE, "utf8")).trim();
1339
+ } catch {
1340
+ return null;
1341
+ }
1342
+ }
1343
+ async function ensureMonitorRunning() {
1344
+ const existingPort = await readPortFile();
1345
+ const existingToken = await readTokenFile();
1346
+ if (existingPort && existingToken) {
1347
+ try {
1348
+ const status = await httpJson(
1349
+ "GET",
1350
+ `http://127.0.0.1:${existingPort}/status`,
1351
+ existingToken
1352
+ );
1353
+ if (status.running) {
1354
+ return { port: existingPort, token: existingToken };
1355
+ }
1356
+ } catch {
1357
+ }
1358
+ }
1359
+ const cdp = await discoverCdpPort();
1360
+ if (!cdp) {
1361
+ throw new Error("Cannot start monitor: no browser connection found");
1362
+ }
1363
+ const token = randomBytes(32).toString("hex");
1364
+ const monitorPort = DEFAULT_MONITOR_PORT;
1365
+ const monitorScript = findMonitorScript();
1366
+ await mkdir2(MONITOR_DIR, { recursive: true });
1367
+ await writeFile2(TOKEN_FILE, token, { mode: 384 });
1368
+ const child = spawn2(process.execPath, [
1369
+ monitorScript,
1370
+ "--cdp-host",
1371
+ cdp.host,
1372
+ "--cdp-port",
1373
+ String(cdp.port),
1374
+ "--monitor-port",
1375
+ String(monitorPort),
1376
+ "--token",
1377
+ token
1378
+ ], {
1379
+ detached: true,
1380
+ stdio: "ignore"
1381
+ });
1382
+ child.unref();
1383
+ const deadline = Date.now() + 5e3;
1384
+ while (Date.now() < deadline) {
1385
+ await new Promise((r) => setTimeout(r, 200));
1386
+ try {
1387
+ const status = await httpJson(
1388
+ "GET",
1389
+ `http://127.0.0.1:${monitorPort}/status`,
1390
+ token
1391
+ );
1392
+ if (status.running) {
1393
+ return { port: monitorPort, token };
1394
+ }
1395
+ } catch {
1396
+ }
1397
+ }
1398
+ throw new Error("Monitor process did not start in time");
1399
+ }
1400
+ async function monitorCommand(request) {
1401
+ const { port, token } = await ensureMonitorRunning();
1402
+ return httpJson(
1403
+ "POST",
1404
+ `http://127.0.0.1:${port}/command`,
1405
+ token,
1406
+ request
1407
+ );
1408
+ }
1409
+ function findMonitorScript() {
1410
+ const currentFile = fileURLToPath2(import.meta.url);
1411
+ const currentDir = dirname(currentFile);
1412
+ const candidates = [
1413
+ // Built output (tsup puts it next to cli.js)
1414
+ resolve(currentDir, "cdp-monitor.js"),
1415
+ // Development: packages/cli/src -> packages/cli/dist
1416
+ resolve(currentDir, "../dist/cdp-monitor.js"),
1417
+ // Monorepo root dist
1418
+ resolve(currentDir, "../../dist/cdp-monitor.js"),
1419
+ resolve(currentDir, "../../../dist/cdp-monitor.js")
1420
+ ];
1421
+ for (const candidate of candidates) {
1422
+ if (existsSync2(candidate)) {
1423
+ return candidate;
1424
+ }
1425
+ }
1426
+ return candidates[0];
1427
+ }
1428
+
1429
+ // packages/cli/src/client.ts
1430
+ var MONITOR_ACTIONS = /* @__PURE__ */ new Set(["network", "console", "errors", "trace"]);
1431
+ var jqExpression;
1432
+ function setJqExpression(expression) {
1433
+ jqExpression = expression;
1434
+ }
1435
+ function printJqResults(response) {
1436
+ const target = response.data ?? response;
1437
+ const results = applyJq(target, jqExpression || ".");
1438
+ for (const result of results) {
1439
+ console.log(typeof result === "string" ? result : JSON.stringify(result));
1440
+ }
1441
+ process.exit(0);
1442
+ }
1443
+ function handleJqResponse(response) {
1444
+ if (jqExpression) {
1445
+ printJqResults(response);
1446
+ }
1447
+ }
1448
+ async function sendCommand2(request) {
1449
+ if (MONITOR_ACTIONS.has(request.action)) {
1450
+ try {
1451
+ return await monitorCommand(request);
1452
+ } catch {
1453
+ return sendCommand(request);
1454
+ }
1455
+ }
1456
+ return sendCommand(request);
1457
+ }
1458
+
1459
+ // packages/cli/src/daemon-manager.ts
1460
+ import { fileURLToPath as fileURLToPath3 } from "url";
1461
+ import { dirname as dirname2, resolve as resolve2 } from "path";
1462
+ import { existsSync as existsSync3 } from "fs";
1463
+ async function isDaemonRunning() {
1464
+ return await isManagedBrowserRunning();
1465
+ }
1466
+ async function ensureDaemonRunning() {
1467
+ try {
1468
+ await ensureCdpConnection();
1469
+ } catch (error) {
1470
+ if (error instanceof Error && error.message.includes("No browser connection found")) {
1471
+ throw new Error([
1472
+ "bb-browser: Could not start browser.",
1473
+ "",
1474
+ "Make sure Chrome is installed, then try again.",
1475
+ "Or specify a CDP port manually: bb-browser --port 9222"
1476
+ ].join("\n"));
1477
+ }
1478
+ throw error;
1479
+ }
1480
+ }
1481
+
1482
+ // packages/cli/src/history-sqlite.ts
1483
+ import { copyFileSync, existsSync as existsSync4, unlinkSync as unlinkSync2 } from "fs";
1484
+ import { execSync as execSync2 } from "child_process";
1485
+ import { homedir, tmpdir } from "os";
1486
+ import { join } from "path";
1487
+ function getHistoryPathCandidates() {
1488
+ const home = homedir();
1489
+ const localAppData = process.env.LOCALAPPDATA || "";
1490
+ const candidates = [
1491
+ join(home, "Library/Application Support/Google/Chrome/Default/History"),
1492
+ join(home, "Library/Application Support/Microsoft Edge/Default/History"),
1493
+ join(home, "Library/Application Support/BraveSoftware/Brave-Browser/Default/History"),
1494
+ join(home, "Library/Application Support/Arc/User Data/Default/History"),
1495
+ join(home, ".config/google-chrome/Default/History")
1496
+ ];
1497
+ if (localAppData) {
1498
+ candidates.push(
1499
+ join(localAppData, "Google/Chrome/User Data/Default/History"),
1500
+ join(localAppData, "Microsoft/Edge/User Data/Default/History")
1501
+ );
1502
+ }
1503
+ return candidates;
1504
+ }
1505
+ function findHistoryPath() {
1506
+ for (const historyPath of getHistoryPathCandidates()) {
1507
+ if (existsSync4(historyPath)) {
1508
+ return historyPath;
1509
+ }
1510
+ }
1511
+ return null;
1512
+ }
1513
+ function sqlEscape(value) {
1514
+ return value.replace(/'/g, "''");
1515
+ }
1516
+ function buildTimeWhere(days) {
1517
+ if (!days || days <= 0) {
1518
+ return "";
1519
+ }
1520
+ return `last_visit_time > (strftime('%s', 'now') - ${Math.floor(days)}*86400) * 1000000 + 11644473600000000`;
1521
+ }
1522
+ function runHistoryQuery(sql, mapRow) {
1523
+ const historyPath = findHistoryPath();
1524
+ if (!historyPath) {
1525
+ return [];
1526
+ }
1527
+ const tmpPath = join(tmpdir(), `bb-history-${Date.now()}-${Math.random().toString(36).slice(2)}.db`);
1528
+ try {
1529
+ copyFileSync(historyPath, tmpPath);
1530
+ const escapedTmpPath = tmpPath.replace(/"/g, '\\"');
1531
+ const escapedSql = sql.replace(/"/g, '\\"');
1532
+ const output = execSync2(`sqlite3 -separator $'\\t' "${escapedTmpPath}" "${escapedSql}"`, {
1533
+ encoding: "utf-8",
1534
+ stdio: ["pipe", "pipe", "pipe"]
1535
+ });
1536
+ return output.split("\n").filter(Boolean).map((line) => mapRow(line.split(" "))).filter((item) => item !== null);
1537
+ } catch {
1538
+ return [];
1539
+ } finally {
1540
+ try {
1541
+ unlinkSync2(tmpPath);
1542
+ } catch {
1543
+ }
1544
+ }
1545
+ }
1546
+ function searchHistory(query, days) {
1547
+ const conditions = [];
1548
+ const trimmedQuery = query?.trim();
1549
+ if (trimmedQuery) {
1550
+ const escapedQuery = sqlEscape(trimmedQuery);
1551
+ conditions.push(`(url LIKE '%${escapedQuery}%' OR title LIKE '%${escapedQuery}%')`);
1552
+ }
1553
+ const timeWhere = buildTimeWhere(days);
1554
+ if (timeWhere) {
1555
+ conditions.push(timeWhere);
1556
+ }
1557
+ const whereClause = conditions.length > 0 ? `WHERE ${conditions.join(" AND ")}` : "";
1558
+ const sql = `
1559
+ SELECT
1560
+ url,
1561
+ REPLACE(IFNULL(title, ''), char(9), ' '),
1562
+ IFNULL(visit_count, 0),
1563
+ IFNULL(last_visit_time, 0)
1564
+ FROM urls
1565
+ ${whereClause}
1566
+ ORDER BY last_visit_time DESC
1567
+ LIMIT 100;
1568
+ `.trim();
1569
+ return runHistoryQuery(sql, (row) => {
1570
+ if (row.length < 4) {
1571
+ return null;
1572
+ }
1573
+ const chromeTimestamp = Number(row[3]) || 0;
1574
+ return {
1575
+ url: row[0] || "",
1576
+ title: row[1] || "",
1577
+ visitCount: Number(row[2]) || 0,
1578
+ lastVisitTime: chromeTimestamp > 0 ? chromeTimestamp / 1e6 - 11644473600 : 0
1579
+ };
1580
+ });
1581
+ }
1582
+ function getHistoryDomains(days) {
1583
+ const timeWhere = buildTimeWhere(days);
1584
+ const whereClause = timeWhere ? `WHERE ${timeWhere}` : "";
1585
+ const sql = `
1586
+ SELECT
1587
+ domain,
1588
+ SUM(visit_count) AS visits,
1589
+ GROUP_CONCAT(title, char(31)) AS titles
1590
+ FROM (
1591
+ SELECT
1592
+ CASE
1593
+ WHEN instr(url, '//') > 0 AND instr(substr(url, instr(url, '//') + 2), '/') > 0
1594
+ THEN substr(
1595
+ substr(url, instr(url, '//') + 2),
1596
+ 1,
1597
+ instr(substr(url, instr(url, '//') + 2), '/') - 1
1598
+ )
1599
+ WHEN instr(url, '//') > 0 THEN substr(url, instr(url, '//') + 2)
1600
+ WHEN instr(url, '/') > 0 THEN substr(url, 1, instr(url, '/') - 1)
1601
+ ELSE url
1602
+ END AS domain,
1603
+ IFNULL(visit_count, 0) AS visit_count,
1604
+ REPLACE(IFNULL(title, ''), char(31), ' ') AS title
1605
+ FROM urls
1606
+ ${whereClause}
1607
+ )
1608
+ WHERE domain != ''
1609
+ GROUP BY domain
1610
+ ORDER BY visits DESC
1611
+ LIMIT 50;
1612
+ `.trim();
1613
+ return runHistoryQuery(sql, (row) => {
1614
+ if (row.length < 3) {
1615
+ return null;
1616
+ }
1617
+ const titles = row[2] ? Array.from(new Set(row[2].split(String.fromCharCode(31)).map((title) => title.trim()).filter(Boolean))).slice(0, 10) : [];
1618
+ return {
1619
+ domain: row[0] || "",
1620
+ visits: Number(row[1]) || 0,
1621
+ titles
1622
+ };
1623
+ });
1624
+ }
1625
+
1626
+ // packages/cli/src/commands/site.ts
1627
+ import { readFileSync as readFileSync2, readdirSync, existsSync as existsSync5, mkdirSync } from "fs";
1628
+ import { join as join2, relative } from "path";
1629
+ import { homedir as homedir2 } from "os";
1630
+ import { execSync as execSync3 } from "child_process";
1631
+ var BB_DIR = join2(homedir2(), ".bb-browser");
1632
+ var LOCAL_SITES_DIR = join2(BB_DIR, "sites");
1633
+ var COMMUNITY_SITES_DIR = join2(BB_DIR, "bb-sites");
1634
+ var COMMUNITY_REPO = "https://github.com/epiral/bb-sites.git";
1635
+ function checkCliUpdate() {
1636
+ try {
1637
+ const current = execSync3("bb-browser --version", { timeout: 3e3, stdio: ["pipe", "pipe", "pipe"] }).toString().trim();
1638
+ const latest = execSync3("npm view bb-browser version", { timeout: 5e3, stdio: ["pipe", "pipe", "pipe"] }).toString().trim();
1639
+ if (latest && current && latest !== current && latest.localeCompare(current, void 0, { numeric: true }) > 0) {
1640
+ console.log(`
1641
+ \u{1F4E6} bb-browser ${latest} available (current: ${current}). Run: npm install -g bb-browser`);
1642
+ }
1643
+ } catch {
1644
+ }
1645
+ }
1646
+ function exitJsonError(error, extra = {}) {
1647
+ console.log(JSON.stringify({ success: false, error, ...extra }, null, 2));
1648
+ process.exit(1);
1649
+ }
1650
+ function parseSiteMeta(filePath, source) {
1651
+ let content;
1652
+ try {
1653
+ content = readFileSync2(filePath, "utf-8");
1654
+ } catch {
1655
+ return null;
1656
+ }
1657
+ const sitesDir = source === "local" ? LOCAL_SITES_DIR : COMMUNITY_SITES_DIR;
1658
+ const relPath = relative(sitesDir, filePath);
1659
+ const defaultName = relPath.replace(/\.js$/, "").replace(/\\/g, "/");
1660
+ const metaMatch = content.match(/\/\*\s*@meta\s*\n([\s\S]*?)\*\//);
1661
+ if (metaMatch) {
1662
+ try {
1663
+ const metaJson = JSON.parse(metaMatch[1]);
1664
+ return {
1665
+ name: metaJson.name || defaultName,
1666
+ description: metaJson.description || "",
1667
+ domain: metaJson.domain || "",
1668
+ args: metaJson.args || {},
1669
+ capabilities: metaJson.capabilities,
1670
+ readOnly: metaJson.readOnly,
1671
+ example: metaJson.example,
1672
+ filePath,
1673
+ source
1674
+ };
1675
+ } catch {
1676
+ }
1677
+ }
1678
+ const meta = {
1679
+ name: defaultName,
1680
+ description: "",
1681
+ domain: "",
1682
+ args: {},
1683
+ filePath,
1684
+ source
1685
+ };
1686
+ const tagPattern = /\/\/\s*@(\w+)[ \t]+(.*)/g;
1687
+ let match;
1688
+ while ((match = tagPattern.exec(content)) !== null) {
1689
+ const [, key, value] = match;
1690
+ switch (key) {
1691
+ case "name":
1692
+ meta.name = value.trim();
1693
+ break;
1694
+ case "description":
1695
+ meta.description = value.trim();
1696
+ break;
1697
+ case "domain":
1698
+ meta.domain = value.trim();
1699
+ break;
1700
+ case "args":
1701
+ for (const arg of value.trim().split(/[,\s]+/).filter(Boolean)) {
1702
+ meta.args[arg] = { required: true };
1703
+ }
1704
+ break;
1705
+ case "example":
1706
+ meta.example = value.trim();
1707
+ break;
1708
+ }
1709
+ }
1710
+ return meta;
1711
+ }
1712
+ function scanSites(dir, source) {
1713
+ if (!existsSync5(dir)) return [];
1714
+ const sites = [];
1715
+ function walk(currentDir) {
1716
+ let entries;
1717
+ try {
1718
+ entries = readdirSync(currentDir, { withFileTypes: true });
1719
+ } catch {
1720
+ return;
1721
+ }
1722
+ for (const entry of entries) {
1723
+ const fullPath = join2(currentDir, entry.name);
1724
+ if (entry.isDirectory() && !entry.name.startsWith(".")) {
1725
+ walk(fullPath);
1726
+ } else if (entry.isFile() && entry.name.endsWith(".js")) {
1727
+ const meta = parseSiteMeta(fullPath, source);
1728
+ if (meta) sites.push(meta);
1729
+ }
1730
+ }
1731
+ }
1732
+ walk(dir);
1733
+ return sites;
1734
+ }
1735
+ function getSiteHintForDomain(url) {
1736
+ try {
1737
+ const hostname = new URL(url).hostname;
1738
+ const sites = getAllSites();
1739
+ const matched = sites.filter((s) => s.domain && (hostname === s.domain || hostname.endsWith("." + s.domain)));
1740
+ if (matched.length === 0) return null;
1741
+ const names = matched.map((s) => s.name);
1742
+ const example = matched[0].example || `bb-browser site ${names[0]}`;
1743
+ return `This website has ${names.length} site adapters for direct data access. Try: ${example}`;
1744
+ } catch {
1745
+ return null;
1746
+ }
1747
+ }
1748
+ function getAllSites() {
1749
+ const community = scanSites(COMMUNITY_SITES_DIR, "community");
1750
+ const local = scanSites(LOCAL_SITES_DIR, "local");
1751
+ const byName = /* @__PURE__ */ new Map();
1752
+ for (const s of community) byName.set(s.name, s);
1753
+ for (const s of local) byName.set(s.name, s);
1754
+ return Array.from(byName.values()).sort((a, b) => a.name.localeCompare(b.name));
1755
+ }
1756
+ function matchTabOrigin(tabUrl, domain) {
1757
+ try {
1758
+ const tabOrigin = new URL(tabUrl).hostname;
1759
+ return tabOrigin === domain || tabOrigin.endsWith("." + domain);
1760
+ } catch {
1761
+ return false;
1762
+ }
1763
+ }
1764
+ function siteList(options) {
1765
+ const sites = getAllSites();
1766
+ if (sites.length === 0) {
1767
+ if (options.json) {
1768
+ console.log("[]");
1769
+ return;
1770
+ }
1771
+ console.log("No site adapters found.");
1772
+ console.log(" Install community adapters: bb-browser site update");
1773
+ console.log(` Private adapter directory: ${LOCAL_SITES_DIR}`);
1774
+ return;
1775
+ }
1776
+ if (options.json) {
1777
+ console.log(JSON.stringify(sites.map((s) => ({
1778
+ name: s.name,
1779
+ description: s.description,
1780
+ domain: s.domain,
1781
+ args: s.args,
1782
+ source: s.source
1783
+ })), null, 2));
1784
+ return;
1785
+ }
1786
+ const groups = /* @__PURE__ */ new Map();
1787
+ for (const s of sites) {
1788
+ const platform = s.name.split("/")[0];
1789
+ if (!groups.has(platform)) groups.set(platform, []);
1790
+ groups.get(platform).push(s);
1791
+ }
1792
+ for (const [platform, items] of groups) {
1793
+ console.log(`
1794
+ ${platform}/`);
1795
+ for (const s of items) {
1796
+ const cmd = s.name.split("/").slice(1).join("/");
1797
+ const src = s.source === "local" ? " (local)" : "";
1798
+ const desc = s.description ? ` - ${s.description}` : "";
1799
+ console.log(` ${cmd.padEnd(20)}${desc}${src}`);
1800
+ }
1801
+ }
1802
+ console.log();
1803
+ }
1804
+ function siteSearch(query, options) {
1805
+ const sites = getAllSites();
1806
+ const q = query.toLowerCase();
1807
+ const matches = sites.filter(
1808
+ (s) => s.name.toLowerCase().includes(q) || s.description.toLowerCase().includes(q) || s.domain.toLowerCase().includes(q)
1809
+ );
1810
+ if (matches.length === 0) {
1811
+ if (options.json) {
1812
+ console.log("[]");
1813
+ return;
1814
+ }
1815
+ console.log(`No adapters matching "${query}" were found.`);
1816
+ console.log(" View all: bb-browser site list");
1817
+ return;
1818
+ }
1819
+ if (options.json) {
1820
+ console.log(JSON.stringify(matches.map((s) => ({
1821
+ name: s.name,
1822
+ description: s.description,
1823
+ domain: s.domain,
1824
+ source: s.source
1825
+ })), null, 2));
1826
+ return;
1827
+ }
1828
+ for (const s of matches) {
1829
+ const src = s.source === "local" ? " (local)" : "";
1830
+ console.log(`${s.name.padEnd(24)} ${s.description}${src}`);
1831
+ }
1832
+ }
1833
+ function siteUpdate(options = {}) {
1834
+ mkdirSync(BB_DIR, { recursive: true });
1835
+ const updateMode = existsSync5(join2(COMMUNITY_SITES_DIR, ".git")) ? "pull" : "clone";
1836
+ if (updateMode === "pull") {
1837
+ if (!options.json) {
1838
+ console.log("Updating community site adapter repository...");
1839
+ }
1840
+ try {
1841
+ execSync3("git pull --ff-only", { cwd: COMMUNITY_SITES_DIR, stdio: "pipe" });
1842
+ if (!options.json) {
1843
+ console.log("Update complete.");
1844
+ console.log("");
1845
+ console.log("\u{1F4A1} Run bb-browser site recommend to discover adapters matching your browsing habits");
1846
+ }
1847
+ } catch (e) {
1848
+ const message = e instanceof Error ? e.message : String(e);
1849
+ const manualAction = "cd ~/.bb-browser/bb-sites && git pull";
1850
+ if (options.json) {
1851
+ exitJsonError(`Update failed: ${message}`, { action: manualAction, updateMode });
1852
+ }
1853
+ console.error(`Update failed: ${e instanceof Error ? e.message : e}`);
1854
+ console.error(" Manual fix: cd ~/.bb-browser/bb-sites && git pull");
1855
+ process.exit(1);
1856
+ }
1857
+ } else {
1858
+ if (!options.json) {
1859
+ console.log(`Cloning community adapter repository: ${COMMUNITY_REPO}`);
1860
+ }
1861
+ try {
1862
+ execSync3(`git clone ${COMMUNITY_REPO} ${COMMUNITY_SITES_DIR}`, { stdio: "pipe" });
1863
+ if (!options.json) {
1864
+ console.log("Clone complete.");
1865
+ console.log("");
1866
+ console.log("\u{1F4A1} Run bb-browser site recommend to discover adapters matching your browsing habits");
1867
+ }
1868
+ } catch (e) {
1869
+ const message = e instanceof Error ? e.message : String(e);
1870
+ const manualAction = `git clone ${COMMUNITY_REPO} ~/.bb-browser/bb-sites`;
1871
+ if (options.json) {
1872
+ exitJsonError(`Clone failed: ${message}`, { action: manualAction, updateMode });
1873
+ }
1874
+ console.error(`Clone failed: ${e instanceof Error ? e.message : e}`);
1875
+ console.error(` Manual fix: git clone ${COMMUNITY_REPO} ~/.bb-browser/bb-sites`);
1876
+ process.exit(1);
1877
+ }
1878
+ }
1879
+ const sites = scanSites(COMMUNITY_SITES_DIR, "community");
1880
+ if (options.json) {
1881
+ console.log(JSON.stringify({
1882
+ success: true,
1883
+ updateMode,
1884
+ communityRepo: COMMUNITY_REPO,
1885
+ communityDir: COMMUNITY_SITES_DIR,
1886
+ siteCount: sites.length
1887
+ }, null, 2));
1888
+ return;
1889
+ }
1890
+ console.log(`Installed ${sites.length} community adapters.`);
1891
+ console.log(`\u2B50 Like bb-browser? \u2192 bb-browser star`);
1892
+ checkCliUpdate();
1893
+ }
1894
+ function findSiteByName(name) {
1895
+ return getAllSites().find((site) => site.name === name);
1896
+ }
1897
+ function siteInfo(name, options) {
1898
+ const site = findSiteByName(name);
1899
+ if (!site) {
1900
+ if (options.json) {
1901
+ exitJsonError(`adapter "${name}" not found`, { action: "bb-browser site list" });
1902
+ }
1903
+ console.error(`[error] site info: adapter "${name}" not found.`);
1904
+ console.error(" Try: bb-browser site list");
1905
+ process.exit(1);
1906
+ }
1907
+ const meta = {
1908
+ name: site.name,
1909
+ description: site.description,
1910
+ domain: site.domain,
1911
+ args: site.args,
1912
+ example: site.example,
1913
+ readOnly: site.readOnly
1914
+ };
1915
+ if (options.json) {
1916
+ console.log(JSON.stringify(meta, null, 2));
1917
+ return;
1918
+ }
1919
+ console.log(`${site.name} \u2014 ${site.description}`);
1920
+ console.log();
1921
+ console.log("Arguments:");
1922
+ const argEntries = Object.entries(site.args);
1923
+ if (argEntries.length === 0) {
1924
+ console.log(" (none)");
1925
+ } else {
1926
+ for (const [argName, argDef] of argEntries) {
1927
+ const requiredText = argDef.required ? "required" : "optional";
1928
+ const description = argDef.description || "";
1929
+ console.log(` ${argName} (${requiredText}) ${description}`.trimEnd());
1930
+ }
1931
+ }
1932
+ console.log();
1933
+ console.log("Example:");
1934
+ console.log(` ${site.example || `bb-browser site ${site.name}`}`);
1935
+ console.log();
1936
+ console.log(`Domain: ${site.domain || "(not declared)"}`);
1937
+ console.log(`Read-only: ${site.readOnly ? "yes" : "no"}`);
1938
+ }
1939
+ async function siteRecommend(options) {
1940
+ const days = options.days ?? 30;
1941
+ const historyDomains = getHistoryDomains(days);
1942
+ const sites = getAllSites();
1943
+ const sitesByDomain = /* @__PURE__ */ new Map();
1944
+ for (const site of sites) {
1945
+ if (!site.domain) continue;
1946
+ const domain = site.domain.toLowerCase();
1947
+ const existing = sitesByDomain.get(domain) || [];
1948
+ existing.push(site);
1949
+ sitesByDomain.set(domain, existing);
1950
+ }
1951
+ const available = [];
1952
+ const notAvailable = [];
1953
+ for (const item of historyDomains) {
1954
+ const adapters = sitesByDomain.get(item.domain.toLowerCase());
1955
+ if (adapters && adapters.length > 0) {
1956
+ const sortedAdapters = [...adapters].sort((a, b) => a.name.localeCompare(b.name));
1957
+ available.push({
1958
+ domain: item.domain,
1959
+ visits: item.visits,
1960
+ adapterCount: sortedAdapters.length,
1961
+ adapters: sortedAdapters.map((site) => ({
1962
+ name: site.name,
1963
+ description: site.description,
1964
+ example: site.example || `bb-browser site ${site.name}`
1965
+ }))
1966
+ });
1967
+ } else if (item.visits >= 5 && item.domain && !item.domain.includes("localhost") && item.domain.includes(".")) {
1968
+ notAvailable.push(item);
1969
+ }
1970
+ }
1971
+ const jsonData = {
1972
+ days,
1973
+ available,
1974
+ not_available: notAvailable
1975
+ };
1976
+ if (options.jq) {
1977
+ handleJqResponse({ id: generateId(), success: true, data: jsonData });
1978
+ }
1979
+ if (options.json) {
1980
+ console.log(JSON.stringify(jsonData, null, 2));
1981
+ return;
1982
+ }
1983
+ console.log(`Based on your recent ${days} days of browsing history:`);
1984
+ console.log();
1985
+ console.log("\u{1F3AF} Frequently used sites with available adapters:");
1986
+ console.log();
1987
+ if (available.length === 0) {
1988
+ console.log(" (no matching adapters yet)");
1989
+ } else {
1990
+ for (const item of available) {
1991
+ console.log(` ${item.domain.padEnd(20)} ${item.visits} visits ${item.adapterCount} commands`);
1992
+ console.log(` Try: ${item.adapters[0]?.example || `bb-browser site ${item.adapters[0]?.name || ""}`}`);
1993
+ console.log();
1994
+ }
1995
+ }
1996
+ console.log("\u{1F4CB} Frequently used sites without adapters:");
1997
+ console.log();
1998
+ if (notAvailable.length === 0) {
1999
+ console.log(" (none)");
2000
+ } else {
2001
+ for (const item of notAvailable) {
2002
+ console.log(` ${item.domain.padEnd(20)} ${item.visits} visits`);
2003
+ }
2004
+ }
2005
+ console.log();
2006
+ console.log('\u{1F4A1} Tell your AI agent "turn notion.so into a CLI", and it can automate it.');
2007
+ console.log();
2008
+ console.log(`All analysis is done locally. Use --days 7 to view only the last week.`);
2009
+ }
2010
+ async function siteRun(name, args, options) {
2011
+ const sites = getAllSites();
2012
+ const site = sites.find((s) => s.name === name);
2013
+ if (!site) {
2014
+ const fuzzy = sites.filter((s) => s.name.includes(name));
2015
+ if (options.json) {
2016
+ exitJsonError(`site "${name}" not found`, {
2017
+ suggestions: fuzzy.slice(0, 5).map((s) => s.name),
2018
+ action: fuzzy.length > 0 ? void 0 : "bb-browser site update"
2019
+ });
2020
+ }
2021
+ console.error(`[error] site: "${name}" not found.`);
2022
+ if (fuzzy.length > 0) {
2023
+ console.error(" Did you mean:");
2024
+ for (const s of fuzzy.slice(0, 5)) {
2025
+ console.error(` bb-browser site ${s.name}`);
2026
+ }
2027
+ } else {
2028
+ console.error(" Try: bb-browser site list");
2029
+ console.error(" Or: bb-browser site update");
2030
+ }
2031
+ process.exit(1);
2032
+ }
2033
+ const argNames = Object.keys(site.args);
2034
+ const argMap = {};
2035
+ const positionalArgs = [];
2036
+ for (let i = 0; i < args.length; i++) {
2037
+ if (args[i].startsWith("--")) {
2038
+ const flagName = args[i].slice(2);
2039
+ if (flagName in site.args && args[i + 1]) {
2040
+ argMap[flagName] = args[i + 1];
2041
+ i++;
2042
+ }
2043
+ } else {
2044
+ positionalArgs.push(args[i]);
2045
+ }
2046
+ }
2047
+ let posIdx = 0;
2048
+ for (const argName of argNames) {
2049
+ if (!argMap[argName] && posIdx < positionalArgs.length) {
2050
+ argMap[argName] = positionalArgs[posIdx++];
2051
+ }
2052
+ }
2053
+ for (const [argName, argDef] of Object.entries(site.args)) {
2054
+ if (argDef.required && !argMap[argName]) {
2055
+ const usage = argNames.map((a) => {
2056
+ const def = site.args[a];
2057
+ return def.required ? `<${a}>` : `[${a}]`;
2058
+ }).join(" ");
2059
+ if (options.json) {
2060
+ exitJsonError(`missing required argument "${argName}"`, {
2061
+ usage: `bb-browser site ${name} ${usage}`,
2062
+ example: site.example
2063
+ });
2064
+ }
2065
+ console.error(`[error] site ${name}: missing required argument "${argName}".`);
2066
+ console.error(` Usage: bb-browser site ${name} ${usage}`);
2067
+ if (site.example) console.error(` Example: ${site.example}`);
2068
+ process.exit(1);
2069
+ }
2070
+ }
2071
+ const jsContent = readFileSync2(site.filePath, "utf-8");
2072
+ const jsBody = jsContent.replace(/\/\*\s*@meta[\s\S]*?\*\//, "").trim();
2073
+ const argsJson = JSON.stringify(argMap);
2074
+ const script = `(${jsBody})(${argsJson})`;
2075
+ if (options.openclaw) {
2076
+ const { ocGetTabs, ocFindTabByDomain, ocOpenTab, ocEvaluate } = await import("./openclaw-bridge-7BW5M4YX.js");
2077
+ let targetId;
2078
+ if (site.domain) {
2079
+ const tabs = ocGetTabs();
2080
+ const existing = ocFindTabByDomain(tabs, site.domain);
2081
+ if (existing) {
2082
+ targetId = existing.targetId;
2083
+ } else {
2084
+ targetId = ocOpenTab(`https://${site.domain}`);
2085
+ await new Promise((resolve3) => setTimeout(resolve3, 3e3));
2086
+ }
2087
+ } else {
2088
+ const tabs = ocGetTabs();
2089
+ if (tabs.length === 0) {
2090
+ throw new Error("No tabs open in OpenClaw browser");
2091
+ }
2092
+ targetId = tabs[0].targetId;
2093
+ }
2094
+ const wrappedFn = `async () => { const __fn = ${jsBody}; return await __fn(${argsJson}); }`;
2095
+ const parsed2 = ocEvaluate(targetId, wrappedFn);
2096
+ if (typeof parsed2 === "object" && parsed2 !== null && "error" in parsed2) {
2097
+ const errObj = parsed2;
2098
+ const checkText = `${errObj.error} ${errObj.hint || ""}`;
2099
+ const isAuthError = /401|403|unauthorized|forbidden|not.?logged|login.?required|sign.?in|auth/i.test(checkText);
2100
+ const loginHint = isAuthError && site.domain ? `Please log in to https://${site.domain} in your OpenClaw browser first, then retry.` : void 0;
2101
+ const hint = loginHint || errObj.hint;
2102
+ const reportHint = `If this is an adapter bug, report via: gh issue create --repo epiral/bb-sites --title "[${name}] <description>" OR: bb-browser site github/issue-create epiral/bb-sites --title "[${name}] <description>"`;
2103
+ if (options.json) {
2104
+ console.log(JSON.stringify({ id: "openclaw", success: false, error: errObj.error, hint, reportHint }));
2105
+ } else {
2106
+ console.error(`[error] site ${name}: ${errObj.error}`);
2107
+ if (hint) console.error(` Hint: ${hint}`);
2108
+ console.error(` Report: gh issue create --repo epiral/bb-sites --title "[${name}] ..."`);
2109
+ console.error(` or: bb-browser site github/issue-create epiral/bb-sites --title "[${name}] ..."`);
2110
+ }
2111
+ process.exit(1);
2112
+ }
2113
+ if (options.jq) {
2114
+ const { applyJq: applyJq2 } = await import("./jq-PS4THHQK.js");
2115
+ const expr = options.jq.replace(/^\.data\./, ".");
2116
+ const results = applyJq2(parsed2, expr);
2117
+ for (const r of results) {
2118
+ console.log(typeof r === "string" ? r : JSON.stringify(r));
2119
+ }
2120
+ } else if (options.json) {
2121
+ console.log(JSON.stringify({ id: "openclaw", success: true, data: parsed2 }));
2122
+ } else {
2123
+ console.log(JSON.stringify(parsed2, null, 2));
2124
+ }
2125
+ return;
2126
+ }
2127
+ await ensureDaemonRunning();
2128
+ let targetTabId = options.tabId;
2129
+ if (!targetTabId && site.domain) {
2130
+ const listReq = { id: generateId(), action: "tab_list" };
2131
+ const listResp = await sendCommand2(listReq);
2132
+ if (listResp.success && listResp.data?.tabs) {
2133
+ const matchingTab = listResp.data.tabs.find(
2134
+ (tab) => matchTabOrigin(tab.url, site.domain)
2135
+ );
2136
+ if (matchingTab) {
2137
+ targetTabId = matchingTab.tabId;
2138
+ }
2139
+ }
2140
+ if (!targetTabId) {
2141
+ const newResp = await sendCommand2({
2142
+ id: generateId(),
2143
+ action: "tab_new",
2144
+ url: `https://${site.domain}`
2145
+ });
2146
+ targetTabId = newResp.data?.tabId;
2147
+ await new Promise((resolve3) => setTimeout(resolve3, 3e3));
2148
+ }
2149
+ }
2150
+ const evalReq = { id: generateId(), action: "eval", script, tabId: targetTabId };
2151
+ const evalResp = await sendCommand2(evalReq);
2152
+ if (!evalResp.success) {
2153
+ const hint = site.domain ? `Open https://${site.domain} in your browser, make sure you are logged in, then retry.` : void 0;
2154
+ if (options.json) {
2155
+ console.log(JSON.stringify({ id: evalReq.id, success: false, error: evalResp.error || "eval failed", hint }));
2156
+ } else {
2157
+ console.error(`[error] site ${name}: ${evalResp.error || "eval failed"}`);
2158
+ if (hint) console.error(` Hint: ${hint}`);
2159
+ }
2160
+ process.exit(1);
2161
+ }
2162
+ const result = evalResp.data?.result;
2163
+ if (result === void 0 || result === null) {
2164
+ if (options.json) {
2165
+ console.log(JSON.stringify({ id: evalReq.id, success: true, data: null }));
2166
+ } else {
2167
+ console.log("(no output)");
2168
+ }
2169
+ return;
2170
+ }
2171
+ let parsed;
2172
+ try {
2173
+ parsed = typeof result === "string" ? JSON.parse(result) : result;
2174
+ } catch {
2175
+ parsed = result;
2176
+ }
2177
+ if (typeof parsed === "object" && parsed !== null && "error" in parsed) {
2178
+ const errObj = parsed;
2179
+ const checkText = `${errObj.error} ${errObj.hint || ""}`;
2180
+ const isAuthError = /401|403|unauthorized|forbidden|not.?logged|login.?required|sign.?in|auth/i.test(checkText);
2181
+ const loginHint = isAuthError && site.domain ? `Please log in to https://${site.domain} in your browser first, then retry.` : void 0;
2182
+ const hint = loginHint || errObj.hint;
2183
+ const reportHint = `If this is an adapter bug, report via: gh issue create --repo epiral/bb-sites --title "[${name}] <description>" OR: bb-browser site github/issue-create epiral/bb-sites --title "[${name}] <description>"`;
2184
+ if (options.json) {
2185
+ console.log(JSON.stringify({ id: evalReq.id, success: false, error: errObj.error, hint, reportHint }));
2186
+ } else {
2187
+ console.error(`[error] site ${name}: ${errObj.error}`);
2188
+ if (hint) console.error(` Hint: ${hint}`);
2189
+ console.error(` Report: gh issue create --repo epiral/bb-sites --title "[${name}] ..."`);
2190
+ console.error(` or: bb-browser site github/issue-create epiral/bb-sites --title "[${name}] ..."`);
2191
+ }
2192
+ process.exit(1);
2193
+ }
2194
+ if (options.jq) {
2195
+ const { applyJq: applyJq2 } = await import("./jq-PS4THHQK.js");
2196
+ const expr = options.jq.replace(/^\.data\./, ".");
2197
+ const results = applyJq2(parsed, expr);
2198
+ for (const r of results) {
2199
+ console.log(typeof r === "string" ? r : JSON.stringify(r));
2200
+ }
2201
+ } else if (options.json) {
2202
+ console.log(JSON.stringify({ id: evalReq.id, success: true, data: parsed }));
2203
+ } else {
2204
+ console.log(JSON.stringify(parsed, null, 2));
2205
+ }
2206
+ }
2207
+ async function siteCommand(args, options = {}) {
2208
+ const subCommand = args[0];
2209
+ if (!subCommand || subCommand === "--help" || subCommand === "-h") {
2210
+ console.log(`bb-browser site - Website CLI adapter manager and runner
2211
+
2212
+ \u7528\u6CD5:
2213
+ bb-browser site list List all available adapters
2214
+ bb-browser site info <name> View adapter metadata
2215
+ bb-browser site recommend Recommend adapters based on history
2216
+ bb-browser site search <query> Search adapters
2217
+ bb-browser site <name> [args...] Run adapter (shorthand)
2218
+ bb-browser site run <name> [args...] Run adapter
2219
+ bb-browser site update Update community adapter repository (git clone/pull)
2220
+
2221
+ Directories:
2222
+ ${LOCAL_SITES_DIR} Private adapters (preferred)
2223
+ ${COMMUNITY_SITES_DIR} Community adapters
2224
+
2225
+ Examples:
2226
+ bb-browser site update
2227
+ bb-browser site list
2228
+ bb-browser site reddit/thread https://www.reddit.com/r/LocalLLaMA/comments/...
2229
+ bb-browser site twitter/user yan5xu
2230
+ bb-browser site search reddit
2231
+
2232
+ Create a new adapter: bb-browser guide
2233
+ Report issues: gh issue create --repo epiral/bb-sites --title "[adapter-name] description"
2234
+ Contribute to community: https://github.com/epiral/bb-sites`);
2235
+ return;
2236
+ }
2237
+ switch (subCommand) {
2238
+ case "list":
2239
+ siteList(options);
2240
+ break;
2241
+ case "search":
2242
+ if (!args[1]) {
2243
+ console.error("[error] site search: <query> is required.");
2244
+ console.error(" Usage: bb-browser site search <query>");
2245
+ process.exit(1);
2246
+ }
2247
+ siteSearch(args[1], options);
2248
+ break;
2249
+ case "info":
2250
+ if (!args[1]) {
2251
+ console.error("[error] site info: <name> is required.");
2252
+ console.error(" Usage: bb-browser site info <name>");
2253
+ process.exit(1);
2254
+ }
2255
+ siteInfo(args[1], options);
2256
+ break;
2257
+ case "recommend":
2258
+ await siteRecommend(options);
2259
+ break;
2260
+ case "update":
2261
+ siteUpdate(options);
2262
+ break;
2263
+ case "run":
2264
+ if (!args[1]) {
2265
+ console.error("[error] site run: <name> is required.");
2266
+ console.error(" Usage: bb-browser site run <name> [args...]");
2267
+ console.error(" Try: bb-browser site list");
2268
+ process.exit(1);
2269
+ }
2270
+ await siteRun(args[1], args.slice(2), options);
2271
+ break;
2272
+ default:
2273
+ if (subCommand.includes("/")) {
2274
+ await siteRun(subCommand, args.slice(1), options);
2275
+ } else {
2276
+ console.error(`[error] site: unknown subcommand "${subCommand}".`);
2277
+ console.error(" Available: list, info, recommend, search, run, update");
2278
+ console.error(" Try: bb-browser site --help");
2279
+ process.exit(1);
2280
+ }
2281
+ break;
2282
+ }
2283
+ silentUpdate();
2284
+ }
2285
+ function silentUpdate() {
2286
+ const gitDir = join2(COMMUNITY_SITES_DIR, ".git");
2287
+ if (!existsSync5(gitDir)) return;
2288
+ import("child_process").then(({ spawn: spawn3 }) => {
2289
+ const child = spawn3("git", ["pull", "--ff-only"], {
2290
+ cwd: COMMUNITY_SITES_DIR,
2291
+ stdio: "ignore",
2292
+ detached: true
2293
+ });
2294
+ child.unref();
2295
+ }).catch(() => {
2296
+ });
2297
+ }
2298
+
2299
+ // packages/cli/src/commands/open.ts
2300
+ async function openCommand(url, options = {}) {
2301
+ if (!url) {
2302
+ throw new Error("Missing URL argument");
2303
+ }
2304
+ await ensureDaemonRunning();
2305
+ let normalizedUrl = url;
2306
+ if (!url.startsWith("http://") && !url.startsWith("https://")) {
2307
+ normalizedUrl = "https://" + url;
2308
+ }
2309
+ const request = {
2310
+ id: generateId(),
2311
+ action: "open",
2312
+ url: normalizedUrl
2313
+ };
2314
+ if (options.tab !== void 0) {
2315
+ if (options.tab === "current") {
2316
+ request.tabId = "current";
2317
+ } else {
2318
+ const tabId = parseInt(options.tab, 10);
2319
+ if (isNaN(tabId)) {
2320
+ throw new Error(`Invalid tabId: ${options.tab}`);
2321
+ }
2322
+ request.tabId = tabId;
2323
+ }
2324
+ }
2325
+ const response = await sendCommand2(request);
2326
+ if (options.json) {
2327
+ console.log(JSON.stringify(response, null, 2));
2328
+ } else {
2329
+ if (response.success) {
2330
+ console.log(`Opened: ${response.data?.url ?? normalizedUrl}`);
2331
+ if (response.data?.title) {
2332
+ console.log(`Title: ${response.data.title}`);
2333
+ }
2334
+ if (response.data?.tabId) {
2335
+ console.log(`Tab ID: ${response.data.tabId}`);
2336
+ }
2337
+ const siteHint = getSiteHintForDomain(normalizedUrl);
2338
+ if (siteHint) {
2339
+ console.log(`
2340
+ \u{1F4A1} ${siteHint}`);
2341
+ }
2342
+ } else {
2343
+ console.error(`Error: ${response.error}`);
2344
+ process.exit(1);
2345
+ }
2346
+ }
2347
+ }
2348
+
2349
+ // packages/cli/src/commands/snapshot.ts
2350
+ async function snapshotCommand(options = {}) {
2351
+ await ensureDaemonRunning();
2352
+ const request = {
2353
+ id: generateId(),
2354
+ action: "snapshot",
2355
+ interactive: options.interactive,
2356
+ compact: options.compact,
2357
+ maxDepth: options.maxDepth,
2358
+ selector: options.selector,
2359
+ tabId: options.tabId
2360
+ };
2361
+ const response = await sendCommand2(request);
2362
+ if (options.json) {
2363
+ console.log(JSON.stringify(response, null, 2));
2364
+ } else {
2365
+ if (response.success) {
2366
+ console.log(`Title: ${response.data?.title ?? "(untitled)"}`);
2367
+ console.log(`URL: ${response.data?.url ?? "(unknown)"}`);
2368
+ if (response.data?.snapshotData?.snapshot) {
2369
+ console.log("");
2370
+ console.log(response.data.snapshotData.snapshot);
2371
+ }
2372
+ } else {
2373
+ console.error(`Error: ${response.error}`);
2374
+ process.exit(1);
2375
+ }
2376
+ }
2377
+ }
2378
+
2379
+ // packages/cli/src/commands/click.ts
2380
+ function parseRef2(ref) {
2381
+ return ref.startsWith("@") ? ref.slice(1) : ref;
2382
+ }
2383
+ async function clickCommand(ref, options = {}) {
2384
+ if (!ref) {
2385
+ throw new Error("Missing ref argument");
2386
+ }
2387
+ await ensureDaemonRunning();
2388
+ const parsedRef = parseRef2(ref);
2389
+ const request = {
2390
+ id: generateId(),
2391
+ action: "click",
2392
+ ref: parsedRef,
2393
+ tabId: options.tabId
2394
+ };
2395
+ const response = await sendCommand2(request);
2396
+ if (options.json) {
2397
+ console.log(JSON.stringify(response, null, 2));
2398
+ } else {
2399
+ if (response.success) {
2400
+ const role = response.data?.role ?? "element";
2401
+ const name = response.data?.name;
2402
+ if (name) {
2403
+ console.log(`Clicked: ${role} "${name}"`);
2404
+ } else {
2405
+ console.log(`Clicked: ${role}`);
2406
+ }
2407
+ } else {
2408
+ console.error(`Error: ${response.error}`);
2409
+ process.exit(1);
2410
+ }
2411
+ }
2412
+ }
2413
+
2414
+ // packages/cli/src/commands/hover.ts
2415
+ function parseRef3(ref) {
2416
+ return ref.startsWith("@") ? ref.slice(1) : ref;
2417
+ }
2418
+ async function hoverCommand(ref, options = {}) {
2419
+ if (!ref) {
2420
+ throw new Error("Missing ref argument");
2421
+ }
2422
+ await ensureDaemonRunning();
2423
+ const parsedRef = parseRef3(ref);
2424
+ const request = {
2425
+ id: generateId(),
2426
+ action: "hover",
2427
+ ref: parsedRef,
2428
+ tabId: options.tabId
2429
+ };
2430
+ const response = await sendCommand2(request);
2431
+ if (options.json) {
2432
+ console.log(JSON.stringify(response, null, 2));
2433
+ } else {
2434
+ if (response.success) {
2435
+ const role = response.data?.role ?? "element";
2436
+ const name = response.data?.name;
2437
+ if (name) {
2438
+ console.log(`Hovered: ${role} "${name}"`);
2439
+ } else {
2440
+ console.log(`Hovered: ${role}`);
2441
+ }
2442
+ } else {
2443
+ console.error(`Error: ${response.error}`);
2444
+ process.exit(1);
2445
+ }
2446
+ }
2447
+ }
2448
+
2449
+ // packages/cli/src/commands/fill.ts
2450
+ function parseRef4(ref) {
2451
+ return ref.startsWith("@") ? ref.slice(1) : ref;
2452
+ }
2453
+ async function fillCommand(ref, text, options = {}) {
2454
+ if (!ref) {
2455
+ throw new Error("Missing ref argument");
2456
+ }
2457
+ if (text === void 0 || text === null) {
2458
+ throw new Error("Missing text argument");
2459
+ }
2460
+ await ensureDaemonRunning();
2461
+ const parsedRef = parseRef4(ref);
2462
+ const request = {
2463
+ id: generateId(),
2464
+ action: "fill",
2465
+ ref: parsedRef,
2466
+ text,
2467
+ tabId: options.tabId
2468
+ };
2469
+ const response = await sendCommand2(request);
2470
+ if (options.json) {
2471
+ console.log(JSON.stringify(response, null, 2));
2472
+ } else {
2473
+ if (response.success) {
2474
+ const role = response.data?.role ?? "element";
2475
+ const name = response.data?.name;
2476
+ if (name) {
2477
+ console.log(`Filled: ${role} "${name}"`);
2478
+ } else {
2479
+ console.log(`Filled: ${role}`);
2480
+ }
2481
+ console.log(`Content: "${text}"`);
2482
+ } else {
2483
+ console.error(`Error: ${response.error}`);
2484
+ process.exit(1);
2485
+ }
2486
+ }
2487
+ }
2488
+
2489
+ // packages/cli/src/commands/type.ts
2490
+ function parseRef5(ref) {
2491
+ return ref.startsWith("@") ? ref.slice(1) : ref;
2492
+ }
2493
+ async function typeCommand(ref, text, options = {}) {
2494
+ if (!ref) {
2495
+ throw new Error("Missing ref argument");
2496
+ }
2497
+ if (text === void 0 || text === null) {
2498
+ throw new Error("Missing text argument");
2499
+ }
2500
+ await ensureDaemonRunning();
2501
+ const parsedRef = parseRef5(ref);
2502
+ const request = {
2503
+ id: generateId(),
2504
+ action: "type",
2505
+ ref: parsedRef,
2506
+ text,
2507
+ tabId: options.tabId
2508
+ };
2509
+ const response = await sendCommand2(request);
2510
+ if (options.json) {
2511
+ console.log(JSON.stringify(response, null, 2));
2512
+ } else {
2513
+ if (response.success) {
2514
+ const role = response.data?.role ?? "element";
2515
+ const name = response.data?.name;
2516
+ if (name) {
2517
+ console.log(`Typed: ${role} "${name}"`);
2518
+ } else {
2519
+ console.log(`Typed: ${role}`);
2520
+ }
2521
+ console.log(`Content: "${text}"`);
2522
+ } else {
2523
+ console.error(`Error: ${response.error}`);
2524
+ process.exit(1);
2525
+ }
2526
+ }
2527
+ }
2528
+
2529
+ // packages/cli/src/commands/close.ts
2530
+ async function closeCommand(options = {}) {
2531
+ await ensureDaemonRunning();
2532
+ const request = {
2533
+ id: generateId(),
2534
+ action: "close",
2535
+ tabId: options.tabId
2536
+ };
2537
+ const response = await sendCommand2(request);
2538
+ if (options.json) {
2539
+ console.log(JSON.stringify(response, null, 2));
2540
+ } else {
2541
+ if (response.success) {
2542
+ const title = response.data?.title ?? "";
2543
+ if (title) {
2544
+ console.log(`Closed: "${title}"`);
2545
+ } else {
2546
+ console.log("Closed current tab");
2547
+ }
2548
+ } else {
2549
+ console.error(`Error: ${response.error}`);
2550
+ process.exit(1);
2551
+ }
2552
+ }
2553
+ }
2554
+
2555
+ // packages/cli/src/commands/get.ts
2556
+ function parseRef6(ref) {
2557
+ return ref.startsWith("@") ? ref.slice(1) : ref;
2558
+ }
2559
+ async function getCommand(attribute, ref, options = {}) {
2560
+ if (attribute === "text" && !ref) {
2561
+ throw new Error("get text requires ref argument, e.g. get text @5");
2562
+ }
2563
+ await ensureDaemonRunning();
2564
+ const request = {
2565
+ id: generateId(),
2566
+ action: "get",
2567
+ attribute,
2568
+ ref: ref ? parseRef6(ref) : void 0,
2569
+ tabId: options.tabId
2570
+ };
2571
+ const response = await sendCommand2(request);
2572
+ if (options.json) {
2573
+ console.log(JSON.stringify(response, null, 2));
2574
+ } else {
2575
+ if (response.success) {
2576
+ const value = response.data?.value ?? "";
2577
+ console.log(value);
2578
+ } else {
2579
+ console.error(`Error: ${response.error}`);
2580
+ process.exit(1);
2581
+ }
2582
+ }
2583
+ }
2584
+
2585
+ // packages/cli/src/commands/screenshot.ts
2586
+ import fs from "fs";
2587
+ import path4 from "path";
2588
+ import os4 from "os";
2589
+ function getDefaultPath() {
2590
+ const timestamp = (/* @__PURE__ */ new Date()).toISOString().replace(/[:.]/g, "-");
2591
+ const filename = `bb-screenshot-${timestamp}.png`;
2592
+ return path4.join(os4.tmpdir(), filename);
2593
+ }
2594
+ function saveBase64Image(dataUrl, filePath) {
2595
+ const base64Data = dataUrl.replace(/^data:image\/png;base64,/, "");
2596
+ const buffer = Buffer.from(base64Data, "base64");
2597
+ const dir = path4.dirname(filePath);
2598
+ if (!fs.existsSync(dir)) {
2599
+ fs.mkdirSync(dir, { recursive: true });
2600
+ }
2601
+ fs.writeFileSync(filePath, buffer);
2602
+ }
2603
+ async function screenshotCommand(outputPath, options = {}) {
2604
+ await ensureDaemonRunning();
2605
+ const filePath = outputPath ? path4.resolve(outputPath) : getDefaultPath();
2606
+ const request = {
2607
+ id: generateId(),
2608
+ action: "screenshot",
2609
+ tabId: options.tabId
2610
+ };
2611
+ const response = await sendCommand2(request);
2612
+ if (response.success && response.data?.dataUrl) {
2613
+ const dataUrl = response.data.dataUrl;
2614
+ saveBase64Image(dataUrl, filePath);
2615
+ if (options.json) {
2616
+ console.log(JSON.stringify({
2617
+ success: true,
2618
+ path: filePath,
2619
+ base64: dataUrl
2620
+ }, null, 2));
2621
+ } else {
2622
+ console.log(`Screenshot saved: ${filePath}`);
2623
+ }
2624
+ } else {
2625
+ if (options.json) {
2626
+ console.log(JSON.stringify(response, null, 2));
2627
+ } else {
2628
+ console.error(`Error: ${response.error}`);
2629
+ }
2630
+ process.exit(1);
2631
+ }
2632
+ }
2633
+
2634
+ // packages/cli/src/commands/wait.ts
2635
+ function isTimeWait(target) {
2636
+ return /^\d+$/.test(target);
2637
+ }
2638
+ function parseRef7(ref) {
2639
+ return ref.startsWith("@") ? ref.slice(1) : ref;
2640
+ }
2641
+ async function waitCommand(target, options = {}) {
2642
+ if (!target) {
2643
+ throw new Error("Missing wait target argument");
2644
+ }
2645
+ await ensureDaemonRunning();
2646
+ let request;
2647
+ if (isTimeWait(target)) {
2648
+ const ms = parseInt(target, 10);
2649
+ request = {
2650
+ id: generateId(),
2651
+ action: "wait",
2652
+ waitType: "time",
2653
+ ms,
2654
+ tabId: options.tabId
2655
+ };
2656
+ } else {
2657
+ const ref = parseRef7(target);
2658
+ request = {
2659
+ id: generateId(),
2660
+ action: "wait",
2661
+ waitType: "element",
2662
+ ref,
2663
+ tabId: options.tabId
2664
+ };
2665
+ }
2666
+ const response = await sendCommand2(request);
2667
+ if (options.json) {
2668
+ console.log(JSON.stringify(response, null, 2));
2669
+ } else {
2670
+ if (response.success) {
2671
+ if (isTimeWait(target)) {
2672
+ console.log(`Waited ${target}ms`);
2673
+ } else {
2674
+ console.log(`Element @${parseRef7(target)} is now visible`);
2675
+ }
2676
+ } else {
2677
+ console.error(`Error: ${response.error}`);
2678
+ process.exit(1);
2679
+ }
2680
+ }
2681
+ }
2682
+
2683
+ // packages/cli/src/commands/press.ts
2684
+ function parseKey(keyString) {
2685
+ const parts = keyString.split("+");
2686
+ const modifierNames = ["Control", "Alt", "Shift", "Meta"];
2687
+ const modifiers = [];
2688
+ let key = "";
2689
+ for (const part of parts) {
2690
+ if (modifierNames.includes(part)) {
2691
+ modifiers.push(part);
2692
+ } else {
2693
+ key = part;
2694
+ }
2695
+ }
2696
+ return { key, modifiers };
2697
+ }
2698
+ async function pressCommand(keyString, options = {}) {
2699
+ if (!keyString) {
2700
+ throw new Error("Missing key argument");
2701
+ }
2702
+ await ensureDaemonRunning();
2703
+ const { key, modifiers } = parseKey(keyString);
2704
+ if (!key) {
2705
+ throw new Error("Invalid key format");
2706
+ }
2707
+ const request = {
2708
+ id: generateId(),
2709
+ action: "press",
2710
+ key,
2711
+ modifiers,
2712
+ tabId: options.tabId
2713
+ };
2714
+ const response = await sendCommand2(request);
2715
+ if (options.json) {
2716
+ console.log(JSON.stringify(response, null, 2));
2717
+ } else {
2718
+ if (response.success) {
2719
+ const displayKey = modifiers.length > 0 ? `${modifiers.join("+")}+${key}` : key;
2720
+ console.log(`Pressed: ${displayKey}`);
2721
+ } else {
2722
+ console.error(`Error: ${response.error}`);
2723
+ process.exit(1);
2724
+ }
2725
+ }
2726
+ }
2727
+
2728
+ // packages/cli/src/commands/scroll.ts
2729
+ var VALID_DIRECTIONS = ["up", "down", "left", "right"];
2730
+ var DEFAULT_PIXELS = 300;
2731
+ async function scrollCommand(direction, pixels, options = {}) {
2732
+ if (!direction) {
2733
+ throw new Error("Missing direction argument");
2734
+ }
2735
+ if (!VALID_DIRECTIONS.includes(direction)) {
2736
+ throw new Error(
2737
+ `Invalid scroll direction: ${direction}, supported: ${VALID_DIRECTIONS.join(", ")}`
2738
+ );
2739
+ }
2740
+ let pixelValue = DEFAULT_PIXELS;
2741
+ if (pixels !== void 0) {
2742
+ pixelValue = parseInt(pixels, 10);
2743
+ if (isNaN(pixelValue) || pixelValue <= 0) {
2744
+ throw new Error(`Invalid pixel value: ${pixels}`);
2745
+ }
2746
+ }
2747
+ await ensureDaemonRunning();
2748
+ const request = {
2749
+ id: generateId(),
2750
+ action: "scroll",
2751
+ direction,
2752
+ pixels: pixelValue,
2753
+ tabId: options.tabId
2754
+ };
2755
+ const response = await sendCommand2(request);
2756
+ if (options.json) {
2757
+ console.log(JSON.stringify(response, null, 2));
2758
+ } else {
2759
+ if (response.success) {
2760
+ console.log(`Scrolled: ${direction} ${pixelValue}px`);
2761
+ } else {
2762
+ console.error(`Error: ${response.error}`);
2763
+ process.exit(1);
2764
+ }
2765
+ }
2766
+ }
2767
+
2768
+ // packages/cli/src/commands/nav.ts
2769
+ async function backCommand(options = {}) {
2770
+ await ensureDaemonRunning();
2771
+ const request = {
2772
+ id: generateId(),
2773
+ action: "back",
2774
+ tabId: options.tabId
2775
+ };
2776
+ const response = await sendCommand2(request);
2777
+ if (options.json) {
2778
+ console.log(JSON.stringify(response, null, 2));
2779
+ } else {
2780
+ if (response.success) {
2781
+ const url = response.data?.url ?? "";
2782
+ if (url) {
2783
+ console.log(`Navigated back to: ${url}`);
2784
+ } else {
2785
+ console.log("Navigated back");
2786
+ }
2787
+ } else {
2788
+ console.error(`Error: ${response.error}`);
2789
+ process.exit(1);
2790
+ }
2791
+ }
2792
+ }
2793
+ async function forwardCommand(options = {}) {
2794
+ await ensureDaemonRunning();
2795
+ const request = {
2796
+ id: generateId(),
2797
+ action: "forward",
2798
+ tabId: options.tabId
2799
+ };
2800
+ const response = await sendCommand2(request);
2801
+ if (options.json) {
2802
+ console.log(JSON.stringify(response, null, 2));
2803
+ } else {
2804
+ if (response.success) {
2805
+ const url = response.data?.url ?? "";
2806
+ if (url) {
2807
+ console.log(`Navigated forward to: ${url}`);
2808
+ } else {
2809
+ console.log("Navigated forward");
2810
+ }
2811
+ } else {
2812
+ console.error(`Error: ${response.error}`);
2813
+ process.exit(1);
2814
+ }
2815
+ }
2816
+ }
2817
+ async function refreshCommand(options = {}) {
2818
+ await ensureDaemonRunning();
2819
+ const request = {
2820
+ id: generateId(),
2821
+ action: "refresh",
2822
+ tabId: options.tabId
2823
+ };
2824
+ const response = await sendCommand2(request);
2825
+ if (options.json) {
2826
+ console.log(JSON.stringify(response, null, 2));
2827
+ } else {
2828
+ if (response.success) {
2829
+ const title = response.data?.title ?? "";
2830
+ if (title) {
2831
+ console.log(`Refreshed: "${title}"`);
2832
+ } else {
2833
+ console.log("Refreshed page");
2834
+ }
2835
+ } else {
2836
+ console.error(`Error: ${response.error}`);
2837
+ process.exit(1);
2838
+ }
2839
+ }
2840
+ }
2841
+
2842
+ // packages/cli/src/commands/check.ts
2843
+ function parseRef8(ref) {
2844
+ return ref.startsWith("@") ? ref.slice(1) : ref;
2845
+ }
2846
+ async function checkCommand(ref, options = {}) {
2847
+ if (!ref) {
2848
+ throw new Error("Missing ref argument");
2849
+ }
2850
+ await ensureDaemonRunning();
2851
+ const parsedRef = parseRef8(ref);
2852
+ const request = {
2853
+ id: generateId(),
2854
+ action: "check",
2855
+ ref: parsedRef,
2856
+ tabId: options.tabId
2857
+ };
2858
+ const response = await sendCommand2(request);
2859
+ if (options.json) {
2860
+ console.log(JSON.stringify(response, null, 2));
2861
+ } else {
2862
+ if (response.success) {
2863
+ const role = response.data?.role ?? "checkbox";
2864
+ const name = response.data?.name;
2865
+ const wasAlreadyChecked = response.data?.wasAlreadyChecked;
2866
+ if (wasAlreadyChecked) {
2867
+ if (name) {
2868
+ console.log(`Checked (already checked): ${role} "${name}"`);
2869
+ } else {
2870
+ console.log(`Checked (already checked): ${role}`);
2871
+ }
2872
+ } else {
2873
+ if (name) {
2874
+ console.log(`Checked: ${role} "${name}"`);
2875
+ } else {
2876
+ console.log(`Checked: ${role}`);
2877
+ }
2878
+ }
2879
+ } else {
2880
+ console.error(`Error: ${response.error}`);
2881
+ process.exit(1);
2882
+ }
2883
+ }
2884
+ }
2885
+ async function uncheckCommand(ref, options = {}) {
2886
+ if (!ref) {
2887
+ throw new Error("Missing ref argument");
2888
+ }
2889
+ await ensureDaemonRunning();
2890
+ const parsedRef = parseRef8(ref);
2891
+ const request = {
2892
+ id: generateId(),
2893
+ action: "uncheck",
2894
+ ref: parsedRef,
2895
+ tabId: options.tabId
2896
+ };
2897
+ const response = await sendCommand2(request);
2898
+ if (options.json) {
2899
+ console.log(JSON.stringify(response, null, 2));
2900
+ } else {
2901
+ if (response.success) {
2902
+ const role = response.data?.role ?? "checkbox";
2903
+ const name = response.data?.name;
2904
+ const wasAlreadyUnchecked = response.data?.wasAlreadyUnchecked;
2905
+ if (wasAlreadyUnchecked) {
2906
+ if (name) {
2907
+ console.log(`Unchecked (already unchecked): ${role} "${name}"`);
2908
+ } else {
2909
+ console.log(`Unchecked (already unchecked): ${role}`);
2910
+ }
2911
+ } else {
2912
+ if (name) {
2913
+ console.log(`Unchecked: ${role} "${name}"`);
2914
+ } else {
2915
+ console.log(`Unchecked: ${role}`);
2916
+ }
2917
+ }
2918
+ } else {
2919
+ console.error(`Error: ${response.error}`);
2920
+ process.exit(1);
2921
+ }
2922
+ }
2923
+ }
2924
+
2925
+ // packages/cli/src/commands/select.ts
2926
+ function parseRef9(ref) {
2927
+ return ref.startsWith("@") ? ref.slice(1) : ref;
2928
+ }
2929
+ async function selectCommand(ref, value, options = {}) {
2930
+ if (!ref) {
2931
+ throw new Error("Missing ref argument");
2932
+ }
2933
+ if (value === void 0 || value === null) {
2934
+ throw new Error("Missing value argument");
2935
+ }
2936
+ await ensureDaemonRunning();
2937
+ const parsedRef = parseRef9(ref);
2938
+ const request = {
2939
+ id: generateId(),
2940
+ action: "select",
2941
+ ref: parsedRef,
2942
+ value,
2943
+ tabId: options.tabId
2944
+ };
2945
+ const response = await sendCommand2(request);
2946
+ if (options.json) {
2947
+ console.log(JSON.stringify(response, null, 2));
2948
+ } else {
2949
+ if (response.success) {
2950
+ const role = response.data?.role ?? "combobox";
2951
+ const name = response.data?.name;
2952
+ const selectedValue = response.data?.selectedValue;
2953
+ const selectedLabel = response.data?.selectedLabel;
2954
+ if (name) {
2955
+ console.log(`Selected: ${role} "${name}"`);
2956
+ } else {
2957
+ console.log(`Selected: ${role}`);
2958
+ }
2959
+ if (selectedLabel && selectedLabel !== selectedValue) {
2960
+ console.log(`Option: "${selectedLabel}" (value="${selectedValue}")`);
2961
+ } else {
2962
+ console.log(`Option: "${selectedValue}"`);
2963
+ }
2964
+ } else {
2965
+ console.error(`Error: ${response.error}`);
2966
+ process.exit(1);
2967
+ }
2968
+ }
2969
+ }
2970
+
2971
+ // packages/cli/src/commands/eval.ts
2972
+ async function evalCommand(script, options = {}) {
2973
+ if (!script) {
2974
+ throw new Error("Missing script argument");
2975
+ }
2976
+ await ensureDaemonRunning();
2977
+ const request = {
2978
+ id: generateId(),
2979
+ action: "eval",
2980
+ script,
2981
+ tabId: options.tabId
2982
+ };
2983
+ const response = await sendCommand2(request);
2984
+ if (options.json) {
2985
+ console.log(JSON.stringify(response, null, 2));
2986
+ } else {
2987
+ if (response.success) {
2988
+ const result = response.data?.result;
2989
+ if (result !== void 0) {
2990
+ if (typeof result === "object" && result !== null) {
2991
+ console.log(JSON.stringify(result, null, 2));
2992
+ } else {
2993
+ console.log(result);
2994
+ }
2995
+ } else {
2996
+ console.log("undefined");
2997
+ }
2998
+ } else {
2999
+ console.error(`Error: ${response.error}`);
3000
+ process.exit(1);
3001
+ }
3002
+ }
3003
+ }
3004
+
3005
+ // packages/cli/src/commands/tab.ts
3006
+ function parseTabSubcommand(args, rawArgv) {
3007
+ let tabId;
3008
+ if (rawArgv) {
3009
+ const idIdx = rawArgv.indexOf("--id");
3010
+ if (idIdx >= 0 && rawArgv[idIdx + 1]) {
3011
+ tabId = parseInt(rawArgv[idIdx + 1], 10);
3012
+ if (isNaN(tabId)) {
3013
+ throw new Error(`Invalid tabId: ${rawArgv[idIdx + 1]}`);
3014
+ }
3015
+ }
3016
+ }
3017
+ if (args.length === 0) {
3018
+ return { action: "tab_list" };
3019
+ }
3020
+ const first = args[0];
3021
+ if (first === "list") {
3022
+ return { action: "tab_list" };
3023
+ }
3024
+ if (first === "new") {
3025
+ return { action: "tab_new", url: args[1] };
3026
+ }
3027
+ if (first === "select") {
3028
+ if (tabId !== void 0) {
3029
+ return { action: "tab_select", tabId };
3030
+ }
3031
+ throw new Error("tab select requires --id argument. Usage: bb-browser tab select --id <tabId>");
3032
+ }
3033
+ if (first === "close") {
3034
+ if (tabId !== void 0) {
3035
+ return { action: "tab_close", tabId };
3036
+ }
3037
+ const indexArg = args[1];
3038
+ if (indexArg !== void 0) {
3039
+ const index2 = parseInt(indexArg, 10);
3040
+ if (isNaN(index2) || index2 < 0) {
3041
+ throw new Error(`Invalid tab index: ${indexArg}`);
3042
+ }
3043
+ return { action: "tab_close", index: index2 };
3044
+ }
3045
+ return { action: "tab_close" };
3046
+ }
3047
+ const index = parseInt(first, 10);
3048
+ if (!isNaN(index) && index >= 0) {
3049
+ return { action: "tab_select", index };
3050
+ }
3051
+ throw new Error(`Unknown tab subcommand: ${first}`);
3052
+ }
3053
+ function formatTabList(tabs, activeIndex) {
3054
+ const lines = [];
3055
+ lines.push(`Tab list (${tabs.length} total, current #${activeIndex}):`);
3056
+ for (const tab of tabs) {
3057
+ const prefix = tab.active ? "*" : " ";
3058
+ const title = tab.title || "(untitled)";
3059
+ lines.push(`${prefix} [${tab.index}] ${tab.url} - ${title}`);
3060
+ }
3061
+ return lines.join("\n");
3062
+ }
3063
+ async function tabCommand(args, options = {}) {
3064
+ await ensureDaemonRunning();
3065
+ const parsed = parseTabSubcommand(args, process.argv);
3066
+ const request = {
3067
+ id: generateId(),
3068
+ action: parsed.action,
3069
+ url: parsed.url,
3070
+ index: parsed.index,
3071
+ tabId: parsed.tabId
3072
+ };
3073
+ const response = await sendCommand2(request);
3074
+ if (options.json) {
3075
+ console.log(JSON.stringify(response, null, 2));
3076
+ } else {
3077
+ if (response.success) {
3078
+ switch (parsed.action) {
3079
+ case "tab_list": {
3080
+ const tabs = response.data?.tabs ?? [];
3081
+ const activeIndex = response.data?.activeIndex ?? 0;
3082
+ console.log(formatTabList(tabs, activeIndex));
3083
+ break;
3084
+ }
3085
+ case "tab_new": {
3086
+ const url = response.data?.url ?? "about:blank";
3087
+ console.log(`Created new tab: ${url}`);
3088
+ break;
3089
+ }
3090
+ case "tab_select": {
3091
+ const title = response.data?.title ?? "(untitled)";
3092
+ const url = response.data?.url ?? "";
3093
+ console.log(`Switched to tab #${parsed.index}: ${title}`);
3094
+ console.log(` URL: ${url}`);
3095
+ break;
3096
+ }
3097
+ case "tab_close": {
3098
+ const closedTitle = response.data?.title ?? "(untitled)";
3099
+ console.log(`Closed tab: ${closedTitle}`);
3100
+ break;
3101
+ }
3102
+ }
3103
+ } else {
3104
+ console.error(`Error: ${response.error}`);
3105
+ process.exit(1);
3106
+ }
3107
+ }
3108
+ }
3109
+
3110
+ // packages/cli/src/commands/frame.ts
3111
+ async function frameCommand(selector, options = {}) {
3112
+ if (!selector) {
3113
+ throw new Error("Missing selector argument");
3114
+ }
3115
+ await ensureDaemonRunning();
3116
+ const request = {
3117
+ id: generateId(),
3118
+ action: "frame",
3119
+ selector,
3120
+ tabId: options.tabId
3121
+ };
3122
+ const response = await sendCommand2(request);
3123
+ if (options.json) {
3124
+ console.log(JSON.stringify(response, null, 2));
3125
+ } else {
3126
+ if (response.success) {
3127
+ const frameInfo = response.data?.frameInfo;
3128
+ if (frameInfo?.url) {
3129
+ console.log(`Switched to frame: ${selector} (${frameInfo.url})`);
3130
+ } else {
3131
+ console.log(`Switched to frame: ${selector}`);
3132
+ }
3133
+ } else {
3134
+ console.error(`Error: ${response.error}`);
3135
+ process.exit(1);
3136
+ }
3137
+ }
3138
+ }
3139
+ async function frameMainCommand(options = {}) {
3140
+ await ensureDaemonRunning();
3141
+ const request = {
3142
+ id: generateId(),
3143
+ action: "frame_main",
3144
+ tabId: options.tabId
3145
+ };
3146
+ const response = await sendCommand2(request);
3147
+ if (options.json) {
3148
+ console.log(JSON.stringify(response, null, 2));
3149
+ } else {
3150
+ if (response.success) {
3151
+ console.log("Returned to main frame");
3152
+ } else {
3153
+ console.error(`Error: ${response.error}`);
3154
+ process.exit(1);
3155
+ }
3156
+ }
3157
+ }
3158
+
3159
+ // packages/cli/src/commands/dialog.ts
3160
+ async function dialogCommand(subCommand, promptText, options = {}) {
3161
+ if (!subCommand || !["accept", "dismiss"].includes(subCommand)) {
3162
+ throw new Error("Use 'dialog accept [text]' or 'dialog dismiss'");
3163
+ }
3164
+ await ensureDaemonRunning();
3165
+ const request = {
3166
+ id: generateId(),
3167
+ action: "dialog",
3168
+ dialogResponse: subCommand,
3169
+ promptText: subCommand === "accept" ? promptText : void 0,
3170
+ tabId: options.tabId
3171
+ };
3172
+ const response = await sendCommand2(request);
3173
+ if (options.json) {
3174
+ console.log(JSON.stringify(response, null, 2));
3175
+ } else {
3176
+ if (response.success) {
3177
+ const dialogInfo = response.data?.dialogInfo;
3178
+ if (dialogInfo) {
3179
+ const action = subCommand === "accept" ? "Accepted " : "Dismissed ";
3180
+ console.log(`${action}dialog (${dialogInfo.type}): "${dialogInfo.message}"`);
3181
+ } else {
3182
+ console.log("Dialog handled");
3183
+ }
3184
+ } else {
3185
+ console.error(`Error: ${response.error}`);
3186
+ process.exit(1);
3187
+ }
3188
+ }
3189
+ }
3190
+
3191
+ // packages/cli/src/commands/network.ts
3192
+ async function networkCommand(subCommand, urlOrFilter, options = {}) {
3193
+ const response = await sendCommand2({
3194
+ id: generateId(),
3195
+ action: "network",
3196
+ networkCommand: subCommand,
3197
+ url: subCommand === "route" || subCommand === "unroute" ? urlOrFilter : void 0,
3198
+ filter: subCommand === "requests" ? urlOrFilter : void 0,
3199
+ routeOptions: subCommand === "route" ? {
3200
+ abort: options.abort,
3201
+ body: options.body
3202
+ } : void 0,
3203
+ withBody: subCommand === "requests" ? options.withBody : void 0,
3204
+ tabId: options.tabId
3205
+ });
3206
+ if (options.json) {
3207
+ console.log(JSON.stringify(response));
3208
+ return;
3209
+ }
3210
+ if (!response.success) {
3211
+ throw new Error(response.error || "Network command failed");
3212
+ }
3213
+ const data = response.data;
3214
+ switch (subCommand) {
3215
+ case "requests": {
3216
+ const requests = data?.networkRequests || [];
3217
+ if (requests.length === 0) {
3218
+ console.log("No network requests recorded");
3219
+ console.log("Tip: running network requests starts monitoring automatically");
3220
+ } else {
3221
+ console.log(`Network requests (${requests.length} items):
3222
+ `);
3223
+ for (const req of requests) {
3224
+ const status = req.failed ? `FAILED (${req.failureReason})` : req.status ? `${req.status} ${req.statusText || ""}` : "pending";
3225
+ console.log(`${req.method} ${req.url}`);
3226
+ console.log(` Type: ${req.type}, Status: ${status}`);
3227
+ if (options.withBody) {
3228
+ const requestHeaderCount = req.requestHeaders ? Object.keys(req.requestHeaders).length : 0;
3229
+ const responseHeaderCount = req.responseHeaders ? Object.keys(req.responseHeaders).length : 0;
3230
+ console.log(` Request headers: ${requestHeaderCount}, Response headers: ${responseHeaderCount}`);
3231
+ if (req.requestBody !== void 0) {
3232
+ const preview = req.requestBody.length > 200 ? `${req.requestBody.slice(0, 200)}...` : req.requestBody;
3233
+ console.log(` Request body: ${preview}`);
3234
+ }
3235
+ if (req.responseBody !== void 0) {
3236
+ const preview = req.responseBody.length > 200 ? `${req.responseBody.slice(0, 200)}...` : req.responseBody;
3237
+ console.log(` Response body: ${preview}`);
3238
+ }
3239
+ if (req.bodyError) {
3240
+ console.log(` Body error: ${req.bodyError}`);
3241
+ }
3242
+ }
3243
+ console.log("");
3244
+ }
3245
+ }
3246
+ break;
3247
+ }
3248
+ case "route": {
3249
+ console.log(`Added route rule: ${urlOrFilter}`);
3250
+ if (options.abort) {
3251
+ console.log(" Action: abort request");
3252
+ } else if (options.body) {
3253
+ console.log(" Action: return mock body");
3254
+ } else {
3255
+ console.log(" Action: continue request");
3256
+ }
3257
+ console.log(`Current rule count: ${data?.routeCount || 0}`);
3258
+ break;
3259
+ }
3260
+ case "unroute": {
3261
+ if (urlOrFilter) {
3262
+ console.log(`Removed route rule: ${urlOrFilter}`);
3263
+ } else {
3264
+ console.log("Removed all route rules");
3265
+ }
3266
+ console.log(`Remaining rule count: ${data?.routeCount || 0}`);
3267
+ break;
3268
+ }
3269
+ case "clear": {
3270
+ console.log("Cleared network request records");
3271
+ break;
3272
+ }
3273
+ default:
3274
+ throw new Error(`Unknown network subcommand: ${subCommand}`);
3275
+ }
3276
+ }
3277
+
3278
+ // packages/cli/src/commands/console.ts
3279
+ async function consoleCommand(options = {}) {
3280
+ const response = await sendCommand2({
3281
+ id: generateId(),
3282
+ action: "console",
3283
+ consoleCommand: options.clear ? "clear" : "get",
3284
+ tabId: options.tabId
3285
+ });
3286
+ if (options.json) {
3287
+ console.log(JSON.stringify(response));
3288
+ return;
3289
+ }
3290
+ if (!response.success) {
3291
+ throw new Error(response.error || "Console command failed");
3292
+ }
3293
+ if (options.clear) {
3294
+ console.log("Cleared console messages");
3295
+ return;
3296
+ }
3297
+ const messages = response.data?.consoleMessages || [];
3298
+ if (messages.length === 0) {
3299
+ console.log("No console messages");
3300
+ console.log("Tip: console command starts monitoring automatically");
3301
+ return;
3302
+ }
3303
+ console.log(`Console messages (${messages.length} items):
3304
+ `);
3305
+ const typeColors = {
3306
+ log: "",
3307
+ info: "[INFO]",
3308
+ warn: "[WARN]",
3309
+ error: "[ERROR]",
3310
+ debug: "[DEBUG]"
3311
+ };
3312
+ for (const msg of messages) {
3313
+ const prefix = typeColors[msg.type] || `[${msg.type.toUpperCase()}]`;
3314
+ const location = msg.url ? ` (${msg.url}${msg.lineNumber ? `:${msg.lineNumber}` : ""})` : "";
3315
+ if (prefix) {
3316
+ console.log(`${prefix} ${msg.text}${location}`);
3317
+ } else {
3318
+ console.log(`${msg.text}${location}`);
3319
+ }
3320
+ }
3321
+ }
3322
+
3323
+ // packages/cli/src/commands/errors.ts
3324
+ async function errorsCommand(options = {}) {
3325
+ const response = await sendCommand2({
3326
+ id: generateId(),
3327
+ action: "errors",
3328
+ errorsCommand: options.clear ? "clear" : "get",
3329
+ tabId: options.tabId
3330
+ });
3331
+ if (options.json) {
3332
+ console.log(JSON.stringify(response));
3333
+ return;
3334
+ }
3335
+ if (!response.success) {
3336
+ throw new Error(response.error || "Errors command failed");
3337
+ }
3338
+ if (options.clear) {
3339
+ console.log("Cleared JS error records");
3340
+ return;
3341
+ }
3342
+ const errors = response.data?.jsErrors || [];
3343
+ if (errors.length === 0) {
3344
+ console.log("No JS errors");
3345
+ console.log("Tip: errors command starts monitoring automatically");
3346
+ return;
3347
+ }
3348
+ console.log(`JS errors (${errors.length} items):
3349
+ `);
3350
+ for (const err of errors) {
3351
+ console.log(`[ERROR] ${err.message}`);
3352
+ if (err.url) {
3353
+ console.log(` Location: ${err.url}:${err.lineNumber || 0}:${err.columnNumber || 0}`);
3354
+ }
3355
+ if (err.stackTrace) {
3356
+ console.log(` Stack trace:`);
3357
+ console.log(err.stackTrace.split("\n").map((line) => ` ${line}`).join("\n"));
3358
+ }
3359
+ console.log("");
3360
+ }
3361
+ }
3362
+
3363
+ // packages/cli/src/commands/trace.ts
3364
+ async function traceCommand(subCommand, options = {}) {
3365
+ const response = await sendCommand2({
3366
+ id: generateId(),
3367
+ action: "trace",
3368
+ traceCommand: subCommand,
3369
+ tabId: options.tabId
3370
+ });
3371
+ if (options.json) {
3372
+ console.log(JSON.stringify(response));
3373
+ return;
3374
+ }
3375
+ if (!response.success) {
3376
+ throw new Error(response.error || "Trace command failed");
3377
+ }
3378
+ const data = response.data;
3379
+ switch (subCommand) {
3380
+ case "start": {
3381
+ const status = data?.traceStatus;
3382
+ console.log("Started recording user actions");
3383
+ console.log(`Tab ID: ${status?.tabId || "N/A"}`);
3384
+ console.log("\nPerform actions in the browser, then run 'bb-browser trace stop' to stop recording");
3385
+ break;
3386
+ }
3387
+ case "stop": {
3388
+ const events = data?.traceEvents || [];
3389
+ const status = data?.traceStatus;
3390
+ console.log(`Recording complete, total ${events.length} events
3391
+ `);
3392
+ if (events.length === 0) {
3393
+ console.log("No recorded actions");
3394
+ break;
3395
+ }
3396
+ for (let i = 0; i < events.length; i++) {
3397
+ const event = events[i];
3398
+ const refStr = event.ref !== void 0 ? `@${event.ref}` : "";
3399
+ switch (event.type) {
3400
+ case "navigation":
3401
+ console.log(`${i + 1}. Navigate to: ${event.url}`);
3402
+ break;
3403
+ case "click":
3404
+ console.log(`${i + 1}. Click ${refStr} [${event.elementRole}] "${event.elementName || ""}"`);
3405
+ break;
3406
+ case "fill":
3407
+ console.log(`${i + 1}. Fill ${refStr} [${event.elementRole}] "${event.elementName || ""}" <- "${event.value}"`);
3408
+ break;
3409
+ case "select":
3410
+ console.log(`${i + 1}. Select ${refStr} [${event.elementRole}] "${event.elementName || ""}" <- "${event.value}"`);
3411
+ break;
3412
+ case "check":
3413
+ console.log(`${i + 1}. ${event.checked ? "Check" : "Uncheck"} ${refStr} [${event.elementRole}] "${event.elementName || ""}"`);
3414
+ break;
3415
+ case "press":
3416
+ console.log(`${i + 1}. Key ${event.key}`);
3417
+ break;
3418
+ case "scroll":
3419
+ console.log(`${i + 1}. Scroll ${event.direction} ${event.pixels}px`);
3420
+ break;
3421
+ default:
3422
+ console.log(`${i + 1}. ${event.type}`);
3423
+ }
3424
+ }
3425
+ console.log(`
3426
+ Status: ${status?.recording ? "recording" : "stopped"}`);
3427
+ break;
3428
+ }
3429
+ case "status": {
3430
+ const status = data?.traceStatus;
3431
+ if (status?.recording) {
3432
+ console.log(`Recording (tab ${status.tabId})`);
3433
+ console.log(`Recorded ${status.eventCount} events`);
3434
+ } else {
3435
+ console.log("Not recording");
3436
+ }
3437
+ break;
3438
+ }
3439
+ default:
3440
+ throw new Error(`Unknown trace subcommand: ${subCommand}`);
3441
+ }
3442
+ }
3443
+
3444
+ // packages/cli/src/commands/fetch.ts
3445
+ function matchTabOrigin2(tabUrl, targetHostname) {
3446
+ try {
3447
+ const tabHostname = new URL(tabUrl).hostname;
3448
+ return tabHostname === targetHostname || tabHostname.endsWith("." + targetHostname);
3449
+ } catch {
3450
+ return false;
3451
+ }
3452
+ }
3453
+ async function ensureTabForOrigin(origin, hostname) {
3454
+ const listReq = { id: generateId(), action: "tab_list" };
3455
+ const listResp = await sendCommand2(listReq);
3456
+ if (listResp.success && listResp.data?.tabs) {
3457
+ const matchingTab = listResp.data.tabs.find(
3458
+ (tab) => matchTabOrigin2(tab.url, hostname)
3459
+ );
3460
+ if (matchingTab) {
3461
+ return matchingTab.tabId;
3462
+ }
3463
+ }
3464
+ const newResp = await sendCommand2({ id: generateId(), action: "tab_new", url: origin });
3465
+ if (!newResp.success) {
3466
+ throw new Error(`Unable to open ${origin}: ${newResp.error}`);
3467
+ }
3468
+ await new Promise((resolve3) => setTimeout(resolve3, 3e3));
3469
+ return newResp.data?.tabId;
3470
+ }
3471
+ function buildFetchScript(url, options) {
3472
+ const method = (options.method || "GET").toUpperCase();
3473
+ const hasBody = options.body && method !== "GET" && method !== "HEAD";
3474
+ let headersExpr = "{}";
3475
+ if (options.headers) {
3476
+ try {
3477
+ JSON.parse(options.headers);
3478
+ headersExpr = options.headers;
3479
+ } catch {
3480
+ throw new Error(`--headers must be valid JSON. Got: ${options.headers}`);
3481
+ }
3482
+ }
3483
+ return `(async () => {
3484
+ try {
3485
+ const resp = await fetch(${JSON.stringify(url)}, {
3486
+ method: ${JSON.stringify(method)},
3487
+ credentials: 'include',
3488
+ headers: ${headersExpr}${hasBody ? `,
3489
+ body: ${JSON.stringify(options.body)}` : ""}
3490
+ });
3491
+ const contentType = resp.headers.get('content-type') || '';
3492
+ let body;
3493
+ if (contentType.includes('application/json') && resp.status !== 204) {
3494
+ try { body = await resp.json(); } catch { body = await resp.text(); }
3495
+ } else {
3496
+ body = await resp.text();
3497
+ }
3498
+ return JSON.stringify({
3499
+ status: resp.status,
3500
+ contentType,
3501
+ body
3502
+ });
3503
+ } catch (e) {
3504
+ return JSON.stringify({ error: e.message });
3505
+ }
3506
+ })()`;
3507
+ }
3508
+ async function fetchCommand(url, options = {}) {
3509
+ if (!url) {
3510
+ throw new Error(
3511
+ "Missing URL argument\n Usage: bb-browser fetch <url> [--json] [--method POST] [--body '{...}']\n Example: bb-browser fetch https://www.reddit.com/api/me.json --json"
3512
+ );
3513
+ }
3514
+ await ensureDaemonRunning();
3515
+ const isAbsolute = url.startsWith("http://") || url.startsWith("https://");
3516
+ let targetTabId = options.tabId;
3517
+ if (isAbsolute) {
3518
+ let origin;
3519
+ let hostname;
3520
+ try {
3521
+ const parsed = new URL(url);
3522
+ origin = parsed.origin;
3523
+ hostname = parsed.hostname;
3524
+ } catch {
3525
+ throw new Error(`Invalid URL: ${url}`);
3526
+ }
3527
+ if (!targetTabId) {
3528
+ targetTabId = await ensureTabForOrigin(origin, hostname);
3529
+ }
3530
+ }
3531
+ const script = buildFetchScript(url, options);
3532
+ const evalReq = { id: generateId(), action: "eval", script, tabId: targetTabId };
3533
+ const evalResp = await sendCommand2(evalReq);
3534
+ if (!evalResp.success) {
3535
+ throw new Error(`Fetch failed: ${evalResp.error}`);
3536
+ }
3537
+ const rawResult = evalResp.data?.result;
3538
+ if (rawResult === void 0 || rawResult === null) {
3539
+ throw new Error("Fetch returned no result");
3540
+ }
3541
+ let result;
3542
+ try {
3543
+ result = typeof rawResult === "string" ? JSON.parse(rawResult) : rawResult;
3544
+ } catch {
3545
+ console.log(rawResult);
3546
+ return;
3547
+ }
3548
+ if (result.error) {
3549
+ throw new Error(`Fetch error: ${result.error}`);
3550
+ }
3551
+ if (options.output) {
3552
+ const { writeFileSync: writeFileSync2 } = await import("fs");
3553
+ const content = typeof result.body === "object" ? JSON.stringify(result.body, null, 2) : String(result.body);
3554
+ writeFileSync2(options.output, content, "utf-8");
3555
+ console.log(`Written to ${options.output} (${result.status}, ${content.length} bytes)`);
3556
+ return;
3557
+ }
3558
+ if (typeof result.body === "object") {
3559
+ console.log(JSON.stringify(result.body, null, 2));
3560
+ } else {
3561
+ console.log(result.body);
3562
+ }
3563
+ }
3564
+
3565
+ // packages/cli/src/commands/history.ts
3566
+ async function historyCommand(subCommand, options = {}) {
3567
+ const days = options.days || 30;
3568
+ const data = subCommand === "search" ? { historyItems: searchHistory(options.query, days) } : { historyDomains: getHistoryDomains(days) };
3569
+ if (options.json) {
3570
+ console.log(JSON.stringify({
3571
+ id: generateId(),
3572
+ success: true,
3573
+ data
3574
+ }));
3575
+ return;
3576
+ }
3577
+ switch (subCommand) {
3578
+ case "search": {
3579
+ const items = data?.historyItems || [];
3580
+ console.log(`Found ${items.length} history entries
3581
+ `);
3582
+ if (items.length === 0) {
3583
+ console.log("No matching history entries found");
3584
+ break;
3585
+ }
3586
+ for (let i = 0; i < items.length; i++) {
3587
+ const item = items[i];
3588
+ console.log(`${i + 1}. ${item.title || "(untitled)"}`);
3589
+ console.log(` ${item.url}`);
3590
+ console.log(` Visit count: ${item.visitCount}`);
3591
+ }
3592
+ break;
3593
+ }
3594
+ case "domains": {
3595
+ const domains = data?.historyDomains || [];
3596
+ console.log(`Found ${domains.length} domains
3597
+ `);
3598
+ if (domains.length === 0) {
3599
+ console.log("No history records found");
3600
+ break;
3601
+ }
3602
+ for (let i = 0; i < domains.length; i++) {
3603
+ const domain = domains[i];
3604
+ console.log(`${i + 1}. ${domain.domain}`);
3605
+ console.log(` Visit count: ${domain.visits}`);
3606
+ }
3607
+ break;
3608
+ }
3609
+ default:
3610
+ throw new Error(`Unknown history subcommand: ${subCommand}`);
3611
+ }
3612
+ }
3613
+
3614
+ // packages/cli/src/commands/daemon.ts
3615
+ async function statusCommand(options = {}) {
3616
+ const running = await isDaemonRunning();
3617
+ if (options.json) {
3618
+ console.log(JSON.stringify({ running }));
3619
+ } else {
3620
+ console.log(running ? "Browser is running" : "Browser is not running");
3621
+ }
3622
+ }
3623
+
3624
+ // packages/cli/src/index.ts
3625
+ var VERSION = "0.13.0";
3626
+ var HELP_TEXT = `
3627
+ bb-browser - Browser automation for AI agents
3628
+
3629
+ Install:
3630
+ npm install -g bb-browser
3631
+
3632
+ Tip: For most data-fetch tasks, use site commands directly instead of manual browser actions:
3633
+ bb-browser site list List all available adapters
3634
+ bb-browser site twitter/search "AI" Example: search tweets
3635
+ bb-browser site xueqiu/hot-stock 5 Example: get hot stocks
3636
+
3637
+ Usage:
3638
+ bb-browser <command> [options]
3639
+
3640
+ Getting started:
3641
+ site recommend Recommend adapters based on browsing history
3642
+ site list List all adapters
3643
+ site info <name> Show adapter usage (args, return fields, examples)
3644
+ site <name> [args] Run adapter
3645
+ site update Update community adapter repository
3646
+ guide Learn how to turn any website into an adapter
3647
+ star \u2B50 Star bb-browser on GitHub
3648
+
3649
+ Browser actions:
3650
+ open <url> [--tab] Open URL
3651
+ snapshot [-i] [-c] [-d <n>] Get page snapshot
3652
+ click <ref> Click element
3653
+ hover <ref> Hover element
3654
+ fill <ref> <text> Fill input (clear then type)
3655
+ type <ref> <text> Type character by character (without clearing)
3656
+ check/uncheck <ref> Check/uncheck checkbox
3657
+ select <ref> <val> Select dropdown option
3658
+ press <key> Send keyboard input
3659
+ scroll <dir> [px] Scroll page
3660
+
3661
+ Page info:
3662
+ get text|url|title <ref> Get page content
3663
+ screenshot [path] Take screenshot
3664
+ eval "<js>" Execute JavaScript
3665
+ fetch <url> HTTP request with browser login state
3666
+
3667
+ Tabs:
3668
+ tab [list|new|close|<n>] Manage tabs
3669
+ status Show managed browser status
3670
+
3671
+ Navigation:
3672
+ back / forward / refresh Navigate back / forward / refresh
3673
+
3674
+ Debugging:
3675
+ network requests [filter] Show network requests
3676
+ console [--clear] Show/clear console messages
3677
+ errors [--clear] Show/clear JS errors
3678
+ trace start|stop|status Record user actions
3679
+ history search|domains Show browsing history
3680
+
3681
+ Options:
3682
+ --json Output as JSON
3683
+ --port <n> Set Chrome CDP port
3684
+ --openclaw Prefer reusing OpenClaw browser instance
3685
+ --jq <expr> Apply jq filter on JSON output (directly on data, skips id/success envelope)
3686
+ -i, --interactive Output interactive elements only (snapshot)
3687
+ -c, --compact Remove empty structural nodes (snapshot)
3688
+ -d, --depth <n> Limit tree depth (snapshot)
3689
+ -s, --selector <sel> Restrict CSS selector scope (snapshot)
3690
+ --tab <tabId> Specify target tab ID
3691
+ --mcp Start MCP server (for Claude Code / Cursor and other AI tools)
3692
+ --help, -h Show help
3693
+ --version, -v Show version
3694
+ `.trim();
3695
+ function parseArgs(argv) {
3696
+ const args = argv.slice(2);
3697
+ const result = {
3698
+ command: null,
3699
+ args: [],
3700
+ flags: {
3701
+ json: false,
3702
+ help: false,
3703
+ version: false,
3704
+ interactive: false,
3705
+ compact: false
3706
+ }
3707
+ };
3708
+ let skipNext = false;
3709
+ for (const arg of args) {
3710
+ if (skipNext) {
3711
+ skipNext = false;
3712
+ continue;
3713
+ }
3714
+ if (arg === "--json") {
3715
+ result.flags.json = true;
3716
+ } else if (arg === "--jq") {
3717
+ skipNext = true;
3718
+ const nextIdx = args.indexOf(arg) + 1;
3719
+ if (nextIdx < args.length) {
3720
+ result.flags.jq = args[nextIdx];
3721
+ result.flags.json = true;
3722
+ }
3723
+ } else if (arg === "--openclaw") {
3724
+ result.flags.openclaw = true;
3725
+ } else if (arg === "--port") {
3726
+ skipNext = true;
3727
+ const nextIdx = args.indexOf(arg) + 1;
3728
+ if (nextIdx < args.length) {
3729
+ result.flags.port = parseInt(args[nextIdx], 10);
3730
+ }
3731
+ } else if (arg === "--help" || arg === "-h") {
3732
+ result.flags.help = true;
3733
+ } else if (arg === "--version" || arg === "-v") {
3734
+ result.flags.version = true;
3735
+ } else if (arg === "--interactive" || arg === "-i") {
3736
+ result.flags.interactive = true;
3737
+ } else if (arg === "--compact" || arg === "-c") {
3738
+ result.flags.compact = true;
3739
+ } else if (arg === "--depth" || arg === "-d") {
3740
+ skipNext = true;
3741
+ const nextIdx = args.indexOf(arg) + 1;
3742
+ if (nextIdx < args.length) {
3743
+ result.flags.depth = parseInt(args[nextIdx], 10);
3744
+ }
3745
+ } else if (arg === "--selector" || arg === "-s") {
3746
+ skipNext = true;
3747
+ const nextIdx = args.indexOf(arg) + 1;
3748
+ if (nextIdx < args.length) {
3749
+ result.flags.selector = args[nextIdx];
3750
+ }
3751
+ } else if (arg === "--days") {
3752
+ skipNext = true;
3753
+ const nextIdx = args.indexOf(arg) + 1;
3754
+ if (nextIdx < args.length) {
3755
+ result.flags.days = parseInt(args[nextIdx], 10);
3756
+ }
3757
+ } else if (arg === "--id") {
3758
+ skipNext = true;
3759
+ } else if (arg === "--tab") {
3760
+ skipNext = true;
3761
+ } else if (arg.startsWith("-")) {
3762
+ } else if (result.command === null) {
3763
+ result.command = arg;
3764
+ } else {
3765
+ result.args.push(arg);
3766
+ }
3767
+ }
3768
+ return result;
3769
+ }
3770
+ async function main() {
3771
+ const parsed = parseArgs(process.argv);
3772
+ setJqExpression(parsed.flags.jq);
3773
+ const tabArgIdx = process.argv.indexOf("--tab");
3774
+ const globalTabId = tabArgIdx >= 0 && process.argv[tabArgIdx + 1] ? parseInt(process.argv[tabArgIdx + 1], 10) : void 0;
3775
+ if (parsed.flags.version) {
3776
+ console.log(VERSION);
3777
+ return;
3778
+ }
3779
+ if (process.argv.includes("--mcp")) {
3780
+ const mcpPath = fileURLToPath4(new URL("./mcp.js", import.meta.url));
3781
+ const { spawn: spawn3 } = await import("child_process");
3782
+ const child = spawn3(process.execPath, [mcpPath], { stdio: "inherit" });
3783
+ child.on("exit", (code) => process.exit(code ?? 0));
3784
+ return;
3785
+ }
3786
+ if (parsed.flags.help || !parsed.command) {
3787
+ console.log(HELP_TEXT);
3788
+ return;
3789
+ }
3790
+ try {
3791
+ switch (parsed.command) {
3792
+ case "open": {
3793
+ const url = parsed.args[0];
3794
+ if (!url) {
3795
+ console.error("Error: missing URL argument");
3796
+ console.error("Usage: bb-browser open <url> [--tab current|<tabId>]");
3797
+ process.exit(1);
3798
+ }
3799
+ const tabIndex = process.argv.findIndex((a) => a === "--tab");
3800
+ const tab = tabIndex >= 0 ? process.argv[tabIndex + 1] : void 0;
3801
+ await openCommand(url, { json: parsed.flags.json, tab });
3802
+ break;
3803
+ }
3804
+ case "snapshot": {
3805
+ await snapshotCommand({
3806
+ json: parsed.flags.json,
3807
+ interactive: parsed.flags.interactive,
3808
+ compact: parsed.flags.compact,
3809
+ maxDepth: parsed.flags.depth,
3810
+ selector: parsed.flags.selector,
3811
+ tabId: globalTabId
3812
+ });
3813
+ break;
3814
+ }
3815
+ case "click": {
3816
+ const ref = parsed.args[0];
3817
+ if (!ref) {
3818
+ console.error("Error: missing ref argument");
3819
+ console.error("Usage: bb-browser click <ref>");
3820
+ console.error("Example: bb-browser click @5");
3821
+ process.exit(1);
3822
+ }
3823
+ await clickCommand(ref, { json: parsed.flags.json, tabId: globalTabId });
3824
+ break;
3825
+ }
3826
+ case "hover": {
3827
+ const ref = parsed.args[0];
3828
+ if (!ref) {
3829
+ console.error("Error: missing ref argument");
3830
+ console.error("Usage: bb-browser hover <ref>");
3831
+ console.error("Example: bb-browser hover @5");
3832
+ process.exit(1);
3833
+ }
3834
+ await hoverCommand(ref, { json: parsed.flags.json, tabId: globalTabId });
3835
+ break;
3836
+ }
3837
+ case "check": {
3838
+ const ref = parsed.args[0];
3839
+ if (!ref) {
3840
+ console.error("Error: missing ref argument");
3841
+ console.error("Usage: bb-browser check <ref>");
3842
+ console.error("Example: bb-browser check @5");
3843
+ process.exit(1);
3844
+ }
3845
+ await checkCommand(ref, { json: parsed.flags.json, tabId: globalTabId });
3846
+ break;
3847
+ }
3848
+ case "uncheck": {
3849
+ const ref = parsed.args[0];
3850
+ if (!ref) {
3851
+ console.error("Error: missing ref argument");
3852
+ console.error("Usage: bb-browser uncheck <ref>");
3853
+ console.error("Example: bb-browser uncheck @5");
3854
+ process.exit(1);
3855
+ }
3856
+ await uncheckCommand(ref, { json: parsed.flags.json, tabId: globalTabId });
3857
+ break;
3858
+ }
3859
+ case "fill": {
3860
+ const ref = parsed.args[0];
3861
+ const text = parsed.args[1];
3862
+ if (!ref) {
3863
+ console.error("Error: missing ref argument");
3864
+ console.error("Usage: bb-browser fill <ref> <text>");
3865
+ console.error('Example: bb-browser fill @3 "hello world"');
3866
+ process.exit(1);
3867
+ }
3868
+ if (text === void 0) {
3869
+ console.error("Error: missing text argument");
3870
+ console.error("Usage: bb-browser fill <ref> <text>");
3871
+ console.error('Example: bb-browser fill @3 "hello world"');
3872
+ process.exit(1);
3873
+ }
3874
+ await fillCommand(ref, text, { json: parsed.flags.json, tabId: globalTabId });
3875
+ break;
3876
+ }
3877
+ case "type": {
3878
+ const ref = parsed.args[0];
3879
+ const text = parsed.args[1];
3880
+ if (!ref) {
3881
+ console.error("Error: missing ref argument");
3882
+ console.error("Usage: bb-browser type <ref> <text>");
3883
+ console.error('Example: bb-browser type @3 "append text"');
3884
+ process.exit(1);
3885
+ }
3886
+ if (text === void 0) {
3887
+ console.error("Error: missing text argument");
3888
+ console.error("Usage: bb-browser type <ref> <text>");
3889
+ console.error('Example: bb-browser type @3 "append text"');
3890
+ process.exit(1);
3891
+ }
3892
+ await typeCommand(ref, text, { json: parsed.flags.json, tabId: globalTabId });
3893
+ break;
3894
+ }
3895
+ case "select": {
3896
+ const ref = parsed.args[0];
3897
+ const value = parsed.args[1];
3898
+ if (!ref) {
3899
+ console.error("Error: missing ref argument");
3900
+ console.error("Usage: bb-browser select <ref> <value>");
3901
+ console.error('Example: bb-browser select @4 "option1"');
3902
+ process.exit(1);
3903
+ }
3904
+ if (value === void 0) {
3905
+ console.error("Error: missing value argument");
3906
+ console.error("Usage: bb-browser select <ref> <value>");
3907
+ console.error('Example: bb-browser select @4 "option1"');
3908
+ process.exit(1);
3909
+ }
3910
+ await selectCommand(ref, value, { json: parsed.flags.json, tabId: globalTabId });
3911
+ break;
3912
+ }
3913
+ case "eval": {
3914
+ const script = parsed.args[0];
3915
+ if (!script) {
3916
+ console.error("Error: missing script argument");
3917
+ console.error("Usage: bb-browser eval <script>");
3918
+ console.error('Example: bb-browser eval "document.title"');
3919
+ process.exit(1);
3920
+ }
3921
+ await evalCommand(script, { json: parsed.flags.json, tabId: globalTabId });
3922
+ break;
3923
+ }
3924
+ case "get": {
3925
+ const attribute = parsed.args[0];
3926
+ if (!attribute) {
3927
+ console.error("Error: missing attribute argument");
3928
+ console.error("Usage: bb-browser get <text|url|title> [ref]");
3929
+ console.error("Example: bb-browser get text @5");
3930
+ console.error(" bb-browser get url");
3931
+ process.exit(1);
3932
+ }
3933
+ if (!["text", "url", "title"].includes(attribute)) {
3934
+ console.error(`Error: unknown attribute "${attribute}"`);
3935
+ console.error("Supported attributes: text, url, title");
3936
+ process.exit(1);
3937
+ }
3938
+ const ref = parsed.args[1];
3939
+ await getCommand(attribute, ref, { json: parsed.flags.json, tabId: globalTabId });
3940
+ break;
3941
+ }
3942
+ case "daemon":
3943
+ case "close": {
3944
+ await closeCommand({ json: parsed.flags.json, tabId: globalTabId });
3945
+ break;
3946
+ }
3947
+ case "back": {
3948
+ await backCommand({ json: parsed.flags.json, tabId: globalTabId });
3949
+ break;
3950
+ }
3951
+ case "forward": {
3952
+ await forwardCommand({ json: parsed.flags.json, tabId: globalTabId });
3953
+ break;
3954
+ }
3955
+ case "refresh": {
3956
+ await refreshCommand({ json: parsed.flags.json, tabId: globalTabId });
3957
+ break;
3958
+ }
3959
+ case "screenshot": {
3960
+ const outputPath = parsed.args[0];
3961
+ await screenshotCommand(outputPath, { json: parsed.flags.json, tabId: globalTabId });
3962
+ break;
3963
+ }
3964
+ case "wait": {
3965
+ const target = parsed.args[0];
3966
+ if (!target) {
3967
+ console.error("Error: missing wait target argument");
3968
+ console.error("Usage: bb-browser wait <ms|@ref>");
3969
+ console.error("Example: bb-browser wait 2000");
3970
+ console.error(" bb-browser wait @5");
3971
+ process.exit(1);
3972
+ }
3973
+ await waitCommand(target, { json: parsed.flags.json, tabId: globalTabId });
3974
+ break;
3975
+ }
3976
+ case "press": {
3977
+ const key = parsed.args[0];
3978
+ if (!key) {
3979
+ console.error("Error: missing key argument");
3980
+ console.error("Usage: bb-browser press <key>");
3981
+ console.error("Example: bb-browser press Enter");
3982
+ console.error(" bb-browser press Control+a");
3983
+ process.exit(1);
3984
+ }
3985
+ await pressCommand(key, { json: parsed.flags.json, tabId: globalTabId });
3986
+ break;
3987
+ }
3988
+ case "scroll": {
3989
+ const direction = parsed.args[0];
3990
+ const pixels = parsed.args[1];
3991
+ if (!direction) {
3992
+ console.error("Error: missing direction argument");
3993
+ console.error("Usage: bb-browser scroll <up|down|left|right> [pixels]");
3994
+ console.error("Example: bb-browser scroll down");
3995
+ console.error(" bb-browser scroll up 500");
3996
+ process.exit(1);
3997
+ }
3998
+ await scrollCommand(direction, pixels, { json: parsed.flags.json, tabId: globalTabId });
3999
+ break;
4000
+ }
4001
+ case "tab": {
4002
+ await tabCommand(parsed.args, { json: parsed.flags.json });
4003
+ break;
4004
+ }
4005
+ case "status": {
4006
+ await statusCommand({ json: parsed.flags.json });
4007
+ break;
4008
+ }
4009
+ case "frame": {
4010
+ const selectorOrMain = parsed.args[0];
4011
+ if (!selectorOrMain) {
4012
+ console.error("Error: missing selector argument");
4013
+ console.error("Usage: bb-browser frame <selector>");
4014
+ console.error('Example: bb-browser frame "iframe#editor"');
4015
+ console.error(" bb-browser frame main");
4016
+ process.exit(1);
4017
+ }
4018
+ if (selectorOrMain === "main") {
4019
+ await frameMainCommand({ json: parsed.flags.json, tabId: globalTabId });
4020
+ } else {
4021
+ await frameCommand(selectorOrMain, { json: parsed.flags.json, tabId: globalTabId });
4022
+ }
4023
+ break;
4024
+ }
4025
+ case "dialog": {
4026
+ const subCommand = parsed.args[0];
4027
+ if (!subCommand) {
4028
+ console.error("Error: missing subcommand");
4029
+ console.error("Usage: bb-browser dialog <accept|dismiss> [text]");
4030
+ console.error("Example: bb-browser dialog accept");
4031
+ console.error(' bb-browser dialog accept "my input"');
4032
+ console.error(" bb-browser dialog dismiss");
4033
+ process.exit(1);
4034
+ }
4035
+ const promptText = parsed.args[1];
4036
+ await dialogCommand(subCommand, promptText, { json: parsed.flags.json, tabId: globalTabId });
4037
+ break;
4038
+ }
4039
+ case "network": {
4040
+ const subCommand = parsed.args[0] || "requests";
4041
+ const urlOrFilter = parsed.args[1];
4042
+ const abort = process.argv.includes("--abort");
4043
+ const withBody = process.argv.includes("--with-body");
4044
+ const bodyIndex = process.argv.findIndex((a) => a === "--body");
4045
+ const body = bodyIndex >= 0 ? process.argv[bodyIndex + 1] : void 0;
4046
+ await networkCommand(subCommand, urlOrFilter, { json: parsed.flags.json, abort, body, withBody, tabId: globalTabId });
4047
+ break;
4048
+ }
4049
+ case "console": {
4050
+ const clear = process.argv.includes("--clear");
4051
+ await consoleCommand({ json: parsed.flags.json, clear, tabId: globalTabId });
4052
+ break;
4053
+ }
4054
+ case "errors": {
4055
+ const clear = process.argv.includes("--clear");
4056
+ await errorsCommand({ json: parsed.flags.json, clear, tabId: globalTabId });
4057
+ break;
4058
+ }
4059
+ case "trace": {
4060
+ const subCmd = parsed.args[0];
4061
+ if (!subCmd || !["start", "stop", "status"].includes(subCmd)) {
4062
+ console.error("Error: missing or invalid subcommand");
4063
+ console.error("Usage: bb-browser trace <start|stop|status>");
4064
+ console.error("Example: bb-browser trace start");
4065
+ console.error(" bb-browser trace stop");
4066
+ console.error(" bb-browser trace status");
4067
+ process.exit(1);
4068
+ }
4069
+ await traceCommand(subCmd, { json: parsed.flags.json, tabId: globalTabId });
4070
+ break;
4071
+ }
4072
+ case "history": {
4073
+ const subCmd = parsed.args[0];
4074
+ if (!subCmd || !["search", "domains"].includes(subCmd)) {
4075
+ console.error("Error: missing or invalid subcommand");
4076
+ console.error("Usage: bb-browser history <search|domains> [query] [--days <n>]");
4077
+ console.error("Example: bb-browser history search github");
4078
+ console.error(" bb-browser history domains --days 7");
4079
+ process.exit(1);
4080
+ }
4081
+ const query = parsed.args.slice(1).join(" ");
4082
+ await historyCommand(subCmd, {
4083
+ json: parsed.flags.json,
4084
+ days: parsed.flags.days || 30,
4085
+ query
4086
+ });
4087
+ break;
4088
+ }
4089
+ case "fetch": {
4090
+ const fetchUrl = parsed.args[0];
4091
+ if (!fetchUrl) {
4092
+ console.error("[error] fetch: <url> is required.");
4093
+ console.error(" Usage: bb-browser fetch <url> [--json] [--method POST] [--body '{...}']");
4094
+ console.error(" Example: bb-browser fetch https://www.reddit.com/api/me.json --json");
4095
+ process.exit(1);
4096
+ }
4097
+ const methodIdx = process.argv.findIndex((a) => a === "--method");
4098
+ const fetchMethod = methodIdx >= 0 ? process.argv[methodIdx + 1] : void 0;
4099
+ const fetchBodyIdx = process.argv.findIndex((a) => a === "--body");
4100
+ const fetchBody = fetchBodyIdx >= 0 ? process.argv[fetchBodyIdx + 1] : void 0;
4101
+ const headersIdx = process.argv.findIndex((a) => a === "--headers");
4102
+ const fetchHeaders = headersIdx >= 0 ? process.argv[headersIdx + 1] : void 0;
4103
+ const outputIdx = process.argv.findIndex((a) => a === "--output");
4104
+ const fetchOutput = outputIdx >= 0 ? process.argv[outputIdx + 1] : void 0;
4105
+ await fetchCommand(fetchUrl, {
4106
+ json: parsed.flags.json,
4107
+ method: fetchMethod,
4108
+ body: fetchBody,
4109
+ headers: fetchHeaders,
4110
+ output: fetchOutput,
4111
+ tabId: globalTabId
4112
+ });
4113
+ break;
4114
+ }
4115
+ case "site": {
4116
+ await siteCommand(parsed.args, {
4117
+ json: parsed.flags.json,
4118
+ jq: parsed.flags.jq,
4119
+ days: parsed.flags.days,
4120
+ tabId: globalTabId,
4121
+ openclaw: parsed.flags.openclaw
4122
+ });
4123
+ break;
4124
+ }
4125
+ case "star": {
4126
+ const { execSync: execSync4 } = await import("child_process");
4127
+ try {
4128
+ execSync4("gh auth status", { stdio: "pipe" });
4129
+ } catch {
4130
+ console.error("Please install and sign in to GitHub CLI first: https://cli.github.com");
4131
+ console.error(" brew install gh && gh auth login");
4132
+ process.exit(1);
4133
+ }
4134
+ const repos = ["epiral/bb-browser", "epiral/bb-sites"];
4135
+ for (const repo of repos) {
4136
+ try {
4137
+ execSync4(`gh api user/starred/${repo} -X PUT`, { stdio: "pipe" });
4138
+ console.log(`\u2B50 Starred ${repo}`);
4139
+ } catch {
4140
+ console.log(`Already starred or failed: ${repo}`);
4141
+ }
4142
+ }
4143
+ console.log("\nThanks for your support! \u{1F64F}");
4144
+ break;
4145
+ }
4146
+ case "guide": {
4147
+ console.log(`How to turn any website into a bb-browser site adapter
4148
+ =======================================================
4149
+
4150
+ 1. REVERSE ENGINEER the API
4151
+ bb-browser network clear --tab <tabId>
4152
+ bb-browser refresh --tab <tabId>
4153
+ bb-browser network requests --filter "api" --with-body --json --tab <tabId>
4154
+
4155
+ 2. TEST if direct fetch works (Tier 1)
4156
+ bb-browser eval "fetch('/api/endpoint',{credentials:'include'}).then(r=>r.json())" --tab <tabId>
4157
+
4158
+ If it works \u2192 Tier 1 (Cookie auth, like Reddit/GitHub/Zhihu/Bilibili)
4159
+ If needs extra headers \u2192 Tier 2 (like Twitter: Bearer + CSRF token)
4160
+ If needs request signing \u2192 Tier 3 (like Xiaohongshu: Pinia store actions)
4161
+
4162
+ 3. WRITE the adapter (one JS file per operation)
4163
+
4164
+ /* @meta
4165
+ {
4166
+ "name": "platform/command",
4167
+ "description": "What it does",
4168
+ "domain": "www.example.com",
4169
+ "args": { "query": {"required": true, "description": "Search query"} },
4170
+ "readOnly": true,
4171
+ "example": "bb-browser site platform/command value"
4172
+ }
4173
+ */
4174
+ async function(args) {
4175
+ if (!args.query) return {error: 'Missing argument: query'};
4176
+ const resp = await fetch('/api/search?q=' + encodeURIComponent(args.query), {credentials: 'include'});
4177
+ if (!resp.ok) return {error: 'HTTP ' + resp.status, hint: 'Not logged in?'};
4178
+ return await resp.json();
4179
+ }
4180
+
4181
+ 4. TEST it
4182
+ Save to ~/.bb-browser/sites/platform/command.js (private, takes priority)
4183
+ bb-browser site platform/command "test query" --json
4184
+
4185
+ 5. CONTRIBUTE
4186
+ Option A (with gh CLI):
4187
+ git clone https://github.com/epiral/bb-sites && cd bb-sites
4188
+ git checkout -b feat-platform
4189
+ # add adapter files
4190
+ git push -u origin feat-platform
4191
+ gh pr create --repo epiral/bb-sites
4192
+
4193
+ Option B (without gh CLI, using bb-browser itself):
4194
+ bb-browser site github/fork epiral/bb-sites
4195
+ git clone https://github.com/YOUR_USER/bb-sites && cd bb-sites
4196
+ git checkout -b feat-platform
4197
+ # add adapter files
4198
+ git push -u origin feat-platform
4199
+ bb-browser site github/pr-create epiral/bb-sites --title "feat(platform): add adapters" --head "YOUR_USER:feat-platform"
4200
+
4201
+ Private adapters: ~/.bb-browser/sites/<platform>/<command>.js
4202
+ Community: ~/.bb-browser/bb-sites/ (via bb-browser site update)
4203
+ Full guide: https://github.com/epiral/bb-sites/blob/main/SKILL.md`);
4204
+ break;
4205
+ }
4206
+ default: {
4207
+ console.error(`Error: unknown command "${parsed.command}"`);
4208
+ console.error("Run bb-browser --help to view available commands");
4209
+ process.exit(1);
4210
+ }
4211
+ }
4212
+ } catch (error) {
4213
+ const message = error instanceof Error ? error.message : String(error);
4214
+ if (parsed.flags.json) {
4215
+ console.log(
4216
+ JSON.stringify({
4217
+ success: false,
4218
+ error: message
4219
+ })
4220
+ );
4221
+ } else {
4222
+ console.error(`Error: ${message}`);
4223
+ }
4224
+ process.exit(1);
4225
+ }
4226
+ }
4227
+ main().then(() => process.exit(0));
4228
+ //# sourceMappingURL=cli.js.map