opendevbrowser 0.0.12 → 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 +216 -28
- package/dist/chunk-JVBMT2O5.js +7173 -0
- package/dist/chunk-JVBMT2O5.js.map +1 -0
- package/dist/cli/index.js +2486 -589
- package/dist/cli/index.js.map +1 -1
- package/dist/index.js +1057 -194
- package/dist/index.js.map +1 -1
- package/dist/opendevbrowser.js +1057 -194
- 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 +1194 -32
- 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 +370 -25
- package/extension/dist/relay-settings.js +1 -0
- 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/manifest.json +17 -3
- package/extension/popup.html +144 -0
- package/package.json +2 -2
- package/skills/AGENTS.md +34 -62
- 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-WTFSMBVH.js +0 -2815
- package/dist/chunk-WTFSMBVH.js.map +0 -1
- package/extension/dist/popup.jsx +0 -150
package/dist/index.js
CHANGED
|
@@ -1,7 +1,211 @@
|
|
|
1
1
|
import {
|
|
2
|
+
DaemonClient,
|
|
3
|
+
ScriptRunner,
|
|
4
|
+
buildAnnotateResult,
|
|
2
5
|
createOpenDevBrowserCore,
|
|
3
|
-
extractExtension
|
|
4
|
-
|
|
6
|
+
extractExtension,
|
|
7
|
+
fetchDaemonStatusFromMetadata,
|
|
8
|
+
startDaemon
|
|
9
|
+
} from "./chunk-JVBMT2O5.js";
|
|
10
|
+
|
|
11
|
+
// src/cli/remote-manager.ts
|
|
12
|
+
function isLegacyRelayEndpoint(wsEndpoint) {
|
|
13
|
+
try {
|
|
14
|
+
const url = new URL(wsEndpoint);
|
|
15
|
+
const path = url.pathname.endsWith("/") ? url.pathname.slice(0, -1) : url.pathname;
|
|
16
|
+
return path === "/cdp";
|
|
17
|
+
} catch {
|
|
18
|
+
return false;
|
|
19
|
+
}
|
|
20
|
+
}
|
|
21
|
+
var RemoteManager = class {
|
|
22
|
+
client;
|
|
23
|
+
constructor(client) {
|
|
24
|
+
this.client = client;
|
|
25
|
+
}
|
|
26
|
+
launch(options) {
|
|
27
|
+
return this.client.call("session.launch", options);
|
|
28
|
+
}
|
|
29
|
+
connect(options) {
|
|
30
|
+
return this.client.call("session.connect", options);
|
|
31
|
+
}
|
|
32
|
+
connectRelay(wsEndpoint) {
|
|
33
|
+
return this.client.call(
|
|
34
|
+
"session.connect",
|
|
35
|
+
isLegacyRelayEndpoint(wsEndpoint) ? { wsEndpoint, extensionLegacy: true } : { wsEndpoint }
|
|
36
|
+
);
|
|
37
|
+
}
|
|
38
|
+
disconnect(sessionId, closeBrowser = false) {
|
|
39
|
+
return this.client.call("session.disconnect", { sessionId, closeBrowser });
|
|
40
|
+
}
|
|
41
|
+
status(sessionId) {
|
|
42
|
+
return this.client.call("session.status", { sessionId });
|
|
43
|
+
}
|
|
44
|
+
goto(sessionId, url, waitUntil = "load", timeoutMs = 3e4) {
|
|
45
|
+
return this.client.call("nav.goto", { sessionId, url, waitUntil, timeoutMs });
|
|
46
|
+
}
|
|
47
|
+
waitForLoad(sessionId, until, timeoutMs = 3e4) {
|
|
48
|
+
return this.client.call("nav.wait", { sessionId, until, timeoutMs });
|
|
49
|
+
}
|
|
50
|
+
waitForRef(sessionId, ref, state = "attached", timeoutMs = 3e4) {
|
|
51
|
+
return this.client.call("nav.wait", { sessionId, ref, state, timeoutMs });
|
|
52
|
+
}
|
|
53
|
+
snapshot(sessionId, mode, maxChars, cursor) {
|
|
54
|
+
return this.client.call("nav.snapshot", { sessionId, mode, maxChars, cursor });
|
|
55
|
+
}
|
|
56
|
+
click(sessionId, ref) {
|
|
57
|
+
return this.client.call("interact.click", { sessionId, ref });
|
|
58
|
+
}
|
|
59
|
+
hover(sessionId, ref) {
|
|
60
|
+
return this.client.call("interact.hover", { sessionId, ref });
|
|
61
|
+
}
|
|
62
|
+
press(sessionId, key, ref) {
|
|
63
|
+
return this.client.call("interact.press", { sessionId, key, ref });
|
|
64
|
+
}
|
|
65
|
+
check(sessionId, ref) {
|
|
66
|
+
return this.client.call("interact.check", { sessionId, ref });
|
|
67
|
+
}
|
|
68
|
+
uncheck(sessionId, ref) {
|
|
69
|
+
return this.client.call("interact.uncheck", { sessionId, ref });
|
|
70
|
+
}
|
|
71
|
+
type(sessionId, ref, text, clear = false, submit = false) {
|
|
72
|
+
return this.client.call("interact.type", { sessionId, ref, text, clear, submit });
|
|
73
|
+
}
|
|
74
|
+
select(sessionId, ref, values) {
|
|
75
|
+
return this.client.call("interact.select", { sessionId, ref, values });
|
|
76
|
+
}
|
|
77
|
+
scroll(sessionId, dy, ref) {
|
|
78
|
+
return this.client.call("interact.scroll", { sessionId, dy, ref });
|
|
79
|
+
}
|
|
80
|
+
scrollIntoView(sessionId, ref) {
|
|
81
|
+
return this.client.call("interact.scrollIntoView", { sessionId, ref });
|
|
82
|
+
}
|
|
83
|
+
domGetHtml(sessionId, ref, maxChars = 8e3) {
|
|
84
|
+
return this.client.call("dom.getHtml", { sessionId, ref, maxChars });
|
|
85
|
+
}
|
|
86
|
+
domGetText(sessionId, ref, maxChars = 8e3) {
|
|
87
|
+
return this.client.call("dom.getText", { sessionId, ref, maxChars });
|
|
88
|
+
}
|
|
89
|
+
domGetAttr(sessionId, ref, name) {
|
|
90
|
+
return this.client.call("dom.getAttr", { sessionId, ref, name });
|
|
91
|
+
}
|
|
92
|
+
domGetValue(sessionId, ref) {
|
|
93
|
+
return this.client.call("dom.getValue", { sessionId, ref });
|
|
94
|
+
}
|
|
95
|
+
domIsVisible(sessionId, ref) {
|
|
96
|
+
return this.client.call("dom.isVisible", { sessionId, ref });
|
|
97
|
+
}
|
|
98
|
+
domIsEnabled(sessionId, ref) {
|
|
99
|
+
return this.client.call("dom.isEnabled", { sessionId, ref });
|
|
100
|
+
}
|
|
101
|
+
domIsChecked(sessionId, ref) {
|
|
102
|
+
return this.client.call("dom.isChecked", { sessionId, ref });
|
|
103
|
+
}
|
|
104
|
+
clonePage(sessionId) {
|
|
105
|
+
return this.client.call("export.clonePage", { sessionId });
|
|
106
|
+
}
|
|
107
|
+
cloneComponent(sessionId, ref) {
|
|
108
|
+
return this.client.call("export.cloneComponent", { sessionId, ref });
|
|
109
|
+
}
|
|
110
|
+
perfMetrics(sessionId) {
|
|
111
|
+
return this.client.call("devtools.perf", { sessionId });
|
|
112
|
+
}
|
|
113
|
+
screenshot(sessionId, path) {
|
|
114
|
+
return this.client.call("page.screenshot", { sessionId, path });
|
|
115
|
+
}
|
|
116
|
+
consolePoll(sessionId, sinceSeq, max = 50) {
|
|
117
|
+
return this.client.call("devtools.consolePoll", { sessionId, sinceSeq, max });
|
|
118
|
+
}
|
|
119
|
+
networkPoll(sessionId, sinceSeq, max = 50) {
|
|
120
|
+
return this.client.call("devtools.networkPoll", { sessionId, sinceSeq, max });
|
|
121
|
+
}
|
|
122
|
+
listTargets(sessionId, includeUrls = false) {
|
|
123
|
+
return this.client.call("targets.list", { sessionId, includeUrls });
|
|
124
|
+
}
|
|
125
|
+
useTarget(sessionId, targetId) {
|
|
126
|
+
return this.client.call("targets.use", { sessionId, targetId });
|
|
127
|
+
}
|
|
128
|
+
newTarget(sessionId, url) {
|
|
129
|
+
return this.client.call("targets.new", { sessionId, url });
|
|
130
|
+
}
|
|
131
|
+
closeTarget(sessionId, targetId) {
|
|
132
|
+
return this.client.call("targets.close", { sessionId, targetId });
|
|
133
|
+
}
|
|
134
|
+
page(sessionId, name, url) {
|
|
135
|
+
return this.client.call("page.open", { sessionId, name, url });
|
|
136
|
+
}
|
|
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
|
|
168
|
+
}
|
|
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() {
|
|
180
|
+
try {
|
|
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;
|
|
189
|
+
} catch {
|
|
190
|
+
this.lastStatus = emptyStatus;
|
|
191
|
+
this.lastCdpUrl = null;
|
|
192
|
+
this.lastAnnotationUrl = null;
|
|
193
|
+
this.lastOpsUrl = null;
|
|
194
|
+
}
|
|
195
|
+
}
|
|
196
|
+
status() {
|
|
197
|
+
return this.lastStatus;
|
|
198
|
+
}
|
|
199
|
+
getCdpUrl() {
|
|
200
|
+
return this.lastCdpUrl;
|
|
201
|
+
}
|
|
202
|
+
getAnnotationUrl() {
|
|
203
|
+
return this.lastAnnotationUrl;
|
|
204
|
+
}
|
|
205
|
+
getOpsUrl() {
|
|
206
|
+
return this.lastOpsUrl;
|
|
207
|
+
}
|
|
208
|
+
};
|
|
5
209
|
|
|
6
210
|
// src/skills/skill-nudge.ts
|
|
7
211
|
var SKILL_NUDGE_MARKER = "[opendevbrowser:skill-nudge]";
|
|
@@ -98,7 +302,7 @@ function serializeError(error) {
|
|
|
98
302
|
var z = tool.schema;
|
|
99
303
|
function createLaunchTool(deps) {
|
|
100
304
|
return tool({
|
|
101
|
-
description: "Launch a
|
|
305
|
+
description: "Launch a browser session (extension relay first) and return a sessionId.",
|
|
102
306
|
args: {
|
|
103
307
|
profile: z.string().optional().describe("Profile name for persistent browsing"),
|
|
104
308
|
headless: z.boolean().optional().describe("Run Chrome in headless mode"),
|
|
@@ -108,102 +312,350 @@ function createLaunchTool(deps) {
|
|
|
108
312
|
persistProfile: z.boolean().optional().describe("Persist profile data between sessions"),
|
|
109
313
|
noExtension: z.boolean().optional().describe("Skip extension relay and launch a new browser"),
|
|
110
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"),
|
|
111
316
|
waitForExtension: z.boolean().optional().describe("Wait for extension to connect before launching"),
|
|
112
317
|
waitTimeoutMs: z.number().int().optional().describe("Timeout for waiting on extension (ms)")
|
|
113
318
|
},
|
|
114
319
|
async execute(args) {
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
const
|
|
121
|
-
|
|
122
|
-
|
|
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"}`;
|
|
123
331
|
}
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
result = await deps.manager.connectRelay(relayUrl);
|
|
135
|
-
usedRelay = true;
|
|
136
|
-
} catch {
|
|
137
|
-
if (args.extensionOnly) {
|
|
138
|
-
return failure("Extension relay connection failed.", "extension_connect_failed");
|
|
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;
|
|
139
342
|
}
|
|
140
|
-
relayWarning = "Relay connection failed; falling back to managed Chrome.";
|
|
141
343
|
}
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
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
|
+
}
|
|
146
421
|
}
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
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
|
|
154
432
|
});
|
|
433
|
+
} catch (error) {
|
|
434
|
+
return failure(serializeError(error).message, "launch_failed");
|
|
155
435
|
}
|
|
156
|
-
if (usedRelay && args.startUrl && result.activeTargetId) {
|
|
157
|
-
await deps.manager.goto(result.sessionId, args.startUrl, "load", 3e4);
|
|
158
|
-
}
|
|
159
|
-
const warnings = [
|
|
160
|
-
...result.warnings ?? [],
|
|
161
|
-
...relayWarning ? [relayWarning] : []
|
|
162
|
-
];
|
|
163
|
-
return ok({
|
|
164
|
-
sessionId: result.sessionId,
|
|
165
|
-
mode: result.mode,
|
|
166
|
-
browserWsEndpoint: result.wsEndpoint,
|
|
167
|
-
activeTargetId: result.activeTargetId,
|
|
168
|
-
warnings: warnings.length ? warnings : void 0
|
|
169
|
-
});
|
|
170
|
-
} catch (error) {
|
|
171
|
-
return failure(serializeError(error).message, "launch_failed");
|
|
172
436
|
}
|
|
173
437
|
}
|
|
174
438
|
});
|
|
175
439
|
}
|
|
176
|
-
|
|
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) {
|
|
177
476
|
const start = Date.now();
|
|
477
|
+
let delay = WAIT_MIN_DELAY_MS;
|
|
178
478
|
while (Date.now() - start < timeoutMs) {
|
|
179
|
-
if (relay
|
|
479
|
+
if (relay?.status().extensionHandshakeComplete) {
|
|
480
|
+
return true;
|
|
481
|
+
}
|
|
482
|
+
const observedStatus = await fetchRelayObservedStatus(observedPort);
|
|
483
|
+
if (observedStatus?.extensionHandshakeComplete) {
|
|
180
484
|
return true;
|
|
181
485
|
}
|
|
182
|
-
await new Promise((resolve) => setTimeout(resolve,
|
|
486
|
+
await new Promise((resolve) => setTimeout(resolve, delay));
|
|
487
|
+
delay = Math.min(delay * 2, WAIT_MAX_DELAY_MS);
|
|
183
488
|
}
|
|
184
489
|
return false;
|
|
185
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
|
+
}
|
|
186
605
|
|
|
187
606
|
// src/tools/connect.ts
|
|
188
607
|
import { tool as tool2 } from "@opencode-ai/plugin";
|
|
189
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
|
+
}
|
|
190
624
|
function createConnectTool(deps) {
|
|
191
625
|
return tool2({
|
|
192
|
-
description: "Connect to an existing Chrome CDP endpoint.",
|
|
626
|
+
description: "Connect to an existing Chrome CDP endpoint or extension relay.",
|
|
193
627
|
args: {
|
|
194
628
|
wsEndpoint: z2.string().optional().describe("Full WebSocket endpoint to connect to"),
|
|
195
629
|
host: z2.string().optional().describe("Host for /json/version lookup"),
|
|
196
|
-
port: z2.number().int().optional().describe("Port 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")
|
|
197
632
|
},
|
|
198
633
|
async execute(args) {
|
|
199
634
|
try {
|
|
200
|
-
|
|
201
|
-
const
|
|
202
|
-
const
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
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
|
+
}
|
|
207
659
|
return ok({
|
|
208
660
|
sessionId: result.sessionId,
|
|
209
661
|
mode: result.mode,
|
|
@@ -244,6 +696,13 @@ import { readFileSync } from "fs";
|
|
|
244
696
|
import { dirname, join } from "path";
|
|
245
697
|
import { fileURLToPath } from "url";
|
|
246
698
|
import { tool as tool4 } from "@opencode-ai/plugin";
|
|
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
|
|
247
706
|
var z4 = tool4.schema;
|
|
248
707
|
function getPackageVersion() {
|
|
249
708
|
try {
|
|
@@ -285,17 +744,45 @@ async function fetchLatestVersion(packageName) {
|
|
|
285
744
|
}
|
|
286
745
|
function createStatusTool(deps) {
|
|
287
746
|
return tool4({
|
|
288
|
-
description: "Get
|
|
747
|
+
description: "Get daemon or session status.",
|
|
289
748
|
args: {
|
|
290
|
-
sessionId: z4.string().describe("Session id")
|
|
749
|
+
sessionId: z4.string().optional().describe("Session id (required when hub is disabled)")
|
|
291
750
|
},
|
|
292
751
|
async execute(args) {
|
|
293
752
|
try {
|
|
294
|
-
const status = await deps.manager.status(args.sessionId);
|
|
295
|
-
const extensionPath = deps.getExtensionPath?.() ?? null;
|
|
296
753
|
const config = deps.config.get();
|
|
754
|
+
const hubEnabled = isHubEnabled(config);
|
|
755
|
+
const extensionPath = deps.getExtensionPath?.() ?? null;
|
|
297
756
|
const version = getPackageVersion();
|
|
298
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);
|
|
299
786
|
if (config.checkForUpdates && version) {
|
|
300
787
|
const latest = await fetchLatestVersion("opendevbrowser");
|
|
301
788
|
if (latest && latest !== version) {
|
|
@@ -303,10 +790,10 @@ function createStatusTool(deps) {
|
|
|
303
790
|
}
|
|
304
791
|
}
|
|
305
792
|
return ok({
|
|
306
|
-
mode:
|
|
307
|
-
activeTargetId:
|
|
308
|
-
url:
|
|
309
|
-
title:
|
|
793
|
+
mode: sessionStatus.mode,
|
|
794
|
+
activeTargetId: sessionStatus.activeTargetId,
|
|
795
|
+
url: sessionStatus.url,
|
|
796
|
+
title: sessionStatus.title,
|
|
310
797
|
extensionPath: extensionPath ?? void 0,
|
|
311
798
|
version,
|
|
312
799
|
updateHint
|
|
@@ -599,18 +1086,103 @@ function createClickTool(deps) {
|
|
|
599
1086
|
});
|
|
600
1087
|
}
|
|
601
1088
|
|
|
602
|
-
// src/tools/
|
|
1089
|
+
// src/tools/hover.ts
|
|
603
1090
|
import { tool as tool16 } from "@opencode-ai/plugin";
|
|
604
1091
|
var z16 = tool16.schema;
|
|
605
|
-
function
|
|
1092
|
+
function createHoverTool(deps) {
|
|
606
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({
|
|
607
1179
|
description: "Type text into a referenced input.",
|
|
608
1180
|
args: {
|
|
609
|
-
sessionId:
|
|
610
|
-
ref:
|
|
611
|
-
text:
|
|
612
|
-
clear:
|
|
613
|
-
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")
|
|
614
1186
|
},
|
|
615
1187
|
async execute(args) {
|
|
616
1188
|
try {
|
|
@@ -630,15 +1202,15 @@ function createTypeTool(deps) {
|
|
|
630
1202
|
}
|
|
631
1203
|
|
|
632
1204
|
// src/tools/select.ts
|
|
633
|
-
import { tool as
|
|
634
|
-
var
|
|
1205
|
+
import { tool as tool21 } from "@opencode-ai/plugin";
|
|
1206
|
+
var z21 = tool21.schema;
|
|
635
1207
|
function createSelectTool(deps) {
|
|
636
|
-
return
|
|
1208
|
+
return tool21({
|
|
637
1209
|
description: "Select options in a referenced select element.",
|
|
638
1210
|
args: {
|
|
639
|
-
sessionId:
|
|
640
|
-
ref:
|
|
641
|
-
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")
|
|
642
1214
|
},
|
|
643
1215
|
async execute(args) {
|
|
644
1216
|
try {
|
|
@@ -652,15 +1224,15 @@ function createSelectTool(deps) {
|
|
|
652
1224
|
}
|
|
653
1225
|
|
|
654
1226
|
// src/tools/scroll.ts
|
|
655
|
-
import { tool as
|
|
656
|
-
var
|
|
1227
|
+
import { tool as tool22 } from "@opencode-ai/plugin";
|
|
1228
|
+
var z22 = tool22.schema;
|
|
657
1229
|
function createScrollTool(deps) {
|
|
658
|
-
return
|
|
1230
|
+
return tool22({
|
|
659
1231
|
description: "Scroll the page or a referenced element.",
|
|
660
1232
|
args: {
|
|
661
|
-
sessionId:
|
|
662
|
-
dy:
|
|
663
|
-
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")
|
|
664
1236
|
},
|
|
665
1237
|
async execute(args) {
|
|
666
1238
|
try {
|
|
@@ -673,16 +1245,37 @@ function createScrollTool(deps) {
|
|
|
673
1245
|
});
|
|
674
1246
|
}
|
|
675
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
|
+
|
|
676
1269
|
// src/tools/dom_get_html.ts
|
|
677
|
-
import { tool as
|
|
678
|
-
var
|
|
1270
|
+
import { tool as tool24 } from "@opencode-ai/plugin";
|
|
1271
|
+
var z24 = tool24.schema;
|
|
679
1272
|
function createDomGetHtmlTool(deps) {
|
|
680
|
-
return
|
|
1273
|
+
return tool24({
|
|
681
1274
|
description: "Get outerHTML for a referenced element.",
|
|
682
1275
|
args: {
|
|
683
|
-
sessionId:
|
|
684
|
-
ref:
|
|
685
|
-
maxChars:
|
|
1276
|
+
sessionId: z24.string().describe("Session id"),
|
|
1277
|
+
ref: z24.string().describe("Element ref"),
|
|
1278
|
+
maxChars: z24.number().int().optional().describe("Max characters")
|
|
686
1279
|
},
|
|
687
1280
|
async execute(args) {
|
|
688
1281
|
try {
|
|
@@ -704,15 +1297,15 @@ function createDomGetHtmlTool(deps) {
|
|
|
704
1297
|
}
|
|
705
1298
|
|
|
706
1299
|
// src/tools/dom_get_text.ts
|
|
707
|
-
import { tool as
|
|
708
|
-
var
|
|
1300
|
+
import { tool as tool25 } from "@opencode-ai/plugin";
|
|
1301
|
+
var z25 = tool25.schema;
|
|
709
1302
|
function createDomGetTextTool(deps) {
|
|
710
|
-
return
|
|
1303
|
+
return tool25({
|
|
711
1304
|
description: "Get inner text for a referenced element.",
|
|
712
1305
|
args: {
|
|
713
|
-
sessionId:
|
|
714
|
-
ref:
|
|
715
|
-
maxChars:
|
|
1306
|
+
sessionId: z25.string().describe("Session id"),
|
|
1307
|
+
ref: z25.string().describe("Element ref"),
|
|
1308
|
+
maxChars: z25.number().int().optional().describe("Max characters")
|
|
716
1309
|
},
|
|
717
1310
|
async execute(args) {
|
|
718
1311
|
try {
|
|
@@ -733,21 +1326,127 @@ function createDomGetTextTool(deps) {
|
|
|
733
1326
|
});
|
|
734
1327
|
}
|
|
735
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
|
+
|
|
736
1435
|
// src/tools/run.ts
|
|
737
|
-
import { tool as
|
|
738
|
-
var
|
|
739
|
-
var stepSchema =
|
|
740
|
-
action:
|
|
741
|
-
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")
|
|
742
1441
|
});
|
|
743
1442
|
function createRunTool(deps) {
|
|
744
|
-
return
|
|
1443
|
+
return tool31({
|
|
745
1444
|
description: "Run multiple actions in a single tool call.",
|
|
746
1445
|
args: {
|
|
747
|
-
sessionId:
|
|
748
|
-
steps:
|
|
749
|
-
stopOnError:
|
|
750
|
-
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")
|
|
751
1450
|
},
|
|
752
1451
|
async execute(args) {
|
|
753
1452
|
try {
|
|
@@ -780,13 +1479,13 @@ function normalizeSteps(steps, maxSnapshotChars) {
|
|
|
780
1479
|
}
|
|
781
1480
|
|
|
782
1481
|
// src/tools/prompting_guide.ts
|
|
783
|
-
import { tool as
|
|
784
|
-
var
|
|
1482
|
+
import { tool as tool32 } from "@opencode-ai/plugin";
|
|
1483
|
+
var z32 = tool32.schema;
|
|
785
1484
|
function createPromptingGuideTool(deps) {
|
|
786
|
-
return
|
|
1485
|
+
return tool32({
|
|
787
1486
|
description: "Return best-practice prompting guidance for OpenDevBrowser.",
|
|
788
1487
|
args: {
|
|
789
|
-
topic:
|
|
1488
|
+
topic: z32.string().optional().describe("Optional topic for guidance")
|
|
790
1489
|
},
|
|
791
1490
|
async execute(args) {
|
|
792
1491
|
try {
|
|
@@ -800,19 +1499,19 @@ function createPromptingGuideTool(deps) {
|
|
|
800
1499
|
}
|
|
801
1500
|
|
|
802
1501
|
// src/tools/console_poll.ts
|
|
803
|
-
import { tool as
|
|
804
|
-
var
|
|
1502
|
+
import { tool as tool33 } from "@opencode-ai/plugin";
|
|
1503
|
+
var z33 = tool33.schema;
|
|
805
1504
|
function createConsolePollTool(deps) {
|
|
806
|
-
return
|
|
1505
|
+
return tool33({
|
|
807
1506
|
description: "Poll console events for the active target.",
|
|
808
1507
|
args: {
|
|
809
|
-
sessionId:
|
|
810
|
-
sinceSeq:
|
|
811
|
-
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")
|
|
812
1511
|
},
|
|
813
1512
|
async execute(args) {
|
|
814
1513
|
try {
|
|
815
|
-
const result = deps.manager.consolePoll(
|
|
1514
|
+
const result = await deps.manager.consolePoll(
|
|
816
1515
|
args.sessionId,
|
|
817
1516
|
args.sinceSeq,
|
|
818
1517
|
args.max ?? 50
|
|
@@ -826,19 +1525,19 @@ function createConsolePollTool(deps) {
|
|
|
826
1525
|
}
|
|
827
1526
|
|
|
828
1527
|
// src/tools/network_poll.ts
|
|
829
|
-
import { tool as
|
|
830
|
-
var
|
|
1528
|
+
import { tool as tool34 } from "@opencode-ai/plugin";
|
|
1529
|
+
var z34 = tool34.schema;
|
|
831
1530
|
function createNetworkPollTool(deps) {
|
|
832
|
-
return
|
|
1531
|
+
return tool34({
|
|
833
1532
|
description: "Poll network events for the active target.",
|
|
834
1533
|
args: {
|
|
835
|
-
sessionId:
|
|
836
|
-
sinceSeq:
|
|
837
|
-
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")
|
|
838
1537
|
},
|
|
839
1538
|
async execute(args) {
|
|
840
1539
|
try {
|
|
841
|
-
const result = deps.manager.networkPoll(
|
|
1540
|
+
const result = await deps.manager.networkPoll(
|
|
842
1541
|
args.sessionId,
|
|
843
1542
|
args.sinceSeq,
|
|
844
1543
|
args.max ?? 50
|
|
@@ -852,13 +1551,13 @@ function createNetworkPollTool(deps) {
|
|
|
852
1551
|
}
|
|
853
1552
|
|
|
854
1553
|
// src/tools/clone_page.ts
|
|
855
|
-
import { tool as
|
|
856
|
-
var
|
|
1554
|
+
import { tool as tool35 } from "@opencode-ai/plugin";
|
|
1555
|
+
var z35 = tool35.schema;
|
|
857
1556
|
function createClonePageTool(deps) {
|
|
858
|
-
return
|
|
1557
|
+
return tool35({
|
|
859
1558
|
description: "Export the active page as a React component and CSS bundle.",
|
|
860
1559
|
args: {
|
|
861
|
-
sessionId:
|
|
1560
|
+
sessionId: z35.string().describe("Active browser session id")
|
|
862
1561
|
},
|
|
863
1562
|
async execute(args) {
|
|
864
1563
|
try {
|
|
@@ -872,14 +1571,14 @@ function createClonePageTool(deps) {
|
|
|
872
1571
|
}
|
|
873
1572
|
|
|
874
1573
|
// src/tools/clone_component.ts
|
|
875
|
-
import { tool as
|
|
876
|
-
var
|
|
1574
|
+
import { tool as tool36 } from "@opencode-ai/plugin";
|
|
1575
|
+
var z36 = tool36.schema;
|
|
877
1576
|
function createCloneComponentTool(deps) {
|
|
878
|
-
return
|
|
1577
|
+
return tool36({
|
|
879
1578
|
description: "Export a selected element subtree as a React component and CSS bundle.",
|
|
880
1579
|
args: {
|
|
881
|
-
sessionId:
|
|
882
|
-
ref:
|
|
1580
|
+
sessionId: z36.string().describe("Active browser session id"),
|
|
1581
|
+
ref: z36.string().describe("Element ref from snapshot")
|
|
883
1582
|
},
|
|
884
1583
|
async execute(args) {
|
|
885
1584
|
try {
|
|
@@ -893,13 +1592,13 @@ function createCloneComponentTool(deps) {
|
|
|
893
1592
|
}
|
|
894
1593
|
|
|
895
1594
|
// src/tools/perf.ts
|
|
896
|
-
import { tool as
|
|
897
|
-
var
|
|
1595
|
+
import { tool as tool37 } from "@opencode-ai/plugin";
|
|
1596
|
+
var z37 = tool37.schema;
|
|
898
1597
|
function createPerfTool(deps) {
|
|
899
|
-
return
|
|
1598
|
+
return tool37({
|
|
900
1599
|
description: "Fetch lightweight performance metrics from the active page.",
|
|
901
1600
|
args: {
|
|
902
|
-
sessionId:
|
|
1601
|
+
sessionId: z37.string().describe("Active browser session id")
|
|
903
1602
|
},
|
|
904
1603
|
async execute(args) {
|
|
905
1604
|
try {
|
|
@@ -913,14 +1612,14 @@ function createPerfTool(deps) {
|
|
|
913
1612
|
}
|
|
914
1613
|
|
|
915
1614
|
// src/tools/screenshot.ts
|
|
916
|
-
import { tool as
|
|
917
|
-
var
|
|
1615
|
+
import { tool as tool38 } from "@opencode-ai/plugin";
|
|
1616
|
+
var z38 = tool38.schema;
|
|
918
1617
|
function createScreenshotTool(deps) {
|
|
919
|
-
return
|
|
1618
|
+
return tool38({
|
|
920
1619
|
description: "Capture a screenshot of the active page.",
|
|
921
1620
|
args: {
|
|
922
|
-
sessionId:
|
|
923
|
-
path:
|
|
1621
|
+
sessionId: z38.string().describe("Active browser session id"),
|
|
1622
|
+
path: z38.string().optional().describe("Optional output file path")
|
|
924
1623
|
},
|
|
925
1624
|
async execute(args) {
|
|
926
1625
|
try {
|
|
@@ -933,10 +1632,67 @@ function createScreenshotTool(deps) {
|
|
|
933
1632
|
});
|
|
934
1633
|
}
|
|
935
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
|
+
|
|
936
1692
|
// src/tools/skill_list.ts
|
|
937
|
-
import { tool as
|
|
1693
|
+
import { tool as tool40 } from "@opencode-ai/plugin";
|
|
938
1694
|
function createSkillListTool(deps) {
|
|
939
|
-
return
|
|
1695
|
+
return tool40({
|
|
940
1696
|
description: "List available skills from OpenCode skill directories (compatibility wrapper)",
|
|
941
1697
|
args: {},
|
|
942
1698
|
async execute() {
|
|
@@ -952,14 +1708,14 @@ function createSkillListTool(deps) {
|
|
|
952
1708
|
}
|
|
953
1709
|
|
|
954
1710
|
// src/tools/skill_load.ts
|
|
955
|
-
import { tool as
|
|
956
|
-
var
|
|
1711
|
+
import { tool as tool41 } from "@opencode-ai/plugin";
|
|
1712
|
+
var z40 = tool41.schema;
|
|
957
1713
|
function createSkillLoadTool(deps) {
|
|
958
|
-
return
|
|
1714
|
+
return tool41({
|
|
959
1715
|
description: "Load a specific skill by name from OpenCode skill directories (compatibility wrapper)",
|
|
960
1716
|
args: {
|
|
961
|
-
name:
|
|
962
|
-
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")
|
|
963
1719
|
},
|
|
964
1720
|
async execute(args) {
|
|
965
1721
|
try {
|
|
@@ -975,44 +1731,74 @@ function createSkillLoadTool(deps) {
|
|
|
975
1731
|
|
|
976
1732
|
// src/tools/index.ts
|
|
977
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
|
+
};
|
|
978
1747
|
return {
|
|
979
|
-
opendevbrowser_launch: createLaunchTool(deps),
|
|
980
|
-
opendevbrowser_connect: createConnectTool(deps),
|
|
981
|
-
opendevbrowser_disconnect: createDisconnectTool(deps),
|
|
982
|
-
opendevbrowser_status: createStatusTool(deps),
|
|
983
|
-
opendevbrowser_targets_list: createTargetsListTool(deps),
|
|
984
|
-
opendevbrowser_target_use: createTargetUseTool(deps),
|
|
985
|
-
opendevbrowser_target_new: createTargetNewTool(deps),
|
|
986
|
-
opendevbrowser_target_close: createTargetCloseTool(deps),
|
|
987
|
-
opendevbrowser_page: createPageTool(deps),
|
|
988
|
-
opendevbrowser_list: createListTool(deps),
|
|
989
|
-
opendevbrowser_close: createCloseTool(deps),
|
|
990
|
-
opendevbrowser_goto: createGotoTool(deps),
|
|
991
|
-
opendevbrowser_wait: createWaitTool(deps),
|
|
992
|
-
opendevbrowser_snapshot: createSnapshotTool(deps),
|
|
993
|
-
opendevbrowser_click: createClickTool(deps),
|
|
994
|
-
|
|
995
|
-
|
|
996
|
-
|
|
997
|
-
|
|
998
|
-
|
|
999
|
-
|
|
1000
|
-
|
|
1001
|
-
|
|
1002
|
-
|
|
1003
|
-
|
|
1004
|
-
|
|
1005
|
-
|
|
1006
|
-
|
|
1007
|
-
|
|
1008
|
-
|
|
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))
|
|
1009
1789
|
};
|
|
1010
1790
|
}
|
|
1011
1791
|
|
|
1012
1792
|
// src/index.ts
|
|
1013
1793
|
var OpenDevBrowserPlugin = async ({ directory, worktree }) => {
|
|
1014
1794
|
const core = createOpenDevBrowserCore({ directory, worktree });
|
|
1015
|
-
const { config, configStore,
|
|
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;
|
|
1016
1802
|
const skillNudgeState = createSkillNudgeState();
|
|
1017
1803
|
const continuityNudgeState = createContinuityNudgeState();
|
|
1018
1804
|
console.info(
|
|
@@ -1023,12 +1809,89 @@ var OpenDevBrowserPlugin = async ({ directory, worktree }) => {
|
|
|
1023
1809
|
} catch (error) {
|
|
1024
1810
|
console.warn("Extension extraction failed:", error instanceof Error ? error.message : error);
|
|
1025
1811
|
}
|
|
1026
|
-
|
|
1027
|
-
|
|
1028
|
-
|
|
1029
|
-
|
|
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)) {
|
|
1837
|
+
return;
|
|
1838
|
+
}
|
|
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;
|
|
1865
|
+
}
|
|
1866
|
+
throw new Error("Hub daemon unavailable.");
|
|
1867
|
+
};
|
|
1868
|
+
toolDeps.ensureHub = ensureHub;
|
|
1869
|
+
const hubEnabled = isHubEnabled(config);
|
|
1870
|
+
if (hubEnabled) {
|
|
1871
|
+
bindRemote();
|
|
1872
|
+
try {
|
|
1873
|
+
await ensureHub();
|
|
1874
|
+
} catch (error) {
|
|
1875
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
1876
|
+
console.warn(`[opendevbrowser] Hub daemon unavailable: ${message}`);
|
|
1877
|
+
}
|
|
1878
|
+
} else {
|
|
1879
|
+
await ensureRelay(config.relayPort);
|
|
1880
|
+
}
|
|
1881
|
+
const cleanupAll = () => {
|
|
1882
|
+
if (hubStop) {
|
|
1883
|
+
hubStop().catch(() => {
|
|
1884
|
+
});
|
|
1885
|
+
}
|
|
1886
|
+
daemonClient?.releaseBinding().catch(() => {
|
|
1887
|
+
});
|
|
1888
|
+
cleanup();
|
|
1889
|
+
};
|
|
1890
|
+
process.on("SIGINT", cleanupAll);
|
|
1891
|
+
process.on("SIGTERM", cleanupAll);
|
|
1892
|
+
process.on("beforeExit", cleanupAll);
|
|
1030
1893
|
return {
|
|
1031
|
-
tool: createTools(
|
|
1894
|
+
tool: createTools(toolDeps),
|
|
1032
1895
|
"chat.message": async (_input, output) => {
|
|
1033
1896
|
const config2 = configStore.get();
|
|
1034
1897
|
if (output.message.role !== "user") return;
|