@different-ai/opencode-browser 3.0.0 → 4.0.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.
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "manifest_version": 3,
3
3
  "name": "OpenCode Browser Automation",
4
- "version": "2.0.0",
4
+ "version": "4.0.0",
5
5
  "description": "Browser automation for OpenCode",
6
6
  "permissions": [
7
7
  "tabs",
@@ -9,7 +9,8 @@
9
9
  "scripting",
10
10
  "storage",
11
11
  "notifications",
12
- "alarms"
12
+ "alarms",
13
+ "nativeMessaging"
13
14
  ],
14
15
  "host_permissions": [
15
16
  "<all_urls>"
package/package.json CHANGED
@@ -1,32 +1,34 @@
1
1
  {
2
2
  "name": "@different-ai/opencode-browser",
3
- "version": "3.0.0",
4
- "description": "Browser automation MCP server for OpenCode. Control your real Chrome browser with existing logins and cookies.",
3
+ "version": "4.0.1",
4
+ "description": "Browser automation plugin for OpenCode (native messaging + per-tab ownership).",
5
5
  "type": "module",
6
6
  "bin": {
7
- "opencode-browser": "./bin/cli.js"
7
+ "opencode-browser": "bin/cli.js"
8
8
  },
9
- "main": "./src/mcp-server.ts",
9
+ "main": "./src/plugin.ts",
10
10
  "exports": {
11
- ".": "./src/mcp-server.ts"
11
+ ".": "./src/plugin.ts",
12
+ "./plugin": "./src/plugin.ts"
12
13
  },
13
14
  "files": [
14
15
  "bin",
15
- "src",
16
+ "src/plugin.ts",
16
17
  "extension",
17
18
  "README.md"
18
19
  ],
19
20
  "scripts": {
20
- "install-extension": "node bin/cli.js install",
21
- "serve": "bun run src/mcp-server.ts"
21
+ "install": "node bin/cli.js install",
22
+ "uninstall": "node bin/cli.js uninstall",
23
+ "status": "node bin/cli.js status"
22
24
  },
23
25
  "keywords": [
24
26
  "opencode",
25
27
  "browser",
26
28
  "automation",
27
29
  "chrome",
28
- "mcp",
29
- "model-context-protocol"
30
+ "plugin",
31
+ "native-messaging"
30
32
  ],
31
33
  "author": "Benjamin Shafii",
32
34
  "license": "MIT",
@@ -38,11 +40,11 @@
38
40
  "url": "https://github.com/different-ai/opencode-browser/issues"
39
41
  },
40
42
  "homepage": "https://github.com/different-ai/opencode-browser#readme",
41
- "dependencies": {
42
- "@modelcontextprotocol/sdk": "^1.25.2",
43
- "zod": "^4.3.5"
43
+ "peerDependencies": {
44
+ "@opencode-ai/plugin": "*"
44
45
  },
45
46
  "devDependencies": {
47
+ "@opencode-ai/plugin": "*",
46
48
  "bun-types": "*"
47
49
  }
48
50
  }
package/src/plugin.ts ADDED
@@ -0,0 +1,299 @@
1
+ import type { Plugin } from "@opencode-ai/plugin";
2
+ import { tool } from "@opencode-ai/plugin";
3
+ import net from "net";
4
+ import { existsSync, mkdirSync, readFileSync } from "fs";
5
+ import { homedir } from "os";
6
+ import { dirname, join } from "path";
7
+ import { spawn } from "child_process";
8
+ import { fileURLToPath } from "url";
9
+
10
+ const __filename = fileURLToPath(import.meta.url);
11
+ const __dirname = dirname(__filename);
12
+ const PACKAGE_JSON_PATH = join(__dirname, "..", "package.json");
13
+
14
+ let cachedVersion: string | null = null;
15
+
16
+ function getPackageVersion(): string {
17
+ if (cachedVersion) return cachedVersion;
18
+ try {
19
+ const pkg = JSON.parse(readFileSync(PACKAGE_JSON_PATH, "utf8"));
20
+ if (typeof pkg?.version === "string") {
21
+ cachedVersion = pkg.version;
22
+ return cachedVersion;
23
+ }
24
+ } catch {
25
+ // ignore
26
+ }
27
+ cachedVersion = "unknown";
28
+ return cachedVersion;
29
+ }
30
+
31
+ const BASE_DIR = join(homedir(), ".opencode-browser");
32
+ const SOCKET_PATH = join(BASE_DIR, "broker.sock");
33
+
34
+ mkdirSync(BASE_DIR, { recursive: true });
35
+
36
+ type BrokerResponse =
37
+ | { type: "response"; id: number; ok: true; data: any }
38
+ | { type: "response"; id: number; ok: false; error: string };
39
+
40
+ function createJsonLineParser(onMessage: (msg: any) => void): (chunk: Buffer) => void {
41
+ let buffer = "";
42
+ return (chunk: Buffer) => {
43
+ buffer += chunk.toString("utf8");
44
+ while (true) {
45
+ const idx = buffer.indexOf("\n");
46
+ if (idx === -1) return;
47
+ const line = buffer.slice(0, idx);
48
+ buffer = buffer.slice(idx + 1);
49
+ if (!line.trim()) continue;
50
+ try {
51
+ onMessage(JSON.parse(line));
52
+ } catch {
53
+ // ignore
54
+ }
55
+ }
56
+ };
57
+ }
58
+
59
+ function writeJsonLine(socket: net.Socket, msg: any): void {
60
+ socket.write(JSON.stringify(msg) + "\n");
61
+ }
62
+
63
+ function maybeStartBroker(): void {
64
+ const brokerPath = join(BASE_DIR, "broker.cjs");
65
+ if (!existsSync(brokerPath)) return;
66
+
67
+ try {
68
+ const child = spawn(process.execPath, [brokerPath], { detached: true, stdio: "ignore" });
69
+ child.unref();
70
+ } catch {
71
+ // ignore
72
+ }
73
+ }
74
+
75
+ async function connectToBroker(): Promise<net.Socket> {
76
+ return await new Promise((resolve, reject) => {
77
+ const socket = net.createConnection(SOCKET_PATH);
78
+ socket.once("connect", () => resolve(socket));
79
+ socket.once("error", (err) => reject(err));
80
+ });
81
+ }
82
+
83
+ async function sleep(ms: number): Promise<void> {
84
+ return await new Promise((r) => setTimeout(r, ms));
85
+ }
86
+
87
+ let socket: net.Socket | null = null;
88
+ let sessionId = Math.random().toString(36).slice(2);
89
+ let reqId = 0;
90
+ const pending = new Map<number, { resolve: (v: any) => void; reject: (e: Error) => void }>();
91
+
92
+ async function ensureBrokerSocket(): Promise<net.Socket> {
93
+ if (socket && !socket.destroyed) return socket;
94
+
95
+ // Try to connect; if missing, try to start broker and retry.
96
+ try {
97
+ socket = await connectToBroker();
98
+ } catch {
99
+ maybeStartBroker();
100
+ for (let i = 0; i < 20; i++) {
101
+ await sleep(100);
102
+ try {
103
+ socket = await connectToBroker();
104
+ break;
105
+ } catch {}
106
+ }
107
+ }
108
+
109
+ if (!socket || socket.destroyed) {
110
+ throw new Error(
111
+ "Could not connect to local broker. Run `npx @different-ai/opencode-browser install` and ensure the extension is loaded."
112
+ );
113
+ }
114
+
115
+ socket.setNoDelay(true);
116
+ socket.on(
117
+ "data",
118
+ createJsonLineParser((msg) => {
119
+ if (msg?.type !== "response" || typeof msg.id !== "number") return;
120
+ const p = pending.get(msg.id);
121
+ if (!p) return;
122
+ pending.delete(msg.id);
123
+ const res = msg as BrokerResponse;
124
+ if (!res.ok) p.reject(new Error(res.error));
125
+ else p.resolve(res.data);
126
+ })
127
+ );
128
+
129
+ socket.on("close", () => {
130
+ socket = null;
131
+ });
132
+
133
+ socket.on("error", () => {
134
+ socket = null;
135
+ });
136
+
137
+ writeJsonLine(socket, { type: "hello", role: "plugin", sessionId, pid: process.pid });
138
+
139
+ return socket;
140
+ }
141
+
142
+ async function brokerRequest(op: string, payload: Record<string, any>): Promise<any> {
143
+ const s = await ensureBrokerSocket();
144
+ const id = ++reqId;
145
+
146
+ return await new Promise((resolve, reject) => {
147
+ pending.set(id, { resolve, reject });
148
+ writeJsonLine(s, { type: "request", id, op, ...payload });
149
+ setTimeout(() => {
150
+ if (!pending.has(id)) return;
151
+ pending.delete(id);
152
+ reject(new Error("Timed out waiting for broker response"));
153
+ }, 60000);
154
+ });
155
+ }
156
+
157
+ function toolResultText(data: any, fallback: string): string {
158
+ if (typeof data?.content === "string") return data.content;
159
+ if (typeof data === "string") return data;
160
+ if (data?.content != null) return JSON.stringify(data.content);
161
+ return fallback;
162
+ }
163
+
164
+ const plugin: Plugin = {
165
+ name: "opencode-browser",
166
+ tools: [
167
+
168
+ tool(
169
+ "browser_status",
170
+ "Check broker/native-host connection status and current tab claims.",
171
+ {},
172
+ async () => {
173
+ const data = await brokerRequest("status", {});
174
+ return JSON.stringify(data);
175
+ }
176
+ ),
177
+
178
+ tool(
179
+ "browser_get_tabs",
180
+ "List all open browser tabs",
181
+ {},
182
+ async () => {
183
+ const data = await brokerRequest("tool", { tool: "get_tabs", args: {} });
184
+ return toolResultText(data, "ok");
185
+ }
186
+ ),
187
+ tool(
188
+ "browser_navigate",
189
+ "Navigate to a URL in the browser",
190
+ { url: { type: "string" }, tabId: { type: "number", optional: true } },
191
+ async ({ url, tabId }: any) => {
192
+ const data = await brokerRequest("tool", { tool: "navigate", args: { url, tabId } });
193
+ return toolResultText(data, `Navigated to ${url}`);
194
+ }
195
+ ),
196
+ tool(
197
+ "browser_click",
198
+ "Click an element on the page using a CSS selector",
199
+ { selector: { type: "string" }, tabId: { type: "number", optional: true } },
200
+ async ({ selector, tabId }: any) => {
201
+ const data = await brokerRequest("tool", { tool: "click", args: { selector, tabId } });
202
+ return toolResultText(data, `Clicked ${selector}`);
203
+ }
204
+ ),
205
+ tool(
206
+ "browser_type",
207
+ "Type text into an input element",
208
+ {
209
+ selector: { type: "string" },
210
+ text: { type: "string" },
211
+ clear: { type: "boolean", optional: true },
212
+ tabId: { type: "number", optional: true },
213
+ },
214
+ async ({ selector, text, clear, tabId }: any) => {
215
+ const data = await brokerRequest("tool", { tool: "type", args: { selector, text, clear, tabId } });
216
+ return toolResultText(data, `Typed \"${text}\" into ${selector}`);
217
+ }
218
+ ),
219
+ tool(
220
+ "browser_screenshot",
221
+ "Take a screenshot of the current page. Returns base64 image data URL.",
222
+ { tabId: { type: "number", optional: true } },
223
+ async ({ tabId }: any) => {
224
+ const data = await brokerRequest("tool", { tool: "screenshot", args: { tabId } });
225
+ return toolResultText(data, "Screenshot failed");
226
+ }
227
+ ),
228
+ tool(
229
+ "browser_snapshot",
230
+ "Get an accessibility tree snapshot of the page.",
231
+ { tabId: { type: "number", optional: true } },
232
+ async ({ tabId }: any) => {
233
+ const data = await brokerRequest("tool", { tool: "snapshot", args: { tabId } });
234
+ return toolResultText(data, "Snapshot failed");
235
+ }
236
+ ),
237
+ tool(
238
+ "browser_scroll",
239
+ "Scroll the page or scroll an element into view",
240
+ {
241
+ selector: { type: "string", optional: true },
242
+ x: { type: "number", optional: true },
243
+ y: { type: "number", optional: true },
244
+ tabId: { type: "number", optional: true },
245
+ },
246
+ async ({ selector, x, y, tabId }: any) => {
247
+ const data = await brokerRequest("tool", { tool: "scroll", args: { selector, x, y, tabId } });
248
+ return toolResultText(data, "Scrolled");
249
+ }
250
+ ),
251
+ tool(
252
+ "browser_wait",
253
+ "Wait for a specified duration",
254
+ { ms: { type: "number", optional: true }, tabId: { type: "number", optional: true } },
255
+ async ({ ms, tabId }: any) => {
256
+ const data = await brokerRequest("tool", { tool: "wait", args: { ms, tabId } });
257
+ return toolResultText(data, "Waited");
258
+ }
259
+ ),
260
+ tool(
261
+ "browser_execute",
262
+ "Execute JavaScript code in the page context and return the result.",
263
+ { code: { type: "string" }, tabId: { type: "number", optional: true } },
264
+ async ({ code, tabId }: any) => {
265
+ const data = await brokerRequest("tool", { tool: "execute_script", args: { code, tabId } });
266
+ return toolResultText(data, "Execute failed");
267
+ }
268
+ ),
269
+ tool(
270
+ "browser_claim_tab",
271
+ "Claim a tab for this OpenCode session (per-tab ownership).",
272
+ { tabId: { type: "number" }, force: { type: "boolean", optional: true } },
273
+ async ({ tabId, force }: any) => {
274
+ const data = await brokerRequest("claim_tab", { tabId, force });
275
+ return JSON.stringify(data);
276
+ }
277
+ ),
278
+ tool(
279
+ "browser_release_tab",
280
+ "Release a previously claimed tab.",
281
+ { tabId: { type: "number" } },
282
+ async ({ tabId }: any) => {
283
+ const data = await brokerRequest("release_tab", { tabId });
284
+ return JSON.stringify(data);
285
+ }
286
+ ),
287
+ tool(
288
+ "browser_list_claims",
289
+ "List current tab ownership claims.",
290
+ {},
291
+ async () => {
292
+ const data = await brokerRequest("list_claims", {});
293
+ return JSON.stringify(data);
294
+ }
295
+ ),
296
+ ],
297
+ };
298
+
299
+ export default plugin;