opendevbrowser 0.0.11 → 0.0.12

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.
@@ -0,0 +1,2815 @@
1
+ // src/extension-extractor.ts
2
+ import { existsSync, mkdirSync, cpSync, readFileSync, writeFileSync, rmSync, renameSync } from "fs";
3
+ import { dirname, join } from "path";
4
+ import { homedir } from "os";
5
+ import { fileURLToPath } from "url";
6
+ var EXTENSION_DIR_NAME = "opendevbrowser";
7
+ var VERSION_FILE = ".version";
8
+ function getConfigDir() {
9
+ return join(homedir(), ".config", "opencode", EXTENSION_DIR_NAME, "extension");
10
+ }
11
+ function getPackageVersion() {
12
+ try {
13
+ const pkgPath = join(dirname(fileURLToPath(import.meta.url)), "..", "package.json");
14
+ const pkg = JSON.parse(readFileSync(pkgPath, "utf-8"));
15
+ return pkg.version || "0.0.0";
16
+ } catch (error) {
17
+ console.warn("[opendevbrowser] Failed to read package.json for extension version:", error);
18
+ return "0.0.0";
19
+ }
20
+ }
21
+ function getInstalledVersion(destDir) {
22
+ try {
23
+ const versionPath = join(destDir, VERSION_FILE);
24
+ if (existsSync(versionPath)) {
25
+ return readFileSync(versionPath, "utf-8").trim();
26
+ }
27
+ } catch (error) {
28
+ console.warn("[opendevbrowser] Failed to read installed extension version:", error);
29
+ }
30
+ return null;
31
+ }
32
+ function getBundledExtensionPath() {
33
+ const candidates = [
34
+ join(dirname(fileURLToPath(import.meta.url)), "..", "extension"),
35
+ join(dirname(fileURLToPath(import.meta.url)), "..", "..", "extension")
36
+ ];
37
+ for (const candidate of candidates) {
38
+ if (existsSync(join(candidate, "manifest.json"))) {
39
+ return candidate;
40
+ }
41
+ }
42
+ return null;
43
+ }
44
+ function isCompleteInstall(dir) {
45
+ const required = ["manifest.json", VERSION_FILE];
46
+ return required.every((file) => existsSync(join(dir, file)));
47
+ }
48
+ function extractExtension() {
49
+ const bundledPath = getBundledExtensionPath();
50
+ if (!bundledPath) {
51
+ return null;
52
+ }
53
+ const destDir = getConfigDir();
54
+ const currentVersion = getPackageVersion();
55
+ const installedVersion = getInstalledVersion(destDir);
56
+ if (installedVersion === currentVersion && isCompleteInstall(destDir)) {
57
+ return destDir;
58
+ }
59
+ const parentDir = dirname(destDir);
60
+ const stagingDir = join(parentDir, `.opendevbrowser-staging-${process.pid}-${Date.now()}`);
61
+ const backupDir = join(parentDir, `.opendevbrowser-backup-${process.pid}-${Date.now()}`);
62
+ try {
63
+ mkdirSync(stagingDir, { recursive: true });
64
+ const itemsToCopy = ["manifest.json", "popup.html", "dist", "icons"];
65
+ for (const item of itemsToCopy) {
66
+ const src = join(bundledPath, item);
67
+ const dest = join(stagingDir, item);
68
+ if (existsSync(src)) {
69
+ cpSync(src, dest, { recursive: true, force: true });
70
+ }
71
+ }
72
+ writeFileSync(join(stagingDir, VERSION_FILE), currentVersion, "utf-8");
73
+ if (!isCompleteInstall(stagingDir)) {
74
+ throw new Error("Staging directory incomplete after copy");
75
+ }
76
+ if (existsSync(destDir)) {
77
+ renameSync(destDir, backupDir);
78
+ }
79
+ renameSync(stagingDir, destDir);
80
+ if (existsSync(backupDir)) {
81
+ rmSync(backupDir, { recursive: true, force: true });
82
+ }
83
+ return destDir;
84
+ } catch (error) {
85
+ if (existsSync(backupDir) && !existsSync(destDir)) {
86
+ try {
87
+ renameSync(backupDir, destDir);
88
+ } catch (rollbackError) {
89
+ console.warn(`[opendevbrowser] Warning: Rollback failed for ${backupDir}:`, rollbackError);
90
+ }
91
+ }
92
+ if (existsSync(stagingDir)) {
93
+ try {
94
+ rmSync(stagingDir, { recursive: true, force: true });
95
+ } catch (stagingCleanupError) {
96
+ console.warn(`[opendevbrowser] Warning: Failed to clean up staging directory ${stagingDir}:`, stagingCleanupError);
97
+ }
98
+ }
99
+ if (existsSync(backupDir)) {
100
+ try {
101
+ rmSync(backupDir, { recursive: true, force: true });
102
+ } catch (backupCleanupError) {
103
+ console.warn(`[opendevbrowser] Warning: Failed to clean up backup directory ${backupDir}:`, backupCleanupError);
104
+ }
105
+ }
106
+ throw error;
107
+ }
108
+ }
109
+ function getExtensionPath() {
110
+ const destDir = getConfigDir();
111
+ if (isCompleteInstall(destDir)) {
112
+ return destDir;
113
+ }
114
+ return getBundledExtensionPath();
115
+ }
116
+
117
+ // src/browser/browser-manager.ts
118
+ import { randomUUID as randomUUID3 } from "crypto";
119
+ import { mkdir as mkdir2, rm } from "fs/promises";
120
+ import { join as join4 } from "path";
121
+ import { chromium } from "playwright-core";
122
+ import { Mutex } from "async-mutex";
123
+
124
+ // src/cache/paths.ts
125
+ import { createHash } from "crypto";
126
+ import { mkdir, stat } from "fs/promises";
127
+ import { homedir as homedir2 } from "os";
128
+ import { join as join2 } from "path";
129
+ function safeHash(value) {
130
+ return createHash("sha256").update(value).digest("hex").slice(0, 16);
131
+ }
132
+ async function ensureDir(path2) {
133
+ await mkdir(path2, { recursive: true });
134
+ }
135
+ async function resolveCachePaths(worktree, profile) {
136
+ const base = process.env.OPENCODE_CACHE_DIR ?? process.env.XDG_CACHE_HOME ?? join2(homedir2(), ".cache");
137
+ const root = join2(base, "opendevbrowser");
138
+ const projectRoot = join2(root, "projects", safeHash(worktree));
139
+ const profileDir = join2(projectRoot, "profiles", profile);
140
+ const chromeDir = join2(root, "chrome");
141
+ await ensureDir(root);
142
+ await ensureDir(projectRoot);
143
+ await ensureDir(profileDir);
144
+ await ensureDir(chromeDir);
145
+ return { root, projectRoot, profileDir, chromeDir };
146
+ }
147
+
148
+ // src/cache/chrome-locator.ts
149
+ import { access } from "fs/promises";
150
+ import { delimiter, join as join3 } from "path";
151
+ async function pathExists(path2) {
152
+ try {
153
+ await access(path2);
154
+ return true;
155
+ } catch {
156
+ return false;
157
+ }
158
+ }
159
+ function pathCandidatesByPlatform() {
160
+ const platform = process.platform;
161
+ if (platform === "darwin") {
162
+ return [
163
+ "/Applications/Google Chrome.app/Contents/MacOS/Google Chrome",
164
+ "/Applications/Google Chrome Canary.app/Contents/MacOS/Google Chrome Canary"
165
+ ];
166
+ }
167
+ if (platform === "win32") {
168
+ const programFiles = process.env.PROGRAMFILES || "C:\\Program Files";
169
+ const programFilesX86 = process.env["PROGRAMFILES(X86)"] || "C:\\Program Files (x86)";
170
+ const localAppData = process.env.LOCALAPPDATA || "";
171
+ return [
172
+ join3(programFiles, "Google", "Chrome", "Application", "chrome.exe"),
173
+ join3(programFilesX86, "Google", "Chrome", "Application", "chrome.exe"),
174
+ join3(localAppData, "Google", "Chrome", "Application", "chrome.exe")
175
+ ];
176
+ }
177
+ return [];
178
+ }
179
+ function binaryCandidatesInPath() {
180
+ return [
181
+ "google-chrome",
182
+ "google-chrome-stable",
183
+ "chromium",
184
+ "chromium-browser"
185
+ ];
186
+ }
187
+ async function findInPath(binary) {
188
+ const pathValue = process.env.PATH;
189
+ if (!pathValue) return null;
190
+ const candidates = process.platform === "win32" ? [binary, `${binary}.exe`] : [binary];
191
+ for (const dir of pathValue.split(delimiter)) {
192
+ for (const name of candidates) {
193
+ const fullPath = join3(dir, name);
194
+ if (await pathExists(fullPath)) return fullPath;
195
+ }
196
+ }
197
+ return null;
198
+ }
199
+ async function findChromeExecutable(overridePath) {
200
+ if (overridePath && await pathExists(overridePath)) {
201
+ return overridePath;
202
+ }
203
+ for (const candidate of pathCandidatesByPlatform()) {
204
+ if (await pathExists(candidate)) return candidate;
205
+ }
206
+ for (const binary of binaryCandidatesInPath()) {
207
+ const found = await findInPath(binary);
208
+ if (found) return found;
209
+ }
210
+ return null;
211
+ }
212
+
213
+ // src/cache/downloader.ts
214
+ import { Browser, detectBrowserPlatform, install, resolveBuildId } from "@puppeteer/browsers";
215
+ async function downloadChromeForTesting(cacheDir) {
216
+ const platform = detectBrowserPlatform();
217
+ if (!platform) {
218
+ throw new Error("Unsupported platform for Chrome download");
219
+ }
220
+ const buildId = await resolveBuildId(Browser.CHROME, platform, "latest");
221
+ const result = await install({
222
+ browser: Browser.CHROME,
223
+ buildId,
224
+ cacheDir,
225
+ downloadProgressCallback: () => void 0
226
+ });
227
+ return {
228
+ executablePath: result.executablePath,
229
+ buildId
230
+ };
231
+ }
232
+
233
+ // src/devtools/console-tracker.ts
234
+ var JWT_PATTERN = /\beyJ[A-Za-z0-9_-]+\.[A-Za-z0-9_-]+\.[A-Za-z0-9_-]+\b/g;
235
+ var TOKEN_LIKE_PATTERN = /\b[A-Za-z0-9_-]{16,}\b/g;
236
+ var API_KEY_PREFIX_PATTERN = /\b(sk_|pk_|api_|key_|token_|secret_|bearer_)[A-Za-z0-9_-]+\b/gi;
237
+ var SENSITIVE_KV_PATTERN = /\b(token|key|secret|password|auth|bearer|credential)[=:]\s*\S+/gi;
238
+ function shouldRedactToken(token) {
239
+ if (/^(sk_|pk_|api_|key_|token_|secret_|bearer_)/i.test(token)) {
240
+ return true;
241
+ }
242
+ const categories = [
243
+ /[a-z]/.test(token),
244
+ /[A-Z]/.test(token),
245
+ /\d/.test(token),
246
+ /[_-]/.test(token)
247
+ ].filter(Boolean).length;
248
+ return categories >= 2;
249
+ }
250
+ function redactText(text) {
251
+ let result = text.replace(SENSITIVE_KV_PATTERN, (match) => {
252
+ const sepIndex = match.search(/[=:]/);
253
+ return match.slice(0, sepIndex + 1) + "[REDACTED]";
254
+ });
255
+ result = result.replace(JWT_PATTERN, "[REDACTED]");
256
+ result = result.replace(API_KEY_PREFIX_PATTERN, "[REDACTED]");
257
+ result = result.replace(TOKEN_LIKE_PATTERN, (match) => shouldRedactToken(match) ? "[REDACTED]" : match);
258
+ return result;
259
+ }
260
+ var ConsoleTracker = class {
261
+ events = [];
262
+ maxEvents;
263
+ seq = 0;
264
+ page = null;
265
+ handler;
266
+ showFullConsole;
267
+ constructor(maxEvents = 200, options = {}) {
268
+ this.maxEvents = maxEvents;
269
+ this.showFullConsole = options.showFullConsole ?? false;
270
+ }
271
+ setOptions(options) {
272
+ if (typeof options.showFullConsole === "boolean") {
273
+ this.showFullConsole = options.showFullConsole;
274
+ }
275
+ }
276
+ attach(page) {
277
+ if (this.page === page) return;
278
+ this.detach();
279
+ this.page = page;
280
+ this.handler = (msg) => {
281
+ const rawText = msg.text();
282
+ const text = this.showFullConsole ? rawText : redactText(rawText);
283
+ this.seq += 1;
284
+ this.events.push({
285
+ seq: this.seq,
286
+ level: msg.type(),
287
+ text,
288
+ ts: Date.now()
289
+ });
290
+ if (this.events.length > this.maxEvents) {
291
+ this.events.shift();
292
+ }
293
+ };
294
+ page.on("console", this.handler);
295
+ }
296
+ detach() {
297
+ if (this.page && this.handler) {
298
+ this.page.off("console", this.handler);
299
+ }
300
+ this.page = null;
301
+ this.handler = void 0;
302
+ }
303
+ poll(sinceSeq = 0, max = 50) {
304
+ const events = this.events.filter((event) => event.seq > sinceSeq).slice(0, max);
305
+ const last = events[events.length - 1];
306
+ const nextSeq = last ? last.seq : sinceSeq;
307
+ return { events, nextSeq };
308
+ }
309
+ };
310
+
311
+ // src/devtools/network-tracker.ts
312
+ function shouldRedactPathSegment(segment) {
313
+ if (segment.length < 16) return false;
314
+ if (/^\d+$/.test(segment)) return false;
315
+ if (/^[a-f0-9-]{36}$/i.test(segment)) return false;
316
+ if (/^(sk_|pk_|api_|key_|token_|secret_|bearer_)/i.test(segment)) return true;
317
+ const categories = [/[a-z]/, /[A-Z]/, /\d/, /[_-]/].filter((r) => r.test(segment)).length;
318
+ return categories >= 3 && segment.length >= 20;
319
+ }
320
+ function redactUrl(rawUrl) {
321
+ try {
322
+ const parsed = new URL(rawUrl);
323
+ parsed.search = "";
324
+ parsed.hash = "";
325
+ const segments = parsed.pathname.split("/");
326
+ const redactedSegments = segments.map(
327
+ (segment) => shouldRedactPathSegment(segment) ? "[REDACTED]" : segment
328
+ );
329
+ parsed.pathname = redactedSegments.join("/");
330
+ return parsed.toString();
331
+ } catch {
332
+ return rawUrl.split(/[?#]/)[0] ?? rawUrl;
333
+ }
334
+ }
335
+ var NetworkTracker = class {
336
+ events = [];
337
+ maxEvents;
338
+ seq = 0;
339
+ page = null;
340
+ requestHandler;
341
+ responseHandler;
342
+ showFullUrls;
343
+ constructor(maxEvents = 300, options = {}) {
344
+ this.maxEvents = maxEvents;
345
+ this.showFullUrls = options.showFullUrls ?? false;
346
+ }
347
+ setOptions(options) {
348
+ if (typeof options.showFullUrls === "boolean") {
349
+ this.showFullUrls = options.showFullUrls;
350
+ }
351
+ }
352
+ attach(page) {
353
+ if (this.page === page) return;
354
+ this.detach();
355
+ this.page = page;
356
+ this.requestHandler = (req) => {
357
+ this.push({
358
+ method: req.method(),
359
+ url: this.showFullUrls ? req.url() : redactUrl(req.url()),
360
+ resourceType: req.resourceType(),
361
+ ts: Date.now()
362
+ });
363
+ };
364
+ this.responseHandler = (res) => {
365
+ const req = res.request();
366
+ this.push({
367
+ method: req.method(),
368
+ url: this.showFullUrls ? res.url() : redactUrl(res.url()),
369
+ status: res.status(),
370
+ resourceType: req.resourceType(),
371
+ ts: Date.now()
372
+ });
373
+ };
374
+ page.on("request", this.requestHandler);
375
+ page.on("response", this.responseHandler);
376
+ }
377
+ detach() {
378
+ if (this.page && this.requestHandler) {
379
+ this.page.off("request", this.requestHandler);
380
+ }
381
+ if (this.page && this.responseHandler) {
382
+ this.page.off("response", this.responseHandler);
383
+ }
384
+ this.page = null;
385
+ this.requestHandler = void 0;
386
+ this.responseHandler = void 0;
387
+ }
388
+ poll(sinceSeq = 0, max = 50) {
389
+ const events = this.events.filter((event) => event.seq > sinceSeq).slice(0, max);
390
+ const last = events[events.length - 1];
391
+ const nextSeq = last ? last.seq : sinceSeq;
392
+ return { events, nextSeq };
393
+ }
394
+ push(event) {
395
+ this.seq += 1;
396
+ this.events.push({
397
+ seq: this.seq,
398
+ ...event
399
+ });
400
+ if (this.events.length > this.maxEvents) {
401
+ this.events.shift();
402
+ }
403
+ }
404
+ };
405
+
406
+ // src/export/css-extract.ts
407
+ var STYLE_ALLOWLIST = /* @__PURE__ */ new Set([
408
+ "align-content",
409
+ "align-items",
410
+ "align-self",
411
+ "background",
412
+ "background-attachment",
413
+ "background-clip",
414
+ "background-color",
415
+ "background-image",
416
+ "background-origin",
417
+ "background-position",
418
+ "background-position-x",
419
+ "background-position-y",
420
+ "background-repeat",
421
+ "background-size",
422
+ "border",
423
+ "border-bottom",
424
+ "border-bottom-color",
425
+ "border-bottom-left-radius",
426
+ "border-bottom-right-radius",
427
+ "border-bottom-style",
428
+ "border-bottom-width",
429
+ "border-color",
430
+ "border-left",
431
+ "border-left-color",
432
+ "border-left-style",
433
+ "border-left-width",
434
+ "border-radius",
435
+ "border-right",
436
+ "border-right-color",
437
+ "border-right-style",
438
+ "border-right-width",
439
+ "border-style",
440
+ "border-top",
441
+ "border-top-color",
442
+ "border-top-left-radius",
443
+ "border-top-right-radius",
444
+ "border-top-style",
445
+ "border-top-width",
446
+ "border-width",
447
+ "box-shadow",
448
+ "box-sizing",
449
+ "color",
450
+ "column-gap",
451
+ "contain",
452
+ "direction",
453
+ "display",
454
+ "filter",
455
+ "flex",
456
+ "flex-direction",
457
+ "flex-flow",
458
+ "flex-wrap",
459
+ "font",
460
+ "font-family",
461
+ "font-feature-settings",
462
+ "font-kerning",
463
+ "font-size",
464
+ "font-size-adjust",
465
+ "font-stretch",
466
+ "font-style",
467
+ "font-variant",
468
+ "font-variant-caps",
469
+ "font-variant-east-asian",
470
+ "font-variant-ligatures",
471
+ "font-variant-numeric",
472
+ "font-variation-settings",
473
+ "font-weight",
474
+ "gap",
475
+ "grid",
476
+ "grid-auto-columns",
477
+ "grid-auto-flow",
478
+ "grid-auto-rows",
479
+ "grid-template-areas",
480
+ "grid-template-columns",
481
+ "grid-template-rows",
482
+ "height",
483
+ "hyphens",
484
+ "inset",
485
+ "inset-block",
486
+ "inset-inline",
487
+ "isolation",
488
+ "justify-content",
489
+ "left",
490
+ "letter-spacing",
491
+ "line-height",
492
+ "margin",
493
+ "margin-bottom",
494
+ "margin-left",
495
+ "margin-right",
496
+ "margin-top",
497
+ "max-height",
498
+ "max-width",
499
+ "min-height",
500
+ "min-width",
501
+ "opacity",
502
+ "outline",
503
+ "outline-color",
504
+ "outline-offset",
505
+ "outline-style",
506
+ "outline-width",
507
+ "overflow",
508
+ "overflow-wrap",
509
+ "overflow-x",
510
+ "overflow-y",
511
+ "padding",
512
+ "padding-bottom",
513
+ "padding-left",
514
+ "padding-right",
515
+ "padding-top",
516
+ "position",
517
+ "right",
518
+ "row-gap",
519
+ "text-align",
520
+ "text-align-last",
521
+ "text-decoration",
522
+ "text-decoration-color",
523
+ "text-decoration-line",
524
+ "text-decoration-style",
525
+ "text-decoration-thickness",
526
+ "text-indent",
527
+ "text-rendering",
528
+ "text-shadow",
529
+ "text-transform",
530
+ "top",
531
+ "transform",
532
+ "transform-origin",
533
+ "visibility",
534
+ "white-space",
535
+ "width",
536
+ "word-break",
537
+ "word-spacing",
538
+ "writing-mode",
539
+ "z-index"
540
+ ]);
541
+ var SKIP_STYLE_VALUES = /* @__PURE__ */ new Set([
542
+ "",
543
+ "initial",
544
+ "unset",
545
+ "revert",
546
+ "revert-layer"
547
+ ]);
548
+ function extractCss(capture) {
549
+ const shouldFilter = capture.inlineStyles !== false;
550
+ const lines = [];
551
+ lines.push(".opendevbrowser-root {");
552
+ for (const [key, value] of Object.entries(capture.styles)) {
553
+ const trimmed = value.trim();
554
+ if (trimmed.length === 0) continue;
555
+ if (shouldFilter) {
556
+ if (!STYLE_ALLOWLIST.has(key)) continue;
557
+ if (SKIP_STYLE_VALUES.has(trimmed)) continue;
558
+ }
559
+ lines.push(` ${key}: ${value};`);
560
+ }
561
+ lines.push("}");
562
+ return lines.join("\n");
563
+ }
564
+
565
+ // src/export/dom-capture.ts
566
+ var DEFAULT_MAX_NODES = 1e3;
567
+ async function captureDom(page, selector, options = {}) {
568
+ const shouldSanitize = options.sanitize !== false;
569
+ const maxNodes = options.maxNodes ?? DEFAULT_MAX_NODES;
570
+ const inlineStyles = options.inlineStyles !== false;
571
+ const styleAllowlist = Array.from(STYLE_ALLOWLIST);
572
+ const skipStyleValues = Array.from(SKIP_STYLE_VALUES);
573
+ return page.$eval(
574
+ selector,
575
+ (el, opts) => {
576
+ const style = window.getComputedStyle(el);
577
+ const styles = {};
578
+ for (const prop of Array.from(style)) {
579
+ styles[prop] = style.getPropertyValue(prop);
580
+ }
581
+ const warnings = [];
582
+ const root = el;
583
+ const clone = root.cloneNode(true);
584
+ const originalElements = [root, ...Array.from(root.querySelectorAll("*"))];
585
+ const cloneElements = [clone, ...Array.from(clone.querySelectorAll("*"))];
586
+ const nodeLimit = Math.max(1, opts.maxNodes);
587
+ if (originalElements.length > nodeLimit) {
588
+ const omitted = originalElements.length - nodeLimit;
589
+ warnings.push(`Export truncated at ${nodeLimit} nodes; ${omitted} nodes omitted.`);
590
+ }
591
+ const limit = Math.min(originalElements.length, nodeLimit);
592
+ if (opts.inlineStyles) {
593
+ const skipSet = new Set(opts.skipStyleValues);
594
+ for (let index = 0; index < limit; index += 1) {
595
+ const source = originalElements[index];
596
+ const target = cloneElements[index];
597
+ if (!source || !target) continue;
598
+ const computed = window.getComputedStyle(source);
599
+ const parts = [];
600
+ for (const prop of opts.styleAllowlist) {
601
+ const value = computed.getPropertyValue(prop).trim();
602
+ if (value && !skipSet.has(value)) {
603
+ parts.push(`${prop}: ${value};`);
604
+ }
605
+ }
606
+ if (parts.length > 0) {
607
+ target.setAttribute("style", parts.join(" "));
608
+ }
609
+ }
610
+ }
611
+ if (originalElements.length > nodeLimit) {
612
+ for (let index = nodeLimit; index < cloneElements.length; index += 1) {
613
+ const target = cloneElements[index];
614
+ if (target) {
615
+ target.remove();
616
+ }
617
+ }
618
+ }
619
+ const container = document.createElement("template");
620
+ container.content.appendChild(clone);
621
+ if (opts.shouldSanitize) {
622
+ const blockedTags = /* @__PURE__ */ new Set([
623
+ "script",
624
+ "iframe",
625
+ "object",
626
+ "embed",
627
+ "frame",
628
+ "frameset",
629
+ "applet",
630
+ "base",
631
+ "link",
632
+ "meta",
633
+ "noscript"
634
+ ]);
635
+ const urlAttrs = /* @__PURE__ */ new Set(["href", "src", "action", "formaction", "xlink:href", "srcset"]);
636
+ const isDangerousUrl = (value) => {
637
+ const normalized = value.trim().toLowerCase();
638
+ return normalized.startsWith("javascript:") || normalized.startsWith("data:") || normalized.startsWith("vbscript:");
639
+ };
640
+ const isDangerousSrcset = (value) => {
641
+ const entries = value.split(",");
642
+ return entries.some((entry) => {
643
+ const url = entry.trim().split(/\s+/)[0] ?? "";
644
+ return isDangerousUrl(url);
645
+ });
646
+ };
647
+ const DANGEROUS_CSS_PATTERNS = [
648
+ /url\s*\(/i,
649
+ /expression\s*\(/i,
650
+ /-moz-binding/i,
651
+ /behavior\s*:/i,
652
+ /javascript\s*:/i
653
+ ];
654
+ const sanitizeStyle = (styleValue) => {
655
+ let result = styleValue;
656
+ let wasModified = false;
657
+ for (const pattern of DANGEROUS_CSS_PATTERNS) {
658
+ if (pattern.test(result)) {
659
+ result = result.replace(new RegExp(pattern.source, "gi"), "/* blocked */");
660
+ wasModified = true;
661
+ }
662
+ }
663
+ return { sanitized: result, wasModified };
664
+ };
665
+ const sanitizeSvg = (svg) => {
666
+ const scripts = svg.querySelectorAll("script");
667
+ scripts.forEach((script) => {
668
+ script.remove();
669
+ warnings.push("Removed script element from SVG");
670
+ });
671
+ const foreignObjects = svg.querySelectorAll("foreignObject");
672
+ foreignObjects.forEach((fo) => {
673
+ fo.remove();
674
+ warnings.push("Removed foreignObject from SVG");
675
+ });
676
+ const allElements = svg.querySelectorAll("*");
677
+ allElements.forEach((el2) => {
678
+ for (const attr of Array.from(el2.attributes)) {
679
+ if (attr.name.toLowerCase().startsWith("on")) {
680
+ el2.removeAttribute(attr.name);
681
+ }
682
+ }
683
+ });
684
+ };
685
+ const sanitizeElement = (element) => {
686
+ const tag = element.tagName.toLowerCase();
687
+ if (blockedTags.has(tag)) {
688
+ element.remove();
689
+ return;
690
+ }
691
+ if (tag === "svg") {
692
+ sanitizeSvg(element);
693
+ }
694
+ for (const attr of Array.from(element.attributes)) {
695
+ const name = attr.name.toLowerCase();
696
+ if (name.startsWith("on")) {
697
+ element.removeAttribute(attr.name);
698
+ continue;
699
+ }
700
+ if (name === "style") {
701
+ const { sanitized, wasModified } = sanitizeStyle(attr.value);
702
+ if (wasModified) {
703
+ element.setAttribute("style", sanitized);
704
+ warnings.push("Sanitized dangerous CSS in style attribute");
705
+ }
706
+ continue;
707
+ }
708
+ if (urlAttrs.has(name)) {
709
+ const value = attr.value || "";
710
+ const dangerous = name === "srcset" ? isDangerousSrcset(value) : isDangerousUrl(value);
711
+ if (dangerous) {
712
+ element.removeAttribute(attr.name);
713
+ }
714
+ }
715
+ }
716
+ };
717
+ for (const element of Array.from(container.content.querySelectorAll("*"))) {
718
+ sanitizeElement(element);
719
+ }
720
+ if (container.content.firstElementChild) {
721
+ sanitizeElement(container.content.firstElementChild);
722
+ }
723
+ }
724
+ return { html: container.innerHTML, styles, warnings, inlineStyles: opts.inlineStyles };
725
+ },
726
+ { shouldSanitize, maxNodes, inlineStyles, styleAllowlist, skipStyleValues }
727
+ );
728
+ }
729
+
730
+ // src/export/react-emitter.ts
731
+ function emitReactComponent(capture, css, options = {}) {
732
+ const warnings = [...capture.warnings ?? []];
733
+ if (options.allowUnsafeExport) {
734
+ warnings.push("Unsafe export enabled: HTML sanitization disabled.");
735
+ }
736
+ const warningComment = options.allowUnsafeExport ? "// WARNING: Unsafe export enabled. HTML sanitization disabled.\n" : "";
737
+ const component = `${warningComment}import "./opendevbrowser.css";
738
+
739
+ export default function OpenDevBrowserComponent() {
740
+ return (
741
+ <div className="opendevbrowser-root" dangerouslySetInnerHTML={{ __html: ${JSON.stringify(capture.html)} }} />
742
+ );
743
+ }`;
744
+ return { component, css, warnings: warnings.length > 0 ? warnings : void 0 };
745
+ }
746
+
747
+ // src/snapshot/refs.ts
748
+ import { randomUUID } from "crypto";
749
+ var RefStore = class {
750
+ refsByTarget = /* @__PURE__ */ new Map();
751
+ snapshotByTarget = /* @__PURE__ */ new Map();
752
+ setSnapshot(targetId, entries) {
753
+ const map = /* @__PURE__ */ new Map();
754
+ for (const entry of entries) {
755
+ map.set(entry.ref, entry);
756
+ }
757
+ const snapshotId = randomUUID();
758
+ this.refsByTarget.set(targetId, map);
759
+ this.snapshotByTarget.set(targetId, snapshotId);
760
+ return { snapshotId, targetId, count: entries.length };
761
+ }
762
+ resolve(targetId, ref) {
763
+ const map = this.refsByTarget.get(targetId);
764
+ if (!map) return null;
765
+ return map.get(ref) ?? null;
766
+ }
767
+ getSnapshotId(targetId) {
768
+ return this.snapshotByTarget.get(targetId) ?? null;
769
+ }
770
+ getRefCount(targetId) {
771
+ const map = this.refsByTarget.get(targetId);
772
+ return map ? map.size : 0;
773
+ }
774
+ clearTarget(targetId) {
775
+ this.refsByTarget.delete(targetId);
776
+ this.snapshotByTarget.delete(targetId);
777
+ }
778
+ };
779
+
780
+ // src/snapshot/snapshotter.ts
781
+ var Snapshotter = class {
782
+ refStore;
783
+ constructor(refStore) {
784
+ this.refStore = refStore;
785
+ }
786
+ async snapshot(page, targetId, options) {
787
+ const startTime = Date.now();
788
+ const session = await page.context().newCDPSession(page);
789
+ let snapshotData;
790
+ try {
791
+ snapshotData = await buildSnapshot(session, options.mode, options.mainFrameOnly ?? true, options.maxNodes);
792
+ } finally {
793
+ await session.detach();
794
+ }
795
+ const snapshot = this.refStore.setSnapshot(targetId, snapshotData.entries);
796
+ const formatted = snapshotData.lines;
797
+ const startIndex = parseCursor(options.cursor);
798
+ const { content, truncated, nextCursor } = paginate(formatted, startIndex, options.maxChars);
799
+ const timingMs = Date.now() - startTime;
800
+ let url;
801
+ let title;
802
+ try {
803
+ url = page.url();
804
+ title = await page.title();
805
+ } catch (_err) {
806
+ void _err;
807
+ url = void 0;
808
+ title = void 0;
809
+ }
810
+ return {
811
+ snapshotId: snapshot.snapshotId,
812
+ url,
813
+ title,
814
+ content,
815
+ truncated,
816
+ nextCursor,
817
+ refCount: snapshot.count,
818
+ timingMs,
819
+ warnings: snapshotData.warnings
820
+ };
821
+ }
822
+ };
823
+ var DEFAULT_MAX_AX_NODES = 1e3;
824
+ var ACTIONABLE_ROLES = /* @__PURE__ */ new Set([
825
+ "button",
826
+ "link",
827
+ "textbox",
828
+ "searchbox",
829
+ "textarea",
830
+ "checkbox",
831
+ "radio",
832
+ "combobox",
833
+ "listbox",
834
+ "menuitem",
835
+ "menuitemcheckbox",
836
+ "menuitemradio",
837
+ "option",
838
+ "switch",
839
+ "tab",
840
+ "slider",
841
+ "spinbutton",
842
+ "treeitem"
843
+ ]);
844
+ var SEMANTIC_ROLES = /* @__PURE__ */ new Set([
845
+ "heading",
846
+ "article",
847
+ "main",
848
+ "navigation",
849
+ "region",
850
+ "section",
851
+ "form",
852
+ "list",
853
+ "listitem",
854
+ "paragraph",
855
+ "img",
856
+ "table",
857
+ "row",
858
+ "cell",
859
+ "columnheader",
860
+ "rowheader",
861
+ "banner",
862
+ "contentinfo",
863
+ "complementary"
864
+ ]);
865
+ var selectorFunction = function() {
866
+ if (!(this instanceof Element)) return null;
867
+ const escape = (value) => {
868
+ if (typeof CSS !== "undefined" && CSS.escape) {
869
+ return CSS.escape(value);
870
+ }
871
+ return String(value).replace(/([^\w-])/g, "\\$1");
872
+ };
873
+ const testId = this.getAttribute("data-testid");
874
+ if (testId) {
875
+ return '[data-testid="' + escape(testId) + '"]';
876
+ }
877
+ const ariaLabel = this.getAttribute("aria-label");
878
+ if (ariaLabel && ariaLabel.length < 50) {
879
+ return '[aria-label="' + escape(ariaLabel) + '"]';
880
+ }
881
+ const buildPathSelector = (start) => {
882
+ const parts = [];
883
+ let current = start;
884
+ while (current && current.nodeType === Node.ELEMENT_NODE) {
885
+ let selector = current.nodeName.toLowerCase();
886
+ if (current.id) {
887
+ selector += "#" + escape(current.id);
888
+ parts.unshift(selector);
889
+ break;
890
+ }
891
+ const parentEl = current.parentElement;
892
+ if (!parentEl) {
893
+ parts.unshift(selector);
894
+ break;
895
+ }
896
+ let index = 1;
897
+ let sibling = current;
898
+ while (sibling && sibling.previousElementSibling) {
899
+ sibling = sibling.previousElementSibling;
900
+ index += 1;
901
+ }
902
+ selector += ":nth-child(" + index + ")";
903
+ parts.unshift(selector);
904
+ current = parentEl;
905
+ }
906
+ return parts.join(" > ");
907
+ };
908
+ return buildPathSelector(this);
909
+ };
910
+ var SELECTOR_FUNCTION = selectorFunction.toString();
911
+ async function buildSnapshot(session, mode, mainFrameOnly = true, maxNodes) {
912
+ await session.send("Accessibility.enable");
913
+ await session.send("DOM.enable");
914
+ const result = await session.send("Accessibility.getFullAXTree");
915
+ const nodes = Array.isArray(result.nodes) ? result.nodes : [];
916
+ const entries = [];
917
+ const lines = [];
918
+ const warnings = [];
919
+ const maxEntries = typeof maxNodes === "number" ? maxNodes : DEFAULT_MAX_AX_NODES;
920
+ let skippedFrameCount = 0;
921
+ for (const node of nodes) {
922
+ if (entries.length >= maxEntries) break;
923
+ if (node.ignored) continue;
924
+ if (typeof node.backendDOMNodeId !== "number") continue;
925
+ if (mainFrameOnly && node.frameId) {
926
+ skippedFrameCount += 1;
927
+ continue;
928
+ }
929
+ const role = extractValue(node.role) || extractValue(node.chromeRole);
930
+ if (!role) continue;
931
+ if (!shouldInclude(role, mode)) continue;
932
+ const selector = await resolveSelector(session, node.backendDOMNodeId);
933
+ if (!selector) continue;
934
+ const ref = `r${entries.length + 1}`;
935
+ const name = redactText2(extractValue(node.name));
936
+ const value = redactText2(extractValue(node.value));
937
+ const disabled = isTruthyProperty(node.properties, "disabled");
938
+ const checked = isTruthyProperty(node.properties, "checked");
939
+ entries.push({
940
+ ref,
941
+ selector,
942
+ backendNodeId: node.backendDOMNodeId,
943
+ frameId: node.frameId,
944
+ role,
945
+ name
946
+ });
947
+ lines.push(formatNode({
948
+ ref,
949
+ role,
950
+ name,
951
+ value,
952
+ disabled,
953
+ checked
954
+ }));
955
+ }
956
+ if (mainFrameOnly && skippedFrameCount > 0) {
957
+ warnings.push(`Skipped ${skippedFrameCount} iframe nodes; snapshot limited to main frame.`);
958
+ }
959
+ return { entries, lines, warnings };
960
+ }
961
+ async function resolveSelector(session, backendNodeId) {
962
+ const resolved = await session.send("DOM.resolveNode", { backendNodeId });
963
+ const objectId = resolved.object?.objectId;
964
+ if (!objectId) return null;
965
+ const result = await session.send("Runtime.callFunctionOn", {
966
+ objectId,
967
+ functionDeclaration: SELECTOR_FUNCTION,
968
+ returnByValue: true
969
+ });
970
+ const selector = result.result?.value;
971
+ if (typeof selector !== "string" || selector.trim().length === 0) {
972
+ return null;
973
+ }
974
+ return selector;
975
+ }
976
+ function shouldInclude(role, mode) {
977
+ const normalized = role.toLowerCase();
978
+ if (ACTIONABLE_ROLES.has(normalized)) return true;
979
+ if (mode === "actionables") return false;
980
+ return SEMANTIC_ROLES.has(normalized);
981
+ }
982
+ function parseCursor(cursor) {
983
+ if (!cursor) return 0;
984
+ const value = Number(cursor);
985
+ if (!Number.isFinite(value) || value < 0) return 0;
986
+ return Math.floor(value);
987
+ }
988
+ function paginate(lines, startIndex, maxChars) {
989
+ let total = 0;
990
+ const parts = [];
991
+ let idx = startIndex;
992
+ while (idx < lines.length) {
993
+ const line = lines[idx];
994
+ if (line === void 0) {
995
+ break;
996
+ }
997
+ if (total + line.length + 1 > maxChars && parts.length > 0) {
998
+ break;
999
+ }
1000
+ parts.push(line);
1001
+ total += line.length + 1;
1002
+ idx += 1;
1003
+ }
1004
+ const truncated = idx < lines.length;
1005
+ const nextCursor = truncated ? String(idx) : void 0;
1006
+ return {
1007
+ content: parts.join("\n"),
1008
+ truncated,
1009
+ nextCursor
1010
+ };
1011
+ }
1012
+ function formatNode(node) {
1013
+ const name = redactText2(node.name || "");
1014
+ const value = redactText2(node.value || "");
1015
+ const parts = [];
1016
+ parts.push(`[${node.ref}]`);
1017
+ parts.push(node.role);
1018
+ if (node.disabled) {
1019
+ parts.push("disabled");
1020
+ }
1021
+ if (node.checked) {
1022
+ parts.push("checked");
1023
+ }
1024
+ if (name) {
1025
+ parts.push(`"${name}"`);
1026
+ }
1027
+ if (value) {
1028
+ parts.push(`value="${value}"`);
1029
+ }
1030
+ return parts.join(" ");
1031
+ }
1032
+ function redactText2(text) {
1033
+ const trimmed = (text ?? "").trim();
1034
+ if (!trimmed) return "";
1035
+ return trimmed.replace(/[A-Za-z0-9+/_-]{24,}/g, "[redacted]");
1036
+ }
1037
+ function extractValue(value) {
1038
+ if (!value || typeof value.value === "undefined" || value.value === null) return "";
1039
+ if (typeof value.value === "string") return value.value;
1040
+ if (typeof value.value === "number" || typeof value.value === "boolean") {
1041
+ return String(value.value);
1042
+ }
1043
+ return "";
1044
+ }
1045
+ function isTruthyProperty(properties, name) {
1046
+ if (!properties) return false;
1047
+ const found = properties.find((prop) => prop.name === name);
1048
+ if (!found || !found.value) return false;
1049
+ const value = found.value.value;
1050
+ if (typeof value === "boolean") return value;
1051
+ if (typeof value === "string") return value.toLowerCase() === "true";
1052
+ if (typeof value === "number") return value !== 0;
1053
+ return false;
1054
+ }
1055
+
1056
+ // src/browser/session-store.ts
1057
+ var SessionStore = class {
1058
+ sessions = /* @__PURE__ */ new Map();
1059
+ add(session) {
1060
+ this.sessions.set(session.id, session);
1061
+ }
1062
+ get(sessionId) {
1063
+ const session = this.sessions.get(sessionId);
1064
+ if (!session) {
1065
+ throw new Error(`Unknown sessionId: ${sessionId}`);
1066
+ }
1067
+ return session;
1068
+ }
1069
+ has(sessionId) {
1070
+ return this.sessions.has(sessionId);
1071
+ }
1072
+ delete(sessionId) {
1073
+ this.sessions.delete(sessionId);
1074
+ }
1075
+ list() {
1076
+ return Array.from(this.sessions.values());
1077
+ }
1078
+ };
1079
+
1080
+ // src/browser/target-manager.ts
1081
+ import { randomUUID as randomUUID2 } from "crypto";
1082
+ var TargetManager = class {
1083
+ targets = /* @__PURE__ */ new Map();
1084
+ activeTargetId = null;
1085
+ nameToTarget = /* @__PURE__ */ new Map();
1086
+ targetToName = /* @__PURE__ */ new Map();
1087
+ registerPage(page, name) {
1088
+ const targetId = randomUUID2();
1089
+ this.targets.set(targetId, page);
1090
+ if (!this.activeTargetId) {
1091
+ this.activeTargetId = targetId;
1092
+ }
1093
+ if (name) {
1094
+ this.setName(targetId, name);
1095
+ }
1096
+ return targetId;
1097
+ }
1098
+ registerExistingPages(pages) {
1099
+ for (const page of pages) {
1100
+ this.registerPage(page);
1101
+ }
1102
+ }
1103
+ setName(targetId, name) {
1104
+ const trimmed = name.trim();
1105
+ if (!trimmed) {
1106
+ throw new Error("Name must be non-empty");
1107
+ }
1108
+ if (!this.targets.has(targetId)) {
1109
+ throw new Error(`Unknown targetId: ${targetId}`);
1110
+ }
1111
+ const existing = this.nameToTarget.get(trimmed);
1112
+ if (existing && existing !== targetId) {
1113
+ throw new Error(`Name already in use: ${trimmed}`);
1114
+ }
1115
+ const previousName = this.targetToName.get(targetId);
1116
+ if (previousName && previousName !== trimmed) {
1117
+ this.nameToTarget.delete(previousName);
1118
+ }
1119
+ this.nameToTarget.set(trimmed, targetId);
1120
+ this.targetToName.set(targetId, trimmed);
1121
+ }
1122
+ getTargetIdByName(name) {
1123
+ return this.nameToTarget.get(name.trim()) ?? null;
1124
+ }
1125
+ getName(targetId) {
1126
+ return this.targetToName.get(targetId) ?? null;
1127
+ }
1128
+ listNamedTargets() {
1129
+ return Array.from(this.nameToTarget.entries()).map(([name, targetId]) => ({
1130
+ name,
1131
+ targetId
1132
+ }));
1133
+ }
1134
+ removeName(name) {
1135
+ const trimmed = name.trim();
1136
+ const targetId = this.nameToTarget.get(trimmed);
1137
+ if (targetId) {
1138
+ this.nameToTarget.delete(trimmed);
1139
+ this.targetToName.delete(targetId);
1140
+ }
1141
+ }
1142
+ setActiveTarget(targetId) {
1143
+ if (!this.targets.has(targetId)) {
1144
+ throw new Error(`Unknown targetId: ${targetId}`);
1145
+ }
1146
+ this.activeTargetId = targetId;
1147
+ }
1148
+ getActiveTargetId() {
1149
+ return this.activeTargetId;
1150
+ }
1151
+ getActivePage() {
1152
+ if (!this.activeTargetId) {
1153
+ throw new Error("No active target");
1154
+ }
1155
+ const page = this.targets.get(this.activeTargetId);
1156
+ if (!page) {
1157
+ throw new Error(`Missing active target: ${this.activeTargetId}`);
1158
+ }
1159
+ return page;
1160
+ }
1161
+ getPage(targetId) {
1162
+ const page = this.targets.get(targetId);
1163
+ if (!page) {
1164
+ throw new Error(`Unknown targetId: ${targetId}`);
1165
+ }
1166
+ return page;
1167
+ }
1168
+ async listTargets(includeUrls = false) {
1169
+ const entries = Array.from(this.targets.entries());
1170
+ return Promise.all(entries.map(async ([targetId, page]) => {
1171
+ const info = {
1172
+ targetId,
1173
+ title: void 0,
1174
+ url: void 0,
1175
+ type: "page"
1176
+ };
1177
+ try {
1178
+ info.title = await page.title();
1179
+ } catch {
1180
+ info.title = void 0;
1181
+ }
1182
+ if (includeUrls) {
1183
+ try {
1184
+ info.url = page.url();
1185
+ } catch {
1186
+ info.url = void 0;
1187
+ }
1188
+ }
1189
+ return info;
1190
+ }));
1191
+ }
1192
+ async closeTarget(targetId) {
1193
+ const page = this.getPage(targetId);
1194
+ let closeError;
1195
+ try {
1196
+ await page.close();
1197
+ } catch (error) {
1198
+ closeError = error;
1199
+ } finally {
1200
+ this.targets.delete(targetId);
1201
+ const name = this.targetToName.get(targetId);
1202
+ if (name) {
1203
+ this.nameToTarget.delete(name);
1204
+ this.targetToName.delete(targetId);
1205
+ }
1206
+ if (this.activeTargetId === targetId) {
1207
+ const remaining = Array.from(this.targets.keys());
1208
+ this.activeTargetId = remaining[0] ?? null;
1209
+ }
1210
+ }
1211
+ if (closeError) {
1212
+ throw closeError;
1213
+ }
1214
+ }
1215
+ listPageEntries() {
1216
+ return Array.from(this.targets.entries()).map(([targetId, page]) => ({
1217
+ targetId,
1218
+ page
1219
+ }));
1220
+ }
1221
+ };
1222
+
1223
+ // src/browser/browser-manager.ts
1224
+ var BrowserManager = class {
1225
+ store = new SessionStore();
1226
+ sessions = /* @__PURE__ */ new Map();
1227
+ sessionMutexes = /* @__PURE__ */ new Map();
1228
+ worktree;
1229
+ config;
1230
+ pageListeners = /* @__PURE__ */ new WeakMap();
1231
+ constructor(worktree, config) {
1232
+ this.worktree = worktree;
1233
+ this.config = config;
1234
+ }
1235
+ getMutex(sessionId) {
1236
+ let mutex = this.sessionMutexes.get(sessionId);
1237
+ if (!mutex) {
1238
+ mutex = new Mutex();
1239
+ this.sessionMutexes.set(sessionId, mutex);
1240
+ }
1241
+ return mutex;
1242
+ }
1243
+ updateConfig(config) {
1244
+ this.config = config;
1245
+ for (const managed of this.sessions.values()) {
1246
+ managed.consoleTracker.setOptions({ showFullConsole: config.devtools.showFullConsole });
1247
+ managed.networkTracker.setOptions({ showFullUrls: config.devtools.showFullUrls });
1248
+ }
1249
+ }
1250
+ async launch(options) {
1251
+ const resolvedProfile = options.profile ?? this.config.profile;
1252
+ const resolvedHeadless = options.headless ?? this.config.headless;
1253
+ const persistProfile = options.persistProfile ?? this.config.persistProfile;
1254
+ const cachePaths = await resolveCachePaths(this.worktree, resolvedProfile);
1255
+ const executable = await findChromeExecutable(options.chromePath ?? this.config.chromePath);
1256
+ const warnings = [];
1257
+ let executablePath = executable;
1258
+ if (!executablePath) {
1259
+ const download = await downloadChromeForTesting(cachePaths.chromeDir);
1260
+ warnings.push("System Chrome not found. Downloaded Chrome for Testing.");
1261
+ executablePath = download.executablePath;
1262
+ }
1263
+ const profileDir = persistProfile ? cachePaths.profileDir : join4(cachePaths.projectRoot, "temp-profiles", randomUUID3());
1264
+ await mkdir2(profileDir, { recursive: true });
1265
+ let context = null;
1266
+ try {
1267
+ context = await chromium.launchPersistentContext(profileDir, {
1268
+ headless: resolvedHeadless,
1269
+ executablePath: executablePath ?? void 0,
1270
+ args: options.flags ?? this.config.flags,
1271
+ viewport: null
1272
+ });
1273
+ const browser = context.browser();
1274
+ if (!browser) {
1275
+ throw new Error("Browser instance unavailable");
1276
+ }
1277
+ const sessionId = randomUUID3();
1278
+ const targets = new TargetManager();
1279
+ const pages = context.pages();
1280
+ if (pages.length === 0) {
1281
+ const page = await context.newPage();
1282
+ targets.registerPage(page);
1283
+ } else {
1284
+ targets.registerExistingPages(pages);
1285
+ }
1286
+ const activeTargetId = targets.getActiveTargetId();
1287
+ if (options.startUrl && activeTargetId) {
1288
+ await this.goto(sessionId, options.startUrl, "load", 3e4, { browser, context, targets });
1289
+ }
1290
+ const refStore = new RefStore();
1291
+ const snapshotter = new Snapshotter(refStore);
1292
+ const consoleTracker = new ConsoleTracker(200, { showFullConsole: this.config.devtools.showFullConsole });
1293
+ const networkTracker = new NetworkTracker(300, { showFullUrls: this.config.devtools.showFullUrls });
1294
+ const managed = {
1295
+ sessionId,
1296
+ mode: "A",
1297
+ browser,
1298
+ context,
1299
+ profileDir,
1300
+ persistProfile,
1301
+ targets,
1302
+ refStore,
1303
+ snapshotter,
1304
+ consoleTracker,
1305
+ networkTracker
1306
+ };
1307
+ this.store.add({ id: sessionId, mode: "A", browser, context });
1308
+ this.sessions.set(sessionId, managed);
1309
+ this.attachTrackers(managed);
1310
+ this.attachRefInvalidation(managed);
1311
+ const wsEndpointProvider = browser;
1312
+ const wsEndpoint = typeof wsEndpointProvider.wsEndpoint === "function" ? wsEndpointProvider.wsEndpoint() : void 0;
1313
+ return { sessionId, mode: "A", activeTargetId, warnings, wsEndpoint: wsEndpoint || void 0 };
1314
+ } catch (error) {
1315
+ const launchMessage = error instanceof Error ? error.message : "Unknown error";
1316
+ const cleanupErrors = [];
1317
+ if (context) {
1318
+ try {
1319
+ await context.close();
1320
+ } catch (closeError) {
1321
+ cleanupErrors.push(closeError);
1322
+ }
1323
+ }
1324
+ if (!persistProfile) {
1325
+ try {
1326
+ await rm(profileDir, { recursive: true, force: true, maxRetries: 5, retryDelay: 100 });
1327
+ } catch (cleanupError) {
1328
+ cleanupErrors.push(cleanupError);
1329
+ }
1330
+ }
1331
+ if (cleanupErrors.length > 0) {
1332
+ throw new AggregateError(
1333
+ [error, ...cleanupErrors],
1334
+ `Failed to launch browser context: ${launchMessage}. Cleanup failed.`
1335
+ );
1336
+ }
1337
+ throw new Error(`Failed to launch browser context: ${launchMessage}`, { cause: error });
1338
+ }
1339
+ }
1340
+ async connect(options) {
1341
+ const wsEndpoint = await this.resolveWsEndpoint(options);
1342
+ return this.connectWithEndpoint(wsEndpoint, "B");
1343
+ }
1344
+ async connectRelay(wsEndpoint) {
1345
+ this.ensureLocalEndpoint(wsEndpoint);
1346
+ return this.connectWithEndpoint(wsEndpoint, "C");
1347
+ }
1348
+ async closeAll() {
1349
+ const sessions = Array.from(this.sessions.keys());
1350
+ await Promise.allSettled(sessions.map((id) => this.disconnect(id, true)));
1351
+ }
1352
+ async disconnect(sessionId, closeBrowser = false) {
1353
+ const managed = this.getManaged(sessionId);
1354
+ const cleanupErrors = [];
1355
+ try {
1356
+ for (const entry of managed.targets.listPageEntries()) {
1357
+ const cleanup = this.pageListeners.get(entry.page);
1358
+ if (cleanup) {
1359
+ try {
1360
+ cleanup();
1361
+ } catch (error) {
1362
+ cleanupErrors.push(error);
1363
+ }
1364
+ this.pageListeners.delete(entry.page);
1365
+ }
1366
+ }
1367
+ try {
1368
+ if (closeBrowser) {
1369
+ await managed.browser.close();
1370
+ } else {
1371
+ await managed.context.close();
1372
+ }
1373
+ } catch (error) {
1374
+ cleanupErrors.push(error);
1375
+ }
1376
+ try {
1377
+ managed.consoleTracker.detach();
1378
+ } catch (error) {
1379
+ cleanupErrors.push(error);
1380
+ }
1381
+ try {
1382
+ managed.networkTracker.detach();
1383
+ } catch (error) {
1384
+ cleanupErrors.push(error);
1385
+ }
1386
+ if (!managed.persistProfile && managed.profileDir) {
1387
+ try {
1388
+ await rm(managed.profileDir, { recursive: true, force: true, maxRetries: 5, retryDelay: 100 });
1389
+ } catch (error) {
1390
+ cleanupErrors.push(error);
1391
+ }
1392
+ }
1393
+ } finally {
1394
+ this.sessions.delete(sessionId);
1395
+ this.sessionMutexes.delete(sessionId);
1396
+ this.store.delete(sessionId);
1397
+ }
1398
+ if (cleanupErrors.length === 1) {
1399
+ throw cleanupErrors[0];
1400
+ }
1401
+ if (cleanupErrors.length > 1) {
1402
+ throw new AggregateError(cleanupErrors, "Failed to disconnect browser session.");
1403
+ }
1404
+ }
1405
+ async status(sessionId) {
1406
+ const managed = this.getManaged(sessionId);
1407
+ const activeTargetId = managed.targets.getActiveTargetId();
1408
+ const page = activeTargetId ? managed.targets.getPage(activeTargetId) : null;
1409
+ const title = await this.safePageTitle(page, "BrowserManager.status");
1410
+ const url = this.safePageUrl(page, "BrowserManager.status");
1411
+ return {
1412
+ mode: managed.mode,
1413
+ activeTargetId,
1414
+ url,
1415
+ title
1416
+ };
1417
+ }
1418
+ async listTargets(sessionId, includeUrls = false) {
1419
+ const managed = this.getManaged(sessionId);
1420
+ const targets = await managed.targets.listTargets(includeUrls);
1421
+ return {
1422
+ activeTargetId: managed.targets.getActiveTargetId(),
1423
+ targets
1424
+ };
1425
+ }
1426
+ async page(sessionId, name, url) {
1427
+ const managed = this.getManaged(sessionId);
1428
+ const existingTargetId = managed.targets.getTargetIdByName(name);
1429
+ let targetId = existingTargetId;
1430
+ let created = false;
1431
+ if (targetId) {
1432
+ managed.targets.setActiveTarget(targetId);
1433
+ } else {
1434
+ const page2 = await managed.context.newPage();
1435
+ targetId = managed.targets.registerPage(page2, name);
1436
+ managed.targets.setActiveTarget(targetId);
1437
+ this.attachRefInvalidationForPage(managed, targetId, page2);
1438
+ created = true;
1439
+ }
1440
+ this.attachTrackers(managed);
1441
+ if (url) {
1442
+ await this.goto(sessionId, url, "load", 3e4);
1443
+ }
1444
+ const page = managed.targets.getPage(targetId);
1445
+ const title = await this.safePageTitle(page, "BrowserManager.page");
1446
+ const finalUrl = this.safePageUrl(page, "BrowserManager.page");
1447
+ return { targetId, created, url: finalUrl, title };
1448
+ }
1449
+ async listPages(sessionId) {
1450
+ const managed = this.getManaged(sessionId);
1451
+ const named = managed.targets.listNamedTargets();
1452
+ const pages = [];
1453
+ for (const entry of named) {
1454
+ const page = managed.targets.getPage(entry.targetId);
1455
+ const title = await this.safePageTitle(page, "BrowserManager.listPages");
1456
+ const url = this.safePageUrl(page, "BrowserManager.listPages");
1457
+ pages.push({ name: entry.name, targetId: entry.targetId, url, title });
1458
+ }
1459
+ return { pages };
1460
+ }
1461
+ async closePage(sessionId, name) {
1462
+ const managed = this.getManaged(sessionId);
1463
+ const targetId = managed.targets.getTargetIdByName(name);
1464
+ if (!targetId) {
1465
+ throw new Error(`Unknown page name: ${name}`);
1466
+ }
1467
+ await managed.targets.closeTarget(targetId);
1468
+ managed.refStore.clearTarget(targetId);
1469
+ this.attachTrackers(managed);
1470
+ }
1471
+ async useTarget(sessionId, targetId) {
1472
+ const managed = this.getManaged(sessionId);
1473
+ managed.targets.setActiveTarget(targetId);
1474
+ this.attachTrackers(managed);
1475
+ const page = managed.targets.getPage(targetId);
1476
+ const title = await this.safePageTitle(page, "BrowserManager.useTarget");
1477
+ return {
1478
+ activeTargetId: targetId,
1479
+ url: this.safePageUrl(page, "BrowserManager.useTarget"),
1480
+ title
1481
+ };
1482
+ }
1483
+ async newTarget(sessionId, url) {
1484
+ const managed = this.getManaged(sessionId);
1485
+ const page = await managed.context.newPage();
1486
+ const targetId = managed.targets.registerPage(page);
1487
+ managed.targets.setActiveTarget(targetId);
1488
+ this.attachRefInvalidationForPage(managed, targetId, page);
1489
+ if (url) {
1490
+ await page.goto(url, { waitUntil: "load" });
1491
+ }
1492
+ this.attachTrackers(managed);
1493
+ return { targetId };
1494
+ }
1495
+ async closeTarget(sessionId, targetId) {
1496
+ const managed = this.getManaged(sessionId);
1497
+ await managed.targets.closeTarget(targetId);
1498
+ managed.refStore.clearTarget(targetId);
1499
+ this.attachTrackers(managed);
1500
+ }
1501
+ async goto(sessionId, url, waitUntil = "load", timeoutMs = 3e4, sessionOverride) {
1502
+ const startTime = Date.now();
1503
+ const managed = sessionOverride ? this.buildOverrideSession(sessionOverride) : this.getManaged(sessionId);
1504
+ const page = managed.targets.getActivePage();
1505
+ const response = await page.goto(url, { waitUntil, timeout: timeoutMs });
1506
+ return {
1507
+ finalUrl: page.url(),
1508
+ status: response?.status(),
1509
+ timingMs: Date.now() - startTime
1510
+ };
1511
+ }
1512
+ async waitForLoad(sessionId, until, timeoutMs = 3e4) {
1513
+ const startTime = Date.now();
1514
+ const managed = this.getManaged(sessionId);
1515
+ const page = managed.targets.getActivePage();
1516
+ await page.waitForLoadState(until, { timeout: timeoutMs });
1517
+ return { timingMs: Date.now() - startTime };
1518
+ }
1519
+ async waitForRef(sessionId, ref, state = "attached", timeoutMs = 3e4) {
1520
+ const startTime = Date.now();
1521
+ const managed = this.getManaged(sessionId);
1522
+ const selector = this.resolveSelector(managed, ref);
1523
+ const page = managed.targets.getActivePage();
1524
+ await page.locator(selector).waitFor({ state, timeout: timeoutMs });
1525
+ return { timingMs: Date.now() - startTime };
1526
+ }
1527
+ async snapshot(sessionId, mode, maxChars, cursor) {
1528
+ const mutex = this.getMutex(sessionId);
1529
+ return mutex.runExclusive(async () => {
1530
+ const managed = this.getManaged(sessionId);
1531
+ const targetId = managed.targets.getActiveTargetId();
1532
+ if (!targetId) {
1533
+ throw new Error("No active target for snapshot");
1534
+ }
1535
+ const page = managed.targets.getActivePage();
1536
+ return managed.snapshotter.snapshot(page, targetId, {
1537
+ mode,
1538
+ maxChars,
1539
+ cursor,
1540
+ maxNodes: this.config.snapshot.maxNodes
1541
+ });
1542
+ });
1543
+ }
1544
+ async click(sessionId, ref) {
1545
+ const mutex = this.getMutex(sessionId);
1546
+ return mutex.runExclusive(async () => {
1547
+ const startTime = Date.now();
1548
+ const managed = this.getManaged(sessionId);
1549
+ const selector = this.resolveSelector(managed, ref);
1550
+ const page = managed.targets.getActivePage();
1551
+ const previousUrl = page.url();
1552
+ await page.locator(selector).click();
1553
+ const navigated = page.url() !== previousUrl;
1554
+ return { timingMs: Date.now() - startTime, navigated };
1555
+ });
1556
+ }
1557
+ async type(sessionId, ref, text, clear = false, submit = false) {
1558
+ const mutex = this.getMutex(sessionId);
1559
+ return mutex.runExclusive(async () => {
1560
+ const startTime = Date.now();
1561
+ const managed = this.getManaged(sessionId);
1562
+ const selector = this.resolveSelector(managed, ref);
1563
+ const locator = managed.targets.getActivePage().locator(selector);
1564
+ if (clear) {
1565
+ await locator.fill("");
1566
+ }
1567
+ await locator.fill(text);
1568
+ if (submit) {
1569
+ await locator.press("Enter");
1570
+ }
1571
+ return { timingMs: Date.now() - startTime };
1572
+ });
1573
+ }
1574
+ async select(sessionId, ref, values) {
1575
+ const managed = this.getManaged(sessionId);
1576
+ const selector = this.resolveSelector(managed, ref);
1577
+ await managed.targets.getActivePage().locator(selector).selectOption(values);
1578
+ }
1579
+ async scroll(sessionId, dy, ref) {
1580
+ const managed = this.getManaged(sessionId);
1581
+ const page = managed.targets.getActivePage();
1582
+ if (ref) {
1583
+ const selector = this.resolveSelector(managed, ref);
1584
+ await page.locator(selector).evaluate((el, delta) => {
1585
+ el.scrollBy(0, delta);
1586
+ }, dy);
1587
+ } else {
1588
+ await page.mouse.wheel(0, dy);
1589
+ }
1590
+ }
1591
+ async domGetHtml(sessionId, ref, maxChars = 8e3) {
1592
+ const managed = this.getManaged(sessionId);
1593
+ const selector = this.resolveSelector(managed, ref);
1594
+ const html = await managed.targets.getActivePage().$eval(selector, (el) => el.outerHTML);
1595
+ return truncateHtml(html, maxChars);
1596
+ }
1597
+ async domGetText(sessionId, ref, maxChars = 8e3) {
1598
+ const managed = this.getManaged(sessionId);
1599
+ const selector = this.resolveSelector(managed, ref);
1600
+ const text = await managed.targets.getActivePage().$eval(selector, (el) => el.innerText || el.textContent || "");
1601
+ return truncateText(text, maxChars);
1602
+ }
1603
+ async clonePage(sessionId) {
1604
+ const managed = this.getManaged(sessionId);
1605
+ const page = managed.targets.getActivePage();
1606
+ const allowUnsafeExport = this.config.security.allowUnsafeExport;
1607
+ const exportConfig = this.config.export;
1608
+ const capture = await captureDom(page, "body", {
1609
+ sanitize: !allowUnsafeExport,
1610
+ maxNodes: exportConfig.maxNodes,
1611
+ inlineStyles: exportConfig.inlineStyles
1612
+ });
1613
+ const css = extractCss(capture);
1614
+ return emitReactComponent(capture, css, { allowUnsafeExport });
1615
+ }
1616
+ async cloneComponent(sessionId, ref) {
1617
+ const managed = this.getManaged(sessionId);
1618
+ const selector = this.resolveSelector(managed, ref);
1619
+ const allowUnsafeExport = this.config.security.allowUnsafeExport;
1620
+ const exportConfig = this.config.export;
1621
+ const capture = await captureDom(managed.targets.getActivePage(), selector, {
1622
+ sanitize: !allowUnsafeExport,
1623
+ maxNodes: exportConfig.maxNodes,
1624
+ inlineStyles: exportConfig.inlineStyles
1625
+ });
1626
+ const css = extractCss(capture);
1627
+ return emitReactComponent(capture, css, { allowUnsafeExport });
1628
+ }
1629
+ async perfMetrics(sessionId) {
1630
+ const managed = this.getManaged(sessionId);
1631
+ const page = managed.targets.getActivePage();
1632
+ const session = await managed.context.newCDPSession(page);
1633
+ const result = await session.send("Performance.getMetrics");
1634
+ await session.detach();
1635
+ const metrics = Array.isArray(result.metrics) ? result.metrics : [];
1636
+ return { metrics };
1637
+ }
1638
+ async screenshot(sessionId, path2) {
1639
+ const managed = this.getManaged(sessionId);
1640
+ const page = managed.targets.getActivePage();
1641
+ if (path2) {
1642
+ await page.screenshot({ path: path2, type: "png" });
1643
+ return { path: path2 };
1644
+ }
1645
+ const buffer = await page.screenshot({ type: "png" });
1646
+ return { base64: buffer.toString("base64") };
1647
+ }
1648
+ consolePoll(sessionId, sinceSeq, max = 50) {
1649
+ const managed = this.getManaged(sessionId);
1650
+ return managed.consoleTracker.poll(sinceSeq, max);
1651
+ }
1652
+ networkPoll(sessionId, sinceSeq, max = 50) {
1653
+ const managed = this.getManaged(sessionId);
1654
+ return managed.networkTracker.poll(sinceSeq, max);
1655
+ }
1656
+ buildOverrideSession(input) {
1657
+ const refStore = new RefStore();
1658
+ return {
1659
+ sessionId: "override",
1660
+ mode: "A",
1661
+ browser: input.browser,
1662
+ context: input.context,
1663
+ profileDir: "",
1664
+ persistProfile: true,
1665
+ targets: input.targets,
1666
+ refStore,
1667
+ snapshotter: new Snapshotter(refStore),
1668
+ consoleTracker: new ConsoleTracker(200, { showFullConsole: this.config.devtools.showFullConsole }),
1669
+ networkTracker: new NetworkTracker(300, { showFullUrls: this.config.devtools.showFullUrls })
1670
+ };
1671
+ }
1672
+ getManaged(sessionId) {
1673
+ const managed = this.sessions.get(sessionId);
1674
+ if (!managed) {
1675
+ throw new Error(`Unknown sessionId: ${sessionId}`);
1676
+ }
1677
+ return managed;
1678
+ }
1679
+ resolveSelector(managed, ref) {
1680
+ const targetId = managed.targets.getActiveTargetId();
1681
+ if (!targetId) {
1682
+ throw new Error("No active target for ref resolution");
1683
+ }
1684
+ const entry = managed.refStore.resolve(targetId, ref);
1685
+ if (!entry) {
1686
+ throw new Error(`Unknown ref: ${ref}. Take a new snapshot first.`);
1687
+ }
1688
+ return entry.selector;
1689
+ }
1690
+ async safePageTitle(page, context) {
1691
+ if (!page) return void 0;
1692
+ try {
1693
+ return await page.title();
1694
+ } catch {
1695
+ console.warn(`${context}: failed to read page title`);
1696
+ return void 0;
1697
+ }
1698
+ }
1699
+ safePageUrl(page, context) {
1700
+ if (!page) return void 0;
1701
+ try {
1702
+ return page.url();
1703
+ } catch {
1704
+ console.warn(`${context}: failed to read page url`);
1705
+ return void 0;
1706
+ }
1707
+ }
1708
+ attachTrackers(managed) {
1709
+ const activeTargetId = managed.targets.getActiveTargetId();
1710
+ if (!activeTargetId) return;
1711
+ const page = managed.targets.getActivePage();
1712
+ managed.consoleTracker.attach(page);
1713
+ managed.networkTracker.attach(page);
1714
+ }
1715
+ attachRefInvalidation(managed) {
1716
+ const entries = managed.targets.listPageEntries();
1717
+ for (const entry of entries) {
1718
+ this.attachRefInvalidationForPage(managed, entry.targetId, entry.page);
1719
+ }
1720
+ }
1721
+ attachRefInvalidationForPage(managed, targetId, page) {
1722
+ if (this.pageListeners.has(page)) return;
1723
+ const onNavigate = (frame) => {
1724
+ if (frame.parentFrame() === null) {
1725
+ managed.refStore.clearTarget(targetId);
1726
+ }
1727
+ };
1728
+ const onClose = () => {
1729
+ managed.refStore.clearTarget(targetId);
1730
+ };
1731
+ page.on("framenavigated", onNavigate);
1732
+ page.on("close", onClose);
1733
+ this.pageListeners.set(page, () => {
1734
+ page.off("framenavigated", onNavigate);
1735
+ page.off("close", onClose);
1736
+ });
1737
+ }
1738
+ async resolveWsEndpoint(options) {
1739
+ if (options.wsEndpoint) {
1740
+ this.ensureLocalEndpoint(options.wsEndpoint);
1741
+ return options.wsEndpoint;
1742
+ }
1743
+ const host = options.host ?? "127.0.0.1";
1744
+ const port = options.port ?? 9222;
1745
+ const url = `http://${host}:${port}/json/version`;
1746
+ this.ensureLocalEndpoint(url);
1747
+ const response = await fetch(url);
1748
+ if (!response.ok) {
1749
+ throw new Error(`Failed to fetch CDP endpoint from ${url}`);
1750
+ }
1751
+ const data = await response.json();
1752
+ if (!data.webSocketDebuggerUrl) {
1753
+ throw new Error("webSocketDebuggerUrl missing from /json/version response");
1754
+ }
1755
+ this.ensureLocalEndpoint(data.webSocketDebuggerUrl);
1756
+ return data.webSocketDebuggerUrl;
1757
+ }
1758
+ ensureLocalEndpoint(endpoint) {
1759
+ if (this.config.security.allowNonLocalCdp) return;
1760
+ const ALLOWED_PROTOCOLS = /* @__PURE__ */ new Set(["ws:", "wss:", "http:", "https:"]);
1761
+ const LOCAL_HOSTNAMES = /* @__PURE__ */ new Set(["localhost", "127.0.0.1", "::1", "[::1]"]);
1762
+ let parsed;
1763
+ try {
1764
+ parsed = new URL(endpoint);
1765
+ } catch {
1766
+ throw new Error("Invalid CDP endpoint URL.");
1767
+ }
1768
+ if (!ALLOWED_PROTOCOLS.has(parsed.protocol)) {
1769
+ throw new Error(`Disallowed protocol "${parsed.protocol}" for CDP endpoint. Allowed: ws, wss, http, https.`);
1770
+ }
1771
+ const hostname = parsed.hostname.toLowerCase();
1772
+ if (!LOCAL_HOSTNAMES.has(hostname) && !hostname.toLowerCase().startsWith("::ffff:127.")) {
1773
+ throw new Error("Non-local CDP endpoints are disabled by default.");
1774
+ }
1775
+ }
1776
+ async connectWithEndpoint(wsEndpoint, mode) {
1777
+ const browser = await chromium.connectOverCDP(wsEndpoint);
1778
+ const contexts = browser.contexts();
1779
+ const context = contexts[0] ?? await browser.newContext();
1780
+ const sessionId = randomUUID3();
1781
+ const targets = new TargetManager();
1782
+ const pages = context.pages();
1783
+ if (pages.length === 0) {
1784
+ const page = await context.newPage();
1785
+ targets.registerPage(page);
1786
+ } else {
1787
+ targets.registerExistingPages(pages);
1788
+ }
1789
+ const refStore = new RefStore();
1790
+ const snapshotter = new Snapshotter(refStore);
1791
+ const consoleTracker = new ConsoleTracker(200, { showFullConsole: this.config.devtools.showFullConsole });
1792
+ const networkTracker = new NetworkTracker(300, { showFullUrls: this.config.devtools.showFullUrls });
1793
+ const managed = {
1794
+ sessionId,
1795
+ mode,
1796
+ browser,
1797
+ context,
1798
+ profileDir: "",
1799
+ persistProfile: true,
1800
+ targets,
1801
+ refStore,
1802
+ snapshotter,
1803
+ consoleTracker,
1804
+ networkTracker
1805
+ };
1806
+ this.store.add({ id: sessionId, mode, browser, context });
1807
+ this.sessions.set(sessionId, managed);
1808
+ this.attachTrackers(managed);
1809
+ this.attachRefInvalidation(managed);
1810
+ return { sessionId, mode, activeTargetId: targets.getActiveTargetId(), warnings: [], wsEndpoint };
1811
+ }
1812
+ };
1813
+ function truncateHtml(value, maxChars) {
1814
+ if (value.length <= maxChars) {
1815
+ return { outerHTML: value, truncated: false };
1816
+ }
1817
+ return { outerHTML: value.slice(0, maxChars), truncated: true };
1818
+ }
1819
+ function truncateText(value, maxChars) {
1820
+ if (value.length <= maxChars) {
1821
+ return { text: value, truncated: false };
1822
+ }
1823
+ return { text: value.slice(0, maxChars), truncated: true };
1824
+ }
1825
+
1826
+ // src/browser/script-runner.ts
1827
+ var ScriptRunner = class {
1828
+ manager;
1829
+ constructor(manager) {
1830
+ this.manager = manager;
1831
+ }
1832
+ async run(sessionId, steps, stopOnError = true) {
1833
+ const startTime = Date.now();
1834
+ const results = [];
1835
+ for (let i = 0; i < steps.length; i += 1) {
1836
+ const step = steps[i];
1837
+ if (!step) {
1838
+ continue;
1839
+ }
1840
+ try {
1841
+ const data = await this.executeStep(sessionId, step);
1842
+ results.push({ i, ok: true, data });
1843
+ } catch (error) {
1844
+ results.push({
1845
+ i,
1846
+ ok: false,
1847
+ error: { message: error instanceof Error ? error.message : "Unknown error" }
1848
+ });
1849
+ if (stopOnError) {
1850
+ break;
1851
+ }
1852
+ }
1853
+ }
1854
+ return { results, timingMs: Date.now() - startTime };
1855
+ }
1856
+ async executeStep(sessionId, step) {
1857
+ const args = step.args ?? {};
1858
+ switch (step.action) {
1859
+ case "goto":
1860
+ return this.manager.goto(
1861
+ sessionId,
1862
+ requireString(args.url, "url"),
1863
+ requireWaitUntil(args.waitUntil),
1864
+ requireNumber(args.timeoutMs, 3e4)
1865
+ );
1866
+ case "wait":
1867
+ if (typeof args.ref === "string") {
1868
+ const ref = args.ref;
1869
+ const state = requireState(args.state);
1870
+ const timeoutMs = requireNumber(args.timeoutMs, 3e4);
1871
+ return withRetry("wait", () => this.manager.waitForRef(
1872
+ sessionId,
1873
+ ref,
1874
+ state,
1875
+ timeoutMs
1876
+ ));
1877
+ }
1878
+ return withRetry("wait", () => this.manager.waitForLoad(
1879
+ sessionId,
1880
+ requireWaitUntil(args.until),
1881
+ requireNumber(args.timeoutMs, 3e4)
1882
+ ));
1883
+ case "snapshot":
1884
+ return this.manager.snapshot(
1885
+ sessionId,
1886
+ requireSnapshotMode(args.format ?? args.mode),
1887
+ requireNumber(args.maxChars, 16e3),
1888
+ typeof args.cursor === "string" ? args.cursor : void 0
1889
+ );
1890
+ case "click":
1891
+ return withRetry("click", () => this.manager.click(sessionId, requireString(args.ref, "ref")));
1892
+ case "type":
1893
+ return withRetry("type", () => this.manager.type(
1894
+ sessionId,
1895
+ requireString(args.ref, "ref"),
1896
+ requireString(args.text, "text"),
1897
+ Boolean(args.clear),
1898
+ Boolean(args.submit)
1899
+ ));
1900
+ case "select":
1901
+ return withRetry("select", () => this.manager.select(
1902
+ sessionId,
1903
+ requireString(args.ref, "ref"),
1904
+ requireStringArray(args.values, "values")
1905
+ ));
1906
+ case "scroll":
1907
+ return withRetry("scroll", () => this.manager.scroll(
1908
+ sessionId,
1909
+ requireNumber(args.dy, 0),
1910
+ typeof args.ref === "string" ? args.ref : void 0
1911
+ ));
1912
+ case "dom_get_html":
1913
+ return this.manager.domGetHtml(sessionId, requireString(args.ref, "ref"), requireNumber(args.maxChars, 8e3));
1914
+ case "dom_get_text":
1915
+ return this.manager.domGetText(sessionId, requireString(args.ref, "ref"), requireNumber(args.maxChars, 8e3));
1916
+ default:
1917
+ throw new Error(`Unknown action: ${step.action}`);
1918
+ }
1919
+ }
1920
+ };
1921
+ function requireString(value, label) {
1922
+ if (typeof value !== "string" || !value.trim()) {
1923
+ throw new Error(`Missing ${label}`);
1924
+ }
1925
+ return value;
1926
+ }
1927
+ function requireStringArray(value, label) {
1928
+ if (!Array.isArray(value) || value.some((item) => typeof item !== "string")) {
1929
+ throw new Error(`Invalid ${label}`);
1930
+ }
1931
+ return value;
1932
+ }
1933
+ function requireNumber(value, fallback) {
1934
+ if (typeof value === "number" && Number.isFinite(value)) {
1935
+ return value;
1936
+ }
1937
+ return fallback;
1938
+ }
1939
+ function requireWaitUntil(value) {
1940
+ if (value === "domcontentloaded" || value === "load" || value === "networkidle") {
1941
+ return value;
1942
+ }
1943
+ return "load";
1944
+ }
1945
+ function requireSnapshotMode(value) {
1946
+ if (value === "actionables") return "actionables";
1947
+ return "outline";
1948
+ }
1949
+ function requireState(value) {
1950
+ if (value === "visible" || value === "hidden") return value;
1951
+ return "attached";
1952
+ }
1953
+ var RETRY_ACTIONS = /* @__PURE__ */ new Set(["click", "type", "select", "scroll", "wait"]);
1954
+ var RETRY_MAX_ATTEMPTS = 2;
1955
+ var RETRY_BASE_DELAY_MS = 150;
1956
+ var RETRY_MAX_DELAY_MS = 1e3;
1957
+ async function withRetry(action, fn) {
1958
+ if (!RETRY_ACTIONS.has(action)) {
1959
+ return fn();
1960
+ }
1961
+ let attempt = 0;
1962
+ let delay = RETRY_BASE_DELAY_MS;
1963
+ while (true) {
1964
+ try {
1965
+ return await fn();
1966
+ } catch (error) {
1967
+ attempt += 1;
1968
+ if (attempt >= RETRY_MAX_ATTEMPTS || !shouldRetry(error)) {
1969
+ throw error;
1970
+ }
1971
+ await sleep(delay);
1972
+ delay = Math.min(delay * 2, RETRY_MAX_DELAY_MS);
1973
+ }
1974
+ }
1975
+ }
1976
+ function shouldRetry(error) {
1977
+ const message = error instanceof Error ? error.message : "";
1978
+ if (!message) return true;
1979
+ return !/missing|invalid|unknown ref|no active target/i.test(message);
1980
+ }
1981
+ function sleep(ms) {
1982
+ return new Promise((resolve) => setTimeout(resolve, ms));
1983
+ }
1984
+
1985
+ // src/config.ts
1986
+ import { z } from "zod";
1987
+ import * as fs from "fs";
1988
+ import * as path from "path";
1989
+ import * as os from "os";
1990
+ import { parse as parseJsonc } from "jsonc-parser";
1991
+
1992
+ // src/utils/crypto.ts
1993
+ import { randomBytes } from "crypto";
1994
+ function generateSecureToken() {
1995
+ return randomBytes(32).toString("hex");
1996
+ }
1997
+
1998
+ // src/config.ts
1999
+ function isExecutable(filePath) {
2000
+ try {
2001
+ fs.accessSync(filePath, fs.constants.X_OK);
2002
+ return true;
2003
+ } catch {
2004
+ return false;
2005
+ }
2006
+ }
2007
+ var DEFAULT_RELAY_PORT = 8787;
2008
+ function buildDefaultConfigJsonc(token) {
2009
+ return `{
2010
+ // Set relayToken to false to disable extension pairing.
2011
+ "relayPort": ${DEFAULT_RELAY_PORT},
2012
+ "relayToken": "${token}"
2013
+ }
2014
+ `;
2015
+ }
2016
+ var snapshotSchema = z.object({
2017
+ maxChars: z.number().int().min(500).max(2e5).default(16e3),
2018
+ maxNodes: z.number().int().min(50).max(5e3).default(1e3)
2019
+ });
2020
+ var securitySchema = z.object({
2021
+ allowRawCDP: z.boolean().default(false),
2022
+ allowNonLocalCdp: z.boolean().default(false),
2023
+ allowUnsafeExport: z.boolean().default(false)
2024
+ });
2025
+ var devtoolsSchema = z.object({
2026
+ showFullUrls: z.boolean().default(false),
2027
+ showFullConsole: z.boolean().default(false)
2028
+ });
2029
+ var exportSchema = z.object({
2030
+ maxNodes: z.number().int().min(1).max(5e3).default(1e3),
2031
+ inlineStyles: z.boolean().default(true)
2032
+ });
2033
+ var skillsNudgeSchema = z.object({
2034
+ enabled: z.boolean().default(true),
2035
+ keywords: z.array(z.string()).default([
2036
+ "login",
2037
+ "sign in",
2038
+ "sign-in",
2039
+ "auth",
2040
+ "authentication",
2041
+ "mfa",
2042
+ "form",
2043
+ "submit",
2044
+ "validation",
2045
+ "extract",
2046
+ "scrape",
2047
+ "scraping",
2048
+ "table",
2049
+ "pagination",
2050
+ "crawl"
2051
+ ]),
2052
+ maxAgeMs: z.number().int().min(1e3).max(6e5).default(6e4)
2053
+ });
2054
+ var skillsSchema = z.object({
2055
+ nudge: skillsNudgeSchema.default({})
2056
+ }).default({});
2057
+ var continuityNudgeSchema = z.object({
2058
+ enabled: z.boolean().default(true),
2059
+ keywords: z.array(z.string()).default([
2060
+ "plan",
2061
+ "multi-step",
2062
+ "multi step",
2063
+ "long-running",
2064
+ "long running",
2065
+ "refactor",
2066
+ "migration",
2067
+ "rollout",
2068
+ "release",
2069
+ "upgrade",
2070
+ "investigate",
2071
+ "follow-up",
2072
+ "continue"
2073
+ ]),
2074
+ maxAgeMs: z.number().int().min(1e3).max(6e5).default(6e4)
2075
+ });
2076
+ var continuitySchema = z.object({
2077
+ enabled: z.boolean().default(true),
2078
+ filePath: z.string().min(1).default("opendevbrowser_continuity.md"),
2079
+ nudge: continuityNudgeSchema.default({})
2080
+ }).default({});
2081
+ var configSchema = z.object({
2082
+ headless: z.boolean().default(false),
2083
+ profile: z.string().min(1).default("default"),
2084
+ snapshot: snapshotSchema.default({}),
2085
+ security: securitySchema.default({}),
2086
+ devtools: devtoolsSchema.default({}),
2087
+ export: exportSchema.default({}),
2088
+ skills: skillsSchema.default({}),
2089
+ continuity: continuitySchema.default({}),
2090
+ relayPort: z.number().int().min(0).max(65535).default(DEFAULT_RELAY_PORT),
2091
+ relayToken: z.union([z.string(), z.literal(false)]).optional(),
2092
+ chromePath: z.string().min(1).optional().refine(
2093
+ (val) => val === void 0 || isExecutable(val),
2094
+ { message: "chromePath must point to an executable file" }
2095
+ ),
2096
+ flags: z.array(z.string()).default([]),
2097
+ checkForUpdates: z.boolean().default(false),
2098
+ persistProfile: z.boolean().default(true),
2099
+ skillPaths: z.array(z.string()).default([])
2100
+ });
2101
+ var CONFIG_FILE_NAME = "opendevbrowser.jsonc";
2102
+ function getGlobalConfigPath() {
2103
+ const configDir = process.env.OPENCODE_CONFIG_DIR || path.join(os.homedir(), ".config", "opencode");
2104
+ return path.join(configDir, CONFIG_FILE_NAME);
2105
+ }
2106
+ function ensureConfigFile(filePath) {
2107
+ const token = generateSecureToken();
2108
+ if (fs.existsSync(filePath)) {
2109
+ return token;
2110
+ }
2111
+ try {
2112
+ fs.mkdirSync(path.dirname(filePath), { recursive: true, mode: 448 });
2113
+ fs.writeFileSync(filePath, buildDefaultConfigJsonc(token), { encoding: "utf-8", mode: 384 });
2114
+ } catch (error) {
2115
+ console.warn(`[opendevbrowser] Warning: Could not create config file at ${filePath}:`, error);
2116
+ }
2117
+ return token;
2118
+ }
2119
+ function loadConfigFile(filePath) {
2120
+ if (!fs.existsSync(filePath)) {
2121
+ const token = ensureConfigFile(filePath);
2122
+ return { raw: {}, generatedToken: token };
2123
+ }
2124
+ const content = fs.readFileSync(filePath, "utf-8");
2125
+ const errors = [];
2126
+ const parsed = parseJsonc(content, errors, { allowTrailingComma: true });
2127
+ if (errors.length > 0) {
2128
+ const firstError = errors[0];
2129
+ throw new Error(`Invalid JSONC in opendevbrowser config at ${filePath}: parse error at offset ${firstError.offset}`);
2130
+ }
2131
+ return { raw: parsed ?? {}, generatedToken: null };
2132
+ }
2133
+ function loadGlobalConfig() {
2134
+ const configPath = getGlobalConfigPath();
2135
+ const { raw, generatedToken } = loadConfigFile(configPath);
2136
+ const parsed = configSchema.safeParse(raw);
2137
+ if (!parsed.success) {
2138
+ const issues = parsed.error.issues.map((issue) => issue.message).join("; ");
2139
+ throw new Error(`Invalid opendevbrowser config at ${configPath}: ${issues}`);
2140
+ }
2141
+ const data = parsed.data;
2142
+ const relayToken = data.relayToken ?? generatedToken ?? generateSecureToken();
2143
+ return { ...data, relayToken };
2144
+ }
2145
+ var ConfigStore = class {
2146
+ current;
2147
+ constructor(initial) {
2148
+ this.current = initial;
2149
+ }
2150
+ get() {
2151
+ return this.current;
2152
+ }
2153
+ set(next) {
2154
+ this.current = next;
2155
+ }
2156
+ };
2157
+
2158
+ // src/relay/relay-server.ts
2159
+ import { createServer } from "http";
2160
+ import { timingSafeEqual } from "crypto";
2161
+ import { WebSocket, WebSocketServer } from "ws";
2162
+ var DEFAULT_DISCOVERY_PORT = 8787;
2163
+ var CONFIG_PATH = "/config";
2164
+ var PAIR_PATH = "/pair";
2165
+ var RelayServer = class _RelayServer {
2166
+ running = false;
2167
+ baseUrl = null;
2168
+ port = null;
2169
+ server = null;
2170
+ discoveryServer = null;
2171
+ extensionWss = null;
2172
+ cdpWss = null;
2173
+ extensionSocket = null;
2174
+ cdpSocket = null;
2175
+ extensionInfo = null;
2176
+ pairingToken = null;
2177
+ configuredDiscoveryPort;
2178
+ discoveryPort = null;
2179
+ handshakeAttempts = /* @__PURE__ */ new Map();
2180
+ cdpAllowlist = null;
2181
+ static MAX_HANDSHAKE_ATTEMPTS = 5;
2182
+ static RATE_LIMIT_WINDOW_MS = 6e4;
2183
+ constructor(options = {}) {
2184
+ this.configuredDiscoveryPort = options.discoveryPort ?? DEFAULT_DISCOVERY_PORT;
2185
+ }
2186
+ async start(port = 8787) {
2187
+ if (this.running && this.baseUrl && this.port !== null) {
2188
+ return { url: this.baseUrl, port: this.port };
2189
+ }
2190
+ this.server = createServer();
2191
+ this.extensionWss = new WebSocketServer({ noServer: true });
2192
+ this.cdpWss = new WebSocketServer({ noServer: true });
2193
+ this.extensionWss.on("connection", (socket) => {
2194
+ if (this.extensionSocket) {
2195
+ this.extensionSocket.close(1e3, "Replaced by a new extension client");
2196
+ }
2197
+ this.extensionSocket = socket;
2198
+ this.extensionInfo = null;
2199
+ socket.on("message", (data) => {
2200
+ this.handleExtensionMessage(data);
2201
+ });
2202
+ socket.on("close", () => {
2203
+ if (this.extensionSocket === socket) {
2204
+ this.extensionSocket = null;
2205
+ this.extensionInfo = null;
2206
+ }
2207
+ if (this.cdpSocket) {
2208
+ this.cdpSocket.close(1011, "Extension disconnected");
2209
+ }
2210
+ });
2211
+ });
2212
+ this.cdpWss.on("connection", (socket) => {
2213
+ if (this.cdpSocket) {
2214
+ socket.close(1008, "Only one CDP client supported");
2215
+ return;
2216
+ }
2217
+ this.cdpSocket = socket;
2218
+ socket.on("message", (data) => {
2219
+ this.handleCdpMessage(data);
2220
+ });
2221
+ socket.on("close", () => {
2222
+ if (this.cdpSocket === socket) {
2223
+ this.cdpSocket = null;
2224
+ }
2225
+ });
2226
+ });
2227
+ this.server.on("request", (request, response) => {
2228
+ const pathname = new URL(request.url ?? "", "http://127.0.0.1").pathname;
2229
+ const origin = request.headers.origin;
2230
+ if (pathname === CONFIG_PATH && request.method === "OPTIONS") {
2231
+ this.handleConfigPreflight(origin, response);
2232
+ return;
2233
+ }
2234
+ if (pathname === CONFIG_PATH && request.method === "GET") {
2235
+ this.handleConfigRequest(origin, response);
2236
+ return;
2237
+ }
2238
+ if (pathname === PAIR_PATH && request.method === "OPTIONS") {
2239
+ if (origin && origin.startsWith("chrome-extension://")) {
2240
+ response.setHeader("Access-Control-Allow-Origin", origin);
2241
+ response.setHeader("Access-Control-Allow-Methods", "GET, OPTIONS");
2242
+ response.setHeader("Access-Control-Allow-Headers", "Content-Type");
2243
+ }
2244
+ response.writeHead(204);
2245
+ response.end();
2246
+ return;
2247
+ }
2248
+ if (pathname === PAIR_PATH && request.method === "GET") {
2249
+ if (!this.isExtensionOrigin(origin)) {
2250
+ response.writeHead(403, { "Content-Type": "application/json" });
2251
+ response.end(JSON.stringify({ error: "Forbidden: extension origin required" }));
2252
+ return;
2253
+ }
2254
+ response.setHeader("Access-Control-Allow-Origin", origin);
2255
+ response.writeHead(200, { "Content-Type": "application/json" });
2256
+ response.end(JSON.stringify({ token: this.pairingToken }));
2257
+ return;
2258
+ }
2259
+ response.writeHead(404);
2260
+ response.end();
2261
+ });
2262
+ this.server.on("upgrade", (request, socket, head) => {
2263
+ const origin = request.headers.origin;
2264
+ const ip = request.socket.remoteAddress ?? "unknown";
2265
+ if (!this.isAllowedOrigin(origin)) {
2266
+ this.logSecurityEvent("origin_blocked", { origin, ip });
2267
+ socket.write("HTTP/1.1 403 Forbidden\r\n\r\n");
2268
+ socket.destroy();
2269
+ return;
2270
+ }
2271
+ if (this.isRateLimited(ip)) {
2272
+ this.logSecurityEvent("rate_limited", { ip });
2273
+ socket.write("HTTP/1.1 429 Too Many Requests\r\n\r\n");
2274
+ socket.destroy();
2275
+ return;
2276
+ }
2277
+ const pathname = new URL(request.url ?? "", "http://127.0.0.1").pathname;
2278
+ if (pathname === "/extension") {
2279
+ this.extensionWss?.handleUpgrade(request, socket, head, (ws) => {
2280
+ this.extensionWss?.emit("connection", ws, request);
2281
+ });
2282
+ return;
2283
+ }
2284
+ if (pathname === "/cdp") {
2285
+ this.cdpWss?.handleUpgrade(request, socket, head, (ws) => {
2286
+ this.cdpWss?.emit("connection", ws, request);
2287
+ });
2288
+ return;
2289
+ }
2290
+ socket.destroy();
2291
+ });
2292
+ await new Promise((resolve, reject) => {
2293
+ this.server?.once("error", reject);
2294
+ this.server?.listen(port, "127.0.0.1", () => {
2295
+ resolve();
2296
+ });
2297
+ });
2298
+ const address = this.server.address();
2299
+ if (!address) {
2300
+ throw new Error("Relay server did not expose a port");
2301
+ }
2302
+ this.port = address.port;
2303
+ this.baseUrl = `ws://127.0.0.1:${address.port}`;
2304
+ this.running = true;
2305
+ try {
2306
+ await this.startDiscoveryServer();
2307
+ } catch (error) {
2308
+ const message = error instanceof Error ? error.message : String(error);
2309
+ console.warn(`[opendevbrowser] Discovery server failed to start: ${message}`);
2310
+ this.stopDiscoveryServer();
2311
+ }
2312
+ return { url: this.baseUrl, port: address.port };
2313
+ }
2314
+ stop() {
2315
+ this.running = false;
2316
+ this.baseUrl = null;
2317
+ this.port = null;
2318
+ this.extensionInfo = null;
2319
+ this.stopDiscoveryServer();
2320
+ if (this.extensionSocket) {
2321
+ this.extensionSocket.close(1e3, "Relay stopped");
2322
+ this.extensionSocket = null;
2323
+ }
2324
+ if (this.cdpSocket) {
2325
+ this.cdpSocket.close(1e3, "Relay stopped");
2326
+ this.cdpSocket = null;
2327
+ }
2328
+ this.extensionWss?.close();
2329
+ this.cdpWss?.close();
2330
+ this.server?.close();
2331
+ this.extensionWss = null;
2332
+ this.cdpWss = null;
2333
+ this.server = null;
2334
+ }
2335
+ status() {
2336
+ return {
2337
+ running: this.running,
2338
+ url: this.baseUrl || void 0,
2339
+ port: this.port ?? void 0,
2340
+ extensionConnected: Boolean(this.extensionSocket),
2341
+ cdpConnected: Boolean(this.cdpSocket),
2342
+ extension: this.extensionInfo ?? void 0
2343
+ };
2344
+ }
2345
+ getCdpUrl() {
2346
+ return this.baseUrl ? `${this.baseUrl}/cdp` : null;
2347
+ }
2348
+ getDiscoveryPort() {
2349
+ if (this.port !== null && this.port === this.configuredDiscoveryPort) {
2350
+ return this.port;
2351
+ }
2352
+ return this.discoveryPort;
2353
+ }
2354
+ setToken(token) {
2355
+ const trimmed = typeof token === "string" ? token.trim() : "";
2356
+ this.pairingToken = trimmed.length ? trimmed : null;
2357
+ }
2358
+ setCdpAllowlist(methods) {
2359
+ if (!methods || methods.length === 0) {
2360
+ this.cdpAllowlist = null;
2361
+ return;
2362
+ }
2363
+ this.cdpAllowlist = new Set(methods);
2364
+ }
2365
+ isAllowedOrigin(origin) {
2366
+ if (!origin) {
2367
+ return true;
2368
+ }
2369
+ if (origin.startsWith("chrome-extension://")) {
2370
+ return true;
2371
+ }
2372
+ return false;
2373
+ }
2374
+ isExtensionOrigin(origin) {
2375
+ return Boolean(origin && origin.startsWith("chrome-extension://"));
2376
+ }
2377
+ handleConfigPreflight(origin, response) {
2378
+ if (this.isExtensionOrigin(origin)) {
2379
+ response.setHeader("Access-Control-Allow-Origin", origin);
2380
+ response.setHeader("Access-Control-Allow-Methods", "GET, OPTIONS");
2381
+ response.setHeader("Access-Control-Allow-Headers", "Content-Type");
2382
+ }
2383
+ response.writeHead(204);
2384
+ response.end();
2385
+ }
2386
+ handleConfigRequest(origin, response) {
2387
+ if (!this.isExtensionOrigin(origin)) {
2388
+ response.writeHead(403, { "Content-Type": "application/json" });
2389
+ response.end(JSON.stringify({ error: "Forbidden: extension origin required" }));
2390
+ return;
2391
+ }
2392
+ if (origin) {
2393
+ response.setHeader("Access-Control-Allow-Origin", origin);
2394
+ }
2395
+ if (this.port === null) {
2396
+ response.writeHead(503, { "Content-Type": "application/json" });
2397
+ response.end(JSON.stringify({ error: "Relay not running" }));
2398
+ return;
2399
+ }
2400
+ response.writeHead(200, {
2401
+ "Content-Type": "application/json",
2402
+ "Cache-Control": "no-store"
2403
+ });
2404
+ response.end(JSON.stringify({
2405
+ relayPort: this.port,
2406
+ pairingRequired: Boolean(this.pairingToken)
2407
+ }));
2408
+ }
2409
+ async startDiscoveryServer() {
2410
+ if (this.port === null || this.discoveryServer) {
2411
+ return;
2412
+ }
2413
+ if (this.configuredDiscoveryPort > 0 && this.configuredDiscoveryPort === this.port) {
2414
+ return;
2415
+ }
2416
+ this.discoveryServer = createServer((request, response) => {
2417
+ const pathname = new URL(request.url ?? "", "http://127.0.0.1").pathname;
2418
+ const origin = request.headers.origin;
2419
+ if (pathname === CONFIG_PATH && request.method === "OPTIONS") {
2420
+ this.handleConfigPreflight(origin, response);
2421
+ return;
2422
+ }
2423
+ if (pathname === CONFIG_PATH && request.method === "GET") {
2424
+ this.handleConfigRequest(origin, response);
2425
+ return;
2426
+ }
2427
+ response.writeHead(404);
2428
+ response.end();
2429
+ });
2430
+ await new Promise((resolve, reject) => {
2431
+ this.discoveryServer?.once("error", reject);
2432
+ this.discoveryServer?.listen(this.configuredDiscoveryPort, "127.0.0.1", () => {
2433
+ resolve();
2434
+ });
2435
+ });
2436
+ const address = this.discoveryServer.address();
2437
+ if (!address) {
2438
+ throw new Error("Discovery server did not expose a port");
2439
+ }
2440
+ this.discoveryPort = address.port;
2441
+ }
2442
+ stopDiscoveryServer() {
2443
+ if (this.discoveryServer) {
2444
+ this.discoveryServer.close();
2445
+ this.discoveryServer = null;
2446
+ }
2447
+ this.discoveryPort = null;
2448
+ }
2449
+ isRateLimited(ip) {
2450
+ const now = Date.now();
2451
+ const record = this.handshakeAttempts.get(ip);
2452
+ if (!record || now > record.resetAt) {
2453
+ this.handshakeAttempts.set(ip, { count: 1, resetAt: now + _RelayServer.RATE_LIMIT_WINDOW_MS });
2454
+ return false;
2455
+ }
2456
+ record.count++;
2457
+ return record.count > _RelayServer.MAX_HANDSHAKE_ATTEMPTS;
2458
+ }
2459
+ isCommandAllowed(method) {
2460
+ if (!this.cdpAllowlist) return true;
2461
+ return this.cdpAllowlist.has(method);
2462
+ }
2463
+ logSecurityEvent(event, details) {
2464
+ const safeDetails = { ...details };
2465
+ delete safeDetails.token;
2466
+ delete safeDetails.pairingToken;
2467
+ console.warn(`[security] ${event}`, JSON.stringify(safeDetails));
2468
+ }
2469
+ handleCdpMessage(data) {
2470
+ const message = parseJson(data);
2471
+ if (!isRecord(message)) {
2472
+ return;
2473
+ }
2474
+ const id = message.id;
2475
+ const method = message.method;
2476
+ if (typeof id !== "string" && typeof id !== "number" || typeof method !== "string") {
2477
+ return;
2478
+ }
2479
+ if (!this.extensionSocket) {
2480
+ this.sendJson(this.cdpSocket, {
2481
+ id,
2482
+ error: { message: "Extension not connected to relay" }
2483
+ });
2484
+ return;
2485
+ }
2486
+ if (!this.isCommandAllowed(method)) {
2487
+ this.logSecurityEvent("command_blocked", { method });
2488
+ this.sendJson(this.cdpSocket, {
2489
+ id,
2490
+ error: { message: `CDP command '${method}' not in allowlist` }
2491
+ });
2492
+ return;
2493
+ }
2494
+ const relayCommand = {
2495
+ id,
2496
+ method: "forwardCDPCommand",
2497
+ params: {
2498
+ method,
2499
+ params: message.params,
2500
+ sessionId: typeof message.sessionId === "string" ? message.sessionId : void 0
2501
+ }
2502
+ };
2503
+ this.sendJson(this.extensionSocket, relayCommand);
2504
+ }
2505
+ handleExtensionMessage(data) {
2506
+ const message = parseJson(data);
2507
+ if (!isRecord(message)) {
2508
+ return;
2509
+ }
2510
+ if (isHandshake(message)) {
2511
+ if (!this.isPairingTokenValid(message)) {
2512
+ this.logSecurityEvent("handshake_failed", { reason: "invalid_token", tabId: message.payload.tabId });
2513
+ this.extensionInfo = null;
2514
+ this.extensionSocket?.close(1008, "Invalid pairing token");
2515
+ return;
2516
+ }
2517
+ this.extensionInfo = {
2518
+ tabId: message.payload.tabId,
2519
+ url: message.payload.url,
2520
+ title: message.payload.title,
2521
+ groupId: message.payload.groupId
2522
+ };
2523
+ return;
2524
+ }
2525
+ if (message.method === "forwardCDPEvent" && isRecord(message.params)) {
2526
+ const params = message.params;
2527
+ const event = {
2528
+ method: params.method,
2529
+ params: params.params ?? {}
2530
+ };
2531
+ if (params.sessionId) {
2532
+ event.sessionId = params.sessionId;
2533
+ }
2534
+ this.sendJson(this.cdpSocket, event);
2535
+ return;
2536
+ }
2537
+ if (typeof message.id === "string" || typeof message.id === "number") {
2538
+ const response = { id: message.id };
2539
+ if (typeof message.result !== "undefined") {
2540
+ response.result = message.result;
2541
+ }
2542
+ if (message.error) {
2543
+ response.error = message.error;
2544
+ }
2545
+ if (typeof message.sessionId === "string") {
2546
+ response.sessionId = message.sessionId;
2547
+ }
2548
+ this.sendJson(this.cdpSocket, response);
2549
+ }
2550
+ }
2551
+ sendJson(socket, payload) {
2552
+ if (!socket || socket.readyState !== WebSocket.OPEN) {
2553
+ return;
2554
+ }
2555
+ socket.send(JSON.stringify(payload));
2556
+ }
2557
+ isPairingTokenValid(handshake) {
2558
+ if (!this.pairingToken) {
2559
+ return true;
2560
+ }
2561
+ const expected = this.pairingToken;
2562
+ const received = handshake.payload.pairingToken ?? "";
2563
+ const expectedBuf = Buffer.from(expected, "utf-8");
2564
+ const receivedBuf = Buffer.from(received, "utf-8");
2565
+ if (expectedBuf.length !== receivedBuf.length) {
2566
+ timingSafeEqual(expectedBuf, expectedBuf);
2567
+ return false;
2568
+ }
2569
+ return timingSafeEqual(expectedBuf, receivedBuf);
2570
+ }
2571
+ };
2572
+ var parseJson = (data) => {
2573
+ const text = typeof data === "string" ? data : data.toString();
2574
+ try {
2575
+ return JSON.parse(text);
2576
+ } catch {
2577
+ return null;
2578
+ }
2579
+ };
2580
+ var isRecord = (value) => {
2581
+ return typeof value === "object" && value !== null;
2582
+ };
2583
+ var isHandshake = (value) => {
2584
+ if (value.type !== "handshake" || !isRecord(value.payload)) {
2585
+ return false;
2586
+ }
2587
+ return typeof value.payload.tabId === "number";
2588
+ };
2589
+
2590
+ // src/skills/skill-loader.ts
2591
+ import { readFile, readdir } from "fs/promises";
2592
+ import { join as join6 } from "path";
2593
+ import * as os2 from "os";
2594
+ var SkillLoader = class {
2595
+ rootDir;
2596
+ additionalPaths;
2597
+ skillCache = null;
2598
+ constructor(rootDir, additionalPaths = []) {
2599
+ this.rootDir = rootDir;
2600
+ this.additionalPaths = additionalPaths.map((p) => this.expandPath(p));
2601
+ }
2602
+ expandPath(p) {
2603
+ if (p.startsWith("~")) {
2604
+ return join6(os2.homedir(), p.slice(1));
2605
+ }
2606
+ return p;
2607
+ }
2608
+ async loadBestPractices(topic) {
2609
+ return this.loadSkill("opendevbrowser-best-practices", topic);
2610
+ }
2611
+ async loadSkill(name, topic) {
2612
+ const skills = await this.listSkills();
2613
+ const skill = skills.find((s) => s.name === name);
2614
+ if (!skill) {
2615
+ const available = skills.map((s) => s.name).join(", ") || "none";
2616
+ throw new Error(`Skill "${name}" not found. Available: ${available}`);
2617
+ }
2618
+ const content = await readFile(skill.path, "utf8");
2619
+ const trimmed = content.trim();
2620
+ if (!topic || !topic.trim()) {
2621
+ return trimmed;
2622
+ }
2623
+ const filtered = filterSections(trimmed, topic);
2624
+ return filtered || trimmed;
2625
+ }
2626
+ async listSkills() {
2627
+ if (this.skillCache) {
2628
+ return this.skillCache;
2629
+ }
2630
+ const skills = [];
2631
+ const searchPaths = this.getSearchPaths();
2632
+ for (const searchPath of searchPaths) {
2633
+ const discovered = await this.discoverSkillsInPath(searchPath);
2634
+ for (const skill of discovered) {
2635
+ if (!skills.some((s) => s.name === skill.name)) {
2636
+ skills.push(skill);
2637
+ }
2638
+ }
2639
+ }
2640
+ this.skillCache = skills;
2641
+ return skills;
2642
+ }
2643
+ getSearchPaths() {
2644
+ const configDir = process.env.OPENCODE_CONFIG_DIR || join6(os2.homedir(), ".config", "opencode");
2645
+ const searchPaths = [
2646
+ join6(this.rootDir, ".opencode", "skill"),
2647
+ join6(configDir, "skill"),
2648
+ join6(this.rootDir, ".claude", "skills"),
2649
+ join6(os2.homedir(), ".claude", "skills"),
2650
+ ...this.additionalPaths
2651
+ ];
2652
+ return Array.from(new Set(searchPaths));
2653
+ }
2654
+ async discoverSkillsInPath(searchPath) {
2655
+ const skills = [];
2656
+ try {
2657
+ const entries = await readdir(searchPath, { withFileTypes: true });
2658
+ for (const entry of entries) {
2659
+ if (!entry.isDirectory()) continue;
2660
+ const skillPath = join6(searchPath, entry.name, "SKILL.md");
2661
+ try {
2662
+ const content = await readFile(skillPath, "utf8");
2663
+ const metadata = this.parseSkillMetadata(content, entry.name);
2664
+ skills.push({
2665
+ name: metadata.name,
2666
+ description: metadata.description,
2667
+ version: metadata.version ?? "1.0.0",
2668
+ path: skillPath
2669
+ });
2670
+ } catch {
2671
+ }
2672
+ }
2673
+ } catch {
2674
+ }
2675
+ return skills;
2676
+ }
2677
+ parseSkillMetadata(content, dirName) {
2678
+ const frontmatterMatch = content.match(/^---\s*\n([\s\S]*?)\n---/);
2679
+ if (!frontmatterMatch) {
2680
+ return {
2681
+ name: dirName,
2682
+ description: this.extractFirstParagraph(content) || `Skill: ${dirName}`
2683
+ };
2684
+ }
2685
+ const frontmatter = frontmatterMatch[1] ?? "";
2686
+ const metadata = {
2687
+ name: dirName,
2688
+ description: ""
2689
+ };
2690
+ const nameMatch = frontmatter.match(/^name:\s*["']?([^"'\n]+)["']?\s*$/m);
2691
+ if (nameMatch?.[1]) {
2692
+ metadata.name = nameMatch[1].trim();
2693
+ }
2694
+ const descMatch = frontmatter.match(/^description:\s*["']?([^"'\n]+)["']?\s*$/m);
2695
+ if (descMatch?.[1]) {
2696
+ metadata.description = descMatch[1].trim();
2697
+ }
2698
+ const versionMatch = frontmatter.match(/^version:\s*["']?([^"'\n]+)["']?\s*$/m);
2699
+ if (versionMatch?.[1]) {
2700
+ metadata.version = versionMatch[1].trim();
2701
+ }
2702
+ if (!metadata.description) {
2703
+ const afterFrontmatter = content.slice(frontmatterMatch[0].length);
2704
+ metadata.description = this.extractFirstParagraph(afterFrontmatter) || `Skill: ${metadata.name}`;
2705
+ }
2706
+ return metadata;
2707
+ }
2708
+ extractFirstParagraph(content) {
2709
+ const lines = content.trim().split(/\n/);
2710
+ const paragraphLines = [];
2711
+ for (const line of lines) {
2712
+ const trimmedLine = line.trim();
2713
+ if (trimmedLine.startsWith("#")) continue;
2714
+ if (trimmedLine === "" && paragraphLines.length > 0) break;
2715
+ if (trimmedLine !== "") {
2716
+ paragraphLines.push(trimmedLine);
2717
+ }
2718
+ }
2719
+ const paragraph = paragraphLines.join(" ").trim();
2720
+ return paragraph.length > 0 ? paragraph.slice(0, 200) : null;
2721
+ }
2722
+ clearCache() {
2723
+ this.skillCache = null;
2724
+ }
2725
+ };
2726
+ function filterSections(content, topic) {
2727
+ const normalized = topic.trim().toLowerCase();
2728
+ const lines = content.split(/\r?\n/);
2729
+ const sections = [];
2730
+ let currentHeading = "";
2731
+ let currentBody = [];
2732
+ const flush = () => {
2733
+ if (currentHeading || currentBody.length > 0) {
2734
+ sections.push({ heading: currentHeading, body: [...currentBody] });
2735
+ }
2736
+ currentHeading = "";
2737
+ currentBody = [];
2738
+ };
2739
+ for (const line of lines) {
2740
+ const headingMatch = line.match(/^(#{1,3})\s+(.*)$/);
2741
+ if (headingMatch) {
2742
+ flush();
2743
+ currentHeading = headingMatch[2]?.trim() ?? "";
2744
+ currentBody.push(line);
2745
+ continue;
2746
+ }
2747
+ currentBody.push(line);
2748
+ }
2749
+ flush();
2750
+ const matches = sections.filter((section) => section.heading.toLowerCase().includes(normalized));
2751
+ if (matches.length === 0) {
2752
+ return null;
2753
+ }
2754
+ return matches.map((section) => section.body.join("\n")).join("\n\n");
2755
+ }
2756
+
2757
+ // src/core/bootstrap.ts
2758
+ function createOpenDevBrowserCore(options) {
2759
+ const config = options.config ?? loadGlobalConfig();
2760
+ const configStore = new ConfigStore(config);
2761
+ const cacheRoot = options.worktree ?? options.directory;
2762
+ const manager = new BrowserManager(cacheRoot, config);
2763
+ const runner = new ScriptRunner(manager);
2764
+ const skills = new SkillLoader(cacheRoot, config.skillPaths);
2765
+ const relay = new RelayServer();
2766
+ relay.setToken(config.relayToken);
2767
+ const ensureRelay = async (port = config.relayPort) => {
2768
+ if (port <= 0 || config.relayToken === false) {
2769
+ relay.stop();
2770
+ return;
2771
+ }
2772
+ const status = relay.status();
2773
+ if (status.running && status.port === port) {
2774
+ return;
2775
+ }
2776
+ relay.stop();
2777
+ try {
2778
+ await relay.start(port);
2779
+ } catch (error) {
2780
+ const message = error instanceof Error ? error.message : String(error);
2781
+ if (message.includes("EADDRINUSE") || message.includes("in use")) {
2782
+ console.warn(`[opendevbrowser] Relay server port ${port} is already in use. Extension pairing will be unavailable.`);
2783
+ console.warn(`[opendevbrowser] To fix: kill the process using port ${port} or change relayPort in config.`);
2784
+ } else {
2785
+ console.warn(`[opendevbrowser] Failed to start relay server: ${message}`);
2786
+ }
2787
+ }
2788
+ };
2789
+ const cleanup = () => {
2790
+ relay.stop();
2791
+ manager.closeAll().catch(() => {
2792
+ });
2793
+ };
2794
+ return {
2795
+ cacheRoot,
2796
+ config,
2797
+ configStore,
2798
+ manager,
2799
+ runner,
2800
+ skills,
2801
+ relay,
2802
+ ensureRelay,
2803
+ cleanup,
2804
+ getExtensionPath
2805
+ };
2806
+ }
2807
+
2808
+ export {
2809
+ generateSecureToken,
2810
+ loadGlobalConfig,
2811
+ extractExtension,
2812
+ createOpenDevBrowserCore
2813
+ };
2814
+ /* v8 ignore next -- @preserve */
2815
+ //# sourceMappingURL=chunk-WTFSMBVH.js.map