@vymalo/opencode-browser 0.17.0 → 0.18.1

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 CHANGED
@@ -7,6 +7,8 @@
7
7
  ![node: >=22](https://img.shields.io/badge/node-%3E%3D22-339933)
8
8
  ![license: MIT](https://img.shields.io/badge/license-MIT-blue)
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 tools](#the-33-tools)
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
- "plugin": [["@vymalo/opencode-browser", { "port": 4517 }]]
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
- Second argument to the plugin tuple in `opencode.json`:
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 worktree). |
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 33 tools
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
- "plugin": [["@vymalo/opencode-browser", { "groups": ["page", "control", "debug"] }]],
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` hook shuts the bridge down, and the dropped socket
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.
@@ -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;
@@ -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
- this.socket = this.deps.createSocket(this.opts.url, {
39
- onOpen: () => {
40
- this.socket?.send(encodeFrame(helloFrame(this.opts.token, {
41
- role: "agent",
42
- client: this.opts.label
43
- })));
44
- // Resolve only once the broker replies `ready`.
45
- },
46
- onMessage: (data) => {
47
- const frame = decodeFrame(data);
48
- if (!frame) {
49
- return;
50
- }
51
- if (frame.type === "ready") {
52
- this.ready = true;
53
- this.deps.logger.trace("browser_guest_ready", { label: this.opts.label });
54
- for (const w of this.readyWaiters.splice(0)) {
55
- w();
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
- resolve();
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() {
@@ -1 +1 @@
1
- {"mappings":"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","names":[],"sources":["../src/agent-client.ts"],"version":3,"file":"agent-client.js","sourceRoot":""}
1
+ {"mappings":"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","names":[],"sources":["../src/agent-client.ts"],"version":3,"file":"agent-client.js","sourceRoot":""}
@@ -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,
@@ -1 +1 @@
1
- {"mappings":"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","names":[],"sources":["../src/endpoint.ts"],"version":3,"file":"endpoint.js","sourceRoot":""}
1
+ {"mappings":"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","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, fromOpenCodeLogLevel, type LogFields, type Logger, type LogLevel } from "./logging.js";
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, fromOpenCodeLogLevel } from "./logging.js";
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,6BACK;AAEP,SAEE,QACA,mBAGK;AAEP,SACE,aACA,wBAMK;AAEP,SACE,sBAKK;AAEP,SAGE,mBAEK;AAEP,SAAS,uBAAuB,2BAA2B;AAE3D,SAEE,gBACA,oBAEA,uBACK;AAEP,SAAS,0BAA2E;AAEpF,SAEE,eACA,gBAGA,mBAGK;AAEP,SAAsD,oBAAoB;AAE1E,SACE,yBACA,mBACA,4BAIK;AAEP,SACE,iBAKA,aACA,aAIA,YACA,QACA,wBAGK","names":[],"sources":["../src/lib.ts"],"version":3,"file":"lib.js","sourceRoot":""}
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
- export declare function fromOpenCodeLogLevel(value: unknown): LogLevel | undefined;
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
- export function fromOpenCodeLogLevel(value) {
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
- case "DEBUG":
60
- // Host DEBUG unlocks our most-verbose internal tier (trace) — the browser
61
- // bridge is the package users debug most, so DEBUG opts into full tracing.
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";
@@ -1 +1 @@
1
- {"mappings":"AAIA,OAAO,MAAM,qBAA+C;CAC1D,OAAO;CACP,OAAO;CACP,MAAM;CACN,MAAM;CACN,OAAO;AACT;AAEA,OAAO,MAAM,oBAA8B;AAc3C,SAAS,aAAa,QAA2C;CAC/D,IAAI,CAAC,QAAQ;EACX,OAAO;CACT;CACA,MAAM,WAAsB,CAAC;CAC7B,KAAK,MAAM,CAAC,KAAK,UAAU,OAAO,QAAQ,MAAM,GAAG;EACjD,IAAI,uCAAuC,KAAK,GAAG,GAAG;GACpD,SAAS,OAAO;GAChB;EACF;EACA,SAAS,OAAO;CAClB;CACA,OAAO;AACT;AAEA,OAAO,SAAS,wBAAwB,WAAqB,mBAA2B;CACtF,MAAM,cAAc,mBAAmB;CAEvC,MAAM,SAAS,OAAiB,OAAe,WAA6B;EAC1E,IAAI,mBAAmB,SAAS,aAAa;GAC3C;EACF;EACA,MAAM,UAAU;GACd,IAAI,IAAI,KAAK,CAAC,CAAC,YAAY;GAC3B;GACA;GACA,GAAI,aAAa,MAAM,KAAK,CAAC;EAC/B;EACA,MAAM,OAAO,KAAK,UAAU,OAAO;EACnC,IAAI,UAAU,SAAS;GACrB,QAAQ,MAAM,IAAI;GAClB;EACF;EACA,IAAI,UAAU,QAAQ;GACpB,QAAQ,KAAK,IAAI;GACjB;EACF;EACA,QAAQ,IAAI,IAAI;CAClB;CAEA,OAAO;EACL,QAAQ,OAAO,WAAW,MAAM,SAAS,OAAO,MAAM;EACtD,QAAQ,OAAO,WAAW,MAAM,SAAS,OAAO,MAAM;EACtD,OAAO,OAAO,WAAW,MAAM,QAAQ,OAAO,MAAM;EACpD,OAAO,OAAO,WAAW,MAAM,QAAQ,OAAO,MAAM;EACpD,QAAQ,OAAO,WAAW,MAAM,SAAS,OAAO,MAAM;CACxD;AACF;AAEA,OAAO,SAAS,qBAAqB,OAAsC;CACzE,IAAI,OAAO,UAAU,UAAU;EAC7B,OAAO;CACT;CACA,QAAQ,MAAM,YAAY,GAA1B;EACE,KAAK;;;EAGH,OAAO;EACT,KAAK,QACH,OAAO;EACT,KAAK,QACH,OAAO;EACT,KAAK,SACH,OAAO;EACT,SACE,OAAO;CACX;AACF","names":[],"sources":["../src/logging.ts"],"version":3,"file":"logging.js","sourceRoot":""}
1
+ {"mappings":"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","names":[],"sources":["../src/logging.ts"],"version":3,"file":"logging.js","sourceRoot":""}
@@ -1,10 +1,12 @@
1
- import type { Plugin } from "@opencode-ai/plugin";
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 OpenCode-piped logger). */
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
- export declare function createBrowserPlugin(factoryOptions?: BrowserPluginFactoryOptions): Plugin;
19
- export declare const OpencodeBrowserPlugin: Plugin;
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 { createJsonConsoleLogger, DEFAULT_LOG_LEVEL, fromOpenCodeLogLevel, LOG_LEVEL_PRIORITY } from "./logging.js";
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,49 +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) — and no plugin in this suite may write to the user's TUI at all
34
- // (ADR-0014, superseding ADR-0013's narrower per-plugin scope), so even a
35
- // warn/error about a down bridge stays off the terminal. Only mirror to the
36
- // JSON console when explicitly requested; set VYMALO_PLUGIN_CONSOLE_LOG=1 to
37
- // restore full console output for debugging.
38
- const consoleAll = /^(1|true|yes|on)$/i.test(process.env.VYMALO_PLUGIN_CONSOLE_LOG ?? "");
39
- const write = (level, event, fields) => {
40
- if (LOG_LEVEL_PRIORITY[level] < LOG_LEVEL_PRIORITY[getMinLevel()]) {
41
- return;
42
- }
43
- if (consoleAll) {
44
- fallback[level](event, fields);
45
- }
46
- // OpenCode's host log API has no `trace` tier — fold it into `debug` for the
47
- // host call (our own level filter above already gates trace by DEBUG).
48
- const hostLevel = level === "trace" ? "debug" : level;
49
- void client.app.log({ body: {
50
- service: PLUGIN_SERVICE_NAME,
51
- level: hostLevel,
52
- message: event,
53
- extra: fields
54
- } }).catch(() => {
55
- /* best-effort */
56
- });
57
- };
58
- return {
59
- trace: (event, fields) => write("trace", event, fields),
60
- debug: (event, fields) => write("debug", event, fields),
61
- info: (event, fields) => write("info", event, fields),
62
- warn: (event, fields) => write("warn", event, fields),
63
- error: (event, fields) => write("error", event, fields)
64
- };
65
- }
66
26
  function resolveOptions(raw, generateToken) {
67
27
  const opts = raw ?? {};
68
28
  const token = typeof opts.token === "string" && opts.token.length > 0 ? opts.token : generateToken();
@@ -77,90 +37,124 @@ function resolveOptions(raw, generateToken) {
77
37
  screenshotDir: opts.screenshotDir ?? DEFAULTS.screenshotDir
78
38
  };
79
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
+ */
80
55
  export function createBrowserPlugin(factoryOptions = {}) {
81
- return async ({ client }, pluginOptions) => {
82
- let currentLogLevel = DEFAULT_LOG_LEVEL;
83
- const logger = factoryOptions.logger ?? createOpenCodeLogger(client, () => currentLogLevel);
84
- const generateToken = factoryOptions.generateToken ?? (() => randomBytes(24).toString("hex"));
85
- const options = resolveOptions(pluginOptions, generateToken);
86
- const rawOptions = pluginOptions;
87
- // Empty string is NOT an explicit token (resolveOptions generates one for
88
- // it). Treating "" as provided would mark the shared token "explicit" and
89
- // skip the paste_into_extension log, leaving the extension unable to learn
90
- // the generated token.
91
- const tokenProvided = typeof rawOptions?.token === "string" && rawOptions.token.length > 0;
92
- // Only forward the executor preference when the operator set it explicitly;
93
- // otherwise the extension keeps its own dashboard choice.
94
- const executorProvided = typeof rawOptions?.executor === "string";
95
- if (!options.enabled) {
96
- logger.info("browser_plugin_disabled", {});
97
- return { config: async (config) => {
98
- currentLogLevel = fromOpenCodeLogLevel(config.logLevel) ?? DEFAULT_LOG_LEVEL;
99
- } };
100
- }
101
- // Share the token across adapters via the per-user state file (explicit wins).
102
- const { token, source } = resolveSharedToken(options.port, tokenProvided ? options.token : undefined, generateToken);
103
- // Persist + advertise the token, **host-only** (fired via onHost so a host
104
- // reached by failover re-election does it too). Writing only on the host —
105
- // not unconditionally at load like before — means a guest that resolved a
106
- // fresh token on a transient read miss can never clobber the live host's
107
- // token in the shared file. We write whenever the file doesn't already match
108
- // OUR (port, token): that keeps a port change in sync and lets an explicit
109
- // token overwrite a stale generated one — but a no-op when the file already
110
- // agrees, so a concurrent host doesn't thrash it. The `paste_into_extension`
111
- // field name dodges the logger's redaction filter so the value stays copyable.
112
- const advertiseToken = () => {
113
- const existing = readBridgeFile();
114
- if (!existing || existing.port !== options.port || existing.token !== token) {
115
- writeBridgeFile(options.port, token);
116
- }
117
- if (source !== "explicit") {
118
- logger.info("browser_bridge_token", {
119
- paste_into_extension: token,
120
- source,
121
- mode: "host"
122
- });
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);
123
66
  }
124
- };
125
- // Auto-elect: host the broker (win the bind) or join an existing one as a guest.
126
- const endpoint = await createEndpoint({
127
- host: options.host,
128
- port: options.port,
129
- token,
130
- executor: executorProvided ? options.executor : undefined,
131
- timeoutMs: options.timeoutMs,
132
- label: "opencode-plugin",
133
- onHost: advertiseToken,
134
- // Lets a long-lived host pick up a rotated token from the shared file on
135
- // a failed handshake, instead of rejecting forever until it's restarted.
136
- // Disabled for an EXPLICIT token: the operator pinned it, so the file
137
- // must never override their configured secret.
138
- reloadToken: source === "explicit" ? undefined : () => readBridgeFile()?.token
139
- }, {
140
- logger,
141
- createServerTransport: factoryOptions.createServerTransport ?? createNodeTransport,
142
- createAgentSocket: factoryOptions.createAgentSocket ?? createNodeAgentSocket
143
- });
144
- // When OpenCode exits, hand the browser back automatically — the user
145
- // shouldn't have to click Disconnect.
146
- process.once("exit", () => {
147
- try {
148
- endpoint.shutdown();
149
- } catch {}
150
- });
151
- const tools = createBrowserTools({
152
- send: endpoint.send,
153
- options,
154
- logger,
155
- saveScreenshot: factoryOptions.saveScreenshot
156
- });
157
- return {
158
- tool: tools,
159
- config: async (config) => {
160
- 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;
161
82
  }
162
- };
163
- };
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
+ });
164
158
  }
165
159
  export const OpencodeBrowserPlugin = createBrowserPlugin();
166
160
  export default OpencodeBrowserPlugin;
@@ -1 +1 @@
1
- {"mappings":"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","names":[],"sources":["../src/opencode.ts"],"version":3,"file":"opencode.js","sourceRoot":""}
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 { type ToolDefinition } from "@opencode-ai/plugin";
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 map registered under `Hooks.tool`, filtered to the
21
- * enabled groups. Each tool is a thin adapter over the shared catalog: validate
22
- * args (zod), forward to the bridge, render the neutral result.
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, ToolDefinition>;
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 { tool } from "@opencode-ai/plugin";
3
+ import { Data, Effect } from "effect";
4
4
  import { BROWSER_TOOLS } from "./catalog.js";
5
- const z = tool.schema;
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
- function fieldToZod(field) {
33
- let schema;
34
- switch (field.type) {
35
- case "string":
36
- schema = field.enum ? z.enum(field.enum) : z.string();
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
- output: result.text,
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
- output: `Saved screenshot to ${path} (${result.width}×${result.height}). Use the read tool to view it.${partialNote}`,
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 map registered under `Hooks.tool`, filtered to the
106
- * enabled groups. Each tool is a thin adapter over the shared catalog: validate
107
- * args (zod), forward to the bridge, render the neutral result.
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
- const definition = {
104
+ tools[spec.name] = {
105
+ name: spec.name,
118
106
  description: spec.description,
119
- args: buildShape(spec.input),
120
- async execute(args, ctx) {
121
- const group = typeof args.group === "string" ? args.group : "";
122
- const target = typeof args.target === "string" ? args.target : undefined;
123
- const params = spec.params ? spec.params(args) : args;
124
- deps.logger.trace("browser_tool_invoked", {
125
- tool: spec.name,
126
- action: spec.action,
127
- group,
128
- target
129
- });
130
- const data = await deps.send(spec.action, group, params, ctx.abort, target, spec.timeoutMs);
131
- deps.logger.trace("browser_tool_completed", {
132
- tool: spec.name,
133
- group
134
- });
135
- const result = spec.result ? spec.result(data, args) : {
136
- kind: "text",
137
- text: `${spec.name} ok`
138
- };
139
- return renderOpenCode(result, {
140
- group,
141
- worktree: ctx.worktree,
142
- saveScreenshot,
143
- logger: deps.logger
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":"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","names":[],"sources":["../src/tools.ts"],"version":3,"file":"tools.js","sourceRoot":""}
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, supplied as the second argument to the plugin
16
- * factory by OpenCode (the `[name, options]` tuple form in `plugin`). All
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.17.0",
3
+ "version": "0.18.1",
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-ai/plugin": "1.15.10",
53
- "ws": "^8.21.0"
53
+ "@opencode/plugin": "2.2.6",
54
+ "ws": "^8.21.3"
54
55
  },
55
56
  "devDependencies": {
56
- "@types/ws": "^8.18.0",
57
- "vite": "^8.2.1",
58
- "vitest": "^4.1.7"
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",