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