opendevbrowser 0.0.11 → 0.0.12
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +74 -1
- package/dist/chunk-WTFSMBVH.js +2815 -0
- package/dist/chunk-WTFSMBVH.js.map +1 -0
- package/dist/cli/index.js +1589 -71
- package/dist/cli/index.js.map +1 -1
- package/dist/index.js +164 -2804
- package/dist/index.js.map +1 -1
- package/dist/opendevbrowser.js +164 -2804
- package/dist/opendevbrowser.js.map +1 -1
- package/extension/dist/background.js +121 -0
- package/extension/dist/popup.js +37 -5
- package/extension/dist/relay-settings.js +2 -1
- package/extension/icons/icon128.png +0 -0
- package/extension/icons/icon16.png +0 -0
- package/extension/icons/icon32.png +0 -0
- package/extension/icons/icon48.png +0 -0
- package/extension/manifest.json +1 -1
- package/extension/popup.html +326 -66
- package/package.json +1 -1
- package/skills/AGENTS.md +1 -0
- package/dist/chunk-R5VUZEUU.js +0 -128
- package/dist/chunk-R5VUZEUU.js.map +0 -1
package/dist/opendevbrowser.js
CHANGED
|
@@ -1,2208 +1,7 @@
|
|
|
1
1
|
import {
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
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,441 +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 DEFAULT_DISCOVERY_PORT = 8787;
|
|
2280
|
-
var CONFIG_PATH = "/config";
|
|
2281
|
-
var PAIR_PATH = "/pair";
|
|
2282
|
-
var RelayServer = class _RelayServer {
|
|
2283
|
-
running = false;
|
|
2284
|
-
baseUrl = null;
|
|
2285
|
-
port = null;
|
|
2286
|
-
server = null;
|
|
2287
|
-
discoveryServer = null;
|
|
2288
|
-
extensionWss = null;
|
|
2289
|
-
cdpWss = null;
|
|
2290
|
-
extensionSocket = null;
|
|
2291
|
-
cdpSocket = null;
|
|
2292
|
-
extensionInfo = null;
|
|
2293
|
-
pairingToken = null;
|
|
2294
|
-
configuredDiscoveryPort;
|
|
2295
|
-
discoveryPort = null;
|
|
2296
|
-
handshakeAttempts = /* @__PURE__ */ new Map();
|
|
2297
|
-
cdpAllowlist = null;
|
|
2298
|
-
static MAX_HANDSHAKE_ATTEMPTS = 5;
|
|
2299
|
-
static RATE_LIMIT_WINDOW_MS = 6e4;
|
|
2300
|
-
constructor(options = {}) {
|
|
2301
|
-
this.configuredDiscoveryPort = options.discoveryPort ?? DEFAULT_DISCOVERY_PORT;
|
|
2302
|
-
}
|
|
2303
|
-
async start(port = 8787) {
|
|
2304
|
-
if (this.running && this.baseUrl && this.port !== null) {
|
|
2305
|
-
return { url: this.baseUrl, port: this.port };
|
|
2306
|
-
}
|
|
2307
|
-
this.server = createServer();
|
|
2308
|
-
this.extensionWss = new WebSocketServer({ noServer: true });
|
|
2309
|
-
this.cdpWss = new WebSocketServer({ noServer: true });
|
|
2310
|
-
this.extensionWss.on("connection", (socket) => {
|
|
2311
|
-
if (this.extensionSocket) {
|
|
2312
|
-
this.extensionSocket.close(1e3, "Replaced by a new extension client");
|
|
2313
|
-
}
|
|
2314
|
-
this.extensionSocket = socket;
|
|
2315
|
-
this.extensionInfo = null;
|
|
2316
|
-
socket.on("message", (data) => {
|
|
2317
|
-
this.handleExtensionMessage(data);
|
|
2318
|
-
});
|
|
2319
|
-
socket.on("close", () => {
|
|
2320
|
-
if (this.extensionSocket === socket) {
|
|
2321
|
-
this.extensionSocket = null;
|
|
2322
|
-
this.extensionInfo = null;
|
|
2323
|
-
}
|
|
2324
|
-
if (this.cdpSocket) {
|
|
2325
|
-
this.cdpSocket.close(1011, "Extension disconnected");
|
|
2326
|
-
}
|
|
2327
|
-
});
|
|
2328
|
-
});
|
|
2329
|
-
this.cdpWss.on("connection", (socket) => {
|
|
2330
|
-
if (this.cdpSocket) {
|
|
2331
|
-
socket.close(1008, "Only one CDP client supported");
|
|
2332
|
-
return;
|
|
2333
|
-
}
|
|
2334
|
-
this.cdpSocket = socket;
|
|
2335
|
-
socket.on("message", (data) => {
|
|
2336
|
-
this.handleCdpMessage(data);
|
|
2337
|
-
});
|
|
2338
|
-
socket.on("close", () => {
|
|
2339
|
-
if (this.cdpSocket === socket) {
|
|
2340
|
-
this.cdpSocket = null;
|
|
2341
|
-
}
|
|
2342
|
-
});
|
|
2343
|
-
});
|
|
2344
|
-
this.server.on("request", (request, response) => {
|
|
2345
|
-
const pathname = new URL(request.url ?? "", "http://127.0.0.1").pathname;
|
|
2346
|
-
const origin = request.headers.origin;
|
|
2347
|
-
if (pathname === CONFIG_PATH && request.method === "OPTIONS") {
|
|
2348
|
-
this.handleConfigPreflight(origin, response);
|
|
2349
|
-
return;
|
|
2350
|
-
}
|
|
2351
|
-
if (pathname === CONFIG_PATH && request.method === "GET") {
|
|
2352
|
-
this.handleConfigRequest(origin, response);
|
|
2353
|
-
return;
|
|
2354
|
-
}
|
|
2355
|
-
if (pathname === PAIR_PATH && request.method === "OPTIONS") {
|
|
2356
|
-
if (origin && origin.startsWith("chrome-extension://")) {
|
|
2357
|
-
response.setHeader("Access-Control-Allow-Origin", origin);
|
|
2358
|
-
response.setHeader("Access-Control-Allow-Methods", "GET, OPTIONS");
|
|
2359
|
-
response.setHeader("Access-Control-Allow-Headers", "Content-Type");
|
|
2360
|
-
}
|
|
2361
|
-
response.writeHead(204);
|
|
2362
|
-
response.end();
|
|
2363
|
-
return;
|
|
2364
|
-
}
|
|
2365
|
-
if (pathname === PAIR_PATH && request.method === "GET") {
|
|
2366
|
-
const isLocalhost = !origin || origin.startsWith("chrome-extension://");
|
|
2367
|
-
if (!isLocalhost) {
|
|
2368
|
-
response.writeHead(403, { "Content-Type": "application/json" });
|
|
2369
|
-
response.end(JSON.stringify({ error: "Forbidden: only localhost/extension allowed" }));
|
|
2370
|
-
return;
|
|
2371
|
-
}
|
|
2372
|
-
if (origin && origin.startsWith("chrome-extension://")) {
|
|
2373
|
-
response.setHeader("Access-Control-Allow-Origin", origin);
|
|
2374
|
-
}
|
|
2375
|
-
response.writeHead(200, { "Content-Type": "application/json" });
|
|
2376
|
-
response.end(JSON.stringify({ token: this.pairingToken }));
|
|
2377
|
-
return;
|
|
2378
|
-
}
|
|
2379
|
-
response.writeHead(404);
|
|
2380
|
-
response.end();
|
|
2381
|
-
});
|
|
2382
|
-
this.server.on("upgrade", (request, socket, head) => {
|
|
2383
|
-
const origin = request.headers.origin;
|
|
2384
|
-
const ip = request.socket.remoteAddress ?? "unknown";
|
|
2385
|
-
if (!this.isAllowedOrigin(origin)) {
|
|
2386
|
-
this.logSecurityEvent("origin_blocked", { origin, ip });
|
|
2387
|
-
socket.write("HTTP/1.1 403 Forbidden\r\n\r\n");
|
|
2388
|
-
socket.destroy();
|
|
2389
|
-
return;
|
|
2390
|
-
}
|
|
2391
|
-
if (this.isRateLimited(ip)) {
|
|
2392
|
-
this.logSecurityEvent("rate_limited", { ip });
|
|
2393
|
-
socket.write("HTTP/1.1 429 Too Many Requests\r\n\r\n");
|
|
2394
|
-
socket.destroy();
|
|
2395
|
-
return;
|
|
2396
|
-
}
|
|
2397
|
-
const pathname = new URL(request.url ?? "", "http://127.0.0.1").pathname;
|
|
2398
|
-
if (pathname === "/extension") {
|
|
2399
|
-
this.extensionWss?.handleUpgrade(request, socket, head, (ws) => {
|
|
2400
|
-
this.extensionWss?.emit("connection", ws, request);
|
|
2401
|
-
});
|
|
2402
|
-
return;
|
|
2403
|
-
}
|
|
2404
|
-
if (pathname === "/cdp") {
|
|
2405
|
-
this.cdpWss?.handleUpgrade(request, socket, head, (ws) => {
|
|
2406
|
-
this.cdpWss?.emit("connection", ws, request);
|
|
2407
|
-
});
|
|
2408
|
-
return;
|
|
2409
|
-
}
|
|
2410
|
-
socket.destroy();
|
|
2411
|
-
});
|
|
2412
|
-
await new Promise((resolve, reject) => {
|
|
2413
|
-
this.server?.once("error", reject);
|
|
2414
|
-
this.server?.listen(port, "127.0.0.1", () => {
|
|
2415
|
-
resolve();
|
|
2416
|
-
});
|
|
2417
|
-
});
|
|
2418
|
-
const address = this.server.address();
|
|
2419
|
-
if (!address) {
|
|
2420
|
-
throw new Error("Relay server did not expose a port");
|
|
2421
|
-
}
|
|
2422
|
-
this.port = address.port;
|
|
2423
|
-
this.baseUrl = `ws://127.0.0.1:${address.port}`;
|
|
2424
|
-
this.running = true;
|
|
2425
|
-
try {
|
|
2426
|
-
await this.startDiscoveryServer();
|
|
2427
|
-
} catch (error) {
|
|
2428
|
-
const message = error instanceof Error ? error.message : String(error);
|
|
2429
|
-
console.warn(`[opendevbrowser] Discovery server failed to start: ${message}`);
|
|
2430
|
-
this.stopDiscoveryServer();
|
|
2431
|
-
}
|
|
2432
|
-
return { url: this.baseUrl, port: address.port };
|
|
2433
|
-
}
|
|
2434
|
-
stop() {
|
|
2435
|
-
this.running = false;
|
|
2436
|
-
this.baseUrl = null;
|
|
2437
|
-
this.port = null;
|
|
2438
|
-
this.extensionInfo = null;
|
|
2439
|
-
this.stopDiscoveryServer();
|
|
2440
|
-
if (this.extensionSocket) {
|
|
2441
|
-
this.extensionSocket.close(1e3, "Relay stopped");
|
|
2442
|
-
this.extensionSocket = null;
|
|
2443
|
-
}
|
|
2444
|
-
if (this.cdpSocket) {
|
|
2445
|
-
this.cdpSocket.close(1e3, "Relay stopped");
|
|
2446
|
-
this.cdpSocket = null;
|
|
2447
|
-
}
|
|
2448
|
-
this.extensionWss?.close();
|
|
2449
|
-
this.cdpWss?.close();
|
|
2450
|
-
this.server?.close();
|
|
2451
|
-
this.extensionWss = null;
|
|
2452
|
-
this.cdpWss = null;
|
|
2453
|
-
this.server = null;
|
|
2454
|
-
}
|
|
2455
|
-
status() {
|
|
2456
|
-
return {
|
|
2457
|
-
running: this.running,
|
|
2458
|
-
url: this.baseUrl || void 0,
|
|
2459
|
-
port: this.port ?? void 0,
|
|
2460
|
-
extensionConnected: Boolean(this.extensionSocket),
|
|
2461
|
-
cdpConnected: Boolean(this.cdpSocket),
|
|
2462
|
-
extension: this.extensionInfo ?? void 0
|
|
2463
|
-
};
|
|
2464
|
-
}
|
|
2465
|
-
getCdpUrl() {
|
|
2466
|
-
return this.baseUrl ? `${this.baseUrl}/cdp` : null;
|
|
2467
|
-
}
|
|
2468
|
-
getDiscoveryPort() {
|
|
2469
|
-
if (this.port !== null && this.port === this.configuredDiscoveryPort) {
|
|
2470
|
-
return this.port;
|
|
2471
|
-
}
|
|
2472
|
-
return this.discoveryPort;
|
|
2473
|
-
}
|
|
2474
|
-
setToken(token) {
|
|
2475
|
-
const trimmed = typeof token === "string" ? token.trim() : "";
|
|
2476
|
-
this.pairingToken = trimmed.length ? trimmed : null;
|
|
2477
|
-
}
|
|
2478
|
-
setCdpAllowlist(methods) {
|
|
2479
|
-
if (!methods || methods.length === 0) {
|
|
2480
|
-
this.cdpAllowlist = null;
|
|
2481
|
-
return;
|
|
2482
|
-
}
|
|
2483
|
-
this.cdpAllowlist = new Set(methods);
|
|
2484
|
-
}
|
|
2485
|
-
isAllowedOrigin(origin) {
|
|
2486
|
-
if (!origin) {
|
|
2487
|
-
return true;
|
|
2488
|
-
}
|
|
2489
|
-
if (origin.startsWith("chrome-extension://")) {
|
|
2490
|
-
return true;
|
|
2491
|
-
}
|
|
2492
|
-
return false;
|
|
2493
|
-
}
|
|
2494
|
-
isExtensionOrigin(origin) {
|
|
2495
|
-
return Boolean(origin && origin.startsWith("chrome-extension://"));
|
|
2496
|
-
}
|
|
2497
|
-
handleConfigPreflight(origin, response) {
|
|
2498
|
-
if (this.isExtensionOrigin(origin)) {
|
|
2499
|
-
response.setHeader("Access-Control-Allow-Origin", origin);
|
|
2500
|
-
response.setHeader("Access-Control-Allow-Methods", "GET, OPTIONS");
|
|
2501
|
-
response.setHeader("Access-Control-Allow-Headers", "Content-Type");
|
|
2502
|
-
}
|
|
2503
|
-
response.writeHead(204);
|
|
2504
|
-
response.end();
|
|
2505
|
-
}
|
|
2506
|
-
handleConfigRequest(origin, response) {
|
|
2507
|
-
if (!this.isExtensionOrigin(origin)) {
|
|
2508
|
-
response.writeHead(403, { "Content-Type": "application/json" });
|
|
2509
|
-
response.end(JSON.stringify({ error: "Forbidden: extension origin required" }));
|
|
2510
|
-
return;
|
|
2511
|
-
}
|
|
2512
|
-
if (origin) {
|
|
2513
|
-
response.setHeader("Access-Control-Allow-Origin", origin);
|
|
2514
|
-
}
|
|
2515
|
-
if (this.port === null) {
|
|
2516
|
-
response.writeHead(503, { "Content-Type": "application/json" });
|
|
2517
|
-
response.end(JSON.stringify({ error: "Relay not running" }));
|
|
2518
|
-
return;
|
|
2519
|
-
}
|
|
2520
|
-
response.writeHead(200, {
|
|
2521
|
-
"Content-Type": "application/json",
|
|
2522
|
-
"Cache-Control": "no-store"
|
|
2523
|
-
});
|
|
2524
|
-
response.end(JSON.stringify({
|
|
2525
|
-
relayPort: this.port,
|
|
2526
|
-
pairingRequired: Boolean(this.pairingToken)
|
|
2527
|
-
}));
|
|
2528
|
-
}
|
|
2529
|
-
async startDiscoveryServer() {
|
|
2530
|
-
if (this.port === null || this.discoveryServer) {
|
|
2531
|
-
return;
|
|
2532
|
-
}
|
|
2533
|
-
if (this.configuredDiscoveryPort > 0 && this.configuredDiscoveryPort === this.port) {
|
|
2534
|
-
return;
|
|
2535
|
-
}
|
|
2536
|
-
this.discoveryServer = createServer((request, response) => {
|
|
2537
|
-
const pathname = new URL(request.url ?? "", "http://127.0.0.1").pathname;
|
|
2538
|
-
const origin = request.headers.origin;
|
|
2539
|
-
if (pathname === CONFIG_PATH && request.method === "OPTIONS") {
|
|
2540
|
-
this.handleConfigPreflight(origin, response);
|
|
2541
|
-
return;
|
|
2542
|
-
}
|
|
2543
|
-
if (pathname === CONFIG_PATH && request.method === "GET") {
|
|
2544
|
-
this.handleConfigRequest(origin, response);
|
|
2545
|
-
return;
|
|
2546
|
-
}
|
|
2547
|
-
response.writeHead(404);
|
|
2548
|
-
response.end();
|
|
2549
|
-
});
|
|
2550
|
-
await new Promise((resolve, reject) => {
|
|
2551
|
-
this.discoveryServer?.once("error", reject);
|
|
2552
|
-
this.discoveryServer?.listen(this.configuredDiscoveryPort, "127.0.0.1", () => {
|
|
2553
|
-
resolve();
|
|
2554
|
-
});
|
|
2555
|
-
});
|
|
2556
|
-
const address = this.discoveryServer.address();
|
|
2557
|
-
if (!address) {
|
|
2558
|
-
throw new Error("Discovery server did not expose a port");
|
|
2559
|
-
}
|
|
2560
|
-
this.discoveryPort = address.port;
|
|
2561
|
-
}
|
|
2562
|
-
stopDiscoveryServer() {
|
|
2563
|
-
if (this.discoveryServer) {
|
|
2564
|
-
this.discoveryServer.close();
|
|
2565
|
-
this.discoveryServer = null;
|
|
2566
|
-
}
|
|
2567
|
-
this.discoveryPort = null;
|
|
2568
|
-
}
|
|
2569
|
-
isRateLimited(ip) {
|
|
2570
|
-
const now = Date.now();
|
|
2571
|
-
const record = this.handshakeAttempts.get(ip);
|
|
2572
|
-
if (!record || now > record.resetAt) {
|
|
2573
|
-
this.handshakeAttempts.set(ip, { count: 1, resetAt: now + _RelayServer.RATE_LIMIT_WINDOW_MS });
|
|
2574
|
-
return false;
|
|
2575
|
-
}
|
|
2576
|
-
record.count++;
|
|
2577
|
-
return record.count > _RelayServer.MAX_HANDSHAKE_ATTEMPTS;
|
|
2578
|
-
}
|
|
2579
|
-
isCommandAllowed(method) {
|
|
2580
|
-
if (!this.cdpAllowlist) return true;
|
|
2581
|
-
return this.cdpAllowlist.has(method);
|
|
2582
|
-
}
|
|
2583
|
-
logSecurityEvent(event, details) {
|
|
2584
|
-
const safeDetails = { ...details };
|
|
2585
|
-
delete safeDetails.token;
|
|
2586
|
-
delete safeDetails.pairingToken;
|
|
2587
|
-
console.warn(`[security] ${event}`, JSON.stringify(safeDetails));
|
|
2588
|
-
}
|
|
2589
|
-
handleCdpMessage(data) {
|
|
2590
|
-
const message = parseJson(data);
|
|
2591
|
-
if (!isRecord(message)) {
|
|
2592
|
-
return;
|
|
2593
|
-
}
|
|
2594
|
-
const id = message.id;
|
|
2595
|
-
const method = message.method;
|
|
2596
|
-
if (typeof id !== "string" && typeof id !== "number" || typeof method !== "string") {
|
|
2597
|
-
return;
|
|
2598
|
-
}
|
|
2599
|
-
if (!this.extensionSocket) {
|
|
2600
|
-
this.sendJson(this.cdpSocket, {
|
|
2601
|
-
id,
|
|
2602
|
-
error: { message: "Extension not connected to relay" }
|
|
2603
|
-
});
|
|
2604
|
-
return;
|
|
2605
|
-
}
|
|
2606
|
-
if (!this.isCommandAllowed(method)) {
|
|
2607
|
-
this.logSecurityEvent("command_blocked", { method });
|
|
2608
|
-
this.sendJson(this.cdpSocket, {
|
|
2609
|
-
id,
|
|
2610
|
-
error: { message: `CDP command '${method}' not in allowlist` }
|
|
2611
|
-
});
|
|
2612
|
-
return;
|
|
2613
|
-
}
|
|
2614
|
-
const relayCommand = {
|
|
2615
|
-
id,
|
|
2616
|
-
method: "forwardCDPCommand",
|
|
2617
|
-
params: {
|
|
2618
|
-
method,
|
|
2619
|
-
params: message.params,
|
|
2620
|
-
sessionId: typeof message.sessionId === "string" ? message.sessionId : void 0
|
|
2621
|
-
}
|
|
2622
|
-
};
|
|
2623
|
-
this.sendJson(this.extensionSocket, relayCommand);
|
|
2624
|
-
}
|
|
2625
|
-
handleExtensionMessage(data) {
|
|
2626
|
-
const message = parseJson(data);
|
|
2627
|
-
if (!isRecord(message)) {
|
|
2628
|
-
return;
|
|
2629
|
-
}
|
|
2630
|
-
if (isHandshake(message)) {
|
|
2631
|
-
if (!this.isPairingTokenValid(message)) {
|
|
2632
|
-
this.logSecurityEvent("handshake_failed", { reason: "invalid_token", tabId: message.payload.tabId });
|
|
2633
|
-
this.extensionInfo = null;
|
|
2634
|
-
this.extensionSocket?.close(1008, "Invalid pairing token");
|
|
2635
|
-
return;
|
|
2636
|
-
}
|
|
2637
|
-
this.extensionInfo = {
|
|
2638
|
-
tabId: message.payload.tabId,
|
|
2639
|
-
url: message.payload.url,
|
|
2640
|
-
title: message.payload.title,
|
|
2641
|
-
groupId: message.payload.groupId
|
|
2642
|
-
};
|
|
2643
|
-
return;
|
|
2644
|
-
}
|
|
2645
|
-
if (message.method === "forwardCDPEvent" && isRecord(message.params)) {
|
|
2646
|
-
const params = message.params;
|
|
2647
|
-
const event = {
|
|
2648
|
-
method: params.method,
|
|
2649
|
-
params: params.params ?? {}
|
|
2650
|
-
};
|
|
2651
|
-
if (params.sessionId) {
|
|
2652
|
-
event.sessionId = params.sessionId;
|
|
2653
|
-
}
|
|
2654
|
-
this.sendJson(this.cdpSocket, event);
|
|
2655
|
-
return;
|
|
2656
|
-
}
|
|
2657
|
-
if (typeof message.id === "string" || typeof message.id === "number") {
|
|
2658
|
-
const response = { id: message.id };
|
|
2659
|
-
if (typeof message.result !== "undefined") {
|
|
2660
|
-
response.result = message.result;
|
|
2661
|
-
}
|
|
2662
|
-
if (message.error) {
|
|
2663
|
-
response.error = message.error;
|
|
2664
|
-
}
|
|
2665
|
-
if (typeof message.sessionId === "string") {
|
|
2666
|
-
response.sessionId = message.sessionId;
|
|
2667
|
-
}
|
|
2668
|
-
this.sendJson(this.cdpSocket, response);
|
|
2669
|
-
}
|
|
2670
|
-
}
|
|
2671
|
-
sendJson(socket, payload) {
|
|
2672
|
-
if (!socket || socket.readyState !== WebSocket.OPEN) {
|
|
2673
|
-
return;
|
|
2674
|
-
}
|
|
2675
|
-
socket.send(JSON.stringify(payload));
|
|
2676
|
-
}
|
|
2677
|
-
isPairingTokenValid(handshake) {
|
|
2678
|
-
if (!this.pairingToken) {
|
|
2679
|
-
return true;
|
|
2680
|
-
}
|
|
2681
|
-
const expected = this.pairingToken;
|
|
2682
|
-
const received = handshake.payload.pairingToken ?? "";
|
|
2683
|
-
const expectedBuf = Buffer.from(expected, "utf-8");
|
|
2684
|
-
const receivedBuf = Buffer.from(received, "utf-8");
|
|
2685
|
-
if (expectedBuf.length !== receivedBuf.length) {
|
|
2686
|
-
timingSafeEqual(expectedBuf, expectedBuf);
|
|
2687
|
-
return false;
|
|
2688
|
-
}
|
|
2689
|
-
return timingSafeEqual(expectedBuf, receivedBuf);
|
|
2690
|
-
}
|
|
2691
|
-
};
|
|
2692
|
-
var parseJson = (data) => {
|
|
2693
|
-
const text = typeof data === "string" ? data : data.toString();
|
|
2694
|
-
try {
|
|
2695
|
-
return JSON.parse(text);
|
|
2696
|
-
} catch {
|
|
2697
|
-
return null;
|
|
2698
|
-
}
|
|
2699
|
-
};
|
|
2700
|
-
var isRecord = (value) => {
|
|
2701
|
-
return typeof value === "object" && value !== null;
|
|
2702
|
-
};
|
|
2703
|
-
var isHandshake = (value) => {
|
|
2704
|
-
if (value.type !== "handshake" || !isRecord(value.payload)) {
|
|
2705
|
-
return false;
|
|
2706
|
-
}
|
|
2707
|
-
return typeof value.payload.tabId === "number";
|
|
2708
|
-
};
|
|
2709
|
-
|
|
2710
74
|
// src/tools/launch.ts
|
|
2711
75
|
import { tool } from "@opencode-ai/plugin";
|
|
2712
76
|
|
|
@@ -2731,35 +95,55 @@ function serializeError(error) {
|
|
|
2731
95
|
}
|
|
2732
96
|
|
|
2733
97
|
// src/tools/launch.ts
|
|
2734
|
-
var
|
|
98
|
+
var z = tool.schema;
|
|
2735
99
|
function createLaunchTool(deps) {
|
|
2736
100
|
return tool({
|
|
2737
101
|
description: "Launch a managed Chrome session and return a sessionId.",
|
|
2738
102
|
args: {
|
|
2739
|
-
profile:
|
|
2740
|
-
headless:
|
|
2741
|
-
startUrl:
|
|
2742
|
-
chromePath:
|
|
2743
|
-
flags:
|
|
2744
|
-
persistProfile:
|
|
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)")
|
|
2745
113
|
},
|
|
2746
114
|
async execute(args) {
|
|
2747
115
|
try {
|
|
2748
|
-
|
|
116
|
+
let relayStatus = deps.relay?.status();
|
|
2749
117
|
const relayUrl = deps.relay?.getCdpUrl();
|
|
2750
|
-
const
|
|
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);
|
|
2751
126
|
let usedRelay = false;
|
|
2752
127
|
let relayWarning = null;
|
|
2753
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
|
+
}
|
|
2754
132
|
if (useRelay && relayUrl) {
|
|
2755
133
|
try {
|
|
2756
134
|
result = await deps.manager.connectRelay(relayUrl);
|
|
2757
135
|
usedRelay = true;
|
|
2758
136
|
} catch {
|
|
137
|
+
if (args.extensionOnly) {
|
|
138
|
+
return failure("Extension relay connection failed.", "extension_connect_failed");
|
|
139
|
+
}
|
|
2759
140
|
relayWarning = "Relay connection failed; falling back to managed Chrome.";
|
|
2760
141
|
}
|
|
2761
142
|
}
|
|
2762
143
|
if (!result) {
|
|
144
|
+
if (relayUrl && !args.noExtension) {
|
|
145
|
+
relayWarning ??= "Extension not connected; launching managed Chrome instead.";
|
|
146
|
+
}
|
|
2763
147
|
result = await deps.manager.launch({
|
|
2764
148
|
profile: args.profile,
|
|
2765
149
|
headless: args.headless,
|
|
@@ -2789,17 +173,27 @@ function createLaunchTool(deps) {
|
|
|
2789
173
|
}
|
|
2790
174
|
});
|
|
2791
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
|
+
}
|
|
2792
186
|
|
|
2793
187
|
// src/tools/connect.ts
|
|
2794
188
|
import { tool as tool2 } from "@opencode-ai/plugin";
|
|
2795
|
-
var
|
|
189
|
+
var z2 = tool2.schema;
|
|
2796
190
|
function createConnectTool(deps) {
|
|
2797
191
|
return tool2({
|
|
2798
192
|
description: "Connect to an existing Chrome CDP endpoint.",
|
|
2799
193
|
args: {
|
|
2800
|
-
wsEndpoint:
|
|
2801
|
-
host:
|
|
2802
|
-
port:
|
|
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")
|
|
2803
197
|
},
|
|
2804
198
|
async execute(args) {
|
|
2805
199
|
try {
|
|
@@ -2826,13 +220,13 @@ function createConnectTool(deps) {
|
|
|
2826
220
|
|
|
2827
221
|
// src/tools/disconnect.ts
|
|
2828
222
|
import { tool as tool3 } from "@opencode-ai/plugin";
|
|
2829
|
-
var
|
|
223
|
+
var z3 = tool3.schema;
|
|
2830
224
|
function createDisconnectTool(deps) {
|
|
2831
225
|
return tool3({
|
|
2832
226
|
description: "Disconnect a browser session.",
|
|
2833
227
|
args: {
|
|
2834
|
-
sessionId:
|
|
2835
|
-
closeBrowser:
|
|
228
|
+
sessionId: z3.string().describe("Session id returned from launch/connect"),
|
|
229
|
+
closeBrowser: z3.boolean().optional().describe("Close the underlying browser process")
|
|
2836
230
|
},
|
|
2837
231
|
async execute(args) {
|
|
2838
232
|
try {
|
|
@@ -2846,21 +240,21 @@ function createDisconnectTool(deps) {
|
|
|
2846
240
|
}
|
|
2847
241
|
|
|
2848
242
|
// src/tools/status.ts
|
|
2849
|
-
import { readFileSync
|
|
2850
|
-
import { dirname
|
|
243
|
+
import { readFileSync } from "fs";
|
|
244
|
+
import { dirname, join } from "path";
|
|
2851
245
|
import { fileURLToPath } from "url";
|
|
2852
246
|
import { tool as tool4 } from "@opencode-ai/plugin";
|
|
2853
|
-
var
|
|
247
|
+
var z4 = tool4.schema;
|
|
2854
248
|
function getPackageVersion() {
|
|
2855
249
|
try {
|
|
2856
|
-
const baseDir =
|
|
250
|
+
const baseDir = dirname(fileURLToPath(import.meta.url));
|
|
2857
251
|
const candidates = [
|
|
2858
|
-
|
|
2859
|
-
|
|
252
|
+
join(baseDir, "..", "..", "package.json"),
|
|
253
|
+
join(baseDir, "..", "package.json")
|
|
2860
254
|
];
|
|
2861
255
|
for (const pkgPath of candidates) {
|
|
2862
256
|
try {
|
|
2863
|
-
const pkg = JSON.parse(
|
|
257
|
+
const pkg = JSON.parse(readFileSync(pkgPath, "utf-8"));
|
|
2864
258
|
if (typeof pkg.version === "string") {
|
|
2865
259
|
return pkg.version;
|
|
2866
260
|
}
|
|
@@ -2893,7 +287,7 @@ function createStatusTool(deps) {
|
|
|
2893
287
|
return tool4({
|
|
2894
288
|
description: "Get status of a browser session.",
|
|
2895
289
|
args: {
|
|
2896
|
-
sessionId:
|
|
290
|
+
sessionId: z4.string().describe("Session id")
|
|
2897
291
|
},
|
|
2898
292
|
async execute(args) {
|
|
2899
293
|
try {
|
|
@@ -2926,13 +320,13 @@ function createStatusTool(deps) {
|
|
|
2926
320
|
|
|
2927
321
|
// src/tools/targets_list.ts
|
|
2928
322
|
import { tool as tool5 } from "@opencode-ai/plugin";
|
|
2929
|
-
var
|
|
323
|
+
var z5 = tool5.schema;
|
|
2930
324
|
function createTargetsListTool(deps) {
|
|
2931
325
|
return tool5({
|
|
2932
326
|
description: "List targets (tabs) in the current session.",
|
|
2933
327
|
args: {
|
|
2934
|
-
sessionId:
|
|
2935
|
-
includeUrls:
|
|
328
|
+
sessionId: z5.string().describe("Session id"),
|
|
329
|
+
includeUrls: z5.boolean().optional().describe("Include target URLs")
|
|
2936
330
|
},
|
|
2937
331
|
async execute(args) {
|
|
2938
332
|
try {
|
|
@@ -2950,13 +344,13 @@ function createTargetsListTool(deps) {
|
|
|
2950
344
|
|
|
2951
345
|
// src/tools/target_use.ts
|
|
2952
346
|
import { tool as tool6 } from "@opencode-ai/plugin";
|
|
2953
|
-
var
|
|
347
|
+
var z6 = tool6.schema;
|
|
2954
348
|
function createTargetUseTool(deps) {
|
|
2955
349
|
return tool6({
|
|
2956
350
|
description: "Set the active target (tab).",
|
|
2957
351
|
args: {
|
|
2958
|
-
sessionId:
|
|
2959
|
-
targetId:
|
|
352
|
+
sessionId: z6.string().describe("Session id"),
|
|
353
|
+
targetId: z6.string().describe("Target id")
|
|
2960
354
|
},
|
|
2961
355
|
async execute(args) {
|
|
2962
356
|
try {
|
|
@@ -2975,13 +369,13 @@ function createTargetUseTool(deps) {
|
|
|
2975
369
|
|
|
2976
370
|
// src/tools/target_new.ts
|
|
2977
371
|
import { tool as tool7 } from "@opencode-ai/plugin";
|
|
2978
|
-
var
|
|
372
|
+
var z7 = tool7.schema;
|
|
2979
373
|
function createTargetNewTool(deps) {
|
|
2980
374
|
return tool7({
|
|
2981
375
|
description: "Open a new target (tab).",
|
|
2982
376
|
args: {
|
|
2983
|
-
sessionId:
|
|
2984
|
-
url:
|
|
377
|
+
sessionId: z7.string().describe("Session id"),
|
|
378
|
+
url: z7.string().optional().describe("Optional URL to open")
|
|
2985
379
|
},
|
|
2986
380
|
async execute(args) {
|
|
2987
381
|
try {
|
|
@@ -2996,13 +390,13 @@ function createTargetNewTool(deps) {
|
|
|
2996
390
|
|
|
2997
391
|
// src/tools/target_close.ts
|
|
2998
392
|
import { tool as tool8 } from "@opencode-ai/plugin";
|
|
2999
|
-
var
|
|
393
|
+
var z8 = tool8.schema;
|
|
3000
394
|
function createTargetCloseTool(deps) {
|
|
3001
395
|
return tool8({
|
|
3002
396
|
description: "Close a target (tab).",
|
|
3003
397
|
args: {
|
|
3004
|
-
sessionId:
|
|
3005
|
-
targetId:
|
|
398
|
+
sessionId: z8.string().describe("Session id"),
|
|
399
|
+
targetId: z8.string().describe("Target id")
|
|
3006
400
|
},
|
|
3007
401
|
async execute(args) {
|
|
3008
402
|
try {
|
|
@@ -3017,14 +411,14 @@ function createTargetCloseTool(deps) {
|
|
|
3017
411
|
|
|
3018
412
|
// src/tools/page.ts
|
|
3019
413
|
import { tool as tool9 } from "@opencode-ai/plugin";
|
|
3020
|
-
var
|
|
414
|
+
var z9 = tool9.schema;
|
|
3021
415
|
function createPageTool(deps) {
|
|
3022
416
|
return tool9({
|
|
3023
417
|
description: "Open or focus a named page, optionally navigating to a URL.",
|
|
3024
418
|
args: {
|
|
3025
|
-
sessionId:
|
|
3026
|
-
name:
|
|
3027
|
-
url:
|
|
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")
|
|
3028
422
|
},
|
|
3029
423
|
async execute(args) {
|
|
3030
424
|
try {
|
|
@@ -3044,12 +438,12 @@ function createPageTool(deps) {
|
|
|
3044
438
|
|
|
3045
439
|
// src/tools/list.ts
|
|
3046
440
|
import { tool as tool10 } from "@opencode-ai/plugin";
|
|
3047
|
-
var
|
|
441
|
+
var z10 = tool10.schema;
|
|
3048
442
|
function createListTool(deps) {
|
|
3049
443
|
return tool10({
|
|
3050
444
|
description: "List named pages in the current session.",
|
|
3051
445
|
args: {
|
|
3052
|
-
sessionId:
|
|
446
|
+
sessionId: z10.string().describe("Active browser session id")
|
|
3053
447
|
},
|
|
3054
448
|
async execute(args) {
|
|
3055
449
|
try {
|
|
@@ -3064,13 +458,13 @@ function createListTool(deps) {
|
|
|
3064
458
|
|
|
3065
459
|
// src/tools/close.ts
|
|
3066
460
|
import { tool as tool11 } from "@opencode-ai/plugin";
|
|
3067
|
-
var
|
|
461
|
+
var z11 = tool11.schema;
|
|
3068
462
|
function createCloseTool(deps) {
|
|
3069
463
|
return tool11({
|
|
3070
464
|
description: "Close a named page within the current session.",
|
|
3071
465
|
args: {
|
|
3072
|
-
sessionId:
|
|
3073
|
-
name:
|
|
466
|
+
sessionId: z11.string().describe("Active browser session id"),
|
|
467
|
+
name: z11.string().describe("Named page to close")
|
|
3074
468
|
},
|
|
3075
469
|
async execute(args) {
|
|
3076
470
|
try {
|
|
@@ -3085,16 +479,16 @@ function createCloseTool(deps) {
|
|
|
3085
479
|
|
|
3086
480
|
// src/tools/goto.ts
|
|
3087
481
|
import { tool as tool12 } from "@opencode-ai/plugin";
|
|
3088
|
-
var
|
|
3089
|
-
var waitUntilSchema =
|
|
482
|
+
var z12 = tool12.schema;
|
|
483
|
+
var waitUntilSchema = z12.enum(["domcontentloaded", "load", "networkidle"]);
|
|
3090
484
|
function createGotoTool(deps) {
|
|
3091
485
|
return tool12({
|
|
3092
486
|
description: "Navigate the active target to a URL.",
|
|
3093
487
|
args: {
|
|
3094
|
-
sessionId:
|
|
3095
|
-
url:
|
|
488
|
+
sessionId: z12.string().describe("Session id"),
|
|
489
|
+
url: z12.string().describe("URL to navigate to"),
|
|
3096
490
|
waitUntil: waitUntilSchema.optional().describe("Load state to wait for"),
|
|
3097
|
-
timeoutMs:
|
|
491
|
+
timeoutMs: z12.number().int().optional().describe("Timeout in milliseconds")
|
|
3098
492
|
},
|
|
3099
493
|
async execute(args) {
|
|
3100
494
|
try {
|
|
@@ -3114,18 +508,18 @@ function createGotoTool(deps) {
|
|
|
3114
508
|
|
|
3115
509
|
// src/tools/wait.ts
|
|
3116
510
|
import { tool as tool13 } from "@opencode-ai/plugin";
|
|
3117
|
-
var
|
|
3118
|
-
var waitUntilSchema2 =
|
|
3119
|
-
var waitStateSchema =
|
|
511
|
+
var z13 = tool13.schema;
|
|
512
|
+
var waitUntilSchema2 = z13.enum(["domcontentloaded", "load", "networkidle"]);
|
|
513
|
+
var waitStateSchema = z13.enum(["attached", "visible", "hidden"]);
|
|
3120
514
|
function createWaitTool(deps) {
|
|
3121
515
|
return tool13({
|
|
3122
516
|
description: "Wait for a load state or a ref state.",
|
|
3123
517
|
args: {
|
|
3124
|
-
sessionId:
|
|
518
|
+
sessionId: z13.string().describe("Session id"),
|
|
3125
519
|
until: waitUntilSchema2.optional().describe("Load state to wait for"),
|
|
3126
|
-
ref:
|
|
520
|
+
ref: z13.string().optional().describe("Ref to wait for"),
|
|
3127
521
|
state: waitStateSchema.optional().describe("Ref state to wait for"),
|
|
3128
|
-
timeoutMs:
|
|
522
|
+
timeoutMs: z13.number().int().optional().describe("Timeout in milliseconds")
|
|
3129
523
|
},
|
|
3130
524
|
async execute(args) {
|
|
3131
525
|
try {
|
|
@@ -3156,16 +550,16 @@ function createWaitTool(deps) {
|
|
|
3156
550
|
|
|
3157
551
|
// src/tools/snapshot.ts
|
|
3158
552
|
import { tool as tool14 } from "@opencode-ai/plugin";
|
|
3159
|
-
var
|
|
3160
|
-
var formatSchema =
|
|
553
|
+
var z14 = tool14.schema;
|
|
554
|
+
var formatSchema = z14.enum(["outline", "actionables"]);
|
|
3161
555
|
function createSnapshotTool(deps) {
|
|
3162
556
|
return tool14({
|
|
3163
557
|
description: "Capture a snapshot of the current page and return refs.",
|
|
3164
558
|
args: {
|
|
3165
|
-
sessionId:
|
|
559
|
+
sessionId: z14.string().describe("Session id"),
|
|
3166
560
|
format: formatSchema.optional().describe("Snapshot format"),
|
|
3167
|
-
maxChars:
|
|
3168
|
-
cursor:
|
|
561
|
+
maxChars: z14.number().int().optional().describe("Max characters for snapshot output"),
|
|
562
|
+
cursor: z14.string().optional().describe("Cursor for paging")
|
|
3169
563
|
},
|
|
3170
564
|
async execute(args) {
|
|
3171
565
|
try {
|
|
@@ -3186,13 +580,13 @@ function createSnapshotTool(deps) {
|
|
|
3186
580
|
|
|
3187
581
|
// src/tools/click.ts
|
|
3188
582
|
import { tool as tool15 } from "@opencode-ai/plugin";
|
|
3189
|
-
var
|
|
583
|
+
var z15 = tool15.schema;
|
|
3190
584
|
function createClickTool(deps) {
|
|
3191
585
|
return tool15({
|
|
3192
586
|
description: "Click a referenced element.",
|
|
3193
587
|
args: {
|
|
3194
|
-
sessionId:
|
|
3195
|
-
ref:
|
|
588
|
+
sessionId: z15.string().describe("Session id"),
|
|
589
|
+
ref: z15.string().describe("Element ref")
|
|
3196
590
|
},
|
|
3197
591
|
async execute(args) {
|
|
3198
592
|
try {
|
|
@@ -3207,16 +601,16 @@ function createClickTool(deps) {
|
|
|
3207
601
|
|
|
3208
602
|
// src/tools/type.ts
|
|
3209
603
|
import { tool as tool16 } from "@opencode-ai/plugin";
|
|
3210
|
-
var
|
|
604
|
+
var z16 = tool16.schema;
|
|
3211
605
|
function createTypeTool(deps) {
|
|
3212
606
|
return tool16({
|
|
3213
607
|
description: "Type text into a referenced input.",
|
|
3214
608
|
args: {
|
|
3215
|
-
sessionId:
|
|
3216
|
-
ref:
|
|
3217
|
-
text:
|
|
3218
|
-
clear:
|
|
3219
|
-
submit:
|
|
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")
|
|
3220
614
|
},
|
|
3221
615
|
async execute(args) {
|
|
3222
616
|
try {
|
|
@@ -3237,14 +631,14 @@ function createTypeTool(deps) {
|
|
|
3237
631
|
|
|
3238
632
|
// src/tools/select.ts
|
|
3239
633
|
import { tool as tool17 } from "@opencode-ai/plugin";
|
|
3240
|
-
var
|
|
634
|
+
var z17 = tool17.schema;
|
|
3241
635
|
function createSelectTool(deps) {
|
|
3242
636
|
return tool17({
|
|
3243
637
|
description: "Select options in a referenced select element.",
|
|
3244
638
|
args: {
|
|
3245
|
-
sessionId:
|
|
3246
|
-
ref:
|
|
3247
|
-
values:
|
|
639
|
+
sessionId: z17.string().describe("Session id"),
|
|
640
|
+
ref: z17.string().describe("Element ref"),
|
|
641
|
+
values: z17.array(z17.string()).describe("Values to select")
|
|
3248
642
|
},
|
|
3249
643
|
async execute(args) {
|
|
3250
644
|
try {
|
|
@@ -3259,14 +653,14 @@ function createSelectTool(deps) {
|
|
|
3259
653
|
|
|
3260
654
|
// src/tools/scroll.ts
|
|
3261
655
|
import { tool as tool18 } from "@opencode-ai/plugin";
|
|
3262
|
-
var
|
|
656
|
+
var z18 = tool18.schema;
|
|
3263
657
|
function createScrollTool(deps) {
|
|
3264
658
|
return tool18({
|
|
3265
659
|
description: "Scroll the page or a referenced element.",
|
|
3266
660
|
args: {
|
|
3267
|
-
sessionId:
|
|
3268
|
-
dy:
|
|
3269
|
-
ref:
|
|
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")
|
|
3270
664
|
},
|
|
3271
665
|
async execute(args) {
|
|
3272
666
|
try {
|
|
@@ -3281,14 +675,14 @@ function createScrollTool(deps) {
|
|
|
3281
675
|
|
|
3282
676
|
// src/tools/dom_get_html.ts
|
|
3283
677
|
import { tool as tool19 } from "@opencode-ai/plugin";
|
|
3284
|
-
var
|
|
678
|
+
var z19 = tool19.schema;
|
|
3285
679
|
function createDomGetHtmlTool(deps) {
|
|
3286
680
|
return tool19({
|
|
3287
681
|
description: "Get outerHTML for a referenced element.",
|
|
3288
682
|
args: {
|
|
3289
|
-
sessionId:
|
|
3290
|
-
ref:
|
|
3291
|
-
maxChars:
|
|
683
|
+
sessionId: z19.string().describe("Session id"),
|
|
684
|
+
ref: z19.string().describe("Element ref"),
|
|
685
|
+
maxChars: z19.number().int().optional().describe("Max characters")
|
|
3292
686
|
},
|
|
3293
687
|
async execute(args) {
|
|
3294
688
|
try {
|
|
@@ -3311,14 +705,14 @@ function createDomGetHtmlTool(deps) {
|
|
|
3311
705
|
|
|
3312
706
|
// src/tools/dom_get_text.ts
|
|
3313
707
|
import { tool as tool20 } from "@opencode-ai/plugin";
|
|
3314
|
-
var
|
|
708
|
+
var z20 = tool20.schema;
|
|
3315
709
|
function createDomGetTextTool(deps) {
|
|
3316
710
|
return tool20({
|
|
3317
711
|
description: "Get inner text for a referenced element.",
|
|
3318
712
|
args: {
|
|
3319
|
-
sessionId:
|
|
3320
|
-
ref:
|
|
3321
|
-
maxChars:
|
|
713
|
+
sessionId: z20.string().describe("Session id"),
|
|
714
|
+
ref: z20.string().describe("Element ref"),
|
|
715
|
+
maxChars: z20.number().int().optional().describe("Max characters")
|
|
3322
716
|
},
|
|
3323
717
|
async execute(args) {
|
|
3324
718
|
try {
|
|
@@ -3341,19 +735,19 @@ function createDomGetTextTool(deps) {
|
|
|
3341
735
|
|
|
3342
736
|
// src/tools/run.ts
|
|
3343
737
|
import { tool as tool21 } from "@opencode-ai/plugin";
|
|
3344
|
-
var
|
|
3345
|
-
var stepSchema =
|
|
3346
|
-
action:
|
|
3347
|
-
args:
|
|
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")
|
|
3348
742
|
});
|
|
3349
743
|
function createRunTool(deps) {
|
|
3350
744
|
return tool21({
|
|
3351
745
|
description: "Run multiple actions in a single tool call.",
|
|
3352
746
|
args: {
|
|
3353
|
-
sessionId:
|
|
3354
|
-
steps:
|
|
3355
|
-
stopOnError:
|
|
3356
|
-
maxSnapshotChars:
|
|
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")
|
|
3357
751
|
},
|
|
3358
752
|
async execute(args) {
|
|
3359
753
|
try {
|
|
@@ -3387,12 +781,12 @@ function normalizeSteps(steps, maxSnapshotChars) {
|
|
|
3387
781
|
|
|
3388
782
|
// src/tools/prompting_guide.ts
|
|
3389
783
|
import { tool as tool22 } from "@opencode-ai/plugin";
|
|
3390
|
-
var
|
|
784
|
+
var z22 = tool22.schema;
|
|
3391
785
|
function createPromptingGuideTool(deps) {
|
|
3392
786
|
return tool22({
|
|
3393
787
|
description: "Return best-practice prompting guidance for OpenDevBrowser.",
|
|
3394
788
|
args: {
|
|
3395
|
-
topic:
|
|
789
|
+
topic: z22.string().optional().describe("Optional topic for guidance")
|
|
3396
790
|
},
|
|
3397
791
|
async execute(args) {
|
|
3398
792
|
try {
|
|
@@ -3407,14 +801,14 @@ function createPromptingGuideTool(deps) {
|
|
|
3407
801
|
|
|
3408
802
|
// src/tools/console_poll.ts
|
|
3409
803
|
import { tool as tool23 } from "@opencode-ai/plugin";
|
|
3410
|
-
var
|
|
804
|
+
var z23 = tool23.schema;
|
|
3411
805
|
function createConsolePollTool(deps) {
|
|
3412
806
|
return tool23({
|
|
3413
807
|
description: "Poll console events for the active target.",
|
|
3414
808
|
args: {
|
|
3415
|
-
sessionId:
|
|
3416
|
-
sinceSeq:
|
|
3417
|
-
max:
|
|
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")
|
|
3418
812
|
},
|
|
3419
813
|
async execute(args) {
|
|
3420
814
|
try {
|
|
@@ -3433,14 +827,14 @@ function createConsolePollTool(deps) {
|
|
|
3433
827
|
|
|
3434
828
|
// src/tools/network_poll.ts
|
|
3435
829
|
import { tool as tool24 } from "@opencode-ai/plugin";
|
|
3436
|
-
var
|
|
830
|
+
var z24 = tool24.schema;
|
|
3437
831
|
function createNetworkPollTool(deps) {
|
|
3438
832
|
return tool24({
|
|
3439
833
|
description: "Poll network events for the active target.",
|
|
3440
834
|
args: {
|
|
3441
|
-
sessionId:
|
|
3442
|
-
sinceSeq:
|
|
3443
|
-
max:
|
|
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")
|
|
3444
838
|
},
|
|
3445
839
|
async execute(args) {
|
|
3446
840
|
try {
|
|
@@ -3459,12 +853,12 @@ function createNetworkPollTool(deps) {
|
|
|
3459
853
|
|
|
3460
854
|
// src/tools/clone_page.ts
|
|
3461
855
|
import { tool as tool25 } from "@opencode-ai/plugin";
|
|
3462
|
-
var
|
|
856
|
+
var z25 = tool25.schema;
|
|
3463
857
|
function createClonePageTool(deps) {
|
|
3464
858
|
return tool25({
|
|
3465
859
|
description: "Export the active page as a React component and CSS bundle.",
|
|
3466
860
|
args: {
|
|
3467
|
-
sessionId:
|
|
861
|
+
sessionId: z25.string().describe("Active browser session id")
|
|
3468
862
|
},
|
|
3469
863
|
async execute(args) {
|
|
3470
864
|
try {
|
|
@@ -3479,13 +873,13 @@ function createClonePageTool(deps) {
|
|
|
3479
873
|
|
|
3480
874
|
// src/tools/clone_component.ts
|
|
3481
875
|
import { tool as tool26 } from "@opencode-ai/plugin";
|
|
3482
|
-
var
|
|
876
|
+
var z26 = tool26.schema;
|
|
3483
877
|
function createCloneComponentTool(deps) {
|
|
3484
878
|
return tool26({
|
|
3485
879
|
description: "Export a selected element subtree as a React component and CSS bundle.",
|
|
3486
880
|
args: {
|
|
3487
|
-
sessionId:
|
|
3488
|
-
ref:
|
|
881
|
+
sessionId: z26.string().describe("Active browser session id"),
|
|
882
|
+
ref: z26.string().describe("Element ref from snapshot")
|
|
3489
883
|
},
|
|
3490
884
|
async execute(args) {
|
|
3491
885
|
try {
|
|
@@ -3500,12 +894,12 @@ function createCloneComponentTool(deps) {
|
|
|
3500
894
|
|
|
3501
895
|
// src/tools/perf.ts
|
|
3502
896
|
import { tool as tool27 } from "@opencode-ai/plugin";
|
|
3503
|
-
var
|
|
897
|
+
var z27 = tool27.schema;
|
|
3504
898
|
function createPerfTool(deps) {
|
|
3505
899
|
return tool27({
|
|
3506
900
|
description: "Fetch lightweight performance metrics from the active page.",
|
|
3507
901
|
args: {
|
|
3508
|
-
sessionId:
|
|
902
|
+
sessionId: z27.string().describe("Active browser session id")
|
|
3509
903
|
},
|
|
3510
904
|
async execute(args) {
|
|
3511
905
|
try {
|
|
@@ -3520,13 +914,13 @@ function createPerfTool(deps) {
|
|
|
3520
914
|
|
|
3521
915
|
// src/tools/screenshot.ts
|
|
3522
916
|
import { tool as tool28 } from "@opencode-ai/plugin";
|
|
3523
|
-
var
|
|
917
|
+
var z28 = tool28.schema;
|
|
3524
918
|
function createScreenshotTool(deps) {
|
|
3525
919
|
return tool28({
|
|
3526
920
|
description: "Capture a screenshot of the active page.",
|
|
3527
921
|
args: {
|
|
3528
|
-
sessionId:
|
|
3529
|
-
path:
|
|
922
|
+
sessionId: z28.string().describe("Active browser session id"),
|
|
923
|
+
path: z28.string().optional().describe("Optional output file path")
|
|
3530
924
|
},
|
|
3531
925
|
async execute(args) {
|
|
3532
926
|
try {
|
|
@@ -3559,13 +953,13 @@ function createSkillListTool(deps) {
|
|
|
3559
953
|
|
|
3560
954
|
// src/tools/skill_load.ts
|
|
3561
955
|
import { tool as tool30 } from "@opencode-ai/plugin";
|
|
3562
|
-
var
|
|
956
|
+
var z29 = tool30.schema;
|
|
3563
957
|
function createSkillLoadTool(deps) {
|
|
3564
958
|
return tool30({
|
|
3565
959
|
description: "Load a specific skill by name from OpenCode skill directories (compatibility wrapper)",
|
|
3566
960
|
args: {
|
|
3567
|
-
name:
|
|
3568
|
-
topic:
|
|
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")
|
|
3569
963
|
},
|
|
3570
964
|
async execute(args) {
|
|
3571
965
|
try {
|
|
@@ -3617,93 +1011,60 @@ function createTools(deps) {
|
|
|
3617
1011
|
|
|
3618
1012
|
// src/index.ts
|
|
3619
1013
|
var OpenDevBrowserPlugin = async ({ directory, worktree }) => {
|
|
3620
|
-
const
|
|
3621
|
-
const configStore =
|
|
3622
|
-
const cacheRoot = worktree || directory;
|
|
3623
|
-
const manager = new BrowserManager(cacheRoot, initialConfig);
|
|
3624
|
-
const runner = new ScriptRunner(manager);
|
|
3625
|
-
const skills = new SkillLoader(cacheRoot, initialConfig.skillPaths);
|
|
3626
|
-
const relay = new RelayServer();
|
|
1014
|
+
const core = createOpenDevBrowserCore({ directory, worktree });
|
|
1015
|
+
const { config, configStore, manager, runner, skills, relay, ensureRelay, cleanup, getExtensionPath } = core;
|
|
3627
1016
|
const skillNudgeState = createSkillNudgeState();
|
|
3628
1017
|
const continuityNudgeState = createContinuityNudgeState();
|
|
3629
|
-
relay.setToken(initialConfig.relayToken);
|
|
3630
1018
|
console.info(
|
|
3631
|
-
`[opendevbrowser] loaded (cacheRoot=${cacheRoot}, relay=${
|
|
1019
|
+
`[opendevbrowser] loaded (cacheRoot=${core.cacheRoot}, relay=${config.relayToken === false ? "disabled" : "enabled"})`
|
|
3632
1020
|
);
|
|
3633
1021
|
try {
|
|
3634
1022
|
extractExtension();
|
|
3635
1023
|
} catch (error) {
|
|
3636
1024
|
console.warn("Extension extraction failed:", error instanceof Error ? error.message : error);
|
|
3637
1025
|
}
|
|
3638
|
-
const ensureRelay = async (port) => {
|
|
3639
|
-
if (port <= 0 || initialConfig.relayToken === false) {
|
|
3640
|
-
relay.stop();
|
|
3641
|
-
return;
|
|
3642
|
-
}
|
|
3643
|
-
const status = relay.status();
|
|
3644
|
-
if (status.running && status.port === port) {
|
|
3645
|
-
return;
|
|
3646
|
-
}
|
|
3647
|
-
relay.stop();
|
|
3648
|
-
try {
|
|
3649
|
-
await relay.start(port);
|
|
3650
|
-
} catch (error) {
|
|
3651
|
-
const message = error instanceof Error ? error.message : String(error);
|
|
3652
|
-
if (message.includes("EADDRINUSE") || message.includes("in use")) {
|
|
3653
|
-
console.warn(`[opendevbrowser] Relay server port ${port} is already in use. Extension pairing will be unavailable.`);
|
|
3654
|
-
console.warn(`[opendevbrowser] To fix: kill the process using port ${port} or change relayPort in config.`);
|
|
3655
|
-
} else {
|
|
3656
|
-
console.warn(`[opendevbrowser] Failed to start relay server: ${message}`);
|
|
3657
|
-
}
|
|
3658
|
-
}
|
|
3659
|
-
};
|
|
3660
|
-
const cleanup = () => {
|
|
3661
|
-
relay.stop();
|
|
3662
|
-
manager.closeAll().catch(() => {
|
|
3663
|
-
});
|
|
3664
|
-
};
|
|
3665
1026
|
process.on("SIGINT", cleanup);
|
|
3666
1027
|
process.on("SIGTERM", cleanup);
|
|
3667
1028
|
process.on("beforeExit", cleanup);
|
|
3668
|
-
await ensureRelay(
|
|
1029
|
+
await ensureRelay(config.relayPort);
|
|
3669
1030
|
return {
|
|
3670
1031
|
tool: createTools({ manager, runner, config: configStore, skills, relay, getExtensionPath }),
|
|
3671
1032
|
"chat.message": async (_input, output) => {
|
|
3672
|
-
const
|
|
1033
|
+
const config2 = configStore.get();
|
|
3673
1034
|
if (output.message.role !== "user") return;
|
|
3674
1035
|
const text = extractTextFromParts(output.parts);
|
|
3675
1036
|
if (!text) return;
|
|
3676
|
-
if (
|
|
1037
|
+
if (config2.skills.nudge.enabled && shouldTriggerSkillNudge(text, config2.skills.nudge.keywords)) {
|
|
3677
1038
|
markSkillNudge(skillNudgeState, Date.now());
|
|
3678
1039
|
}
|
|
3679
|
-
if (
|
|
3680
|
-
if (shouldTriggerContinuityNudge(text,
|
|
1040
|
+
if (config2.continuity.enabled && config2.continuity.nudge.enabled) {
|
|
1041
|
+
if (shouldTriggerContinuityNudge(text, config2.continuity.nudge.keywords)) {
|
|
3681
1042
|
markContinuityNudge(continuityNudgeState, Date.now());
|
|
3682
1043
|
}
|
|
3683
1044
|
}
|
|
3684
1045
|
},
|
|
3685
1046
|
"experimental.chat.system.transform": async (_input, output) => {
|
|
3686
|
-
const
|
|
1047
|
+
const config2 = configStore.get();
|
|
3687
1048
|
const systemEntries = output.system ?? [];
|
|
3688
1049
|
let nextEntries = systemEntries;
|
|
3689
1050
|
let changed = false;
|
|
3690
|
-
if (
|
|
1051
|
+
if (config2.skills.nudge.enabled) {
|
|
3691
1052
|
if (systemEntries.some((entry) => entry.includes(SKILL_NUDGE_MARKER))) {
|
|
3692
1053
|
clearSkillNudge(skillNudgeState);
|
|
3693
|
-
} else if (consumeSkillNudge(skillNudgeState, Date.now(),
|
|
1054
|
+
} else if (consumeSkillNudge(skillNudgeState, Date.now(), config2.skills.nudge.maxAgeMs)) {
|
|
3694
1055
|
nextEntries = [...nextEntries, buildSkillNudgeMessage()];
|
|
3695
1056
|
changed = true;
|
|
3696
1057
|
}
|
|
3697
1058
|
}
|
|
3698
|
-
if (
|
|
1059
|
+
if (config2.continuity.enabled && config2.continuity.nudge.enabled) {
|
|
3699
1060
|
if (systemEntries.some((entry) => entry.includes(CONTINUITY_NUDGE_MARKER))) {
|
|
3700
1061
|
clearContinuityNudge(continuityNudgeState);
|
|
3701
1062
|
} else if (consumeContinuityNudge(
|
|
3702
1063
|
continuityNudgeState,
|
|
3703
1064
|
Date.now(),
|
|
3704
|
-
|
|
1065
|
+
config2.continuity.nudge.maxAgeMs
|
|
3705
1066
|
)) {
|
|
3706
|
-
nextEntries = [...nextEntries, buildContinuityNudgeMessage(
|
|
1067
|
+
nextEntries = [...nextEntries, buildContinuityNudgeMessage(config2.continuity.filePath)];
|
|
3707
1068
|
changed = true;
|
|
3708
1069
|
}
|
|
3709
1070
|
}
|
|
@@ -3717,5 +1078,4 @@ var index_default = OpenDevBrowserPlugin;
|
|
|
3717
1078
|
export {
|
|
3718
1079
|
index_default as default
|
|
3719
1080
|
};
|
|
3720
|
-
/* v8 ignore next -- @preserve */
|
|
3721
1081
|
//# sourceMappingURL=index.js.map
|