opendevbrowser 0.0.11 → 0.0.15
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/LICENSE +21 -0
- package/README.md +289 -28
- package/dist/chunk-JVBMT2O5.js +7173 -0
- package/dist/chunk-JVBMT2O5.js.map +1 -0
- package/dist/cli/index.js +3690 -275
- package/dist/cli/index.js.map +1 -1
- package/dist/index.js +1080 -2857
- package/dist/index.js.map +1 -1
- package/dist/opendevbrowser.js +1080 -2857
- package/dist/opendevbrowser.js.map +1 -1
- package/extension/dist/annotate-content.css +237 -0
- package/extension/dist/annotate-content.js +934 -0
- package/extension/dist/background.js +1291 -8
- package/extension/dist/logging.js +50 -0
- package/extension/dist/ops/dom-bridge.js +355 -0
- package/extension/dist/ops/ops-runtime.js +1249 -0
- package/extension/dist/ops/ops-session-store.js +189 -0
- package/extension/dist/ops/redaction.js +52 -0
- package/extension/dist/ops/snapshot-builder.js +4 -0
- package/extension/dist/ops/snapshot-shared.js +220 -0
- package/extension/dist/popup.js +398 -21
- package/extension/dist/relay-settings.js +3 -1
- package/extension/dist/services/CDPRouter.js +501 -103
- package/extension/dist/services/ConnectionManager.js +464 -57
- package/extension/dist/services/NativePortManager.js +182 -0
- package/extension/dist/services/RelayClient.js +227 -26
- package/extension/dist/services/TabManager.js +81 -0
- package/extension/dist/services/TargetSessionMap.js +146 -0
- package/extension/dist/services/cdp-router-commands.js +203 -0
- package/extension/dist/services/url-restrictions.js +41 -0
- package/extension/dist/types.js +3 -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 +17 -3
- package/extension/popup.html +469 -65
- package/package.json +2 -2
- package/skills/AGENTS.md +34 -61
- package/skills/data-extraction/SKILL.md +95 -103
- package/skills/form-testing/SKILL.md +75 -82
- package/skills/login-automation/SKILL.md +76 -66
- package/skills/opendevbrowser-best-practices/SKILL.md +90 -49
- package/skills/opendevbrowser-continuity-ledger/SKILL.md +57 -23
- package/dist/chunk-R5VUZEUU.js +0 -128
- package/dist/chunk-R5VUZEUU.js.map +0 -1
- package/extension/dist/popup.jsx +0 -150
package/dist/opendevbrowser.js
CHANGED
|
@@ -1,2208 +1,211 @@
|
|
|
1
1
|
import {
|
|
2
|
+
DaemonClient,
|
|
3
|
+
ScriptRunner,
|
|
4
|
+
buildAnnotateResult,
|
|
5
|
+
createOpenDevBrowserCore,
|
|
2
6
|
extractExtension,
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
} from "./chunk-
|
|
7
|
+
fetchDaemonStatusFromMetadata,
|
|
8
|
+
startDaemon
|
|
9
|
+
} from "./chunk-JVBMT2O5.js";
|
|
6
10
|
|
|
7
|
-
// src/
|
|
8
|
-
|
|
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) {
|
|
11
|
+
// src/cli/remote-manager.ts
|
|
12
|
+
function isLegacyRelayEndpoint(wsEndpoint) {
|
|
14
13
|
try {
|
|
15
|
-
|
|
16
|
-
|
|
14
|
+
const url = new URL(wsEndpoint);
|
|
15
|
+
const path = url.pathname.endsWith("/") ? url.pathname.slice(0, -1) : url.pathname;
|
|
16
|
+
return path === "/cdp";
|
|
17
17
|
} catch {
|
|
18
18
|
return false;
|
|
19
19
|
}
|
|
20
20
|
}
|
|
21
|
-
var
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
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");
|
|
21
|
+
var RemoteManager = class {
|
|
22
|
+
client;
|
|
23
|
+
constructor(client) {
|
|
24
|
+
this.client = client;
|
|
274
25
|
}
|
|
275
|
-
|
|
276
|
-
|
|
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
|
-
}
|
|
26
|
+
launch(options) {
|
|
27
|
+
return this.client.call("session.launch", options);
|
|
330
28
|
}
|
|
331
|
-
|
|
332
|
-
|
|
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);
|
|
29
|
+
connect(options) {
|
|
30
|
+
return this.client.call("session.connect", options);
|
|
350
31
|
}
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
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
|
|
32
|
+
connectRelay(wsEndpoint) {
|
|
33
|
+
return this.client.call(
|
|
34
|
+
"session.connect",
|
|
35
|
+
isLegacyRelayEndpoint(wsEndpoint) ? { wsEndpoint, extensionLegacy: true } : { wsEndpoint }
|
|
383
36
|
);
|
|
384
|
-
parsed.pathname = redactedSegments.join("/");
|
|
385
|
-
return parsed.toString();
|
|
386
|
-
} catch {
|
|
387
|
-
return rawUrl.split(/[?#]/)[0] ?? rawUrl;
|
|
388
37
|
}
|
|
389
|
-
|
|
390
|
-
|
|
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;
|
|
38
|
+
disconnect(sessionId, closeBrowser = false) {
|
|
39
|
+
return this.client.call("session.disconnect", { sessionId, closeBrowser });
|
|
401
40
|
}
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
this.showFullUrls = options.showFullUrls;
|
|
405
|
-
}
|
|
41
|
+
status(sessionId) {
|
|
42
|
+
return this.client.call("session.status", { sessionId });
|
|
406
43
|
}
|
|
407
|
-
|
|
408
|
-
|
|
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);
|
|
44
|
+
goto(sessionId, url, waitUntil = "load", timeoutMs = 3e4) {
|
|
45
|
+
return this.client.call("nav.goto", { sessionId, url, waitUntil, timeoutMs });
|
|
431
46
|
}
|
|
432
|
-
|
|
433
|
-
|
|
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;
|
|
47
|
+
waitForLoad(sessionId, until, timeoutMs = 3e4) {
|
|
48
|
+
return this.client.call("nav.wait", { sessionId, until, timeoutMs });
|
|
442
49
|
}
|
|
443
|
-
|
|
444
|
-
|
|
445
|
-
const last = events[events.length - 1];
|
|
446
|
-
const nextSeq = last ? last.seq : sinceSeq;
|
|
447
|
-
return { events, nextSeq };
|
|
50
|
+
waitForRef(sessionId, ref, state = "attached", timeoutMs = 3e4) {
|
|
51
|
+
return this.client.call("nav.wait", { sessionId, ref, state, timeoutMs });
|
|
448
52
|
}
|
|
449
|
-
|
|
450
|
-
this.
|
|
451
|
-
this.events.push({
|
|
452
|
-
seq: this.seq,
|
|
453
|
-
...event
|
|
454
|
-
});
|
|
455
|
-
if (this.events.length > this.maxEvents) {
|
|
456
|
-
this.events.shift();
|
|
457
|
-
}
|
|
53
|
+
snapshot(sessionId, mode, maxChars, cursor) {
|
|
54
|
+
return this.client.call("nav.snapshot", { sessionId, mode, maxChars, cursor });
|
|
458
55
|
}
|
|
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};`);
|
|
56
|
+
click(sessionId, ref) {
|
|
57
|
+
return this.client.call("interact.click", { sessionId, ref });
|
|
615
58
|
}
|
|
616
|
-
|
|
617
|
-
|
|
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.");
|
|
59
|
+
hover(sessionId, ref) {
|
|
60
|
+
return this.client.call("interact.hover", { sessionId, ref });
|
|
790
61
|
}
|
|
791
|
-
|
|
792
|
-
|
|
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 };
|
|
62
|
+
press(sessionId, key, ref) {
|
|
63
|
+
return this.client.call("interact.press", { sessionId, key, ref });
|
|
816
64
|
}
|
|
817
|
-
|
|
818
|
-
|
|
819
|
-
if (!map) return null;
|
|
820
|
-
return map.get(ref) ?? null;
|
|
65
|
+
check(sessionId, ref) {
|
|
66
|
+
return this.client.call("interact.check", { sessionId, ref });
|
|
821
67
|
}
|
|
822
|
-
|
|
823
|
-
return this.
|
|
68
|
+
uncheck(sessionId, ref) {
|
|
69
|
+
return this.client.call("interact.uncheck", { sessionId, ref });
|
|
824
70
|
}
|
|
825
|
-
|
|
826
|
-
|
|
827
|
-
return map ? map.size : 0;
|
|
71
|
+
type(sessionId, ref, text, clear = false, submit = false) {
|
|
72
|
+
return this.client.call("interact.type", { sessionId, ref, text, clear, submit });
|
|
828
73
|
}
|
|
829
|
-
|
|
830
|
-
this.
|
|
831
|
-
this.snapshotByTarget.delete(targetId);
|
|
74
|
+
select(sessionId, ref, values) {
|
|
75
|
+
return this.client.call("interact.select", { sessionId, ref, values });
|
|
832
76
|
}
|
|
833
|
-
|
|
834
|
-
|
|
835
|
-
// src/snapshot/snapshotter.ts
|
|
836
|
-
var Snapshotter = class {
|
|
837
|
-
refStore;
|
|
838
|
-
constructor(refStore) {
|
|
839
|
-
this.refStore = refStore;
|
|
77
|
+
scroll(sessionId, dy, ref) {
|
|
78
|
+
return this.client.call("interact.scroll", { sessionId, dy, ref });
|
|
840
79
|
}
|
|
841
|
-
|
|
842
|
-
|
|
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
|
-
};
|
|
80
|
+
scrollIntoView(sessionId, ref) {
|
|
81
|
+
return this.client.call("interact.scrollIntoView", { sessionId, ref });
|
|
876
82
|
}
|
|
877
|
-
|
|
878
|
-
|
|
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) + '"]';
|
|
83
|
+
domGetHtml(sessionId, ref, maxChars = 8e3) {
|
|
84
|
+
return this.client.call("dom.getHtml", { sessionId, ref, maxChars });
|
|
931
85
|
}
|
|
932
|
-
|
|
933
|
-
|
|
934
|
-
return '[aria-label="' + escape(ariaLabel) + '"]';
|
|
86
|
+
domGetText(sessionId, ref, maxChars = 8e3) {
|
|
87
|
+
return this.client.call("dom.getText", { sessionId, ref, maxChars });
|
|
935
88
|
}
|
|
936
|
-
|
|
937
|
-
|
|
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.`);
|
|
89
|
+
domGetAttr(sessionId, ref, name) {
|
|
90
|
+
return this.client.call("dom.getAttr", { sessionId, ref, name });
|
|
1013
91
|
}
|
|
1014
|
-
|
|
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;
|
|
92
|
+
domGetValue(sessionId, ref) {
|
|
93
|
+
return this.client.call("dom.getValue", { sessionId, ref });
|
|
1028
94
|
}
|
|
1029
|
-
|
|
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;
|
|
95
|
+
domIsVisible(sessionId, ref) {
|
|
96
|
+
return this.client.call("dom.isVisible", { sessionId, ref });
|
|
1058
97
|
}
|
|
1059
|
-
|
|
1060
|
-
|
|
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");
|
|
98
|
+
domIsEnabled(sessionId, ref) {
|
|
99
|
+
return this.client.call("dom.isEnabled", { sessionId, ref });
|
|
1075
100
|
}
|
|
1076
|
-
|
|
1077
|
-
|
|
101
|
+
domIsChecked(sessionId, ref) {
|
|
102
|
+
return this.client.call("dom.isChecked", { sessionId, ref });
|
|
1078
103
|
}
|
|
1079
|
-
|
|
1080
|
-
|
|
104
|
+
clonePage(sessionId) {
|
|
105
|
+
return this.client.call("export.clonePage", { sessionId });
|
|
1081
106
|
}
|
|
1082
|
-
|
|
1083
|
-
|
|
107
|
+
cloneComponent(sessionId, ref) {
|
|
108
|
+
return this.client.call("export.cloneComponent", { sessionId, ref });
|
|
1084
109
|
}
|
|
1085
|
-
|
|
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);
|
|
110
|
+
perfMetrics(sessionId) {
|
|
111
|
+
return this.client.call("devtools.perf", { sessionId });
|
|
1097
112
|
}
|
|
1098
|
-
|
|
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") };
|
|
113
|
+
screenshot(sessionId, path) {
|
|
114
|
+
return this.client.call("page.screenshot", { sessionId, path });
|
|
1702
115
|
}
|
|
1703
116
|
consolePoll(sessionId, sinceSeq, max = 50) {
|
|
1704
|
-
|
|
1705
|
-
return managed.consoleTracker.poll(sinceSeq, max);
|
|
117
|
+
return this.client.call("devtools.consolePoll", { sessionId, sinceSeq, max });
|
|
1706
118
|
}
|
|
1707
119
|
networkPoll(sessionId, sinceSeq, max = 50) {
|
|
1708
|
-
|
|
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
|
-
}
|
|
120
|
+
return this.client.call("devtools.networkPoll", { sessionId, sinceSeq, max });
|
|
2029
121
|
}
|
|
2030
|
-
|
|
2031
|
-
|
|
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));
|
|
122
|
+
listTargets(sessionId, includeUrls = false) {
|
|
123
|
+
return this.client.call("targets.list", { sessionId, includeUrls });
|
|
2051
124
|
}
|
|
2052
|
-
|
|
2053
|
-
|
|
2054
|
-
return join5(os2.homedir(), p.slice(1));
|
|
2055
|
-
}
|
|
2056
|
-
return p;
|
|
125
|
+
useTarget(sessionId, targetId) {
|
|
126
|
+
return this.client.call("targets.use", { sessionId, targetId });
|
|
2057
127
|
}
|
|
2058
|
-
|
|
2059
|
-
return this.
|
|
128
|
+
newTarget(sessionId, url) {
|
|
129
|
+
return this.client.call("targets.new", { sessionId, url });
|
|
2060
130
|
}
|
|
2061
|
-
|
|
2062
|
-
|
|
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;
|
|
131
|
+
closeTarget(sessionId, targetId) {
|
|
132
|
+
return this.client.call("targets.close", { sessionId, targetId });
|
|
2075
133
|
}
|
|
2076
|
-
|
|
2077
|
-
|
|
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;
|
|
134
|
+
page(sessionId, name, url) {
|
|
135
|
+
return this.client.call("page.open", { sessionId, name, url });
|
|
2092
136
|
}
|
|
2093
|
-
|
|
2094
|
-
|
|
2095
|
-
|
|
2096
|
-
|
|
2097
|
-
|
|
2098
|
-
|
|
2099
|
-
|
|
2100
|
-
|
|
2101
|
-
|
|
2102
|
-
|
|
137
|
+
listPages(sessionId) {
|
|
138
|
+
return this.client.call("page.list", { sessionId });
|
|
139
|
+
}
|
|
140
|
+
closePage(sessionId, name) {
|
|
141
|
+
return this.client.call("page.close", { sessionId, name });
|
|
142
|
+
}
|
|
143
|
+
async withPage(_sessionId, _targetId, _fn) {
|
|
144
|
+
throw new Error("Direct annotate is unavailable via daemon-managed sessions.");
|
|
145
|
+
}
|
|
146
|
+
};
|
|
147
|
+
|
|
148
|
+
// src/cli/remote-relay.ts
|
|
149
|
+
var emptyStatus = {
|
|
150
|
+
running: false,
|
|
151
|
+
extensionConnected: false,
|
|
152
|
+
extensionHandshakeComplete: false,
|
|
153
|
+
cdpConnected: false,
|
|
154
|
+
annotationConnected: false,
|
|
155
|
+
opsConnected: false,
|
|
156
|
+
pairingRequired: false,
|
|
157
|
+
instanceId: "",
|
|
158
|
+
epoch: 0,
|
|
159
|
+
health: {
|
|
160
|
+
ok: false,
|
|
161
|
+
reason: "relay_down",
|
|
162
|
+
extensionConnected: false,
|
|
163
|
+
extensionHandshakeComplete: false,
|
|
164
|
+
cdpConnected: false,
|
|
165
|
+
annotationConnected: false,
|
|
166
|
+
opsConnected: false,
|
|
167
|
+
pairingRequired: false
|
|
2103
168
|
}
|
|
2104
|
-
|
|
2105
|
-
|
|
169
|
+
};
|
|
170
|
+
var RemoteRelay = class {
|
|
171
|
+
client;
|
|
172
|
+
lastStatus = emptyStatus;
|
|
173
|
+
lastCdpUrl = null;
|
|
174
|
+
lastAnnotationUrl = null;
|
|
175
|
+
lastOpsUrl = null;
|
|
176
|
+
constructor(client) {
|
|
177
|
+
this.client = client;
|
|
178
|
+
}
|
|
179
|
+
async refresh() {
|
|
2106
180
|
try {
|
|
2107
|
-
const
|
|
2108
|
-
|
|
2109
|
-
|
|
2110
|
-
|
|
2111
|
-
|
|
2112
|
-
|
|
2113
|
-
|
|
2114
|
-
|
|
2115
|
-
name: metadata.name,
|
|
2116
|
-
description: metadata.description,
|
|
2117
|
-
version: metadata.version ?? "1.0.0",
|
|
2118
|
-
path: skillPath
|
|
2119
|
-
});
|
|
2120
|
-
} catch {
|
|
2121
|
-
}
|
|
2122
|
-
}
|
|
181
|
+
const status = await this.client.call("relay.status");
|
|
182
|
+
this.lastStatus = status;
|
|
183
|
+
const cdpUrl = await this.client.call("relay.cdpUrl");
|
|
184
|
+
this.lastCdpUrl = typeof cdpUrl === "string" ? cdpUrl : null;
|
|
185
|
+
const annotationUrl = await this.client.call("relay.annotationUrl");
|
|
186
|
+
this.lastAnnotationUrl = typeof annotationUrl === "string" ? annotationUrl : null;
|
|
187
|
+
const opsUrl = await this.client.call("relay.opsUrl");
|
|
188
|
+
this.lastOpsUrl = typeof opsUrl === "string" ? opsUrl : null;
|
|
2123
189
|
} catch {
|
|
190
|
+
this.lastStatus = emptyStatus;
|
|
191
|
+
this.lastCdpUrl = null;
|
|
192
|
+
this.lastAnnotationUrl = null;
|
|
193
|
+
this.lastOpsUrl = null;
|
|
2124
194
|
}
|
|
2125
|
-
return skills;
|
|
2126
195
|
}
|
|
2127
|
-
|
|
2128
|
-
|
|
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;
|
|
196
|
+
status() {
|
|
197
|
+
return this.lastStatus;
|
|
2171
198
|
}
|
|
2172
|
-
|
|
2173
|
-
this.
|
|
199
|
+
getCdpUrl() {
|
|
200
|
+
return this.lastCdpUrl;
|
|
2174
201
|
}
|
|
2175
|
-
|
|
2176
|
-
|
|
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);
|
|
202
|
+
getAnnotationUrl() {
|
|
203
|
+
return this.lastAnnotationUrl;
|
|
2198
204
|
}
|
|
2199
|
-
|
|
2200
|
-
|
|
2201
|
-
if (matches.length === 0) {
|
|
2202
|
-
return null;
|
|
205
|
+
getOpsUrl() {
|
|
206
|
+
return this.lastOpsUrl;
|
|
2203
207
|
}
|
|
2204
|
-
|
|
2205
|
-
}
|
|
208
|
+
};
|
|
2206
209
|
|
|
2207
210
|
// src/skills/skill-nudge.ts
|
|
2208
211
|
var SKILL_NUDGE_MARKER = "[opendevbrowser:skill-nudge]";
|
|
@@ -2272,441 +275,6 @@ function buildContinuityNudgeMessage(filePath) {
|
|
|
2272
275
|
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
276
|
}
|
|
2274
277
|
|
|
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
278
|
// src/tools/launch.ts
|
|
2711
279
|
import { tool } from "@opencode-ai/plugin";
|
|
2712
280
|
|
|
@@ -2731,85 +299,363 @@ function serializeError(error) {
|
|
|
2731
299
|
}
|
|
2732
300
|
|
|
2733
301
|
// src/tools/launch.ts
|
|
2734
|
-
var
|
|
302
|
+
var z = tool.schema;
|
|
2735
303
|
function createLaunchTool(deps) {
|
|
2736
304
|
return tool({
|
|
2737
|
-
description: "Launch a
|
|
305
|
+
description: "Launch a browser session (extension relay first) and return a sessionId.",
|
|
2738
306
|
args: {
|
|
2739
|
-
profile:
|
|
2740
|
-
headless:
|
|
2741
|
-
startUrl:
|
|
2742
|
-
chromePath:
|
|
2743
|
-
flags:
|
|
2744
|
-
persistProfile:
|
|
307
|
+
profile: z.string().optional().describe("Profile name for persistent browsing"),
|
|
308
|
+
headless: z.boolean().optional().describe("Run Chrome in headless mode"),
|
|
309
|
+
startUrl: z.string().optional().describe("Optional URL to open after launch"),
|
|
310
|
+
chromePath: z.string().optional().describe("Override Chrome executable path"),
|
|
311
|
+
flags: z.array(z.string()).optional().describe("Extra Chrome flags"),
|
|
312
|
+
persistProfile: z.boolean().optional().describe("Persist profile data between sessions"),
|
|
313
|
+
noExtension: z.boolean().optional().describe("Skip extension relay and launch a new browser"),
|
|
314
|
+
extensionOnly: z.boolean().optional().describe("Require extension relay or fail"),
|
|
315
|
+
extensionLegacy: z.boolean().optional().describe("Use legacy extension relay (/cdp) instead of ops"),
|
|
316
|
+
waitForExtension: z.boolean().optional().describe("Wait for extension to connect before launching"),
|
|
317
|
+
waitTimeoutMs: z.number().int().optional().describe("Timeout for waiting on extension (ms)")
|
|
2745
318
|
},
|
|
2746
319
|
async execute(args) {
|
|
2747
|
-
|
|
2748
|
-
|
|
2749
|
-
|
|
2750
|
-
|
|
2751
|
-
|
|
2752
|
-
|
|
2753
|
-
|
|
2754
|
-
|
|
2755
|
-
|
|
2756
|
-
|
|
2757
|
-
|
|
2758
|
-
} catch {
|
|
2759
|
-
relayWarning = "Relay connection failed; falling back to managed Chrome.";
|
|
320
|
+
let attemptedRebind = false;
|
|
321
|
+
while (true) {
|
|
322
|
+
try {
|
|
323
|
+
await deps.relay?.refresh?.();
|
|
324
|
+
const config = deps.config.get();
|
|
325
|
+
const extensionLegacy = args.extensionLegacy === true;
|
|
326
|
+
let relayStatus = deps.relay?.status();
|
|
327
|
+
let relayUrl = extensionLegacy ? deps.relay?.getCdpUrl() ?? null : deps.relay?.getOpsUrl?.() ?? null;
|
|
328
|
+
const relayPort = relayStatus?.port;
|
|
329
|
+
if (!relayUrl && isValidPort(relayPort)) {
|
|
330
|
+
relayUrl = `ws://127.0.0.1:${relayPort}/${extensionLegacy ? "cdp" : "ops"}`;
|
|
2760
331
|
}
|
|
2761
|
-
|
|
2762
|
-
|
|
2763
|
-
|
|
2764
|
-
|
|
2765
|
-
|
|
2766
|
-
|
|
2767
|
-
|
|
2768
|
-
|
|
2769
|
-
|
|
332
|
+
const waitTimeoutMs = clampWaitTimeout(args.waitTimeoutMs ?? 3e4);
|
|
333
|
+
const headlessExplicit = args.headless === true;
|
|
334
|
+
const managedExplicit = Boolean(args.noExtension || headlessExplicit);
|
|
335
|
+
const managedHeadless = headlessExplicit ? true : false;
|
|
336
|
+
if (args.waitForExtension && !managedExplicit) {
|
|
337
|
+
const observedPort2 = resolveObservedPort(relayStatus, config.relayPort);
|
|
338
|
+
const connected = await waitForExtensionHandshake(deps.relay, observedPort2, waitTimeoutMs);
|
|
339
|
+
if (connected) {
|
|
340
|
+
relayStatus = deps.relay?.status() ?? relayStatus;
|
|
341
|
+
relayUrl = extensionLegacy ? deps.relay?.getCdpUrl() ?? relayUrl : deps.relay?.getOpsUrl?.() ?? relayUrl;
|
|
342
|
+
}
|
|
343
|
+
}
|
|
344
|
+
const observedPort = resolveObservedPort(relayStatus, config.relayPort);
|
|
345
|
+
const shouldFetchObserved = !managedExplicit && (!relayUrl || !(relayStatus?.extensionHandshakeComplete || relayStatus?.extensionConnected));
|
|
346
|
+
const observedStatus = shouldFetchObserved ? await fetchRelayObservedStatus(observedPort) : null;
|
|
347
|
+
if (!relayUrl) {
|
|
348
|
+
const fallbackPort = isValidPort(observedStatus?.port) ? observedStatus?.port : observedPort;
|
|
349
|
+
relayUrl = fallbackPort ? `ws://127.0.0.1:${fallbackPort}/${extensionLegacy ? "cdp" : "ops"}` : null;
|
|
350
|
+
}
|
|
351
|
+
const extensionReady = Boolean(
|
|
352
|
+
relayUrl && (relayStatus?.extensionHandshakeComplete || relayStatus?.extensionConnected || observedStatus?.extensionHandshakeComplete || observedStatus?.extensionConnected)
|
|
353
|
+
);
|
|
354
|
+
let usedRelay = false;
|
|
355
|
+
let result = null;
|
|
356
|
+
if (args.extensionOnly && !extensionReady) {
|
|
357
|
+
const diagnostics = buildRelayNotReadyDiagnostics("Extension not connected.", {
|
|
358
|
+
relayUrl,
|
|
359
|
+
relayStatus,
|
|
360
|
+
observedStatus,
|
|
361
|
+
observedPort
|
|
362
|
+
});
|
|
363
|
+
if (await maybeRetryHubMismatch(diagnostics.hint, attemptedRebind, deps)) {
|
|
364
|
+
attemptedRebind = true;
|
|
365
|
+
continue;
|
|
366
|
+
}
|
|
367
|
+
return failure(buildExtensionMissingMessage(diagnostics.message), "extension_not_connected");
|
|
368
|
+
}
|
|
369
|
+
if (!managedExplicit) {
|
|
370
|
+
if (!extensionReady || !relayUrl) {
|
|
371
|
+
const diagnostics = buildRelayNotReadyDiagnostics("Extension not connected.", {
|
|
372
|
+
relayUrl,
|
|
373
|
+
relayStatus,
|
|
374
|
+
observedStatus,
|
|
375
|
+
observedPort
|
|
376
|
+
});
|
|
377
|
+
if (await maybeRetryHubMismatch(diagnostics.hint, attemptedRebind, deps)) {
|
|
378
|
+
attemptedRebind = true;
|
|
379
|
+
continue;
|
|
380
|
+
}
|
|
381
|
+
return failure(buildExtensionMissingMessage(diagnostics.message), "extension_not_connected");
|
|
382
|
+
}
|
|
383
|
+
try {
|
|
384
|
+
result = await deps.manager.connectRelay(relayUrl);
|
|
385
|
+
usedRelay = true;
|
|
386
|
+
} catch (error) {
|
|
387
|
+
const errorMessage = serializeError(error).message;
|
|
388
|
+
const unauthorized = errorMessage.toLowerCase().includes("unauthorized") || errorMessage.includes("401");
|
|
389
|
+
const relayLabel = extensionLegacy ? "/cdp" : "/ops";
|
|
390
|
+
const errorObservedStatus = observedStatus ?? await fetchRelayObservedStatus(observedPort);
|
|
391
|
+
const diagnostics = buildRelayNotReadyDiagnostics(
|
|
392
|
+
unauthorized ? `Extension relay connection failed: relay ${relayLabel} unauthorized (token mismatch).` : `Extension relay connection failed: ${errorMessage}`,
|
|
393
|
+
{
|
|
394
|
+
relayUrl,
|
|
395
|
+
relayStatus,
|
|
396
|
+
observedStatus: errorObservedStatus,
|
|
397
|
+
observedPort
|
|
398
|
+
}
|
|
399
|
+
);
|
|
400
|
+
if (await maybeRetryHubMismatch(diagnostics.hint, attemptedRebind, deps)) {
|
|
401
|
+
attemptedRebind = true;
|
|
402
|
+
continue;
|
|
403
|
+
}
|
|
404
|
+
return failure(buildExtensionMissingMessage(diagnostics.message), "extension_connect_failed");
|
|
405
|
+
}
|
|
406
|
+
}
|
|
407
|
+
if (!result) {
|
|
408
|
+
try {
|
|
409
|
+
result = await deps.manager.launch({
|
|
410
|
+
profile: args.profile,
|
|
411
|
+
headless: managedHeadless,
|
|
412
|
+
startUrl: args.startUrl,
|
|
413
|
+
chromePath: args.chromePath,
|
|
414
|
+
flags: args.flags,
|
|
415
|
+
persistProfile: args.persistProfile,
|
|
416
|
+
noExtension: args.noExtension
|
|
417
|
+
});
|
|
418
|
+
} catch (error) {
|
|
419
|
+
return failure(buildManagedFailureMessage(error), "launch_failed");
|
|
420
|
+
}
|
|
421
|
+
}
|
|
422
|
+
if (usedRelay && args.startUrl && result.activeTargetId) {
|
|
423
|
+
await deps.manager.goto(result.sessionId, args.startUrl, "load", 3e4);
|
|
424
|
+
}
|
|
425
|
+
const warnings = result.warnings ?? [];
|
|
426
|
+
return ok({
|
|
427
|
+
sessionId: result.sessionId,
|
|
428
|
+
mode: result.mode,
|
|
429
|
+
browserWsEndpoint: result.wsEndpoint,
|
|
430
|
+
activeTargetId: result.activeTargetId,
|
|
431
|
+
warnings: warnings.length ? warnings : void 0
|
|
2770
432
|
});
|
|
433
|
+
} catch (error) {
|
|
434
|
+
return failure(serializeError(error).message, "launch_failed");
|
|
2771
435
|
}
|
|
2772
|
-
if (usedRelay && args.startUrl && result.activeTargetId) {
|
|
2773
|
-
await deps.manager.goto(result.sessionId, args.startUrl, "load", 3e4);
|
|
2774
|
-
}
|
|
2775
|
-
const warnings = [
|
|
2776
|
-
...result.warnings ?? [],
|
|
2777
|
-
...relayWarning ? [relayWarning] : []
|
|
2778
|
-
];
|
|
2779
|
-
return ok({
|
|
2780
|
-
sessionId: result.sessionId,
|
|
2781
|
-
mode: result.mode,
|
|
2782
|
-
browserWsEndpoint: result.wsEndpoint,
|
|
2783
|
-
activeTargetId: result.activeTargetId,
|
|
2784
|
-
warnings: warnings.length ? warnings : void 0
|
|
2785
|
-
});
|
|
2786
|
-
} catch (error) {
|
|
2787
|
-
return failure(serializeError(error).message, "launch_failed");
|
|
2788
436
|
}
|
|
2789
437
|
}
|
|
2790
438
|
});
|
|
2791
439
|
}
|
|
440
|
+
var buildExtensionMissingMessage = (reason) => {
|
|
441
|
+
return [
|
|
442
|
+
reason,
|
|
443
|
+
"Connect the extension: open the Chrome extension popup and click Connect, then retry.",
|
|
444
|
+
"Tip: If the popup says Connected, it may be connected to a different relay instance/port than this tool expects.",
|
|
445
|
+
"Legend: ext=extension websocket, handshake=extension handshake, ops=active /ops client, cdp=active /cdp client, pairing=token required.",
|
|
446
|
+
"",
|
|
447
|
+
"Other options (explicit):",
|
|
448
|
+
"- Managed (headed): npx opendevbrowser launch --no-extension",
|
|
449
|
+
"- Managed (headless): npx opendevbrowser launch --no-extension --headless",
|
|
450
|
+
"- Legacy extension relay: npx opendevbrowser launch --extension-legacy",
|
|
451
|
+
"- CDPConnect (default port): npx opendevbrowser connect --cdp-port 9222",
|
|
452
|
+
"- CDPConnect (explicit WS): npx opendevbrowser connect --ws-endpoint ws://127.0.0.1:9222/devtools/browser/<id>",
|
|
453
|
+
"Note: CDPConnect requires Chrome started with --remote-debugging-port=9222."
|
|
454
|
+
].join("\n");
|
|
455
|
+
};
|
|
456
|
+
var buildManagedFailureMessage = (error) => {
|
|
457
|
+
const detail = serializeError(error).message;
|
|
458
|
+
return [
|
|
459
|
+
`Managed session failed: ${detail}`,
|
|
460
|
+
"",
|
|
461
|
+
"Final option (explicit):",
|
|
462
|
+
"- CDPConnect (default port): npx opendevbrowser connect --cdp-port 9222",
|
|
463
|
+
"- CDPConnect (explicit WS): npx opendevbrowser connect --ws-endpoint ws://127.0.0.1:9222/devtools/browser/<id>"
|
|
464
|
+
].join("\n");
|
|
465
|
+
};
|
|
466
|
+
var MIN_WAIT_TIMEOUT_MS = 3e3;
|
|
467
|
+
var WAIT_MIN_DELAY_MS = 250;
|
|
468
|
+
var WAIT_MAX_DELAY_MS = 2e3;
|
|
469
|
+
function clampWaitTimeout(timeoutMs) {
|
|
470
|
+
if (!Number.isFinite(timeoutMs)) {
|
|
471
|
+
return MIN_WAIT_TIMEOUT_MS;
|
|
472
|
+
}
|
|
473
|
+
return Math.max(timeoutMs, MIN_WAIT_TIMEOUT_MS);
|
|
474
|
+
}
|
|
475
|
+
async function waitForExtensionHandshake(relay, observedPort, timeoutMs) {
|
|
476
|
+
const start = Date.now();
|
|
477
|
+
let delay = WAIT_MIN_DELAY_MS;
|
|
478
|
+
while (Date.now() - start < timeoutMs) {
|
|
479
|
+
if (relay?.status().extensionHandshakeComplete) {
|
|
480
|
+
return true;
|
|
481
|
+
}
|
|
482
|
+
const observedStatus = await fetchRelayObservedStatus(observedPort);
|
|
483
|
+
if (observedStatus?.extensionHandshakeComplete) {
|
|
484
|
+
return true;
|
|
485
|
+
}
|
|
486
|
+
await new Promise((resolve) => setTimeout(resolve, delay));
|
|
487
|
+
delay = Math.min(delay * 2, WAIT_MAX_DELAY_MS);
|
|
488
|
+
}
|
|
489
|
+
return false;
|
|
490
|
+
}
|
|
491
|
+
function resolveObservedPort(relayStatus, configPort) {
|
|
492
|
+
const relayPort = relayStatus?.port;
|
|
493
|
+
if (isValidPort(relayPort)) return relayPort;
|
|
494
|
+
if (isValidPort(configPort)) return configPort;
|
|
495
|
+
return null;
|
|
496
|
+
}
|
|
497
|
+
function isValidPort(port) {
|
|
498
|
+
return typeof port === "number" && Number.isInteger(port) && port > 0 && port <= 65535;
|
|
499
|
+
}
|
|
500
|
+
function shortInstanceId(value) {
|
|
501
|
+
if (!value) return "?";
|
|
502
|
+
return value.slice(0, 8);
|
|
503
|
+
}
|
|
504
|
+
function formatRelayUrl(relayUrl) {
|
|
505
|
+
return relayUrl ?? "null";
|
|
506
|
+
}
|
|
507
|
+
function formatLocalStatus(status) {
|
|
508
|
+
return [
|
|
509
|
+
"local(instance=",
|
|
510
|
+
shortInstanceId(status?.instanceId),
|
|
511
|
+
" port=",
|
|
512
|
+
typeof status?.port === "number" ? String(status.port) : "?",
|
|
513
|
+
" ext=",
|
|
514
|
+
String(Boolean(status?.extensionConnected)),
|
|
515
|
+
" handshake=",
|
|
516
|
+
String(Boolean(status?.extensionHandshakeComplete)),
|
|
517
|
+
" ops=",
|
|
518
|
+
String(Boolean(status?.opsConnected)),
|
|
519
|
+
" cdp=",
|
|
520
|
+
String(Boolean(status?.cdpConnected)),
|
|
521
|
+
" pairing=",
|
|
522
|
+
String(Boolean(status?.pairingRequired)),
|
|
523
|
+
")"
|
|
524
|
+
].join("");
|
|
525
|
+
}
|
|
526
|
+
function formatObservedStatus(status, port) {
|
|
527
|
+
const label = port ?? "?";
|
|
528
|
+
if (!status) {
|
|
529
|
+
return `observed@${label}=none`;
|
|
530
|
+
}
|
|
531
|
+
return [
|
|
532
|
+
"observed@",
|
|
533
|
+
label,
|
|
534
|
+
"=instance=",
|
|
535
|
+
shortInstanceId(status.instanceId),
|
|
536
|
+
" ext=",
|
|
537
|
+
String(Boolean(status.extensionConnected)),
|
|
538
|
+
" handshake=",
|
|
539
|
+
String(Boolean(status.extensionHandshakeComplete)),
|
|
540
|
+
" ops=",
|
|
541
|
+
String(Boolean(status.opsConnected)),
|
|
542
|
+
" cdp=",
|
|
543
|
+
String(Boolean(status.cdpConnected)),
|
|
544
|
+
" pairing=",
|
|
545
|
+
String(Boolean(status.pairingRequired))
|
|
546
|
+
].join("");
|
|
547
|
+
}
|
|
548
|
+
function buildRelayNotReadyDiagnostics(baseReason, detail) {
|
|
549
|
+
const localExt = Boolean(detail.relayStatus?.extensionConnected);
|
|
550
|
+
const observedExt = Boolean(detail.observedStatus?.extensionConnected);
|
|
551
|
+
let hint = "none";
|
|
552
|
+
if (detail.relayUrl === null) {
|
|
553
|
+
hint = "relayUrl_null";
|
|
554
|
+
} else if (detail.observedStatus && !localExt && observedExt) {
|
|
555
|
+
hint = "possible_mismatch";
|
|
556
|
+
} else if (detail.relayStatus?.instanceId && detail.observedStatus?.instanceId && detail.relayStatus.instanceId !== detail.observedStatus.instanceId) {
|
|
557
|
+
hint = "possible_mismatch";
|
|
558
|
+
}
|
|
559
|
+
const diagnostics = [
|
|
560
|
+
"Diagnostics: relayUrl=",
|
|
561
|
+
formatRelayUrl(detail.relayUrl),
|
|
562
|
+
" ",
|
|
563
|
+
formatLocalStatus(detail.relayStatus),
|
|
564
|
+
" ",
|
|
565
|
+
formatObservedStatus(detail.observedStatus, detail.observedPort),
|
|
566
|
+
" hint=",
|
|
567
|
+
hint
|
|
568
|
+
].join("");
|
|
569
|
+
return { message: [baseReason, diagnostics].join("\n"), hint };
|
|
570
|
+
}
|
|
571
|
+
async function maybeRetryHubMismatch(hint, attempted, deps) {
|
|
572
|
+
if (attempted) return false;
|
|
573
|
+
if (hint !== "possible_mismatch") return false;
|
|
574
|
+
if (!deps.ensureHub) return false;
|
|
575
|
+
await deps.ensureHub();
|
|
576
|
+
await deps.relay?.refresh?.();
|
|
577
|
+
return true;
|
|
578
|
+
}
|
|
579
|
+
async function fetchRelayObservedStatus(port) {
|
|
580
|
+
if (!isValidPort(port)) return null;
|
|
581
|
+
if (typeof fetch !== "function") return null;
|
|
582
|
+
const controller = new AbortController();
|
|
583
|
+
const timeoutId = setTimeout(() => controller.abort(), 500);
|
|
584
|
+
try {
|
|
585
|
+
const response = await fetch(`http://127.0.0.1:${port}/status`, { signal: controller.signal });
|
|
586
|
+
if (!response.ok) return null;
|
|
587
|
+
const payload = await response.json();
|
|
588
|
+
if (!payload || typeof payload.instanceId !== "string") return null;
|
|
589
|
+
return {
|
|
590
|
+
instanceId: payload.instanceId,
|
|
591
|
+
running: Boolean(payload.running),
|
|
592
|
+
port: typeof payload.port === "number" ? payload.port : void 0,
|
|
593
|
+
extensionConnected: Boolean(payload.extensionConnected),
|
|
594
|
+
extensionHandshakeComplete: Boolean(payload.extensionHandshakeComplete),
|
|
595
|
+
cdpConnected: Boolean(payload.cdpConnected),
|
|
596
|
+
opsConnected: Boolean(payload.opsConnected),
|
|
597
|
+
pairingRequired: Boolean(payload.pairingRequired)
|
|
598
|
+
};
|
|
599
|
+
} catch {
|
|
600
|
+
return null;
|
|
601
|
+
} finally {
|
|
602
|
+
clearTimeout(timeoutId);
|
|
603
|
+
}
|
|
604
|
+
}
|
|
2792
605
|
|
|
2793
606
|
// src/tools/connect.ts
|
|
2794
607
|
import { tool as tool2 } from "@opencode-ai/plugin";
|
|
2795
|
-
var
|
|
608
|
+
var z2 = tool2.schema;
|
|
609
|
+
function normalizeRelayEndpoint(wsEndpoint, path, allowBase) {
|
|
610
|
+
if (!wsEndpoint) return null;
|
|
611
|
+
try {
|
|
612
|
+
const url = new URL(wsEndpoint);
|
|
613
|
+
if (url.protocol !== "ws:" && url.protocol !== "wss:") return null;
|
|
614
|
+
if (url.hostname !== "127.0.0.1" && url.hostname !== "localhost") return null;
|
|
615
|
+
if (!url.port || !/^\d+$/.test(url.port)) return null;
|
|
616
|
+
const normalizedPath = url.pathname.endsWith("/") ? url.pathname.slice(0, -1) : url.pathname;
|
|
617
|
+
if (!allowBase && normalizedPath === "") return null;
|
|
618
|
+
if (normalizedPath && normalizedPath !== `/${path}`) return null;
|
|
619
|
+
return `${url.protocol}//${url.hostname}:${url.port}/${path}`;
|
|
620
|
+
} catch {
|
|
621
|
+
return null;
|
|
622
|
+
}
|
|
623
|
+
}
|
|
2796
624
|
function createConnectTool(deps) {
|
|
2797
625
|
return tool2({
|
|
2798
|
-
description: "Connect to an existing Chrome CDP endpoint.",
|
|
626
|
+
description: "Connect to an existing Chrome CDP endpoint or extension relay.",
|
|
2799
627
|
args: {
|
|
2800
|
-
wsEndpoint:
|
|
2801
|
-
host:
|
|
2802
|
-
port:
|
|
628
|
+
wsEndpoint: z2.string().optional().describe("Full WebSocket endpoint to connect to"),
|
|
629
|
+
host: z2.string().optional().describe("Host for /json/version lookup"),
|
|
630
|
+
port: z2.number().int().optional().describe("Port for /json/version lookup"),
|
|
631
|
+
extensionLegacy: z2.boolean().optional().describe("Use legacy extension relay (/cdp) instead of ops")
|
|
2803
632
|
},
|
|
2804
633
|
async execute(args) {
|
|
2805
634
|
try {
|
|
2806
|
-
|
|
2807
|
-
const
|
|
2808
|
-
const
|
|
2809
|
-
|
|
2810
|
-
|
|
2811
|
-
|
|
2812
|
-
|
|
635
|
+
await deps.relay?.refresh?.();
|
|
636
|
+
const wsEndpoint = args.wsEndpoint;
|
|
637
|
+
const extensionLegacy = args.extensionLegacy === true;
|
|
638
|
+
const hasExplicitCdp = Boolean(wsEndpoint || args.host || args.port);
|
|
639
|
+
const relayUrl = extensionLegacy ? deps.relay?.getCdpUrl() ?? null : deps.relay?.getOpsUrl?.() ?? null;
|
|
640
|
+
const normalizedOpsEndpoint = normalizeRelayEndpoint(wsEndpoint, "ops", true);
|
|
641
|
+
const normalizedLegacyEndpoint = normalizeRelayEndpoint(wsEndpoint, "cdp", extensionLegacy);
|
|
642
|
+
if (normalizedLegacyEndpoint && !extensionLegacy) {
|
|
643
|
+
return failure("Legacy extension relay (/cdp) requires extensionLegacy=true.", "extension_legacy_required");
|
|
644
|
+
}
|
|
645
|
+
const relayEndpoint = relayUrl && wsEndpoint === relayUrl ? relayUrl : extensionLegacy ? normalizedLegacyEndpoint ?? normalizedOpsEndpoint : normalizedOpsEndpoint;
|
|
646
|
+
let result;
|
|
647
|
+
if (relayEndpoint || !hasExplicitCdp && relayUrl) {
|
|
648
|
+
result = await deps.manager.connectRelay(relayEndpoint ?? relayUrl ?? "");
|
|
649
|
+
} else {
|
|
650
|
+
if (!hasExplicitCdp) {
|
|
651
|
+
return failure("Extension relay not available. Connect the extension or pass wsEndpoint/host/port.", "extension_not_connected");
|
|
652
|
+
}
|
|
653
|
+
result = await deps.manager.connect({
|
|
654
|
+
wsEndpoint,
|
|
655
|
+
host: args.host,
|
|
656
|
+
port: args.port
|
|
657
|
+
});
|
|
658
|
+
}
|
|
2813
659
|
return ok({
|
|
2814
660
|
sessionId: result.sessionId,
|
|
2815
661
|
mode: result.mode,
|
|
@@ -2826,13 +672,13 @@ function createConnectTool(deps) {
|
|
|
2826
672
|
|
|
2827
673
|
// src/tools/disconnect.ts
|
|
2828
674
|
import { tool as tool3 } from "@opencode-ai/plugin";
|
|
2829
|
-
var
|
|
675
|
+
var z3 = tool3.schema;
|
|
2830
676
|
function createDisconnectTool(deps) {
|
|
2831
677
|
return tool3({
|
|
2832
678
|
description: "Disconnect a browser session.",
|
|
2833
679
|
args: {
|
|
2834
|
-
sessionId:
|
|
2835
|
-
closeBrowser:
|
|
680
|
+
sessionId: z3.string().describe("Session id returned from launch/connect"),
|
|
681
|
+
closeBrowser: z3.boolean().optional().describe("Close the underlying browser process")
|
|
2836
682
|
},
|
|
2837
683
|
async execute(args) {
|
|
2838
684
|
try {
|
|
@@ -2846,21 +692,28 @@ function createDisconnectTool(deps) {
|
|
|
2846
692
|
}
|
|
2847
693
|
|
|
2848
694
|
// src/tools/status.ts
|
|
2849
|
-
import { readFileSync
|
|
2850
|
-
import { dirname
|
|
695
|
+
import { readFileSync } from "fs";
|
|
696
|
+
import { dirname, join } from "path";
|
|
2851
697
|
import { fileURLToPath } from "url";
|
|
2852
698
|
import { tool as tool4 } from "@opencode-ai/plugin";
|
|
2853
|
-
|
|
699
|
+
|
|
700
|
+
// src/utils/hub-enabled.ts
|
|
701
|
+
var isHubEnabled = (config) => {
|
|
702
|
+
return config.relayToken !== false && config.relayPort > 0;
|
|
703
|
+
};
|
|
704
|
+
|
|
705
|
+
// src/tools/status.ts
|
|
706
|
+
var z4 = tool4.schema;
|
|
2854
707
|
function getPackageVersion() {
|
|
2855
708
|
try {
|
|
2856
|
-
const baseDir =
|
|
709
|
+
const baseDir = dirname(fileURLToPath(import.meta.url));
|
|
2857
710
|
const candidates = [
|
|
2858
|
-
|
|
2859
|
-
|
|
711
|
+
join(baseDir, "..", "..", "package.json"),
|
|
712
|
+
join(baseDir, "..", "package.json")
|
|
2860
713
|
];
|
|
2861
714
|
for (const pkgPath of candidates) {
|
|
2862
715
|
try {
|
|
2863
|
-
const pkg = JSON.parse(
|
|
716
|
+
const pkg = JSON.parse(readFileSync(pkgPath, "utf-8"));
|
|
2864
717
|
if (typeof pkg.version === "string") {
|
|
2865
718
|
return pkg.version;
|
|
2866
719
|
}
|
|
@@ -2891,17 +744,45 @@ async function fetchLatestVersion(packageName) {
|
|
|
2891
744
|
}
|
|
2892
745
|
function createStatusTool(deps) {
|
|
2893
746
|
return tool4({
|
|
2894
|
-
description: "Get
|
|
747
|
+
description: "Get daemon or session status.",
|
|
2895
748
|
args: {
|
|
2896
|
-
sessionId:
|
|
749
|
+
sessionId: z4.string().optional().describe("Session id (required when hub is disabled)")
|
|
2897
750
|
},
|
|
2898
751
|
async execute(args) {
|
|
2899
752
|
try {
|
|
2900
|
-
const status = await deps.manager.status(args.sessionId);
|
|
2901
|
-
const extensionPath = deps.getExtensionPath?.() ?? null;
|
|
2902
753
|
const config = deps.config.get();
|
|
754
|
+
const hubEnabled = isHubEnabled(config);
|
|
755
|
+
const extensionPath = deps.getExtensionPath?.() ?? null;
|
|
2903
756
|
const version = getPackageVersion();
|
|
2904
757
|
let updateHint;
|
|
758
|
+
let sessionStatus = null;
|
|
759
|
+
if (hubEnabled) {
|
|
760
|
+
const daemonStatus = await fetchDaemonStatusFromMetadata();
|
|
761
|
+
if (!daemonStatus) {
|
|
762
|
+
return failure("Daemon not running. Start with `npx opendevbrowser serve`.", "status_failed");
|
|
763
|
+
}
|
|
764
|
+
if (args.sessionId) {
|
|
765
|
+
sessionStatus = await deps.manager.status(args.sessionId);
|
|
766
|
+
}
|
|
767
|
+
if (config.checkForUpdates && version) {
|
|
768
|
+
const latest = await fetchLatestVersion("opendevbrowser");
|
|
769
|
+
if (latest && latest !== version) {
|
|
770
|
+
updateHint = `Update available: ${version} -> ${latest}`;
|
|
771
|
+
}
|
|
772
|
+
}
|
|
773
|
+
return ok({
|
|
774
|
+
...sessionStatus ?? {},
|
|
775
|
+
daemon: daemonStatus,
|
|
776
|
+
hubEnabled: true,
|
|
777
|
+
extensionPath: extensionPath ?? void 0,
|
|
778
|
+
version,
|
|
779
|
+
updateHint
|
|
780
|
+
});
|
|
781
|
+
}
|
|
782
|
+
if (!args.sessionId) {
|
|
783
|
+
return failure("Missing sessionId for status.", "status_failed");
|
|
784
|
+
}
|
|
785
|
+
sessionStatus = await deps.manager.status(args.sessionId);
|
|
2905
786
|
if (config.checkForUpdates && version) {
|
|
2906
787
|
const latest = await fetchLatestVersion("opendevbrowser");
|
|
2907
788
|
if (latest && latest !== version) {
|
|
@@ -2909,10 +790,10 @@ function createStatusTool(deps) {
|
|
|
2909
790
|
}
|
|
2910
791
|
}
|
|
2911
792
|
return ok({
|
|
2912
|
-
mode:
|
|
2913
|
-
activeTargetId:
|
|
2914
|
-
url:
|
|
2915
|
-
title:
|
|
793
|
+
mode: sessionStatus.mode,
|
|
794
|
+
activeTargetId: sessionStatus.activeTargetId,
|
|
795
|
+
url: sessionStatus.url,
|
|
796
|
+
title: sessionStatus.title,
|
|
2916
797
|
extensionPath: extensionPath ?? void 0,
|
|
2917
798
|
version,
|
|
2918
799
|
updateHint
|
|
@@ -2926,13 +807,13 @@ function createStatusTool(deps) {
|
|
|
2926
807
|
|
|
2927
808
|
// src/tools/targets_list.ts
|
|
2928
809
|
import { tool as tool5 } from "@opencode-ai/plugin";
|
|
2929
|
-
var
|
|
810
|
+
var z5 = tool5.schema;
|
|
2930
811
|
function createTargetsListTool(deps) {
|
|
2931
812
|
return tool5({
|
|
2932
813
|
description: "List targets (tabs) in the current session.",
|
|
2933
814
|
args: {
|
|
2934
|
-
sessionId:
|
|
2935
|
-
includeUrls:
|
|
815
|
+
sessionId: z5.string().describe("Session id"),
|
|
816
|
+
includeUrls: z5.boolean().optional().describe("Include target URLs")
|
|
2936
817
|
},
|
|
2937
818
|
async execute(args) {
|
|
2938
819
|
try {
|
|
@@ -2950,13 +831,13 @@ function createTargetsListTool(deps) {
|
|
|
2950
831
|
|
|
2951
832
|
// src/tools/target_use.ts
|
|
2952
833
|
import { tool as tool6 } from "@opencode-ai/plugin";
|
|
2953
|
-
var
|
|
834
|
+
var z6 = tool6.schema;
|
|
2954
835
|
function createTargetUseTool(deps) {
|
|
2955
836
|
return tool6({
|
|
2956
837
|
description: "Set the active target (tab).",
|
|
2957
838
|
args: {
|
|
2958
|
-
sessionId:
|
|
2959
|
-
targetId:
|
|
839
|
+
sessionId: z6.string().describe("Session id"),
|
|
840
|
+
targetId: z6.string().describe("Target id")
|
|
2960
841
|
},
|
|
2961
842
|
async execute(args) {
|
|
2962
843
|
try {
|
|
@@ -2975,13 +856,13 @@ function createTargetUseTool(deps) {
|
|
|
2975
856
|
|
|
2976
857
|
// src/tools/target_new.ts
|
|
2977
858
|
import { tool as tool7 } from "@opencode-ai/plugin";
|
|
2978
|
-
var
|
|
859
|
+
var z7 = tool7.schema;
|
|
2979
860
|
function createTargetNewTool(deps) {
|
|
2980
861
|
return tool7({
|
|
2981
862
|
description: "Open a new target (tab).",
|
|
2982
863
|
args: {
|
|
2983
|
-
sessionId:
|
|
2984
|
-
url:
|
|
864
|
+
sessionId: z7.string().describe("Session id"),
|
|
865
|
+
url: z7.string().optional().describe("Optional URL to open")
|
|
2985
866
|
},
|
|
2986
867
|
async execute(args) {
|
|
2987
868
|
try {
|
|
@@ -2996,13 +877,13 @@ function createTargetNewTool(deps) {
|
|
|
2996
877
|
|
|
2997
878
|
// src/tools/target_close.ts
|
|
2998
879
|
import { tool as tool8 } from "@opencode-ai/plugin";
|
|
2999
|
-
var
|
|
880
|
+
var z8 = tool8.schema;
|
|
3000
881
|
function createTargetCloseTool(deps) {
|
|
3001
882
|
return tool8({
|
|
3002
883
|
description: "Close a target (tab).",
|
|
3003
884
|
args: {
|
|
3004
|
-
sessionId:
|
|
3005
|
-
targetId:
|
|
885
|
+
sessionId: z8.string().describe("Session id"),
|
|
886
|
+
targetId: z8.string().describe("Target id")
|
|
3006
887
|
},
|
|
3007
888
|
async execute(args) {
|
|
3008
889
|
try {
|
|
@@ -3017,14 +898,14 @@ function createTargetCloseTool(deps) {
|
|
|
3017
898
|
|
|
3018
899
|
// src/tools/page.ts
|
|
3019
900
|
import { tool as tool9 } from "@opencode-ai/plugin";
|
|
3020
|
-
var
|
|
901
|
+
var z9 = tool9.schema;
|
|
3021
902
|
function createPageTool(deps) {
|
|
3022
903
|
return tool9({
|
|
3023
904
|
description: "Open or focus a named page, optionally navigating to a URL.",
|
|
3024
905
|
args: {
|
|
3025
|
-
sessionId:
|
|
3026
|
-
name:
|
|
3027
|
-
url:
|
|
906
|
+
sessionId: z9.string().describe("Active browser session id"),
|
|
907
|
+
name: z9.string().describe("Stable page name"),
|
|
908
|
+
url: z9.string().optional().describe("Optional URL to open")
|
|
3028
909
|
},
|
|
3029
910
|
async execute(args) {
|
|
3030
911
|
try {
|
|
@@ -3044,12 +925,12 @@ function createPageTool(deps) {
|
|
|
3044
925
|
|
|
3045
926
|
// src/tools/list.ts
|
|
3046
927
|
import { tool as tool10 } from "@opencode-ai/plugin";
|
|
3047
|
-
var
|
|
928
|
+
var z10 = tool10.schema;
|
|
3048
929
|
function createListTool(deps) {
|
|
3049
930
|
return tool10({
|
|
3050
931
|
description: "List named pages in the current session.",
|
|
3051
932
|
args: {
|
|
3052
|
-
sessionId:
|
|
933
|
+
sessionId: z10.string().describe("Active browser session id")
|
|
3053
934
|
},
|
|
3054
935
|
async execute(args) {
|
|
3055
936
|
try {
|
|
@@ -3064,13 +945,13 @@ function createListTool(deps) {
|
|
|
3064
945
|
|
|
3065
946
|
// src/tools/close.ts
|
|
3066
947
|
import { tool as tool11 } from "@opencode-ai/plugin";
|
|
3067
|
-
var
|
|
948
|
+
var z11 = tool11.schema;
|
|
3068
949
|
function createCloseTool(deps) {
|
|
3069
950
|
return tool11({
|
|
3070
951
|
description: "Close a named page within the current session.",
|
|
3071
952
|
args: {
|
|
3072
|
-
sessionId:
|
|
3073
|
-
name:
|
|
953
|
+
sessionId: z11.string().describe("Active browser session id"),
|
|
954
|
+
name: z11.string().describe("Named page to close")
|
|
3074
955
|
},
|
|
3075
956
|
async execute(args) {
|
|
3076
957
|
try {
|
|
@@ -3085,16 +966,16 @@ function createCloseTool(deps) {
|
|
|
3085
966
|
|
|
3086
967
|
// src/tools/goto.ts
|
|
3087
968
|
import { tool as tool12 } from "@opencode-ai/plugin";
|
|
3088
|
-
var
|
|
3089
|
-
var waitUntilSchema =
|
|
969
|
+
var z12 = tool12.schema;
|
|
970
|
+
var waitUntilSchema = z12.enum(["domcontentloaded", "load", "networkidle"]);
|
|
3090
971
|
function createGotoTool(deps) {
|
|
3091
972
|
return tool12({
|
|
3092
973
|
description: "Navigate the active target to a URL.",
|
|
3093
974
|
args: {
|
|
3094
|
-
sessionId:
|
|
3095
|
-
url:
|
|
975
|
+
sessionId: z12.string().describe("Session id"),
|
|
976
|
+
url: z12.string().describe("URL to navigate to"),
|
|
3096
977
|
waitUntil: waitUntilSchema.optional().describe("Load state to wait for"),
|
|
3097
|
-
timeoutMs:
|
|
978
|
+
timeoutMs: z12.number().int().optional().describe("Timeout in milliseconds")
|
|
3098
979
|
},
|
|
3099
980
|
async execute(args) {
|
|
3100
981
|
try {
|
|
@@ -3114,18 +995,18 @@ function createGotoTool(deps) {
|
|
|
3114
995
|
|
|
3115
996
|
// src/tools/wait.ts
|
|
3116
997
|
import { tool as tool13 } from "@opencode-ai/plugin";
|
|
3117
|
-
var
|
|
3118
|
-
var waitUntilSchema2 =
|
|
3119
|
-
var waitStateSchema =
|
|
998
|
+
var z13 = tool13.schema;
|
|
999
|
+
var waitUntilSchema2 = z13.enum(["domcontentloaded", "load", "networkidle"]);
|
|
1000
|
+
var waitStateSchema = z13.enum(["attached", "visible", "hidden"]);
|
|
3120
1001
|
function createWaitTool(deps) {
|
|
3121
1002
|
return tool13({
|
|
3122
1003
|
description: "Wait for a load state or a ref state.",
|
|
3123
1004
|
args: {
|
|
3124
|
-
sessionId:
|
|
1005
|
+
sessionId: z13.string().describe("Session id"),
|
|
3125
1006
|
until: waitUntilSchema2.optional().describe("Load state to wait for"),
|
|
3126
|
-
ref:
|
|
1007
|
+
ref: z13.string().optional().describe("Ref to wait for"),
|
|
3127
1008
|
state: waitStateSchema.optional().describe("Ref state to wait for"),
|
|
3128
|
-
timeoutMs:
|
|
1009
|
+
timeoutMs: z13.number().int().optional().describe("Timeout in milliseconds")
|
|
3129
1010
|
},
|
|
3130
1011
|
async execute(args) {
|
|
3131
1012
|
try {
|
|
@@ -3156,16 +1037,16 @@ function createWaitTool(deps) {
|
|
|
3156
1037
|
|
|
3157
1038
|
// src/tools/snapshot.ts
|
|
3158
1039
|
import { tool as tool14 } from "@opencode-ai/plugin";
|
|
3159
|
-
var
|
|
3160
|
-
var formatSchema =
|
|
1040
|
+
var z14 = tool14.schema;
|
|
1041
|
+
var formatSchema = z14.enum(["outline", "actionables"]);
|
|
3161
1042
|
function createSnapshotTool(deps) {
|
|
3162
1043
|
return tool14({
|
|
3163
1044
|
description: "Capture a snapshot of the current page and return refs.",
|
|
3164
1045
|
args: {
|
|
3165
|
-
sessionId:
|
|
1046
|
+
sessionId: z14.string().describe("Session id"),
|
|
3166
1047
|
format: formatSchema.optional().describe("Snapshot format"),
|
|
3167
|
-
maxChars:
|
|
3168
|
-
cursor:
|
|
1048
|
+
maxChars: z14.number().int().optional().describe("Max characters for snapshot output"),
|
|
1049
|
+
cursor: z14.string().optional().describe("Cursor for paging")
|
|
3169
1050
|
},
|
|
3170
1051
|
async execute(args) {
|
|
3171
1052
|
try {
|
|
@@ -3186,13 +1067,13 @@ function createSnapshotTool(deps) {
|
|
|
3186
1067
|
|
|
3187
1068
|
// src/tools/click.ts
|
|
3188
1069
|
import { tool as tool15 } from "@opencode-ai/plugin";
|
|
3189
|
-
var
|
|
1070
|
+
var z15 = tool15.schema;
|
|
3190
1071
|
function createClickTool(deps) {
|
|
3191
1072
|
return tool15({
|
|
3192
1073
|
description: "Click a referenced element.",
|
|
3193
1074
|
args: {
|
|
3194
|
-
sessionId:
|
|
3195
|
-
ref:
|
|
1075
|
+
sessionId: z15.string().describe("Session id"),
|
|
1076
|
+
ref: z15.string().describe("Element ref")
|
|
3196
1077
|
},
|
|
3197
1078
|
async execute(args) {
|
|
3198
1079
|
try {
|
|
@@ -3205,18 +1086,103 @@ function createClickTool(deps) {
|
|
|
3205
1086
|
});
|
|
3206
1087
|
}
|
|
3207
1088
|
|
|
3208
|
-
// src/tools/
|
|
1089
|
+
// src/tools/hover.ts
|
|
3209
1090
|
import { tool as tool16 } from "@opencode-ai/plugin";
|
|
3210
|
-
var
|
|
3211
|
-
function
|
|
1091
|
+
var z16 = tool16.schema;
|
|
1092
|
+
function createHoverTool(deps) {
|
|
3212
1093
|
return tool16({
|
|
1094
|
+
description: "Hover over an element by ref.",
|
|
1095
|
+
args: {
|
|
1096
|
+
sessionId: z16.string().describe("Active browser session id"),
|
|
1097
|
+
ref: z16.string().describe("Element ref from snapshot")
|
|
1098
|
+
},
|
|
1099
|
+
async execute(args) {
|
|
1100
|
+
try {
|
|
1101
|
+
const result = await deps.manager.hover(args.sessionId, args.ref);
|
|
1102
|
+
return ok(result);
|
|
1103
|
+
} catch (error) {
|
|
1104
|
+
return failure(serializeError(error).message, "hover_failed");
|
|
1105
|
+
}
|
|
1106
|
+
}
|
|
1107
|
+
});
|
|
1108
|
+
}
|
|
1109
|
+
|
|
1110
|
+
// src/tools/press.ts
|
|
1111
|
+
import { tool as tool17 } from "@opencode-ai/plugin";
|
|
1112
|
+
var z17 = tool17.schema;
|
|
1113
|
+
function createPressTool(deps) {
|
|
1114
|
+
return tool17({
|
|
1115
|
+
description: "Press a keyboard key, optionally focusing a ref first.",
|
|
1116
|
+
args: {
|
|
1117
|
+
sessionId: z17.string().describe("Active browser session id"),
|
|
1118
|
+
key: z17.string().describe("Keyboard key to press, e.g. Enter or ArrowDown"),
|
|
1119
|
+
ref: z17.string().optional().describe("Optional element ref to focus first")
|
|
1120
|
+
},
|
|
1121
|
+
async execute(args) {
|
|
1122
|
+
try {
|
|
1123
|
+
const result = await deps.manager.press(args.sessionId, args.key, args.ref);
|
|
1124
|
+
return ok(result);
|
|
1125
|
+
} catch (error) {
|
|
1126
|
+
return failure(serializeError(error).message, "press_failed");
|
|
1127
|
+
}
|
|
1128
|
+
}
|
|
1129
|
+
});
|
|
1130
|
+
}
|
|
1131
|
+
|
|
1132
|
+
// src/tools/check.ts
|
|
1133
|
+
import { tool as tool18 } from "@opencode-ai/plugin";
|
|
1134
|
+
var z18 = tool18.schema;
|
|
1135
|
+
function createCheckTool(deps) {
|
|
1136
|
+
return tool18({
|
|
1137
|
+
description: "Check a checkbox or toggle by ref.",
|
|
1138
|
+
args: {
|
|
1139
|
+
sessionId: z18.string().describe("Active browser session id"),
|
|
1140
|
+
ref: z18.string().describe("Element ref from snapshot")
|
|
1141
|
+
},
|
|
1142
|
+
async execute(args) {
|
|
1143
|
+
try {
|
|
1144
|
+
const result = await deps.manager.check(args.sessionId, args.ref);
|
|
1145
|
+
return ok(result);
|
|
1146
|
+
} catch (error) {
|
|
1147
|
+
return failure(serializeError(error).message, "check_failed");
|
|
1148
|
+
}
|
|
1149
|
+
}
|
|
1150
|
+
});
|
|
1151
|
+
}
|
|
1152
|
+
|
|
1153
|
+
// src/tools/uncheck.ts
|
|
1154
|
+
import { tool as tool19 } from "@opencode-ai/plugin";
|
|
1155
|
+
var z19 = tool19.schema;
|
|
1156
|
+
function createUncheckTool(deps) {
|
|
1157
|
+
return tool19({
|
|
1158
|
+
description: "Uncheck a checkbox or toggle by ref.",
|
|
1159
|
+
args: {
|
|
1160
|
+
sessionId: z19.string().describe("Active browser session id"),
|
|
1161
|
+
ref: z19.string().describe("Element ref from snapshot")
|
|
1162
|
+
},
|
|
1163
|
+
async execute(args) {
|
|
1164
|
+
try {
|
|
1165
|
+
const result = await deps.manager.uncheck(args.sessionId, args.ref);
|
|
1166
|
+
return ok(result);
|
|
1167
|
+
} catch (error) {
|
|
1168
|
+
return failure(serializeError(error).message, "uncheck_failed");
|
|
1169
|
+
}
|
|
1170
|
+
}
|
|
1171
|
+
});
|
|
1172
|
+
}
|
|
1173
|
+
|
|
1174
|
+
// src/tools/type.ts
|
|
1175
|
+
import { tool as tool20 } from "@opencode-ai/plugin";
|
|
1176
|
+
var z20 = tool20.schema;
|
|
1177
|
+
function createTypeTool(deps) {
|
|
1178
|
+
return tool20({
|
|
3213
1179
|
description: "Type text into a referenced input.",
|
|
3214
1180
|
args: {
|
|
3215
|
-
sessionId:
|
|
3216
|
-
ref:
|
|
3217
|
-
text:
|
|
3218
|
-
clear:
|
|
3219
|
-
submit:
|
|
1181
|
+
sessionId: z20.string().describe("Session id"),
|
|
1182
|
+
ref: z20.string().describe("Element ref"),
|
|
1183
|
+
text: z20.string().describe("Text to type"),
|
|
1184
|
+
clear: z20.boolean().optional().describe("Clear before typing"),
|
|
1185
|
+
submit: z20.boolean().optional().describe("Press Enter after typing")
|
|
3220
1186
|
},
|
|
3221
1187
|
async execute(args) {
|
|
3222
1188
|
try {
|
|
@@ -3236,15 +1202,15 @@ function createTypeTool(deps) {
|
|
|
3236
1202
|
}
|
|
3237
1203
|
|
|
3238
1204
|
// src/tools/select.ts
|
|
3239
|
-
import { tool as
|
|
3240
|
-
var
|
|
1205
|
+
import { tool as tool21 } from "@opencode-ai/plugin";
|
|
1206
|
+
var z21 = tool21.schema;
|
|
3241
1207
|
function createSelectTool(deps) {
|
|
3242
|
-
return
|
|
1208
|
+
return tool21({
|
|
3243
1209
|
description: "Select options in a referenced select element.",
|
|
3244
1210
|
args: {
|
|
3245
|
-
sessionId:
|
|
3246
|
-
ref:
|
|
3247
|
-
values:
|
|
1211
|
+
sessionId: z21.string().describe("Session id"),
|
|
1212
|
+
ref: z21.string().describe("Element ref"),
|
|
1213
|
+
values: z21.array(z21.string()).describe("Values to select")
|
|
3248
1214
|
},
|
|
3249
1215
|
async execute(args) {
|
|
3250
1216
|
try {
|
|
@@ -3258,15 +1224,15 @@ function createSelectTool(deps) {
|
|
|
3258
1224
|
}
|
|
3259
1225
|
|
|
3260
1226
|
// src/tools/scroll.ts
|
|
3261
|
-
import { tool as
|
|
3262
|
-
var
|
|
1227
|
+
import { tool as tool22 } from "@opencode-ai/plugin";
|
|
1228
|
+
var z22 = tool22.schema;
|
|
3263
1229
|
function createScrollTool(deps) {
|
|
3264
|
-
return
|
|
1230
|
+
return tool22({
|
|
3265
1231
|
description: "Scroll the page or a referenced element.",
|
|
3266
1232
|
args: {
|
|
3267
|
-
sessionId:
|
|
3268
|
-
dy:
|
|
3269
|
-
ref:
|
|
1233
|
+
sessionId: z22.string().describe("Session id"),
|
|
1234
|
+
dy: z22.number().describe("Scroll delta in pixels"),
|
|
1235
|
+
ref: z22.string().optional().describe("Optional element ref to scroll")
|
|
3270
1236
|
},
|
|
3271
1237
|
async execute(args) {
|
|
3272
1238
|
try {
|
|
@@ -3279,16 +1245,37 @@ function createScrollTool(deps) {
|
|
|
3279
1245
|
});
|
|
3280
1246
|
}
|
|
3281
1247
|
|
|
1248
|
+
// src/tools/scroll_into_view.ts
|
|
1249
|
+
import { tool as tool23 } from "@opencode-ai/plugin";
|
|
1250
|
+
var z23 = tool23.schema;
|
|
1251
|
+
function createScrollIntoViewTool(deps) {
|
|
1252
|
+
return tool23({
|
|
1253
|
+
description: "Scroll an element into view by ref.",
|
|
1254
|
+
args: {
|
|
1255
|
+
sessionId: z23.string().describe("Active browser session id"),
|
|
1256
|
+
ref: z23.string().describe("Element ref from snapshot")
|
|
1257
|
+
},
|
|
1258
|
+
async execute(args) {
|
|
1259
|
+
try {
|
|
1260
|
+
const result = await deps.manager.scrollIntoView(args.sessionId, args.ref);
|
|
1261
|
+
return ok(result);
|
|
1262
|
+
} catch (error) {
|
|
1263
|
+
return failure(serializeError(error).message, "scroll_into_view_failed");
|
|
1264
|
+
}
|
|
1265
|
+
}
|
|
1266
|
+
});
|
|
1267
|
+
}
|
|
1268
|
+
|
|
3282
1269
|
// src/tools/dom_get_html.ts
|
|
3283
|
-
import { tool as
|
|
3284
|
-
var
|
|
1270
|
+
import { tool as tool24 } from "@opencode-ai/plugin";
|
|
1271
|
+
var z24 = tool24.schema;
|
|
3285
1272
|
function createDomGetHtmlTool(deps) {
|
|
3286
|
-
return
|
|
1273
|
+
return tool24({
|
|
3287
1274
|
description: "Get outerHTML for a referenced element.",
|
|
3288
1275
|
args: {
|
|
3289
|
-
sessionId:
|
|
3290
|
-
ref:
|
|
3291
|
-
maxChars:
|
|
1276
|
+
sessionId: z24.string().describe("Session id"),
|
|
1277
|
+
ref: z24.string().describe("Element ref"),
|
|
1278
|
+
maxChars: z24.number().int().optional().describe("Max characters")
|
|
3292
1279
|
},
|
|
3293
1280
|
async execute(args) {
|
|
3294
1281
|
try {
|
|
@@ -3310,15 +1297,15 @@ function createDomGetHtmlTool(deps) {
|
|
|
3310
1297
|
}
|
|
3311
1298
|
|
|
3312
1299
|
// src/tools/dom_get_text.ts
|
|
3313
|
-
import { tool as
|
|
3314
|
-
var
|
|
1300
|
+
import { tool as tool25 } from "@opencode-ai/plugin";
|
|
1301
|
+
var z25 = tool25.schema;
|
|
3315
1302
|
function createDomGetTextTool(deps) {
|
|
3316
|
-
return
|
|
1303
|
+
return tool25({
|
|
3317
1304
|
description: "Get inner text for a referenced element.",
|
|
3318
1305
|
args: {
|
|
3319
|
-
sessionId:
|
|
3320
|
-
ref:
|
|
3321
|
-
maxChars:
|
|
1306
|
+
sessionId: z25.string().describe("Session id"),
|
|
1307
|
+
ref: z25.string().describe("Element ref"),
|
|
1308
|
+
maxChars: z25.number().int().optional().describe("Max characters")
|
|
3322
1309
|
},
|
|
3323
1310
|
async execute(args) {
|
|
3324
1311
|
try {
|
|
@@ -3339,21 +1326,127 @@ function createDomGetTextTool(deps) {
|
|
|
3339
1326
|
});
|
|
3340
1327
|
}
|
|
3341
1328
|
|
|
1329
|
+
// src/tools/get_attr.ts
|
|
1330
|
+
import { tool as tool26 } from "@opencode-ai/plugin";
|
|
1331
|
+
var z26 = tool26.schema;
|
|
1332
|
+
function createGetAttrTool(deps) {
|
|
1333
|
+
return tool26({
|
|
1334
|
+
description: "Get a DOM attribute value by ref.",
|
|
1335
|
+
args: {
|
|
1336
|
+
sessionId: z26.string().describe("Active browser session id"),
|
|
1337
|
+
ref: z26.string().describe("Element ref from snapshot"),
|
|
1338
|
+
name: z26.string().describe("Attribute name, e.g. href or aria-label")
|
|
1339
|
+
},
|
|
1340
|
+
async execute(args) {
|
|
1341
|
+
try {
|
|
1342
|
+
const result = await deps.manager.domGetAttr(args.sessionId, args.ref, args.name);
|
|
1343
|
+
return ok(result);
|
|
1344
|
+
} catch (error) {
|
|
1345
|
+
return failure(serializeError(error).message, "get_attr_failed");
|
|
1346
|
+
}
|
|
1347
|
+
}
|
|
1348
|
+
});
|
|
1349
|
+
}
|
|
1350
|
+
|
|
1351
|
+
// src/tools/get_value.ts
|
|
1352
|
+
import { tool as tool27 } from "@opencode-ai/plugin";
|
|
1353
|
+
var z27 = tool27.schema;
|
|
1354
|
+
function createGetValueTool(deps) {
|
|
1355
|
+
return tool27({
|
|
1356
|
+
description: "Get the input value for an element by ref.",
|
|
1357
|
+
args: {
|
|
1358
|
+
sessionId: z27.string().describe("Active browser session id"),
|
|
1359
|
+
ref: z27.string().describe("Element ref from snapshot")
|
|
1360
|
+
},
|
|
1361
|
+
async execute(args) {
|
|
1362
|
+
try {
|
|
1363
|
+
const result = await deps.manager.domGetValue(args.sessionId, args.ref);
|
|
1364
|
+
return ok(result);
|
|
1365
|
+
} catch (error) {
|
|
1366
|
+
return failure(serializeError(error).message, "get_value_failed");
|
|
1367
|
+
}
|
|
1368
|
+
}
|
|
1369
|
+
});
|
|
1370
|
+
}
|
|
1371
|
+
|
|
1372
|
+
// src/tools/is_visible.ts
|
|
1373
|
+
import { tool as tool28 } from "@opencode-ai/plugin";
|
|
1374
|
+
var z28 = tool28.schema;
|
|
1375
|
+
function createIsVisibleTool(deps) {
|
|
1376
|
+
return tool28({
|
|
1377
|
+
description: "Check if an element is visible by ref.",
|
|
1378
|
+
args: {
|
|
1379
|
+
sessionId: z28.string().describe("Active browser session id"),
|
|
1380
|
+
ref: z28.string().describe("Element ref from snapshot")
|
|
1381
|
+
},
|
|
1382
|
+
async execute(args) {
|
|
1383
|
+
try {
|
|
1384
|
+
const result = await deps.manager.domIsVisible(args.sessionId, args.ref);
|
|
1385
|
+
return ok(result);
|
|
1386
|
+
} catch (error) {
|
|
1387
|
+
return failure(serializeError(error).message, "is_visible_failed");
|
|
1388
|
+
}
|
|
1389
|
+
}
|
|
1390
|
+
});
|
|
1391
|
+
}
|
|
1392
|
+
|
|
1393
|
+
// src/tools/is_enabled.ts
|
|
1394
|
+
import { tool as tool29 } from "@opencode-ai/plugin";
|
|
1395
|
+
var z29 = tool29.schema;
|
|
1396
|
+
function createIsEnabledTool(deps) {
|
|
1397
|
+
return tool29({
|
|
1398
|
+
description: "Check if an element is enabled by ref.",
|
|
1399
|
+
args: {
|
|
1400
|
+
sessionId: z29.string().describe("Active browser session id"),
|
|
1401
|
+
ref: z29.string().describe("Element ref from snapshot")
|
|
1402
|
+
},
|
|
1403
|
+
async execute(args) {
|
|
1404
|
+
try {
|
|
1405
|
+
const result = await deps.manager.domIsEnabled(args.sessionId, args.ref);
|
|
1406
|
+
return ok(result);
|
|
1407
|
+
} catch (error) {
|
|
1408
|
+
return failure(serializeError(error).message, "is_enabled_failed");
|
|
1409
|
+
}
|
|
1410
|
+
}
|
|
1411
|
+
});
|
|
1412
|
+
}
|
|
1413
|
+
|
|
1414
|
+
// src/tools/is_checked.ts
|
|
1415
|
+
import { tool as tool30 } from "@opencode-ai/plugin";
|
|
1416
|
+
var z30 = tool30.schema;
|
|
1417
|
+
function createIsCheckedTool(deps) {
|
|
1418
|
+
return tool30({
|
|
1419
|
+
description: "Check if an element is checked by ref.",
|
|
1420
|
+
args: {
|
|
1421
|
+
sessionId: z30.string().describe("Active browser session id"),
|
|
1422
|
+
ref: z30.string().describe("Element ref from snapshot")
|
|
1423
|
+
},
|
|
1424
|
+
async execute(args) {
|
|
1425
|
+
try {
|
|
1426
|
+
const result = await deps.manager.domIsChecked(args.sessionId, args.ref);
|
|
1427
|
+
return ok(result);
|
|
1428
|
+
} catch (error) {
|
|
1429
|
+
return failure(serializeError(error).message, "is_checked_failed");
|
|
1430
|
+
}
|
|
1431
|
+
}
|
|
1432
|
+
});
|
|
1433
|
+
}
|
|
1434
|
+
|
|
3342
1435
|
// src/tools/run.ts
|
|
3343
|
-
import { tool as
|
|
3344
|
-
var
|
|
3345
|
-
var stepSchema =
|
|
3346
|
-
action:
|
|
3347
|
-
args:
|
|
1436
|
+
import { tool as tool31 } from "@opencode-ai/plugin";
|
|
1437
|
+
var z31 = tool31.schema;
|
|
1438
|
+
var stepSchema = z31.object({
|
|
1439
|
+
action: z31.string().describe("Action name"),
|
|
1440
|
+
args: z31.record(z31.string(), z31.unknown()).optional().describe("Action arguments")
|
|
3348
1441
|
});
|
|
3349
1442
|
function createRunTool(deps) {
|
|
3350
|
-
return
|
|
1443
|
+
return tool31({
|
|
3351
1444
|
description: "Run multiple actions in a single tool call.",
|
|
3352
1445
|
args: {
|
|
3353
|
-
sessionId:
|
|
3354
|
-
steps:
|
|
3355
|
-
stopOnError:
|
|
3356
|
-
maxSnapshotChars:
|
|
1446
|
+
sessionId: z31.string().describe("Session id"),
|
|
1447
|
+
steps: z31.array(stepSchema).describe("Steps to execute"),
|
|
1448
|
+
stopOnError: z31.boolean().optional().describe("Stop when a step fails"),
|
|
1449
|
+
maxSnapshotChars: z31.number().int().optional().describe("Default maxChars for snapshot steps")
|
|
3357
1450
|
},
|
|
3358
1451
|
async execute(args) {
|
|
3359
1452
|
try {
|
|
@@ -3386,13 +1479,13 @@ function normalizeSteps(steps, maxSnapshotChars) {
|
|
|
3386
1479
|
}
|
|
3387
1480
|
|
|
3388
1481
|
// src/tools/prompting_guide.ts
|
|
3389
|
-
import { tool as
|
|
3390
|
-
var
|
|
1482
|
+
import { tool as tool32 } from "@opencode-ai/plugin";
|
|
1483
|
+
var z32 = tool32.schema;
|
|
3391
1484
|
function createPromptingGuideTool(deps) {
|
|
3392
|
-
return
|
|
1485
|
+
return tool32({
|
|
3393
1486
|
description: "Return best-practice prompting guidance for OpenDevBrowser.",
|
|
3394
1487
|
args: {
|
|
3395
|
-
topic:
|
|
1488
|
+
topic: z32.string().optional().describe("Optional topic for guidance")
|
|
3396
1489
|
},
|
|
3397
1490
|
async execute(args) {
|
|
3398
1491
|
try {
|
|
@@ -3406,19 +1499,19 @@ function createPromptingGuideTool(deps) {
|
|
|
3406
1499
|
}
|
|
3407
1500
|
|
|
3408
1501
|
// src/tools/console_poll.ts
|
|
3409
|
-
import { tool as
|
|
3410
|
-
var
|
|
1502
|
+
import { tool as tool33 } from "@opencode-ai/plugin";
|
|
1503
|
+
var z33 = tool33.schema;
|
|
3411
1504
|
function createConsolePollTool(deps) {
|
|
3412
|
-
return
|
|
1505
|
+
return tool33({
|
|
3413
1506
|
description: "Poll console events for the active target.",
|
|
3414
1507
|
args: {
|
|
3415
|
-
sessionId:
|
|
3416
|
-
sinceSeq:
|
|
3417
|
-
max:
|
|
1508
|
+
sessionId: z33.string().describe("Session id"),
|
|
1509
|
+
sinceSeq: z33.number().int().optional().describe("Sequence to resume from"),
|
|
1510
|
+
max: z33.number().int().optional().describe("Max events to return")
|
|
3418
1511
|
},
|
|
3419
1512
|
async execute(args) {
|
|
3420
1513
|
try {
|
|
3421
|
-
const result = deps.manager.consolePoll(
|
|
1514
|
+
const result = await deps.manager.consolePoll(
|
|
3422
1515
|
args.sessionId,
|
|
3423
1516
|
args.sinceSeq,
|
|
3424
1517
|
args.max ?? 50
|
|
@@ -3432,19 +1525,19 @@ function createConsolePollTool(deps) {
|
|
|
3432
1525
|
}
|
|
3433
1526
|
|
|
3434
1527
|
// src/tools/network_poll.ts
|
|
3435
|
-
import { tool as
|
|
3436
|
-
var
|
|
1528
|
+
import { tool as tool34 } from "@opencode-ai/plugin";
|
|
1529
|
+
var z34 = tool34.schema;
|
|
3437
1530
|
function createNetworkPollTool(deps) {
|
|
3438
|
-
return
|
|
1531
|
+
return tool34({
|
|
3439
1532
|
description: "Poll network events for the active target.",
|
|
3440
1533
|
args: {
|
|
3441
|
-
sessionId:
|
|
3442
|
-
sinceSeq:
|
|
3443
|
-
max:
|
|
1534
|
+
sessionId: z34.string().describe("Session id"),
|
|
1535
|
+
sinceSeq: z34.number().int().optional().describe("Sequence to resume from"),
|
|
1536
|
+
max: z34.number().int().optional().describe("Max events to return")
|
|
3444
1537
|
},
|
|
3445
1538
|
async execute(args) {
|
|
3446
1539
|
try {
|
|
3447
|
-
const result = deps.manager.networkPoll(
|
|
1540
|
+
const result = await deps.manager.networkPoll(
|
|
3448
1541
|
args.sessionId,
|
|
3449
1542
|
args.sinceSeq,
|
|
3450
1543
|
args.max ?? 50
|
|
@@ -3458,13 +1551,13 @@ function createNetworkPollTool(deps) {
|
|
|
3458
1551
|
}
|
|
3459
1552
|
|
|
3460
1553
|
// src/tools/clone_page.ts
|
|
3461
|
-
import { tool as
|
|
3462
|
-
var
|
|
1554
|
+
import { tool as tool35 } from "@opencode-ai/plugin";
|
|
1555
|
+
var z35 = tool35.schema;
|
|
3463
1556
|
function createClonePageTool(deps) {
|
|
3464
|
-
return
|
|
1557
|
+
return tool35({
|
|
3465
1558
|
description: "Export the active page as a React component and CSS bundle.",
|
|
3466
1559
|
args: {
|
|
3467
|
-
sessionId:
|
|
1560
|
+
sessionId: z35.string().describe("Active browser session id")
|
|
3468
1561
|
},
|
|
3469
1562
|
async execute(args) {
|
|
3470
1563
|
try {
|
|
@@ -3478,14 +1571,14 @@ function createClonePageTool(deps) {
|
|
|
3478
1571
|
}
|
|
3479
1572
|
|
|
3480
1573
|
// src/tools/clone_component.ts
|
|
3481
|
-
import { tool as
|
|
3482
|
-
var
|
|
1574
|
+
import { tool as tool36 } from "@opencode-ai/plugin";
|
|
1575
|
+
var z36 = tool36.schema;
|
|
3483
1576
|
function createCloneComponentTool(deps) {
|
|
3484
|
-
return
|
|
1577
|
+
return tool36({
|
|
3485
1578
|
description: "Export a selected element subtree as a React component and CSS bundle.",
|
|
3486
1579
|
args: {
|
|
3487
|
-
sessionId:
|
|
3488
|
-
ref:
|
|
1580
|
+
sessionId: z36.string().describe("Active browser session id"),
|
|
1581
|
+
ref: z36.string().describe("Element ref from snapshot")
|
|
3489
1582
|
},
|
|
3490
1583
|
async execute(args) {
|
|
3491
1584
|
try {
|
|
@@ -3499,13 +1592,13 @@ function createCloneComponentTool(deps) {
|
|
|
3499
1592
|
}
|
|
3500
1593
|
|
|
3501
1594
|
// src/tools/perf.ts
|
|
3502
|
-
import { tool as
|
|
3503
|
-
var
|
|
1595
|
+
import { tool as tool37 } from "@opencode-ai/plugin";
|
|
1596
|
+
var z37 = tool37.schema;
|
|
3504
1597
|
function createPerfTool(deps) {
|
|
3505
|
-
return
|
|
1598
|
+
return tool37({
|
|
3506
1599
|
description: "Fetch lightweight performance metrics from the active page.",
|
|
3507
1600
|
args: {
|
|
3508
|
-
sessionId:
|
|
1601
|
+
sessionId: z37.string().describe("Active browser session id")
|
|
3509
1602
|
},
|
|
3510
1603
|
async execute(args) {
|
|
3511
1604
|
try {
|
|
@@ -3519,14 +1612,14 @@ function createPerfTool(deps) {
|
|
|
3519
1612
|
}
|
|
3520
1613
|
|
|
3521
1614
|
// src/tools/screenshot.ts
|
|
3522
|
-
import { tool as
|
|
3523
|
-
var
|
|
1615
|
+
import { tool as tool38 } from "@opencode-ai/plugin";
|
|
1616
|
+
var z38 = tool38.schema;
|
|
3524
1617
|
function createScreenshotTool(deps) {
|
|
3525
|
-
return
|
|
1618
|
+
return tool38({
|
|
3526
1619
|
description: "Capture a screenshot of the active page.",
|
|
3527
1620
|
args: {
|
|
3528
|
-
sessionId:
|
|
3529
|
-
path:
|
|
1621
|
+
sessionId: z38.string().describe("Active browser session id"),
|
|
1622
|
+
path: z38.string().optional().describe("Optional output file path")
|
|
3530
1623
|
},
|
|
3531
1624
|
async execute(args) {
|
|
3532
1625
|
try {
|
|
@@ -3539,10 +1632,67 @@ function createScreenshotTool(deps) {
|
|
|
3539
1632
|
});
|
|
3540
1633
|
}
|
|
3541
1634
|
|
|
1635
|
+
// src/tools/annotate.ts
|
|
1636
|
+
import { tool as tool39 } from "@opencode-ai/plugin";
|
|
1637
|
+
var z39 = tool39.schema;
|
|
1638
|
+
var screenshotModeSchema = z39.enum(["visible", "full", "none"]);
|
|
1639
|
+
var transportSchema = z39.enum(["auto", "direct", "relay"]);
|
|
1640
|
+
function createAnnotateTool(deps) {
|
|
1641
|
+
return tool39({
|
|
1642
|
+
description: "Request interactive annotations via direct (CDP) or relay transport.",
|
|
1643
|
+
args: {
|
|
1644
|
+
sessionId: z39.string().describe("Session id"),
|
|
1645
|
+
transport: transportSchema.optional().describe("auto | direct | relay (default: auto)"),
|
|
1646
|
+
targetId: z39.string().optional().describe("Optional target id for direct mode"),
|
|
1647
|
+
tabId: z39.number().int().optional().describe("Optional Chrome tab id for relay mode"),
|
|
1648
|
+
url: z39.string().optional().describe("Optional URL to open before annotating"),
|
|
1649
|
+
screenshotMode: screenshotModeSchema.optional().describe("visible | full | none (default: visible)"),
|
|
1650
|
+
debug: z39.boolean().optional().describe("Include debug metadata"),
|
|
1651
|
+
context: z39.string().optional().describe("Optional context for the annotator"),
|
|
1652
|
+
timeoutMs: z39.number().int().optional().describe("Timeout in milliseconds")
|
|
1653
|
+
},
|
|
1654
|
+
async execute(args) {
|
|
1655
|
+
try {
|
|
1656
|
+
const transport = args.transport ?? "auto";
|
|
1657
|
+
if (transport === "relay") {
|
|
1658
|
+
const status = await deps.manager.status(args.sessionId);
|
|
1659
|
+
if (status.mode !== "extension") {
|
|
1660
|
+
return failure("Annotations require extension mode (relay).", "annotate_requires_extension");
|
|
1661
|
+
}
|
|
1662
|
+
}
|
|
1663
|
+
const response = await deps.annotationManager.requestAnnotation({
|
|
1664
|
+
sessionId: args.sessionId,
|
|
1665
|
+
transport,
|
|
1666
|
+
targetId: args.targetId,
|
|
1667
|
+
tabId: args.tabId,
|
|
1668
|
+
url: args.url,
|
|
1669
|
+
screenshotMode: args.screenshotMode ?? "visible",
|
|
1670
|
+
debug: args.debug ?? false,
|
|
1671
|
+
context: args.context,
|
|
1672
|
+
timeoutMs: args.timeoutMs
|
|
1673
|
+
});
|
|
1674
|
+
if (response.status !== "ok" || !response.payload) {
|
|
1675
|
+
const message2 = response.error?.message ?? "Annotation failed.";
|
|
1676
|
+
const code = response.error?.code ?? "annotate_failed";
|
|
1677
|
+
return failure(message2, code);
|
|
1678
|
+
}
|
|
1679
|
+
const { message, details, screenshots } = await buildAnnotateResult(response.payload);
|
|
1680
|
+
return ok({
|
|
1681
|
+
message,
|
|
1682
|
+
details,
|
|
1683
|
+
screenshots
|
|
1684
|
+
});
|
|
1685
|
+
} catch (error) {
|
|
1686
|
+
return failure(serializeError(error).message, "annotate_failed");
|
|
1687
|
+
}
|
|
1688
|
+
}
|
|
1689
|
+
});
|
|
1690
|
+
}
|
|
1691
|
+
|
|
3542
1692
|
// src/tools/skill_list.ts
|
|
3543
|
-
import { tool as
|
|
1693
|
+
import { tool as tool40 } from "@opencode-ai/plugin";
|
|
3544
1694
|
function createSkillListTool(deps) {
|
|
3545
|
-
return
|
|
1695
|
+
return tool40({
|
|
3546
1696
|
description: "List available skills from OpenCode skill directories (compatibility wrapper)",
|
|
3547
1697
|
args: {},
|
|
3548
1698
|
async execute() {
|
|
@@ -3558,14 +1708,14 @@ function createSkillListTool(deps) {
|
|
|
3558
1708
|
}
|
|
3559
1709
|
|
|
3560
1710
|
// src/tools/skill_load.ts
|
|
3561
|
-
import { tool as
|
|
3562
|
-
var
|
|
1711
|
+
import { tool as tool41 } from "@opencode-ai/plugin";
|
|
1712
|
+
var z40 = tool41.schema;
|
|
3563
1713
|
function createSkillLoadTool(deps) {
|
|
3564
|
-
return
|
|
1714
|
+
return tool41({
|
|
3565
1715
|
description: "Load a specific skill by name from OpenCode skill directories (compatibility wrapper)",
|
|
3566
1716
|
args: {
|
|
3567
|
-
name:
|
|
3568
|
-
topic:
|
|
1717
|
+
name: z40.string().describe("Name of the skill to load (e.g., 'login-automation', 'form-testing')"),
|
|
1718
|
+
topic: z40.string().optional().describe("Optional topic to filter the skill content")
|
|
3569
1719
|
},
|
|
3570
1720
|
async execute(args) {
|
|
3571
1721
|
try {
|
|
@@ -3581,129 +1731,203 @@ function createSkillLoadTool(deps) {
|
|
|
3581
1731
|
|
|
3582
1732
|
// src/tools/index.ts
|
|
3583
1733
|
function createTools(deps) {
|
|
1734
|
+
const wrap = (definition) => {
|
|
1735
|
+
if (!deps.ensureHub) return definition;
|
|
1736
|
+
return {
|
|
1737
|
+
...definition,
|
|
1738
|
+
execute: async (args, context) => {
|
|
1739
|
+
try {
|
|
1740
|
+
await deps.ensureHub?.();
|
|
1741
|
+
} catch {
|
|
1742
|
+
}
|
|
1743
|
+
return definition.execute(args, context);
|
|
1744
|
+
}
|
|
1745
|
+
};
|
|
1746
|
+
};
|
|
3584
1747
|
return {
|
|
3585
|
-
opendevbrowser_launch: createLaunchTool(deps),
|
|
3586
|
-
opendevbrowser_connect: createConnectTool(deps),
|
|
3587
|
-
opendevbrowser_disconnect: createDisconnectTool(deps),
|
|
3588
|
-
opendevbrowser_status: createStatusTool(deps),
|
|
3589
|
-
opendevbrowser_targets_list: createTargetsListTool(deps),
|
|
3590
|
-
opendevbrowser_target_use: createTargetUseTool(deps),
|
|
3591
|
-
opendevbrowser_target_new: createTargetNewTool(deps),
|
|
3592
|
-
opendevbrowser_target_close: createTargetCloseTool(deps),
|
|
3593
|
-
opendevbrowser_page: createPageTool(deps),
|
|
3594
|
-
opendevbrowser_list: createListTool(deps),
|
|
3595
|
-
opendevbrowser_close: createCloseTool(deps),
|
|
3596
|
-
opendevbrowser_goto: createGotoTool(deps),
|
|
3597
|
-
opendevbrowser_wait: createWaitTool(deps),
|
|
3598
|
-
opendevbrowser_snapshot: createSnapshotTool(deps),
|
|
3599
|
-
opendevbrowser_click: createClickTool(deps),
|
|
3600
|
-
|
|
3601
|
-
|
|
3602
|
-
|
|
3603
|
-
|
|
3604
|
-
|
|
3605
|
-
|
|
3606
|
-
|
|
3607
|
-
|
|
3608
|
-
|
|
3609
|
-
|
|
3610
|
-
|
|
3611
|
-
|
|
3612
|
-
|
|
3613
|
-
|
|
3614
|
-
|
|
1748
|
+
opendevbrowser_launch: wrap(createLaunchTool(deps)),
|
|
1749
|
+
opendevbrowser_connect: wrap(createConnectTool(deps)),
|
|
1750
|
+
opendevbrowser_disconnect: wrap(createDisconnectTool(deps)),
|
|
1751
|
+
opendevbrowser_status: wrap(createStatusTool(deps)),
|
|
1752
|
+
opendevbrowser_targets_list: wrap(createTargetsListTool(deps)),
|
|
1753
|
+
opendevbrowser_target_use: wrap(createTargetUseTool(deps)),
|
|
1754
|
+
opendevbrowser_target_new: wrap(createTargetNewTool(deps)),
|
|
1755
|
+
opendevbrowser_target_close: wrap(createTargetCloseTool(deps)),
|
|
1756
|
+
opendevbrowser_page: wrap(createPageTool(deps)),
|
|
1757
|
+
opendevbrowser_list: wrap(createListTool(deps)),
|
|
1758
|
+
opendevbrowser_close: wrap(createCloseTool(deps)),
|
|
1759
|
+
opendevbrowser_goto: wrap(createGotoTool(deps)),
|
|
1760
|
+
opendevbrowser_wait: wrap(createWaitTool(deps)),
|
|
1761
|
+
opendevbrowser_snapshot: wrap(createSnapshotTool(deps)),
|
|
1762
|
+
opendevbrowser_click: wrap(createClickTool(deps)),
|
|
1763
|
+
opendevbrowser_hover: wrap(createHoverTool(deps)),
|
|
1764
|
+
opendevbrowser_press: wrap(createPressTool(deps)),
|
|
1765
|
+
opendevbrowser_check: wrap(createCheckTool(deps)),
|
|
1766
|
+
opendevbrowser_uncheck: wrap(createUncheckTool(deps)),
|
|
1767
|
+
opendevbrowser_type: wrap(createTypeTool(deps)),
|
|
1768
|
+
opendevbrowser_select: wrap(createSelectTool(deps)),
|
|
1769
|
+
opendevbrowser_scroll: wrap(createScrollTool(deps)),
|
|
1770
|
+
opendevbrowser_scroll_into_view: wrap(createScrollIntoViewTool(deps)),
|
|
1771
|
+
opendevbrowser_dom_get_html: wrap(createDomGetHtmlTool(deps)),
|
|
1772
|
+
opendevbrowser_dom_get_text: wrap(createDomGetTextTool(deps)),
|
|
1773
|
+
opendevbrowser_get_attr: wrap(createGetAttrTool(deps)),
|
|
1774
|
+
opendevbrowser_get_value: wrap(createGetValueTool(deps)),
|
|
1775
|
+
opendevbrowser_is_visible: wrap(createIsVisibleTool(deps)),
|
|
1776
|
+
opendevbrowser_is_enabled: wrap(createIsEnabledTool(deps)),
|
|
1777
|
+
opendevbrowser_is_checked: wrap(createIsCheckedTool(deps)),
|
|
1778
|
+
opendevbrowser_run: wrap(createRunTool(deps)),
|
|
1779
|
+
opendevbrowser_prompting_guide: wrap(createPromptingGuideTool(deps)),
|
|
1780
|
+
opendevbrowser_console_poll: wrap(createConsolePollTool(deps)),
|
|
1781
|
+
opendevbrowser_network_poll: wrap(createNetworkPollTool(deps)),
|
|
1782
|
+
opendevbrowser_clone_page: wrap(createClonePageTool(deps)),
|
|
1783
|
+
opendevbrowser_clone_component: wrap(createCloneComponentTool(deps)),
|
|
1784
|
+
opendevbrowser_perf: wrap(createPerfTool(deps)),
|
|
1785
|
+
opendevbrowser_screenshot: wrap(createScreenshotTool(deps)),
|
|
1786
|
+
opendevbrowser_annotate: wrap(createAnnotateTool(deps)),
|
|
1787
|
+
opendevbrowser_skill_list: wrap(createSkillListTool(deps)),
|
|
1788
|
+
opendevbrowser_skill_load: wrap(createSkillLoadTool(deps))
|
|
3615
1789
|
};
|
|
3616
1790
|
}
|
|
3617
1791
|
|
|
3618
1792
|
// src/index.ts
|
|
3619
1793
|
var OpenDevBrowserPlugin = async ({ directory, worktree }) => {
|
|
3620
|
-
const
|
|
3621
|
-
const configStore =
|
|
3622
|
-
|
|
3623
|
-
|
|
3624
|
-
|
|
3625
|
-
|
|
3626
|
-
|
|
1794
|
+
const core = createOpenDevBrowserCore({ directory, worktree });
|
|
1795
|
+
const { config, configStore, skills, ensureRelay, cleanup, getExtensionPath } = core;
|
|
1796
|
+
let relay = core.relay;
|
|
1797
|
+
let manager = core.manager;
|
|
1798
|
+
let runner = core.runner;
|
|
1799
|
+
let annotationManager = core.annotationManager;
|
|
1800
|
+
let hubStop = null;
|
|
1801
|
+
let daemonClient = null;
|
|
3627
1802
|
const skillNudgeState = createSkillNudgeState();
|
|
3628
1803
|
const continuityNudgeState = createContinuityNudgeState();
|
|
3629
|
-
relay.setToken(initialConfig.relayToken);
|
|
3630
1804
|
console.info(
|
|
3631
|
-
`[opendevbrowser] loaded (cacheRoot=${cacheRoot}, relay=${
|
|
1805
|
+
`[opendevbrowser] loaded (cacheRoot=${core.cacheRoot}, relay=${config.relayToken === false ? "disabled" : "enabled"})`
|
|
3632
1806
|
);
|
|
3633
1807
|
try {
|
|
3634
1808
|
extractExtension();
|
|
3635
1809
|
} catch (error) {
|
|
3636
1810
|
console.warn("Extension extraction failed:", error instanceof Error ? error.message : error);
|
|
3637
1811
|
}
|
|
3638
|
-
const
|
|
3639
|
-
|
|
3640
|
-
|
|
1812
|
+
const toolDeps = {
|
|
1813
|
+
manager,
|
|
1814
|
+
annotationManager,
|
|
1815
|
+
runner,
|
|
1816
|
+
config: configStore,
|
|
1817
|
+
skills,
|
|
1818
|
+
relay,
|
|
1819
|
+
getExtensionPath
|
|
1820
|
+
};
|
|
1821
|
+
const bindRemote = () => {
|
|
1822
|
+
if (!daemonClient) {
|
|
1823
|
+
daemonClient = new DaemonClient({ autoRenew: true });
|
|
1824
|
+
}
|
|
1825
|
+
manager = new RemoteManager(daemonClient);
|
|
1826
|
+
relay = new RemoteRelay(daemonClient);
|
|
1827
|
+
annotationManager.setRelay(relay);
|
|
1828
|
+
annotationManager.setBrowserManager(manager);
|
|
1829
|
+
runner = new ScriptRunner(manager);
|
|
1830
|
+
toolDeps.manager = manager;
|
|
1831
|
+
toolDeps.relay = relay;
|
|
1832
|
+
toolDeps.runner = runner;
|
|
1833
|
+
};
|
|
1834
|
+
const ensureHub = async () => {
|
|
1835
|
+
const currentConfig = configStore.get();
|
|
1836
|
+
if (!isHubEnabled(currentConfig)) {
|
|
3641
1837
|
return;
|
|
3642
1838
|
}
|
|
3643
|
-
|
|
3644
|
-
|
|
3645
|
-
|
|
1839
|
+
if (!daemonClient) {
|
|
1840
|
+
daemonClient = new DaemonClient({ autoRenew: true });
|
|
1841
|
+
}
|
|
1842
|
+
const deadline = Date.now() + 2e3;
|
|
1843
|
+
let attempt = 0;
|
|
1844
|
+
let lastError = null;
|
|
1845
|
+
while (attempt < 2 && Date.now() < deadline) {
|
|
1846
|
+
attempt += 1;
|
|
1847
|
+
const status = await fetchDaemonStatusFromMetadata(currentConfig);
|
|
1848
|
+
if (status?.ok) {
|
|
1849
|
+
bindRemote();
|
|
1850
|
+
await relay?.refresh?.();
|
|
1851
|
+
return;
|
|
1852
|
+
}
|
|
1853
|
+
try {
|
|
1854
|
+
const { stop } = await startDaemon({ config: currentConfig, directory, worktree });
|
|
1855
|
+
hubStop = stop;
|
|
1856
|
+
} catch (error) {
|
|
1857
|
+
lastError = error instanceof Error ? error : new Error(String(error));
|
|
1858
|
+
}
|
|
1859
|
+
if (Date.now() < deadline) {
|
|
1860
|
+
await new Promise((resolve) => setTimeout(resolve, 500));
|
|
1861
|
+
}
|
|
1862
|
+
}
|
|
1863
|
+
if (lastError) {
|
|
1864
|
+
throw lastError;
|
|
3646
1865
|
}
|
|
3647
|
-
|
|
1866
|
+
throw new Error("Hub daemon unavailable.");
|
|
1867
|
+
};
|
|
1868
|
+
toolDeps.ensureHub = ensureHub;
|
|
1869
|
+
const hubEnabled = isHubEnabled(config);
|
|
1870
|
+
if (hubEnabled) {
|
|
1871
|
+
bindRemote();
|
|
3648
1872
|
try {
|
|
3649
|
-
await
|
|
1873
|
+
await ensureHub();
|
|
3650
1874
|
} catch (error) {
|
|
3651
1875
|
const message = error instanceof Error ? error.message : String(error);
|
|
3652
|
-
|
|
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
|
-
}
|
|
1876
|
+
console.warn(`[opendevbrowser] Hub daemon unavailable: ${message}`);
|
|
3658
1877
|
}
|
|
3659
|
-
}
|
|
3660
|
-
|
|
3661
|
-
|
|
3662
|
-
|
|
1878
|
+
} else {
|
|
1879
|
+
await ensureRelay(config.relayPort);
|
|
1880
|
+
}
|
|
1881
|
+
const cleanupAll = () => {
|
|
1882
|
+
if (hubStop) {
|
|
1883
|
+
hubStop().catch(() => {
|
|
1884
|
+
});
|
|
1885
|
+
}
|
|
1886
|
+
daemonClient?.releaseBinding().catch(() => {
|
|
3663
1887
|
});
|
|
1888
|
+
cleanup();
|
|
3664
1889
|
};
|
|
3665
|
-
process.on("SIGINT",
|
|
3666
|
-
process.on("SIGTERM",
|
|
3667
|
-
process.on("beforeExit",
|
|
3668
|
-
await ensureRelay(initialConfig.relayPort);
|
|
1890
|
+
process.on("SIGINT", cleanupAll);
|
|
1891
|
+
process.on("SIGTERM", cleanupAll);
|
|
1892
|
+
process.on("beforeExit", cleanupAll);
|
|
3669
1893
|
return {
|
|
3670
|
-
tool: createTools(
|
|
1894
|
+
tool: createTools(toolDeps),
|
|
3671
1895
|
"chat.message": async (_input, output) => {
|
|
3672
|
-
const
|
|
1896
|
+
const config2 = configStore.get();
|
|
3673
1897
|
if (output.message.role !== "user") return;
|
|
3674
1898
|
const text = extractTextFromParts(output.parts);
|
|
3675
1899
|
if (!text) return;
|
|
3676
|
-
if (
|
|
1900
|
+
if (config2.skills.nudge.enabled && shouldTriggerSkillNudge(text, config2.skills.nudge.keywords)) {
|
|
3677
1901
|
markSkillNudge(skillNudgeState, Date.now());
|
|
3678
1902
|
}
|
|
3679
|
-
if (
|
|
3680
|
-
if (shouldTriggerContinuityNudge(text,
|
|
1903
|
+
if (config2.continuity.enabled && config2.continuity.nudge.enabled) {
|
|
1904
|
+
if (shouldTriggerContinuityNudge(text, config2.continuity.nudge.keywords)) {
|
|
3681
1905
|
markContinuityNudge(continuityNudgeState, Date.now());
|
|
3682
1906
|
}
|
|
3683
1907
|
}
|
|
3684
1908
|
},
|
|
3685
1909
|
"experimental.chat.system.transform": async (_input, output) => {
|
|
3686
|
-
const
|
|
1910
|
+
const config2 = configStore.get();
|
|
3687
1911
|
const systemEntries = output.system ?? [];
|
|
3688
1912
|
let nextEntries = systemEntries;
|
|
3689
1913
|
let changed = false;
|
|
3690
|
-
if (
|
|
1914
|
+
if (config2.skills.nudge.enabled) {
|
|
3691
1915
|
if (systemEntries.some((entry) => entry.includes(SKILL_NUDGE_MARKER))) {
|
|
3692
1916
|
clearSkillNudge(skillNudgeState);
|
|
3693
|
-
} else if (consumeSkillNudge(skillNudgeState, Date.now(),
|
|
1917
|
+
} else if (consumeSkillNudge(skillNudgeState, Date.now(), config2.skills.nudge.maxAgeMs)) {
|
|
3694
1918
|
nextEntries = [...nextEntries, buildSkillNudgeMessage()];
|
|
3695
1919
|
changed = true;
|
|
3696
1920
|
}
|
|
3697
1921
|
}
|
|
3698
|
-
if (
|
|
1922
|
+
if (config2.continuity.enabled && config2.continuity.nudge.enabled) {
|
|
3699
1923
|
if (systemEntries.some((entry) => entry.includes(CONTINUITY_NUDGE_MARKER))) {
|
|
3700
1924
|
clearContinuityNudge(continuityNudgeState);
|
|
3701
1925
|
} else if (consumeContinuityNudge(
|
|
3702
1926
|
continuityNudgeState,
|
|
3703
1927
|
Date.now(),
|
|
3704
|
-
|
|
1928
|
+
config2.continuity.nudge.maxAgeMs
|
|
3705
1929
|
)) {
|
|
3706
|
-
nextEntries = [...nextEntries, buildContinuityNudgeMessage(
|
|
1930
|
+
nextEntries = [...nextEntries, buildContinuityNudgeMessage(config2.continuity.filePath)];
|
|
3707
1931
|
changed = true;
|
|
3708
1932
|
}
|
|
3709
1933
|
}
|
|
@@ -3717,5 +1941,4 @@ var index_default = OpenDevBrowserPlugin;
|
|
|
3717
1941
|
export {
|
|
3718
1942
|
index_default as default
|
|
3719
1943
|
};
|
|
3720
|
-
/* v8 ignore next -- @preserve */
|
|
3721
1944
|
//# sourceMappingURL=index.js.map
|