bb-browser-api 0.11.5

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/daemon.js ADDED
@@ -0,0 +1,2472 @@
1
+ #!/usr/bin/env node
2
+ import {
3
+ COMMAND_TIMEOUT,
4
+ DAEMON_HOST,
5
+ DAEMON_PORT
6
+ } from "./chunk-CRK6CV23.js";
7
+ import "./chunk-ERAIAHQ5.js";
8
+ import "./chunk-D4HDZEJT.js";
9
+
10
+ // packages/daemon/src/index.ts
11
+ import { parseArgs } from "util";
12
+ import { writeFileSync, unlinkSync, existsSync, readFileSync as readFileSync2 } from "fs";
13
+ import { mkdirSync } from "fs";
14
+ import { randomBytes } from "crypto";
15
+ import os from "os";
16
+ import path2 from "path";
17
+
18
+ // packages/daemon/src/http-server.ts
19
+ import { createServer } from "http";
20
+
21
+ // packages/daemon/src/command-dispatch.ts
22
+ import { readFileSync } from "fs";
23
+ import path from "path";
24
+ import { fileURLToPath } from "url";
25
+ function buildRequestError(error) {
26
+ return error instanceof Error ? error : new Error(String(error));
27
+ }
28
+ function ok(id, data) {
29
+ return { id, success: true, data };
30
+ }
31
+ function fail(id, error) {
32
+ return { id, success: false, error: buildRequestError(error).message };
33
+ }
34
+ var cachedBuildDomTreeScript = null;
35
+ function loadBuildDomTreeScript() {
36
+ if (cachedBuildDomTreeScript) return cachedBuildDomTreeScript;
37
+ const currentDir = path.dirname(fileURLToPath(import.meta.url));
38
+ const candidates = [
39
+ // Built dist: dist/daemon.js → ../packages/shared/buildDomTree.js
40
+ path.resolve(currentDir, "../packages/shared/buildDomTree.js"),
41
+ // Dev mode: packages/daemon/src/ → ../../shared/buildDomTree.js
42
+ path.resolve(currentDir, "../../shared/buildDomTree.js"),
43
+ // npm installed: dist/daemon.js → same level
44
+ path.resolve(currentDir, "./buildDomTree.js"),
45
+ path.resolve(currentDir, "../buildDomTree.js")
46
+ ];
47
+ for (const candidate of candidates) {
48
+ try {
49
+ cachedBuildDomTreeScript = readFileSync(candidate, "utf8");
50
+ return cachedBuildDomTreeScript;
51
+ } catch {
52
+ }
53
+ }
54
+ throw new Error("Cannot find buildDomTree.js");
55
+ }
56
+ function convertBuildDomTreeResult(result, options) {
57
+ const { interactiveOnly, compact, maxDepth, selector } = options;
58
+ const { rootId, map } = result;
59
+ const refs = {};
60
+ const lines = [];
61
+ const getRole = (node) => {
62
+ const tagName = node.tagName.toLowerCase();
63
+ const role = node.attributes?.role;
64
+ if (role) return role;
65
+ const type = node.attributes?.type?.toLowerCase() || "text";
66
+ const inputRoleMap = {
67
+ text: "textbox",
68
+ password: "textbox",
69
+ email: "textbox",
70
+ url: "textbox",
71
+ tel: "textbox",
72
+ search: "searchbox",
73
+ number: "spinbutton",
74
+ range: "slider",
75
+ checkbox: "checkbox",
76
+ radio: "radio",
77
+ button: "button",
78
+ submit: "button",
79
+ reset: "button",
80
+ file: "button"
81
+ };
82
+ const roleMap = {
83
+ a: "link",
84
+ button: "button",
85
+ input: inputRoleMap[type] || "textbox",
86
+ select: "combobox",
87
+ textarea: "textbox",
88
+ img: "image",
89
+ nav: "navigation",
90
+ main: "main",
91
+ header: "banner",
92
+ footer: "contentinfo",
93
+ aside: "complementary",
94
+ form: "form",
95
+ table: "table",
96
+ ul: "list",
97
+ ol: "list",
98
+ li: "listitem",
99
+ h1: "heading",
100
+ h2: "heading",
101
+ h3: "heading",
102
+ h4: "heading",
103
+ h5: "heading",
104
+ h6: "heading",
105
+ dialog: "dialog",
106
+ article: "article",
107
+ section: "region",
108
+ label: "label",
109
+ details: "group",
110
+ summary: "button"
111
+ };
112
+ return roleMap[tagName] || tagName;
113
+ };
114
+ const collectTextContent = (node, nodeMap, depthLimit = 5) => {
115
+ const texts = [];
116
+ const visit = (nodeId, depth) => {
117
+ if (depth > depthLimit) return;
118
+ const currentNode = nodeMap[nodeId];
119
+ if (!currentNode) return;
120
+ if ("type" in currentNode && currentNode.type === "TEXT_NODE") {
121
+ const text = currentNode.text.trim();
122
+ if (text) texts.push(text);
123
+ return;
124
+ }
125
+ for (const childId of currentNode.children || []) visit(childId, depth + 1);
126
+ };
127
+ for (const childId of node.children || []) visit(childId, 0);
128
+ return texts.join(" ").trim();
129
+ };
130
+ const getName = (node) => {
131
+ const attrs = node.attributes || {};
132
+ return attrs["aria-label"] || attrs.title || attrs.placeholder || attrs.alt || attrs.value || collectTextContent(node, map) || attrs.name || void 0;
133
+ };
134
+ const truncateText = (text, length = 50) => text.length <= length ? text : `${text.slice(0, length - 3)}...`;
135
+ const selectorText = selector?.trim().toLowerCase();
136
+ const matchesSelector = (node, role, name) => {
137
+ if (!selectorText) return true;
138
+ const haystack = [node.tagName, role, name, node.xpath || "", ...Object.values(node.attributes || {})].join(" ").toLowerCase();
139
+ return haystack.includes(selectorText);
140
+ };
141
+ if (interactiveOnly) {
142
+ 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));
143
+ for (const { node } of interactiveNodes) {
144
+ const refId = String(node.highlightIndex);
145
+ const role = getRole(node);
146
+ const name = getName(node);
147
+ if (!matchesSelector(node, role, name)) continue;
148
+ let line = `${role} [ref=${refId}]`;
149
+ if (name) line += ` ${JSON.stringify(truncateText(name))}`;
150
+ lines.push(line);
151
+ refs[refId] = { xpath: node.xpath || "", role, name, tagName: node.tagName.toLowerCase() };
152
+ }
153
+ return { snapshot: lines.join("\n"), refs };
154
+ }
155
+ const walk = (nodeId, depth) => {
156
+ if (maxDepth !== void 0 && depth > maxDepth) return;
157
+ const node = map[nodeId];
158
+ if (!node) return;
159
+ if ("type" in node && node.type === "TEXT_NODE") {
160
+ const text = node.text.trim();
161
+ if (!text) return;
162
+ lines.push(`${" ".repeat(depth)}- text ${JSON.stringify(truncateText(text, compact ? 80 : 120))}`);
163
+ return;
164
+ }
165
+ const el = node;
166
+ const role = getRole(el);
167
+ const name = getName(el);
168
+ if (!matchesSelector(el, role, name)) {
169
+ for (const childId of el.children || []) walk(childId, depth + 1);
170
+ return;
171
+ }
172
+ const indent = " ".repeat(depth);
173
+ const refId = el.highlightIndex !== void 0 && el.highlightIndex !== null ? String(el.highlightIndex) : null;
174
+ let line = `${indent}- ${role}`;
175
+ if (refId) line += ` [ref=${refId}]`;
176
+ if (name) line += ` ${JSON.stringify(truncateText(name, compact ? 50 : 80))}`;
177
+ if (!compact) line += ` <${el.tagName.toLowerCase()}>`;
178
+ lines.push(line);
179
+ if (refId) {
180
+ refs[refId] = { xpath: el.xpath || "", role, name, tagName: el.tagName.toLowerCase() };
181
+ }
182
+ for (const childId of el.children || []) walk(childId, depth + 1);
183
+ };
184
+ walk(rootId, 0);
185
+ return { snapshot: lines.join("\n"), refs };
186
+ }
187
+ var CLEANUP_HIGHLIGHTS_SCRIPT = `(() => {
188
+ if (window._highlightCleanupFunctions && window._highlightCleanupFunctions.length) {
189
+ window._highlightCleanupFunctions.forEach(fn => { try { fn(); } catch {} });
190
+ window._highlightCleanupFunctions = [];
191
+ }
192
+ const c = document.getElementById('playwright-highlight-container');
193
+ if (c) c.remove();
194
+ })()`;
195
+ async function buildSnapshot(cdp, targetId, tab, request) {
196
+ const script = loadBuildDomTreeScript();
197
+ const buildArgs = {
198
+ showHighlightElements: true,
199
+ focusHighlightIndex: -1,
200
+ viewportExpansion: -1,
201
+ debugMode: false,
202
+ startId: 0,
203
+ startHighlightIndex: 0
204
+ };
205
+ 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(buildArgs)}); })()`;
206
+ const value = await cdp.evaluate(targetId, expression, true);
207
+ if (!value || !value.map || !value.rootId) {
208
+ const title = await cdp.evaluate(targetId, "document.title", true);
209
+ const pageUrl = await cdp.evaluate(targetId, "location.href", true);
210
+ tab.refs = {};
211
+ return { snapshot: title || pageUrl, refs: {} };
212
+ }
213
+ const snapshot = convertBuildDomTreeResult(value, {
214
+ interactiveOnly: !!request.interactive,
215
+ compact: !!request.compact,
216
+ maxDepth: request.maxDepth,
217
+ selector: request.selector
218
+ });
219
+ tab.refs = snapshot.refs || {};
220
+ return snapshot;
221
+ }
222
+ async function resolveBackendNodeIdByXPath(cdp, targetId, xpath) {
223
+ await cdp.sessionCommand(targetId, "DOM.getDocument", { depth: 0 });
224
+ const search = await cdp.sessionCommand(
225
+ targetId,
226
+ "DOM.performSearch",
227
+ { query: xpath, includeUserAgentShadowDOM: true }
228
+ );
229
+ try {
230
+ if (!search.resultCount) {
231
+ throw new Error(`Unknown ref xpath: ${xpath}`);
232
+ }
233
+ const { nodeIds } = await cdp.sessionCommand(
234
+ targetId,
235
+ "DOM.getSearchResults",
236
+ { searchId: search.searchId, fromIndex: 0, toIndex: search.resultCount }
237
+ );
238
+ for (const nodeId of nodeIds) {
239
+ const described = await cdp.sessionCommand(targetId, "DOM.describeNode", { nodeId });
240
+ if (described.node.backendNodeId) {
241
+ return described.node.backendNodeId;
242
+ }
243
+ }
244
+ throw new Error(`XPath resolved but no backend node id found: ${xpath}`);
245
+ } finally {
246
+ await cdp.sessionCommand(targetId, "DOM.discardSearchResults", { searchId: search.searchId }).catch(() => {
247
+ });
248
+ }
249
+ }
250
+ async function parseRef(cdp, targetId, tab, ref) {
251
+ const found = tab.refs[ref];
252
+ if (!found) {
253
+ throw new Error(`Unknown ref: ${ref}. Run snapshot first.`);
254
+ }
255
+ if (found.backendDOMNodeId) {
256
+ return found.backendDOMNodeId;
257
+ }
258
+ if (found.xpath) {
259
+ const backendDOMNodeId = await resolveBackendNodeIdByXPath(cdp, targetId, found.xpath);
260
+ found.backendDOMNodeId = backendDOMNodeId;
261
+ return backendDOMNodeId;
262
+ }
263
+ throw new Error(`Unknown ref: ${ref}. Run snapshot first.`);
264
+ }
265
+ async function getInteractablePoint(cdp, targetId, backendNodeId) {
266
+ const resolved = await cdp.sessionCommand(
267
+ targetId,
268
+ "DOM.resolveNode",
269
+ { backendNodeId }
270
+ );
271
+ const call = await cdp.sessionCommand(targetId, "Runtime.callFunctionOn", {
272
+ objectId: resolved.object.objectId,
273
+ functionDeclaration: `function() {
274
+ if (!(this instanceof Element)) {
275
+ throw new Error('Ref does not resolve to an element');
276
+ }
277
+ this.scrollIntoView({ behavior: 'auto', block: 'center', inline: 'center' });
278
+ const rect = this.getBoundingClientRect();
279
+ if (!rect || rect.width <= 0 || rect.height <= 0) {
280
+ throw new Error('Element is not visible');
281
+ }
282
+ return {
283
+ x: rect.left + rect.width / 2,
284
+ y: rect.top + rect.height / 2,
285
+ };
286
+ }`,
287
+ returnByValue: true
288
+ });
289
+ if (call.exceptionDetails) {
290
+ throw new Error(call.exceptionDetails.text || "Failed to resolve element point");
291
+ }
292
+ const point = call.result.value;
293
+ if (!point || typeof point.x !== "number" || typeof point.y !== "number" || !Number.isFinite(point.x) || !Number.isFinite(point.y)) {
294
+ throw new Error("Failed to resolve element point");
295
+ }
296
+ return point;
297
+ }
298
+ async function mouseClick(cdp, targetId, x, y) {
299
+ await cdp.sessionCommand(targetId, "Input.dispatchMouseEvent", {
300
+ type: "mouseMoved",
301
+ x,
302
+ y,
303
+ button: "none"
304
+ });
305
+ await cdp.sessionCommand(targetId, "Input.dispatchMouseEvent", {
306
+ type: "mousePressed",
307
+ x,
308
+ y,
309
+ button: "left",
310
+ clickCount: 1
311
+ });
312
+ await cdp.sessionCommand(targetId, "Input.dispatchMouseEvent", {
313
+ type: "mouseReleased",
314
+ x,
315
+ y,
316
+ button: "left",
317
+ clickCount: 1
318
+ });
319
+ }
320
+ async function insertTextIntoNode(cdp, targetId, backendNodeId, text, clearFirst) {
321
+ const resolved = await cdp.sessionCommand(
322
+ targetId,
323
+ "DOM.resolveNode",
324
+ { backendNodeId }
325
+ );
326
+ await cdp.sessionCommand(targetId, "Runtime.callFunctionOn", {
327
+ objectId: resolved.object.objectId,
328
+ functionDeclaration: `function(clearFirst) {
329
+ if (typeof this.scrollIntoView === 'function') {
330
+ this.scrollIntoView({ behavior: 'auto', block: 'center', inline: 'center' });
331
+ }
332
+ if (typeof this.focus === 'function') this.focus();
333
+ if (this instanceof HTMLInputElement || this instanceof HTMLTextAreaElement) {
334
+ if (clearFirst) {
335
+ this.value = '';
336
+ this.dispatchEvent(new Event('input', { bubbles: true }));
337
+ }
338
+ if (typeof this.setSelectionRange === 'function') {
339
+ const end = this.value.length;
340
+ this.setSelectionRange(end, end);
341
+ }
342
+ return true;
343
+ }
344
+ if (this instanceof HTMLElement && this.isContentEditable) {
345
+ if (clearFirst) {
346
+ this.textContent = '';
347
+ this.dispatchEvent(new Event('input', { bubbles: true }));
348
+ }
349
+ const selection = window.getSelection();
350
+ if (selection) {
351
+ const range = document.createRange();
352
+ range.selectNodeContents(this);
353
+ range.collapse(false);
354
+ selection.removeAllRanges();
355
+ selection.addRange(range);
356
+ }
357
+ return true;
358
+ }
359
+ return false;
360
+ }`,
361
+ arguments: [{ value: clearFirst }],
362
+ returnByValue: true
363
+ });
364
+ if (text) {
365
+ await cdp.sessionCommand(targetId, "DOM.focus", { backendNodeId });
366
+ await cdp.sessionCommand(targetId, "Input.insertText", { text });
367
+ }
368
+ }
369
+ async function getAttributeValue(cdp, targetId, backendNodeId, attribute) {
370
+ if (attribute === "text") {
371
+ const resolved = await cdp.sessionCommand(
372
+ targetId,
373
+ "DOM.resolveNode",
374
+ { backendNodeId }
375
+ );
376
+ const call2 = await cdp.sessionCommand(
377
+ targetId,
378
+ "Runtime.callFunctionOn",
379
+ {
380
+ objectId: resolved.object.objectId,
381
+ functionDeclaration: `function() { return (this instanceof HTMLElement ? this.innerText : this.textContent || '').trim(); }`,
382
+ returnByValue: true
383
+ }
384
+ );
385
+ return String(call2.result.value ?? "");
386
+ }
387
+ const result = await cdp.sessionCommand(
388
+ targetId,
389
+ "DOM.resolveNode",
390
+ { backendNodeId }
391
+ );
392
+ const call = await cdp.sessionCommand(
393
+ targetId,
394
+ "Runtime.callFunctionOn",
395
+ {
396
+ objectId: result.object.objectId,
397
+ 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)}) || ''; }`,
398
+ returnByValue: true
399
+ }
400
+ );
401
+ return String(call.result.value ?? "");
402
+ }
403
+ var traceRecording = false;
404
+ var traceEvents = [];
405
+ async function dispatchRequest(cdp, request) {
406
+ const tabRef = request.tabId;
407
+ if (request.action === "tab_new") {
408
+ const url = request.url ?? "about:blank";
409
+ const created = await cdp.browserCommand(
410
+ "Target.createTarget",
411
+ { url, background: true }
412
+ );
413
+ await cdp.attachAndEnable(created.targetId);
414
+ const newTab = cdp.tabManager.getTab(created.targetId);
415
+ return ok(request.id, {
416
+ tabId: created.targetId,
417
+ url,
418
+ tab: newTab?.shortId ?? created.targetId.slice(-4).toLowerCase(),
419
+ seq: newTab?.recordAction()
420
+ });
421
+ }
422
+ const target = await cdp.ensurePageTarget(
423
+ tabRef !== void 0 ? String(tabRef) : void 0
424
+ );
425
+ const tab = cdp.tabManager.getTab(target.id);
426
+ if (!tab) throw new Error("Internal error: tab state not found");
427
+ const shortId = tab.shortId;
428
+ switch (request.action) {
429
+ // -----------------------------------------------------------------------
430
+ // Navigation
431
+ // -----------------------------------------------------------------------
432
+ case "open": {
433
+ if (!request.url) return fail(request.id, "Missing url parameter");
434
+ const seq = tab.recordAction();
435
+ if (tabRef === void 0) {
436
+ const created = await cdp.browserCommand(
437
+ "Target.createTarget",
438
+ { url: request.url, background: true }
439
+ );
440
+ const newTarget = await cdp.ensurePageTarget(created.targetId);
441
+ const newTab = cdp.tabManager.getTab(newTarget.id);
442
+ return ok(request.id, {
443
+ url: request.url,
444
+ tabId: newTarget.id,
445
+ tab: newTab?.shortId ?? shortId,
446
+ seq
447
+ });
448
+ }
449
+ await cdp.pageCommand(target.id, "Page.navigate", { url: request.url });
450
+ tab.refs = {};
451
+ return ok(request.id, {
452
+ url: request.url,
453
+ title: target.title,
454
+ tabId: target.id,
455
+ tab: shortId,
456
+ seq
457
+ });
458
+ }
459
+ case "back": {
460
+ const seq = tab.recordAction();
461
+ await cdp.evaluate(target.id, "history.back(); undefined");
462
+ return ok(request.id, { tab: shortId, seq });
463
+ }
464
+ case "forward": {
465
+ const seq = tab.recordAction();
466
+ await cdp.evaluate(target.id, "history.forward(); undefined");
467
+ return ok(request.id, { tab: shortId, seq });
468
+ }
469
+ case "refresh": {
470
+ const seq = tab.recordAction();
471
+ await cdp.sessionCommand(target.id, "Page.reload", { ignoreCache: false });
472
+ return ok(request.id, { tab: shortId, seq });
473
+ }
474
+ case "close": {
475
+ const seq = tab.recordAction();
476
+ await cdp.browserCommand("Target.closeTarget", { targetId: target.id });
477
+ tab.refs = {};
478
+ return ok(request.id, { tab: shortId, seq });
479
+ }
480
+ // -----------------------------------------------------------------------
481
+ // Snapshot / observation
482
+ // -----------------------------------------------------------------------
483
+ case "snapshot": {
484
+ const snapshotData = await buildSnapshot(cdp, target.id, tab, request);
485
+ return ok(request.id, {
486
+ title: target.title,
487
+ url: target.url,
488
+ snapshotData,
489
+ tab: shortId
490
+ });
491
+ }
492
+ case "screenshot": {
493
+ await cdp.evaluate(target.id, CLEANUP_HIGHLIGHTS_SCRIPT, true).catch(() => {
494
+ });
495
+ const result = await cdp.sessionCommand(
496
+ target.id,
497
+ "Page.captureScreenshot",
498
+ { format: "png", fromSurface: true }
499
+ );
500
+ return ok(request.id, {
501
+ dataUrl: `data:image/png;base64,${result.data}`,
502
+ tab: shortId
503
+ });
504
+ }
505
+ // -----------------------------------------------------------------------
506
+ // Element interaction
507
+ // -----------------------------------------------------------------------
508
+ case "click":
509
+ case "hover": {
510
+ if (!request.ref) return fail(request.id, "Missing ref parameter");
511
+ const seq = tab.recordAction();
512
+ const backendNodeId = await parseRef(cdp, target.id, tab, request.ref);
513
+ const point = await getInteractablePoint(cdp, target.id, backendNodeId);
514
+ await cdp.sessionCommand(target.id, "Input.dispatchMouseEvent", {
515
+ type: "mouseMoved",
516
+ x: point.x,
517
+ y: point.y,
518
+ button: "none"
519
+ });
520
+ if (request.action === "click") {
521
+ await mouseClick(cdp, target.id, point.x, point.y);
522
+ }
523
+ return ok(request.id, { tab: shortId, seq });
524
+ }
525
+ case "fill":
526
+ case "type": {
527
+ if (!request.ref) return fail(request.id, "Missing ref parameter");
528
+ if (request.text == null) return fail(request.id, "Missing text parameter");
529
+ const seq = tab.recordAction();
530
+ const backendNodeId = await parseRef(cdp, target.id, tab, request.ref);
531
+ await insertTextIntoNode(cdp, target.id, backendNodeId, request.text, request.action === "fill");
532
+ return ok(request.id, {
533
+ value: request.text,
534
+ tab: shortId,
535
+ seq
536
+ });
537
+ }
538
+ case "check":
539
+ case "uncheck": {
540
+ if (!request.ref) return fail(request.id, "Missing ref parameter");
541
+ const seq = tab.recordAction();
542
+ const desired = request.action === "check";
543
+ const backendNodeId = await parseRef(cdp, target.id, tab, request.ref);
544
+ const resolved = await cdp.sessionCommand(
545
+ target.id,
546
+ "DOM.resolveNode",
547
+ { backendNodeId }
548
+ );
549
+ await cdp.sessionCommand(target.id, "Runtime.callFunctionOn", {
550
+ objectId: resolved.object.objectId,
551
+ functionDeclaration: `function() { this.checked = ${desired}; this.dispatchEvent(new Event('input', { bubbles: true })); this.dispatchEvent(new Event('change', { bubbles: true })); }`
552
+ });
553
+ return ok(request.id, { tab: shortId, seq });
554
+ }
555
+ case "select": {
556
+ if (!request.ref || request.value == null) return fail(request.id, "Missing ref or value parameter");
557
+ const seq = tab.recordAction();
558
+ const backendNodeId = await parseRef(cdp, target.id, tab, request.ref);
559
+ const resolved = await cdp.sessionCommand(
560
+ target.id,
561
+ "DOM.resolveNode",
562
+ { backendNodeId }
563
+ );
564
+ await cdp.sessionCommand(target.id, "Runtime.callFunctionOn", {
565
+ objectId: resolved.object.objectId,
566
+ functionDeclaration: `function() { this.value = ${JSON.stringify(request.value)}; this.dispatchEvent(new Event('input', { bubbles: true })); this.dispatchEvent(new Event('change', { bubbles: true })); }`
567
+ });
568
+ return ok(request.id, {
569
+ value: request.value,
570
+ tab: shortId,
571
+ seq
572
+ });
573
+ }
574
+ case "get": {
575
+ if (!request.attribute) return fail(request.id, "Missing attribute parameter");
576
+ if (request.attribute === "url" && !request.ref) {
577
+ return ok(request.id, {
578
+ value: await cdp.evaluate(target.id, "location.href", true),
579
+ tab: shortId
580
+ });
581
+ }
582
+ if (request.attribute === "title" && !request.ref) {
583
+ return ok(request.id, {
584
+ value: await cdp.evaluate(target.id, "document.title", true),
585
+ tab: shortId
586
+ });
587
+ }
588
+ if (!request.ref) return fail(request.id, "Missing ref parameter");
589
+ const value = await getAttributeValue(
590
+ cdp,
591
+ target.id,
592
+ await parseRef(cdp, target.id, tab, request.ref),
593
+ request.attribute
594
+ );
595
+ return ok(request.id, { value, tab: shortId });
596
+ }
597
+ case "press": {
598
+ if (!request.key) return fail(request.id, "Missing key parameter");
599
+ const seq = tab.recordAction();
600
+ await cdp.sessionCommand(target.id, "Input.dispatchKeyEvent", {
601
+ type: "keyDown",
602
+ key: request.key
603
+ });
604
+ if (request.key.length === 1) {
605
+ await cdp.sessionCommand(target.id, "Input.dispatchKeyEvent", {
606
+ type: "char",
607
+ text: request.key,
608
+ key: request.key
609
+ });
610
+ }
611
+ await cdp.sessionCommand(target.id, "Input.dispatchKeyEvent", {
612
+ type: "keyUp",
613
+ key: request.key
614
+ });
615
+ return ok(request.id, { tab: shortId, seq });
616
+ }
617
+ case "scroll": {
618
+ const seq = tab.recordAction();
619
+ const pixels = request.pixels ?? 300;
620
+ let deltaX = 0;
621
+ let deltaY = 0;
622
+ switch (request.direction) {
623
+ case "up":
624
+ deltaY = -pixels;
625
+ break;
626
+ case "down":
627
+ deltaY = pixels;
628
+ break;
629
+ case "left":
630
+ deltaX = -pixels;
631
+ break;
632
+ case "right":
633
+ deltaX = pixels;
634
+ break;
635
+ }
636
+ await cdp.sessionCommand(target.id, "Input.dispatchMouseEvent", {
637
+ type: "mouseWheel",
638
+ x: 0,
639
+ y: 0,
640
+ deltaX,
641
+ deltaY
642
+ });
643
+ return ok(request.id, { tab: shortId, seq });
644
+ }
645
+ case "wait": {
646
+ await new Promise((resolve) => setTimeout(resolve, request.ms ?? 1e3));
647
+ return ok(request.id, { tab: shortId });
648
+ }
649
+ case "eval": {
650
+ if (!request.script) return fail(request.id, "Missing script parameter");
651
+ const seq = tab.recordAction();
652
+ const result = await cdp.evaluate(target.id, request.script, true);
653
+ return ok(request.id, {
654
+ result,
655
+ tab: shortId,
656
+ seq
657
+ });
658
+ }
659
+ case "fetch": {
660
+ if (!request.url) return fail(request.id, "Missing url parameter");
661
+ const seq = tab.recordAction();
662
+ const method = (request.method || "GET").toUpperCase();
663
+ const hasBody = request.body && method !== "GET" && method !== "HEAD";
664
+ let headersExpr = "{}";
665
+ if (request.headers) {
666
+ headersExpr = JSON.stringify(request.headers);
667
+ }
668
+ const credentials = request.credentials || "omit";
669
+ const fetchScript = `(async () => {
670
+ try {
671
+ const resp = await fetch(${JSON.stringify(request.url)}, {
672
+ method: ${JSON.stringify(method)},
673
+ credentials: ${JSON.stringify(credentials)},
674
+ headers: ${headersExpr}${hasBody ? `,
675
+ body: ${JSON.stringify(request.body)}` : ""}
676
+ });
677
+ const contentType = resp.headers.get('content-type') || '';
678
+ let body;
679
+ if (contentType.includes('application/json') && resp.status !== 204) {
680
+ try { body = await resp.json(); } catch { body = await resp.text(); }
681
+ } else {
682
+ body = await resp.text();
683
+ }
684
+ return {
685
+ status: resp.status,
686
+ contentType,
687
+ body
688
+ };
689
+ } catch (e) {
690
+ return {
691
+ error: e.message,
692
+ errorName: e.name,
693
+ errorStack: e.stack,
694
+ currentUrl: location.href
695
+ };
696
+ }
697
+ })()`;
698
+ const result = await cdp.evaluate(
699
+ target.id,
700
+ fetchScript,
701
+ true
702
+ );
703
+ if (result?.error) {
704
+ const errorDetails = [
705
+ `Fetch error: ${result.error}`,
706
+ result.errorName ? `(${result.errorName})` : "",
707
+ result.currentUrl ? `from page: ${result.currentUrl}` : ""
708
+ ].filter(Boolean).join(" ");
709
+ return fail(request.id, errorDetails);
710
+ }
711
+ return ok(request.id, {
712
+ fetchResponse: {
713
+ status: result?.status ?? 0,
714
+ contentType: result?.contentType ?? "",
715
+ body: result?.body
716
+ },
717
+ tab: shortId,
718
+ seq
719
+ });
720
+ }
721
+ // -----------------------------------------------------------------------
722
+ // Tab management
723
+ // -----------------------------------------------------------------------
724
+ case "tab_list": {
725
+ const targets = (await cdp.getTargets()).filter((t) => t.type === "page");
726
+ const tabs = targets.map((t, index) => {
727
+ const tState = cdp.tabManager.getTab(t.id);
728
+ return {
729
+ index,
730
+ url: t.url,
731
+ title: t.title,
732
+ active: t.id === cdp.currentTargetId || !cdp.currentTargetId && index === 0,
733
+ tabId: t.id,
734
+ tab: tState?.shortId ?? t.id.slice(-4).toLowerCase()
735
+ };
736
+ });
737
+ return ok(request.id, {
738
+ tabs,
739
+ activeIndex: tabs.findIndex((t) => t.active)
740
+ });
741
+ }
742
+ // tab_new is handled before ensurePageTarget() above
743
+ case "tab_select": {
744
+ const targets = (await cdp.getTargets()).filter((t) => t.type === "page");
745
+ let selected;
746
+ if (request.tabId !== void 0) {
747
+ const tabIdStr = String(request.tabId);
748
+ const resolvedId = cdp.tabManager.resolveShortId(tabIdStr);
749
+ if (resolvedId) {
750
+ selected = targets.find((t) => t.id === resolvedId);
751
+ }
752
+ if (!selected) {
753
+ selected = targets.find((t) => t.id === tabIdStr);
754
+ }
755
+ if (!selected) {
756
+ const num = Number(tabIdStr);
757
+ if (!Number.isNaN(num)) {
758
+ selected = targets[num];
759
+ }
760
+ }
761
+ } else {
762
+ selected = targets[request.index ?? 0];
763
+ }
764
+ if (!selected) return fail(request.id, "Tab not found");
765
+ cdp.currentTargetId = selected.id;
766
+ await cdp.attachAndEnable(selected.id);
767
+ const selTab = cdp.tabManager.getTab(selected.id);
768
+ return ok(request.id, {
769
+ tabId: selected.id,
770
+ url: selected.url,
771
+ title: selected.title,
772
+ tab: selTab?.shortId
773
+ });
774
+ }
775
+ case "tab_close": {
776
+ const targets = (await cdp.getTargets()).filter((t) => t.type === "page");
777
+ let selected;
778
+ if (request.tabId !== void 0) {
779
+ const tabIdStr = String(request.tabId);
780
+ const resolvedId = cdp.tabManager.resolveShortId(tabIdStr);
781
+ if (resolvedId) {
782
+ selected = targets.find((t) => t.id === resolvedId);
783
+ }
784
+ if (!selected) {
785
+ selected = targets.find((t) => t.id === tabIdStr);
786
+ }
787
+ if (!selected) {
788
+ const num = Number(tabIdStr);
789
+ if (!Number.isNaN(num)) {
790
+ selected = targets[num];
791
+ }
792
+ }
793
+ } else {
794
+ selected = targets[request.index ?? 0];
795
+ }
796
+ if (!selected) return fail(request.id, "Tab not found");
797
+ const closedTab = cdp.tabManager.getTab(selected.id);
798
+ const closedShort = closedTab?.shortId;
799
+ await cdp.browserCommand("Target.closeTarget", { targetId: selected.id });
800
+ if (cdp.currentTargetId === selected.id) {
801
+ cdp.currentTargetId = void 0;
802
+ }
803
+ return ok(request.id, {
804
+ tabId: selected.id,
805
+ tab: closedShort
806
+ });
807
+ }
808
+ // -----------------------------------------------------------------------
809
+ // Frame navigation
810
+ // -----------------------------------------------------------------------
811
+ case "frame": {
812
+ if (!request.selector) return fail(request.id, "Missing selector parameter");
813
+ const seq = tab.recordAction();
814
+ const document = await cdp.pageCommand(
815
+ target.id,
816
+ "DOM.getDocument",
817
+ {}
818
+ );
819
+ const node = await cdp.pageCommand(
820
+ target.id,
821
+ "DOM.querySelector",
822
+ { nodeId: document.root.nodeId, selector: request.selector }
823
+ );
824
+ if (!node.nodeId) return fail(request.id, `iframe not found: ${request.selector}`);
825
+ const described = await cdp.pageCommand(target.id, "DOM.describeNode", { nodeId: node.nodeId });
826
+ const frameId = described.node.frameId;
827
+ const nodeName = String(described.node.nodeName ?? "").toLowerCase();
828
+ if (!frameId) return fail(request.id, `Cannot get iframe frameId: ${request.selector}`);
829
+ if (nodeName && nodeName !== "iframe" && nodeName !== "frame") {
830
+ return fail(request.id, `Element is not an iframe: ${nodeName}`);
831
+ }
832
+ tab.activeFrameId = frameId;
833
+ const attributes = described.node.attributes ?? [];
834
+ const attrMap = {};
835
+ for (let i = 0; i < attributes.length; i += 2) {
836
+ attrMap[String(attributes[i])] = String(attributes[i + 1] ?? "");
837
+ }
838
+ return ok(request.id, {
839
+ frameInfo: {
840
+ selector: request.selector,
841
+ name: attrMap.name ?? "",
842
+ url: attrMap.src ?? "",
843
+ frameId
844
+ },
845
+ tab: shortId,
846
+ seq
847
+ });
848
+ }
849
+ case "frame_main": {
850
+ const seq = tab.recordAction();
851
+ tab.activeFrameId = null;
852
+ return ok(request.id, {
853
+ frameInfo: { frameId: 0 },
854
+ tab: shortId,
855
+ seq
856
+ });
857
+ }
858
+ // -----------------------------------------------------------------------
859
+ // Dialog
860
+ // -----------------------------------------------------------------------
861
+ case "dialog": {
862
+ const seq = tab.recordAction();
863
+ tab.dialogHandler = {
864
+ accept: request.dialogResponse !== "dismiss",
865
+ ...request.promptText !== void 0 ? { promptText: request.promptText } : {}
866
+ };
867
+ await cdp.sessionCommand(target.id, "Page.enable");
868
+ return ok(request.id, {
869
+ dialogInfo: {
870
+ type: "armed",
871
+ message: `Dialog handler armed: ${request.dialogResponse ?? "accept"}`,
872
+ handled: false
873
+ },
874
+ tab: shortId,
875
+ seq
876
+ });
877
+ }
878
+ // -----------------------------------------------------------------------
879
+ // Network observation
880
+ // -----------------------------------------------------------------------
881
+ case "network": {
882
+ const subCommand = request.networkCommand ?? "requests";
883
+ switch (subCommand) {
884
+ case "requests": {
885
+ const queryResult = tab.getNetworkRequests({
886
+ since: request.since,
887
+ filter: request.filter,
888
+ method: request.method,
889
+ status: request.status,
890
+ limit: request.limit
891
+ });
892
+ const items = queryResult.items;
893
+ if (request.withBody) {
894
+ await Promise.all(
895
+ items.map(async (item) => {
896
+ if (item.failed || item.responseBody !== void 0 || item.bodyError !== void 0) return;
897
+ try {
898
+ const body = await cdp.sessionCommand(
899
+ target.id,
900
+ "Network.getResponseBody",
901
+ { requestId: item.requestId }
902
+ );
903
+ item.responseBody = body.body;
904
+ item.responseBodyBase64 = body.base64Encoded;
905
+ } catch (error) {
906
+ item.bodyError = error instanceof Error ? error.message : String(error);
907
+ }
908
+ })
909
+ );
910
+ }
911
+ return ok(request.id, {
912
+ networkRequests: items,
913
+ tab: shortId,
914
+ cursor: queryResult.cursor
915
+ });
916
+ }
917
+ case "route":
918
+ return ok(request.id, { routeCount: 0, tab: shortId });
919
+ case "unroute":
920
+ return ok(request.id, { routeCount: 0, tab: shortId });
921
+ case "clear":
922
+ tab.clearNetwork();
923
+ return ok(request.id, { tab: shortId });
924
+ default:
925
+ return fail(request.id, `Unknown network subcommand: ${subCommand}`);
926
+ }
927
+ }
928
+ // -----------------------------------------------------------------------
929
+ // Console observation
930
+ // -----------------------------------------------------------------------
931
+ case "console": {
932
+ const subCommand = request.consoleCommand ?? "get";
933
+ switch (subCommand) {
934
+ case "get": {
935
+ const queryResult = tab.getConsoleMessages({
936
+ since: request.since,
937
+ filter: request.filter,
938
+ limit: request.limit
939
+ });
940
+ return ok(request.id, {
941
+ consoleMessages: queryResult.items,
942
+ tab: shortId,
943
+ cursor: queryResult.cursor
944
+ });
945
+ }
946
+ case "clear":
947
+ tab.clearConsole();
948
+ return ok(request.id, { tab: shortId });
949
+ default:
950
+ return fail(request.id, `Unknown console subcommand: ${subCommand}`);
951
+ }
952
+ }
953
+ // -----------------------------------------------------------------------
954
+ // JS Errors observation
955
+ // -----------------------------------------------------------------------
956
+ case "errors": {
957
+ const subCommand = request.errorsCommand ?? "get";
958
+ switch (subCommand) {
959
+ case "get": {
960
+ const queryResult = tab.getJSErrors({
961
+ since: request.since,
962
+ filter: request.filter,
963
+ limit: request.limit
964
+ });
965
+ return ok(request.id, {
966
+ jsErrors: queryResult.items,
967
+ tab: shortId,
968
+ cursor: queryResult.cursor
969
+ });
970
+ }
971
+ case "clear":
972
+ tab.clearErrors();
973
+ return ok(request.id, { tab: shortId });
974
+ default:
975
+ return fail(request.id, `Unknown errors subcommand: ${subCommand}`);
976
+ }
977
+ }
978
+ // -----------------------------------------------------------------------
979
+ // Trace
980
+ // -----------------------------------------------------------------------
981
+ case "trace": {
982
+ const subCommand = request.traceCommand ?? "status";
983
+ switch (subCommand) {
984
+ case "start":
985
+ traceRecording = true;
986
+ traceEvents.length = 0;
987
+ return ok(request.id, {
988
+ traceStatus: { recording: true, eventCount: 0 },
989
+ tab: shortId
990
+ });
991
+ case "stop": {
992
+ traceRecording = false;
993
+ return ok(request.id, {
994
+ traceEvents: [...traceEvents],
995
+ traceStatus: { recording: false, eventCount: traceEvents.length },
996
+ tab: shortId
997
+ });
998
+ }
999
+ case "status":
1000
+ return ok(request.id, {
1001
+ traceStatus: { recording: traceRecording, eventCount: traceEvents.length },
1002
+ tab: shortId
1003
+ });
1004
+ default:
1005
+ return fail(request.id, `Unknown trace subcommand: ${subCommand}`);
1006
+ }
1007
+ }
1008
+ // -----------------------------------------------------------------------
1009
+ // History (not implemented in daemon yet)
1010
+ // -----------------------------------------------------------------------
1011
+ case "history": {
1012
+ return fail(request.id, "History command is not supported in daemon mode");
1013
+ }
1014
+ default:
1015
+ return fail(request.id, `Unknown action: ${request.action}`);
1016
+ }
1017
+ }
1018
+
1019
+ // packages/daemon/src/http-server.ts
1020
+ var HttpServer = class {
1021
+ server = null;
1022
+ host;
1023
+ port;
1024
+ token;
1025
+ cdp;
1026
+ onShutdown;
1027
+ startTime = 0;
1028
+ constructor(options) {
1029
+ this.host = options.host ?? "127.0.0.1";
1030
+ this.port = options.port ?? DAEMON_PORT;
1031
+ this.token = options.token ?? null;
1032
+ this.cdp = options.cdp;
1033
+ this.onShutdown = options.onShutdown;
1034
+ }
1035
+ // ---------------------------------------------------------------------------
1036
+ // Lifecycle
1037
+ // ---------------------------------------------------------------------------
1038
+ async start() {
1039
+ return new Promise((resolve, reject) => {
1040
+ this.server = createServer((req, res) => {
1041
+ this.handleRequest(req, res);
1042
+ });
1043
+ this.server.on("error", reject);
1044
+ this.server.listen(this.port, this.host, () => {
1045
+ this.startTime = Date.now();
1046
+ resolve();
1047
+ });
1048
+ });
1049
+ }
1050
+ async stop() {
1051
+ if (this.server) {
1052
+ return new Promise((resolve) => {
1053
+ this.server.close(() => resolve());
1054
+ });
1055
+ }
1056
+ }
1057
+ get uptime() {
1058
+ if (this.startTime === 0) return 0;
1059
+ return Math.floor((Date.now() - this.startTime) / 1e3);
1060
+ }
1061
+ // ---------------------------------------------------------------------------
1062
+ // Auth
1063
+ // ---------------------------------------------------------------------------
1064
+ checkAuth(req, res) {
1065
+ if (!this.token) return true;
1066
+ const auth = req.headers.authorization ?? "";
1067
+ if (auth === `Bearer ${this.token}`) return true;
1068
+ this.sendJson(res, 401, { error: "Unauthorized" });
1069
+ return false;
1070
+ }
1071
+ // ---------------------------------------------------------------------------
1072
+ // Routing
1073
+ // ---------------------------------------------------------------------------
1074
+ handleRequest(req, res) {
1075
+ res.setHeader("Access-Control-Allow-Origin", "*");
1076
+ res.setHeader("Access-Control-Allow-Methods", "GET, POST, OPTIONS");
1077
+ res.setHeader("Access-Control-Allow-Headers", "Content-Type, Authorization");
1078
+ if (req.method === "OPTIONS") {
1079
+ res.writeHead(204);
1080
+ res.end();
1081
+ return;
1082
+ }
1083
+ if (!this.checkAuth(req, res)) return;
1084
+ const url = req.url ?? "/";
1085
+ if (req.method === "POST" && url === "/command") {
1086
+ this.handleCommand(req, res);
1087
+ } else if (req.method === "POST" && url === "/api/fetch") {
1088
+ this.handleApiFetch(req, res);
1089
+ } else if (req.method === "GET" && url.startsWith("/api/capture")) {
1090
+ this.handleApiCapture(req, res);
1091
+ } else if (req.method === "GET" && url.startsWith("/api/storage")) {
1092
+ this.handleApiStorage(req, res);
1093
+ } else if (req.method === "GET" && url === "/status") {
1094
+ this.handleStatus(req, res);
1095
+ } else if (req.method === "POST" && url === "/shutdown") {
1096
+ this.handleShutdown(req, res);
1097
+ } else {
1098
+ this.sendJson(res, 404, { error: "Not found" });
1099
+ }
1100
+ }
1101
+ // ---------------------------------------------------------------------------
1102
+ // POST /command
1103
+ // ---------------------------------------------------------------------------
1104
+ async handleCommand(req, res) {
1105
+ try {
1106
+ const body = await this.readBody(req);
1107
+ const request = JSON.parse(body);
1108
+ if (!this.cdp.connected) {
1109
+ try {
1110
+ await Promise.race([
1111
+ this.cdp.waitUntilReady(),
1112
+ new Promise(
1113
+ (_, reject) => setTimeout(() => reject(new Error("CDP connection timeout")), COMMAND_TIMEOUT)
1114
+ )
1115
+ ]);
1116
+ } catch {
1117
+ const cdpTarget = `${this.cdp.host}:${this.cdp.port}`;
1118
+ const reason = this.cdp.lastError || "unknown";
1119
+ this.sendJson(res, 503, {
1120
+ id: request.id,
1121
+ success: false,
1122
+ error: `Chrome not connected (CDP at ${cdpTarget})`,
1123
+ reason,
1124
+ hint: "Make sure Chrome is running. Try: bb-browser daemon shutdown && bb-browser tab list"
1125
+ });
1126
+ return;
1127
+ }
1128
+ }
1129
+ const timeout = new Promise(
1130
+ (_, reject) => setTimeout(() => reject(new Error("Command timeout")), COMMAND_TIMEOUT)
1131
+ );
1132
+ const response = await Promise.race([
1133
+ dispatchRequest(this.cdp, request),
1134
+ timeout
1135
+ ]);
1136
+ this.sendJson(res, 200, response);
1137
+ } catch (error) {
1138
+ this.sendJson(res, 400, {
1139
+ success: false,
1140
+ error: error instanceof Error ? error.message : "Invalid request"
1141
+ });
1142
+ }
1143
+ }
1144
+ // ---------------------------------------------------------------------------
1145
+ // POST /api/fetch
1146
+ // ---------------------------------------------------------------------------
1147
+ /**
1148
+ * 封装的 fetch API 路由
1149
+ *
1150
+ * 请求体格式:
1151
+ * {
1152
+ * "url": "https://example.com/api/data",
1153
+ * "method": "GET", // 可选,默认 GET
1154
+ * "body": "...", // 可选
1155
+ * "headers": {}, // 可选
1156
+ * "tabId": "..." // 可选,指定 tab ID(支持短 ID)
1157
+ * }
1158
+ *
1159
+ * 响应格式:
1160
+ * {
1161
+ * "status": 200,
1162
+ * "contentType": "application/json",
1163
+ * "body": {...}
1164
+ * }
1165
+ */
1166
+ async handleApiFetch(req, res) {
1167
+ try {
1168
+ const body = await this.readBody(req);
1169
+ const params = JSON.parse(body);
1170
+ if (!params.url) {
1171
+ this.sendJson(res, 400, {
1172
+ error: "Missing url parameter",
1173
+ hint: "\u8BF7\u6C42\u4F53\u5FC5\u987B\u5305\u542B url \u5B57\u6BB5"
1174
+ });
1175
+ return;
1176
+ }
1177
+ if (!this.cdp.connected) {
1178
+ try {
1179
+ await Promise.race([
1180
+ this.cdp.waitUntilReady(),
1181
+ new Promise(
1182
+ (_, reject) => setTimeout(() => reject(new Error("CDP connection timeout")), COMMAND_TIMEOUT)
1183
+ )
1184
+ ]);
1185
+ } catch {
1186
+ const cdpTarget = `${this.cdp.host}:${this.cdp.port}`;
1187
+ const reason = this.cdp.lastError || "unknown";
1188
+ this.sendJson(res, 503, {
1189
+ error: `Chrome not connected (CDP at ${cdpTarget})`,
1190
+ reason,
1191
+ hint: "Make sure Chrome is running. Try: bb-browser daemon shutdown && bb-browser tab list"
1192
+ });
1193
+ return;
1194
+ }
1195
+ }
1196
+ let targetTabId = params.tabId;
1197
+ if (!targetTabId && params.url) {
1198
+ try {
1199
+ const targetUrl = new URL(params.url);
1200
+ const targetOrigin = targetUrl.origin;
1201
+ const targets = (await this.cdp.getTargets()).filter((t) => t.type === "page");
1202
+ const sameOriginTab = targets.find((t) => {
1203
+ try {
1204
+ const tabUrl = new URL(t.url);
1205
+ return tabUrl.origin === targetOrigin;
1206
+ } catch {
1207
+ return false;
1208
+ }
1209
+ });
1210
+ if (sameOriginTab) {
1211
+ const tabState = this.cdp.tabManager.getTab(sameOriginTab.id);
1212
+ targetTabId = tabState?.shortId || sameOriginTab.id;
1213
+ } else {
1214
+ const newTabResp = await this.cdp.browserCommand(
1215
+ "Target.createTarget",
1216
+ { url: targetOrigin, background: true }
1217
+ );
1218
+ await this.cdp.attachAndEnable(newTabResp.targetId);
1219
+ const newTab = this.cdp.tabManager.getTab(newTabResp.targetId);
1220
+ targetTabId = newTab?.shortId || newTabResp.targetId;
1221
+ await new Promise((resolve) => setTimeout(resolve, 1e3));
1222
+ }
1223
+ } catch (e) {
1224
+ }
1225
+ }
1226
+ const request = {
1227
+ id: `fetch-${Date.now()}`,
1228
+ action: "fetch",
1229
+ url: params.url,
1230
+ method: params.method,
1231
+ body: params.body,
1232
+ headers: params.headers,
1233
+ credentials: params.credentials,
1234
+ tabId: targetTabId
1235
+ };
1236
+ const timeout = new Promise(
1237
+ (_, reject) => setTimeout(() => reject(new Error("Fetch timeout")), COMMAND_TIMEOUT)
1238
+ );
1239
+ const response = await Promise.race([
1240
+ dispatchRequest(this.cdp, request),
1241
+ timeout
1242
+ ]);
1243
+ if (!response.success) {
1244
+ this.sendJson(res, 500, {
1245
+ error: response.error,
1246
+ hint: "Fetch \u6267\u884C\u5931\u8D25"
1247
+ });
1248
+ return;
1249
+ }
1250
+ this.sendJson(res, 200, response.data?.fetchResponse ?? {
1251
+ error: "No fetch response data"
1252
+ });
1253
+ } catch (error) {
1254
+ this.sendJson(res, 400, {
1255
+ error: error instanceof Error ? error.message : "Invalid request",
1256
+ hint: "\u8BF7\u6C42\u683C\u5F0F\u9519\u8BEF\u6216\u6267\u884C\u5931\u8D25"
1257
+ });
1258
+ }
1259
+ }
1260
+ // ---------------------------------------------------------------------------
1261
+ // GET /api/capture
1262
+ // ---------------------------------------------------------------------------
1263
+ /**
1264
+ * 抓包接口
1265
+ *
1266
+ * 请求格式:
1267
+ * GET /api/capture?url=https://example.com&pattern=api\\.&timeout=5000
1268
+ *
1269
+ * 参数说明:
1270
+ * - url: 要访问的页面 URL(必填)
1271
+ * - pattern: URL 匹配正则(可选)
1272
+ * - timeout: 等待时间(毫秒,可选,默认 5000)
1273
+ *
1274
+ * 响应格式:
1275
+ * {
1276
+ * "url": "...",
1277
+ * "pattern": "...",
1278
+ * "totalRequests": 50,
1279
+ * "matchedRequests": 3,
1280
+ * "requests": [...]
1281
+ * }
1282
+ */
1283
+ async handleApiCapture(req, res) {
1284
+ try {
1285
+ const urlObj = new URL(req.url || "", `http://${req.headers.host}`);
1286
+ const targetUrl = urlObj.searchParams.get("url");
1287
+ const pattern = urlObj.searchParams.get("pattern") || void 0;
1288
+ const timeout = parseInt(urlObj.searchParams.get("timeout") || "5000", 10);
1289
+ if (!targetUrl) {
1290
+ this.sendJson(res, 400, {
1291
+ error: "Missing url parameter",
1292
+ hint: "\u8BF7\u6C42\u5FC5\u987B\u5305\u542B url \u53C2\u6570\uFF0C\u4F8B\u5982: /api/capture?url=https://example.com"
1293
+ });
1294
+ return;
1295
+ }
1296
+ if (!this.cdp.connected) {
1297
+ try {
1298
+ await Promise.race([
1299
+ this.cdp.waitUntilReady(),
1300
+ new Promise(
1301
+ (_, reject) => setTimeout(() => reject(new Error("CDP connection timeout")), COMMAND_TIMEOUT)
1302
+ )
1303
+ ]);
1304
+ } catch {
1305
+ const cdpTarget = `${this.cdp.host}:${this.cdp.port}`;
1306
+ const reason = this.cdp.lastError || "unknown";
1307
+ this.sendJson(res, 503, {
1308
+ error: `Chrome not connected (CDP at ${cdpTarget})`,
1309
+ reason,
1310
+ hint: "Make sure Chrome is running."
1311
+ });
1312
+ return;
1313
+ }
1314
+ }
1315
+ const created = await this.cdp.browserCommand(
1316
+ "Target.createTarget",
1317
+ { url: "about:blank", background: true }
1318
+ );
1319
+ await this.cdp.attachAndEnable(created.targetId);
1320
+ const tab = this.cdp.tabManager.getTab(created.targetId);
1321
+ if (!tab) {
1322
+ this.sendJson(res, 500, {
1323
+ error: "Failed to create tab",
1324
+ hint: "\u65E0\u6CD5\u521B\u5EFA\u65B0\u6807\u7B7E\u9875"
1325
+ });
1326
+ return;
1327
+ }
1328
+ try {
1329
+ tab.clearNetwork();
1330
+ await this.cdp.pageCommand(created.targetId, "Page.navigate", { url: targetUrl });
1331
+ const waitTime = timeout;
1332
+ await new Promise((resolve) => setTimeout(resolve, waitTime));
1333
+ const allRequests = tab.getNetworkRequests({}).items;
1334
+ let filteredRequests = allRequests;
1335
+ if (pattern) {
1336
+ try {
1337
+ const regex = new RegExp(pattern);
1338
+ filteredRequests = allRequests.filter((req2) => regex.test(req2.url));
1339
+ } catch (e) {
1340
+ filteredRequests = allRequests.filter((req2) => req2.url.includes(pattern));
1341
+ }
1342
+ }
1343
+ await Promise.all(
1344
+ filteredRequests.map(async (item) => {
1345
+ if (item.failed || item.responseBody !== void 0 || item.bodyError !== void 0) return;
1346
+ try {
1347
+ const bodyResult = await this.cdp.sessionCommand(
1348
+ created.targetId,
1349
+ "Network.getResponseBody",
1350
+ { requestId: item.requestId }
1351
+ );
1352
+ item.responseBody = bodyResult.body;
1353
+ item.responseBodyBase64 = bodyResult.base64Encoded;
1354
+ } catch (error) {
1355
+ item.bodyError = error instanceof Error ? error.message : String(error);
1356
+ }
1357
+ })
1358
+ );
1359
+ this.sendJson(res, 200, {
1360
+ url: targetUrl,
1361
+ pattern,
1362
+ totalRequests: allRequests.length,
1363
+ matchedRequests: filteredRequests.length,
1364
+ requests: filteredRequests
1365
+ });
1366
+ } finally {
1367
+ await this.cdp.browserCommand("Target.closeTarget", { targetId: created.targetId }).catch(() => {
1368
+ });
1369
+ }
1370
+ } catch (error) {
1371
+ this.sendJson(res, 400, {
1372
+ error: error instanceof Error ? error.message : "Invalid request",
1373
+ hint: "\u8BF7\u6C42\u683C\u5F0F\u9519\u8BEF\u6216\u6267\u884C\u5931\u8D25"
1374
+ });
1375
+ }
1376
+ }
1377
+ // ---------------------------------------------------------------------------
1378
+ // GET /api/storage
1379
+ // ---------------------------------------------------------------------------
1380
+ /**
1381
+ * 存储接口
1382
+ *
1383
+ * 请求格式:
1384
+ * GET /api/storage?domain=https://example.com
1385
+ *
1386
+ * 参数说明:
1387
+ * - domain: 目标域名(必填,必须包含协议)
1388
+ *
1389
+ * 响应格式:
1390
+ * {
1391
+ * "domain": "https://example.com",
1392
+ * "cookies": [...],
1393
+ * "localStorage": {...},
1394
+ * "sessionStorage": {...}
1395
+ * }
1396
+ */
1397
+ async handleApiStorage(req, res) {
1398
+ try {
1399
+ const urlObj = new URL(req.url || "", `http://${req.headers.host}`);
1400
+ const domain = urlObj.searchParams.get("domain");
1401
+ if (!domain) {
1402
+ this.sendJson(res, 400, {
1403
+ error: "Missing domain parameter",
1404
+ hint: "\u8BF7\u6C42\u5FC5\u987B\u5305\u542B domain \u53C2\u6570\uFF0C\u4F8B\u5982: /api/storage?domain=https://example.com"
1405
+ });
1406
+ return;
1407
+ }
1408
+ if (!this.cdp.connected) {
1409
+ try {
1410
+ await Promise.race([
1411
+ this.cdp.waitUntilReady(),
1412
+ new Promise(
1413
+ (_, reject) => setTimeout(() => reject(new Error("CDP connection timeout")), COMMAND_TIMEOUT)
1414
+ )
1415
+ ]);
1416
+ } catch {
1417
+ const cdpTarget = `${this.cdp.host}:${this.cdp.port}`;
1418
+ const reason = this.cdp.lastError || "unknown";
1419
+ this.sendJson(res, 503, {
1420
+ error: `Chrome not connected (CDP at ${cdpTarget})`,
1421
+ reason,
1422
+ hint: "Make sure Chrome is running."
1423
+ });
1424
+ return;
1425
+ }
1426
+ }
1427
+ let targetId;
1428
+ const targets = (await this.cdp.getTargets()).filter((t) => t.type === "page");
1429
+ const sameOriginTab = targets.find((t) => {
1430
+ try {
1431
+ const tabUrl = new URL(t.url);
1432
+ const targetUrl = new URL(domain);
1433
+ return tabUrl.origin === targetUrl.origin;
1434
+ } catch {
1435
+ return false;
1436
+ }
1437
+ });
1438
+ if (sameOriginTab) {
1439
+ targetId = sameOriginTab.id;
1440
+ } else {
1441
+ const created = await this.cdp.browserCommand(
1442
+ "Target.createTarget",
1443
+ { url: domain, background: true }
1444
+ );
1445
+ await this.cdp.attachAndEnable(created.targetId);
1446
+ targetId = created.targetId;
1447
+ await new Promise((resolve) => setTimeout(resolve, 2e3));
1448
+ }
1449
+ const cookiesResult = await this.cdp.sessionCommand(targetId, "Network.getCookies", {});
1450
+ const storageScript = `(() => {
1451
+ const local = {};
1452
+ const session = {};
1453
+ try {
1454
+ for (let i = 0; i < localStorage.length; i++) {
1455
+ const key = localStorage.key(i);
1456
+ if (key) local[key] = localStorage.getItem(key);
1457
+ }
1458
+ } catch (e) {}
1459
+ try {
1460
+ for (let i = 0; i < sessionStorage.length; i++) {
1461
+ const key = sessionStorage.key(i);
1462
+ if (key) session[key] = sessionStorage.getItem(key);
1463
+ }
1464
+ } catch (e) {}
1465
+ return { localStorage: local, sessionStorage: session };
1466
+ })()`;
1467
+ const storageResult = await this.cdp.evaluate(targetId, storageScript, true);
1468
+ this.sendJson(res, 200, {
1469
+ domain,
1470
+ cookies: cookiesResult.cookies,
1471
+ localStorage: storageResult?.localStorage ?? {},
1472
+ sessionStorage: storageResult?.sessionStorage ?? {}
1473
+ });
1474
+ } catch (error) {
1475
+ this.sendJson(res, 400, {
1476
+ error: error instanceof Error ? error.message : "Invalid request",
1477
+ hint: "\u8BF7\u6C42\u683C\u5F0F\u9519\u8BEF\u6216\u6267\u884C\u5931\u8D25"
1478
+ });
1479
+ }
1480
+ }
1481
+ // ---------------------------------------------------------------------------
1482
+ // GET /status
1483
+ // ---------------------------------------------------------------------------
1484
+ handleStatus(_req, res) {
1485
+ const tabs = this.cdp.tabManager.allTabs().map((tab) => ({
1486
+ shortId: tab.shortId,
1487
+ targetId: tab.targetId,
1488
+ networkRequests: tab.networkRequests.size,
1489
+ consoleMessages: tab.consoleMessages.size,
1490
+ jsErrors: tab.jsErrors.size,
1491
+ lastActionSeq: tab.lastActionSeq
1492
+ }));
1493
+ this.sendJson(res, 200, {
1494
+ running: true,
1495
+ cdpConnected: this.cdp.connected,
1496
+ uptime: this.uptime,
1497
+ currentSeq: this.cdp.tabManager.currentSeq(),
1498
+ currentTargetId: this.cdp.currentTargetId,
1499
+ tabs
1500
+ });
1501
+ }
1502
+ // ---------------------------------------------------------------------------
1503
+ // POST /shutdown
1504
+ // ---------------------------------------------------------------------------
1505
+ handleShutdown(_req, res) {
1506
+ this.sendJson(res, 200, { code: 0, message: "Shutting down" });
1507
+ setTimeout(() => {
1508
+ if (this.onShutdown) {
1509
+ this.onShutdown();
1510
+ }
1511
+ }, 100);
1512
+ }
1513
+ // ---------------------------------------------------------------------------
1514
+ // Utility
1515
+ // ---------------------------------------------------------------------------
1516
+ readBody(req) {
1517
+ return new Promise((resolve, reject) => {
1518
+ const chunks = [];
1519
+ req.on("data", (chunk) => chunks.push(chunk));
1520
+ req.on("end", () => resolve(Buffer.concat(chunks).toString("utf-8")));
1521
+ req.on("error", reject);
1522
+ });
1523
+ }
1524
+ sendJson(res, status, data) {
1525
+ const body = JSON.stringify(data);
1526
+ res.writeHead(status, {
1527
+ "Content-Type": "application/json",
1528
+ "Content-Length": Buffer.byteLength(body)
1529
+ });
1530
+ res.end(body);
1531
+ }
1532
+ };
1533
+
1534
+ // packages/daemon/src/cdp-connection.ts
1535
+ import { request as httpRequest } from "http";
1536
+ import WebSocket from "ws";
1537
+ function fetchJson(url) {
1538
+ return new Promise((resolve, reject) => {
1539
+ const req = httpRequest(url, { method: "GET" }, (res) => {
1540
+ const chunks = [];
1541
+ res.on("data", (chunk) => chunks.push(Buffer.from(chunk)));
1542
+ res.on("end", () => {
1543
+ const raw = Buffer.concat(chunks).toString("utf8");
1544
+ if ((res.statusCode ?? 500) >= 400) {
1545
+ reject(new Error(`HTTP ${res.statusCode ?? 500}: ${raw}`));
1546
+ return;
1547
+ }
1548
+ try {
1549
+ resolve(JSON.parse(raw));
1550
+ } catch (error) {
1551
+ reject(error);
1552
+ }
1553
+ });
1554
+ });
1555
+ req.on("error", reject);
1556
+ req.end();
1557
+ });
1558
+ }
1559
+ function connectWebSocket(url) {
1560
+ return new Promise((resolve, reject) => {
1561
+ const ws = new WebSocket(url);
1562
+ ws.once("open", () => resolve(ws));
1563
+ ws.once("error", reject);
1564
+ });
1565
+ }
1566
+ function normalizeHeaders(headers) {
1567
+ if (!headers || typeof headers !== "object") return void 0;
1568
+ return Object.fromEntries(
1569
+ Object.entries(headers).map(([key, value]) => [key, String(value)])
1570
+ );
1571
+ }
1572
+ var CdpConnection = class {
1573
+ socket = null;
1574
+ pending = /* @__PURE__ */ new Map();
1575
+ nextId = 1;
1576
+ /** targetId -> sessionId (flat-mode) */
1577
+ sessions = /* @__PURE__ */ new Map();
1578
+ /** sessionId -> targetId */
1579
+ attachedTargets = /* @__PURE__ */ new Map();
1580
+ host;
1581
+ port;
1582
+ tabManager;
1583
+ /** Current (most recently selected) target ID. */
1584
+ currentTargetId;
1585
+ connectionPromise = null;
1586
+ _connected = false;
1587
+ /** Last connection error (for diagnostics in 503 responses). */
1588
+ lastError = null;
1589
+ /** Resolvers for commands queued before CDP is ready. */
1590
+ readyWaiters = [];
1591
+ constructor(host, port, tabManager) {
1592
+ this.host = host;
1593
+ this.port = port;
1594
+ this.tabManager = tabManager;
1595
+ }
1596
+ get connected() {
1597
+ return this._connected && this.socket !== null && this.socket.readyState === WebSocket.OPEN;
1598
+ }
1599
+ // ---------------------------------------------------------------------------
1600
+ // Connection lifecycle
1601
+ // ---------------------------------------------------------------------------
1602
+ /**
1603
+ * Connect to Chrome's browser-level WebSocket endpoint.
1604
+ * Idempotent — returns immediately if already connected.
1605
+ */
1606
+ async connect() {
1607
+ if (this._connected) return;
1608
+ if (this.connectionPromise) return this.connectionPromise;
1609
+ this.connectionPromise = this.doConnect();
1610
+ try {
1611
+ await this.connectionPromise;
1612
+ this.lastError = null;
1613
+ } catch (err) {
1614
+ this.lastError = err instanceof Error ? err.message : String(err);
1615
+ const connErr = new Error(this.lastError);
1616
+ for (const waiter of this.readyWaiters) {
1617
+ waiter.reject(connErr);
1618
+ }
1619
+ this.readyWaiters = [];
1620
+ throw err;
1621
+ } finally {
1622
+ this.connectionPromise = null;
1623
+ }
1624
+ }
1625
+ async doConnect() {
1626
+ const versionData = await fetchJson(
1627
+ `http://${this.host}:${this.port}/json/version`
1628
+ );
1629
+ const wsUrl = versionData.webSocketDebuggerUrl;
1630
+ if (typeof wsUrl !== "string" || !wsUrl) {
1631
+ throw new Error("CDP endpoint missing webSocketDebuggerUrl");
1632
+ }
1633
+ const ws = await connectWebSocket(wsUrl);
1634
+ this.socket = ws;
1635
+ this._connected = true;
1636
+ this.setupListeners(ws);
1637
+ await this.browserCommand("Target.setDiscoverTargets", { discover: true });
1638
+ const result = await this.browserCommand("Target.getTargets");
1639
+ const pages = (result.targetInfos || []).filter((t) => t.type === "page");
1640
+ for (const page of pages) {
1641
+ await this.attachAndEnable(page.targetId).catch(() => {
1642
+ });
1643
+ }
1644
+ for (const waiter of this.readyWaiters) {
1645
+ waiter.resolve();
1646
+ }
1647
+ this.readyWaiters = [];
1648
+ }
1649
+ /** Wait until CDP connection is established (for two-phase startup). */
1650
+ waitUntilReady() {
1651
+ if (this._connected) return Promise.resolve();
1652
+ if (this.lastError) return Promise.reject(new Error(this.lastError));
1653
+ return new Promise((resolve, reject) => {
1654
+ this.readyWaiters.push({ resolve, reject });
1655
+ });
1656
+ }
1657
+ /** Gracefully close the CDP connection. */
1658
+ disconnect() {
1659
+ if (this.socket) {
1660
+ try {
1661
+ this.socket.close();
1662
+ } catch {
1663
+ }
1664
+ }
1665
+ this.socket = null;
1666
+ this._connected = false;
1667
+ for (const p of this.pending.values()) {
1668
+ p.reject(new Error("CDP connection closed"));
1669
+ }
1670
+ this.pending.clear();
1671
+ for (const waiter of this.readyWaiters) {
1672
+ waiter.reject(new Error("CDP connection closed before ready"));
1673
+ }
1674
+ this.readyWaiters = [];
1675
+ }
1676
+ // ---------------------------------------------------------------------------
1677
+ // WebSocket message handling
1678
+ // ---------------------------------------------------------------------------
1679
+ setupListeners(ws) {
1680
+ ws.on("message", (raw) => {
1681
+ const message = JSON.parse(raw.toString());
1682
+ if (typeof message.id === "number") {
1683
+ const p = this.pending.get(message.id);
1684
+ if (!p) return;
1685
+ this.pending.delete(message.id);
1686
+ if (message.error) {
1687
+ p.reject(
1688
+ new Error(
1689
+ `${p.method}: ${message.error.message ?? "Unknown CDP error"}`
1690
+ )
1691
+ );
1692
+ } else {
1693
+ p.resolve(message.result);
1694
+ }
1695
+ return;
1696
+ }
1697
+ if (message.method === "Target.attachedToTarget") {
1698
+ const params = message.params;
1699
+ const sessionId = params.sessionId;
1700
+ const targetInfo = params.targetInfo;
1701
+ if (typeof sessionId === "string" && typeof targetInfo?.targetId === "string") {
1702
+ this.sessions.set(targetInfo.targetId, sessionId);
1703
+ this.attachedTargets.set(sessionId, targetInfo.targetId);
1704
+ }
1705
+ return;
1706
+ }
1707
+ if (message.method === "Target.detachedFromTarget") {
1708
+ const params = message.params;
1709
+ const sessionId = params.sessionId;
1710
+ if (typeof sessionId === "string") {
1711
+ const targetId = this.attachedTargets.get(sessionId);
1712
+ if (targetId) {
1713
+ this.sessions.delete(targetId);
1714
+ this.attachedTargets.delete(sessionId);
1715
+ this.tabManager.removeTab(targetId);
1716
+ if (this.currentTargetId === targetId) {
1717
+ this.currentTargetId = void 0;
1718
+ }
1719
+ }
1720
+ }
1721
+ return;
1722
+ }
1723
+ if (message.method === "Target.targetCreated") {
1724
+ const params = message.params;
1725
+ const targetInfo = params.targetInfo;
1726
+ if (targetInfo?.type === "page" && typeof targetInfo.targetId === "string") {
1727
+ this.attachAndEnable(targetInfo.targetId).catch(() => {
1728
+ });
1729
+ }
1730
+ return;
1731
+ }
1732
+ if (message.method === "Target.targetDestroyed") {
1733
+ const params = message.params;
1734
+ const targetId = params.targetId;
1735
+ if (typeof targetId === "string") {
1736
+ const sessionId = this.sessions.get(targetId);
1737
+ if (sessionId) {
1738
+ this.sessions.delete(targetId);
1739
+ this.attachedTargets.delete(sessionId);
1740
+ }
1741
+ this.tabManager.removeTab(targetId);
1742
+ if (this.currentTargetId === targetId) {
1743
+ this.currentTargetId = void 0;
1744
+ }
1745
+ }
1746
+ return;
1747
+ }
1748
+ if (typeof message.sessionId === "string" && typeof message.method === "string") {
1749
+ const targetId = this.attachedTargets.get(message.sessionId);
1750
+ if (targetId) {
1751
+ this.handleSessionEvent(targetId, message).catch(() => {
1752
+ });
1753
+ }
1754
+ }
1755
+ });
1756
+ ws.on("close", () => {
1757
+ this._connected = false;
1758
+ this.socket = null;
1759
+ this.lastError = "CDP WebSocket closed unexpectedly";
1760
+ for (const p of this.pending.values()) {
1761
+ p.reject(new Error("CDP connection closed"));
1762
+ }
1763
+ this.pending.clear();
1764
+ const closeErr = new Error(this.lastError);
1765
+ for (const waiter of this.readyWaiters) {
1766
+ waiter.reject(closeErr);
1767
+ }
1768
+ this.readyWaiters = [];
1769
+ });
1770
+ ws.on("error", () => {
1771
+ });
1772
+ }
1773
+ // ---------------------------------------------------------------------------
1774
+ // Session event routing (network, console, errors, dialog)
1775
+ // ---------------------------------------------------------------------------
1776
+ async handleSessionEvent(targetId, event) {
1777
+ const method = event.method;
1778
+ const params = event.params ?? {};
1779
+ if (typeof method !== "string") return;
1780
+ const tab = this.tabManager.getTab(targetId);
1781
+ if (!tab) return;
1782
+ if (method === "Page.javascriptDialogOpening") {
1783
+ if (tab.dialogHandler) {
1784
+ await this.sessionCommand(targetId, "Page.handleJavaScriptDialog", {
1785
+ accept: tab.dialogHandler.accept,
1786
+ ...tab.dialogHandler.promptText !== void 0 ? { promptText: tab.dialogHandler.promptText } : {}
1787
+ });
1788
+ }
1789
+ return;
1790
+ }
1791
+ if (method === "Network.requestWillBeSent") {
1792
+ const requestId = typeof params.requestId === "string" ? params.requestId : void 0;
1793
+ const request = params.request;
1794
+ if (!requestId || !request) return;
1795
+ tab.addNetworkRequest(requestId, {
1796
+ url: String(request.url ?? ""),
1797
+ method: String(request.method ?? "GET"),
1798
+ type: String(params.type ?? "Other"),
1799
+ timestamp: Math.round(Number(params.timestamp ?? Date.now()) * 1e3),
1800
+ requestHeaders: normalizeHeaders(request.headers),
1801
+ requestBody: typeof request.postData === "string" ? request.postData : void 0
1802
+ });
1803
+ return;
1804
+ }
1805
+ if (method === "Network.responseReceived") {
1806
+ const requestId = typeof params.requestId === "string" ? params.requestId : void 0;
1807
+ const response = params.response;
1808
+ if (!requestId || !response) return;
1809
+ tab.updateNetworkResponse(requestId, {
1810
+ status: typeof response.status === "number" ? response.status : void 0,
1811
+ statusText: typeof response.statusText === "string" ? response.statusText : void 0,
1812
+ responseHeaders: normalizeHeaders(response.headers),
1813
+ mimeType: typeof response.mimeType === "string" ? response.mimeType : void 0
1814
+ });
1815
+ return;
1816
+ }
1817
+ if (method === "Network.loadingFailed") {
1818
+ const requestId = typeof params.requestId === "string" ? params.requestId : void 0;
1819
+ if (!requestId) return;
1820
+ tab.updateNetworkFailure(
1821
+ requestId,
1822
+ typeof params.errorText === "string" ? params.errorText : "Unknown error"
1823
+ );
1824
+ return;
1825
+ }
1826
+ if (method === "Runtime.consoleAPICalled") {
1827
+ const type = String(params.type ?? "log");
1828
+ const args = Array.isArray(params.args) ? params.args : [];
1829
+ const text = args.map((arg) => {
1830
+ if (typeof arg.value === "string") return arg.value;
1831
+ if (arg.value !== void 0) return String(arg.value);
1832
+ if (typeof arg.description === "string") return arg.description;
1833
+ return "";
1834
+ }).filter(Boolean).join(" ");
1835
+ const stack = params.stackTrace;
1836
+ const firstCallFrame = Array.isArray(stack?.callFrames) ? stack?.callFrames[0] : void 0;
1837
+ const consoleTypeMap = { warning: "warn" };
1838
+ const normalizedType = consoleTypeMap[type] || type;
1839
+ tab.addConsoleMessage({
1840
+ type: ["log", "info", "warn", "error", "debug"].includes(normalizedType) ? normalizedType : "log",
1841
+ text,
1842
+ timestamp: Math.round(Number(params.timestamp ?? Date.now())),
1843
+ url: typeof firstCallFrame?.url === "string" ? firstCallFrame.url : void 0,
1844
+ lineNumber: typeof firstCallFrame?.lineNumber === "number" ? firstCallFrame.lineNumber : void 0
1845
+ });
1846
+ return;
1847
+ }
1848
+ if (method === "Runtime.exceptionThrown") {
1849
+ const details = params.exceptionDetails;
1850
+ if (!details) return;
1851
+ const exception = details.exception;
1852
+ const stackTrace = details.stackTrace;
1853
+ const callFrames = Array.isArray(stackTrace?.callFrames) ? stackTrace.callFrames : [];
1854
+ tab.addJSError({
1855
+ message: typeof exception?.description === "string" ? exception.description : String(details.text ?? "JavaScript exception"),
1856
+ url: typeof details.url === "string" ? details.url : typeof callFrames[0]?.url === "string" ? String(callFrames[0].url) : void 0,
1857
+ lineNumber: typeof details.lineNumber === "number" ? details.lineNumber : void 0,
1858
+ columnNumber: typeof details.columnNumber === "number" ? details.columnNumber : void 0,
1859
+ stackTrace: callFrames.length > 0 ? callFrames.map(
1860
+ (frame) => `${String(frame.functionName ?? "<anonymous>")} (${String(frame.url ?? "")}:${String(frame.lineNumber ?? 0)}:${String(frame.columnNumber ?? 0)})`
1861
+ ).join("\n") : void 0,
1862
+ timestamp: Date.now()
1863
+ });
1864
+ }
1865
+ }
1866
+ // ---------------------------------------------------------------------------
1867
+ // Target management
1868
+ // ---------------------------------------------------------------------------
1869
+ /** Attach to a target and enable required CDP domains. */
1870
+ async attachAndEnable(targetId) {
1871
+ if (this.sessions.has(targetId)) {
1872
+ this.tabManager.addTab(targetId);
1873
+ return this.sessions.get(targetId);
1874
+ }
1875
+ const result = await this.browserCommand(
1876
+ "Target.attachToTarget",
1877
+ { targetId, flatten: true }
1878
+ );
1879
+ this.sessions.set(targetId, result.sessionId);
1880
+ this.attachedTargets.set(result.sessionId, targetId);
1881
+ this.tabManager.addTab(targetId);
1882
+ await this.sessionCommand(targetId, "Page.enable").catch(() => {
1883
+ });
1884
+ await this.sessionCommand(targetId, "Runtime.enable").catch(() => {
1885
+ });
1886
+ await this.sessionCommand(targetId, "Network.enable").catch(() => {
1887
+ });
1888
+ await this.sessionCommand(targetId, "DOM.enable").catch(() => {
1889
+ });
1890
+ await this.sessionCommand(targetId, "Accessibility.enable").catch(() => {
1891
+ });
1892
+ return result.sessionId;
1893
+ }
1894
+ /** Get all targets via CDP Target.getTargets. */
1895
+ async getTargets() {
1896
+ const result = await this.browserCommand("Target.getTargets");
1897
+ return (result.targetInfos || []).map((t) => ({
1898
+ id: t.targetId,
1899
+ type: t.type,
1900
+ title: t.title,
1901
+ url: t.url
1902
+ }));
1903
+ }
1904
+ /**
1905
+ * Ensure we have a valid page target and return it. Supports resolution by:
1906
+ * - short ID string
1907
+ * - full target ID string
1908
+ * - numeric index
1909
+ * - undefined (use currentTargetId or first page)
1910
+ */
1911
+ async ensurePageTarget(tabRef) {
1912
+ const targets = (await this.getTargets()).filter((t) => t.type === "page");
1913
+ if (targets.length === 0) throw new Error("No page target found");
1914
+ let target;
1915
+ if (typeof tabRef === "string") {
1916
+ const resolvedTargetId = this.tabManager.resolveShortId(tabRef);
1917
+ if (resolvedTargetId) {
1918
+ target = targets.find((t) => t.id === resolvedTargetId);
1919
+ }
1920
+ if (!target) {
1921
+ target = targets.find((t) => t.id === tabRef);
1922
+ }
1923
+ if (!target) {
1924
+ const num = Number(tabRef);
1925
+ if (!Number.isNaN(num)) {
1926
+ target = targets[num];
1927
+ }
1928
+ }
1929
+ } else if (typeof tabRef === "number") {
1930
+ target = targets[tabRef];
1931
+ } else if (this.currentTargetId) {
1932
+ target = targets.find((t) => t.id === this.currentTargetId);
1933
+ }
1934
+ if (typeof tabRef === "string" && !target) {
1935
+ throw new Error(`Tab not found: ${tabRef}`);
1936
+ }
1937
+ if (!target) {
1938
+ target = targets.find((t) => t.url && !t.url.startsWith("about:") && !t.url.startsWith("chrome:"));
1939
+ }
1940
+ target ??= targets[0];
1941
+ this.currentTargetId = target.id;
1942
+ await this.attachAndEnable(target.id);
1943
+ return target;
1944
+ }
1945
+ /** Check if a session exists for a given targetId. */
1946
+ hasSession(targetId) {
1947
+ return this.sessions.has(targetId);
1948
+ }
1949
+ // ---------------------------------------------------------------------------
1950
+ // CDP command sending
1951
+ // ---------------------------------------------------------------------------
1952
+ /** Send a browser-level CDP command. */
1953
+ async browserCommand(method, params = {}) {
1954
+ if (!this.socket) throw new Error("CDP not connected");
1955
+ const id = this.nextId++;
1956
+ const payload = JSON.stringify({ id, method, params });
1957
+ return new Promise((resolve, reject) => {
1958
+ this.pending.set(id, {
1959
+ resolve,
1960
+ reject,
1961
+ method
1962
+ });
1963
+ this.socket.send(payload);
1964
+ });
1965
+ }
1966
+ /** Send a session-level CDP command (flat protocol). */
1967
+ async sessionCommand(targetId, method, params = {}) {
1968
+ if (!this.socket) throw new Error("CDP not connected");
1969
+ const sessionId = this.sessions.get(targetId) ?? await this.attachAndEnable(targetId);
1970
+ const id = this.nextId++;
1971
+ const payload = JSON.stringify({ id, method, params, sessionId });
1972
+ return new Promise((resolve, reject) => {
1973
+ const check = (raw) => {
1974
+ const msg = JSON.parse(raw.toString());
1975
+ if (msg.id === id && msg.sessionId === sessionId) {
1976
+ this.socket.off("message", check);
1977
+ if (msg.error) {
1978
+ reject(
1979
+ new Error(
1980
+ `${method}: ${msg.error.message ?? "Unknown CDP error"}`
1981
+ )
1982
+ );
1983
+ } else {
1984
+ resolve(msg.result);
1985
+ }
1986
+ }
1987
+ };
1988
+ this.socket.on("message", check);
1989
+ this.socket.send(payload);
1990
+ });
1991
+ }
1992
+ /**
1993
+ * Send a page-scoped command. If the tab has an active iframe,
1994
+ * the frameId is injected into the params.
1995
+ */
1996
+ async pageCommand(targetId, method, params = {}) {
1997
+ const tab = this.tabManager.getTab(targetId);
1998
+ const frameId = tab?.activeFrameId;
1999
+ return this.sessionCommand(
2000
+ targetId,
2001
+ method,
2002
+ frameId ? { ...params, frameId } : params
2003
+ );
2004
+ }
2005
+ /**
2006
+ * Evaluate JavaScript expression on a target.
2007
+ */
2008
+ async evaluate(targetId, expression, returnByValue = true) {
2009
+ const result = await this.sessionCommand(targetId, "Runtime.evaluate", {
2010
+ expression,
2011
+ awaitPromise: true,
2012
+ returnByValue
2013
+ });
2014
+ if (result.exceptionDetails) {
2015
+ throw new Error(
2016
+ result.exceptionDetails.exception?.description || result.exceptionDetails.text || "Runtime.evaluate failed"
2017
+ );
2018
+ }
2019
+ return result.result.value ?? result.result;
2020
+ }
2021
+ };
2022
+
2023
+ // packages/daemon/src/ring-buffer.ts
2024
+ var RingBuffer = class {
2025
+ items;
2026
+ head = 0;
2027
+ // next write position
2028
+ count = 0;
2029
+ capacity;
2030
+ constructor(capacity) {
2031
+ if (capacity < 1) throw new Error("RingBuffer capacity must be >= 1");
2032
+ this.capacity = capacity;
2033
+ this.items = new Array(capacity);
2034
+ }
2035
+ /** Number of elements currently stored. */
2036
+ get size() {
2037
+ return this.count;
2038
+ }
2039
+ /** Push a new element, evicting the oldest if at capacity. */
2040
+ push(item) {
2041
+ this.items[this.head] = item;
2042
+ this.head = (this.head + 1) % this.capacity;
2043
+ if (this.count < this.capacity) {
2044
+ this.count++;
2045
+ }
2046
+ }
2047
+ /** Return all stored elements in insertion order (oldest first). */
2048
+ toArray() {
2049
+ if (this.count === 0) return [];
2050
+ const result = new Array(this.count);
2051
+ const start = this.count < this.capacity ? 0 : this.head;
2052
+ for (let i = 0; i < this.count; i++) {
2053
+ result[i] = this.items[(start + i) % this.capacity];
2054
+ }
2055
+ return result;
2056
+ }
2057
+ /** Remove all elements. */
2058
+ clear() {
2059
+ this.items.fill(void 0);
2060
+ this.head = 0;
2061
+ this.count = 0;
2062
+ }
2063
+ };
2064
+
2065
+ // packages/daemon/src/tab-state.ts
2066
+ var NETWORK_CAPACITY = 500;
2067
+ var CONSOLE_CAPACITY = 200;
2068
+ var ERRORS_CAPACITY = 100;
2069
+ var TabState = class {
2070
+ constructor(targetId, shortId, nextSeq) {
2071
+ this.nextSeq = nextSeq;
2072
+ this.targetId = targetId;
2073
+ this.shortId = shortId;
2074
+ }
2075
+ targetId;
2076
+ shortId;
2077
+ networkRequests = new RingBuffer(NETWORK_CAPACITY);
2078
+ consoleMessages = new RingBuffer(CONSOLE_CAPACITY);
2079
+ jsErrors = new RingBuffer(ERRORS_CAPACITY);
2080
+ /** Lookup in-flight network requests by requestId for response/failure updates. */
2081
+ networkByRequestId = /* @__PURE__ */ new Map();
2082
+ /** seq of the last user-initiated action on this tab. */
2083
+ lastActionSeq = 0;
2084
+ /** Element refs from the most recent snapshot. */
2085
+ refs = {};
2086
+ /** Active frame ID for iframe navigation, null = main frame. */
2087
+ activeFrameId = null;
2088
+ /** Dialog auto-handler config. */
2089
+ dialogHandler = null;
2090
+ // --------------- Action seq ---------------
2091
+ /** Increment global seq and record it as this tab's last action. */
2092
+ recordAction() {
2093
+ const seq = this.nextSeq();
2094
+ this.lastActionSeq = seq;
2095
+ return seq;
2096
+ }
2097
+ // --------------- Network events ---------------
2098
+ addNetworkRequest(requestId, info) {
2099
+ const seq = this.nextSeq();
2100
+ const entry = { ...info, requestId, seq };
2101
+ this.networkRequests.push(entry);
2102
+ this.networkByRequestId.set(requestId, entry);
2103
+ }
2104
+ updateNetworkResponse(requestId, data) {
2105
+ const existing = this.networkByRequestId.get(requestId);
2106
+ if (!existing) return;
2107
+ if (data.status !== void 0) existing.status = data.status;
2108
+ if (data.statusText !== void 0) existing.statusText = data.statusText;
2109
+ if (data.responseHeaders !== void 0) existing.responseHeaders = data.responseHeaders;
2110
+ if (data.mimeType !== void 0) existing.mimeType = data.mimeType;
2111
+ }
2112
+ updateNetworkFailure(requestId, reason) {
2113
+ const existing = this.networkByRequestId.get(requestId);
2114
+ if (!existing) return;
2115
+ existing.failed = true;
2116
+ existing.failureReason = reason;
2117
+ }
2118
+ // --------------- Console events ---------------
2119
+ addConsoleMessage(info) {
2120
+ const seq = this.nextSeq();
2121
+ this.consoleMessages.push({ ...info, seq });
2122
+ }
2123
+ // --------------- JS Error events ---------------
2124
+ addJSError(info) {
2125
+ const seq = this.nextSeq();
2126
+ this.jsErrors.push({ ...info, seq });
2127
+ }
2128
+ // --------------- Query helpers ---------------
2129
+ getNetworkRequests(options) {
2130
+ let items = this.networkRequests.toArray();
2131
+ if (options?.since !== void 0) {
2132
+ const threshold = options.since === "last_action" ? this.lastActionSeq : options.since;
2133
+ items = items.filter((item) => item.seq > threshold);
2134
+ }
2135
+ if (options?.filter) {
2136
+ const f = options.filter;
2137
+ items = items.filter((item) => item.url.includes(f));
2138
+ }
2139
+ if (options?.method) {
2140
+ const m = options.method.toUpperCase();
2141
+ items = items.filter((item) => item.method === m);
2142
+ }
2143
+ if (options?.status) {
2144
+ const s = options.status;
2145
+ if (s === "4xx") {
2146
+ items = items.filter((item) => item.status !== void 0 && item.status >= 400 && item.status < 500);
2147
+ } else if (s === "5xx") {
2148
+ items = items.filter((item) => item.status !== void 0 && item.status >= 500 && item.status < 600);
2149
+ } else {
2150
+ const code = Number(s);
2151
+ if (!Number.isNaN(code)) {
2152
+ items = items.filter((item) => item.status === code);
2153
+ }
2154
+ }
2155
+ }
2156
+ if (options?.limit !== void 0 && options.limit > 0 && items.length > options.limit) {
2157
+ items = items.slice(-options.limit);
2158
+ }
2159
+ const sinceThreshold = options?.since !== void 0 ? options.since === "last_action" ? this.lastActionSeq : options.since : 0;
2160
+ const cursor = items.length > 0 ? Math.max(...items.map((i) => i.seq)) : sinceThreshold;
2161
+ return { items, cursor };
2162
+ }
2163
+ getConsoleMessages(options) {
2164
+ let items = this.consoleMessages.toArray();
2165
+ if (options?.since !== void 0) {
2166
+ const threshold = options.since === "last_action" ? this.lastActionSeq : options.since;
2167
+ items = items.filter((item) => item.seq > threshold);
2168
+ }
2169
+ if (options?.filter) {
2170
+ const f = options.filter;
2171
+ items = items.filter((item) => item.text.includes(f));
2172
+ }
2173
+ if (options?.limit !== void 0 && options.limit > 0 && items.length > options.limit) {
2174
+ items = items.slice(-options.limit);
2175
+ }
2176
+ const sinceThreshold = options?.since !== void 0 ? options.since === "last_action" ? this.lastActionSeq : options.since : 0;
2177
+ const cursor = items.length > 0 ? Math.max(...items.map((i) => i.seq)) : sinceThreshold;
2178
+ return { items, cursor };
2179
+ }
2180
+ getJSErrors(options) {
2181
+ let items = this.jsErrors.toArray();
2182
+ if (options?.since !== void 0) {
2183
+ const threshold = options.since === "last_action" ? this.lastActionSeq : options.since;
2184
+ items = items.filter((item) => item.seq > threshold);
2185
+ }
2186
+ if (options?.filter) {
2187
+ const f = options.filter;
2188
+ items = items.filter(
2189
+ (item) => item.message.includes(f) || (item.url?.includes(f) ?? false)
2190
+ );
2191
+ }
2192
+ if (options?.limit !== void 0 && options.limit > 0 && items.length > options.limit) {
2193
+ items = items.slice(-options.limit);
2194
+ }
2195
+ const sinceThreshold = options?.since !== void 0 ? options.since === "last_action" ? this.lastActionSeq : options.since : 0;
2196
+ const cursor = items.length > 0 ? Math.max(...items.map((i) => i.seq)) : sinceThreshold;
2197
+ return { items, cursor };
2198
+ }
2199
+ // --------------- Clear helpers ---------------
2200
+ clearNetwork() {
2201
+ this.networkRequests.clear();
2202
+ this.networkByRequestId.clear();
2203
+ }
2204
+ clearConsole() {
2205
+ this.consoleMessages.clear();
2206
+ }
2207
+ clearErrors() {
2208
+ this.jsErrors.clear();
2209
+ }
2210
+ };
2211
+ var TabStateManager = class {
2212
+ seq = 0;
2213
+ tabs = /* @__PURE__ */ new Map();
2214
+ // targetId -> TabState
2215
+ shortToTarget = /* @__PURE__ */ new Map();
2216
+ // shortId -> targetId
2217
+ targetToShort = /* @__PURE__ */ new Map();
2218
+ // targetId -> shortId
2219
+ /** Generate a globally unique short ID for a target. */
2220
+ generateShortId(targetId) {
2221
+ for (let len = 4; len <= targetId.length; len++) {
2222
+ const candidate = targetId.slice(-len).toLowerCase();
2223
+ if (!this.shortToTarget.has(candidate)) {
2224
+ return candidate;
2225
+ }
2226
+ }
2227
+ return targetId.toLowerCase();
2228
+ }
2229
+ /** Get next seq (globally monotonic). */
2230
+ nextSeq() {
2231
+ return ++this.seq;
2232
+ }
2233
+ /** Get current seq without incrementing. */
2234
+ currentSeq() {
2235
+ return this.seq;
2236
+ }
2237
+ /** Register a new tab. Returns the TabState. */
2238
+ addTab(targetId) {
2239
+ const existing = this.tabs.get(targetId);
2240
+ if (existing) return existing;
2241
+ const shortId = this.generateShortId(targetId);
2242
+ const tab = new TabState(targetId, shortId, () => this.nextSeq());
2243
+ this.tabs.set(targetId, tab);
2244
+ this.shortToTarget.set(shortId, targetId);
2245
+ this.targetToShort.set(targetId, shortId);
2246
+ return tab;
2247
+ }
2248
+ /** Remove a tab (on targetDestroyed / detach). */
2249
+ removeTab(targetId) {
2250
+ const tab = this.tabs.get(targetId);
2251
+ if (!tab) return;
2252
+ this.shortToTarget.delete(tab.shortId);
2253
+ this.targetToShort.delete(targetId);
2254
+ this.tabs.delete(targetId);
2255
+ }
2256
+ /** Get tab by targetId. */
2257
+ getTab(targetId) {
2258
+ return this.tabs.get(targetId);
2259
+ }
2260
+ /** Resolve a short ID to a targetId. */
2261
+ resolveShortId(shortId) {
2262
+ return this.shortToTarget.get(shortId);
2263
+ }
2264
+ /** Get the short ID for a targetId. */
2265
+ getShortId(targetId) {
2266
+ return this.targetToShort.get(targetId);
2267
+ }
2268
+ /** Get all active tabs. */
2269
+ allTabs() {
2270
+ return Array.from(this.tabs.values());
2271
+ }
2272
+ /** Get tab count. */
2273
+ get tabCount() {
2274
+ return this.tabs.size;
2275
+ }
2276
+ };
2277
+
2278
+ // packages/daemon/src/index.ts
2279
+ var DAEMON_DIR = process.env.BB_BROWSER_HOME || path2.join(os.homedir(), ".bb-browser");
2280
+ var DAEMON_JSON = path2.join(DAEMON_DIR, "daemon.json");
2281
+ var DEFAULT_CDP_PORT = 19825;
2282
+ function parseOptions() {
2283
+ const { values } = parseArgs({
2284
+ allowPositionals: true,
2285
+ options: {
2286
+ host: {
2287
+ type: "string",
2288
+ short: "H",
2289
+ default: DAEMON_HOST
2290
+ },
2291
+ port: {
2292
+ type: "string",
2293
+ short: "p",
2294
+ default: String(DAEMON_PORT)
2295
+ },
2296
+ "cdp-host": {
2297
+ type: "string",
2298
+ default: "127.0.0.1"
2299
+ },
2300
+ "cdp-port": {
2301
+ type: "string",
2302
+ default: String(DEFAULT_CDP_PORT)
2303
+ },
2304
+ token: {
2305
+ type: "string",
2306
+ default: ""
2307
+ },
2308
+ help: {
2309
+ type: "boolean",
2310
+ short: "h",
2311
+ default: false
2312
+ }
2313
+ }
2314
+ });
2315
+ if (values.help) {
2316
+ console.error(`
2317
+ bb-browser-daemon \u2014 CDP-direct backend for bb-browser
2318
+
2319
+ Usage:
2320
+ bb-browser-daemon [options]
2321
+
2322
+ Options:
2323
+ -H, --host <host> HTTP server host (default: ${DAEMON_HOST})
2324
+ -p, --port <port> HTTP server port (default: ${DAEMON_PORT})
2325
+ --cdp-host <host> Chrome CDP host (default: 127.0.0.1)
2326
+ --cdp-port <port> Chrome CDP port (default: ${DEFAULT_CDP_PORT})
2327
+ --token <token> Bearer auth token (auto-generated if empty)
2328
+ -h, --help Show this help message
2329
+
2330
+ Endpoints:
2331
+ POST /command Send command and get result (via CDP)
2332
+ GET /status Daemon health + per-tab stats
2333
+ POST /shutdown Graceful shutdown
2334
+ `);
2335
+ process.exit(0);
2336
+ }
2337
+ let token = values.token ?? "";
2338
+ const host = values.host ?? DAEMON_HOST;
2339
+ if (!token && host !== "127.0.0.1" && host !== "localhost") {
2340
+ token = randomBytes(16).toString("hex");
2341
+ }
2342
+ return {
2343
+ host: values.host ?? DAEMON_HOST,
2344
+ port: parseInt(values.port ?? String(DAEMON_PORT), 10),
2345
+ cdpHost: values["cdp-host"] ?? "127.0.0.1",
2346
+ cdpPort: parseInt(values["cdp-port"] ?? String(DEFAULT_CDP_PORT), 10),
2347
+ token
2348
+ };
2349
+ }
2350
+ function writeDaemonJson(info) {
2351
+ try {
2352
+ mkdirSync(DAEMON_DIR, { recursive: true });
2353
+ writeFileSync(DAEMON_JSON, JSON.stringify(info), { mode: 384 });
2354
+ } catch {
2355
+ }
2356
+ }
2357
+ function cleanupDaemonJson() {
2358
+ if (existsSync(DAEMON_JSON)) {
2359
+ try {
2360
+ unlinkSync(DAEMON_JSON);
2361
+ } catch {
2362
+ }
2363
+ }
2364
+ }
2365
+ async function discoverCdpPort(host, port) {
2366
+ try {
2367
+ const controller = new AbortController();
2368
+ const timer = setTimeout(() => controller.abort(), 2e3);
2369
+ try {
2370
+ const response = await fetch(`http://${host}:${port}/json/version`, {
2371
+ signal: controller.signal
2372
+ });
2373
+ if (response.ok) {
2374
+ return { host, port };
2375
+ }
2376
+ } finally {
2377
+ clearTimeout(timer);
2378
+ }
2379
+ } catch {
2380
+ }
2381
+ const managedPortFile = path2.join(DAEMON_DIR, "browser", "cdp-port");
2382
+ try {
2383
+ const rawPort = readFileSync2(managedPortFile, "utf8").trim();
2384
+ const managedPort = parseInt(rawPort, 10);
2385
+ if (Number.isInteger(managedPort) && managedPort > 0) {
2386
+ try {
2387
+ const controller = new AbortController();
2388
+ const timer = setTimeout(() => controller.abort(), 2e3);
2389
+ try {
2390
+ const response = await fetch(`http://127.0.0.1:${managedPort}/json/version`, {
2391
+ signal: controller.signal
2392
+ });
2393
+ if (response.ok) {
2394
+ return { host: "127.0.0.1", port: managedPort };
2395
+ }
2396
+ } finally {
2397
+ clearTimeout(timer);
2398
+ }
2399
+ } catch {
2400
+ }
2401
+ }
2402
+ } catch {
2403
+ }
2404
+ throw new Error(
2405
+ `Cannot connect to Chrome CDP at ${host}:${port}. Make sure Chrome is running with --remote-debugging-port=${port}`
2406
+ );
2407
+ }
2408
+ async function main() {
2409
+ const options = parseOptions();
2410
+ const tabManager = new TabStateManager();
2411
+ let cdpEndpoint;
2412
+ try {
2413
+ cdpEndpoint = await discoverCdpPort(options.cdpHost, options.cdpPort);
2414
+ } catch (error) {
2415
+ console.error(
2416
+ `[Daemon] ${error instanceof Error ? error.message : String(error)}`
2417
+ );
2418
+ process.exit(1);
2419
+ }
2420
+ const cdp = new CdpConnection(cdpEndpoint.host, cdpEndpoint.port, tabManager);
2421
+ let shuttingDown = false;
2422
+ const shutdown = async () => {
2423
+ if (shuttingDown) return;
2424
+ shuttingDown = true;
2425
+ console.error("[Daemon] Shutting down...");
2426
+ cdp.disconnect();
2427
+ await httpServer.stop();
2428
+ cleanupDaemonJson();
2429
+ process.exit(0);
2430
+ };
2431
+ const httpServer = new HttpServer({
2432
+ host: options.host,
2433
+ port: options.port,
2434
+ token: options.token,
2435
+ cdp,
2436
+ onShutdown: shutdown
2437
+ });
2438
+ process.on("SIGINT", shutdown);
2439
+ process.on("SIGTERM", shutdown);
2440
+ await httpServer.start();
2441
+ writeDaemonJson({
2442
+ pid: process.pid,
2443
+ host: options.host,
2444
+ port: options.port,
2445
+ token: options.token
2446
+ });
2447
+ console.error(
2448
+ `[Daemon] HTTP server listening on http://${options.host}:${options.port}`
2449
+ );
2450
+ console.error(`[Daemon] Auth token: ${options.token}`);
2451
+ console.error(
2452
+ `[Daemon] Connecting to Chrome CDP at ${cdpEndpoint.host}:${cdpEndpoint.port}...`
2453
+ );
2454
+ try {
2455
+ await cdp.connect();
2456
+ const tabCount = tabManager.tabCount;
2457
+ console.error(
2458
+ `[Daemon] CDP connected, monitoring ${tabCount} tab(s)`
2459
+ );
2460
+ } catch (error) {
2461
+ console.error(
2462
+ `[Daemon] Failed to connect to CDP: ${error instanceof Error ? error.message : String(error)}`
2463
+ );
2464
+ console.error("[Daemon] HTTP server is running, but commands will fail until CDP connects.");
2465
+ }
2466
+ }
2467
+ main().catch((error) => {
2468
+ console.error("[Daemon] Fatal error:", error);
2469
+ cleanupDaemonJson();
2470
+ process.exit(1);
2471
+ });
2472
+ //# sourceMappingURL=daemon.js.map