@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 +109 -0
- package/bin/cli.js +316 -0
- package/extension/background.js +456 -0
- package/extension/icons/icon128.png +0 -0
- package/extension/icons/icon16.png +0 -0
- package/extension/icons/icon48.png +0 -0
- package/extension/manifest.json +34 -0
- package/package.json +35 -0
- package/src/host.js +282 -0
- package/src/server.js +379 -0
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
|
+
});
|