@different-ai/opencode-browser 1.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 ADDED
@@ -0,0 +1,109 @@
1
+ # OpenCode Browser
2
+
3
+ Browser automation for [OpenCode](https://github.com/opencode-ai/opencode) via Chrome extension + Native Messaging.
4
+
5
+ **Inspired by Claude in Chrome** - Anthropic's browser extension that lets Claude Code test code directly in the browser and see client-side errors via console logs. This project brings similar capabilities to OpenCode.
6
+
7
+ ## Why?
8
+
9
+ Get access to your fully credentialed chrome instance to perform privileged web operations.
10
+
11
+ Chrome 136+ blocks `--remote-debugging-port` on your default profile for security reasons. This means DevTools-based automation (like Playwright or chrome-devtools-mcp) triggers a security prompt every time.
12
+
13
+ OpenCode Browser bypasses this entirely using Chrome's Native Messaging API - the same approach Claude uses. Your automation works with your existing browser session, logins, and bookmarks. No prompts. No separate profiles.
14
+
15
+ ## Installation
16
+
17
+ ```bash
18
+ npx opencode-browser install
19
+ ```
20
+
21
+ The installer will:
22
+ 1. Copy the extension to `~/.opencode-browser/extension/`
23
+ 2. Open Chrome for you to load the extension
24
+ 3. Register the native messaging host
25
+ 4. Optionally update your `opencode.json`
26
+
27
+ ## Manual Setup
28
+
29
+ If you prefer manual installation:
30
+
31
+ 1. **Load the extension**
32
+ - Go to `chrome://extensions`
33
+ - Enable "Developer mode"
34
+ - Click "Load unpacked" and select `~/.opencode-browser/extension/`
35
+ - Copy the extension ID
36
+
37
+ 2. **Run the installer** to register the native host:
38
+ ```bash
39
+ npx opencode-browser install
40
+ ```
41
+
42
+ 3. **Add to opencode.json**:
43
+ ```json
44
+ {
45
+ "mcp": {
46
+ "browser": {
47
+ "type": "local",
48
+ "command": ["npx", "opencode-browser", "start"],
49
+ "enabled": true
50
+ }
51
+ }
52
+ }
53
+ ```
54
+
55
+ ## Available Tools
56
+
57
+ | Tool | Description |
58
+ |------|-------------|
59
+ | `browser_navigate` | Navigate to a URL |
60
+ | `browser_click` | Click an element by CSS selector |
61
+ | `browser_type` | Type text into an input field |
62
+ | `browser_screenshot` | Capture the visible page |
63
+ | `browser_snapshot` | Get accessibility tree with selectors |
64
+ | `browser_get_tabs` | List all open tabs |
65
+ | `browser_scroll` | Scroll page or element into view |
66
+ | `browser_wait` | Wait for a duration |
67
+ | `browser_execute` | Run JavaScript in page context |
68
+
69
+ ## Architecture
70
+
71
+ ```
72
+ OpenCode ──MCP──> server.js ──Unix Socket──> host.js ──Native Messaging──> Chrome Extension
73
+
74
+
75
+ chrome.tabs
76
+ chrome.scripting
77
+ ```
78
+
79
+ - **server.js** - MCP server that OpenCode connects to
80
+ - **host.js** - Native messaging host launched by Chrome
81
+ - **extension/** - Chrome extension with browser automation tools
82
+
83
+ No DevTools Protocol = No security prompts.
84
+
85
+ ## Uninstall
86
+
87
+ ```bash
88
+ npx opencode-browser uninstall
89
+ ```
90
+
91
+ Then remove the extension from Chrome and delete `~/.opencode-browser/` if desired.
92
+
93
+ ## Logs
94
+
95
+ Logs are written to `~/.opencode-browser/logs/browser-mcp-host.log`
96
+
97
+ ## Platform Support
98
+
99
+ - macOS ✓
100
+ - Linux ✓
101
+ - Windows (not yet supported)
102
+
103
+ ## License
104
+
105
+ MIT
106
+
107
+ ## Credits
108
+
109
+ Inspired by [Claude in Chrome](https://www.anthropic.com/news/claude-in-chrome) by Anthropic.
package/bin/cli.js ADDED
@@ -0,0 +1,316 @@
1
+ #!/usr/bin/env node
2
+ /**
3
+ * OpenCode Browser - CLI Installer
4
+ *
5
+ * Installs the Chrome extension and native messaging host for browser automation.
6
+ */
7
+
8
+ import { createInterface } from "readline";
9
+ import { existsSync, mkdirSync, writeFileSync, readFileSync, copyFileSync, readdirSync } from "fs";
10
+ import { homedir, platform } from "os";
11
+ import { join, dirname } from "path";
12
+ import { fileURLToPath } from "url";
13
+ import { execSync } from "child_process";
14
+
15
+ const __filename = fileURLToPath(import.meta.url);
16
+ const __dirname = dirname(__filename);
17
+ const PACKAGE_ROOT = join(__dirname, "..");
18
+
19
+ const COLORS = {
20
+ reset: "\x1b[0m",
21
+ bright: "\x1b[1m",
22
+ red: "\x1b[31m",
23
+ green: "\x1b[32m",
24
+ yellow: "\x1b[33m",
25
+ blue: "\x1b[34m",
26
+ cyan: "\x1b[36m",
27
+ };
28
+
29
+ function color(c, text) {
30
+ return `${COLORS[c]}${text}${COLORS.reset}`;
31
+ }
32
+
33
+ function log(msg) {
34
+ console.log(msg);
35
+ }
36
+
37
+ function success(msg) {
38
+ console.log(color("green", "✓ " + msg));
39
+ }
40
+
41
+ function warn(msg) {
42
+ console.log(color("yellow", "⚠ " + msg));
43
+ }
44
+
45
+ function error(msg) {
46
+ console.log(color("red", "✗ " + msg));
47
+ }
48
+
49
+ function header(msg) {
50
+ console.log("\n" + color("cyan", color("bright", msg)));
51
+ console.log(color("cyan", "─".repeat(msg.length)));
52
+ }
53
+
54
+ const rl = createInterface({
55
+ input: process.stdin,
56
+ output: process.stdout,
57
+ });
58
+
59
+ function ask(question) {
60
+ return new Promise((resolve) => {
61
+ rl.question(question, (answer) => {
62
+ resolve(answer.trim());
63
+ });
64
+ });
65
+ }
66
+
67
+ async function confirm(question) {
68
+ const answer = await ask(`${question} (y/n): `);
69
+ return answer.toLowerCase() === "y" || answer.toLowerCase() === "yes";
70
+ }
71
+
72
+ async function main() {
73
+ console.log(`
74
+ ${color("cyan", color("bright", "╔═══════════════════════════════════════════════════════════╗"))}
75
+ ${color("cyan", color("bright", "║"))} ${color("bright", "OpenCode Browser")} - Browser Automation for OpenCode ${color("cyan", color("bright", "║"))}
76
+ ${color("cyan", color("bright", "║"))} ${color("cyan", color("bright", "║"))}
77
+ ${color("cyan", color("bright", "║"))} Inspired by Claude in Chrome - browser automation that ${color("cyan", color("bright", "║"))}
78
+ ${color("cyan", color("bright", "║"))} works with your existing logins and bookmarks. ${color("cyan", color("bright", "║"))}
79
+ ${color("cyan", color("bright", "╚═══════════════════════════════════════════════════════════╝"))}
80
+ `);
81
+
82
+ const command = process.argv[2];
83
+
84
+ if (command === "install") {
85
+ await install();
86
+ } else if (command === "uninstall") {
87
+ await uninstall();
88
+ } else {
89
+ log(`
90
+ ${color("bright", "Usage:")}
91
+ npx opencode-browser install Install extension and native host
92
+ npx opencode-browser uninstall Remove native host registration
93
+
94
+ ${color("bright", "After installation:")}
95
+ The MCP server starts automatically when OpenCode connects.
96
+ `);
97
+ }
98
+
99
+ rl.close();
100
+ }
101
+
102
+ async function install() {
103
+ header("Step 1: Check Platform");
104
+
105
+ const os = platform();
106
+ if (os !== "darwin" && os !== "linux") {
107
+ error(`Unsupported platform: ${os}`);
108
+ error("OpenCode Browser currently supports macOS and Linux only.");
109
+ process.exit(1);
110
+ }
111
+ success(`Platform: ${os === "darwin" ? "macOS" : "Linux"}`);
112
+
113
+ header("Step 2: Install Extension Directory");
114
+
115
+ const extensionDir = join(homedir(), ".opencode-browser", "extension");
116
+ const srcExtensionDir = join(PACKAGE_ROOT, "extension");
117
+
118
+ mkdirSync(extensionDir, { recursive: true });
119
+
120
+ const files = readdirSync(srcExtensionDir, { recursive: true });
121
+ for (const file of files) {
122
+ const srcPath = join(srcExtensionDir, file);
123
+ const destPath = join(extensionDir, file);
124
+
125
+ try {
126
+ const stat = readdirSync(srcPath);
127
+ mkdirSync(destPath, { recursive: true });
128
+ } catch {
129
+ mkdirSync(dirname(destPath), { recursive: true });
130
+ copyFileSync(srcPath, destPath);
131
+ }
132
+ }
133
+
134
+ success(`Extension files copied to: ${extensionDir}`);
135
+
136
+ header("Step 3: Load Extension in Chrome");
137
+
138
+ log(`
139
+ To load the extension:
140
+
141
+ 1. Open Chrome and go to: ${color("cyan", "chrome://extensions")}
142
+ 2. Enable ${color("bright", "Developer mode")} (toggle in top right)
143
+ 3. Click ${color("bright", "Load unpacked")}
144
+ 4. Select this folder: ${color("cyan", extensionDir)}
145
+ 5. Copy the ${color("bright", "Extension ID")} shown under the extension name
146
+ (looks like: abcdefghijklmnopqrstuvwxyz123456)
147
+ `);
148
+
149
+ const openChrome = await confirm("Open Chrome extensions page now?");
150
+ if (openChrome) {
151
+ try {
152
+ if (os === "darwin") {
153
+ execSync('open -a "Google Chrome" "chrome://extensions"', { stdio: "ignore" });
154
+ } else {
155
+ execSync('xdg-open "chrome://extensions"', { stdio: "ignore" });
156
+ }
157
+ } catch {}
158
+ }
159
+
160
+ const openFinder = await confirm("Open extension folder in file manager?");
161
+ if (openFinder) {
162
+ try {
163
+ if (os === "darwin") {
164
+ execSync(`open "${extensionDir}"`, { stdio: "ignore" });
165
+ } else {
166
+ execSync(`xdg-open "${extensionDir}"`, { stdio: "ignore" });
167
+ }
168
+ } catch {}
169
+ }
170
+
171
+ log("");
172
+ const extensionId = await ask(color("bright", "Enter your Extension ID: "));
173
+
174
+ if (!extensionId) {
175
+ error("Extension ID is required");
176
+ process.exit(1);
177
+ }
178
+
179
+ if (!/^[a-z]{32}$/.test(extensionId)) {
180
+ warn("Extension ID format looks unusual (expected 32 lowercase letters)");
181
+ const proceed = await confirm("Continue anyway?");
182
+ if (!proceed) process.exit(1);
183
+ }
184
+
185
+ header("Step 4: Register Native Messaging Host");
186
+
187
+ const nativeHostDir = os === "darwin"
188
+ ? join(homedir(), "Library", "Application Support", "Google", "Chrome", "NativeMessagingHosts")
189
+ : join(homedir(), ".config", "google-chrome", "NativeMessagingHosts");
190
+
191
+ mkdirSync(nativeHostDir, { recursive: true });
192
+
193
+ const nodePath = process.execPath;
194
+ const hostScriptPath = join(PACKAGE_ROOT, "src", "host.js");
195
+
196
+ const wrapperDir = join(homedir(), ".opencode-browser");
197
+ const wrapperPath = join(wrapperDir, "host-wrapper.sh");
198
+
199
+ writeFileSync(wrapperPath, `#!/bin/bash
200
+ exec "${nodePath}" "${hostScriptPath}" "$@"
201
+ `, { mode: 0o755 });
202
+
203
+ const manifest = {
204
+ name: "com.opencode.browser_automation",
205
+ description: "OpenCode Browser Automation Native Messaging Host",
206
+ path: wrapperPath,
207
+ type: "stdio",
208
+ allowed_origins: [`chrome-extension://${extensionId}/`],
209
+ };
210
+
211
+ const manifestPath = join(nativeHostDir, "com.opencode.browser_automation.json");
212
+ writeFileSync(manifestPath, JSON.stringify(manifest, null, 2));
213
+
214
+ success(`Native host registered at: ${manifestPath}`);
215
+
216
+ const logsDir = join(homedir(), ".opencode-browser", "logs");
217
+ mkdirSync(logsDir, { recursive: true });
218
+
219
+ header("Step 5: Configure OpenCode");
220
+
221
+ const serverPath = join(PACKAGE_ROOT, "src", "server.js");
222
+ const mcpConfig = {
223
+ browser: {
224
+ type: "local",
225
+ command: ["node", serverPath],
226
+ enabled: true,
227
+ },
228
+ };
229
+
230
+ log(`
231
+ Add this to your ${color("cyan", "opencode.json")} under "mcp":
232
+
233
+ ${color("bright", JSON.stringify(mcpConfig, null, 2))}
234
+ `);
235
+
236
+ const opencodeJsonPath = join(process.cwd(), "opencode.json");
237
+ let shouldUpdateConfig = false;
238
+
239
+ if (existsSync(opencodeJsonPath)) {
240
+ shouldUpdateConfig = await confirm(`Found opencode.json in current directory. Add browser config automatically?`);
241
+
242
+ if (shouldUpdateConfig) {
243
+ try {
244
+ const config = JSON.parse(readFileSync(opencodeJsonPath, "utf-8"));
245
+ config.mcp = config.mcp || {};
246
+ config.mcp.browser = mcpConfig.browser;
247
+ writeFileSync(opencodeJsonPath, JSON.stringify(config, null, 2) + "\n");
248
+ success("Updated opencode.json with browser MCP config");
249
+ } catch (e) {
250
+ error(`Failed to update opencode.json: ${e.message}`);
251
+ log("Please add the config manually.");
252
+ }
253
+ }
254
+ } else {
255
+ log(`No opencode.json found in current directory.`);
256
+ log(`Add the config above to your project's opencode.json manually.`);
257
+ }
258
+
259
+ header("Installation Complete!");
260
+
261
+ log(`
262
+ ${color("green", "✓")} Extension installed at: ${extensionDir}
263
+ ${color("green", "✓")} Native host registered
264
+ ${shouldUpdateConfig ? color("green", "✓") + " opencode.json updated" : color("yellow", "○") + " Remember to update opencode.json"}
265
+
266
+ ${color("bright", "Next steps:")}
267
+ 1. ${color("cyan", "Restart Chrome")} (close all windows and reopen)
268
+ 2. Click the extension icon to verify connection
269
+ 3. Restart OpenCode to load the new MCP server
270
+
271
+ ${color("bright", "Available tools:")}
272
+ browser_navigate - Go to a URL
273
+ browser_click - Click an element
274
+ browser_type - Type into an input
275
+ browser_screenshot - Capture the page
276
+ browser_snapshot - Get accessibility tree
277
+ browser_get_tabs - List open tabs
278
+ browser_scroll - Scroll the page
279
+ browser_wait - Wait for duration
280
+ browser_execute - Run JavaScript
281
+
282
+ ${color("bright", "Logs:")} ~/.opencode-browser/logs/
283
+ `);
284
+ }
285
+
286
+ async function uninstall() {
287
+ header("Uninstalling OpenCode Browser");
288
+
289
+ const os = platform();
290
+ const nativeHostDir = os === "darwin"
291
+ ? join(homedir(), "Library", "Application Support", "Google", "Chrome", "NativeMessagingHosts")
292
+ : join(homedir(), ".config", "google-chrome", "NativeMessagingHosts");
293
+
294
+ const manifestPath = join(nativeHostDir, "com.opencode.browser_automation.json");
295
+
296
+ if (existsSync(manifestPath)) {
297
+ const { unlinkSync } = await import("fs");
298
+ unlinkSync(manifestPath);
299
+ success("Removed native host registration");
300
+ } else {
301
+ warn("Native host manifest not found");
302
+ }
303
+
304
+ log(`
305
+ ${color("bright", "Note:")} The extension files at ~/.opencode-browser/ were not removed.
306
+ Remove them manually if needed:
307
+ rm -rf ~/.opencode-browser/
308
+
309
+ Also remove the "browser" entry from your opencode.json.
310
+ `);
311
+ }
312
+
313
+ main().catch((e) => {
314
+ error(e.message);
315
+ process.exit(1);
316
+ });