opendevbrowser 0.0.10 → 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.
@@ -1,2208 +1,7 @@
1
1
  import {
2
- extractExtension,
3
- generateSecureToken,
4
- getExtensionPath
5
- } from "./chunk-R5VUZEUU.js";
6
-
7
- // src/config.ts
8
- import { z } from "zod";
9
- import * as fs from "fs";
10
- import * as path from "path";
11
- import * as os from "os";
12
- import { parse as parseJsonc } from "jsonc-parser";
13
- function isExecutable(filePath) {
14
- try {
15
- fs.accessSync(filePath, fs.constants.X_OK);
16
- return true;
17
- } catch {
18
- return false;
19
- }
20
- }
21
- var DEFAULT_RELAY_PORT = 8787;
22
- function buildDefaultConfigJsonc(token) {
23
- return `{
24
- // Set relayToken to false to disable extension pairing.
25
- "relayPort": ${DEFAULT_RELAY_PORT},
26
- "relayToken": "${token}"
27
- }
28
- `;
29
- }
30
- var snapshotSchema = z.object({
31
- maxChars: z.number().int().min(500).max(2e5).default(16e3),
32
- maxNodes: z.number().int().min(50).max(5e3).default(1e3)
33
- });
34
- var securitySchema = z.object({
35
- allowRawCDP: z.boolean().default(false),
36
- allowNonLocalCdp: z.boolean().default(false),
37
- allowUnsafeExport: z.boolean().default(false)
38
- });
39
- var devtoolsSchema = z.object({
40
- showFullUrls: z.boolean().default(false),
41
- showFullConsole: z.boolean().default(false)
42
- });
43
- var exportSchema = z.object({
44
- maxNodes: z.number().int().min(1).max(5e3).default(1e3),
45
- inlineStyles: z.boolean().default(true)
46
- });
47
- var skillsNudgeSchema = z.object({
48
- enabled: z.boolean().default(true),
49
- keywords: z.array(z.string()).default([
50
- "login",
51
- "sign in",
52
- "sign-in",
53
- "auth",
54
- "authentication",
55
- "mfa",
56
- "form",
57
- "submit",
58
- "validation",
59
- "extract",
60
- "scrape",
61
- "scraping",
62
- "table",
63
- "pagination",
64
- "crawl"
65
- ]),
66
- maxAgeMs: z.number().int().min(1e3).max(6e5).default(6e4)
67
- });
68
- var skillsSchema = z.object({
69
- nudge: skillsNudgeSchema.default({})
70
- }).default({});
71
- var continuityNudgeSchema = z.object({
72
- enabled: z.boolean().default(true),
73
- keywords: z.array(z.string()).default([
74
- "plan",
75
- "multi-step",
76
- "multi step",
77
- "long-running",
78
- "long running",
79
- "refactor",
80
- "migration",
81
- "rollout",
82
- "release",
83
- "upgrade",
84
- "investigate",
85
- "follow-up",
86
- "continue"
87
- ]),
88
- maxAgeMs: z.number().int().min(1e3).max(6e5).default(6e4)
89
- });
90
- var continuitySchema = z.object({
91
- enabled: z.boolean().default(true),
92
- filePath: z.string().min(1).default("opendevbrowser_continuity.md"),
93
- nudge: continuityNudgeSchema.default({})
94
- }).default({});
95
- var configSchema = z.object({
96
- headless: z.boolean().default(false),
97
- profile: z.string().min(1).default("default"),
98
- snapshot: snapshotSchema.default({}),
99
- security: securitySchema.default({}),
100
- devtools: devtoolsSchema.default({}),
101
- export: exportSchema.default({}),
102
- skills: skillsSchema.default({}),
103
- continuity: continuitySchema.default({}),
104
- relayPort: z.number().int().min(0).max(65535).default(DEFAULT_RELAY_PORT),
105
- relayToken: z.union([z.string(), z.literal(false)]).optional(),
106
- chromePath: z.string().min(1).optional().refine(
107
- (val) => val === void 0 || isExecutable(val),
108
- { message: "chromePath must point to an executable file" }
109
- ),
110
- flags: z.array(z.string()).default([]),
111
- checkForUpdates: z.boolean().default(false),
112
- persistProfile: z.boolean().default(true),
113
- skillPaths: z.array(z.string()).default([])
114
- });
115
- var CONFIG_FILE_NAME = "opendevbrowser.jsonc";
116
- function getGlobalConfigPath() {
117
- const configDir = process.env.OPENCODE_CONFIG_DIR || path.join(os.homedir(), ".config", "opencode");
118
- return path.join(configDir, CONFIG_FILE_NAME);
119
- }
120
- function ensureConfigFile(filePath) {
121
- const token = generateSecureToken();
122
- if (fs.existsSync(filePath)) {
123
- return token;
124
- }
125
- try {
126
- fs.mkdirSync(path.dirname(filePath), { recursive: true, mode: 448 });
127
- fs.writeFileSync(filePath, buildDefaultConfigJsonc(token), { encoding: "utf-8", mode: 384 });
128
- } catch (error) {
129
- console.warn(`[opendevbrowser] Warning: Could not create config file at ${filePath}:`, error);
130
- }
131
- return token;
132
- }
133
- function loadConfigFile(filePath) {
134
- if (!fs.existsSync(filePath)) {
135
- const token = ensureConfigFile(filePath);
136
- return { raw: {}, generatedToken: token };
137
- }
138
- const content = fs.readFileSync(filePath, "utf-8");
139
- const errors = [];
140
- const parsed = parseJsonc(content, errors, { allowTrailingComma: true });
141
- if (errors.length > 0) {
142
- const firstError = errors[0];
143
- throw new Error(`Invalid JSONC in opendevbrowser config at ${filePath}: parse error at offset ${firstError?.offset ?? 0}`);
144
- }
145
- return { raw: parsed ?? {}, generatedToken: null };
146
- }
147
- function loadGlobalConfig() {
148
- const configPath = getGlobalConfigPath();
149
- const { raw, generatedToken } = loadConfigFile(configPath);
150
- const parsed = configSchema.safeParse(raw);
151
- if (!parsed.success) {
152
- const issues = parsed.error.issues.map((issue) => issue.message).join("; ");
153
- throw new Error(`Invalid opendevbrowser config at ${configPath}: ${issues}`);
154
- }
155
- const data = parsed.data;
156
- const relayToken = data.relayToken ?? generatedToken ?? generateSecureToken();
157
- return { ...data, relayToken };
158
- }
159
- var ConfigStore = class {
160
- current;
161
- constructor(initial) {
162
- this.current = initial;
163
- }
164
- get() {
165
- return this.current;
166
- }
167
- set(next) {
168
- this.current = next;
169
- }
170
- };
171
-
172
- // src/browser/browser-manager.ts
173
- import { randomUUID as randomUUID3 } from "crypto";
174
- import { mkdir as mkdir2, rm } from "fs/promises";
175
- import { join as join4 } from "path";
176
- import { chromium } from "playwright-core";
177
- import { Mutex } from "async-mutex";
178
-
179
- // src/cache/paths.ts
180
- import { createHash } from "crypto";
181
- import { mkdir, stat } from "fs/promises";
182
- import { homedir as homedir2 } from "os";
183
- import { join as join2 } from "path";
184
- function safeHash(value) {
185
- return createHash("sha256").update(value).digest("hex").slice(0, 16);
186
- }
187
- async function ensureDir(path2) {
188
- await mkdir(path2, { recursive: true });
189
- }
190
- async function resolveCachePaths(worktree, profile) {
191
- const base = process.env.OPENCODE_CACHE_DIR ?? process.env.XDG_CACHE_HOME ?? join2(homedir2(), ".cache");
192
- const root = join2(base, "opendevbrowser");
193
- const projectRoot = join2(root, "projects", safeHash(worktree));
194
- const profileDir = join2(projectRoot, "profiles", profile);
195
- const chromeDir = join2(root, "chrome");
196
- await ensureDir(root);
197
- await ensureDir(projectRoot);
198
- await ensureDir(profileDir);
199
- await ensureDir(chromeDir);
200
- return { root, projectRoot, profileDir, chromeDir };
201
- }
202
-
203
- // src/cache/chrome-locator.ts
204
- import { access } from "fs/promises";
205
- import { delimiter, join as join3 } from "path";
206
- async function pathExists(path2) {
207
- try {
208
- await access(path2);
209
- return true;
210
- } catch {
211
- return false;
212
- }
213
- }
214
- function pathCandidatesByPlatform() {
215
- const platform = process.platform;
216
- if (platform === "darwin") {
217
- return [
218
- "/Applications/Google Chrome.app/Contents/MacOS/Google Chrome",
219
- "/Applications/Google Chrome Canary.app/Contents/MacOS/Google Chrome Canary"
220
- ];
221
- }
222
- if (platform === "win32") {
223
- const programFiles = process.env.PROGRAMFILES || "C:\\Program Files";
224
- const programFilesX86 = process.env["PROGRAMFILES(X86)"] || "C:\\Program Files (x86)";
225
- const localAppData = process.env.LOCALAPPDATA || "";
226
- return [
227
- join3(programFiles, "Google", "Chrome", "Application", "chrome.exe"),
228
- join3(programFilesX86, "Google", "Chrome", "Application", "chrome.exe"),
229
- join3(localAppData, "Google", "Chrome", "Application", "chrome.exe")
230
- ];
231
- }
232
- return [];
233
- }
234
- function binaryCandidatesInPath() {
235
- return [
236
- "google-chrome",
237
- "google-chrome-stable",
238
- "chromium",
239
- "chromium-browser"
240
- ];
241
- }
242
- async function findInPath(binary) {
243
- const pathValue = process.env.PATH;
244
- if (!pathValue) return null;
245
- const candidates = process.platform === "win32" ? [binary, `${binary}.exe`] : [binary];
246
- for (const dir of pathValue.split(delimiter)) {
247
- for (const name of candidates) {
248
- const fullPath = join3(dir, name);
249
- if (await pathExists(fullPath)) return fullPath;
250
- }
251
- }
252
- return null;
253
- }
254
- async function findChromeExecutable(overridePath) {
255
- if (overridePath && await pathExists(overridePath)) {
256
- return overridePath;
257
- }
258
- for (const candidate of pathCandidatesByPlatform()) {
259
- if (await pathExists(candidate)) return candidate;
260
- }
261
- for (const binary of binaryCandidatesInPath()) {
262
- const found = await findInPath(binary);
263
- if (found) return found;
264
- }
265
- return null;
266
- }
267
-
268
- // src/cache/downloader.ts
269
- import { Browser, detectBrowserPlatform, install, resolveBuildId } from "@puppeteer/browsers";
270
- async function downloadChromeForTesting(cacheDir) {
271
- const platform = detectBrowserPlatform();
272
- if (!platform) {
273
- throw new Error("Unsupported platform for Chrome download");
274
- }
275
- const buildId = await resolveBuildId(Browser.CHROME, platform, "latest");
276
- const result = await install({
277
- browser: Browser.CHROME,
278
- buildId,
279
- cacheDir,
280
- downloadProgressCallback: () => void 0
281
- });
282
- return {
283
- executablePath: result.executablePath,
284
- buildId
285
- };
286
- }
287
-
288
- // src/devtools/console-tracker.ts
289
- var JWT_PATTERN = /\beyJ[A-Za-z0-9_-]+\.[A-Za-z0-9_-]+\.[A-Za-z0-9_-]+\b/g;
290
- var TOKEN_LIKE_PATTERN = /\b[A-Za-z0-9_-]{16,}\b/g;
291
- var API_KEY_PREFIX_PATTERN = /\b(sk_|pk_|api_|key_|token_|secret_|bearer_)[A-Za-z0-9_-]+\b/gi;
292
- var SENSITIVE_KV_PATTERN = /\b(token|key|secret|password|auth|bearer|credential)[=:]\s*\S+/gi;
293
- function shouldRedactToken(token) {
294
- if (/^(sk_|pk_|api_|key_|token_|secret_|bearer_)/i.test(token)) {
295
- return true;
296
- }
297
- const categories = [
298
- /[a-z]/.test(token),
299
- /[A-Z]/.test(token),
300
- /\d/.test(token),
301
- /[_-]/.test(token)
302
- ].filter(Boolean).length;
303
- return categories >= 2;
304
- }
305
- function redactText(text) {
306
- let result = text.replace(SENSITIVE_KV_PATTERN, (match) => {
307
- const sepIndex = match.search(/[=:]/);
308
- return match.slice(0, sepIndex + 1) + "[REDACTED]";
309
- });
310
- result = result.replace(JWT_PATTERN, "[REDACTED]");
311
- result = result.replace(API_KEY_PREFIX_PATTERN, "[REDACTED]");
312
- result = result.replace(TOKEN_LIKE_PATTERN, (match) => shouldRedactToken(match) ? "[REDACTED]" : match);
313
- return result;
314
- }
315
- var ConsoleTracker = class {
316
- events = [];
317
- maxEvents;
318
- seq = 0;
319
- page = null;
320
- handler;
321
- showFullConsole;
322
- constructor(maxEvents = 200, options = {}) {
323
- this.maxEvents = maxEvents;
324
- this.showFullConsole = options.showFullConsole ?? false;
325
- }
326
- setOptions(options) {
327
- if (typeof options.showFullConsole === "boolean") {
328
- this.showFullConsole = options.showFullConsole;
329
- }
330
- }
331
- attach(page) {
332
- if (this.page === page) return;
333
- this.detach();
334
- this.page = page;
335
- this.handler = (msg) => {
336
- const rawText = msg.text();
337
- const text = this.showFullConsole ? rawText : redactText(rawText);
338
- this.seq += 1;
339
- this.events.push({
340
- seq: this.seq,
341
- level: msg.type(),
342
- text,
343
- ts: Date.now()
344
- });
345
- if (this.events.length > this.maxEvents) {
346
- this.events.shift();
347
- }
348
- };
349
- page.on("console", this.handler);
350
- }
351
- detach() {
352
- if (this.page && this.handler) {
353
- this.page.off("console", this.handler);
354
- }
355
- this.page = null;
356
- this.handler = void 0;
357
- }
358
- poll(sinceSeq = 0, max = 50) {
359
- const events = this.events.filter((event) => event.seq > sinceSeq).slice(0, max);
360
- const last = events[events.length - 1];
361
- const nextSeq = last ? last.seq : sinceSeq;
362
- return { events, nextSeq };
363
- }
364
- };
365
-
366
- // src/devtools/network-tracker.ts
367
- function shouldRedactPathSegment(segment) {
368
- if (segment.length < 16) return false;
369
- if (/^\d+$/.test(segment)) return false;
370
- if (/^[a-f0-9-]{36}$/i.test(segment)) return false;
371
- if (/^(sk_|pk_|api_|key_|token_|secret_|bearer_)/i.test(segment)) return true;
372
- const categories = [/[a-z]/, /[A-Z]/, /\d/, /[_-]/].filter((r) => r.test(segment)).length;
373
- return categories >= 3 && segment.length >= 20;
374
- }
375
- function redactUrl(rawUrl) {
376
- try {
377
- const parsed = new URL(rawUrl);
378
- parsed.search = "";
379
- parsed.hash = "";
380
- const segments = parsed.pathname.split("/");
381
- const redactedSegments = segments.map(
382
- (segment) => shouldRedactPathSegment(segment) ? "[REDACTED]" : segment
383
- );
384
- parsed.pathname = redactedSegments.join("/");
385
- return parsed.toString();
386
- } catch {
387
- return rawUrl.split(/[?#]/)[0] ?? rawUrl;
388
- }
389
- }
390
- var NetworkTracker = class {
391
- events = [];
392
- maxEvents;
393
- seq = 0;
394
- page = null;
395
- requestHandler;
396
- responseHandler;
397
- showFullUrls;
398
- constructor(maxEvents = 300, options = {}) {
399
- this.maxEvents = maxEvents;
400
- this.showFullUrls = options.showFullUrls ?? false;
401
- }
402
- setOptions(options) {
403
- if (typeof options.showFullUrls === "boolean") {
404
- this.showFullUrls = options.showFullUrls;
405
- }
406
- }
407
- attach(page) {
408
- if (this.page === page) return;
409
- this.detach();
410
- this.page = page;
411
- this.requestHandler = (req) => {
412
- this.push({
413
- method: req.method(),
414
- url: this.showFullUrls ? req.url() : redactUrl(req.url()),
415
- resourceType: req.resourceType(),
416
- ts: Date.now()
417
- });
418
- };
419
- this.responseHandler = (res) => {
420
- const req = res.request();
421
- this.push({
422
- method: req.method(),
423
- url: this.showFullUrls ? res.url() : redactUrl(res.url()),
424
- status: res.status(),
425
- resourceType: req.resourceType(),
426
- ts: Date.now()
427
- });
428
- };
429
- page.on("request", this.requestHandler);
430
- page.on("response", this.responseHandler);
431
- }
432
- detach() {
433
- if (this.page && this.requestHandler) {
434
- this.page.off("request", this.requestHandler);
435
- }
436
- if (this.page && this.responseHandler) {
437
- this.page.off("response", this.responseHandler);
438
- }
439
- this.page = null;
440
- this.requestHandler = void 0;
441
- this.responseHandler = void 0;
442
- }
443
- poll(sinceSeq = 0, max = 50) {
444
- const events = this.events.filter((event) => event.seq > sinceSeq).slice(0, max);
445
- const last = events[events.length - 1];
446
- const nextSeq = last ? last.seq : sinceSeq;
447
- return { events, nextSeq };
448
- }
449
- push(event) {
450
- this.seq += 1;
451
- this.events.push({
452
- seq: this.seq,
453
- ...event
454
- });
455
- if (this.events.length > this.maxEvents) {
456
- this.events.shift();
457
- }
458
- }
459
- };
460
-
461
- // src/export/css-extract.ts
462
- var STYLE_ALLOWLIST = /* @__PURE__ */ new Set([
463
- "align-content",
464
- "align-items",
465
- "align-self",
466
- "background",
467
- "background-attachment",
468
- "background-clip",
469
- "background-color",
470
- "background-image",
471
- "background-origin",
472
- "background-position",
473
- "background-position-x",
474
- "background-position-y",
475
- "background-repeat",
476
- "background-size",
477
- "border",
478
- "border-bottom",
479
- "border-bottom-color",
480
- "border-bottom-left-radius",
481
- "border-bottom-right-radius",
482
- "border-bottom-style",
483
- "border-bottom-width",
484
- "border-color",
485
- "border-left",
486
- "border-left-color",
487
- "border-left-style",
488
- "border-left-width",
489
- "border-radius",
490
- "border-right",
491
- "border-right-color",
492
- "border-right-style",
493
- "border-right-width",
494
- "border-style",
495
- "border-top",
496
- "border-top-color",
497
- "border-top-left-radius",
498
- "border-top-right-radius",
499
- "border-top-style",
500
- "border-top-width",
501
- "border-width",
502
- "box-shadow",
503
- "box-sizing",
504
- "color",
505
- "column-gap",
506
- "contain",
507
- "direction",
508
- "display",
509
- "filter",
510
- "flex",
511
- "flex-direction",
512
- "flex-flow",
513
- "flex-wrap",
514
- "font",
515
- "font-family",
516
- "font-feature-settings",
517
- "font-kerning",
518
- "font-size",
519
- "font-size-adjust",
520
- "font-stretch",
521
- "font-style",
522
- "font-variant",
523
- "font-variant-caps",
524
- "font-variant-east-asian",
525
- "font-variant-ligatures",
526
- "font-variant-numeric",
527
- "font-variation-settings",
528
- "font-weight",
529
- "gap",
530
- "grid",
531
- "grid-auto-columns",
532
- "grid-auto-flow",
533
- "grid-auto-rows",
534
- "grid-template-areas",
535
- "grid-template-columns",
536
- "grid-template-rows",
537
- "height",
538
- "hyphens",
539
- "inset",
540
- "inset-block",
541
- "inset-inline",
542
- "isolation",
543
- "justify-content",
544
- "left",
545
- "letter-spacing",
546
- "line-height",
547
- "margin",
548
- "margin-bottom",
549
- "margin-left",
550
- "margin-right",
551
- "margin-top",
552
- "max-height",
553
- "max-width",
554
- "min-height",
555
- "min-width",
556
- "opacity",
557
- "outline",
558
- "outline-color",
559
- "outline-offset",
560
- "outline-style",
561
- "outline-width",
562
- "overflow",
563
- "overflow-wrap",
564
- "overflow-x",
565
- "overflow-y",
566
- "padding",
567
- "padding-bottom",
568
- "padding-left",
569
- "padding-right",
570
- "padding-top",
571
- "position",
572
- "right",
573
- "row-gap",
574
- "text-align",
575
- "text-align-last",
576
- "text-decoration",
577
- "text-decoration-color",
578
- "text-decoration-line",
579
- "text-decoration-style",
580
- "text-decoration-thickness",
581
- "text-indent",
582
- "text-rendering",
583
- "text-shadow",
584
- "text-transform",
585
- "top",
586
- "transform",
587
- "transform-origin",
588
- "visibility",
589
- "white-space",
590
- "width",
591
- "word-break",
592
- "word-spacing",
593
- "writing-mode",
594
- "z-index"
595
- ]);
596
- var SKIP_STYLE_VALUES = /* @__PURE__ */ new Set([
597
- "",
598
- "initial",
599
- "unset",
600
- "revert",
601
- "revert-layer"
602
- ]);
603
- function extractCss(capture) {
604
- const shouldFilter = capture.inlineStyles !== false;
605
- const lines = [];
606
- lines.push(".opendevbrowser-root {");
607
- for (const [key, value] of Object.entries(capture.styles)) {
608
- const trimmed = value.trim();
609
- if (trimmed.length === 0) continue;
610
- if (shouldFilter) {
611
- if (!STYLE_ALLOWLIST.has(key)) continue;
612
- if (SKIP_STYLE_VALUES.has(trimmed)) continue;
613
- }
614
- lines.push(` ${key}: ${value};`);
615
- }
616
- lines.push("}");
617
- return lines.join("\n");
618
- }
619
-
620
- // src/export/dom-capture.ts
621
- var DEFAULT_MAX_NODES = 1e3;
622
- async function captureDom(page, selector, options = {}) {
623
- const shouldSanitize = options.sanitize !== false;
624
- const maxNodes = options.maxNodes ?? DEFAULT_MAX_NODES;
625
- const inlineStyles = options.inlineStyles !== false;
626
- const styleAllowlist = Array.from(STYLE_ALLOWLIST);
627
- const skipStyleValues = Array.from(SKIP_STYLE_VALUES);
628
- return page.$eval(
629
- selector,
630
- (el, opts) => {
631
- const style = window.getComputedStyle(el);
632
- const styles = {};
633
- for (const prop of Array.from(style)) {
634
- styles[prop] = style.getPropertyValue(prop);
635
- }
636
- const warnings = [];
637
- const root = el;
638
- const clone = root.cloneNode(true);
639
- const originalElements = [root, ...Array.from(root.querySelectorAll("*"))];
640
- const cloneElements = [clone, ...Array.from(clone.querySelectorAll("*"))];
641
- const nodeLimit = Math.max(1, opts.maxNodes);
642
- if (originalElements.length > nodeLimit) {
643
- const omitted = originalElements.length - nodeLimit;
644
- warnings.push(`Export truncated at ${nodeLimit} nodes; ${omitted} nodes omitted.`);
645
- }
646
- const limit = Math.min(originalElements.length, nodeLimit);
647
- if (opts.inlineStyles) {
648
- const skipSet = new Set(opts.skipStyleValues);
649
- for (let index = 0; index < limit; index += 1) {
650
- const source = originalElements[index];
651
- const target = cloneElements[index];
652
- if (!source || !target) continue;
653
- const computed = window.getComputedStyle(source);
654
- const parts = [];
655
- for (const prop of opts.styleAllowlist) {
656
- const value = computed.getPropertyValue(prop).trim();
657
- if (value && !skipSet.has(value)) {
658
- parts.push(`${prop}: ${value};`);
659
- }
660
- }
661
- if (parts.length > 0) {
662
- target.setAttribute("style", parts.join(" "));
663
- }
664
- }
665
- }
666
- if (originalElements.length > nodeLimit) {
667
- for (let index = nodeLimit; index < cloneElements.length; index += 1) {
668
- const target = cloneElements[index];
669
- if (target) {
670
- target.remove();
671
- }
672
- }
673
- }
674
- const container = document.createElement("template");
675
- container.content.appendChild(clone);
676
- if (opts.shouldSanitize) {
677
- const blockedTags = /* @__PURE__ */ new Set([
678
- "script",
679
- "iframe",
680
- "object",
681
- "embed",
682
- "frame",
683
- "frameset",
684
- "applet",
685
- "base",
686
- "link",
687
- "meta",
688
- "noscript"
689
- ]);
690
- const urlAttrs = /* @__PURE__ */ new Set(["href", "src", "action", "formaction", "xlink:href", "srcset"]);
691
- const isDangerousUrl = (value) => {
692
- const normalized = value.trim().toLowerCase();
693
- return normalized.startsWith("javascript:") || normalized.startsWith("data:") || normalized.startsWith("vbscript:");
694
- };
695
- const isDangerousSrcset = (value) => {
696
- const entries = value.split(",");
697
- return entries.some((entry) => {
698
- const url = entry.trim().split(/\s+/)[0] ?? "";
699
- return isDangerousUrl(url);
700
- });
701
- };
702
- const DANGEROUS_CSS_PATTERNS = [
703
- /url\s*\(/i,
704
- /expression\s*\(/i,
705
- /-moz-binding/i,
706
- /behavior\s*:/i,
707
- /javascript\s*:/i
708
- ];
709
- const sanitizeStyle = (styleValue) => {
710
- let result = styleValue;
711
- let wasModified = false;
712
- for (const pattern of DANGEROUS_CSS_PATTERNS) {
713
- if (pattern.test(result)) {
714
- result = result.replace(new RegExp(pattern.source, "gi"), "/* blocked */");
715
- wasModified = true;
716
- }
717
- }
718
- return { sanitized: result, wasModified };
719
- };
720
- const sanitizeSvg = (svg) => {
721
- const scripts = svg.querySelectorAll("script");
722
- scripts.forEach((script) => {
723
- script.remove();
724
- warnings.push("Removed script element from SVG");
725
- });
726
- const foreignObjects = svg.querySelectorAll("foreignObject");
727
- foreignObjects.forEach((fo) => {
728
- fo.remove();
729
- warnings.push("Removed foreignObject from SVG");
730
- });
731
- const allElements = svg.querySelectorAll("*");
732
- allElements.forEach((el2) => {
733
- for (const attr of Array.from(el2.attributes)) {
734
- if (attr.name.toLowerCase().startsWith("on")) {
735
- el2.removeAttribute(attr.name);
736
- }
737
- }
738
- });
739
- };
740
- const sanitizeElement = (element) => {
741
- const tag = element.tagName.toLowerCase();
742
- if (blockedTags.has(tag)) {
743
- element.remove();
744
- return;
745
- }
746
- if (tag === "svg") {
747
- sanitizeSvg(element);
748
- }
749
- for (const attr of Array.from(element.attributes)) {
750
- const name = attr.name.toLowerCase();
751
- if (name.startsWith("on")) {
752
- element.removeAttribute(attr.name);
753
- continue;
754
- }
755
- if (name === "style") {
756
- const { sanitized, wasModified } = sanitizeStyle(attr.value);
757
- if (wasModified) {
758
- element.setAttribute("style", sanitized);
759
- warnings.push("Sanitized dangerous CSS in style attribute");
760
- }
761
- continue;
762
- }
763
- if (urlAttrs.has(name)) {
764
- const value = attr.value || "";
765
- const dangerous = name === "srcset" ? isDangerousSrcset(value) : isDangerousUrl(value);
766
- if (dangerous) {
767
- element.removeAttribute(attr.name);
768
- }
769
- }
770
- }
771
- };
772
- for (const element of Array.from(container.content.querySelectorAll("*"))) {
773
- sanitizeElement(element);
774
- }
775
- if (container.content.firstElementChild) {
776
- sanitizeElement(container.content.firstElementChild);
777
- }
778
- }
779
- return { html: container.innerHTML, styles, warnings, inlineStyles: opts.inlineStyles };
780
- },
781
- { shouldSanitize, maxNodes, inlineStyles, styleAllowlist, skipStyleValues }
782
- );
783
- }
784
-
785
- // src/export/react-emitter.ts
786
- function emitReactComponent(capture, css, options = {}) {
787
- const warnings = [...capture.warnings ?? []];
788
- if (options.allowUnsafeExport) {
789
- warnings.push("Unsafe export enabled: HTML sanitization disabled.");
790
- }
791
- const warningComment = options.allowUnsafeExport ? "// WARNING: Unsafe export enabled. HTML sanitization disabled.\n" : "";
792
- const component = `${warningComment}import "./opendevbrowser.css";
793
-
794
- export default function OpenDevBrowserComponent() {
795
- return (
796
- <div className="opendevbrowser-root" dangerouslySetInnerHTML={{ __html: ${JSON.stringify(capture.html)} }} />
797
- );
798
- }`;
799
- return { component, css, warnings: warnings.length > 0 ? warnings : void 0 };
800
- }
801
-
802
- // src/snapshot/refs.ts
803
- import { randomUUID } from "crypto";
804
- var RefStore = class {
805
- refsByTarget = /* @__PURE__ */ new Map();
806
- snapshotByTarget = /* @__PURE__ */ new Map();
807
- setSnapshot(targetId, entries) {
808
- const map = /* @__PURE__ */ new Map();
809
- for (const entry of entries) {
810
- map.set(entry.ref, entry);
811
- }
812
- const snapshotId = randomUUID();
813
- this.refsByTarget.set(targetId, map);
814
- this.snapshotByTarget.set(targetId, snapshotId);
815
- return { snapshotId, targetId, count: entries.length };
816
- }
817
- resolve(targetId, ref) {
818
- const map = this.refsByTarget.get(targetId);
819
- if (!map) return null;
820
- return map.get(ref) ?? null;
821
- }
822
- getSnapshotId(targetId) {
823
- return this.snapshotByTarget.get(targetId) ?? null;
824
- }
825
- getRefCount(targetId) {
826
- const map = this.refsByTarget.get(targetId);
827
- return map ? map.size : 0;
828
- }
829
- clearTarget(targetId) {
830
- this.refsByTarget.delete(targetId);
831
- this.snapshotByTarget.delete(targetId);
832
- }
833
- };
834
-
835
- // src/snapshot/snapshotter.ts
836
- var Snapshotter = class {
837
- refStore;
838
- constructor(refStore) {
839
- this.refStore = refStore;
840
- }
841
- async snapshot(page, targetId, options) {
842
- const startTime = Date.now();
843
- const session = await page.context().newCDPSession(page);
844
- let snapshotData;
845
- try {
846
- snapshotData = await buildSnapshot(session, options.mode, options.mainFrameOnly ?? true, options.maxNodes);
847
- } finally {
848
- await session.detach();
849
- }
850
- const snapshot = this.refStore.setSnapshot(targetId, snapshotData.entries);
851
- const formatted = snapshotData.lines;
852
- const startIndex = parseCursor(options.cursor);
853
- const { content, truncated, nextCursor } = paginate(formatted, startIndex, options.maxChars);
854
- const timingMs = Date.now() - startTime;
855
- let url;
856
- let title;
857
- try {
858
- url = page.url();
859
- title = await page.title();
860
- } catch (_err) {
861
- void _err;
862
- url = void 0;
863
- title = void 0;
864
- }
865
- return {
866
- snapshotId: snapshot.snapshotId,
867
- url,
868
- title,
869
- content,
870
- truncated,
871
- nextCursor,
872
- refCount: snapshot.count,
873
- timingMs,
874
- warnings: snapshotData.warnings
875
- };
876
- }
877
- };
878
- var DEFAULT_MAX_AX_NODES = 1e3;
879
- var ACTIONABLE_ROLES = /* @__PURE__ */ new Set([
880
- "button",
881
- "link",
882
- "textbox",
883
- "searchbox",
884
- "textarea",
885
- "checkbox",
886
- "radio",
887
- "combobox",
888
- "listbox",
889
- "menuitem",
890
- "menuitemcheckbox",
891
- "menuitemradio",
892
- "option",
893
- "switch",
894
- "tab",
895
- "slider",
896
- "spinbutton",
897
- "treeitem"
898
- ]);
899
- var SEMANTIC_ROLES = /* @__PURE__ */ new Set([
900
- "heading",
901
- "article",
902
- "main",
903
- "navigation",
904
- "region",
905
- "section",
906
- "form",
907
- "list",
908
- "listitem",
909
- "paragraph",
910
- "img",
911
- "table",
912
- "row",
913
- "cell",
914
- "columnheader",
915
- "rowheader",
916
- "banner",
917
- "contentinfo",
918
- "complementary"
919
- ]);
920
- var selectorFunction = function() {
921
- if (!(this instanceof Element)) return null;
922
- const escape = (value) => {
923
- if (typeof CSS !== "undefined" && CSS.escape) {
924
- return CSS.escape(value);
925
- }
926
- return String(value).replace(/([^\w-])/g, "\\$1");
927
- };
928
- const testId = this.getAttribute("data-testid");
929
- if (testId) {
930
- return '[data-testid="' + escape(testId) + '"]';
931
- }
932
- const ariaLabel = this.getAttribute("aria-label");
933
- if (ariaLabel && ariaLabel.length < 50) {
934
- return '[aria-label="' + escape(ariaLabel) + '"]';
935
- }
936
- const buildPathSelector = (start) => {
937
- const parts = [];
938
- let current = start;
939
- while (current && current.nodeType === Node.ELEMENT_NODE) {
940
- let selector = current.nodeName.toLowerCase();
941
- if (current.id) {
942
- selector += "#" + escape(current.id);
943
- parts.unshift(selector);
944
- break;
945
- }
946
- const parentEl = current.parentElement;
947
- if (!parentEl) {
948
- parts.unshift(selector);
949
- break;
950
- }
951
- let index = 1;
952
- let sibling = current;
953
- while (sibling && sibling.previousElementSibling) {
954
- sibling = sibling.previousElementSibling;
955
- index += 1;
956
- }
957
- selector += ":nth-child(" + index + ")";
958
- parts.unshift(selector);
959
- current = parentEl;
960
- }
961
- return parts.join(" > ");
962
- };
963
- return buildPathSelector(this);
964
- };
965
- var SELECTOR_FUNCTION = selectorFunction.toString();
966
- async function buildSnapshot(session, mode, mainFrameOnly = true, maxNodes) {
967
- await session.send("Accessibility.enable");
968
- await session.send("DOM.enable");
969
- const result = await session.send("Accessibility.getFullAXTree");
970
- const nodes = Array.isArray(result.nodes) ? result.nodes : [];
971
- const entries = [];
972
- const lines = [];
973
- const warnings = [];
974
- const maxEntries = typeof maxNodes === "number" ? maxNodes : DEFAULT_MAX_AX_NODES;
975
- let skippedFrameCount = 0;
976
- for (const node of nodes) {
977
- if (entries.length >= maxEntries) break;
978
- if (node.ignored) continue;
979
- if (typeof node.backendDOMNodeId !== "number") continue;
980
- if (mainFrameOnly && node.frameId) {
981
- skippedFrameCount += 1;
982
- continue;
983
- }
984
- const role = extractValue(node.role) || extractValue(node.chromeRole);
985
- if (!role) continue;
986
- if (!shouldInclude(role, mode)) continue;
987
- const selector = await resolveSelector(session, node.backendDOMNodeId);
988
- if (!selector) continue;
989
- const ref = `r${entries.length + 1}`;
990
- const name = redactText2(extractValue(node.name));
991
- const value = redactText2(extractValue(node.value));
992
- const disabled = isTruthyProperty(node.properties, "disabled");
993
- const checked = isTruthyProperty(node.properties, "checked");
994
- entries.push({
995
- ref,
996
- selector,
997
- backendNodeId: node.backendDOMNodeId,
998
- frameId: node.frameId,
999
- role,
1000
- name
1001
- });
1002
- lines.push(formatNode({
1003
- ref,
1004
- role,
1005
- name,
1006
- value,
1007
- disabled,
1008
- checked
1009
- }));
1010
- }
1011
- if (mainFrameOnly && skippedFrameCount > 0) {
1012
- warnings.push(`Skipped ${skippedFrameCount} iframe nodes; snapshot limited to main frame.`);
1013
- }
1014
- return { entries, lines, warnings };
1015
- }
1016
- async function resolveSelector(session, backendNodeId) {
1017
- const resolved = await session.send("DOM.resolveNode", { backendNodeId });
1018
- const objectId = resolved.object?.objectId;
1019
- if (!objectId) return null;
1020
- const result = await session.send("Runtime.callFunctionOn", {
1021
- objectId,
1022
- functionDeclaration: SELECTOR_FUNCTION,
1023
- returnByValue: true
1024
- });
1025
- const selector = result.result?.value;
1026
- if (typeof selector !== "string" || selector.trim().length === 0) {
1027
- return null;
1028
- }
1029
- return selector;
1030
- }
1031
- function shouldInclude(role, mode) {
1032
- const normalized = role.toLowerCase();
1033
- if (ACTIONABLE_ROLES.has(normalized)) return true;
1034
- if (mode === "actionables") return false;
1035
- return SEMANTIC_ROLES.has(normalized);
1036
- }
1037
- function parseCursor(cursor) {
1038
- if (!cursor) return 0;
1039
- const value = Number(cursor);
1040
- if (!Number.isFinite(value) || value < 0) return 0;
1041
- return Math.floor(value);
1042
- }
1043
- function paginate(lines, startIndex, maxChars) {
1044
- let total = 0;
1045
- const parts = [];
1046
- let idx = startIndex;
1047
- while (idx < lines.length) {
1048
- const line = lines[idx];
1049
- if (line === void 0) {
1050
- break;
1051
- }
1052
- if (total + line.length + 1 > maxChars && parts.length > 0) {
1053
- break;
1054
- }
1055
- parts.push(line);
1056
- total += line.length + 1;
1057
- idx += 1;
1058
- }
1059
- const truncated = idx < lines.length;
1060
- const nextCursor = truncated ? String(idx) : void 0;
1061
- return {
1062
- content: parts.join("\n"),
1063
- truncated,
1064
- nextCursor
1065
- };
1066
- }
1067
- function formatNode(node) {
1068
- const name = redactText2(node.name || "");
1069
- const value = redactText2(node.value || "");
1070
- const parts = [];
1071
- parts.push(`[${node.ref}]`);
1072
- parts.push(node.role);
1073
- if (node.disabled) {
1074
- parts.push("disabled");
1075
- }
1076
- if (node.checked) {
1077
- parts.push("checked");
1078
- }
1079
- if (name) {
1080
- parts.push(`"${name}"`);
1081
- }
1082
- if (value) {
1083
- parts.push(`value="${value}"`);
1084
- }
1085
- return parts.join(" ");
1086
- }
1087
- function redactText2(text) {
1088
- const trimmed = (text ?? "").trim();
1089
- if (!trimmed) return "";
1090
- return trimmed.replace(/[A-Za-z0-9+/_-]{24,}/g, "[redacted]");
1091
- }
1092
- function extractValue(value) {
1093
- if (!value || typeof value.value === "undefined" || value.value === null) return "";
1094
- if (typeof value.value === "string") return value.value;
1095
- if (typeof value.value === "number" || typeof value.value === "boolean") {
1096
- return String(value.value);
1097
- }
1098
- return "";
1099
- }
1100
- function isTruthyProperty(properties, name) {
1101
- if (!properties) return false;
1102
- const found = properties.find((prop) => prop.name === name);
1103
- if (!found || !found.value) return false;
1104
- const value = found.value.value;
1105
- if (typeof value === "boolean") return value;
1106
- if (typeof value === "string") return value.toLowerCase() === "true";
1107
- if (typeof value === "number") return value !== 0;
1108
- return false;
1109
- }
1110
-
1111
- // src/browser/session-store.ts
1112
- var SessionStore = class {
1113
- sessions = /* @__PURE__ */ new Map();
1114
- add(session) {
1115
- this.sessions.set(session.id, session);
1116
- }
1117
- get(sessionId) {
1118
- const session = this.sessions.get(sessionId);
1119
- if (!session) {
1120
- throw new Error(`Unknown sessionId: ${sessionId}`);
1121
- }
1122
- return session;
1123
- }
1124
- has(sessionId) {
1125
- return this.sessions.has(sessionId);
1126
- }
1127
- delete(sessionId) {
1128
- this.sessions.delete(sessionId);
1129
- }
1130
- list() {
1131
- return Array.from(this.sessions.values());
1132
- }
1133
- };
1134
-
1135
- // src/browser/target-manager.ts
1136
- import { randomUUID as randomUUID2 } from "crypto";
1137
- var TargetManager = class {
1138
- targets = /* @__PURE__ */ new Map();
1139
- activeTargetId = null;
1140
- nameToTarget = /* @__PURE__ */ new Map();
1141
- targetToName = /* @__PURE__ */ new Map();
1142
- registerPage(page, name) {
1143
- const targetId = randomUUID2();
1144
- this.targets.set(targetId, page);
1145
- if (!this.activeTargetId) {
1146
- this.activeTargetId = targetId;
1147
- }
1148
- if (name) {
1149
- this.setName(targetId, name);
1150
- }
1151
- return targetId;
1152
- }
1153
- registerExistingPages(pages) {
1154
- for (const page of pages) {
1155
- this.registerPage(page);
1156
- }
1157
- }
1158
- setName(targetId, name) {
1159
- const trimmed = name.trim();
1160
- if (!trimmed) {
1161
- throw new Error("Name must be non-empty");
1162
- }
1163
- if (!this.targets.has(targetId)) {
1164
- throw new Error(`Unknown targetId: ${targetId}`);
1165
- }
1166
- const existing = this.nameToTarget.get(trimmed);
1167
- if (existing && existing !== targetId) {
1168
- throw new Error(`Name already in use: ${trimmed}`);
1169
- }
1170
- const previousName = this.targetToName.get(targetId);
1171
- if (previousName && previousName !== trimmed) {
1172
- this.nameToTarget.delete(previousName);
1173
- }
1174
- this.nameToTarget.set(trimmed, targetId);
1175
- this.targetToName.set(targetId, trimmed);
1176
- }
1177
- getTargetIdByName(name) {
1178
- return this.nameToTarget.get(name.trim()) ?? null;
1179
- }
1180
- getName(targetId) {
1181
- return this.targetToName.get(targetId) ?? null;
1182
- }
1183
- listNamedTargets() {
1184
- return Array.from(this.nameToTarget.entries()).map(([name, targetId]) => ({
1185
- name,
1186
- targetId
1187
- }));
1188
- }
1189
- removeName(name) {
1190
- const trimmed = name.trim();
1191
- const targetId = this.nameToTarget.get(trimmed);
1192
- if (targetId) {
1193
- this.nameToTarget.delete(trimmed);
1194
- this.targetToName.delete(targetId);
1195
- }
1196
- }
1197
- setActiveTarget(targetId) {
1198
- if (!this.targets.has(targetId)) {
1199
- throw new Error(`Unknown targetId: ${targetId}`);
1200
- }
1201
- this.activeTargetId = targetId;
1202
- }
1203
- getActiveTargetId() {
1204
- return this.activeTargetId;
1205
- }
1206
- getActivePage() {
1207
- if (!this.activeTargetId) {
1208
- throw new Error("No active target");
1209
- }
1210
- const page = this.targets.get(this.activeTargetId);
1211
- if (!page) {
1212
- throw new Error(`Missing active target: ${this.activeTargetId}`);
1213
- }
1214
- return page;
1215
- }
1216
- getPage(targetId) {
1217
- const page = this.targets.get(targetId);
1218
- if (!page) {
1219
- throw new Error(`Unknown targetId: ${targetId}`);
1220
- }
1221
- return page;
1222
- }
1223
- async listTargets(includeUrls = false) {
1224
- const entries = Array.from(this.targets.entries());
1225
- return Promise.all(entries.map(async ([targetId, page]) => {
1226
- const info = {
1227
- targetId,
1228
- title: void 0,
1229
- url: void 0,
1230
- type: "page"
1231
- };
1232
- try {
1233
- info.title = await page.title();
1234
- } catch {
1235
- info.title = void 0;
1236
- }
1237
- if (includeUrls) {
1238
- try {
1239
- info.url = page.url();
1240
- } catch {
1241
- info.url = void 0;
1242
- }
1243
- }
1244
- return info;
1245
- }));
1246
- }
1247
- async closeTarget(targetId) {
1248
- const page = this.getPage(targetId);
1249
- let closeError;
1250
- try {
1251
- await page.close();
1252
- } catch (error) {
1253
- closeError = error;
1254
- } finally {
1255
- this.targets.delete(targetId);
1256
- const name = this.targetToName.get(targetId);
1257
- if (name) {
1258
- this.nameToTarget.delete(name);
1259
- this.targetToName.delete(targetId);
1260
- }
1261
- if (this.activeTargetId === targetId) {
1262
- const remaining = Array.from(this.targets.keys());
1263
- this.activeTargetId = remaining[0] ?? null;
1264
- }
1265
- }
1266
- if (closeError) {
1267
- throw closeError;
1268
- }
1269
- }
1270
- listPageEntries() {
1271
- return Array.from(this.targets.entries()).map(([targetId, page]) => ({
1272
- targetId,
1273
- page
1274
- }));
1275
- }
1276
- };
1277
-
1278
- // src/browser/browser-manager.ts
1279
- var BrowserManager = class {
1280
- store = new SessionStore();
1281
- sessions = /* @__PURE__ */ new Map();
1282
- sessionMutexes = /* @__PURE__ */ new Map();
1283
- worktree;
1284
- config;
1285
- pageListeners = /* @__PURE__ */ new WeakMap();
1286
- constructor(worktree, config) {
1287
- this.worktree = worktree;
1288
- this.config = config;
1289
- }
1290
- getMutex(sessionId) {
1291
- let mutex = this.sessionMutexes.get(sessionId);
1292
- if (!mutex) {
1293
- mutex = new Mutex();
1294
- this.sessionMutexes.set(sessionId, mutex);
1295
- }
1296
- return mutex;
1297
- }
1298
- updateConfig(config) {
1299
- this.config = config;
1300
- for (const managed of this.sessions.values()) {
1301
- managed.consoleTracker.setOptions({ showFullConsole: config.devtools.showFullConsole });
1302
- managed.networkTracker.setOptions({ showFullUrls: config.devtools.showFullUrls });
1303
- }
1304
- }
1305
- async launch(options) {
1306
- const resolvedProfile = options.profile ?? this.config.profile;
1307
- const resolvedHeadless = options.headless ?? this.config.headless;
1308
- const persistProfile = options.persistProfile ?? this.config.persistProfile;
1309
- const cachePaths = await resolveCachePaths(this.worktree, resolvedProfile);
1310
- const executable = await findChromeExecutable(options.chromePath ?? this.config.chromePath);
1311
- const warnings = [];
1312
- let executablePath = executable;
1313
- if (!executablePath) {
1314
- const download = await downloadChromeForTesting(cachePaths.chromeDir);
1315
- warnings.push("System Chrome not found. Downloaded Chrome for Testing.");
1316
- executablePath = download.executablePath;
1317
- }
1318
- const profileDir = persistProfile ? cachePaths.profileDir : join4(cachePaths.projectRoot, "temp-profiles", randomUUID3());
1319
- await mkdir2(profileDir, { recursive: true });
1320
- let context = null;
1321
- try {
1322
- context = await chromium.launchPersistentContext(profileDir, {
1323
- headless: resolvedHeadless,
1324
- executablePath: executablePath ?? void 0,
1325
- args: options.flags ?? this.config.flags,
1326
- viewport: null
1327
- });
1328
- const browser = context.browser();
1329
- if (!browser) {
1330
- throw new Error("Browser instance unavailable");
1331
- }
1332
- const sessionId = randomUUID3();
1333
- const targets = new TargetManager();
1334
- const pages = context.pages();
1335
- if (pages.length === 0) {
1336
- const page = await context.newPage();
1337
- targets.registerPage(page);
1338
- } else {
1339
- targets.registerExistingPages(pages);
1340
- }
1341
- const activeTargetId = targets.getActiveTargetId();
1342
- if (options.startUrl && activeTargetId) {
1343
- await this.goto(sessionId, options.startUrl, "load", 3e4, { browser, context, targets });
1344
- }
1345
- const refStore = new RefStore();
1346
- const snapshotter = new Snapshotter(refStore);
1347
- const consoleTracker = new ConsoleTracker(200, { showFullConsole: this.config.devtools.showFullConsole });
1348
- const networkTracker = new NetworkTracker(300, { showFullUrls: this.config.devtools.showFullUrls });
1349
- const managed = {
1350
- sessionId,
1351
- mode: "A",
1352
- browser,
1353
- context,
1354
- profileDir,
1355
- persistProfile,
1356
- targets,
1357
- refStore,
1358
- snapshotter,
1359
- consoleTracker,
1360
- networkTracker
1361
- };
1362
- this.store.add({ id: sessionId, mode: "A", browser, context });
1363
- this.sessions.set(sessionId, managed);
1364
- this.attachTrackers(managed);
1365
- this.attachRefInvalidation(managed);
1366
- const wsEndpointProvider = browser;
1367
- const wsEndpoint = typeof wsEndpointProvider.wsEndpoint === "function" ? wsEndpointProvider.wsEndpoint() : void 0;
1368
- return { sessionId, mode: "A", activeTargetId, warnings, wsEndpoint: wsEndpoint || void 0 };
1369
- } catch (error) {
1370
- const launchMessage = error instanceof Error ? error.message : "Unknown error";
1371
- const cleanupErrors = [];
1372
- if (context) {
1373
- try {
1374
- await context.close();
1375
- } catch (closeError) {
1376
- cleanupErrors.push(closeError);
1377
- }
1378
- }
1379
- if (!persistProfile) {
1380
- try {
1381
- await rm(profileDir, { recursive: true, force: true, maxRetries: 5, retryDelay: 100 });
1382
- } catch (cleanupError) {
1383
- cleanupErrors.push(cleanupError);
1384
- }
1385
- }
1386
- if (cleanupErrors.length > 0) {
1387
- throw new AggregateError(
1388
- [error, ...cleanupErrors],
1389
- `Failed to launch browser context: ${launchMessage}. Cleanup failed.`
1390
- );
1391
- }
1392
- throw new Error(`Failed to launch browser context: ${launchMessage}`, { cause: error });
1393
- }
1394
- }
1395
- async connect(options) {
1396
- const wsEndpoint = await this.resolveWsEndpoint(options);
1397
- return this.connectWithEndpoint(wsEndpoint, "B");
1398
- }
1399
- async connectRelay(wsEndpoint) {
1400
- this.ensureLocalEndpoint(wsEndpoint);
1401
- return this.connectWithEndpoint(wsEndpoint, "C");
1402
- }
1403
- async closeAll() {
1404
- const sessions = Array.from(this.sessions.keys());
1405
- await Promise.allSettled(sessions.map((id) => this.disconnect(id, true)));
1406
- }
1407
- async disconnect(sessionId, closeBrowser = false) {
1408
- const managed = this.getManaged(sessionId);
1409
- const cleanupErrors = [];
1410
- try {
1411
- for (const entry of managed.targets.listPageEntries()) {
1412
- const cleanup = this.pageListeners.get(entry.page);
1413
- if (cleanup) {
1414
- try {
1415
- cleanup();
1416
- } catch (error) {
1417
- cleanupErrors.push(error);
1418
- }
1419
- this.pageListeners.delete(entry.page);
1420
- }
1421
- }
1422
- try {
1423
- if (closeBrowser) {
1424
- await managed.browser.close();
1425
- } else {
1426
- await managed.context.close();
1427
- }
1428
- } catch (error) {
1429
- cleanupErrors.push(error);
1430
- }
1431
- try {
1432
- managed.consoleTracker.detach();
1433
- } catch (error) {
1434
- cleanupErrors.push(error);
1435
- }
1436
- try {
1437
- managed.networkTracker.detach();
1438
- } catch (error) {
1439
- cleanupErrors.push(error);
1440
- }
1441
- if (!managed.persistProfile && managed.profileDir) {
1442
- try {
1443
- await rm(managed.profileDir, { recursive: true, force: true, maxRetries: 5, retryDelay: 100 });
1444
- } catch (error) {
1445
- cleanupErrors.push(error);
1446
- }
1447
- }
1448
- } finally {
1449
- this.sessions.delete(sessionId);
1450
- this.sessionMutexes.delete(sessionId);
1451
- this.store.delete(sessionId);
1452
- }
1453
- if (cleanupErrors.length === 1) {
1454
- throw cleanupErrors[0];
1455
- }
1456
- if (cleanupErrors.length > 1) {
1457
- throw new AggregateError(cleanupErrors, "Failed to disconnect browser session.");
1458
- }
1459
- }
1460
- async status(sessionId) {
1461
- const managed = this.getManaged(sessionId);
1462
- const activeTargetId = managed.targets.getActiveTargetId();
1463
- const page = activeTargetId ? managed.targets.getPage(activeTargetId) : null;
1464
- const title = await this.safePageTitle(page, "BrowserManager.status");
1465
- const url = this.safePageUrl(page, "BrowserManager.status");
1466
- return {
1467
- mode: managed.mode,
1468
- activeTargetId,
1469
- url,
1470
- title
1471
- };
1472
- }
1473
- async listTargets(sessionId, includeUrls = false) {
1474
- const managed = this.getManaged(sessionId);
1475
- const targets = await managed.targets.listTargets(includeUrls);
1476
- return {
1477
- activeTargetId: managed.targets.getActiveTargetId(),
1478
- targets
1479
- };
1480
- }
1481
- async page(sessionId, name, url) {
1482
- const managed = this.getManaged(sessionId);
1483
- const existingTargetId = managed.targets.getTargetIdByName(name);
1484
- let targetId = existingTargetId;
1485
- let created = false;
1486
- if (targetId) {
1487
- managed.targets.setActiveTarget(targetId);
1488
- } else {
1489
- const page2 = await managed.context.newPage();
1490
- targetId = managed.targets.registerPage(page2, name);
1491
- managed.targets.setActiveTarget(targetId);
1492
- this.attachRefInvalidationForPage(managed, targetId, page2);
1493
- created = true;
1494
- }
1495
- this.attachTrackers(managed);
1496
- if (url) {
1497
- await this.goto(sessionId, url, "load", 3e4);
1498
- }
1499
- const page = managed.targets.getPage(targetId);
1500
- const title = await this.safePageTitle(page, "BrowserManager.page");
1501
- const finalUrl = this.safePageUrl(page, "BrowserManager.page");
1502
- return { targetId, created, url: finalUrl, title };
1503
- }
1504
- async listPages(sessionId) {
1505
- const managed = this.getManaged(sessionId);
1506
- const named = managed.targets.listNamedTargets();
1507
- const pages = [];
1508
- for (const entry of named) {
1509
- const page = managed.targets.getPage(entry.targetId);
1510
- const title = await this.safePageTitle(page, "BrowserManager.listPages");
1511
- const url = this.safePageUrl(page, "BrowserManager.listPages");
1512
- pages.push({ name: entry.name, targetId: entry.targetId, url, title });
1513
- }
1514
- return { pages };
1515
- }
1516
- async closePage(sessionId, name) {
1517
- const managed = this.getManaged(sessionId);
1518
- const targetId = managed.targets.getTargetIdByName(name);
1519
- if (!targetId) {
1520
- throw new Error(`Unknown page name: ${name}`);
1521
- }
1522
- await managed.targets.closeTarget(targetId);
1523
- managed.refStore.clearTarget(targetId);
1524
- this.attachTrackers(managed);
1525
- }
1526
- async useTarget(sessionId, targetId) {
1527
- const managed = this.getManaged(sessionId);
1528
- managed.targets.setActiveTarget(targetId);
1529
- this.attachTrackers(managed);
1530
- const page = managed.targets.getPage(targetId);
1531
- const title = await this.safePageTitle(page, "BrowserManager.useTarget");
1532
- return {
1533
- activeTargetId: targetId,
1534
- url: this.safePageUrl(page, "BrowserManager.useTarget"),
1535
- title
1536
- };
1537
- }
1538
- async newTarget(sessionId, url) {
1539
- const managed = this.getManaged(sessionId);
1540
- const page = await managed.context.newPage();
1541
- const targetId = managed.targets.registerPage(page);
1542
- managed.targets.setActiveTarget(targetId);
1543
- this.attachRefInvalidationForPage(managed, targetId, page);
1544
- if (url) {
1545
- await page.goto(url, { waitUntil: "load" });
1546
- }
1547
- this.attachTrackers(managed);
1548
- return { targetId };
1549
- }
1550
- async closeTarget(sessionId, targetId) {
1551
- const managed = this.getManaged(sessionId);
1552
- await managed.targets.closeTarget(targetId);
1553
- managed.refStore.clearTarget(targetId);
1554
- this.attachTrackers(managed);
1555
- }
1556
- async goto(sessionId, url, waitUntil = "load", timeoutMs = 3e4, sessionOverride) {
1557
- const startTime = Date.now();
1558
- const managed = sessionOverride ? this.buildOverrideSession(sessionOverride) : this.getManaged(sessionId);
1559
- const page = managed.targets.getActivePage();
1560
- const response = await page.goto(url, { waitUntil, timeout: timeoutMs });
1561
- return {
1562
- finalUrl: page.url(),
1563
- status: response?.status(),
1564
- timingMs: Date.now() - startTime
1565
- };
1566
- }
1567
- async waitForLoad(sessionId, until, timeoutMs = 3e4) {
1568
- const startTime = Date.now();
1569
- const managed = this.getManaged(sessionId);
1570
- const page = managed.targets.getActivePage();
1571
- await page.waitForLoadState(until, { timeout: timeoutMs });
1572
- return { timingMs: Date.now() - startTime };
1573
- }
1574
- async waitForRef(sessionId, ref, state = "attached", timeoutMs = 3e4) {
1575
- const startTime = Date.now();
1576
- const managed = this.getManaged(sessionId);
1577
- const selector = this.resolveSelector(managed, ref);
1578
- const page = managed.targets.getActivePage();
1579
- await page.locator(selector).waitFor({ state, timeout: timeoutMs });
1580
- return { timingMs: Date.now() - startTime };
1581
- }
1582
- async snapshot(sessionId, mode, maxChars, cursor) {
1583
- const mutex = this.getMutex(sessionId);
1584
- return mutex.runExclusive(async () => {
1585
- const managed = this.getManaged(sessionId);
1586
- const targetId = managed.targets.getActiveTargetId();
1587
- if (!targetId) {
1588
- throw new Error("No active target for snapshot");
1589
- }
1590
- const page = managed.targets.getActivePage();
1591
- return managed.snapshotter.snapshot(page, targetId, {
1592
- mode,
1593
- maxChars,
1594
- cursor,
1595
- maxNodes: this.config.snapshot.maxNodes
1596
- });
1597
- });
1598
- }
1599
- async click(sessionId, ref) {
1600
- const mutex = this.getMutex(sessionId);
1601
- return mutex.runExclusive(async () => {
1602
- const startTime = Date.now();
1603
- const managed = this.getManaged(sessionId);
1604
- const selector = this.resolveSelector(managed, ref);
1605
- const page = managed.targets.getActivePage();
1606
- const previousUrl = page.url();
1607
- await page.locator(selector).click();
1608
- const navigated = page.url() !== previousUrl;
1609
- return { timingMs: Date.now() - startTime, navigated };
1610
- });
1611
- }
1612
- async type(sessionId, ref, text, clear = false, submit = false) {
1613
- const mutex = this.getMutex(sessionId);
1614
- return mutex.runExclusive(async () => {
1615
- const startTime = Date.now();
1616
- const managed = this.getManaged(sessionId);
1617
- const selector = this.resolveSelector(managed, ref);
1618
- const locator = managed.targets.getActivePage().locator(selector);
1619
- if (clear) {
1620
- await locator.fill("");
1621
- }
1622
- await locator.fill(text);
1623
- if (submit) {
1624
- await locator.press("Enter");
1625
- }
1626
- return { timingMs: Date.now() - startTime };
1627
- });
1628
- }
1629
- async select(sessionId, ref, values) {
1630
- const managed = this.getManaged(sessionId);
1631
- const selector = this.resolveSelector(managed, ref);
1632
- await managed.targets.getActivePage().locator(selector).selectOption(values);
1633
- }
1634
- async scroll(sessionId, dy, ref) {
1635
- const managed = this.getManaged(sessionId);
1636
- const page = managed.targets.getActivePage();
1637
- if (ref) {
1638
- const selector = this.resolveSelector(managed, ref);
1639
- await page.locator(selector).evaluate((el, delta) => {
1640
- el.scrollBy(0, delta);
1641
- }, dy);
1642
- } else {
1643
- await page.mouse.wheel(0, dy);
1644
- }
1645
- }
1646
- async domGetHtml(sessionId, ref, maxChars = 8e3) {
1647
- const managed = this.getManaged(sessionId);
1648
- const selector = this.resolveSelector(managed, ref);
1649
- const html = await managed.targets.getActivePage().$eval(selector, (el) => el.outerHTML);
1650
- return truncateHtml(html, maxChars);
1651
- }
1652
- async domGetText(sessionId, ref, maxChars = 8e3) {
1653
- const managed = this.getManaged(sessionId);
1654
- const selector = this.resolveSelector(managed, ref);
1655
- const text = await managed.targets.getActivePage().$eval(selector, (el) => el.innerText || el.textContent || "");
1656
- return truncateText(text, maxChars);
1657
- }
1658
- async clonePage(sessionId) {
1659
- const managed = this.getManaged(sessionId);
1660
- const page = managed.targets.getActivePage();
1661
- const allowUnsafeExport = this.config.security.allowUnsafeExport;
1662
- const exportConfig = this.config.export;
1663
- const capture = await captureDom(page, "body", {
1664
- sanitize: !allowUnsafeExport,
1665
- maxNodes: exportConfig.maxNodes,
1666
- inlineStyles: exportConfig.inlineStyles
1667
- });
1668
- const css = extractCss(capture);
1669
- return emitReactComponent(capture, css, { allowUnsafeExport });
1670
- }
1671
- async cloneComponent(sessionId, ref) {
1672
- const managed = this.getManaged(sessionId);
1673
- const selector = this.resolveSelector(managed, ref);
1674
- const allowUnsafeExport = this.config.security.allowUnsafeExport;
1675
- const exportConfig = this.config.export;
1676
- const capture = await captureDom(managed.targets.getActivePage(), selector, {
1677
- sanitize: !allowUnsafeExport,
1678
- maxNodes: exportConfig.maxNodes,
1679
- inlineStyles: exportConfig.inlineStyles
1680
- });
1681
- const css = extractCss(capture);
1682
- return emitReactComponent(capture, css, { allowUnsafeExport });
1683
- }
1684
- async perfMetrics(sessionId) {
1685
- const managed = this.getManaged(sessionId);
1686
- const page = managed.targets.getActivePage();
1687
- const session = await managed.context.newCDPSession(page);
1688
- const result = await session.send("Performance.getMetrics");
1689
- await session.detach();
1690
- const metrics = Array.isArray(result.metrics) ? result.metrics : [];
1691
- return { metrics };
1692
- }
1693
- async screenshot(sessionId, path2) {
1694
- const managed = this.getManaged(sessionId);
1695
- const page = managed.targets.getActivePage();
1696
- if (path2) {
1697
- await page.screenshot({ path: path2, type: "png" });
1698
- return { path: path2 };
1699
- }
1700
- const buffer = await page.screenshot({ type: "png" });
1701
- return { base64: buffer.toString("base64") };
1702
- }
1703
- consolePoll(sessionId, sinceSeq, max = 50) {
1704
- const managed = this.getManaged(sessionId);
1705
- return managed.consoleTracker.poll(sinceSeq, max);
1706
- }
1707
- networkPoll(sessionId, sinceSeq, max = 50) {
1708
- const managed = this.getManaged(sessionId);
1709
- return managed.networkTracker.poll(sinceSeq, max);
1710
- }
1711
- buildOverrideSession(input) {
1712
- const refStore = new RefStore();
1713
- return {
1714
- sessionId: "override",
1715
- mode: "A",
1716
- browser: input.browser,
1717
- context: input.context,
1718
- profileDir: "",
1719
- persistProfile: true,
1720
- targets: input.targets,
1721
- refStore,
1722
- snapshotter: new Snapshotter(refStore),
1723
- consoleTracker: new ConsoleTracker(200, { showFullConsole: this.config.devtools.showFullConsole }),
1724
- networkTracker: new NetworkTracker(300, { showFullUrls: this.config.devtools.showFullUrls })
1725
- };
1726
- }
1727
- getManaged(sessionId) {
1728
- const managed = this.sessions.get(sessionId);
1729
- if (!managed) {
1730
- throw new Error(`Unknown sessionId: ${sessionId}`);
1731
- }
1732
- return managed;
1733
- }
1734
- resolveSelector(managed, ref) {
1735
- const targetId = managed.targets.getActiveTargetId();
1736
- if (!targetId) {
1737
- throw new Error("No active target for ref resolution");
1738
- }
1739
- const entry = managed.refStore.resolve(targetId, ref);
1740
- if (!entry) {
1741
- throw new Error(`Unknown ref: ${ref}. Take a new snapshot first.`);
1742
- }
1743
- return entry.selector;
1744
- }
1745
- async safePageTitle(page, context) {
1746
- if (!page) return void 0;
1747
- try {
1748
- return await page.title();
1749
- } catch {
1750
- console.warn(`${context}: failed to read page title`);
1751
- return void 0;
1752
- }
1753
- }
1754
- safePageUrl(page, context) {
1755
- if (!page) return void 0;
1756
- try {
1757
- return page.url();
1758
- } catch {
1759
- console.warn(`${context}: failed to read page url`);
1760
- return void 0;
1761
- }
1762
- }
1763
- attachTrackers(managed) {
1764
- const activeTargetId = managed.targets.getActiveTargetId();
1765
- if (!activeTargetId) return;
1766
- const page = managed.targets.getActivePage();
1767
- managed.consoleTracker.attach(page);
1768
- managed.networkTracker.attach(page);
1769
- }
1770
- attachRefInvalidation(managed) {
1771
- const entries = managed.targets.listPageEntries();
1772
- for (const entry of entries) {
1773
- this.attachRefInvalidationForPage(managed, entry.targetId, entry.page);
1774
- }
1775
- }
1776
- attachRefInvalidationForPage(managed, targetId, page) {
1777
- if (this.pageListeners.has(page)) return;
1778
- const onNavigate = (frame) => {
1779
- if (frame.parentFrame() === null) {
1780
- managed.refStore.clearTarget(targetId);
1781
- }
1782
- };
1783
- const onClose = () => {
1784
- managed.refStore.clearTarget(targetId);
1785
- };
1786
- page.on("framenavigated", onNavigate);
1787
- page.on("close", onClose);
1788
- this.pageListeners.set(page, () => {
1789
- page.off("framenavigated", onNavigate);
1790
- page.off("close", onClose);
1791
- });
1792
- }
1793
- async resolveWsEndpoint(options) {
1794
- if (options.wsEndpoint) {
1795
- this.ensureLocalEndpoint(options.wsEndpoint);
1796
- return options.wsEndpoint;
1797
- }
1798
- const host = options.host ?? "127.0.0.1";
1799
- const port = options.port ?? 9222;
1800
- const url = `http://${host}:${port}/json/version`;
1801
- this.ensureLocalEndpoint(url);
1802
- const response = await fetch(url);
1803
- if (!response.ok) {
1804
- throw new Error(`Failed to fetch CDP endpoint from ${url}`);
1805
- }
1806
- const data = await response.json();
1807
- if (!data.webSocketDebuggerUrl) {
1808
- throw new Error("webSocketDebuggerUrl missing from /json/version response");
1809
- }
1810
- this.ensureLocalEndpoint(data.webSocketDebuggerUrl);
1811
- return data.webSocketDebuggerUrl;
1812
- }
1813
- ensureLocalEndpoint(endpoint) {
1814
- if (this.config.security.allowNonLocalCdp) return;
1815
- const ALLOWED_PROTOCOLS = /* @__PURE__ */ new Set(["ws:", "wss:", "http:", "https:"]);
1816
- const LOCAL_HOSTNAMES = /* @__PURE__ */ new Set(["localhost", "127.0.0.1", "::1", "[::1]"]);
1817
- let parsed;
1818
- try {
1819
- parsed = new URL(endpoint);
1820
- } catch {
1821
- throw new Error("Invalid CDP endpoint URL.");
1822
- }
1823
- if (!ALLOWED_PROTOCOLS.has(parsed.protocol)) {
1824
- throw new Error(`Disallowed protocol "${parsed.protocol}" for CDP endpoint. Allowed: ws, wss, http, https.`);
1825
- }
1826
- const hostname = parsed.hostname.toLowerCase();
1827
- if (!LOCAL_HOSTNAMES.has(hostname) && !hostname.toLowerCase().startsWith("::ffff:127.")) {
1828
- throw new Error("Non-local CDP endpoints are disabled by default.");
1829
- }
1830
- }
1831
- async connectWithEndpoint(wsEndpoint, mode) {
1832
- const browser = await chromium.connectOverCDP(wsEndpoint);
1833
- const contexts = browser.contexts();
1834
- const context = contexts[0] ?? await browser.newContext();
1835
- const sessionId = randomUUID3();
1836
- const targets = new TargetManager();
1837
- const pages = context.pages();
1838
- if (pages.length === 0) {
1839
- const page = await context.newPage();
1840
- targets.registerPage(page);
1841
- } else {
1842
- targets.registerExistingPages(pages);
1843
- }
1844
- const refStore = new RefStore();
1845
- const snapshotter = new Snapshotter(refStore);
1846
- const consoleTracker = new ConsoleTracker(200, { showFullConsole: this.config.devtools.showFullConsole });
1847
- const networkTracker = new NetworkTracker(300, { showFullUrls: this.config.devtools.showFullUrls });
1848
- const managed = {
1849
- sessionId,
1850
- mode,
1851
- browser,
1852
- context,
1853
- profileDir: "",
1854
- persistProfile: true,
1855
- targets,
1856
- refStore,
1857
- snapshotter,
1858
- consoleTracker,
1859
- networkTracker
1860
- };
1861
- this.store.add({ id: sessionId, mode, browser, context });
1862
- this.sessions.set(sessionId, managed);
1863
- this.attachTrackers(managed);
1864
- this.attachRefInvalidation(managed);
1865
- return { sessionId, mode, activeTargetId: targets.getActiveTargetId(), warnings: [], wsEndpoint };
1866
- }
1867
- };
1868
- function truncateHtml(value, maxChars) {
1869
- if (value.length <= maxChars) {
1870
- return { outerHTML: value, truncated: false };
1871
- }
1872
- return { outerHTML: value.slice(0, maxChars), truncated: true };
1873
- }
1874
- function truncateText(value, maxChars) {
1875
- if (value.length <= maxChars) {
1876
- return { text: value, truncated: false };
1877
- }
1878
- return { text: value.slice(0, maxChars), truncated: true };
1879
- }
1880
-
1881
- // src/browser/script-runner.ts
1882
- var ScriptRunner = class {
1883
- manager;
1884
- constructor(manager) {
1885
- this.manager = manager;
1886
- }
1887
- async run(sessionId, steps, stopOnError = true) {
1888
- const startTime = Date.now();
1889
- const results = [];
1890
- for (let i = 0; i < steps.length; i += 1) {
1891
- const step = steps[i];
1892
- if (!step) {
1893
- continue;
1894
- }
1895
- try {
1896
- const data = await this.executeStep(sessionId, step);
1897
- results.push({ i, ok: true, data });
1898
- } catch (error) {
1899
- results.push({
1900
- i,
1901
- ok: false,
1902
- error: { message: error instanceof Error ? error.message : "Unknown error" }
1903
- });
1904
- if (stopOnError) {
1905
- break;
1906
- }
1907
- }
1908
- }
1909
- return { results, timingMs: Date.now() - startTime };
1910
- }
1911
- async executeStep(sessionId, step) {
1912
- const args = step.args ?? {};
1913
- switch (step.action) {
1914
- case "goto":
1915
- return this.manager.goto(
1916
- sessionId,
1917
- requireString(args.url, "url"),
1918
- requireWaitUntil(args.waitUntil),
1919
- requireNumber(args.timeoutMs, 3e4)
1920
- );
1921
- case "wait":
1922
- if (typeof args.ref === "string") {
1923
- const ref = args.ref;
1924
- const state = requireState(args.state);
1925
- const timeoutMs = requireNumber(args.timeoutMs, 3e4);
1926
- return withRetry("wait", () => this.manager.waitForRef(
1927
- sessionId,
1928
- ref,
1929
- state,
1930
- timeoutMs
1931
- ));
1932
- }
1933
- return withRetry("wait", () => this.manager.waitForLoad(
1934
- sessionId,
1935
- requireWaitUntil(args.until),
1936
- requireNumber(args.timeoutMs, 3e4)
1937
- ));
1938
- case "snapshot":
1939
- return this.manager.snapshot(
1940
- sessionId,
1941
- requireSnapshotMode(args.format ?? args.mode),
1942
- requireNumber(args.maxChars, 16e3),
1943
- typeof args.cursor === "string" ? args.cursor : void 0
1944
- );
1945
- case "click":
1946
- return withRetry("click", () => this.manager.click(sessionId, requireString(args.ref, "ref")));
1947
- case "type":
1948
- return withRetry("type", () => this.manager.type(
1949
- sessionId,
1950
- requireString(args.ref, "ref"),
1951
- requireString(args.text, "text"),
1952
- Boolean(args.clear),
1953
- Boolean(args.submit)
1954
- ));
1955
- case "select":
1956
- return withRetry("select", () => this.manager.select(
1957
- sessionId,
1958
- requireString(args.ref, "ref"),
1959
- requireStringArray(args.values, "values")
1960
- ));
1961
- case "scroll":
1962
- return withRetry("scroll", () => this.manager.scroll(
1963
- sessionId,
1964
- requireNumber(args.dy, 0),
1965
- typeof args.ref === "string" ? args.ref : void 0
1966
- ));
1967
- case "dom_get_html":
1968
- return this.manager.domGetHtml(sessionId, requireString(args.ref, "ref"), requireNumber(args.maxChars, 8e3));
1969
- case "dom_get_text":
1970
- return this.manager.domGetText(sessionId, requireString(args.ref, "ref"), requireNumber(args.maxChars, 8e3));
1971
- default:
1972
- throw new Error(`Unknown action: ${step.action}`);
1973
- }
1974
- }
1975
- };
1976
- function requireString(value, label) {
1977
- if (typeof value !== "string" || !value.trim()) {
1978
- throw new Error(`Missing ${label}`);
1979
- }
1980
- return value;
1981
- }
1982
- function requireStringArray(value, label) {
1983
- if (!Array.isArray(value) || value.some((item) => typeof item !== "string")) {
1984
- throw new Error(`Invalid ${label}`);
1985
- }
1986
- return value;
1987
- }
1988
- function requireNumber(value, fallback) {
1989
- if (typeof value === "number" && Number.isFinite(value)) {
1990
- return value;
1991
- }
1992
- return fallback;
1993
- }
1994
- function requireWaitUntil(value) {
1995
- if (value === "domcontentloaded" || value === "load" || value === "networkidle") {
1996
- return value;
1997
- }
1998
- return "load";
1999
- }
2000
- function requireSnapshotMode(value) {
2001
- if (value === "actionables") return "actionables";
2002
- return "outline";
2003
- }
2004
- function requireState(value) {
2005
- if (value === "visible" || value === "hidden") return value;
2006
- return "attached";
2007
- }
2008
- var RETRY_ACTIONS = /* @__PURE__ */ new Set(["click", "type", "select", "scroll", "wait"]);
2009
- var RETRY_MAX_ATTEMPTS = 2;
2010
- var RETRY_BASE_DELAY_MS = 150;
2011
- var RETRY_MAX_DELAY_MS = 1e3;
2012
- async function withRetry(action, fn) {
2013
- if (!RETRY_ACTIONS.has(action)) {
2014
- return fn();
2015
- }
2016
- let attempt = 0;
2017
- let delay = RETRY_BASE_DELAY_MS;
2018
- while (true) {
2019
- try {
2020
- return await fn();
2021
- } catch (error) {
2022
- attempt += 1;
2023
- if (attempt >= RETRY_MAX_ATTEMPTS || !shouldRetry(error)) {
2024
- throw error;
2025
- }
2026
- await sleep(delay);
2027
- delay = Math.min(delay * 2, RETRY_MAX_DELAY_MS);
2028
- }
2029
- }
2030
- }
2031
- function shouldRetry(error) {
2032
- const message = error instanceof Error ? error.message : "";
2033
- if (!message) return true;
2034
- return !/missing|invalid|unknown ref|no active target/i.test(message);
2035
- }
2036
- function sleep(ms) {
2037
- return new Promise((resolve) => setTimeout(resolve, ms));
2038
- }
2039
-
2040
- // src/skills/skill-loader.ts
2041
- import { readFile, readdir } from "fs/promises";
2042
- import { join as join5 } from "path";
2043
- import * as os2 from "os";
2044
- var SkillLoader = class {
2045
- rootDir;
2046
- additionalPaths;
2047
- skillCache = null;
2048
- constructor(rootDir, additionalPaths = []) {
2049
- this.rootDir = rootDir;
2050
- this.additionalPaths = additionalPaths.map((p) => this.expandPath(p));
2051
- }
2052
- expandPath(p) {
2053
- if (p.startsWith("~")) {
2054
- return join5(os2.homedir(), p.slice(1));
2055
- }
2056
- return p;
2057
- }
2058
- async loadBestPractices(topic) {
2059
- return this.loadSkill("opendevbrowser-best-practices", topic);
2060
- }
2061
- async loadSkill(name, topic) {
2062
- const skills = await this.listSkills();
2063
- const skill = skills.find((s) => s.name === name);
2064
- if (!skill) {
2065
- const available = skills.map((s) => s.name).join(", ") || "none";
2066
- throw new Error(`Skill "${name}" not found. Available: ${available}`);
2067
- }
2068
- const content = await readFile(skill.path, "utf8");
2069
- const trimmed = content.trim();
2070
- if (!topic || !topic.trim()) {
2071
- return trimmed;
2072
- }
2073
- const filtered = filterSections(trimmed, topic);
2074
- return filtered || trimmed;
2075
- }
2076
- async listSkills() {
2077
- if (this.skillCache) {
2078
- return this.skillCache;
2079
- }
2080
- const skills = [];
2081
- const searchPaths = this.getSearchPaths();
2082
- for (const searchPath of searchPaths) {
2083
- const discovered = await this.discoverSkillsInPath(searchPath);
2084
- for (const skill of discovered) {
2085
- if (!skills.some((s) => s.name === skill.name)) {
2086
- skills.push(skill);
2087
- }
2088
- }
2089
- }
2090
- this.skillCache = skills;
2091
- return skills;
2092
- }
2093
- getSearchPaths() {
2094
- const configDir = process.env.OPENCODE_CONFIG_DIR || join5(os2.homedir(), ".config", "opencode");
2095
- const searchPaths = [
2096
- join5(this.rootDir, ".opencode", "skill"),
2097
- join5(configDir, "skill"),
2098
- join5(this.rootDir, ".claude", "skills"),
2099
- join5(os2.homedir(), ".claude", "skills"),
2100
- ...this.additionalPaths
2101
- ];
2102
- return Array.from(new Set(searchPaths));
2103
- }
2104
- async discoverSkillsInPath(searchPath) {
2105
- const skills = [];
2106
- try {
2107
- const entries = await readdir(searchPath, { withFileTypes: true });
2108
- for (const entry of entries) {
2109
- if (!entry.isDirectory()) continue;
2110
- const skillPath = join5(searchPath, entry.name, "SKILL.md");
2111
- try {
2112
- const content = await readFile(skillPath, "utf8");
2113
- const metadata = this.parseSkillMetadata(content, entry.name);
2114
- skills.push({
2115
- name: metadata.name,
2116
- description: metadata.description,
2117
- version: metadata.version ?? "1.0.0",
2118
- path: skillPath
2119
- });
2120
- } catch {
2121
- }
2122
- }
2123
- } catch {
2124
- }
2125
- return skills;
2126
- }
2127
- parseSkillMetadata(content, dirName) {
2128
- const frontmatterMatch = content.match(/^---\s*\n([\s\S]*?)\n---/);
2129
- if (!frontmatterMatch) {
2130
- return {
2131
- name: dirName,
2132
- description: this.extractFirstParagraph(content) || `Skill: ${dirName}`
2133
- };
2134
- }
2135
- const frontmatter = frontmatterMatch[1] ?? "";
2136
- const metadata = {
2137
- name: dirName,
2138
- description: ""
2139
- };
2140
- const nameMatch = frontmatter.match(/^name:\s*["']?([^"'\n]+)["']?\s*$/m);
2141
- if (nameMatch?.[1]) {
2142
- metadata.name = nameMatch[1].trim();
2143
- }
2144
- const descMatch = frontmatter.match(/^description:\s*["']?([^"'\n]+)["']?\s*$/m);
2145
- if (descMatch?.[1]) {
2146
- metadata.description = descMatch[1].trim();
2147
- }
2148
- const versionMatch = frontmatter.match(/^version:\s*["']?([^"'\n]+)["']?\s*$/m);
2149
- if (versionMatch?.[1]) {
2150
- metadata.version = versionMatch[1].trim();
2151
- }
2152
- if (!metadata.description) {
2153
- const afterFrontmatter = content.slice(frontmatterMatch[0].length);
2154
- metadata.description = this.extractFirstParagraph(afterFrontmatter) || `Skill: ${metadata.name}`;
2155
- }
2156
- return metadata;
2157
- }
2158
- extractFirstParagraph(content) {
2159
- const lines = content.trim().split(/\n/);
2160
- const paragraphLines = [];
2161
- for (const line of lines) {
2162
- const trimmedLine = line.trim();
2163
- if (trimmedLine.startsWith("#")) continue;
2164
- if (trimmedLine === "" && paragraphLines.length > 0) break;
2165
- if (trimmedLine !== "") {
2166
- paragraphLines.push(trimmedLine);
2167
- }
2168
- }
2169
- const paragraph = paragraphLines.join(" ").trim();
2170
- return paragraph.length > 0 ? paragraph.slice(0, 200) : null;
2171
- }
2172
- clearCache() {
2173
- this.skillCache = null;
2174
- }
2175
- };
2176
- function filterSections(content, topic) {
2177
- const normalized = topic.trim().toLowerCase();
2178
- const lines = content.split(/\r?\n/);
2179
- const sections = [];
2180
- let currentHeading = "";
2181
- let currentBody = [];
2182
- const flush = () => {
2183
- if (currentHeading || currentBody.length > 0) {
2184
- sections.push({ heading: currentHeading, body: [...currentBody] });
2185
- }
2186
- currentHeading = "";
2187
- currentBody = [];
2188
- };
2189
- for (const line of lines) {
2190
- const headingMatch = line.match(/^(#{1,3})\s+(.*)$/);
2191
- if (headingMatch) {
2192
- flush();
2193
- currentHeading = headingMatch[2]?.trim() ?? "";
2194
- currentBody.push(line);
2195
- continue;
2196
- }
2197
- currentBody.push(line);
2198
- }
2199
- flush();
2200
- const matches = sections.filter((section) => section.heading.toLowerCase().includes(normalized));
2201
- if (matches.length === 0) {
2202
- return null;
2203
- }
2204
- return matches.map((section) => section.body.join("\n")).join("\n\n");
2205
- }
2
+ createOpenDevBrowserCore,
3
+ extractExtension
4
+ } from "./chunk-WTFSMBVH.js";
2206
5
 
2207
6
  // src/skills/skill-nudge.ts
2208
7
  var SKILL_NUDGE_MARKER = "[opendevbrowser:skill-nudge]";
@@ -2272,335 +71,6 @@ function buildContinuityNudgeMessage(filePath) {
2272
71
  return `${CONTINUITY_NUDGE_MARKER} For long-running tasks, create or update ${target} at the repo root with Goal, Constraints/Assumptions, Key decisions, State (Done/Now/Next), Open questions, and Working set. Keep it brief.`;
2273
72
  }
2274
73
 
2275
- // src/relay/relay-server.ts
2276
- import { createServer } from "http";
2277
- import { timingSafeEqual } from "crypto";
2278
- import { WebSocket, WebSocketServer } from "ws";
2279
- var RelayServer = class _RelayServer {
2280
- running = false;
2281
- baseUrl = null;
2282
- port = null;
2283
- server = null;
2284
- extensionWss = null;
2285
- cdpWss = null;
2286
- extensionSocket = null;
2287
- cdpSocket = null;
2288
- extensionInfo = null;
2289
- pairingToken = null;
2290
- handshakeAttempts = /* @__PURE__ */ new Map();
2291
- cdpAllowlist = null;
2292
- static MAX_HANDSHAKE_ATTEMPTS = 5;
2293
- static RATE_LIMIT_WINDOW_MS = 6e4;
2294
- async start(port = 8787) {
2295
- if (this.running && this.baseUrl && this.port !== null) {
2296
- return { url: this.baseUrl, port: this.port };
2297
- }
2298
- this.server = createServer();
2299
- this.extensionWss = new WebSocketServer({ noServer: true });
2300
- this.cdpWss = new WebSocketServer({ noServer: true });
2301
- this.extensionWss.on("connection", (socket) => {
2302
- if (this.extensionSocket) {
2303
- this.extensionSocket.close(1e3, "Replaced by a new extension client");
2304
- }
2305
- this.extensionSocket = socket;
2306
- this.extensionInfo = null;
2307
- socket.on("message", (data) => {
2308
- this.handleExtensionMessage(data);
2309
- });
2310
- socket.on("close", () => {
2311
- if (this.extensionSocket === socket) {
2312
- this.extensionSocket = null;
2313
- this.extensionInfo = null;
2314
- }
2315
- if (this.cdpSocket) {
2316
- this.cdpSocket.close(1011, "Extension disconnected");
2317
- }
2318
- });
2319
- });
2320
- this.cdpWss.on("connection", (socket) => {
2321
- if (this.cdpSocket) {
2322
- socket.close(1008, "Only one CDP client supported");
2323
- return;
2324
- }
2325
- this.cdpSocket = socket;
2326
- socket.on("message", (data) => {
2327
- this.handleCdpMessage(data);
2328
- });
2329
- socket.on("close", () => {
2330
- if (this.cdpSocket === socket) {
2331
- this.cdpSocket = null;
2332
- }
2333
- });
2334
- });
2335
- this.server.on("request", (request, response) => {
2336
- const pathname = new URL(request.url ?? "", "http://127.0.0.1").pathname;
2337
- const origin = request.headers.origin;
2338
- if (pathname === "/pair" && request.method === "OPTIONS") {
2339
- if (origin && origin.startsWith("chrome-extension://")) {
2340
- response.setHeader("Access-Control-Allow-Origin", origin);
2341
- response.setHeader("Access-Control-Allow-Methods", "GET, OPTIONS");
2342
- response.setHeader("Access-Control-Allow-Headers", "Content-Type");
2343
- }
2344
- response.writeHead(204);
2345
- response.end();
2346
- return;
2347
- }
2348
- if (pathname === "/pair" && request.method === "GET") {
2349
- const isLocalhost = !origin || origin.startsWith("chrome-extension://");
2350
- if (!isLocalhost) {
2351
- response.writeHead(403, { "Content-Type": "application/json" });
2352
- response.end(JSON.stringify({ error: "Forbidden: only localhost/extension allowed" }));
2353
- return;
2354
- }
2355
- if (origin && origin.startsWith("chrome-extension://")) {
2356
- response.setHeader("Access-Control-Allow-Origin", origin);
2357
- }
2358
- response.writeHead(200, { "Content-Type": "application/json" });
2359
- response.end(JSON.stringify({ token: this.pairingToken }));
2360
- return;
2361
- }
2362
- response.writeHead(404);
2363
- response.end();
2364
- });
2365
- this.server.on("upgrade", (request, socket, head) => {
2366
- const origin = request.headers.origin;
2367
- const ip = request.socket.remoteAddress ?? "unknown";
2368
- if (!this.isAllowedOrigin(origin)) {
2369
- this.logSecurityEvent("origin_blocked", { origin, ip });
2370
- socket.write("HTTP/1.1 403 Forbidden\r\n\r\n");
2371
- socket.destroy();
2372
- return;
2373
- }
2374
- if (this.isRateLimited(ip)) {
2375
- this.logSecurityEvent("rate_limited", { ip });
2376
- socket.write("HTTP/1.1 429 Too Many Requests\r\n\r\n");
2377
- socket.destroy();
2378
- return;
2379
- }
2380
- const pathname = new URL(request.url ?? "", "http://127.0.0.1").pathname;
2381
- if (pathname === "/extension") {
2382
- this.extensionWss?.handleUpgrade(request, socket, head, (ws) => {
2383
- this.extensionWss?.emit("connection", ws, request);
2384
- });
2385
- return;
2386
- }
2387
- if (pathname === "/cdp") {
2388
- this.cdpWss?.handleUpgrade(request, socket, head, (ws) => {
2389
- this.cdpWss?.emit("connection", ws, request);
2390
- });
2391
- return;
2392
- }
2393
- socket.destroy();
2394
- });
2395
- await new Promise((resolve, reject) => {
2396
- this.server?.once("error", reject);
2397
- this.server?.listen(port, "127.0.0.1", () => {
2398
- resolve();
2399
- });
2400
- });
2401
- const address = this.server.address();
2402
- if (!address) {
2403
- throw new Error("Relay server did not expose a port");
2404
- }
2405
- this.port = address.port;
2406
- this.baseUrl = `ws://127.0.0.1:${address.port}`;
2407
- this.running = true;
2408
- return { url: this.baseUrl, port: address.port };
2409
- }
2410
- stop() {
2411
- this.running = false;
2412
- this.baseUrl = null;
2413
- this.port = null;
2414
- this.extensionInfo = null;
2415
- if (this.extensionSocket) {
2416
- this.extensionSocket.close(1e3, "Relay stopped");
2417
- this.extensionSocket = null;
2418
- }
2419
- if (this.cdpSocket) {
2420
- this.cdpSocket.close(1e3, "Relay stopped");
2421
- this.cdpSocket = null;
2422
- }
2423
- this.extensionWss?.close();
2424
- this.cdpWss?.close();
2425
- this.server?.close();
2426
- this.extensionWss = null;
2427
- this.cdpWss = null;
2428
- this.server = null;
2429
- }
2430
- status() {
2431
- return {
2432
- running: this.running,
2433
- url: this.baseUrl || void 0,
2434
- port: this.port ?? void 0,
2435
- extensionConnected: Boolean(this.extensionSocket),
2436
- cdpConnected: Boolean(this.cdpSocket),
2437
- extension: this.extensionInfo ?? void 0
2438
- };
2439
- }
2440
- getCdpUrl() {
2441
- return this.baseUrl ? `${this.baseUrl}/cdp` : null;
2442
- }
2443
- setToken(token) {
2444
- const trimmed = typeof token === "string" ? token.trim() : "";
2445
- this.pairingToken = trimmed.length ? trimmed : null;
2446
- }
2447
- setCdpAllowlist(methods) {
2448
- if (!methods || methods.length === 0) {
2449
- this.cdpAllowlist = null;
2450
- return;
2451
- }
2452
- this.cdpAllowlist = new Set(methods);
2453
- }
2454
- isAllowedOrigin(origin) {
2455
- if (!origin) {
2456
- return true;
2457
- }
2458
- if (origin.startsWith("chrome-extension://")) {
2459
- return true;
2460
- }
2461
- return false;
2462
- }
2463
- isRateLimited(ip) {
2464
- const now = Date.now();
2465
- const record = this.handshakeAttempts.get(ip);
2466
- if (!record || now > record.resetAt) {
2467
- this.handshakeAttempts.set(ip, { count: 1, resetAt: now + _RelayServer.RATE_LIMIT_WINDOW_MS });
2468
- return false;
2469
- }
2470
- record.count++;
2471
- return record.count > _RelayServer.MAX_HANDSHAKE_ATTEMPTS;
2472
- }
2473
- isCommandAllowed(method) {
2474
- if (!this.cdpAllowlist) return true;
2475
- return this.cdpAllowlist.has(method);
2476
- }
2477
- logSecurityEvent(event, details) {
2478
- const safeDetails = { ...details };
2479
- delete safeDetails.token;
2480
- delete safeDetails.pairingToken;
2481
- console.warn(`[security] ${event}`, JSON.stringify(safeDetails));
2482
- }
2483
- handleCdpMessage(data) {
2484
- const message = parseJson(data);
2485
- if (!isRecord(message)) {
2486
- return;
2487
- }
2488
- const id = message.id;
2489
- const method = message.method;
2490
- if (typeof id !== "string" && typeof id !== "number" || typeof method !== "string") {
2491
- return;
2492
- }
2493
- if (!this.extensionSocket) {
2494
- this.sendJson(this.cdpSocket, {
2495
- id,
2496
- error: { message: "Extension not connected to relay" }
2497
- });
2498
- return;
2499
- }
2500
- if (!this.isCommandAllowed(method)) {
2501
- this.logSecurityEvent("command_blocked", { method });
2502
- this.sendJson(this.cdpSocket, {
2503
- id,
2504
- error: { message: `CDP command '${method}' not in allowlist` }
2505
- });
2506
- return;
2507
- }
2508
- const relayCommand = {
2509
- id,
2510
- method: "forwardCDPCommand",
2511
- params: {
2512
- method,
2513
- params: message.params,
2514
- sessionId: typeof message.sessionId === "string" ? message.sessionId : void 0
2515
- }
2516
- };
2517
- this.sendJson(this.extensionSocket, relayCommand);
2518
- }
2519
- handleExtensionMessage(data) {
2520
- const message = parseJson(data);
2521
- if (!isRecord(message)) {
2522
- return;
2523
- }
2524
- if (isHandshake(message)) {
2525
- if (!this.isPairingTokenValid(message)) {
2526
- this.logSecurityEvent("handshake_failed", { reason: "invalid_token", tabId: message.payload.tabId });
2527
- this.extensionInfo = null;
2528
- this.extensionSocket?.close(1008, "Invalid pairing token");
2529
- return;
2530
- }
2531
- this.extensionInfo = {
2532
- tabId: message.payload.tabId,
2533
- url: message.payload.url,
2534
- title: message.payload.title,
2535
- groupId: message.payload.groupId
2536
- };
2537
- return;
2538
- }
2539
- if (message.method === "forwardCDPEvent" && isRecord(message.params)) {
2540
- const params = message.params;
2541
- const event = {
2542
- method: params.method,
2543
- params: params.params ?? {}
2544
- };
2545
- if (params.sessionId) {
2546
- event.sessionId = params.sessionId;
2547
- }
2548
- this.sendJson(this.cdpSocket, event);
2549
- return;
2550
- }
2551
- if (typeof message.id === "string" || typeof message.id === "number") {
2552
- const response = { id: message.id };
2553
- if (typeof message.result !== "undefined") {
2554
- response.result = message.result;
2555
- }
2556
- if (message.error) {
2557
- response.error = message.error;
2558
- }
2559
- if (typeof message.sessionId === "string") {
2560
- response.sessionId = message.sessionId;
2561
- }
2562
- this.sendJson(this.cdpSocket, response);
2563
- }
2564
- }
2565
- sendJson(socket, payload) {
2566
- if (!socket || socket.readyState !== WebSocket.OPEN) {
2567
- return;
2568
- }
2569
- socket.send(JSON.stringify(payload));
2570
- }
2571
- isPairingTokenValid(handshake) {
2572
- if (!this.pairingToken) {
2573
- return true;
2574
- }
2575
- const expected = this.pairingToken;
2576
- const received = handshake.payload.pairingToken ?? "";
2577
- const expectedBuf = Buffer.from(expected, "utf-8");
2578
- const receivedBuf = Buffer.from(received, "utf-8");
2579
- if (expectedBuf.length !== receivedBuf.length) {
2580
- timingSafeEqual(expectedBuf, expectedBuf);
2581
- return false;
2582
- }
2583
- return timingSafeEqual(expectedBuf, receivedBuf);
2584
- }
2585
- };
2586
- var parseJson = (data) => {
2587
- const text = typeof data === "string" ? data : data.toString();
2588
- try {
2589
- return JSON.parse(text);
2590
- } catch {
2591
- return null;
2592
- }
2593
- };
2594
- var isRecord = (value) => {
2595
- return typeof value === "object" && value !== null;
2596
- };
2597
- var isHandshake = (value) => {
2598
- if (value.type !== "handshake" || !isRecord(value.payload)) {
2599
- return false;
2600
- }
2601
- return typeof value.payload.tabId === "number";
2602
- };
2603
-
2604
74
  // src/tools/launch.ts
2605
75
  import { tool } from "@opencode-ai/plugin";
2606
76
 
@@ -2625,35 +95,55 @@ function serializeError(error) {
2625
95
  }
2626
96
 
2627
97
  // src/tools/launch.ts
2628
- var z2 = tool.schema;
98
+ var z = tool.schema;
2629
99
  function createLaunchTool(deps) {
2630
100
  return tool({
2631
101
  description: "Launch a managed Chrome session and return a sessionId.",
2632
102
  args: {
2633
- profile: z2.string().optional().describe("Profile name for persistent browsing"),
2634
- headless: z2.boolean().optional().describe("Run Chrome in headless mode"),
2635
- startUrl: z2.string().optional().describe("Optional URL to open after launch"),
2636
- chromePath: z2.string().optional().describe("Override Chrome executable path"),
2637
- flags: z2.array(z2.string()).optional().describe("Extra Chrome flags"),
2638
- persistProfile: z2.boolean().optional().describe("Persist profile data between sessions")
103
+ profile: z.string().optional().describe("Profile name for persistent browsing"),
104
+ headless: z.boolean().optional().describe("Run Chrome in headless mode"),
105
+ startUrl: z.string().optional().describe("Optional URL to open after launch"),
106
+ chromePath: z.string().optional().describe("Override Chrome executable path"),
107
+ flags: z.array(z.string()).optional().describe("Extra Chrome flags"),
108
+ persistProfile: z.boolean().optional().describe("Persist profile data between sessions"),
109
+ noExtension: z.boolean().optional().describe("Skip extension relay and launch a new browser"),
110
+ extensionOnly: z.boolean().optional().describe("Require extension relay or fail"),
111
+ waitForExtension: z.boolean().optional().describe("Wait for extension to connect before launching"),
112
+ waitTimeoutMs: z.number().int().optional().describe("Timeout for waiting on extension (ms)")
2639
113
  },
2640
114
  async execute(args) {
2641
115
  try {
2642
- const relayStatus = deps.relay?.status();
116
+ let relayStatus = deps.relay?.status();
2643
117
  const relayUrl = deps.relay?.getCdpUrl();
2644
- const useRelay = Boolean(relayStatus?.extensionConnected && relayUrl);
118
+ const waitTimeoutMs = args.waitTimeoutMs ?? 3e4;
119
+ if (args.waitForExtension && deps.relay) {
120
+ const connected = await waitForExtension(deps.relay, waitTimeoutMs);
121
+ if (connected) {
122
+ relayStatus = deps.relay.status();
123
+ }
124
+ }
125
+ const useRelay = Boolean(!args.noExtension && relayStatus?.extensionConnected && relayUrl);
2645
126
  let usedRelay = false;
2646
127
  let relayWarning = null;
2647
128
  let result = null;
129
+ if (args.extensionOnly && !useRelay) {
130
+ return failure("Extension not connected; use --no-extension to launch a new browser.", "extension_not_connected");
131
+ }
2648
132
  if (useRelay && relayUrl) {
2649
133
  try {
2650
134
  result = await deps.manager.connectRelay(relayUrl);
2651
135
  usedRelay = true;
2652
136
  } catch {
137
+ if (args.extensionOnly) {
138
+ return failure("Extension relay connection failed.", "extension_connect_failed");
139
+ }
2653
140
  relayWarning = "Relay connection failed; falling back to managed Chrome.";
2654
141
  }
2655
142
  }
2656
143
  if (!result) {
144
+ if (relayUrl && !args.noExtension) {
145
+ relayWarning ??= "Extension not connected; launching managed Chrome instead.";
146
+ }
2657
147
  result = await deps.manager.launch({
2658
148
  profile: args.profile,
2659
149
  headless: args.headless,
@@ -2683,17 +173,27 @@ function createLaunchTool(deps) {
2683
173
  }
2684
174
  });
2685
175
  }
176
+ async function waitForExtension(relay, timeoutMs) {
177
+ const start = Date.now();
178
+ while (Date.now() - start < timeoutMs) {
179
+ if (relay.status().extensionConnected) {
180
+ return true;
181
+ }
182
+ await new Promise((resolve) => setTimeout(resolve, 500));
183
+ }
184
+ return false;
185
+ }
2686
186
 
2687
187
  // src/tools/connect.ts
2688
188
  import { tool as tool2 } from "@opencode-ai/plugin";
2689
- var z3 = tool2.schema;
189
+ var z2 = tool2.schema;
2690
190
  function createConnectTool(deps) {
2691
191
  return tool2({
2692
192
  description: "Connect to an existing Chrome CDP endpoint.",
2693
193
  args: {
2694
- wsEndpoint: z3.string().optional().describe("Full WebSocket endpoint to connect to"),
2695
- host: z3.string().optional().describe("Host for /json/version lookup"),
2696
- port: z3.number().int().optional().describe("Port for /json/version lookup")
194
+ wsEndpoint: z2.string().optional().describe("Full WebSocket endpoint to connect to"),
195
+ host: z2.string().optional().describe("Host for /json/version lookup"),
196
+ port: z2.number().int().optional().describe("Port for /json/version lookup")
2697
197
  },
2698
198
  async execute(args) {
2699
199
  try {
@@ -2720,13 +220,13 @@ function createConnectTool(deps) {
2720
220
 
2721
221
  // src/tools/disconnect.ts
2722
222
  import { tool as tool3 } from "@opencode-ai/plugin";
2723
- var z4 = tool3.schema;
223
+ var z3 = tool3.schema;
2724
224
  function createDisconnectTool(deps) {
2725
225
  return tool3({
2726
226
  description: "Disconnect a browser session.",
2727
227
  args: {
2728
- sessionId: z4.string().describe("Session id returned from launch/connect"),
2729
- closeBrowser: z4.boolean().optional().describe("Close the underlying browser process")
228
+ sessionId: z3.string().describe("Session id returned from launch/connect"),
229
+ closeBrowser: z3.boolean().optional().describe("Close the underlying browser process")
2730
230
  },
2731
231
  async execute(args) {
2732
232
  try {
@@ -2740,21 +240,21 @@ function createDisconnectTool(deps) {
2740
240
  }
2741
241
 
2742
242
  // src/tools/status.ts
2743
- import { readFileSync as readFileSync2 } from "fs";
2744
- import { dirname as dirname2, join as join6 } from "path";
243
+ import { readFileSync } from "fs";
244
+ import { dirname, join } from "path";
2745
245
  import { fileURLToPath } from "url";
2746
246
  import { tool as tool4 } from "@opencode-ai/plugin";
2747
- var z5 = tool4.schema;
247
+ var z4 = tool4.schema;
2748
248
  function getPackageVersion() {
2749
249
  try {
2750
- const baseDir = dirname2(fileURLToPath(import.meta.url));
250
+ const baseDir = dirname(fileURLToPath(import.meta.url));
2751
251
  const candidates = [
2752
- join6(baseDir, "..", "..", "package.json"),
2753
- join6(baseDir, "..", "package.json")
252
+ join(baseDir, "..", "..", "package.json"),
253
+ join(baseDir, "..", "package.json")
2754
254
  ];
2755
255
  for (const pkgPath of candidates) {
2756
256
  try {
2757
- const pkg = JSON.parse(readFileSync2(pkgPath, "utf-8"));
257
+ const pkg = JSON.parse(readFileSync(pkgPath, "utf-8"));
2758
258
  if (typeof pkg.version === "string") {
2759
259
  return pkg.version;
2760
260
  }
@@ -2787,7 +287,7 @@ function createStatusTool(deps) {
2787
287
  return tool4({
2788
288
  description: "Get status of a browser session.",
2789
289
  args: {
2790
- sessionId: z5.string().describe("Session id")
290
+ sessionId: z4.string().describe("Session id")
2791
291
  },
2792
292
  async execute(args) {
2793
293
  try {
@@ -2820,13 +320,13 @@ function createStatusTool(deps) {
2820
320
 
2821
321
  // src/tools/targets_list.ts
2822
322
  import { tool as tool5 } from "@opencode-ai/plugin";
2823
- var z6 = tool5.schema;
323
+ var z5 = tool5.schema;
2824
324
  function createTargetsListTool(deps) {
2825
325
  return tool5({
2826
326
  description: "List targets (tabs) in the current session.",
2827
327
  args: {
2828
- sessionId: z6.string().describe("Session id"),
2829
- includeUrls: z6.boolean().optional().describe("Include target URLs")
328
+ sessionId: z5.string().describe("Session id"),
329
+ includeUrls: z5.boolean().optional().describe("Include target URLs")
2830
330
  },
2831
331
  async execute(args) {
2832
332
  try {
@@ -2844,13 +344,13 @@ function createTargetsListTool(deps) {
2844
344
 
2845
345
  // src/tools/target_use.ts
2846
346
  import { tool as tool6 } from "@opencode-ai/plugin";
2847
- var z7 = tool6.schema;
347
+ var z6 = tool6.schema;
2848
348
  function createTargetUseTool(deps) {
2849
349
  return tool6({
2850
350
  description: "Set the active target (tab).",
2851
351
  args: {
2852
- sessionId: z7.string().describe("Session id"),
2853
- targetId: z7.string().describe("Target id")
352
+ sessionId: z6.string().describe("Session id"),
353
+ targetId: z6.string().describe("Target id")
2854
354
  },
2855
355
  async execute(args) {
2856
356
  try {
@@ -2869,13 +369,13 @@ function createTargetUseTool(deps) {
2869
369
 
2870
370
  // src/tools/target_new.ts
2871
371
  import { tool as tool7 } from "@opencode-ai/plugin";
2872
- var z8 = tool7.schema;
372
+ var z7 = tool7.schema;
2873
373
  function createTargetNewTool(deps) {
2874
374
  return tool7({
2875
375
  description: "Open a new target (tab).",
2876
376
  args: {
2877
- sessionId: z8.string().describe("Session id"),
2878
- url: z8.string().optional().describe("Optional URL to open")
377
+ sessionId: z7.string().describe("Session id"),
378
+ url: z7.string().optional().describe("Optional URL to open")
2879
379
  },
2880
380
  async execute(args) {
2881
381
  try {
@@ -2890,13 +390,13 @@ function createTargetNewTool(deps) {
2890
390
 
2891
391
  // src/tools/target_close.ts
2892
392
  import { tool as tool8 } from "@opencode-ai/plugin";
2893
- var z9 = tool8.schema;
393
+ var z8 = tool8.schema;
2894
394
  function createTargetCloseTool(deps) {
2895
395
  return tool8({
2896
396
  description: "Close a target (tab).",
2897
397
  args: {
2898
- sessionId: z9.string().describe("Session id"),
2899
- targetId: z9.string().describe("Target id")
398
+ sessionId: z8.string().describe("Session id"),
399
+ targetId: z8.string().describe("Target id")
2900
400
  },
2901
401
  async execute(args) {
2902
402
  try {
@@ -2911,14 +411,14 @@ function createTargetCloseTool(deps) {
2911
411
 
2912
412
  // src/tools/page.ts
2913
413
  import { tool as tool9 } from "@opencode-ai/plugin";
2914
- var z10 = tool9.schema;
414
+ var z9 = tool9.schema;
2915
415
  function createPageTool(deps) {
2916
416
  return tool9({
2917
417
  description: "Open or focus a named page, optionally navigating to a URL.",
2918
418
  args: {
2919
- sessionId: z10.string().describe("Active browser session id"),
2920
- name: z10.string().describe("Stable page name"),
2921
- url: z10.string().optional().describe("Optional URL to open")
419
+ sessionId: z9.string().describe("Active browser session id"),
420
+ name: z9.string().describe("Stable page name"),
421
+ url: z9.string().optional().describe("Optional URL to open")
2922
422
  },
2923
423
  async execute(args) {
2924
424
  try {
@@ -2938,12 +438,12 @@ function createPageTool(deps) {
2938
438
 
2939
439
  // src/tools/list.ts
2940
440
  import { tool as tool10 } from "@opencode-ai/plugin";
2941
- var z11 = tool10.schema;
441
+ var z10 = tool10.schema;
2942
442
  function createListTool(deps) {
2943
443
  return tool10({
2944
444
  description: "List named pages in the current session.",
2945
445
  args: {
2946
- sessionId: z11.string().describe("Active browser session id")
446
+ sessionId: z10.string().describe("Active browser session id")
2947
447
  },
2948
448
  async execute(args) {
2949
449
  try {
@@ -2958,13 +458,13 @@ function createListTool(deps) {
2958
458
 
2959
459
  // src/tools/close.ts
2960
460
  import { tool as tool11 } from "@opencode-ai/plugin";
2961
- var z12 = tool11.schema;
461
+ var z11 = tool11.schema;
2962
462
  function createCloseTool(deps) {
2963
463
  return tool11({
2964
464
  description: "Close a named page within the current session.",
2965
465
  args: {
2966
- sessionId: z12.string().describe("Active browser session id"),
2967
- name: z12.string().describe("Named page to close")
466
+ sessionId: z11.string().describe("Active browser session id"),
467
+ name: z11.string().describe("Named page to close")
2968
468
  },
2969
469
  async execute(args) {
2970
470
  try {
@@ -2979,16 +479,16 @@ function createCloseTool(deps) {
2979
479
 
2980
480
  // src/tools/goto.ts
2981
481
  import { tool as tool12 } from "@opencode-ai/plugin";
2982
- var z13 = tool12.schema;
2983
- var waitUntilSchema = z13.enum(["domcontentloaded", "load", "networkidle"]);
482
+ var z12 = tool12.schema;
483
+ var waitUntilSchema = z12.enum(["domcontentloaded", "load", "networkidle"]);
2984
484
  function createGotoTool(deps) {
2985
485
  return tool12({
2986
486
  description: "Navigate the active target to a URL.",
2987
487
  args: {
2988
- sessionId: z13.string().describe("Session id"),
2989
- url: z13.string().describe("URL to navigate to"),
488
+ sessionId: z12.string().describe("Session id"),
489
+ url: z12.string().describe("URL to navigate to"),
2990
490
  waitUntil: waitUntilSchema.optional().describe("Load state to wait for"),
2991
- timeoutMs: z13.number().int().optional().describe("Timeout in milliseconds")
491
+ timeoutMs: z12.number().int().optional().describe("Timeout in milliseconds")
2992
492
  },
2993
493
  async execute(args) {
2994
494
  try {
@@ -3008,18 +508,18 @@ function createGotoTool(deps) {
3008
508
 
3009
509
  // src/tools/wait.ts
3010
510
  import { tool as tool13 } from "@opencode-ai/plugin";
3011
- var z14 = tool13.schema;
3012
- var waitUntilSchema2 = z14.enum(["domcontentloaded", "load", "networkidle"]);
3013
- var waitStateSchema = z14.enum(["attached", "visible", "hidden"]);
511
+ var z13 = tool13.schema;
512
+ var waitUntilSchema2 = z13.enum(["domcontentloaded", "load", "networkidle"]);
513
+ var waitStateSchema = z13.enum(["attached", "visible", "hidden"]);
3014
514
  function createWaitTool(deps) {
3015
515
  return tool13({
3016
516
  description: "Wait for a load state or a ref state.",
3017
517
  args: {
3018
- sessionId: z14.string().describe("Session id"),
518
+ sessionId: z13.string().describe("Session id"),
3019
519
  until: waitUntilSchema2.optional().describe("Load state to wait for"),
3020
- ref: z14.string().optional().describe("Ref to wait for"),
520
+ ref: z13.string().optional().describe("Ref to wait for"),
3021
521
  state: waitStateSchema.optional().describe("Ref state to wait for"),
3022
- timeoutMs: z14.number().int().optional().describe("Timeout in milliseconds")
522
+ timeoutMs: z13.number().int().optional().describe("Timeout in milliseconds")
3023
523
  },
3024
524
  async execute(args) {
3025
525
  try {
@@ -3050,16 +550,16 @@ function createWaitTool(deps) {
3050
550
 
3051
551
  // src/tools/snapshot.ts
3052
552
  import { tool as tool14 } from "@opencode-ai/plugin";
3053
- var z15 = tool14.schema;
3054
- var formatSchema = z15.enum(["outline", "actionables"]);
553
+ var z14 = tool14.schema;
554
+ var formatSchema = z14.enum(["outline", "actionables"]);
3055
555
  function createSnapshotTool(deps) {
3056
556
  return tool14({
3057
557
  description: "Capture a snapshot of the current page and return refs.",
3058
558
  args: {
3059
- sessionId: z15.string().describe("Session id"),
559
+ sessionId: z14.string().describe("Session id"),
3060
560
  format: formatSchema.optional().describe("Snapshot format"),
3061
- maxChars: z15.number().int().optional().describe("Max characters for snapshot output"),
3062
- cursor: z15.string().optional().describe("Cursor for paging")
561
+ maxChars: z14.number().int().optional().describe("Max characters for snapshot output"),
562
+ cursor: z14.string().optional().describe("Cursor for paging")
3063
563
  },
3064
564
  async execute(args) {
3065
565
  try {
@@ -3080,13 +580,13 @@ function createSnapshotTool(deps) {
3080
580
 
3081
581
  // src/tools/click.ts
3082
582
  import { tool as tool15 } from "@opencode-ai/plugin";
3083
- var z16 = tool15.schema;
583
+ var z15 = tool15.schema;
3084
584
  function createClickTool(deps) {
3085
585
  return tool15({
3086
586
  description: "Click a referenced element.",
3087
587
  args: {
3088
- sessionId: z16.string().describe("Session id"),
3089
- ref: z16.string().describe("Element ref")
588
+ sessionId: z15.string().describe("Session id"),
589
+ ref: z15.string().describe("Element ref")
3090
590
  },
3091
591
  async execute(args) {
3092
592
  try {
@@ -3101,16 +601,16 @@ function createClickTool(deps) {
3101
601
 
3102
602
  // src/tools/type.ts
3103
603
  import { tool as tool16 } from "@opencode-ai/plugin";
3104
- var z17 = tool16.schema;
604
+ var z16 = tool16.schema;
3105
605
  function createTypeTool(deps) {
3106
606
  return tool16({
3107
607
  description: "Type text into a referenced input.",
3108
608
  args: {
3109
- sessionId: z17.string().describe("Session id"),
3110
- ref: z17.string().describe("Element ref"),
3111
- text: z17.string().describe("Text to type"),
3112
- clear: z17.boolean().optional().describe("Clear before typing"),
3113
- submit: z17.boolean().optional().describe("Press Enter after typing")
609
+ sessionId: z16.string().describe("Session id"),
610
+ ref: z16.string().describe("Element ref"),
611
+ text: z16.string().describe("Text to type"),
612
+ clear: z16.boolean().optional().describe("Clear before typing"),
613
+ submit: z16.boolean().optional().describe("Press Enter after typing")
3114
614
  },
3115
615
  async execute(args) {
3116
616
  try {
@@ -3131,14 +631,14 @@ function createTypeTool(deps) {
3131
631
 
3132
632
  // src/tools/select.ts
3133
633
  import { tool as tool17 } from "@opencode-ai/plugin";
3134
- var z18 = tool17.schema;
634
+ var z17 = tool17.schema;
3135
635
  function createSelectTool(deps) {
3136
636
  return tool17({
3137
637
  description: "Select options in a referenced select element.",
3138
638
  args: {
3139
- sessionId: z18.string().describe("Session id"),
3140
- ref: z18.string().describe("Element ref"),
3141
- values: z18.array(z18.string()).describe("Values to select")
639
+ sessionId: z17.string().describe("Session id"),
640
+ ref: z17.string().describe("Element ref"),
641
+ values: z17.array(z17.string()).describe("Values to select")
3142
642
  },
3143
643
  async execute(args) {
3144
644
  try {
@@ -3153,14 +653,14 @@ function createSelectTool(deps) {
3153
653
 
3154
654
  // src/tools/scroll.ts
3155
655
  import { tool as tool18 } from "@opencode-ai/plugin";
3156
- var z19 = tool18.schema;
656
+ var z18 = tool18.schema;
3157
657
  function createScrollTool(deps) {
3158
658
  return tool18({
3159
659
  description: "Scroll the page or a referenced element.",
3160
660
  args: {
3161
- sessionId: z19.string().describe("Session id"),
3162
- dy: z19.number().describe("Scroll delta in pixels"),
3163
- ref: z19.string().optional().describe("Optional element ref to scroll")
661
+ sessionId: z18.string().describe("Session id"),
662
+ dy: z18.number().describe("Scroll delta in pixels"),
663
+ ref: z18.string().optional().describe("Optional element ref to scroll")
3164
664
  },
3165
665
  async execute(args) {
3166
666
  try {
@@ -3175,14 +675,14 @@ function createScrollTool(deps) {
3175
675
 
3176
676
  // src/tools/dom_get_html.ts
3177
677
  import { tool as tool19 } from "@opencode-ai/plugin";
3178
- var z20 = tool19.schema;
678
+ var z19 = tool19.schema;
3179
679
  function createDomGetHtmlTool(deps) {
3180
680
  return tool19({
3181
681
  description: "Get outerHTML for a referenced element.",
3182
682
  args: {
3183
- sessionId: z20.string().describe("Session id"),
3184
- ref: z20.string().describe("Element ref"),
3185
- maxChars: z20.number().int().optional().describe("Max characters")
683
+ sessionId: z19.string().describe("Session id"),
684
+ ref: z19.string().describe("Element ref"),
685
+ maxChars: z19.number().int().optional().describe("Max characters")
3186
686
  },
3187
687
  async execute(args) {
3188
688
  try {
@@ -3205,14 +705,14 @@ function createDomGetHtmlTool(deps) {
3205
705
 
3206
706
  // src/tools/dom_get_text.ts
3207
707
  import { tool as tool20 } from "@opencode-ai/plugin";
3208
- var z21 = tool20.schema;
708
+ var z20 = tool20.schema;
3209
709
  function createDomGetTextTool(deps) {
3210
710
  return tool20({
3211
711
  description: "Get inner text for a referenced element.",
3212
712
  args: {
3213
- sessionId: z21.string().describe("Session id"),
3214
- ref: z21.string().describe("Element ref"),
3215
- maxChars: z21.number().int().optional().describe("Max characters")
713
+ sessionId: z20.string().describe("Session id"),
714
+ ref: z20.string().describe("Element ref"),
715
+ maxChars: z20.number().int().optional().describe("Max characters")
3216
716
  },
3217
717
  async execute(args) {
3218
718
  try {
@@ -3235,19 +735,19 @@ function createDomGetTextTool(deps) {
3235
735
 
3236
736
  // src/tools/run.ts
3237
737
  import { tool as tool21 } from "@opencode-ai/plugin";
3238
- var z22 = tool21.schema;
3239
- var stepSchema = z22.object({
3240
- action: z22.string().describe("Action name"),
3241
- args: z22.record(z22.string(), z22.unknown()).optional().describe("Action arguments")
738
+ var z21 = tool21.schema;
739
+ var stepSchema = z21.object({
740
+ action: z21.string().describe("Action name"),
741
+ args: z21.record(z21.string(), z21.unknown()).optional().describe("Action arguments")
3242
742
  });
3243
743
  function createRunTool(deps) {
3244
744
  return tool21({
3245
745
  description: "Run multiple actions in a single tool call.",
3246
746
  args: {
3247
- sessionId: z22.string().describe("Session id"),
3248
- steps: z22.array(stepSchema).describe("Steps to execute"),
3249
- stopOnError: z22.boolean().optional().describe("Stop when a step fails"),
3250
- maxSnapshotChars: z22.number().int().optional().describe("Default maxChars for snapshot steps")
747
+ sessionId: z21.string().describe("Session id"),
748
+ steps: z21.array(stepSchema).describe("Steps to execute"),
749
+ stopOnError: z21.boolean().optional().describe("Stop when a step fails"),
750
+ maxSnapshotChars: z21.number().int().optional().describe("Default maxChars for snapshot steps")
3251
751
  },
3252
752
  async execute(args) {
3253
753
  try {
@@ -3281,12 +781,12 @@ function normalizeSteps(steps, maxSnapshotChars) {
3281
781
 
3282
782
  // src/tools/prompting_guide.ts
3283
783
  import { tool as tool22 } from "@opencode-ai/plugin";
3284
- var z23 = tool22.schema;
784
+ var z22 = tool22.schema;
3285
785
  function createPromptingGuideTool(deps) {
3286
786
  return tool22({
3287
787
  description: "Return best-practice prompting guidance for OpenDevBrowser.",
3288
788
  args: {
3289
- topic: z23.string().optional().describe("Optional topic for guidance")
789
+ topic: z22.string().optional().describe("Optional topic for guidance")
3290
790
  },
3291
791
  async execute(args) {
3292
792
  try {
@@ -3301,14 +801,14 @@ function createPromptingGuideTool(deps) {
3301
801
 
3302
802
  // src/tools/console_poll.ts
3303
803
  import { tool as tool23 } from "@opencode-ai/plugin";
3304
- var z24 = tool23.schema;
804
+ var z23 = tool23.schema;
3305
805
  function createConsolePollTool(deps) {
3306
806
  return tool23({
3307
807
  description: "Poll console events for the active target.",
3308
808
  args: {
3309
- sessionId: z24.string().describe("Session id"),
3310
- sinceSeq: z24.number().int().optional().describe("Sequence to resume from"),
3311
- max: z24.number().int().optional().describe("Max events to return")
809
+ sessionId: z23.string().describe("Session id"),
810
+ sinceSeq: z23.number().int().optional().describe("Sequence to resume from"),
811
+ max: z23.number().int().optional().describe("Max events to return")
3312
812
  },
3313
813
  async execute(args) {
3314
814
  try {
@@ -3327,14 +827,14 @@ function createConsolePollTool(deps) {
3327
827
 
3328
828
  // src/tools/network_poll.ts
3329
829
  import { tool as tool24 } from "@opencode-ai/plugin";
3330
- var z25 = tool24.schema;
830
+ var z24 = tool24.schema;
3331
831
  function createNetworkPollTool(deps) {
3332
832
  return tool24({
3333
833
  description: "Poll network events for the active target.",
3334
834
  args: {
3335
- sessionId: z25.string().describe("Session id"),
3336
- sinceSeq: z25.number().int().optional().describe("Sequence to resume from"),
3337
- max: z25.number().int().optional().describe("Max events to return")
835
+ sessionId: z24.string().describe("Session id"),
836
+ sinceSeq: z24.number().int().optional().describe("Sequence to resume from"),
837
+ max: z24.number().int().optional().describe("Max events to return")
3338
838
  },
3339
839
  async execute(args) {
3340
840
  try {
@@ -3353,12 +853,12 @@ function createNetworkPollTool(deps) {
3353
853
 
3354
854
  // src/tools/clone_page.ts
3355
855
  import { tool as tool25 } from "@opencode-ai/plugin";
3356
- var z26 = tool25.schema;
856
+ var z25 = tool25.schema;
3357
857
  function createClonePageTool(deps) {
3358
858
  return tool25({
3359
859
  description: "Export the active page as a React component and CSS bundle.",
3360
860
  args: {
3361
- sessionId: z26.string().describe("Active browser session id")
861
+ sessionId: z25.string().describe("Active browser session id")
3362
862
  },
3363
863
  async execute(args) {
3364
864
  try {
@@ -3373,13 +873,13 @@ function createClonePageTool(deps) {
3373
873
 
3374
874
  // src/tools/clone_component.ts
3375
875
  import { tool as tool26 } from "@opencode-ai/plugin";
3376
- var z27 = tool26.schema;
876
+ var z26 = tool26.schema;
3377
877
  function createCloneComponentTool(deps) {
3378
878
  return tool26({
3379
879
  description: "Export a selected element subtree as a React component and CSS bundle.",
3380
880
  args: {
3381
- sessionId: z27.string().describe("Active browser session id"),
3382
- ref: z27.string().describe("Element ref from snapshot")
881
+ sessionId: z26.string().describe("Active browser session id"),
882
+ ref: z26.string().describe("Element ref from snapshot")
3383
883
  },
3384
884
  async execute(args) {
3385
885
  try {
@@ -3394,12 +894,12 @@ function createCloneComponentTool(deps) {
3394
894
 
3395
895
  // src/tools/perf.ts
3396
896
  import { tool as tool27 } from "@opencode-ai/plugin";
3397
- var z28 = tool27.schema;
897
+ var z27 = tool27.schema;
3398
898
  function createPerfTool(deps) {
3399
899
  return tool27({
3400
900
  description: "Fetch lightweight performance metrics from the active page.",
3401
901
  args: {
3402
- sessionId: z28.string().describe("Active browser session id")
902
+ sessionId: z27.string().describe("Active browser session id")
3403
903
  },
3404
904
  async execute(args) {
3405
905
  try {
@@ -3414,13 +914,13 @@ function createPerfTool(deps) {
3414
914
 
3415
915
  // src/tools/screenshot.ts
3416
916
  import { tool as tool28 } from "@opencode-ai/plugin";
3417
- var z29 = tool28.schema;
917
+ var z28 = tool28.schema;
3418
918
  function createScreenshotTool(deps) {
3419
919
  return tool28({
3420
920
  description: "Capture a screenshot of the active page.",
3421
921
  args: {
3422
- sessionId: z29.string().describe("Active browser session id"),
3423
- path: z29.string().optional().describe("Optional output file path")
922
+ sessionId: z28.string().describe("Active browser session id"),
923
+ path: z28.string().optional().describe("Optional output file path")
3424
924
  },
3425
925
  async execute(args) {
3426
926
  try {
@@ -3453,13 +953,13 @@ function createSkillListTool(deps) {
3453
953
 
3454
954
  // src/tools/skill_load.ts
3455
955
  import { tool as tool30 } from "@opencode-ai/plugin";
3456
- var z30 = tool30.schema;
956
+ var z29 = tool30.schema;
3457
957
  function createSkillLoadTool(deps) {
3458
958
  return tool30({
3459
959
  description: "Load a specific skill by name from OpenCode skill directories (compatibility wrapper)",
3460
960
  args: {
3461
- name: z30.string().describe("Name of the skill to load (e.g., 'login-automation', 'form-testing')"),
3462
- topic: z30.string().optional().describe("Optional topic to filter the skill content")
961
+ name: z29.string().describe("Name of the skill to load (e.g., 'login-automation', 'form-testing')"),
962
+ topic: z29.string().optional().describe("Optional topic to filter the skill content")
3463
963
  },
3464
964
  async execute(args) {
3465
965
  try {
@@ -3511,93 +1011,60 @@ function createTools(deps) {
3511
1011
 
3512
1012
  // src/index.ts
3513
1013
  var OpenDevBrowserPlugin = async ({ directory, worktree }) => {
3514
- const initialConfig = loadGlobalConfig();
3515
- const configStore = new ConfigStore(initialConfig);
3516
- const cacheRoot = worktree || directory;
3517
- const manager = new BrowserManager(cacheRoot, initialConfig);
3518
- const runner = new ScriptRunner(manager);
3519
- const skills = new SkillLoader(cacheRoot, initialConfig.skillPaths);
3520
- const relay = new RelayServer();
1014
+ const core = createOpenDevBrowserCore({ directory, worktree });
1015
+ const { config, configStore, manager, runner, skills, relay, ensureRelay, cleanup, getExtensionPath } = core;
3521
1016
  const skillNudgeState = createSkillNudgeState();
3522
1017
  const continuityNudgeState = createContinuityNudgeState();
3523
- relay.setToken(initialConfig.relayToken);
3524
1018
  console.info(
3525
- `[opendevbrowser] loaded (cacheRoot=${cacheRoot}, relay=${initialConfig.relayToken === false ? "disabled" : "enabled"})`
1019
+ `[opendevbrowser] loaded (cacheRoot=${core.cacheRoot}, relay=${config.relayToken === false ? "disabled" : "enabled"})`
3526
1020
  );
3527
1021
  try {
3528
1022
  extractExtension();
3529
1023
  } catch (error) {
3530
1024
  console.warn("Extension extraction failed:", error instanceof Error ? error.message : error);
3531
1025
  }
3532
- const ensureRelay = async (port) => {
3533
- if (port <= 0 || initialConfig.relayToken === false) {
3534
- relay.stop();
3535
- return;
3536
- }
3537
- const status = relay.status();
3538
- if (status.running && status.port === port) {
3539
- return;
3540
- }
3541
- relay.stop();
3542
- try {
3543
- await relay.start(port);
3544
- } catch (error) {
3545
- const message = error instanceof Error ? error.message : String(error);
3546
- if (message.includes("EADDRINUSE") || message.includes("in use")) {
3547
- console.warn(`[opendevbrowser] Relay server port ${port} is already in use. Extension pairing will be unavailable.`);
3548
- console.warn(`[opendevbrowser] To fix: kill the process using port ${port} or change relayPort in config.`);
3549
- } else {
3550
- console.warn(`[opendevbrowser] Failed to start relay server: ${message}`);
3551
- }
3552
- }
3553
- };
3554
- const cleanup = () => {
3555
- relay.stop();
3556
- manager.closeAll().catch(() => {
3557
- });
3558
- };
3559
1026
  process.on("SIGINT", cleanup);
3560
1027
  process.on("SIGTERM", cleanup);
3561
1028
  process.on("beforeExit", cleanup);
3562
- await ensureRelay(initialConfig.relayPort);
1029
+ await ensureRelay(config.relayPort);
3563
1030
  return {
3564
1031
  tool: createTools({ manager, runner, config: configStore, skills, relay, getExtensionPath }),
3565
1032
  "chat.message": async (_input, output) => {
3566
- const config = configStore.get();
1033
+ const config2 = configStore.get();
3567
1034
  if (output.message.role !== "user") return;
3568
1035
  const text = extractTextFromParts(output.parts);
3569
1036
  if (!text) return;
3570
- if (config.skills.nudge.enabled && shouldTriggerSkillNudge(text, config.skills.nudge.keywords)) {
1037
+ if (config2.skills.nudge.enabled && shouldTriggerSkillNudge(text, config2.skills.nudge.keywords)) {
3571
1038
  markSkillNudge(skillNudgeState, Date.now());
3572
1039
  }
3573
- if (config.continuity.enabled && config.continuity.nudge.enabled) {
3574
- if (shouldTriggerContinuityNudge(text, config.continuity.nudge.keywords)) {
1040
+ if (config2.continuity.enabled && config2.continuity.nudge.enabled) {
1041
+ if (shouldTriggerContinuityNudge(text, config2.continuity.nudge.keywords)) {
3575
1042
  markContinuityNudge(continuityNudgeState, Date.now());
3576
1043
  }
3577
1044
  }
3578
1045
  },
3579
1046
  "experimental.chat.system.transform": async (_input, output) => {
3580
- const config = configStore.get();
1047
+ const config2 = configStore.get();
3581
1048
  const systemEntries = output.system ?? [];
3582
1049
  let nextEntries = systemEntries;
3583
1050
  let changed = false;
3584
- if (config.skills.nudge.enabled) {
1051
+ if (config2.skills.nudge.enabled) {
3585
1052
  if (systemEntries.some((entry) => entry.includes(SKILL_NUDGE_MARKER))) {
3586
1053
  clearSkillNudge(skillNudgeState);
3587
- } else if (consumeSkillNudge(skillNudgeState, Date.now(), config.skills.nudge.maxAgeMs)) {
1054
+ } else if (consumeSkillNudge(skillNudgeState, Date.now(), config2.skills.nudge.maxAgeMs)) {
3588
1055
  nextEntries = [...nextEntries, buildSkillNudgeMessage()];
3589
1056
  changed = true;
3590
1057
  }
3591
1058
  }
3592
- if (config.continuity.enabled && config.continuity.nudge.enabled) {
1059
+ if (config2.continuity.enabled && config2.continuity.nudge.enabled) {
3593
1060
  if (systemEntries.some((entry) => entry.includes(CONTINUITY_NUDGE_MARKER))) {
3594
1061
  clearContinuityNudge(continuityNudgeState);
3595
1062
  } else if (consumeContinuityNudge(
3596
1063
  continuityNudgeState,
3597
1064
  Date.now(),
3598
- config.continuity.nudge.maxAgeMs
1065
+ config2.continuity.nudge.maxAgeMs
3599
1066
  )) {
3600
- nextEntries = [...nextEntries, buildContinuityNudgeMessage(config.continuity.filePath)];
1067
+ nextEntries = [...nextEntries, buildContinuityNudgeMessage(config2.continuity.filePath)];
3601
1068
  changed = true;
3602
1069
  }
3603
1070
  }
@@ -3611,5 +1078,4 @@ var index_default = OpenDevBrowserPlugin;
3611
1078
  export {
3612
1079
  index_default as default
3613
1080
  };
3614
- /* v8 ignore next -- @preserve */
3615
1081
  //# sourceMappingURL=index.js.map