@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.
- package/README.md +46 -102
- package/bin/broker.cjs +290 -0
- package/bin/cli.js +266 -240
- package/bin/native-host.cjs +136 -0
- package/extension/background.js +235 -190
- package/extension/manifest.json +3 -2
- package/package.json +15 -13
- package/src/plugin.ts +299 -0
- package/src/mcp-server.ts +0 -440
package/bin/cli.js
CHANGED
|
@@ -2,30 +2,48 @@
|
|
|
2
2
|
/**
|
|
3
3
|
* OpenCode Browser - CLI
|
|
4
4
|
*
|
|
5
|
+
* Architecture (v4):
|
|
6
|
+
* OpenCode Plugin <-> Local Broker (unix socket) <-> Native Messaging Host <-> Chrome Extension
|
|
7
|
+
*
|
|
5
8
|
* Commands:
|
|
6
|
-
* install
|
|
7
|
-
*
|
|
8
|
-
* status
|
|
9
|
+
* install - Install extension + native host
|
|
10
|
+
* uninstall - Remove native host registration
|
|
11
|
+
* status - Show installation status
|
|
9
12
|
*/
|
|
10
13
|
|
|
11
|
-
import {
|
|
14
|
+
import {
|
|
15
|
+
existsSync,
|
|
16
|
+
mkdirSync,
|
|
17
|
+
writeFileSync,
|
|
18
|
+
readFileSync,
|
|
19
|
+
copyFileSync,
|
|
20
|
+
readdirSync,
|
|
21
|
+
unlinkSync,
|
|
22
|
+
chmodSync,
|
|
23
|
+
} from "fs";
|
|
12
24
|
import { homedir, platform } from "os";
|
|
13
25
|
import { join, dirname } from "path";
|
|
14
26
|
import { fileURLToPath } from "url";
|
|
15
|
-
import { execSync, spawn } from "child_process";
|
|
16
27
|
import { createInterface } from "readline";
|
|
17
28
|
|
|
18
29
|
const __filename = fileURLToPath(import.meta.url);
|
|
19
30
|
const __dirname = dirname(__filename);
|
|
20
31
|
const PACKAGE_ROOT = join(__dirname, "..");
|
|
21
32
|
|
|
33
|
+
const BASE_DIR = join(homedir(), ".opencode-browser");
|
|
34
|
+
const EXTENSION_DIR = join(BASE_DIR, "extension");
|
|
35
|
+
const BROKER_DST = join(BASE_DIR, "broker.cjs");
|
|
36
|
+
const NATIVE_HOST_DST = join(BASE_DIR, "native-host.cjs");
|
|
37
|
+
const CONFIG_DST = join(BASE_DIR, "config.json");
|
|
38
|
+
|
|
39
|
+
const NATIVE_HOST_NAME = "com.opencode.browser_automation";
|
|
40
|
+
|
|
22
41
|
const COLORS = {
|
|
23
42
|
reset: "\x1b[0m",
|
|
24
43
|
bright: "\x1b[1m",
|
|
25
44
|
red: "\x1b[31m",
|
|
26
45
|
green: "\x1b[32m",
|
|
27
46
|
yellow: "\x1b[33m",
|
|
28
|
-
blue: "\x1b[34m",
|
|
29
47
|
cyan: "\x1b[36m",
|
|
30
48
|
};
|
|
31
49
|
|
|
@@ -61,9 +79,7 @@ const rl = createInterface({
|
|
|
61
79
|
|
|
62
80
|
function ask(question) {
|
|
63
81
|
return new Promise((resolve) => {
|
|
64
|
-
rl.question(question, (answer) =>
|
|
65
|
-
resolve(answer.trim());
|
|
66
|
-
});
|
|
82
|
+
rl.question(question, (answer) => resolve(answer.trim()));
|
|
67
83
|
});
|
|
68
84
|
}
|
|
69
85
|
|
|
@@ -72,323 +88,333 @@ async function confirm(question) {
|
|
|
72
88
|
return answer.toLowerCase() === "y" || answer.toLowerCase() === "yes";
|
|
73
89
|
}
|
|
74
90
|
|
|
91
|
+
function ensureDir(p) {
|
|
92
|
+
mkdirSync(p, { recursive: true });
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
function copyDirRecursive(srcDir, destDir) {
|
|
96
|
+
ensureDir(destDir);
|
|
97
|
+
const entries = readdirSync(srcDir, { recursive: true });
|
|
98
|
+
for (const entry of entries) {
|
|
99
|
+
const srcPath = join(srcDir, entry);
|
|
100
|
+
const destPath = join(destDir, entry);
|
|
101
|
+
|
|
102
|
+
try {
|
|
103
|
+
readdirSync(srcPath);
|
|
104
|
+
ensureDir(destPath);
|
|
105
|
+
} catch {
|
|
106
|
+
ensureDir(dirname(destPath));
|
|
107
|
+
copyFileSync(srcPath, destPath);
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
function getNativeHostDirs(osName) {
|
|
113
|
+
if (osName === "darwin") {
|
|
114
|
+
const base = join(homedir(), "Library", "Application Support");
|
|
115
|
+
return [
|
|
116
|
+
join(base, "Google", "Chrome", "NativeMessagingHosts"),
|
|
117
|
+
join(base, "Chromium", "NativeMessagingHosts"),
|
|
118
|
+
join(base, "BraveSoftware", "Brave-Browser", "NativeMessagingHosts"),
|
|
119
|
+
];
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
// linux
|
|
123
|
+
const base = join(homedir(), ".config");
|
|
124
|
+
return [
|
|
125
|
+
join(base, "google-chrome", "NativeMessagingHosts"),
|
|
126
|
+
join(base, "chromium", "NativeMessagingHosts"),
|
|
127
|
+
join(base, "BraveSoftware", "Brave-Browser", "NativeMessagingHosts"),
|
|
128
|
+
];
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
function nativeHostManifestPath(dir) {
|
|
132
|
+
return join(dir, `${NATIVE_HOST_NAME}.json`);
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
function writeNativeHostManifest(dir, extensionId) {
|
|
136
|
+
ensureDir(dir);
|
|
137
|
+
|
|
138
|
+
const manifest = {
|
|
139
|
+
name: NATIVE_HOST_NAME,
|
|
140
|
+
description: "OpenCode Browser native messaging host",
|
|
141
|
+
path: NATIVE_HOST_DST,
|
|
142
|
+
type: "stdio",
|
|
143
|
+
allowed_origins: [`chrome-extension://${extensionId}/`],
|
|
144
|
+
};
|
|
145
|
+
|
|
146
|
+
writeFileSync(nativeHostManifestPath(dir), JSON.stringify(manifest, null, 2) + "\n");
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
function loadConfig() {
|
|
150
|
+
try {
|
|
151
|
+
if (!existsSync(CONFIG_DST)) return null;
|
|
152
|
+
return JSON.parse(readFileSync(CONFIG_DST, "utf-8"));
|
|
153
|
+
} catch {
|
|
154
|
+
return null;
|
|
155
|
+
}
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
function saveConfig(config) {
|
|
159
|
+
ensureDir(BASE_DIR);
|
|
160
|
+
writeFileSync(CONFIG_DST, JSON.stringify(config, null, 2) + "\n");
|
|
161
|
+
}
|
|
162
|
+
|
|
75
163
|
async function main() {
|
|
76
164
|
const command = process.argv[2];
|
|
77
165
|
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
166
|
+
console.log(`
|
|
167
|
+
${color("cyan", color("bright", "OpenCode Browser v4"))}
|
|
168
|
+
${color("cyan", "Browser automation plugin (native messaging + per-tab ownership)")}
|
|
169
|
+
`);
|
|
170
|
+
|
|
171
|
+
if (command === "install") {
|
|
83
172
|
await install();
|
|
84
|
-
rl.close();
|
|
85
173
|
} else if (command === "uninstall") {
|
|
86
|
-
await showHeader();
|
|
87
174
|
await uninstall();
|
|
88
|
-
rl.close();
|
|
89
175
|
} else if (command === "status") {
|
|
90
|
-
await showHeader();
|
|
91
176
|
await status();
|
|
92
|
-
rl.close();
|
|
93
177
|
} else {
|
|
94
|
-
await showHeader();
|
|
95
178
|
log(`
|
|
96
179
|
${color("bright", "Usage:")}
|
|
97
|
-
npx @different-ai/opencode-browser install
|
|
98
|
-
npx @different-ai/opencode-browser
|
|
99
|
-
npx @different-ai/opencode-browser
|
|
100
|
-
npx @different-ai/opencode-browser serve Run MCP server (internal)
|
|
180
|
+
npx @different-ai/opencode-browser install
|
|
181
|
+
npx @different-ai/opencode-browser status
|
|
182
|
+
npx @different-ai/opencode-browser uninstall
|
|
101
183
|
|
|
102
184
|
${color("bright", "Quick Start:")}
|
|
103
185
|
1. Run: npx @different-ai/opencode-browser install
|
|
104
|
-
2.
|
|
105
|
-
|
|
106
|
-
3. Restart OpenCode
|
|
186
|
+
2. Restart OpenCode
|
|
187
|
+
3. Use: browser_navigate / browser_click / browser_snapshot
|
|
107
188
|
`);
|
|
108
|
-
rl.close();
|
|
109
189
|
}
|
|
110
|
-
}
|
|
111
190
|
|
|
112
|
-
|
|
113
|
-
console.log(`
|
|
114
|
-
${color("cyan", color("bright", "OpenCode Browser v2.1"))}
|
|
115
|
-
${color("cyan", "Browser automation MCP server for OpenCode")}
|
|
116
|
-
`);
|
|
117
|
-
}
|
|
118
|
-
|
|
119
|
-
async function serve() {
|
|
120
|
-
// Launch the MCP server
|
|
121
|
-
const serverPath = join(PACKAGE_ROOT, "src", "mcp-server.ts");
|
|
122
|
-
|
|
123
|
-
// Use bun to run the TypeScript server
|
|
124
|
-
const child = spawn("bun", ["run", serverPath], {
|
|
125
|
-
stdio: "inherit",
|
|
126
|
-
env: process.env,
|
|
127
|
-
});
|
|
128
|
-
|
|
129
|
-
child.on("error", (err) => {
|
|
130
|
-
console.error("[browser-mcp] Failed to start server:", err);
|
|
131
|
-
process.exit(1);
|
|
132
|
-
});
|
|
133
|
-
|
|
134
|
-
child.on("exit", (code) => {
|
|
135
|
-
process.exit(code || 0);
|
|
136
|
-
});
|
|
137
|
-
|
|
138
|
-
// Forward signals to child
|
|
139
|
-
process.on("SIGINT", () => child.kill("SIGINT"));
|
|
140
|
-
process.on("SIGTERM", () => child.kill("SIGTERM"));
|
|
191
|
+
rl.close();
|
|
141
192
|
}
|
|
142
193
|
|
|
143
194
|
async function install() {
|
|
144
195
|
header("Step 1: Check Platform");
|
|
145
196
|
|
|
146
|
-
const
|
|
147
|
-
if (
|
|
148
|
-
error(`Unsupported platform: ${
|
|
197
|
+
const osName = platform();
|
|
198
|
+
if (osName !== "darwin" && osName !== "linux") {
|
|
199
|
+
error(`Unsupported platform: ${osName}`);
|
|
149
200
|
error("OpenCode Browser currently supports macOS and Linux only.");
|
|
150
201
|
process.exit(1);
|
|
151
202
|
}
|
|
152
|
-
success(`Platform: ${
|
|
203
|
+
success(`Platform: ${osName === "darwin" ? "macOS" : "Linux"}`);
|
|
153
204
|
|
|
154
205
|
header("Step 2: Copy Extension Files");
|
|
155
206
|
|
|
156
|
-
|
|
207
|
+
ensureDir(BASE_DIR);
|
|
157
208
|
const srcExtensionDir = join(PACKAGE_ROOT, "extension");
|
|
209
|
+
copyDirRecursive(srcExtensionDir, EXTENSION_DIR);
|
|
210
|
+
success(`Extension files copied to: ${EXTENSION_DIR}`);
|
|
158
211
|
|
|
159
|
-
|
|
212
|
+
header("Step 3: Load & Pin Extension");
|
|
160
213
|
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
const srcPath = join(srcExtensionDir, file);
|
|
164
|
-
const destPath = join(extensionDir, file);
|
|
214
|
+
log(`
|
|
215
|
+
To load the extension:
|
|
165
216
|
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
copyFileSync(srcPath, destPath);
|
|
172
|
-
}
|
|
173
|
-
}
|
|
217
|
+
1. Open ${color("cyan", "chrome://extensions")}
|
|
218
|
+
2. Enable ${color("bright", "Developer mode")}
|
|
219
|
+
3. Click ${color("bright", "Load unpacked")}
|
|
220
|
+
4. Select:
|
|
221
|
+
${color("cyan", EXTENSION_DIR)}
|
|
174
222
|
|
|
175
|
-
|
|
223
|
+
After loading, ${color("bright", "pin the extension")}: open the Extensions menu (puzzle icon) and click the pin.
|
|
224
|
+
`);
|
|
176
225
|
|
|
177
|
-
|
|
226
|
+
await ask(color("bright", "Press Enter when you've loaded and pinned the extension..."));
|
|
227
|
+
|
|
228
|
+
header("Step 4: Get Extension ID");
|
|
178
229
|
|
|
179
230
|
log(`
|
|
180
|
-
|
|
231
|
+
We need the extension ID to register the native messaging host.
|
|
181
232
|
|
|
182
|
-
|
|
233
|
+
Find it at ${color("cyan", "chrome://extensions")}:
|
|
234
|
+
- Locate ${color("bright", "OpenCode Browser Automation")}
|
|
235
|
+
- Click ${color("bright", "Details")}
|
|
236
|
+
- Copy the ${color("bright", "ID")}
|
|
237
|
+
`);
|
|
183
238
|
|
|
184
|
-
|
|
185
|
-
|
|
239
|
+
const extensionId = await ask(color("bright", "Paste Extension ID: "));
|
|
240
|
+
if (!/^[a-p]{32}$/i.test(extensionId)) {
|
|
241
|
+
warn("That doesn't look like a Chrome extension ID (expected 32 chars a-p). Continuing anyway.");
|
|
242
|
+
}
|
|
186
243
|
|
|
187
|
-
|
|
244
|
+
header("Step 5: Install Local Host + Broker");
|
|
188
245
|
|
|
189
|
-
|
|
246
|
+
const brokerSrc = join(PACKAGE_ROOT, "bin", "broker.cjs");
|
|
247
|
+
const nativeHostSrc = join(PACKAGE_ROOT, "bin", "native-host.cjs");
|
|
190
248
|
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
${os === "darwin" ? color("yellow", "Tip: Press Cmd+Shift+G and paste the path above") : ""}
|
|
194
|
-
`);
|
|
249
|
+
copyFileSync(brokerSrc, BROKER_DST);
|
|
250
|
+
copyFileSync(nativeHostSrc, NATIVE_HOST_DST);
|
|
195
251
|
|
|
196
|
-
|
|
252
|
+
try {
|
|
253
|
+
chmodSync(BROKER_DST, 0o755);
|
|
254
|
+
} catch {}
|
|
255
|
+
try {
|
|
256
|
+
chmodSync(NATIVE_HOST_DST, 0o755);
|
|
257
|
+
} catch {}
|
|
197
258
|
|
|
198
|
-
|
|
259
|
+
success(`Installed broker: ${BROKER_DST}`);
|
|
260
|
+
success(`Installed native host: ${NATIVE_HOST_DST}`);
|
|
199
261
|
|
|
200
|
-
|
|
201
|
-
browser: {
|
|
202
|
-
type: "local",
|
|
203
|
-
command: ["bunx", "@different-ai/opencode-browser", "serve"],
|
|
204
|
-
},
|
|
205
|
-
};
|
|
262
|
+
saveConfig({ extensionId, installedAt: new Date().toISOString() });
|
|
206
263
|
|
|
207
|
-
|
|
208
|
-
Add the MCP server to your ${color("cyan", "opencode.json")}:
|
|
264
|
+
header("Step 6: Register Native Messaging Host");
|
|
209
265
|
|
|
210
|
-
|
|
266
|
+
const hostDirs = getNativeHostDirs(osName);
|
|
267
|
+
for (const dir of hostDirs) {
|
|
268
|
+
try {
|
|
269
|
+
writeNativeHostManifest(dir, extensionId);
|
|
270
|
+
success(`Wrote native host manifest: ${nativeHostManifestPath(dir)}`);
|
|
271
|
+
} catch (e) {
|
|
272
|
+
warn(`Could not write native host manifest to: ${dir}`);
|
|
273
|
+
}
|
|
274
|
+
}
|
|
211
275
|
|
|
212
|
-
|
|
213
|
-
${color("bright", JSON.stringify({ mcp: mcpConfig }, null, 2))}
|
|
214
|
-
`);
|
|
276
|
+
header("Step 7: Configure OpenCode");
|
|
215
277
|
|
|
216
|
-
|
|
278
|
+
// OpenCode config discovery (per upstream docs):
|
|
279
|
+
// - $HOME/.opencode.json
|
|
280
|
+
// - $XDG_CONFIG_HOME/opencode/.opencode.json
|
|
281
|
+
// - ./.opencode.json (project-local)
|
|
282
|
+
// We write the project-local config to avoid touching global state.
|
|
283
|
+
const opencodeJsonPath = join(process.cwd(), ".opencode.json");
|
|
217
284
|
|
|
218
|
-
|
|
219
|
-
const shouldUpdate = await confirm(`Found opencode.json. Add MCP server automatically?`);
|
|
285
|
+
const desiredPlugin = "@different-ai/opencode-browser";
|
|
220
286
|
|
|
287
|
+
function normalizePlugins(val) {
|
|
288
|
+
if (Array.isArray(val)) return val.filter((v) => typeof v === "string");
|
|
289
|
+
if (typeof val === "string" && val.trim()) return [val.trim()];
|
|
290
|
+
return [];
|
|
291
|
+
}
|
|
292
|
+
|
|
293
|
+
function removeLegacyMcp(config) {
|
|
294
|
+
if (config.mcp?.browser) {
|
|
295
|
+
delete config.mcp.browser;
|
|
296
|
+
if (Object.keys(config.mcp).length === 0) delete config.mcp;
|
|
297
|
+
warn("Removed old MCP browser config (replaced by plugin)");
|
|
298
|
+
}
|
|
299
|
+
if (config.mcpServers?.browser) {
|
|
300
|
+
delete config.mcpServers.browser;
|
|
301
|
+
if (Object.keys(config.mcpServers).length === 0) delete config.mcpServers;
|
|
302
|
+
warn("Removed old MCP browser config (replaced by plugin)");
|
|
303
|
+
}
|
|
304
|
+
}
|
|
305
|
+
|
|
306
|
+
if (existsSync(opencodeJsonPath)) {
|
|
307
|
+
const shouldUpdate = await confirm("Found .opencode.json. Add plugin automatically?");
|
|
221
308
|
if (shouldUpdate) {
|
|
222
309
|
try {
|
|
223
310
|
const config = JSON.parse(readFileSync(opencodeJsonPath, "utf-8"));
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
if (config.plugin.length === 0) {
|
|
235
|
-
delete config.plugin;
|
|
236
|
-
}
|
|
237
|
-
}
|
|
238
|
-
|
|
311
|
+
|
|
312
|
+
// Make sure plugin is an array.
|
|
313
|
+
config.plugin = normalizePlugins(config.plugin);
|
|
314
|
+
if (!config.plugin.includes(desiredPlugin)) config.plugin.push(desiredPlugin);
|
|
315
|
+
|
|
316
|
+
removeLegacyMcp(config);
|
|
317
|
+
|
|
318
|
+
// Ensure schema is correct if present.
|
|
319
|
+
if (typeof config.$schema !== "string") config.$schema = "https://opencode.ai/config.json";
|
|
320
|
+
|
|
239
321
|
writeFileSync(opencodeJsonPath, JSON.stringify(config, null, 2) + "\n");
|
|
240
|
-
success("Updated opencode.json with
|
|
322
|
+
success("Updated .opencode.json with plugin");
|
|
241
323
|
} catch (e) {
|
|
242
|
-
error(`Failed to update opencode.json: ${e.message}`);
|
|
243
|
-
log("Please add the MCP config manually.");
|
|
324
|
+
error(`Failed to update .opencode.json: ${e.message}`);
|
|
244
325
|
}
|
|
245
326
|
}
|
|
246
327
|
} else {
|
|
247
|
-
const shouldCreate = await confirm(
|
|
248
|
-
|
|
328
|
+
const shouldCreate = await confirm("No .opencode.json found. Create one?");
|
|
249
329
|
if (shouldCreate) {
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
} catch (e) {
|
|
258
|
-
error(`Failed to create opencode.json: ${e.message}`);
|
|
259
|
-
}
|
|
330
|
+
const config = {
|
|
331
|
+
$schema: "https://opencode.ai/config.json",
|
|
332
|
+
theme: "opencode",
|
|
333
|
+
plugin: [desiredPlugin],
|
|
334
|
+
};
|
|
335
|
+
writeFileSync(opencodeJsonPath, JSON.stringify(config, null, 2) + "\n");
|
|
336
|
+
success("Created .opencode.json with plugin");
|
|
260
337
|
}
|
|
261
338
|
}
|
|
262
339
|
|
|
263
|
-
// Clean up old daemon/plugin if present
|
|
264
|
-
header("Step 5: Cleanup (migration)");
|
|
265
|
-
|
|
266
|
-
const oldDaemonPlist = join(homedir(), "Library", "LaunchAgents", "com.opencode.browser-daemon.plist");
|
|
267
|
-
if (existsSync(oldDaemonPlist)) {
|
|
268
|
-
try {
|
|
269
|
-
execSync(`launchctl unload "${oldDaemonPlist}" 2>/dev/null || true`, { stdio: "ignore" });
|
|
270
|
-
unlinkSync(oldDaemonPlist);
|
|
271
|
-
success("Removed old daemon (no longer needed)");
|
|
272
|
-
} catch {
|
|
273
|
-
warn("Could not remove old daemon plist. Remove manually if needed.");
|
|
274
|
-
}
|
|
275
|
-
}
|
|
276
|
-
|
|
277
|
-
// Remove old lock file
|
|
278
|
-
const oldLockFile = join(homedir(), ".opencode-browser", "lock.json");
|
|
279
|
-
if (existsSync(oldLockFile)) {
|
|
280
|
-
try {
|
|
281
|
-
unlinkSync(oldLockFile);
|
|
282
|
-
success("Removed old lock file (not needed with MCP)");
|
|
283
|
-
} catch {}
|
|
284
|
-
}
|
|
285
|
-
|
|
286
|
-
success("Cleanup complete");
|
|
287
|
-
|
|
288
340
|
header("Installation Complete!");
|
|
289
341
|
|
|
290
342
|
log(`
|
|
291
|
-
${color("
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
${color("bright", "
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
4. Browser tools are available to any OpenCode session!
|
|
299
|
-
|
|
300
|
-
${color("bright", "Available tools:")}
|
|
301
|
-
browser_status - Check if browser is connected
|
|
302
|
-
browser_navigate - Go to a URL
|
|
303
|
-
browser_click - Click an element
|
|
304
|
-
browser_type - Type into an input
|
|
305
|
-
browser_screenshot - Capture the page
|
|
306
|
-
browser_snapshot - Get accessibility tree + all links
|
|
307
|
-
browser_get_tabs - List open tabs
|
|
308
|
-
browser_scroll - Scroll the page
|
|
309
|
-
browser_wait - Wait for duration
|
|
310
|
-
browser_execute - Run JavaScript
|
|
311
|
-
|
|
312
|
-
${color("bright", "Benefits of MCP architecture:")}
|
|
313
|
-
- No session conflicts between OpenCode instances
|
|
314
|
-
- Server runs independently of OpenCode process
|
|
315
|
-
- Clean separation of concerns
|
|
316
|
-
|
|
317
|
-
${color("bright", "Test it:")}
|
|
318
|
-
Restart OpenCode and try: ${color("cyan", '"Check browser status"')}
|
|
343
|
+
${color("bright", "What happens now:")}
|
|
344
|
+
- The extension connects to the native host automatically.
|
|
345
|
+
- OpenCode loads the plugin, which talks to the broker.
|
|
346
|
+
- The broker enforces ${color("bright", "per-tab ownership")}. First touch auto-claims.
|
|
347
|
+
|
|
348
|
+
${color("bright", "Try it:")}
|
|
349
|
+
Restart OpenCode and run: ${color("cyan", "browser_get_tabs")}
|
|
319
350
|
`);
|
|
320
351
|
}
|
|
321
352
|
|
|
322
353
|
async function status() {
|
|
323
|
-
header("
|
|
354
|
+
header("Status");
|
|
324
355
|
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
success("WebSocket server is running on port 19222");
|
|
330
|
-
log(result);
|
|
331
|
-
} else {
|
|
332
|
-
warn("WebSocket server not running (starts on demand via MCP)");
|
|
333
|
-
}
|
|
334
|
-
} catch {
|
|
335
|
-
warn("Could not check port status");
|
|
336
|
-
}
|
|
356
|
+
success(`Base dir: ${BASE_DIR}`);
|
|
357
|
+
success(`Extension dir present: ${existsSync(EXTENSION_DIR)}`);
|
|
358
|
+
success(`Broker installed: ${existsSync(BROKER_DST)}`);
|
|
359
|
+
success(`Native host installed: ${existsSync(NATIVE_HOST_DST)}`);
|
|
337
360
|
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
success(`Extension installed at: ${extensionDir}`);
|
|
361
|
+
const cfg = loadConfig();
|
|
362
|
+
if (cfg?.extensionId) {
|
|
363
|
+
success(`Configured extension ID: ${cfg.extensionId}`);
|
|
342
364
|
} else {
|
|
343
|
-
warn("
|
|
365
|
+
warn("No config.json found (run install)");
|
|
344
366
|
}
|
|
345
|
-
}
|
|
346
|
-
|
|
347
|
-
async function uninstall() {
|
|
348
|
-
header("Uninstalling OpenCode Browser");
|
|
349
367
|
|
|
350
|
-
|
|
351
|
-
const
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
success("Removed daemon plist");
|
|
359
|
-
} catch {}
|
|
368
|
+
const osName = platform();
|
|
369
|
+
const hostDirs = getNativeHostDirs(osName);
|
|
370
|
+
let foundAny = false;
|
|
371
|
+
for (const dir of hostDirs) {
|
|
372
|
+
const p = nativeHostManifestPath(dir);
|
|
373
|
+
if (existsSync(p)) {
|
|
374
|
+
foundAny = true;
|
|
375
|
+
success(`Native host manifest: ${p}`);
|
|
360
376
|
}
|
|
361
377
|
}
|
|
378
|
+
if (!foundAny) {
|
|
379
|
+
warn("No native host manifest found. Run: npx @different-ai/opencode-browser install");
|
|
380
|
+
}
|
|
381
|
+
}
|
|
362
382
|
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
os === "darwin"
|
|
366
|
-
? join(homedir(), "Library", "Application Support", "Google", "Chrome", "NativeMessagingHosts")
|
|
367
|
-
: join(homedir(), ".config", "google-chrome", "NativeMessagingHosts");
|
|
383
|
+
async function uninstall() {
|
|
384
|
+
header("Uninstall");
|
|
368
385
|
|
|
369
|
-
const
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
386
|
+
const osName = platform();
|
|
387
|
+
const hostDirs = getNativeHostDirs(osName);
|
|
388
|
+
for (const dir of hostDirs) {
|
|
389
|
+
const p = nativeHostManifestPath(dir);
|
|
390
|
+
if (!existsSync(p)) continue;
|
|
391
|
+
try {
|
|
392
|
+
unlinkSync(p);
|
|
393
|
+
success(`Removed native host manifest: ${p}`);
|
|
394
|
+
} catch {
|
|
395
|
+
warn(`Could not remove: ${p}`);
|
|
396
|
+
}
|
|
373
397
|
}
|
|
374
398
|
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
399
|
+
for (const p of [BROKER_DST, NATIVE_HOST_DST, CONFIG_DST, join(BASE_DIR, "broker.sock")]) {
|
|
400
|
+
if (!existsSync(p)) continue;
|
|
401
|
+
try {
|
|
402
|
+
unlinkSync(p);
|
|
403
|
+
success(`Removed: ${p}`);
|
|
404
|
+
} catch {
|
|
405
|
+
// ignore
|
|
406
|
+
}
|
|
380
407
|
}
|
|
381
408
|
|
|
382
409
|
log(`
|
|
383
|
-
${color("bright", "Note:")}
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
Also remove the "browser" entry from your opencode.json mcp section.
|
|
410
|
+
${color("bright", "Note:")}
|
|
411
|
+
- The unpacked extension folder remains at: ${EXTENSION_DIR}
|
|
412
|
+
- Remove it manually in ${color("cyan", "chrome://extensions")}
|
|
413
|
+
- Remove ${color("bright", "@different-ai/opencode-browser")} from your opencode.json plugin list if desired.
|
|
388
414
|
`);
|
|
389
415
|
}
|
|
390
416
|
|
|
391
417
|
main().catch((e) => {
|
|
392
|
-
error(e.message);
|
|
418
|
+
error(e.message || String(e));
|
|
393
419
|
process.exit(1);
|
|
394
420
|
});
|