@different-ai/opencode-browser 2.1.0 → 4.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +43 -93
- package/bin/broker.cjs +290 -0
- package/bin/cli.js +231 -186
- package/bin/native-host.cjs +136 -0
- package/extension/background.js +240 -174
- package/extension/manifest.json +3 -2
- package/package.json +8 -5
- package/src/plugin.ts +226 -623
package/bin/cli.js
CHANGED
|
@@ -1,29 +1,49 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
/**
|
|
3
|
-
* OpenCode Browser - CLI
|
|
3
|
+
* OpenCode Browser - CLI
|
|
4
4
|
*
|
|
5
|
-
*
|
|
6
|
-
*
|
|
5
|
+
* Architecture (v4):
|
|
6
|
+
* OpenCode Plugin <-> Local Broker (unix socket) <-> Native Messaging Host <-> Chrome Extension
|
|
7
|
+
*
|
|
8
|
+
* Commands:
|
|
9
|
+
* install - Install extension + native host
|
|
10
|
+
* uninstall - Remove native host registration
|
|
11
|
+
* status - Show installation status
|
|
7
12
|
*/
|
|
8
13
|
|
|
9
|
-
import {
|
|
14
|
+
import {
|
|
15
|
+
existsSync,
|
|
16
|
+
mkdirSync,
|
|
17
|
+
writeFileSync,
|
|
18
|
+
readFileSync,
|
|
19
|
+
copyFileSync,
|
|
20
|
+
readdirSync,
|
|
21
|
+
unlinkSync,
|
|
22
|
+
chmodSync,
|
|
23
|
+
} from "fs";
|
|
10
24
|
import { homedir, platform } from "os";
|
|
11
25
|
import { join, dirname } from "path";
|
|
12
26
|
import { fileURLToPath } from "url";
|
|
13
|
-
import { execSync } from "child_process";
|
|
14
27
|
import { createInterface } from "readline";
|
|
15
28
|
|
|
16
29
|
const __filename = fileURLToPath(import.meta.url);
|
|
17
30
|
const __dirname = dirname(__filename);
|
|
18
31
|
const PACKAGE_ROOT = join(__dirname, "..");
|
|
19
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
|
+
|
|
20
41
|
const COLORS = {
|
|
21
42
|
reset: "\x1b[0m",
|
|
22
43
|
bright: "\x1b[1m",
|
|
23
44
|
red: "\x1b[31m",
|
|
24
45
|
green: "\x1b[32m",
|
|
25
46
|
yellow: "\x1b[33m",
|
|
26
|
-
blue: "\x1b[34m",
|
|
27
47
|
cyan: "\x1b[36m",
|
|
28
48
|
};
|
|
29
49
|
|
|
@@ -59,9 +79,7 @@ const rl = createInterface({
|
|
|
59
79
|
|
|
60
80
|
function ask(question) {
|
|
61
81
|
return new Promise((resolve) => {
|
|
62
|
-
rl.question(question, (answer) =>
|
|
63
|
-
resolve(answer.trim());
|
|
64
|
-
});
|
|
82
|
+
rl.question(question, (answer) => resolve(answer.trim()));
|
|
65
83
|
});
|
|
66
84
|
}
|
|
67
85
|
|
|
@@ -70,14 +88,86 @@ async function confirm(question) {
|
|
|
70
88
|
return answer.toLowerCase() === "y" || answer.toLowerCase() === "yes";
|
|
71
89
|
}
|
|
72
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
|
+
|
|
73
163
|
async function main() {
|
|
164
|
+
const command = process.argv[2];
|
|
165
|
+
|
|
74
166
|
console.log(`
|
|
75
|
-
${color("cyan", color("bright", "OpenCode Browser
|
|
76
|
-
${color("cyan", "Browser automation
|
|
167
|
+
${color("cyan", color("bright", "OpenCode Browser v4"))}
|
|
168
|
+
${color("cyan", "Browser automation plugin (native messaging + per-tab ownership)")}
|
|
77
169
|
`);
|
|
78
170
|
|
|
79
|
-
const command = process.argv[2];
|
|
80
|
-
|
|
81
171
|
if (command === "install") {
|
|
82
172
|
await install();
|
|
83
173
|
} else if (command === "uninstall") {
|
|
@@ -87,13 +177,14 @@ ${color("cyan", "Browser automation for OpenCode")}
|
|
|
87
177
|
} else {
|
|
88
178
|
log(`
|
|
89
179
|
${color("bright", "Usage:")}
|
|
90
|
-
npx @different-ai/opencode-browser install
|
|
91
|
-
npx @different-ai/opencode-browser
|
|
92
|
-
npx @different-ai/opencode-browser
|
|
93
|
-
|
|
94
|
-
${color("bright", "
|
|
95
|
-
|
|
96
|
-
|
|
180
|
+
npx @different-ai/opencode-browser install
|
|
181
|
+
npx @different-ai/opencode-browser status
|
|
182
|
+
npx @different-ai/opencode-browser uninstall
|
|
183
|
+
|
|
184
|
+
${color("bright", "Quick Start:")}
|
|
185
|
+
1. Run: npx @different-ai/opencode-browser install
|
|
186
|
+
2. Restart OpenCode
|
|
187
|
+
3. Use: browser_navigate / browser_click / browser_snapshot
|
|
97
188
|
`);
|
|
98
189
|
}
|
|
99
190
|
|
|
@@ -103,246 +194,200 @@ ${color("bright", "v2.0 Changes:")}
|
|
|
103
194
|
async function install() {
|
|
104
195
|
header("Step 1: Check Platform");
|
|
105
196
|
|
|
106
|
-
const
|
|
107
|
-
if (
|
|
108
|
-
error(`Unsupported platform: ${
|
|
197
|
+
const osName = platform();
|
|
198
|
+
if (osName !== "darwin" && osName !== "linux") {
|
|
199
|
+
error(`Unsupported platform: ${osName}`);
|
|
109
200
|
error("OpenCode Browser currently supports macOS and Linux only.");
|
|
110
201
|
process.exit(1);
|
|
111
202
|
}
|
|
112
|
-
success(`Platform: ${
|
|
203
|
+
success(`Platform: ${osName === "darwin" ? "macOS" : "Linux"}`);
|
|
113
204
|
|
|
114
205
|
header("Step 2: Copy Extension Files");
|
|
115
206
|
|
|
116
|
-
|
|
207
|
+
ensureDir(BASE_DIR);
|
|
117
208
|
const srcExtensionDir = join(PACKAGE_ROOT, "extension");
|
|
209
|
+
copyDirRecursive(srcExtensionDir, EXTENSION_DIR);
|
|
210
|
+
success(`Extension files copied to: ${EXTENSION_DIR}`);
|
|
118
211
|
|
|
119
|
-
|
|
212
|
+
header("Step 3: Load & Pin Extension");
|
|
120
213
|
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
const srcPath = join(srcExtensionDir, file);
|
|
124
|
-
const destPath = join(extensionDir, file);
|
|
214
|
+
log(`
|
|
215
|
+
To load the extension:
|
|
125
216
|
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
copyFileSync(srcPath, destPath);
|
|
132
|
-
}
|
|
133
|
-
}
|
|
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)}
|
|
134
222
|
|
|
135
|
-
|
|
223
|
+
After loading, ${color("bright", "pin the extension")}: open the Extensions menu (puzzle icon) and click the pin.
|
|
224
|
+
`);
|
|
225
|
+
|
|
226
|
+
await ask(color("bright", "Press Enter when you've loaded and pinned the extension..."));
|
|
136
227
|
|
|
137
|
-
header("Step
|
|
228
|
+
header("Step 4: Get Extension ID");
|
|
138
229
|
|
|
139
230
|
log(`
|
|
140
|
-
|
|
231
|
+
We need the extension ID to register the native messaging host.
|
|
141
232
|
|
|
142
|
-
|
|
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
|
+
`);
|
|
143
238
|
|
|
144
|
-
|
|
145
|
-
|
|
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
|
+
}
|
|
146
243
|
|
|
147
|
-
|
|
244
|
+
header("Step 5: Install Local Host + Broker");
|
|
148
245
|
|
|
149
|
-
|
|
246
|
+
const brokerSrc = join(PACKAGE_ROOT, "bin", "broker.cjs");
|
|
247
|
+
const nativeHostSrc = join(PACKAGE_ROOT, "bin", "native-host.cjs");
|
|
150
248
|
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
${os === "darwin" ? color("yellow", "Tip: Press Cmd+Shift+G and paste the path above") : ""}
|
|
154
|
-
`);
|
|
249
|
+
copyFileSync(brokerSrc, BROKER_DST);
|
|
250
|
+
copyFileSync(nativeHostSrc, NATIVE_HOST_DST);
|
|
155
251
|
|
|
156
|
-
|
|
252
|
+
try {
|
|
253
|
+
chmodSync(BROKER_DST, 0o755);
|
|
254
|
+
} catch {}
|
|
255
|
+
try {
|
|
256
|
+
chmodSync(NATIVE_HOST_DST, 0o755);
|
|
257
|
+
} catch {}
|
|
157
258
|
|
|
158
|
-
|
|
259
|
+
success(`Installed broker: ${BROKER_DST}`);
|
|
260
|
+
success(`Installed native host: ${NATIVE_HOST_DST}`);
|
|
159
261
|
|
|
160
|
-
|
|
161
|
-
"$schema": "https://opencode.ai/config.json",
|
|
162
|
-
"plugin": ["@different-ai/opencode-browser"]
|
|
163
|
-
}`;
|
|
262
|
+
saveConfig({ extensionId, installedAt: new Date().toISOString() });
|
|
164
263
|
|
|
165
|
-
|
|
166
|
-
Add the plugin to your ${color("cyan", "opencode.json")}:
|
|
264
|
+
header("Step 6: Register Native Messaging Host");
|
|
167
265
|
|
|
168
|
-
|
|
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
|
+
}
|
|
169
275
|
|
|
170
|
-
|
|
171
|
-
${color("bright", '"plugin": ["@different-ai/opencode-browser"]')}
|
|
172
|
-
`);
|
|
276
|
+
header("Step 7: Configure OpenCode");
|
|
173
277
|
|
|
174
278
|
const opencodeJsonPath = join(process.cwd(), "opencode.json");
|
|
175
279
|
|
|
176
|
-
|
|
177
|
-
const shouldUpdate = await confirm(`Found opencode.json. Add plugin automatically?`);
|
|
280
|
+
const desiredPlugin = "@different-ai/opencode-browser";
|
|
178
281
|
|
|
282
|
+
if (existsSync(opencodeJsonPath)) {
|
|
283
|
+
const shouldUpdate = await confirm("Found opencode.json. Add plugin automatically?");
|
|
179
284
|
if (shouldUpdate) {
|
|
180
285
|
try {
|
|
181
286
|
const config = JSON.parse(readFileSync(opencodeJsonPath, "utf-8"));
|
|
182
287
|
config.plugin = config.plugin || [];
|
|
183
|
-
if (!config.plugin
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
// Remove
|
|
288
|
+
if (!Array.isArray(config.plugin)) config.plugin = [];
|
|
289
|
+
if (!config.plugin.includes(desiredPlugin)) config.plugin.push(desiredPlugin);
|
|
290
|
+
|
|
291
|
+
// Remove MCP config if present
|
|
187
292
|
if (config.mcp?.browser) {
|
|
188
293
|
delete config.mcp.browser;
|
|
189
|
-
if (Object.keys(config.mcp).length === 0)
|
|
190
|
-
delete config.mcp;
|
|
191
|
-
}
|
|
294
|
+
if (Object.keys(config.mcp).length === 0) delete config.mcp;
|
|
192
295
|
warn("Removed old MCP browser config (replaced by plugin)");
|
|
193
296
|
}
|
|
297
|
+
|
|
194
298
|
writeFileSync(opencodeJsonPath, JSON.stringify(config, null, 2) + "\n");
|
|
195
299
|
success("Updated opencode.json with plugin");
|
|
196
300
|
} catch (e) {
|
|
197
301
|
error(`Failed to update opencode.json: ${e.message}`);
|
|
198
|
-
log("Please add the plugin manually.");
|
|
199
302
|
}
|
|
200
303
|
}
|
|
201
304
|
} else {
|
|
202
|
-
const shouldCreate = await confirm(
|
|
203
|
-
|
|
305
|
+
const shouldCreate = await confirm("No opencode.json found. Create one?");
|
|
204
306
|
if (shouldCreate) {
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
plugin: ["@different-ai/opencode-browser"],
|
|
209
|
-
};
|
|
210
|
-
writeFileSync(opencodeJsonPath, JSON.stringify(config, null, 2) + "\n");
|
|
211
|
-
success("Created opencode.json with plugin");
|
|
212
|
-
} catch (e) {
|
|
213
|
-
error(`Failed to create opencode.json: ${e.message}`);
|
|
214
|
-
}
|
|
215
|
-
}
|
|
216
|
-
}
|
|
217
|
-
|
|
218
|
-
// Clean up old daemon if present
|
|
219
|
-
header("Step 5: Cleanup (v1.x migration)");
|
|
220
|
-
|
|
221
|
-
const oldDaemonPlist = join(homedir(), "Library", "LaunchAgents", "com.opencode.browser-daemon.plist");
|
|
222
|
-
if (existsSync(oldDaemonPlist)) {
|
|
223
|
-
try {
|
|
224
|
-
execSync(`launchctl unload "${oldDaemonPlist}" 2>/dev/null || true`, { stdio: "ignore" });
|
|
225
|
-
unlinkSync(oldDaemonPlist);
|
|
226
|
-
success("Removed old daemon (no longer needed in v2.0)");
|
|
227
|
-
} catch {
|
|
228
|
-
warn("Could not remove old daemon plist. Remove manually if needed.");
|
|
307
|
+
const config = { $schema: "https://opencode.ai/config.json", plugin: [desiredPlugin] };
|
|
308
|
+
writeFileSync(opencodeJsonPath, JSON.stringify(config, null, 2) + "\n");
|
|
309
|
+
success("Created opencode.json with plugin");
|
|
229
310
|
}
|
|
230
|
-
} else {
|
|
231
|
-
success("No old daemon to clean up");
|
|
232
311
|
}
|
|
233
312
|
|
|
234
313
|
header("Installation Complete!");
|
|
235
314
|
|
|
236
315
|
log(`
|
|
237
|
-
${color("
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
${color("bright", "
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
4. Browser tools are available!
|
|
245
|
-
|
|
246
|
-
${color("bright", "Available tools:")}
|
|
247
|
-
browser_status - Check if browser is available
|
|
248
|
-
browser_kill_session - Take over from another session
|
|
249
|
-
browser_navigate - Go to a URL
|
|
250
|
-
browser_click - Click an element
|
|
251
|
-
browser_type - Type into an input
|
|
252
|
-
browser_screenshot - Capture the page
|
|
253
|
-
browser_snapshot - Get accessibility tree
|
|
254
|
-
browser_get_tabs - List open tabs
|
|
255
|
-
browser_scroll - Scroll the page
|
|
256
|
-
browser_wait - Wait for duration
|
|
257
|
-
browser_execute - Run JavaScript
|
|
258
|
-
|
|
259
|
-
${color("bright", "Multi-session:")}
|
|
260
|
-
Only one OpenCode session can use browser at a time.
|
|
261
|
-
Use browser_status to check, browser_kill_session to take over.
|
|
262
|
-
|
|
263
|
-
${color("bright", "Test it:")}
|
|
264
|
-
Restart OpenCode and try: ${color("cyan", '"Check browser status"')}
|
|
316
|
+
${color("bright", "What happens now:")}
|
|
317
|
+
- The extension connects to the native host automatically.
|
|
318
|
+
- OpenCode loads the plugin, which talks to the broker.
|
|
319
|
+
- The broker enforces ${color("bright", "per-tab ownership")}. First touch auto-claims.
|
|
320
|
+
|
|
321
|
+
${color("bright", "Try it:")}
|
|
322
|
+
Restart OpenCode and run: ${color("cyan", "browser_get_tabs")}
|
|
265
323
|
`);
|
|
266
324
|
}
|
|
267
325
|
|
|
268
326
|
async function status() {
|
|
269
|
-
header("
|
|
327
|
+
header("Status");
|
|
270
328
|
|
|
271
|
-
|
|
329
|
+
success(`Base dir: ${BASE_DIR}`);
|
|
330
|
+
success(`Extension dir present: ${existsSync(EXTENSION_DIR)}`);
|
|
331
|
+
success(`Broker installed: ${existsSync(BROKER_DST)}`);
|
|
332
|
+
success(`Native host installed: ${existsSync(NATIVE_HOST_DST)}`);
|
|
272
333
|
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
334
|
+
const cfg = loadConfig();
|
|
335
|
+
if (cfg?.extensionId) {
|
|
336
|
+
success(`Configured extension ID: ${cfg.extensionId}`);
|
|
337
|
+
} else {
|
|
338
|
+
warn("No config.json found (run install)");
|
|
276
339
|
}
|
|
277
340
|
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
Working directory: ${lock.cwd}
|
|
287
|
-
`);
|
|
288
|
-
|
|
289
|
-
// Check if process is alive
|
|
290
|
-
try {
|
|
291
|
-
process.kill(lock.pid, 0);
|
|
292
|
-
warn(`Process ${lock.pid} is running. Browser is locked.`);
|
|
293
|
-
} catch {
|
|
294
|
-
success(`Process ${lock.pid} is dead. Lock is stale and will be auto-cleaned.`);
|
|
341
|
+
const osName = platform();
|
|
342
|
+
const hostDirs = getNativeHostDirs(osName);
|
|
343
|
+
let foundAny = false;
|
|
344
|
+
for (const dir of hostDirs) {
|
|
345
|
+
const p = nativeHostManifestPath(dir);
|
|
346
|
+
if (existsSync(p)) {
|
|
347
|
+
foundAny = true;
|
|
348
|
+
success(`Native host manifest: ${p}`);
|
|
295
349
|
}
|
|
296
|
-
}
|
|
297
|
-
|
|
350
|
+
}
|
|
351
|
+
if (!foundAny) {
|
|
352
|
+
warn("No native host manifest found. Run: npx @different-ai/opencode-browser install");
|
|
298
353
|
}
|
|
299
354
|
}
|
|
300
355
|
|
|
301
356
|
async function uninstall() {
|
|
302
|
-
header("
|
|
357
|
+
header("Uninstall");
|
|
303
358
|
|
|
304
|
-
|
|
305
|
-
const
|
|
306
|
-
|
|
307
|
-
const
|
|
308
|
-
if (existsSync(
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
359
|
+
const osName = platform();
|
|
360
|
+
const hostDirs = getNativeHostDirs(osName);
|
|
361
|
+
for (const dir of hostDirs) {
|
|
362
|
+
const p = nativeHostManifestPath(dir);
|
|
363
|
+
if (!existsSync(p)) continue;
|
|
364
|
+
try {
|
|
365
|
+
unlinkSync(p);
|
|
366
|
+
success(`Removed native host manifest: ${p}`);
|
|
367
|
+
} catch {
|
|
368
|
+
warn(`Could not remove: ${p}`);
|
|
314
369
|
}
|
|
315
370
|
}
|
|
316
371
|
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
:
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
unlinkSync(manifestPath);
|
|
326
|
-
success("Removed native host registration");
|
|
327
|
-
}
|
|
328
|
-
|
|
329
|
-
// Remove lock file
|
|
330
|
-
const lockFile = join(homedir(), ".opencode-browser", "lock.json");
|
|
331
|
-
if (existsSync(lockFile)) {
|
|
332
|
-
unlinkSync(lockFile);
|
|
333
|
-
success("Removed lock file");
|
|
372
|
+
for (const p of [BROKER_DST, NATIVE_HOST_DST, CONFIG_DST, join(BASE_DIR, "broker.sock")]) {
|
|
373
|
+
if (!existsSync(p)) continue;
|
|
374
|
+
try {
|
|
375
|
+
unlinkSync(p);
|
|
376
|
+
success(`Removed: ${p}`);
|
|
377
|
+
} catch {
|
|
378
|
+
// ignore
|
|
379
|
+
}
|
|
334
380
|
}
|
|
335
381
|
|
|
336
382
|
log(`
|
|
337
|
-
${color("bright", "Note:")}
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
Also remove "@different-ai/opencode-browser" from your opencode.json plugin array.
|
|
383
|
+
${color("bright", "Note:")}
|
|
384
|
+
- The unpacked extension folder remains at: ${EXTENSION_DIR}
|
|
385
|
+
- Remove it manually in ${color("cyan", "chrome://extensions")}
|
|
386
|
+
- Remove ${color("bright", "@different-ai/opencode-browser")} from your opencode.json plugin list if desired.
|
|
342
387
|
`);
|
|
343
388
|
}
|
|
344
389
|
|
|
345
390
|
main().catch((e) => {
|
|
346
|
-
error(e.message);
|
|
391
|
+
error(e.message || String(e));
|
|
347
392
|
process.exit(1);
|
|
348
393
|
});
|
|
@@ -0,0 +1,136 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
"use strict";
|
|
3
|
+
|
|
4
|
+
// Chrome Native Messaging host for OpenCode Browser.
|
|
5
|
+
// Speaks length-prefixed JSON over stdin/stdout and forwards messages to the local broker over a unix socket.
|
|
6
|
+
|
|
7
|
+
const net = require("net");
|
|
8
|
+
const fs = require("fs");
|
|
9
|
+
const os = require("os");
|
|
10
|
+
const path = require("path");
|
|
11
|
+
const { spawn } = require("child_process");
|
|
12
|
+
|
|
13
|
+
const BASE_DIR = path.join(os.homedir(), ".opencode-browser");
|
|
14
|
+
const SOCKET_PATH = path.join(BASE_DIR, "broker.sock");
|
|
15
|
+
const BROKER_PATH = path.join(BASE_DIR, "broker.cjs");
|
|
16
|
+
|
|
17
|
+
fs.mkdirSync(BASE_DIR, { recursive: true });
|
|
18
|
+
|
|
19
|
+
function createJsonLineParser(onMessage) {
|
|
20
|
+
let buffer = "";
|
|
21
|
+
return (chunk) => {
|
|
22
|
+
buffer += chunk.toString("utf8");
|
|
23
|
+
while (true) {
|
|
24
|
+
const idx = buffer.indexOf("\n");
|
|
25
|
+
if (idx === -1) return;
|
|
26
|
+
const line = buffer.slice(0, idx);
|
|
27
|
+
buffer = buffer.slice(idx + 1);
|
|
28
|
+
if (!line.trim()) continue;
|
|
29
|
+
try {
|
|
30
|
+
onMessage(JSON.parse(line));
|
|
31
|
+
} catch {
|
|
32
|
+
// ignore
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
};
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
function writeJsonLine(socket, msg) {
|
|
39
|
+
socket.write(JSON.stringify(msg) + "\n");
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
function maybeStartBroker() {
|
|
43
|
+
try {
|
|
44
|
+
if (!fs.existsSync(BROKER_PATH)) return;
|
|
45
|
+
const child = spawn(process.execPath, [BROKER_PATH], { detached: true, stdio: "ignore" });
|
|
46
|
+
child.unref();
|
|
47
|
+
} catch {
|
|
48
|
+
// ignore
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
async function connectToBroker() {
|
|
53
|
+
return await new Promise((resolve, reject) => {
|
|
54
|
+
const socket = net.createConnection(SOCKET_PATH);
|
|
55
|
+
socket.once("connect", () => resolve(socket));
|
|
56
|
+
socket.once("error", (err) => reject(err));
|
|
57
|
+
});
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
async function ensureBroker() {
|
|
61
|
+
try {
|
|
62
|
+
return await connectToBroker();
|
|
63
|
+
} catch {
|
|
64
|
+
maybeStartBroker();
|
|
65
|
+
for (let i = 0; i < 50; i++) {
|
|
66
|
+
await new Promise((r) => setTimeout(r, 100));
|
|
67
|
+
try {
|
|
68
|
+
return await connectToBroker();
|
|
69
|
+
} catch {}
|
|
70
|
+
}
|
|
71
|
+
throw new Error("Could not connect to broker");
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
// --- Native messaging framing ---
|
|
76
|
+
let stdinBuffer = Buffer.alloc(0);
|
|
77
|
+
|
|
78
|
+
function writeNativeMessage(obj) {
|
|
79
|
+
try {
|
|
80
|
+
const payload = Buffer.from(JSON.stringify(obj), "utf8");
|
|
81
|
+
const header = Buffer.alloc(4);
|
|
82
|
+
header.writeUInt32LE(payload.length, 0);
|
|
83
|
+
process.stdout.write(Buffer.concat([header, payload]));
|
|
84
|
+
} catch (e) {
|
|
85
|
+
console.error("[native-host] write error", e);
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
function onStdinData(chunk, onMessage) {
|
|
90
|
+
stdinBuffer = Buffer.concat([stdinBuffer, chunk]);
|
|
91
|
+
while (stdinBuffer.length >= 4) {
|
|
92
|
+
const len = stdinBuffer.readUInt32LE(0);
|
|
93
|
+
if (stdinBuffer.length < 4 + len) return;
|
|
94
|
+
const body = stdinBuffer.slice(4, 4 + len);
|
|
95
|
+
stdinBuffer = stdinBuffer.slice(4 + len);
|
|
96
|
+
try {
|
|
97
|
+
onMessage(JSON.parse(body.toString("utf8")));
|
|
98
|
+
} catch {
|
|
99
|
+
// ignore
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
(async () => {
|
|
105
|
+
const broker = await ensureBroker();
|
|
106
|
+
broker.setNoDelay(true);
|
|
107
|
+
broker.on("data", createJsonLineParser((msg) => {
|
|
108
|
+
if (msg && msg.type === "to_extension" && msg.message) {
|
|
109
|
+
writeNativeMessage(msg.message);
|
|
110
|
+
}
|
|
111
|
+
}));
|
|
112
|
+
|
|
113
|
+
broker.on("close", () => {
|
|
114
|
+
process.exit(0);
|
|
115
|
+
});
|
|
116
|
+
|
|
117
|
+
broker.on("error", () => {
|
|
118
|
+
process.exit(1);
|
|
119
|
+
});
|
|
120
|
+
|
|
121
|
+
writeJsonLine(broker, { type: "hello", role: "native-host" });
|
|
122
|
+
|
|
123
|
+
process.stdin.on("data", (chunk) =>
|
|
124
|
+
onStdinData(chunk, (message) => {
|
|
125
|
+
// Forward extension-origin messages to broker.
|
|
126
|
+
writeJsonLine(broker, { type: "from_extension", message });
|
|
127
|
+
})
|
|
128
|
+
);
|
|
129
|
+
|
|
130
|
+
process.stdin.on("end", () => {
|
|
131
|
+
try {
|
|
132
|
+
broker.end();
|
|
133
|
+
} catch {}
|
|
134
|
+
process.exit(0);
|
|
135
|
+
});
|
|
136
|
+
})();
|