@vymalo/opencode-browser 0.16.1 → 0.18.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +27 -7
- package/dist/agent-client.d.ts +2 -0
- package/dist/agent-client.js +74 -35
- package/dist/agent-client.js.map +1 -1
- package/dist/endpoint.d.ts +2 -0
- package/dist/endpoint.js +2 -1
- package/dist/endpoint.js.map +1 -1
- package/dist/lib.d.ts +3 -3
- package/dist/lib.js +2 -2
- package/dist/lib.js.map +1 -1
- package/dist/logging.d.ts +35 -1
- package/dist/logging.js +64 -5
- package/dist/logging.js.map +1 -1
- package/dist/opencode.d.ts +21 -4
- package/dist/opencode.js +121 -124
- package/dist/opencode.js.map +1 -1
- package/dist/tools.d.ts +23 -5
- package/dist/tools.js +63 -72
- package/dist/tools.js.map +1 -1
- package/dist/types.d.ts +2 -2
- package/package.json +11 -6
package/README.md
CHANGED
|
@@ -7,6 +7,8 @@
|
|
|
7
7
|

|
|
8
8
|

|
|
9
9
|
|
|
10
|
+
> **Requires OpenCode v2.** This release targets the v2 plugin API and does not run on v1. Migrating from `0.17.x`? See [Migrating from v1](#migrating-from-v1).
|
|
11
|
+
|
|
10
12
|
This is the **OpenCode plugin** half of a dual plugin. It registers `browser_*` tools the model
|
|
11
13
|
can call and hosts a localhost WebSocket **bridge**. A companion **browser extension**
|
|
12
14
|
(Chromium MV3 + Firefox) connects to the bridge and drives real tabs.
|
|
@@ -28,7 +30,7 @@ flowchart LR
|
|
|
28
30
|
- [Install](#install)
|
|
29
31
|
- [Quickstart](#quickstart)
|
|
30
32
|
- [Options](#options)
|
|
31
|
-
- [The
|
|
33
|
+
- [The 34 tools](#the-34-tools)
|
|
32
34
|
- [Targeting elements](#targeting-elements--prefer-refs)
|
|
33
35
|
- [Screenshots](#screenshots)
|
|
34
36
|
- [Named groups](#named-groups)
|
|
@@ -56,7 +58,7 @@ mirrored into the extension so the two never drift.
|
|
|
56
58
|
// opencode.json
|
|
57
59
|
{
|
|
58
60
|
"$schema": "https://opencode.ai/config.json",
|
|
59
|
-
"
|
|
61
|
+
"plugins": [{ "package": "@vymalo/opencode-browser", "options": { "port": 4517 } }]
|
|
60
62
|
}
|
|
61
63
|
```
|
|
62
64
|
|
|
@@ -97,7 +99,7 @@ every action and keeps a screenshot gallery.
|
|
|
97
99
|
|
|
98
100
|
## Options
|
|
99
101
|
|
|
100
|
-
|
|
102
|
+
The plugin entry's `options` in `opencode.json`:
|
|
101
103
|
|
|
102
104
|
| Option | Default | Meaning |
|
|
103
105
|
| --- | --- | --- |
|
|
@@ -108,9 +110,10 @@ Second argument to the plugin tuple in `opencode.json`:
|
|
|
108
110
|
| `groups` | `["page","control"]` | Tool groups to register (`page` \| `control` \| `debug`). |
|
|
109
111
|
| `executor` | `"auto"` | `auto` \| `cdp` \| `content` — forwarded to the extension as a preference. |
|
|
110
112
|
| `timeoutMs` | `30000` | Per-command timeout. |
|
|
111
|
-
| `screenshotDir` | `".opencode/browser"` | Screenshot output dir (relative → resolved against the
|
|
113
|
+
| `screenshotDir` | `".opencode/browser"` | Screenshot output dir (relative → resolved against the project directory). |
|
|
114
|
+
| `logLevel` | `"info"` | `debug`/`trace` unlock the verbose tiers. Falls back to `OPENCODE_LOG_LEVEL`. Records go to OpenCode's own logger, which gates them again (Effect's default minimum is `Info`) — set `OPENCODE_LOG_LEVEL=DEBUG` too. |
|
|
112
115
|
|
|
113
|
-
## The
|
|
116
|
+
## The 34 tools
|
|
114
117
|
|
|
115
118
|
Names are stable `browser_*` identifiers, partitioned into three **groups** gated by the
|
|
116
119
|
`groups` option. Every tool takes a `group` (the named tab group) unless noted.
|
|
@@ -233,7 +236,7 @@ all groups once and give each agent only the tools it needs with **flat string**
|
|
|
233
236
|
```jsonc
|
|
234
237
|
{
|
|
235
238
|
// full surface registered once — global
|
|
236
|
-
"
|
|
239
|
+
"plugins": [{ "package": "@vymalo/opencode-browser", "options": { "groups": ["page", "control", "debug"] } }],
|
|
237
240
|
"agent": {
|
|
238
241
|
"researcher": {
|
|
239
242
|
// FLAT STRING form → denied tools are dropped from this agent's injected
|
|
@@ -286,7 +289,7 @@ content script. Force one with `"cdp"` / `"content"`.
|
|
|
286
289
|
Control is **plugin-managed** — you never disconnect by hand. It's released when:
|
|
287
290
|
|
|
288
291
|
- the model/plugin calls **`browser_release`** (mid-session), or
|
|
289
|
-
- **OpenCode exits** — the plugin's `exit`
|
|
292
|
+
- **OpenCode exits** — the plugin's scope closes (with a process `exit` safety net) and shuts the bridge down, and the dropped socket
|
|
290
293
|
independently makes the extension release. A hard kill releases too.
|
|
291
294
|
|
|
292
295
|
## Security
|
|
@@ -312,3 +315,20 @@ Full architecture, wire protocol, and tool reference: [`docs/browser.md`](https:
|
|
|
312
315
|
## License
|
|
313
316
|
|
|
314
317
|
[MIT](https://github.com/ADORSYS-GIS/lightbridge-opencode-toolbeit/blob/main/LICENSE) © vymalo contributors
|
|
318
|
+
|
|
319
|
+
## Migrating from v1
|
|
320
|
+
|
|
321
|
+
`0.18.0` targets the OpenCode **v2** plugin API; `0.17.x` was the last v1 release. See
|
|
322
|
+
[ADR-0019](../../docs/adr/0019-opencode-v2-cutover.md) for why there is no dual-support build.
|
|
323
|
+
|
|
324
|
+
| v1 | v2 |
|
|
325
|
+
| --- | --- |
|
|
326
|
+
| `"plugin": [["@vymalo/opencode-browser", { ... }]]` | `"plugins": [{ "package": "@vymalo/opencode-browser", "options": { ... } }]` |
|
|
327
|
+
| host `config.logLevel` drove the log level | the plugin's own `options.logLevel`, else `OPENCODE_LOG_LEVEL` |
|
|
328
|
+
| diagnostics via `client.app.log` | the host's Effect logger (same events, `key=value` fields; still never the terminal) |
|
|
329
|
+
| `screenshotDir` resolved against `ctx.worktree` | resolved against the plugin's `location.project.directory` |
|
|
330
|
+
| `dispose` / `process.on("exit")` | the bridge is owned by the plugin scope and shut down when it closes |
|
|
331
|
+
|
|
332
|
+
Every option (`enabled`, `host`, `port`, `token`, `executor`, `groups`, `timeoutMs`,
|
|
333
|
+
`screenshotDir`), every `browser_*` tool name and argument, the wire protocol and the extension
|
|
334
|
+
are unchanged. Per-agent `permission` rules on `browser_*` names keep working as before.
|
package/dist/agent-client.d.ts
CHANGED
|
@@ -17,6 +17,8 @@ export interface AgentClientOptions {
|
|
|
17
17
|
token: string;
|
|
18
18
|
label?: string;
|
|
19
19
|
timeoutMs: number;
|
|
20
|
+
/** Ceiling for the guest handshake; `connect()` rejects after it. Default 5000. */
|
|
21
|
+
connectTimeoutMs?: number;
|
|
20
22
|
}
|
|
21
23
|
export interface AgentClientDeps {
|
|
22
24
|
logger: Logger;
|
package/dist/agent-client.js
CHANGED
|
@@ -7,6 +7,13 @@ import { decodeFrame, encodeFrame, helloFrame, nextId, PROTOCOL_VERSION } from "
|
|
|
7
7
|
* before — otherwise the guest rejects locally while the executor keeps running.
|
|
8
8
|
*/
|
|
9
9
|
const AGENT_TIMEOUT_GRACE_MS = 5e3;
|
|
10
|
+
/**
|
|
11
|
+
* Default ceiling for the guest handshake (socket open -> broker `ready`).
|
|
12
|
+
* A broker port can stay bound by a dead process (on Windows a listen socket is
|
|
13
|
+
* inherited by child processes), so the TCP/WebSocket open succeeds but no
|
|
14
|
+
* `ready` frame ever arrives. Without a ceiling `connect()` never settles.
|
|
15
|
+
*/
|
|
16
|
+
const DEFAULT_CONNECT_TIMEOUT_MS = 5e3;
|
|
10
17
|
export class AgentClientError extends Error {
|
|
11
18
|
code;
|
|
12
19
|
constructor(message, code) {
|
|
@@ -35,46 +42,78 @@ export class AgentClient {
|
|
|
35
42
|
connect() {
|
|
36
43
|
return new Promise((resolve, reject) => {
|
|
37
44
|
let settled = false;
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
45
|
+
const connectTimeoutMs = this.opts.connectTimeoutMs ?? DEFAULT_CONNECT_TIMEOUT_MS;
|
|
46
|
+
const connectTimer = setTimeout(() => {
|
|
47
|
+
if (settled) {
|
|
48
|
+
return;
|
|
49
|
+
}
|
|
50
|
+
settled = true;
|
|
51
|
+
this.deps.logger.debug("browser_guest_connect_timeout", {
|
|
52
|
+
url: this.opts.url,
|
|
53
|
+
timeoutMs: connectTimeoutMs
|
|
54
|
+
});
|
|
55
|
+
// Abandon this attempt: `ws.close()` against a non-responsive peer can take
|
|
56
|
+
// ~30s to actually fire `onClose`. Marking the client closed makes that late
|
|
57
|
+
// close inert (it must not trigger the endpoint's re-election and wipe a
|
|
58
|
+
// later, successful election). Same flag `stop()` uses.
|
|
59
|
+
this.closed = true;
|
|
60
|
+
const socket = this.socket;
|
|
61
|
+
this.socket = null;
|
|
62
|
+
try {
|
|
63
|
+
socket?.close();
|
|
64
|
+
} catch {}
|
|
65
|
+
reject(new AgentClientError("bridge connect timed out", "connect_failed"));
|
|
66
|
+
}, connectTimeoutMs);
|
|
67
|
+
// A pending handshake must not keep the process alive on its own.
|
|
68
|
+
connectTimer.unref?.();
|
|
69
|
+
try {
|
|
70
|
+
this.socket = this.deps.createSocket(this.opts.url, {
|
|
71
|
+
onOpen: () => {
|
|
72
|
+
this.socket?.send(encodeFrame(helloFrame(this.opts.token, {
|
|
73
|
+
role: "agent",
|
|
74
|
+
client: this.opts.label
|
|
75
|
+
})));
|
|
76
|
+
// Resolve only once the broker replies `ready`.
|
|
77
|
+
},
|
|
78
|
+
onMessage: (data) => {
|
|
79
|
+
const frame = decodeFrame(data);
|
|
80
|
+
if (!frame) {
|
|
81
|
+
return;
|
|
82
|
+
}
|
|
83
|
+
if (frame.type === "ready") {
|
|
84
|
+
this.ready = true;
|
|
85
|
+
this.deps.logger.trace("browser_guest_ready", { label: this.opts.label });
|
|
86
|
+
for (const w of this.readyWaiters.splice(0)) {
|
|
87
|
+
w();
|
|
88
|
+
}
|
|
89
|
+
if (!settled) {
|
|
90
|
+
settled = true;
|
|
91
|
+
clearTimeout(connectTimer);
|
|
92
|
+
resolve();
|
|
93
|
+
}
|
|
94
|
+
} else if (frame.type === "result") {
|
|
95
|
+
this.handleResult(frame);
|
|
56
96
|
}
|
|
97
|
+
},
|
|
98
|
+
onClose: () => {
|
|
99
|
+
this.ready = false;
|
|
100
|
+
this.rejectAllPending(new AgentClientError("disconnected from bridge", "disconnected"));
|
|
101
|
+
this.socket = null;
|
|
57
102
|
if (!settled) {
|
|
58
103
|
settled = true;
|
|
59
|
-
|
|
104
|
+
clearTimeout(connectTimer);
|
|
105
|
+
reject(new AgentClientError("bridge connection closed before ready", "connect_failed"));
|
|
106
|
+
}
|
|
107
|
+
if (!this.closed) {
|
|
108
|
+
this.deps.onClose?.();
|
|
60
109
|
}
|
|
61
|
-
} else if (frame.type === "result") {
|
|
62
|
-
this.handleResult(frame);
|
|
63
|
-
}
|
|
64
|
-
},
|
|
65
|
-
onClose: () => {
|
|
66
|
-
this.ready = false;
|
|
67
|
-
this.rejectAllPending(new AgentClientError("disconnected from bridge", "disconnected"));
|
|
68
|
-
this.socket = null;
|
|
69
|
-
if (!settled) {
|
|
70
|
-
settled = true;
|
|
71
|
-
reject(new AgentClientError("bridge connection closed before ready", "connect_failed"));
|
|
72
|
-
}
|
|
73
|
-
if (!this.closed) {
|
|
74
|
-
this.deps.onClose?.();
|
|
75
110
|
}
|
|
76
|
-
}
|
|
77
|
-
})
|
|
111
|
+
});
|
|
112
|
+
} catch (err) {
|
|
113
|
+
settled = true;
|
|
114
|
+
clearTimeout(connectTimer);
|
|
115
|
+
reject(err);
|
|
116
|
+
}
|
|
78
117
|
});
|
|
79
118
|
}
|
|
80
119
|
stop() {
|
package/dist/agent-client.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"mappings":"AAEA,SAGE,aACA,aACA,YACA,QACA,wBACK;;;;;;;;
|
|
1
|
+
{"mappings":"AAEA,SAGE,aACA,aACA,YACA,QACA,wBACK;;;;;;;;AA8BP,MAAM,yBAAyB;;;;;;;AAQ/B,MAAM,6BAA6B;AAgBnC,OAAO,MAAM,yBAAyB,MAAM;CAC1C,AAAS;CACT,YAAY,SAAiB,MAAe;EAC1C,MAAM,OAAO;EACb,KAAK,OAAO;EACZ,KAAK,OAAO;CACd;AACF;;;;;;AAOA,OAAO,MAAM,YAAqC;CAQ7B;CACA;CARnB,AAAQ,SAA6B;CACrC,AAAQ,QAAQ;CAChB,AAAQ,SAAS;CACjB,AAAiB,UAAU,IAAI,IAAqB;CACpD,AAAQ,eAAkC,CAAC;CAE3C,YACE,AAAiB,MACjB,AAAiB,MACjB;EAFiB;EACA;CAChB;CAEH,UAAyB;EACvB,OAAO,IAAI,SAAe,SAAS,WAAW;GAC5C,IAAI,UAAU;GACd,MAAM,mBAAmB,KAAK,KAAK,oBAAoB;GACvD,MAAM,eAAe,iBAAiB;IACpC,IAAI,SAAS;KACX;IACF;IACA,UAAU;IACV,KAAK,KAAK,OAAO,MAAM,iCAAiC;KACtD,KAAK,KAAK,KAAK;KACf,WAAW;IACb,CAAC;;;;;IAKD,KAAK,SAAS;IACd,MAAM,SAAS,KAAK;IACpB,KAAK,SAAS;IACd,IAAI;KACF,QAAQ,MAAM;IAChB,QAAQ,CAER;IACA,OAAO,IAAI,iBAAiB,4BAA4B,gBAAgB,CAAC;GAC3E,GAAG,gBAAgB;;GAEnB,aAAa,QAAQ;GACrB,IAAI;IACF,KAAK,SAAS,KAAK,KAAK,aAAa,KAAK,KAAK,KAAK;KAClD,cAAc;MACZ,KAAK,QAAQ,KACX,YAAY,WAAW,KAAK,KAAK,OAAO;OAAE,MAAM;OAAS,QAAQ,KAAK,KAAK;MAAM,CAAC,CAAC,CACrF;;KAEF;KACA,YAAY,SAAS;MACnB,MAAM,QAAQ,YAAY,IAAI;MAC9B,IAAI,CAAC,OAAO;OACV;MACF;MACA,IAAI,MAAM,SAAS,SAAS;OAC1B,KAAK,QAAQ;OACb,KAAK,KAAK,OAAO,MAAM,uBAAuB,EAAE,OAAO,KAAK,KAAK,MAAM,CAAC;OACxE,KAAK,MAAM,KAAK,KAAK,aAAa,OAAO,CAAC,GAAG;QAC3C,EAAE;OACJ;OACA,IAAI,CAAC,SAAS;QACZ,UAAU;QACV,aAAa,YAAY;QACzB,QAAQ;OACV;MACF,OAAO,IAAI,MAAM,SAAS,UAAU;OAClC,KAAK,aAAa,KAAK;MACzB;KACF;KACA,eAAe;MACb,KAAK,QAAQ;MACb,KAAK,iBAAiB,IAAI,iBAAiB,4BAA4B,cAAc,CAAC;MACtF,KAAK,SAAS;MACd,IAAI,CAAC,SAAS;OACZ,UAAU;OACV,aAAa,YAAY;OACzB,OACE,IAAI,iBAAiB,yCAAyC,gBAAgB,CAChF;MACF;MACA,IAAI,CAAC,KAAK,QAAQ;OAChB,KAAK,KAAK,UAAU;MACtB;KACF;IACF,CAAC;GACH,SAAS,KAAK;IACZ,UAAU;IACV,aAAa,YAAY;IACzB,OAAO,GAAG;GACZ;EACF,CAAC;CACH;CAEA,OAAa;EACX,KAAK,SAAS;EACd,KAAK,QAAQ,MAAM;EACnB,KAAK,SAAS;EACd,KAAK,iBAAiB,IAAI,iBAAiB,iBAAiB,SAAS,CAAC;CACxE;CAEA,UAAgB;;EAEd,KAAK,KAAK,KAAK,WAAW,IAAI,CAAC,CAAC,CAAC,CAAC,YAAY,CAAC,CAAC;CAClD;CAEA,MAAM,KACJ,QACA,OACA,QACA,QACA,QACA,WACkB;EAClB,IAAI,QAAQ,SAAS;GACnB,MAAM,IAAI,iBAAiB,WAAW,SAAS;EACjD;EACA,MAAM,KAAK,UAAU,MAAM;EAC3B,MAAM,SAAS,KAAK;EACpB,IAAI,CAAC,QAAQ;GACX,MAAM,IAAI,iBAAiB,+BAA+B,cAAc;EAC1E;EACA,MAAM,KAAK,OAAO;EAClB,MAAM,QAAsB;GAC1B,GAAG;GACH,MAAM;GACN;GACA;GACA;GACA;GACA;;GAEA;EACF;EACA,KAAK,KAAK,OAAO,MAAM,8BAA8B;GAAE;GAAI;GAAQ;GAAO;EAAO,CAAC;;;EAGlF,MAAM,YAAY,cAAc,YAAY,YAAY,KAAK,KAAK;EAClE,MAAM,eAAe,YAAY,IAAI,YAAY,yBAAyB;EAC1E,OAAO,IAAI,SAAkB,SAAS,WAAW;GAC/C,MAAM,QACJ,eAAe,IACX,iBAEI,KAAK,aACH,IACA,IAAI,iBAAiB,YAAY,OAAO,cAAc,SAAS,CACjE,GACF,YACF,IACA;GACN,IAAI,cAAmC;GACvC,IAAI,QAAQ;IACV,MAAM,gBAAgB,KAAK,aAAa,IAAI,IAAI,iBAAiB,WAAW,SAAS,CAAC;IACtF,OAAO,iBAAiB,SAAS,SAAS,EAAE,MAAM,KAAK,CAAC;IACxD,oBAAoB,OAAO,oBAAoB,SAAS,OAAO;GACjE;GACA,KAAK,QAAQ,IAAI,IAAI;IAAE;IAAS;IAAQ;IAAO;GAAY,CAAC;GAC5D,IAAI;IACF,OAAO,KAAK,YAAY,KAAK,CAAC;GAChC,SAAS,KAAK;IACZ,KAAK,aACH,IACA,IAAI,iBACF,mBAAmB,OAAO,KAAK,eAAe,QAAQ,IAAI,UAAU,OAAO,GAAG,KAC9E,aACF,CACF;GACF;EACF,CAAC;CACH;CAEA,AAAQ,UAAU,QAAqC;EACrD,IAAI,KAAK,OAAO;GACd,OAAO,QAAQ,QAAQ;EACzB;EACA,IAAI,KAAK,UAAU,CAAC,KAAK,QAAQ;GAC/B,OAAO,QAAQ,OAAO,IAAI,iBAAiB,+BAA+B,cAAc,CAAC;EAC3F;EACA,OAAO,IAAI,SAAe,SAAS,WAAW;GAC5C,MAAM,gBAAgB,OAAO,IAAI,iBAAiB,WAAW,SAAS,CAAC;GACvE,IAAI,QAAQ;IACV,OAAO,iBAAiB,SAAS,SAAS,EAAE,MAAM,KAAK,CAAC;GAC1D;GACA,KAAK,aAAa,WAAW;IAC3B,IAAI,QAAQ;KACV,OAAO,oBAAoB,SAAS,OAAO;IAC7C;IACA,QAAQ;GACV,CAAC;EACH,CAAC;CACH;CAEA,AAAQ,aAAa,OAKZ;EACP,MAAM,IAAI,KAAK,QAAQ,IAAI,MAAM,EAAE;EACnC,IAAI,CAAC,GAAG;GACN;EACF;EACA,KAAK,KAAK,OAAO,MAAM,iCAAiC;GAAE,IAAI,MAAM;GAAI,IAAI,MAAM;EAAG,CAAC;EACtF,KAAK,aAAa,MAAM,EAAE;EAC1B,IAAI,MAAM,IAAI;GACZ,EAAE,QAAQ,MAAM,IAAI;EACtB,OAAO;GACL,EAAE,OAAO,IAAI,iBAAiB,MAAM,OAAO,WAAW,gBAAgB,MAAM,OAAO,IAAI,CAAC;EAC1F;CACF;CAEA,AAAQ,aAAa,IAAkB;EACrC,MAAM,IAAI,KAAK,QAAQ,IAAI,EAAE;EAC7B,IAAI,CAAC,GAAG;GACN;EACF;EACA,IAAI,EAAE,OAAO;GACX,aAAa,EAAE,KAAK;EACtB;EACA,EAAE,cAAc;EAChB,KAAK,QAAQ,OAAO,EAAE;CACxB;CAEA,AAAQ,aAAa,IAAY,KAAkB;EACjD,MAAM,IAAI,KAAK,QAAQ,IAAI,EAAE;EAC7B,IAAI,CAAC,GAAG;GACN;EACF;EACA,KAAK,aAAa,EAAE;EACpB,EAAE,OAAO,GAAG;CACd;CAEA,AAAQ,iBAAiB,KAAkB;EACzC,KAAK,MAAM,CAAC,IAAI,MAAM,KAAK,SAAS;GAClC,IAAI,EAAE,OAAO;IACX,aAAa,EAAE,KAAK;GACtB;GACA,EAAE,cAAc;GAChB,KAAK,QAAQ,OAAO,EAAE;GACtB,EAAE,OAAO,GAAG;EACd;CACF;AACF","names":[],"sources":["../src/agent-client.ts"],"version":3,"file":"agent-client.js","sourceRoot":""}
|
package/dist/endpoint.d.ts
CHANGED
|
@@ -16,6 +16,8 @@ export interface EndpointOptions {
|
|
|
16
16
|
label?: string;
|
|
17
17
|
/** Delay before retrying election after a drop. */
|
|
18
18
|
reelectMs?: number;
|
|
19
|
+
/** Ceiling for the guest handshake (open -> `ready`); default 5000. Forwarded to `AgentClient`. */
|
|
20
|
+
connectTimeoutMs?: number;
|
|
19
21
|
/**
|
|
20
22
|
* Fired whenever this endpoint becomes the host — on the initial election AND
|
|
21
23
|
* on any later failover re-election. Use it for host-only side effects like
|
package/dist/endpoint.js
CHANGED
|
@@ -92,7 +92,8 @@ export async function createEndpoint(opts, deps) {
|
|
|
92
92
|
url,
|
|
93
93
|
token: opts.token,
|
|
94
94
|
label: opts.label,
|
|
95
|
-
timeoutMs: opts.timeoutMs
|
|
95
|
+
timeoutMs: opts.timeoutMs,
|
|
96
|
+
connectTimeoutMs: opts.connectTimeoutMs
|
|
96
97
|
}, {
|
|
97
98
|
logger: deps.logger,
|
|
98
99
|
createSocket: deps.createAgentSocket,
|
package/dist/endpoint.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"mappings":"AAAA,SAAkC,mBAAmB;AACrD,SAA6B,cAAc;AAG3C,SAA+B,mBAAmB;;;;;;;
|
|
1
|
+
{"mappings":"AAAA,SAAkC,mBAAmB;AACrD,SAA6B,cAAc;AAG3C,SAA+B,mBAAmB;;;;;;;AAiElD,OAAO,eAAe,eAAe,MAAuB,MAAuC;CACjG,MAAM,MAAM,KAAK,OAAO,QAAQ,KAAK,KAAK,GAAG,KAAK;CAClD,MAAM,YAAY,KAAK,aAAa;CAEpC,IAAI,OAAqB;CACzB,IAAI,SAAwB;CAC5B,IAAI,cAAkC;CACtC,IAAI,UAAgC;CACpC,IAAI,SAAS;CACb,IAAI,eAAqD;CAEzD,MAAM,wBAA8B;EAClC,SAAS;EACT,cAAc;EACd,UAAU;EACV,OAAO;EACP,IAAI,UAAU,cAAc;GAC1B;EACF;EACA,eAAe,iBAAiB;GAC9B,eAAe;GACf,KAAK,MAAM;EACb,GAAG,SAAS;CACd;CAEA,eAAe,QAAuB;EACpC,IAAI,QAAQ;GACV;EACF;EACA,OAAO;EACP,KAAK,OAAO,MAAM,0BAA0B;GAAE,MAAM,KAAK;GAAM,MAAM,KAAK;EAAK,CAAC;;;;EAKhF,IAAI,YAA2B;EAC/B,IAAI;GACF,KAAK,OAAO,MAAM,wBAAwB;IAAE,MAAM,KAAK;IAAM,MAAM,KAAK;GAAK,CAAC;GAC9E,MAAM,YAAY,KAAK,sBAAsB;GAC7C,YAAY,IAAI,OACd;IACE,MAAM,KAAK;IACX,MAAM,KAAK;IACX,OAAO,KAAK;IACZ,UAAU,KAAK;IACf,WAAW,KAAK;IAChB,cAAc,KAAK;GACrB,GACA;IAAE,QAAQ,KAAK;IAAQ;IAAW,aAAa,KAAK;GAAY,CAClE;GACA,MAAM,UAAU,MAAM;GACtB,SAAS;GACT,UAAU,UAAU,iBAAiB;GACrC,OAAO;GACP,KAAK,OAAO,MAAM,oBAAoB;IAAE,MAAM,KAAK;IAAM,MAAM,KAAK;GAAK,CAAC;GAC1E,KAAK,OAAO,KAAK,yBAAyB,EAAE,MAAM,OAAO,CAAC;;;GAG1D,IAAI;IACF,KAAK,SAAS;GAChB,SAAS,WAAW;IAClB,KAAK,OAAO,MAAM,iCAAiC,EACjD,SAAS,qBAAqB,QAAQ,UAAU,UAAU,OAAO,SAAS,EAC5E,CAAC;GACH;GACA;EACF,SAAS,KAAK;GACZ,IAAI;IACF,WAAW,KAAK;GAClB,QAAQ,CAER;GACA,KAAK,OAAO,MAAM,qBAAqB,EAAE,WAAW,YAAY,GAAG,EAAE,CAAC;GACtE,IAAI,CAAC,YAAY,GAAG,GAAG;IACrB,KAAK,OAAO,KAAK,+BAA+B,EAC9C,SAAS,eAAe,QAAQ,IAAI,UAAU,OAAO,GAAG,EAC1D,CAAC;GACH;EACF;;EAGA,KAAK,OAAO,MAAM,iCAAiC,EAAE,IAAI,CAAC;EAC1D,MAAM,SAAS,IAAI,YACjB;GACE;GACA,OAAO,KAAK;GACZ,OAAO,KAAK;GACZ,WAAW,KAAK;GAChB,kBAAkB,KAAK;EACzB,GACA;GACE,QAAQ,KAAK;GACb,cAAc,KAAK;GACnB,eAAe;IACb,IAAI,CAAC,QAAQ;KACX,gBAAgB;IAClB;GACF;EACF,CACF;EACA,IAAI;GACF,MAAM,OAAO,QAAQ;GACrB,cAAc;GACd,UAAU;GACV,OAAO;;;GAGP,KAAK,OAAO,MAAM,yBAAyB,EAAE,MAAM,QAAQ,CAAC;EAC9D,QAAQ;GACN,gBAAgB;EAClB;CACF;CAEA,MAAM,MAAM;CAEZ,OAAO;EACL,OAAO,QAAQ,OAAO,QAAQ,QAAQ,QAAQ,cAC5C,UACI,QAAQ,KAAK,QAAQ,OAAO,QAAQ,QAAQ,QAAQ,SAAS,IAC7D,QAAQ,OAAO,IAAI,MAAM,uCAAuC,CAAC;EACvE,eAAe,SAAS,QAAQ;EAChC,gBAAgB;GACd,SAAS;GACT,IAAI,cAAc;IAChB,aAAa,YAAY;IACzB,eAAe;GACjB;GACA,IAAI;IACF,QAAQ,KAAK;GACf,QAAQ,CAER;GACA,IAAI;IACF,aAAa,KAAK;GACpB,QAAQ,CAER;EACF;EACA,YAAY;EACZ,cAAc;CAChB;AACF","names":[],"sources":["../src/endpoint.ts"],"version":3,"file":"endpoint.js","sourceRoot":""}
|
package/dist/lib.d.ts
CHANGED
|
@@ -1,13 +1,13 @@
|
|
|
1
|
-
export { type BrowserPluginFactoryOptions, createBrowserPlugin, OpencodeBrowserPlugin } from "./opencode.js";
|
|
1
|
+
export { type BrowserPluginFactoryOptions, createBrowserPlugin, OpencodeBrowserPlugin, PLUGIN_ID } from "./opencode.js";
|
|
2
2
|
export { type AgentEndpoint, Broker, BrokerError, type BrokerDeps, type BrokerOptions } from "./broker.js";
|
|
3
3
|
export { AgentClient, AgentClientError, type AgentClientDeps, type AgentClientOptions, type AgentSocket, type AgentSocketFactory, type AgentSocketHandlers } from "./agent-client.js";
|
|
4
4
|
export { createEndpoint, type Endpoint, type EndpointDeps, type EndpointMode, type EndpointOptions } from "./endpoint.js";
|
|
5
5
|
export { type BridgeTransport, type ClientConnection, isAddrInUse, type TransportHandlers } from "./transport.js";
|
|
6
6
|
export { createNodeAgentSocket, createNodeTransport } from "./node-transport.js";
|
|
7
7
|
export { type BridgeFile, readBridgeFile, resolveSharedToken, type TokenSource, writeBridgeFile } from "./token-file.js";
|
|
8
|
-
export { createBrowserTools, type SaveScreenshot, type SendFn, type ToolDeps } from "./tools.js";
|
|
8
|
+
export { type BrowserToolInfo, createBrowserTools, type SaveScreenshot, type SendFn, type ToolDeps } from "./tools.js";
|
|
9
9
|
export { type Annotation, BROWSER_TOOLS, DEFAULT_GROUPS, type FeedbackResult, type NeutralResult, TOOL_GROUPS, type ToolGroup, type ToolSpec } from "./catalog.js";
|
|
10
10
|
export { type Field, type JsonInput, type JsonSchema, toJsonSchema } from "./schema.js";
|
|
11
|
-
export { createJsonConsoleLogger, DEFAULT_LOG_LEVEL,
|
|
11
|
+
export { createJsonConsoleLogger, DEFAULT_LOG_LEVEL, resolveLogLevel, type LogFields, type Logger, type LogLevel } from "./logging.js";
|
|
12
12
|
export { BROWSER_ACTIONS, type BrowserAction, type BrowserEventName, type ClientRole, type CommandFrame, decodeFrame, encodeFrame, type EventFrame, type Frame, type HelloFrame, helloFrame, nextId, PROTOCOL_VERSION, type ReadyFrame, type ResultFrame } from "./protocol.js";
|
|
13
13
|
export type { BrowserPluginOptions, ExecutorMode, ResolvedBrowserOptions, ScreenshotResult } from "./types.js";
|
package/dist/lib.js
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
export { createBrowserPlugin, OpencodeBrowserPlugin } from "./opencode.js";
|
|
1
|
+
export { createBrowserPlugin, OpencodeBrowserPlugin, PLUGIN_ID } from "./opencode.js";
|
|
2
2
|
export { Broker, BrokerError } from "./broker.js";
|
|
3
3
|
export { AgentClient, AgentClientError } from "./agent-client.js";
|
|
4
4
|
export { createEndpoint } from "./endpoint.js";
|
|
@@ -8,7 +8,7 @@ export { readBridgeFile, resolveSharedToken, writeBridgeFile } from "./token-fil
|
|
|
8
8
|
export { createBrowserTools } from "./tools.js";
|
|
9
9
|
export { BROWSER_TOOLS, DEFAULT_GROUPS, TOOL_GROUPS } from "./catalog.js";
|
|
10
10
|
export { toJsonSchema } from "./schema.js";
|
|
11
|
-
export { createJsonConsoleLogger, DEFAULT_LOG_LEVEL,
|
|
11
|
+
export { createJsonConsoleLogger, DEFAULT_LOG_LEVEL, resolveLogLevel } from "./logging.js";
|
|
12
12
|
export { BROWSER_ACTIONS, decodeFrame, encodeFrame, helloFrame, nextId, PROTOCOL_VERSION } from "./protocol.js";
|
|
13
13
|
|
|
14
14
|
//# sourceMappingURL=lib.js.map
|
package/dist/lib.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"mappings":"AAAA,SAEE,qBACA,
|
|
1
|
+
{"mappings":"AAAA,SAEE,qBACA,uBACA,iBACK;AAEP,SAEE,QACA,mBAGK;AAEP,SACE,aACA,wBAMK;AAEP,SACE,sBAKK;AAEP,SAGE,mBAEK;AAEP,SAAS,uBAAuB,2BAA2B;AAE3D,SAEE,gBACA,oBAEA,uBACK;AAEP,SAEE,0BAIK;AAEP,SAEE,eACA,gBAGA,mBAGK;AAEP,SAAsD,oBAAoB;AAE1E,SACE,yBACA,mBACA,uBAIK;AAEP,SACE,iBAKA,aACA,aAIA,YACA,QACA,wBAGK","names":[],"sources":["../src/lib.ts"],"version":3,"file":"lib.js","sourceRoot":""}
|
package/dist/logging.d.ts
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { type Context as EffectContext, Effect } from "effect";
|
|
1
2
|
import type { LogLevel } from "./types.js";
|
|
2
3
|
export type { LogLevel } from "./types.js";
|
|
3
4
|
export declare const LOG_LEVEL_PRIORITY: Record<LogLevel, number>;
|
|
@@ -13,4 +14,37 @@ export interface Logger {
|
|
|
13
14
|
error(event: string, fields?: LogFields): void;
|
|
14
15
|
}
|
|
15
16
|
export declare function createJsonConsoleLogger(minLevel?: LogLevel): Logger;
|
|
16
|
-
|
|
17
|
+
/**
|
|
18
|
+
* Capture the host's Effect context from inside the plugin's own fiber. The
|
|
19
|
+
* returned value is what {@link createEffectLogger} needs to put a log record
|
|
20
|
+
* on the host's logger rather than on a default one.
|
|
21
|
+
*
|
|
22
|
+
* OpenCode installs its file logger as a `Logger.layer(...)` on the runtime
|
|
23
|
+
* that runs plugin setup, and Effect carries loggers as fiber-level references
|
|
24
|
+
* — so the context captured here is the only route to that sink. A bare
|
|
25
|
+
* `Effect.runFork` would log to Effect's default logger (the terminal) instead,
|
|
26
|
+
* which is exactly what ADR-0014 forbids.
|
|
27
|
+
*/
|
|
28
|
+
export declare const captureHostContext: Effect.Effect<EffectContext.Context<never>>;
|
|
29
|
+
/**
|
|
30
|
+
* Structured logger that writes through the host's Effect logger — the v2
|
|
31
|
+
* replacement for v1's `client.app.log`.
|
|
32
|
+
*
|
|
33
|
+
* ADR-0014 (no plugin writes to the user's TUI) is preserved: OpenCode's own
|
|
34
|
+
* logger is a `Logger.toFile` sink over the shared `opencode-<channel>.log`,
|
|
35
|
+
* so records land in the host log stream and never on the terminal. The event
|
|
36
|
+
* name becomes the log message and `fields` become log annotations, which
|
|
37
|
+
* OpenCode's logfmt formatter flattens to `key=value` — the same shape the v1
|
|
38
|
+
* `extra` payload produced.
|
|
39
|
+
*
|
|
40
|
+
* Host-side severity filtering is Effect's job (`OPENCODE_LOG_LEVEL`); the
|
|
41
|
+
* `getMinLevel` gate here is only this plugin's own `logLevel` option, applied
|
|
42
|
+
* before the record is built so a filtered-out call costs nothing.
|
|
43
|
+
*/
|
|
44
|
+
export declare function createEffectLogger(context: EffectContext.Context<never>, getMinLevel: () => LogLevel): Logger;
|
|
45
|
+
/**
|
|
46
|
+
* Resolve this plugin's log level from its own `logLevel` option, falling back
|
|
47
|
+
* to the host's `OPENCODE_LOG_LEVEL` env var. v2 gives plugins no `config`
|
|
48
|
+
* object, so the v1 `config.logLevel` channel is gone.
|
|
49
|
+
*/
|
|
50
|
+
export declare function resolveLogLevel(value: unknown): LogLevel | undefined;
|
package/dist/logging.js
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { Effect } from "effect";
|
|
1
2
|
export const LOG_LEVEL_PRIORITY = {
|
|
2
3
|
trace: 5,
|
|
3
4
|
debug: 10,
|
|
@@ -51,15 +52,73 @@ export function createJsonConsoleLogger(minLevel = DEFAULT_LOG_LEVEL) {
|
|
|
51
52
|
error: (event, fields) => write("error", event, fields)
|
|
52
53
|
};
|
|
53
54
|
}
|
|
54
|
-
|
|
55
|
+
/**
|
|
56
|
+
* Capture the host's Effect context from inside the plugin's own fiber. The
|
|
57
|
+
* returned value is what {@link createEffectLogger} needs to put a log record
|
|
58
|
+
* on the host's logger rather than on a default one.
|
|
59
|
+
*
|
|
60
|
+
* OpenCode installs its file logger as a `Logger.layer(...)` on the runtime
|
|
61
|
+
* that runs plugin setup, and Effect carries loggers as fiber-level references
|
|
62
|
+
* — so the context captured here is the only route to that sink. A bare
|
|
63
|
+
* `Effect.runFork` would log to Effect's default logger (the terminal) instead,
|
|
64
|
+
* which is exactly what ADR-0014 forbids.
|
|
65
|
+
*/
|
|
66
|
+
export const captureHostContext = Effect.withFiber((fiber) => Effect.succeed(fiber.context));
|
|
67
|
+
/**
|
|
68
|
+
* Structured logger that writes through the host's Effect logger — the v2
|
|
69
|
+
* replacement for v1's `client.app.log`.
|
|
70
|
+
*
|
|
71
|
+
* ADR-0014 (no plugin writes to the user's TUI) is preserved: OpenCode's own
|
|
72
|
+
* logger is a `Logger.toFile` sink over the shared `opencode-<channel>.log`,
|
|
73
|
+
* so records land in the host log stream and never on the terminal. The event
|
|
74
|
+
* name becomes the log message and `fields` become log annotations, which
|
|
75
|
+
* OpenCode's logfmt formatter flattens to `key=value` — the same shape the v1
|
|
76
|
+
* `extra` payload produced.
|
|
77
|
+
*
|
|
78
|
+
* Host-side severity filtering is Effect's job (`OPENCODE_LOG_LEVEL`); the
|
|
79
|
+
* `getMinLevel` gate here is only this plugin's own `logLevel` option, applied
|
|
80
|
+
* before the record is built so a filtered-out call costs nothing.
|
|
81
|
+
*/
|
|
82
|
+
export function createEffectLogger(context, getMinLevel) {
|
|
83
|
+
// `VYMALO_PLUGIN_CONSOLE_LOG=1` mirrors to stdout for local debugging. Off by
|
|
84
|
+
// default precisely because stdout is the user's terminal (ADR-0014).
|
|
85
|
+
const consoleAll = /^(1|true|yes|on)$/i.test(process.env.VYMALO_PLUGIN_CONSOLE_LOG ?? "");
|
|
86
|
+
const fallback = createJsonConsoleLogger("trace");
|
|
87
|
+
const write = (level, event, fields) => {
|
|
88
|
+
if (LOG_LEVEL_PRIORITY[level] < LOG_LEVEL_PRIORITY[getMinLevel()]) {
|
|
89
|
+
return;
|
|
90
|
+
}
|
|
91
|
+
if (consoleAll) {
|
|
92
|
+
fallback[level](event, fields);
|
|
93
|
+
}
|
|
94
|
+
const redacted = redactFields(fields);
|
|
95
|
+
// Effect has no `trace` severity below `debug`; fold the most-verbose tier
|
|
96
|
+
// onto debug, as the v1 adapter did for the host log API.
|
|
97
|
+
const record = level === "trace" ? Effect.logDebug(event) : level === "debug" ? Effect.logDebug(event) : level === "info" ? Effect.logInfo(event) : level === "warn" ? Effect.logWarning(event) : Effect.logError(event);
|
|
98
|
+
const annotated = redacted ? Effect.annotateLogs(record, redacted) : record;
|
|
99
|
+
Effect.runFork(Effect.provideContext(annotated, context));
|
|
100
|
+
};
|
|
101
|
+
return {
|
|
102
|
+
trace: (event, fields) => write("trace", event, fields),
|
|
103
|
+
debug: (event, fields) => write("debug", event, fields),
|
|
104
|
+
info: (event, fields) => write("info", event, fields),
|
|
105
|
+
warn: (event, fields) => write("warn", event, fields),
|
|
106
|
+
error: (event, fields) => write("error", event, fields)
|
|
107
|
+
};
|
|
108
|
+
}
|
|
109
|
+
/**
|
|
110
|
+
* Resolve this plugin's log level from its own `logLevel` option, falling back
|
|
111
|
+
* to the host's `OPENCODE_LOG_LEVEL` env var. v2 gives plugins no `config`
|
|
112
|
+
* object, so the v1 `config.logLevel` channel is gone.
|
|
113
|
+
*/
|
|
114
|
+
export function resolveLogLevel(value) {
|
|
55
115
|
if (typeof value !== "string") {
|
|
56
116
|
return undefined;
|
|
57
117
|
}
|
|
58
118
|
switch (value.toUpperCase()) {
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
return "trace";
|
|
119
|
+
// DEBUG unlocks the most-verbose `trace` tier (host has no finer level).
|
|
120
|
+
case "DEBUG": return "trace";
|
|
121
|
+
case "TRACE": return "trace";
|
|
63
122
|
case "INFO": return "info";
|
|
64
123
|
case "WARN": return "warn";
|
|
65
124
|
case "ERROR": return "error";
|
package/dist/logging.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"mappings":"
|
|
1
|
+
{"mappings":"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","names":[],"sources":["../src/logging.ts"],"version":3,"file":"logging.js","sourceRoot":""}
|
package/dist/opencode.d.ts
CHANGED
|
@@ -1,10 +1,12 @@
|
|
|
1
|
-
import
|
|
1
|
+
import { Plugin } from "@opencode/plugin/effect";
|
|
2
2
|
import type { AgentSocketFactory } from "./agent-client.js";
|
|
3
3
|
import { type Logger } from "./logging.js";
|
|
4
4
|
import { type SaveScreenshot } from "./tools.js";
|
|
5
5
|
import type { BridgeTransport } from "./transport.js";
|
|
6
|
+
/** Stable plugin id. v2 requires one and keys storage/registrations by it. */
|
|
7
|
+
export declare const PLUGIN_ID = "vymalo.browser";
|
|
6
8
|
export interface BrowserPluginFactoryOptions {
|
|
7
|
-
/** Inject a logger (defaults to the
|
|
9
|
+
/** Inject a logger (defaults to the host's Effect logger). */
|
|
8
10
|
logger?: Logger;
|
|
9
11
|
/** Inject the server transport factory for host mode (defaults to the Node `ws` server). */
|
|
10
12
|
createServerTransport?: () => BridgeTransport;
|
|
@@ -15,6 +17,21 @@ export interface BrowserPluginFactoryOptions {
|
|
|
15
17
|
/** Inject token generation (tests). */
|
|
16
18
|
generateToken?: () => string;
|
|
17
19
|
}
|
|
18
|
-
|
|
19
|
-
|
|
20
|
+
/**
|
|
21
|
+
* Build the OpenCode v2 plugin.
|
|
22
|
+
*
|
|
23
|
+
* The plugin Effect does three things, in this order:
|
|
24
|
+
*
|
|
25
|
+
* 1. Captures the host's Effect context so diagnostics reach OpenCode's file
|
|
26
|
+
* logger instead of the terminal (ADR-0014 / ADR-0019) — this has to happen
|
|
27
|
+
* inside the plugin's own fiber.
|
|
28
|
+
* 2. Elects host-or-guest and brings the bridge up, owned by the plugin's
|
|
29
|
+
* **scope**: when the scope closes the finalizer shuts the endpoint down,
|
|
30
|
+
* which stops the broker's `ws` listener (host) or closes the guest socket.
|
|
31
|
+
* A listener that outlived its plugin would make the next process elect as
|
|
32
|
+
* guest of a zombie instead of taking over the port.
|
|
33
|
+
* 3. Registers the `browser_*` tools with `tool.transform`.
|
|
34
|
+
*/
|
|
35
|
+
export declare function createBrowserPlugin(factoryOptions?: BrowserPluginFactoryOptions): Plugin.Plugin;
|
|
36
|
+
export declare const OpencodeBrowserPlugin: Plugin.Plugin;
|
|
20
37
|
export default OpencodeBrowserPlugin;
|
package/dist/opencode.js
CHANGED
|
@@ -1,10 +1,14 @@
|
|
|
1
1
|
import { randomBytes } from "node:crypto";
|
|
2
|
+
import { Plugin } from "@opencode/plugin/effect";
|
|
3
|
+
import { Effect } from "effect";
|
|
2
4
|
import { DEFAULT_GROUPS, TOOL_GROUPS } from "./catalog.js";
|
|
3
5
|
import { createEndpoint } from "./endpoint.js";
|
|
4
|
-
import {
|
|
5
|
-
import { readBridgeFile, resolveSharedToken, writeBridgeFile } from "./token-file.js";
|
|
6
|
+
import { captureHostContext, createEffectLogger, DEFAULT_LOG_LEVEL, resolveLogLevel } from "./logging.js";
|
|
6
7
|
import { createNodeAgentSocket, createNodeTransport } from "./node-transport.js";
|
|
8
|
+
import { readBridgeFile, resolveSharedToken, writeBridgeFile } from "./token-file.js";
|
|
7
9
|
import { createBrowserTools } from "./tools.js";
|
|
10
|
+
/** Stable plugin id. v2 requires one and keys storage/registrations by it. */
|
|
11
|
+
export const PLUGIN_ID = "vymalo.browser";
|
|
8
12
|
function resolveGroups(raw) {
|
|
9
13
|
if (!Array.isArray(raw)) {
|
|
10
14
|
return [...DEFAULT_GROUPS];
|
|
@@ -12,7 +16,6 @@ function resolveGroups(raw) {
|
|
|
12
16
|
const valid = raw.filter((g) => TOOL_GROUPS.includes(g));
|
|
13
17
|
return valid.length > 0 ? valid : [...DEFAULT_GROUPS];
|
|
14
18
|
}
|
|
15
|
-
const PLUGIN_SERVICE_NAME = "opencode-browser-plugin";
|
|
16
19
|
const DEFAULTS = {
|
|
17
20
|
host: "127.0.0.1",
|
|
18
21
|
port: 4517,
|
|
@@ -20,46 +23,6 @@ const DEFAULTS = {
|
|
|
20
23
|
timeoutMs: 3e4,
|
|
21
24
|
screenshotDir: ".opencode/browser"
|
|
22
25
|
};
|
|
23
|
-
/**
|
|
24
|
-
* Pipe plugin logs through OpenCode's `client.app.log` so they show up in the
|
|
25
|
-
* host's structured log stream, with the JSON console as a reliable fallback.
|
|
26
|
-
* Mirrors the pattern used by `@vymalo/opencode-oauth2` / `-ratelimit`.
|
|
27
|
-
*/
|
|
28
|
-
function createOpenCodeLogger(client, getMinLevel) {
|
|
29
|
-
const fallback = createJsonConsoleLogger("debug");
|
|
30
|
-
// OpenCode already captures plugin logs via client.app.log (and filters them
|
|
31
|
-
// by its own log level). Mirroring every event to stdout on top of that is
|
|
32
|
-
// what floods the terminal (e.g. `opencode web` re-loads the plugin per
|
|
33
|
-
// session). So only mirror warn/error to the JSON console by default; set
|
|
34
|
-
// VYMALO_PLUGIN_CONSOLE_LOG=1 to restore full console output for debugging.
|
|
35
|
-
const consoleAll = /^(1|true|yes|on)$/i.test(process.env.VYMALO_PLUGIN_CONSOLE_LOG ?? "");
|
|
36
|
-
const write = (level, event, fields) => {
|
|
37
|
-
if (LOG_LEVEL_PRIORITY[level] < LOG_LEVEL_PRIORITY[getMinLevel()]) {
|
|
38
|
-
return;
|
|
39
|
-
}
|
|
40
|
-
if (consoleAll || level === "warn" || level === "error") {
|
|
41
|
-
fallback[level](event, fields);
|
|
42
|
-
}
|
|
43
|
-
// OpenCode's host log API has no `trace` tier — fold it into `debug` for the
|
|
44
|
-
// host call (our own level filter above already gates trace by DEBUG).
|
|
45
|
-
const hostLevel = level === "trace" ? "debug" : level;
|
|
46
|
-
void client.app.log({ body: {
|
|
47
|
-
service: PLUGIN_SERVICE_NAME,
|
|
48
|
-
level: hostLevel,
|
|
49
|
-
message: event,
|
|
50
|
-
extra: fields
|
|
51
|
-
} }).catch(() => {
|
|
52
|
-
/* best-effort */
|
|
53
|
-
});
|
|
54
|
-
};
|
|
55
|
-
return {
|
|
56
|
-
trace: (event, fields) => write("trace", event, fields),
|
|
57
|
-
debug: (event, fields) => write("debug", event, fields),
|
|
58
|
-
info: (event, fields) => write("info", event, fields),
|
|
59
|
-
warn: (event, fields) => write("warn", event, fields),
|
|
60
|
-
error: (event, fields) => write("error", event, fields)
|
|
61
|
-
};
|
|
62
|
-
}
|
|
63
26
|
function resolveOptions(raw, generateToken) {
|
|
64
27
|
const opts = raw ?? {};
|
|
65
28
|
const token = typeof opts.token === "string" && opts.token.length > 0 ? opts.token : generateToken();
|
|
@@ -74,90 +37,124 @@ function resolveOptions(raw, generateToken) {
|
|
|
74
37
|
screenshotDir: opts.screenshotDir ?? DEFAULTS.screenshotDir
|
|
75
38
|
};
|
|
76
39
|
}
|
|
40
|
+
/**
|
|
41
|
+
* Build the OpenCode v2 plugin.
|
|
42
|
+
*
|
|
43
|
+
* The plugin Effect does three things, in this order:
|
|
44
|
+
*
|
|
45
|
+
* 1. Captures the host's Effect context so diagnostics reach OpenCode's file
|
|
46
|
+
* logger instead of the terminal (ADR-0014 / ADR-0019) — this has to happen
|
|
47
|
+
* inside the plugin's own fiber.
|
|
48
|
+
* 2. Elects host-or-guest and brings the bridge up, owned by the plugin's
|
|
49
|
+
* **scope**: when the scope closes the finalizer shuts the endpoint down,
|
|
50
|
+
* which stops the broker's `ws` listener (host) or closes the guest socket.
|
|
51
|
+
* A listener that outlived its plugin would make the next process elect as
|
|
52
|
+
* guest of a zombie instead of taking over the port.
|
|
53
|
+
* 3. Registers the `browser_*` tools with `tool.transform`.
|
|
54
|
+
*/
|
|
77
55
|
export function createBrowserPlugin(factoryOptions = {}) {
|
|
78
|
-
return
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
const tokenProvided = typeof rawOptions?.token === "string" && rawOptions.token.length > 0;
|
|
89
|
-
// Only forward the executor preference when the operator set it explicitly;
|
|
90
|
-
// otherwise the extension keeps its own dashboard choice.
|
|
91
|
-
const executorProvided = typeof rawOptions?.executor === "string";
|
|
92
|
-
if (!options.enabled) {
|
|
93
|
-
logger.info("browser_plugin_disabled", {});
|
|
94
|
-
return { config: async (config) => {
|
|
95
|
-
currentLogLevel = fromOpenCodeLogLevel(config.logLevel) ?? DEFAULT_LOG_LEVEL;
|
|
96
|
-
} };
|
|
97
|
-
}
|
|
98
|
-
// Share the token across adapters via the per-user state file (explicit wins).
|
|
99
|
-
const { token, source } = resolveSharedToken(options.port, tokenProvided ? options.token : undefined, generateToken);
|
|
100
|
-
// Persist + advertise the token, **host-only** (fired via onHost so a host
|
|
101
|
-
// reached by failover re-election does it too). Writing only on the host —
|
|
102
|
-
// not unconditionally at load like before — means a guest that resolved a
|
|
103
|
-
// fresh token on a transient read miss can never clobber the live host's
|
|
104
|
-
// token in the shared file. We write whenever the file doesn't already match
|
|
105
|
-
// OUR (port, token): that keeps a port change in sync and lets an explicit
|
|
106
|
-
// token overwrite a stale generated one — but a no-op when the file already
|
|
107
|
-
// agrees, so a concurrent host doesn't thrash it. The `paste_into_extension`
|
|
108
|
-
// field name dodges the logger's redaction filter so the value stays copyable.
|
|
109
|
-
const advertiseToken = () => {
|
|
110
|
-
const existing = readBridgeFile();
|
|
111
|
-
if (!existing || existing.port !== options.port || existing.token !== token) {
|
|
112
|
-
writeBridgeFile(options.port, token);
|
|
113
|
-
}
|
|
114
|
-
if (source !== "explicit") {
|
|
115
|
-
logger.info("browser_bridge_token", {
|
|
116
|
-
paste_into_extension: token,
|
|
117
|
-
source,
|
|
118
|
-
mode: "host"
|
|
119
|
-
});
|
|
56
|
+
return Plugin.define({
|
|
57
|
+
id: PLUGIN_ID,
|
|
58
|
+
effect: Effect.fn(function* (ctx) {
|
|
59
|
+
// v2 has no `config` object, so v1's `config.logLevel` channel is gone:
|
|
60
|
+
// this plugin's own `logLevel` option first, then the host's env var.
|
|
61
|
+
const logLevel = resolveLogLevel(ctx.options.logLevel) ?? resolveLogLevel(process.env.OPENCODE_LOG_LEVEL) ?? DEFAULT_LOG_LEVEL;
|
|
62
|
+
let logger = factoryOptions.logger;
|
|
63
|
+
if (!logger) {
|
|
64
|
+
const hostContext = yield* captureHostContext;
|
|
65
|
+
logger = createEffectLogger(hostContext, () => logLevel);
|
|
120
66
|
}
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
token
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
//
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
}, {
|
|
137
|
-
logger,
|
|
138
|
-
createServerTransport: factoryOptions.createServerTransport ?? createNodeTransport,
|
|
139
|
-
createAgentSocket: factoryOptions.createAgentSocket ?? createNodeAgentSocket
|
|
140
|
-
});
|
|
141
|
-
// When OpenCode exits, hand the browser back automatically — the user
|
|
142
|
-
// shouldn't have to click Disconnect.
|
|
143
|
-
process.once("exit", () => {
|
|
144
|
-
try {
|
|
145
|
-
endpoint.shutdown();
|
|
146
|
-
} catch {}
|
|
147
|
-
});
|
|
148
|
-
const tools = createBrowserTools({
|
|
149
|
-
send: endpoint.send,
|
|
150
|
-
options,
|
|
151
|
-
logger,
|
|
152
|
-
saveScreenshot: factoryOptions.saveScreenshot
|
|
153
|
-
});
|
|
154
|
-
return {
|
|
155
|
-
tool: tools,
|
|
156
|
-
config: async (config) => {
|
|
157
|
-
currentLogLevel = fromOpenCodeLogLevel(config.logLevel) ?? DEFAULT_LOG_LEVEL;
|
|
67
|
+
const log = logger;
|
|
68
|
+
const generateToken = factoryOptions.generateToken ?? (() => randomBytes(24).toString("hex"));
|
|
69
|
+
const options = resolveOptions(ctx.options, generateToken);
|
|
70
|
+
const rawOptions = ctx.options;
|
|
71
|
+
// Empty string is NOT an explicit token (resolveOptions generates one for
|
|
72
|
+
// it). Treating "" as provided would mark the shared token "explicit" and
|
|
73
|
+
// skip the paste_into_extension log, leaving the extension unable to learn
|
|
74
|
+
// the generated token.
|
|
75
|
+
const tokenProvided = typeof rawOptions.token === "string" && rawOptions.token.length > 0;
|
|
76
|
+
// Only forward the executor preference when the operator set it explicitly;
|
|
77
|
+
// otherwise the extension keeps its own dashboard choice.
|
|
78
|
+
const executorProvided = typeof rawOptions.executor === "string";
|
|
79
|
+
if (!options.enabled) {
|
|
80
|
+
log.info("browser_plugin_disabled", {});
|
|
81
|
+
return;
|
|
158
82
|
}
|
|
159
|
-
|
|
160
|
-
|
|
83
|
+
// Share the token across adapters via the per-user state file (explicit wins).
|
|
84
|
+
const { token, source } = resolveSharedToken(options.port, tokenProvided ? options.token : undefined, generateToken);
|
|
85
|
+
// Persist + advertise the token, **host-only** (fired via onHost so a host
|
|
86
|
+
// reached by failover re-election does it too). Writing only on the host —
|
|
87
|
+
// not unconditionally at load like before — means a guest that resolved a
|
|
88
|
+
// fresh token on a transient read miss can never clobber the live host's
|
|
89
|
+
// token in the shared file. We write whenever the file doesn't already match
|
|
90
|
+
// OUR (port, token): that keeps a port change in sync and lets an explicit
|
|
91
|
+
// token overwrite a stale generated one — but a no-op when the file already
|
|
92
|
+
// agrees, so a concurrent host doesn't thrash it. The `paste_into_extension`
|
|
93
|
+
// field name dodges the logger's redaction filter so the value stays copyable.
|
|
94
|
+
const advertiseToken = () => {
|
|
95
|
+
const existing = readBridgeFile();
|
|
96
|
+
if (!existing || existing.port !== options.port || existing.token !== token) {
|
|
97
|
+
writeBridgeFile(options.port, token);
|
|
98
|
+
}
|
|
99
|
+
if (source !== "explicit") {
|
|
100
|
+
log.info("browser_bridge_token", {
|
|
101
|
+
paste_into_extension: token,
|
|
102
|
+
source,
|
|
103
|
+
mode: "host"
|
|
104
|
+
});
|
|
105
|
+
}
|
|
106
|
+
};
|
|
107
|
+
// Auto-elect: host the broker (win the bind) or join an existing one as a
|
|
108
|
+
// guest. Acquired into the plugin scope — the release is the teardown.
|
|
109
|
+
const endpoint = yield* Effect.acquireRelease(Effect.promise(() => createEndpoint({
|
|
110
|
+
host: options.host,
|
|
111
|
+
port: options.port,
|
|
112
|
+
token,
|
|
113
|
+
executor: executorProvided ? options.executor : undefined,
|
|
114
|
+
timeoutMs: options.timeoutMs,
|
|
115
|
+
label: "opencode-plugin",
|
|
116
|
+
onHost: advertiseToken,
|
|
117
|
+
// Lets a long-lived host pick up a rotated token from the shared
|
|
118
|
+
// file on a failed handshake, instead of rejecting forever until
|
|
119
|
+
// it's restarted. Disabled for an EXPLICIT token: the operator
|
|
120
|
+
// pinned it, so the file must never override their configured secret.
|
|
121
|
+
reloadToken: source === "explicit" ? undefined : () => readBridgeFile()?.token
|
|
122
|
+
}, {
|
|
123
|
+
logger: log,
|
|
124
|
+
createServerTransport: factoryOptions.createServerTransport ?? createNodeTransport,
|
|
125
|
+
createAgentSocket: factoryOptions.createAgentSocket ?? createNodeAgentSocket
|
|
126
|
+
})), (ep) => Effect.sync(() => {
|
|
127
|
+
try {
|
|
128
|
+
ep.shutdown();
|
|
129
|
+
} catch {}
|
|
130
|
+
}));
|
|
131
|
+
// Safety net for a host that exits without closing the plugin scope: hand
|
|
132
|
+
// the browser back automatically — the user shouldn't have to click
|
|
133
|
+
// Disconnect. Removed again on a clean scope close so it can't leak.
|
|
134
|
+
const onExit = () => {
|
|
135
|
+
try {
|
|
136
|
+
endpoint.shutdown();
|
|
137
|
+
} catch {}
|
|
138
|
+
};
|
|
139
|
+
yield* Effect.acquireRelease(Effect.sync(() => {
|
|
140
|
+
process.once("exit", onExit);
|
|
141
|
+
}), () => Effect.sync(() => {
|
|
142
|
+
process.off("exit", onExit);
|
|
143
|
+
}));
|
|
144
|
+
const tools = createBrowserTools({
|
|
145
|
+
send: endpoint.send,
|
|
146
|
+
options,
|
|
147
|
+
logger: log,
|
|
148
|
+
worktree: ctx.location.project.directory,
|
|
149
|
+
saveScreenshot: factoryOptions.saveScreenshot
|
|
150
|
+
});
|
|
151
|
+
yield* ctx.tool.transform((editor) => {
|
|
152
|
+
for (const info of Object.values(tools)) {
|
|
153
|
+
editor.add(info);
|
|
154
|
+
}
|
|
155
|
+
});
|
|
156
|
+
})
|
|
157
|
+
});
|
|
161
158
|
}
|
|
162
159
|
export const OpencodeBrowserPlugin = createBrowserPlugin();
|
|
163
160
|
export default OpencodeBrowserPlugin;
|
package/dist/opencode.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"mappings":"AAAA,SAAS,mBAAmB;
|
|
1
|
+
{"mappings":"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","names":[],"sources":["../src/opencode.ts"],"version":3,"file":"opencode.js","sourceRoot":""}
|
package/dist/tools.d.ts
CHANGED
|
@@ -1,7 +1,11 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import type { Plugin } from "@opencode/plugin/effect";
|
|
2
2
|
import type { AgentEndpoint } from "./broker.js";
|
|
3
3
|
import type { Logger } from "./logging.js";
|
|
4
4
|
import type { ResolvedBrowserOptions, ScreenshotResult } from "./types.js";
|
|
5
|
+
/** The tool-registration editor the host hands to a `tool.transform` callback. */
|
|
6
|
+
type ToolEditor = Parameters<Parameters<Plugin.Context["tool"]["transform"]>[0]>[0];
|
|
7
|
+
/** One tool definition, exactly as `ToolEditor.add` takes it. */
|
|
8
|
+
export type BrowserToolInfo = Parameters<ToolEditor["add"]>[0];
|
|
5
9
|
/** How a tool reaches the bridge — the endpoint's `send` (host or guest). */
|
|
6
10
|
export type SendFn = AgentEndpoint["send"];
|
|
7
11
|
/** Where the screenshot tool gets its disk-write behavior; swappable in tests. */
|
|
@@ -14,11 +18,25 @@ export interface ToolDeps {
|
|
|
14
18
|
send: SendFn;
|
|
15
19
|
options: ResolvedBrowserOptions;
|
|
16
20
|
logger: Logger;
|
|
21
|
+
/**
|
|
22
|
+
* Directory a relative `screenshotDir` resolves against. v1 read it off the
|
|
23
|
+
* per-call `ctx.worktree`; v2's tool context carries none, so the plugin
|
|
24
|
+
* passes its own `location.project.directory`.
|
|
25
|
+
*/
|
|
26
|
+
worktree: string;
|
|
17
27
|
saveScreenshot?: SaveScreenshot;
|
|
18
28
|
}
|
|
19
29
|
/**
|
|
20
|
-
* Build the `browser_*` tool
|
|
21
|
-
* enabled groups. Each tool is a thin
|
|
22
|
-
*
|
|
30
|
+
* Build the `browser_*` tool definitions registered through `tool.transform`,
|
|
31
|
+
* filtered to the enabled groups, keyed by tool name. Each tool is a thin
|
|
32
|
+
* adapter over the shared catalog: forward to the bridge, render the neutral
|
|
33
|
+
* result. `input` is the catalog's own JSON Schema (`toJsonSchema`) — the very
|
|
34
|
+
* document the MCP server emits.
|
|
35
|
+
*
|
|
36
|
+
* `send` runs inside `Effect.tryPromise`, whose `AbortSignal` fires when the
|
|
37
|
+
* host interrupts the tool fiber. That signal is what v1 received as
|
|
38
|
+
* `ctx.abort`, and it still drives the broker's `cancel` frame (the teardown of
|
|
39
|
+
* a human-in-the-loop `browser_request_feedback` overlay).
|
|
23
40
|
*/
|
|
24
|
-
export declare function createBrowserTools(deps: ToolDeps): Record<string,
|
|
41
|
+
export declare function createBrowserTools(deps: ToolDeps): Record<string, BrowserToolInfo>;
|
|
42
|
+
export {};
|
package/dist/tools.js
CHANGED
|
@@ -1,8 +1,15 @@
|
|
|
1
1
|
import { mkdir, rename, writeFile } from "node:fs/promises";
|
|
2
2
|
import { isAbsolute, join } from "node:path";
|
|
3
|
-
import {
|
|
3
|
+
import { Data, Effect } from "effect";
|
|
4
4
|
import { BROWSER_TOOLS } from "./catalog.js";
|
|
5
|
-
|
|
5
|
+
import { toJsonSchema } from "./schema.js";
|
|
6
|
+
/**
|
|
7
|
+
* Failure channel of a v2 tool. The host's `Tool.Error` is a `Schema.Class`
|
|
8
|
+
* tagged `"Tool.Error"`, but `@opencode/plugin/effect` does not re-export it,
|
|
9
|
+
* so this is a structurally identical tagged error built from `effect` itself
|
|
10
|
+
* (the host's own copy, per the package README).
|
|
11
|
+
*/
|
|
12
|
+
class BrowserToolError extends Data.TaggedError("Tool.Error") {}
|
|
6
13
|
function asRecord(value) {
|
|
7
14
|
return typeof value === "object" && value !== null ? value : {};
|
|
8
15
|
}
|
|
@@ -29,48 +36,18 @@ function makeSaveScreenshot(options) {
|
|
|
29
36
|
return finalPath;
|
|
30
37
|
};
|
|
31
38
|
}
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
break;
|
|
38
|
-
case "number":
|
|
39
|
-
schema = z.number();
|
|
40
|
-
break;
|
|
41
|
-
case "boolean":
|
|
42
|
-
schema = z.boolean();
|
|
43
|
-
break;
|
|
44
|
-
case "array":
|
|
45
|
-
schema = z.array(fieldToZod(field.items));
|
|
46
|
-
break;
|
|
47
|
-
case "object":
|
|
48
|
-
schema = z.object(buildShape(field.properties));
|
|
49
|
-
break;
|
|
50
|
-
}
|
|
51
|
-
if (field.description) {
|
|
52
|
-
schema = schema.describe(field.description);
|
|
53
|
-
}
|
|
54
|
-
if (field.optional) {
|
|
55
|
-
schema = schema.optional();
|
|
56
|
-
}
|
|
57
|
-
return schema;
|
|
58
|
-
}
|
|
59
|
-
function buildShape(input) {
|
|
60
|
-
const shape = {};
|
|
61
|
-
for (const [key, field] of Object.entries(input)) {
|
|
62
|
-
shape[key] = fieldToZod(field);
|
|
63
|
-
}
|
|
64
|
-
return shape;
|
|
65
|
-
}
|
|
66
|
-
/** Render an adapter-neutral result into OpenCode's text-only ToolResult. */
|
|
39
|
+
/**
|
|
40
|
+
* Render an adapter-neutral result into OpenCode's text-only `Tool.Result`. The
|
|
41
|
+
* model-visible text goes in `content`; `output` is the typed-output channel,
|
|
42
|
+
* which these tools (no output schema) leave unset.
|
|
43
|
+
*/
|
|
67
44
|
async function renderOpenCode(result, deps) {
|
|
68
45
|
if (result.kind === "text") {
|
|
69
|
-
return result.text;
|
|
46
|
+
return { content: result.text };
|
|
70
47
|
}
|
|
71
48
|
if (result.kind === "json") {
|
|
72
49
|
return {
|
|
73
|
-
|
|
50
|
+
content: result.text,
|
|
74
51
|
metadata: asRecord(result.data)
|
|
75
52
|
};
|
|
76
53
|
}
|
|
@@ -91,7 +68,7 @@ async function renderOpenCode(result, deps) {
|
|
|
91
68
|
});
|
|
92
69
|
const partialNote = result.partial ? " Note: only the viewport was captured — this executor can't capture beyond it." : "";
|
|
93
70
|
return {
|
|
94
|
-
|
|
71
|
+
content: `Saved screenshot to ${path} (${result.width}×${result.height}). Use the read tool to view it.${partialNote}`,
|
|
95
72
|
metadata: {
|
|
96
73
|
path,
|
|
97
74
|
width: result.width,
|
|
@@ -101,10 +78,20 @@ async function renderOpenCode(result, deps) {
|
|
|
101
78
|
}
|
|
102
79
|
};
|
|
103
80
|
}
|
|
81
|
+
function errorMessage(err) {
|
|
82
|
+
return err instanceof Error ? err.message : String(err);
|
|
83
|
+
}
|
|
104
84
|
/**
|
|
105
|
-
* Build the `browser_*` tool
|
|
106
|
-
* enabled groups. Each tool is a thin
|
|
107
|
-
*
|
|
85
|
+
* Build the `browser_*` tool definitions registered through `tool.transform`,
|
|
86
|
+
* filtered to the enabled groups, keyed by tool name. Each tool is a thin
|
|
87
|
+
* adapter over the shared catalog: forward to the bridge, render the neutral
|
|
88
|
+
* result. `input` is the catalog's own JSON Schema (`toJsonSchema`) — the very
|
|
89
|
+
* document the MCP server emits.
|
|
90
|
+
*
|
|
91
|
+
* `send` runs inside `Effect.tryPromise`, whose `AbortSignal` fires when the
|
|
92
|
+
* host interrupts the tool fiber. That signal is what v1 received as
|
|
93
|
+
* `ctx.abort`, and it still drives the broker's `cancel` frame (the teardown of
|
|
94
|
+
* a human-in-the-loop `browser_request_feedback` overlay).
|
|
108
95
|
*/
|
|
109
96
|
export function createBrowserTools(deps) {
|
|
110
97
|
const enabled = new Set(deps.options.groups);
|
|
@@ -114,37 +101,41 @@ export function createBrowserTools(deps) {
|
|
|
114
101
|
if (!enabled.has(spec.group)) {
|
|
115
102
|
continue;
|
|
116
103
|
}
|
|
117
|
-
|
|
104
|
+
tools[spec.name] = {
|
|
105
|
+
name: spec.name,
|
|
118
106
|
description: spec.description,
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
107
|
+
input: { ...toJsonSchema(spec.input) },
|
|
108
|
+
execute: (input) => Effect.tryPromise({
|
|
109
|
+
try: async (signal) => {
|
|
110
|
+
const args = asRecord(input);
|
|
111
|
+
const group = typeof args.group === "string" ? args.group : "";
|
|
112
|
+
const target = typeof args.target === "string" ? args.target : undefined;
|
|
113
|
+
const params = spec.params ? spec.params(args) : args;
|
|
114
|
+
deps.logger.trace("browser_tool_invoked", {
|
|
115
|
+
tool: spec.name,
|
|
116
|
+
action: spec.action,
|
|
117
|
+
group,
|
|
118
|
+
target
|
|
119
|
+
});
|
|
120
|
+
const data = await deps.send(spec.action, group, params, signal, target, spec.timeoutMs);
|
|
121
|
+
deps.logger.trace("browser_tool_completed", {
|
|
122
|
+
tool: spec.name,
|
|
123
|
+
group
|
|
124
|
+
});
|
|
125
|
+
const result = spec.result ? spec.result(data, args) : {
|
|
126
|
+
kind: "text",
|
|
127
|
+
text: `${spec.name} ok`
|
|
128
|
+
};
|
|
129
|
+
return renderOpenCode(result, {
|
|
130
|
+
group,
|
|
131
|
+
worktree: deps.worktree,
|
|
132
|
+
saveScreenshot,
|
|
133
|
+
logger: deps.logger
|
|
134
|
+
});
|
|
135
|
+
},
|
|
136
|
+
catch: (err) => new BrowserToolError({ message: errorMessage(err) })
|
|
137
|
+
})
|
|
146
138
|
};
|
|
147
|
-
tools[spec.name] = definition;
|
|
148
139
|
}
|
|
149
140
|
return tools;
|
|
150
141
|
}
|
package/dist/tools.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"mappings":"AAAA,SAAS,OAAO,QAAQ,iBAAiB;AACzC,SAAS,YAAY,YAAY;
|
|
1
|
+
{"mappings":"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","names":[],"sources":["../src/tools.ts"],"version":3,"file":"tools.js","sourceRoot":""}
|
package/dist/types.d.ts
CHANGED
|
@@ -12,8 +12,8 @@ export type { ToolGroup } from "./schema.js";
|
|
|
12
12
|
*/
|
|
13
13
|
export type ExecutorMode = "auto" | "cdp" | "content";
|
|
14
14
|
/**
|
|
15
|
-
* Per-plugin configuration,
|
|
16
|
-
*
|
|
15
|
+
* Per-plugin configuration, delivered as `ctx.options` (the `options` of the
|
|
16
|
+
* `plugins` entry). All
|
|
17
17
|
* fields are optional; see DEFAULT_OPTIONS for the resolved defaults.
|
|
18
18
|
*/
|
|
19
19
|
export interface BrowserPluginOptions {
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@vymalo/opencode-browser",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.18.0",
|
|
4
4
|
"description": "OpenCode plugin that gives the model hands in a real browser: it registers browser_* tools (open, click, type, scroll, screenshot, snapshot, …) and hosts a localhost WebSocket bridge that a companion Chromium/Firefox extension connects to. Tabs are organized into named groups so each task stays isolated and inspectable.",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"author": "vymalo contributors",
|
|
@@ -16,6 +16,7 @@
|
|
|
16
16
|
"keywords": [
|
|
17
17
|
"opencode",
|
|
18
18
|
"opencode-plugin",
|
|
19
|
+
"opencode-v2",
|
|
19
20
|
"browser",
|
|
20
21
|
"automation",
|
|
21
22
|
"chromium",
|
|
@@ -49,13 +50,17 @@
|
|
|
49
50
|
"access": "public"
|
|
50
51
|
},
|
|
51
52
|
"dependencies": {
|
|
52
|
-
"@opencode
|
|
53
|
-
"ws": "^8.21.
|
|
53
|
+
"@opencode/plugin": "2.2.6",
|
|
54
|
+
"ws": "^8.21.3"
|
|
54
55
|
},
|
|
55
56
|
"devDependencies": {
|
|
56
|
-
"@types/ws": "^8.18.
|
|
57
|
-
"
|
|
58
|
-
"
|
|
57
|
+
"@types/ws": "^8.18.1",
|
|
58
|
+
"effect": "4.1.0",
|
|
59
|
+
"vite": "^8.3.0",
|
|
60
|
+
"vitest": "^5.0.1"
|
|
61
|
+
},
|
|
62
|
+
"peerDependencies": {
|
|
63
|
+
"effect": "^4.0.0"
|
|
59
64
|
},
|
|
60
65
|
"scripts": {
|
|
61
66
|
"build": "node ../../scripts/build-package.mjs",
|