@different-ai/opencode-browser 1.0.4 → 2.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 +78 -52
- package/bin/cli.js +151 -209
- package/extension/background.js +3 -3
- package/extension/manifest.json +2 -3
- package/package.json +20 -9
- package/src/plugin.ts +450 -0
- package/src/daemon.js +0 -207
- package/src/host.js +0 -282
- package/src/server.js +0 -379
package/README.md
CHANGED
|
@@ -1,61 +1,47 @@
|
|
|
1
1
|
# OpenCode Browser
|
|
2
2
|
|
|
3
|
-
Browser automation for [OpenCode](https://github.com/opencode-ai/opencode)
|
|
3
|
+
Browser automation plugin for [OpenCode](https://github.com/opencode-ai/opencode).
|
|
4
4
|
|
|
5
|
-
|
|
5
|
+
Control your real Chrome browser with existing logins, cookies, and bookmarks. No DevTools Protocol, no security prompts.
|
|
6
6
|
|
|
7
7
|
## Why?
|
|
8
8
|
|
|
9
|
-
|
|
9
|
+
Chrome 136+ blocks `--remote-debugging-port` on your default profile for security reasons. DevTools-based automation (like Playwright) triggers a security prompt every time.
|
|
10
10
|
|
|
11
|
-
|
|
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.
|
|
11
|
+
OpenCode Browser uses a simple WebSocket connection between an OpenCode plugin and a Chrome extension. Your automation works with your existing browser session - no prompts, no separate profiles.
|
|
14
12
|
|
|
15
13
|
## Installation
|
|
16
14
|
|
|
17
15
|
```bash
|
|
18
|
-
npx opencode-browser install
|
|
16
|
+
npx @different-ai/opencode-browser install
|
|
19
17
|
```
|
|
20
18
|
|
|
21
19
|
The installer will:
|
|
22
20
|
1. Copy the extension to `~/.opencode-browser/extension/`
|
|
23
|
-
2.
|
|
24
|
-
3.
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
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
|
-
```
|
|
21
|
+
2. Guide you to load the extension in Chrome
|
|
22
|
+
3. Update your `opencode.json` to use the plugin
|
|
23
|
+
|
|
24
|
+
## Configuration
|
|
25
|
+
|
|
26
|
+
Add to your `opencode.json`:
|
|
27
|
+
|
|
28
|
+
```json
|
|
29
|
+
{
|
|
30
|
+
"plugin": ["@different-ai/opencode-browser"]
|
|
31
|
+
}
|
|
32
|
+
```
|
|
33
|
+
|
|
34
|
+
Then load the extension in Chrome:
|
|
35
|
+
1. Go to `chrome://extensions`
|
|
36
|
+
2. Enable "Developer mode"
|
|
37
|
+
3. Click "Load unpacked" and select `~/.opencode-browser/extension/`
|
|
54
38
|
|
|
55
39
|
## Available Tools
|
|
56
40
|
|
|
57
41
|
| Tool | Description |
|
|
58
42
|
|------|-------------|
|
|
43
|
+
| `browser_status` | Check if browser is available or locked |
|
|
44
|
+
| `browser_kill_session` | Take over from another OpenCode session |
|
|
59
45
|
| `browser_navigate` | Navigate to a URL |
|
|
60
46
|
| `browser_click` | Click an element by CSS selector |
|
|
61
47
|
| `browser_type` | Type text into an input field |
|
|
@@ -66,34 +52,74 @@ If you prefer manual installation:
|
|
|
66
52
|
| `browser_wait` | Wait for a duration |
|
|
67
53
|
| `browser_execute` | Run JavaScript in page context |
|
|
68
54
|
|
|
55
|
+
## Multi-Session Support
|
|
56
|
+
|
|
57
|
+
Only one OpenCode session can use the browser at a time. This prevents conflicts when you have multiple terminals open.
|
|
58
|
+
|
|
59
|
+
- `browser_status` - Check who has the lock
|
|
60
|
+
- `browser_kill_session` - Kill the other session and take over
|
|
61
|
+
|
|
62
|
+
In your prompts, you can say:
|
|
63
|
+
- "If browser is locked, kill the session and proceed"
|
|
64
|
+
- "If browser is locked, skip this task"
|
|
65
|
+
|
|
69
66
|
## Architecture
|
|
70
67
|
|
|
71
68
|
```
|
|
72
|
-
OpenCode
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
69
|
+
OpenCode Plugin ◄──WebSocket:19222──► Chrome Extension
|
|
70
|
+
│ │
|
|
71
|
+
└── Lock file └── chrome.tabs, chrome.scripting
|
|
72
|
+
```
|
|
73
|
+
|
|
74
|
+
**Two components:**
|
|
75
|
+
1. OpenCode plugin (runs WebSocket server, defines tools)
|
|
76
|
+
2. Chrome extension (connects to plugin, executes commands)
|
|
77
|
+
|
|
78
|
+
**No daemon. No MCP server. No native messaging host.**
|
|
79
|
+
|
|
80
|
+
## Upgrading from v1.x
|
|
81
|
+
|
|
82
|
+
v2.0 is a complete rewrite with a simpler architecture:
|
|
83
|
+
|
|
84
|
+
1. Run `npx @different-ai/opencode-browser install` (cleans up old daemon automatically)
|
|
85
|
+
2. Replace MCP config with plugin config in `opencode.json`:
|
|
86
|
+
|
|
87
|
+
```diff
|
|
88
|
+
- "mcp": {
|
|
89
|
+
- "browser": {
|
|
90
|
+
- "type": "local",
|
|
91
|
+
- "command": ["npx", "@different-ai/opencode-browser", "start"],
|
|
92
|
+
- "enabled": true
|
|
93
|
+
- }
|
|
94
|
+
- }
|
|
95
|
+
+ "plugin": ["@different-ai/opencode-browser"]
|
|
77
96
|
```
|
|
78
97
|
|
|
79
|
-
|
|
80
|
-
- **host.js** - Native messaging host launched by Chrome
|
|
81
|
-
- **extension/** - Chrome extension with browser automation tools
|
|
98
|
+
3. Restart OpenCode
|
|
82
99
|
|
|
83
|
-
|
|
100
|
+
## Troubleshooting
|
|
101
|
+
|
|
102
|
+
**"Chrome extension not connected"**
|
|
103
|
+
- Make sure Chrome is running
|
|
104
|
+
- Check that the extension is loaded and enabled
|
|
105
|
+
- Click the extension icon to see connection status
|
|
106
|
+
|
|
107
|
+
**"Browser locked by another session"**
|
|
108
|
+
- Use `browser_kill_session` to take over
|
|
109
|
+
- Or close the other OpenCode session
|
|
110
|
+
|
|
111
|
+
**"Failed to start WebSocket server"**
|
|
112
|
+
- Port 19222 may be in use
|
|
113
|
+
- Check if another OpenCode session is running
|
|
84
114
|
|
|
85
115
|
## Uninstall
|
|
86
116
|
|
|
87
117
|
```bash
|
|
88
|
-
npx opencode-browser uninstall
|
|
118
|
+
npx @different-ai/opencode-browser uninstall
|
|
89
119
|
```
|
|
90
120
|
|
|
91
121
|
Then remove the extension from Chrome and delete `~/.opencode-browser/` if desired.
|
|
92
122
|
|
|
93
|
-
## Logs
|
|
94
|
-
|
|
95
|
-
Logs are written to `~/.opencode-browser/logs/browser-mcp-host.log`
|
|
96
|
-
|
|
97
123
|
## Platform Support
|
|
98
124
|
|
|
99
125
|
- macOS ✓
|
package/bin/cli.js
CHANGED
|
@@ -1,16 +1,17 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
/**
|
|
3
3
|
* OpenCode Browser - CLI Installer
|
|
4
|
-
*
|
|
5
|
-
* Installs the Chrome extension
|
|
4
|
+
*
|
|
5
|
+
* Installs the Chrome extension for browser automation.
|
|
6
|
+
* v2.0: Plugin-based architecture (no daemon, no MCP server)
|
|
6
7
|
*/
|
|
7
8
|
|
|
8
|
-
import {
|
|
9
|
-
import { existsSync, mkdirSync, writeFileSync, readFileSync, copyFileSync, readdirSync } from "fs";
|
|
9
|
+
import { existsSync, mkdirSync, writeFileSync, readFileSync, copyFileSync, readdirSync, unlinkSync } from "fs";
|
|
10
10
|
import { homedir, platform } from "os";
|
|
11
11
|
import { join, dirname } from "path";
|
|
12
12
|
import { fileURLToPath } from "url";
|
|
13
13
|
import { execSync } from "child_process";
|
|
14
|
+
import { createInterface } from "readline";
|
|
14
15
|
|
|
15
16
|
const __filename = fileURLToPath(import.meta.url);
|
|
16
17
|
const __dirname = dirname(__filename);
|
|
@@ -35,20 +36,20 @@ function log(msg) {
|
|
|
35
36
|
}
|
|
36
37
|
|
|
37
38
|
function success(msg) {
|
|
38
|
-
console.log(color("green", "
|
|
39
|
+
console.log(color("green", " " + msg));
|
|
39
40
|
}
|
|
40
41
|
|
|
41
42
|
function warn(msg) {
|
|
42
|
-
console.log(color("yellow", "
|
|
43
|
+
console.log(color("yellow", " " + msg));
|
|
43
44
|
}
|
|
44
45
|
|
|
45
46
|
function error(msg) {
|
|
46
|
-
console.log(color("red", "
|
|
47
|
+
console.log(color("red", " " + msg));
|
|
47
48
|
}
|
|
48
49
|
|
|
49
50
|
function header(msg) {
|
|
50
51
|
console.log("\n" + color("cyan", color("bright", msg)));
|
|
51
|
-
console.log(color("cyan", "
|
|
52
|
+
console.log(color("cyan", "-".repeat(msg.length)));
|
|
52
53
|
}
|
|
53
54
|
|
|
54
55
|
const rl = createInterface({
|
|
@@ -71,12 +72,8 @@ async function confirm(question) {
|
|
|
71
72
|
|
|
72
73
|
async function main() {
|
|
73
74
|
console.log(`
|
|
74
|
-
${color("cyan", color("bright", "
|
|
75
|
-
${color("cyan",
|
|
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", "╚═══════════════════════════════════════════════════════════╝"))}
|
|
75
|
+
${color("cyan", color("bright", "OpenCode Browser v2.0"))}
|
|
76
|
+
${color("cyan", "Browser automation for OpenCode")}
|
|
80
77
|
`);
|
|
81
78
|
|
|
82
79
|
const command = process.argv[2];
|
|
@@ -85,28 +82,18 @@ ${color("cyan", color("bright", "╚══════════════
|
|
|
85
82
|
await install();
|
|
86
83
|
} else if (command === "uninstall") {
|
|
87
84
|
await uninstall();
|
|
88
|
-
} else if (command === "
|
|
89
|
-
await
|
|
90
|
-
} else if (command === "daemon-install") {
|
|
91
|
-
await installDaemon();
|
|
92
|
-
} else if (command === "start") {
|
|
93
|
-
rl.close();
|
|
94
|
-
await import("../src/server.js");
|
|
95
|
-
return;
|
|
85
|
+
} else if (command === "status") {
|
|
86
|
+
await status();
|
|
96
87
|
} else {
|
|
97
88
|
log(`
|
|
98
89
|
${color("bright", "Usage:")}
|
|
99
|
-
npx @different-ai/opencode-browser install
|
|
100
|
-
npx @different-ai/opencode-browser
|
|
101
|
-
npx @different-ai/opencode-browser
|
|
102
|
-
npx @different-ai/opencode-browser start Run MCP server
|
|
103
|
-
npx @different-ai/opencode-browser uninstall Remove installation
|
|
104
|
-
|
|
105
|
-
${color("bright", "With bun:")}
|
|
106
|
-
bunx @different-ai/opencode-browser install
|
|
90
|
+
npx @different-ai/opencode-browser install Install extension
|
|
91
|
+
npx @different-ai/opencode-browser uninstall Remove installation
|
|
92
|
+
npx @different-ai/opencode-browser status Check lock status
|
|
107
93
|
|
|
108
|
-
${color("bright", "
|
|
109
|
-
|
|
94
|
+
${color("bright", "v2.0 Changes:")}
|
|
95
|
+
- Plugin-based architecture (no daemon needed)
|
|
96
|
+
- Add plugin to opencode.json, load extension in Chrome, done
|
|
110
97
|
`);
|
|
111
98
|
}
|
|
112
99
|
|
|
@@ -124,7 +111,7 @@ async function install() {
|
|
|
124
111
|
}
|
|
125
112
|
success(`Platform: ${os === "darwin" ? "macOS" : "Linux"}`);
|
|
126
113
|
|
|
127
|
-
header("Step 2:
|
|
114
|
+
header("Step 2: Copy Extension Files");
|
|
128
115
|
|
|
129
116
|
const extensionDir = join(homedir(), ".opencode-browser", "extension");
|
|
130
117
|
const srcExtensionDir = join(PACKAGE_ROOT, "extension");
|
|
@@ -135,7 +122,7 @@ async function install() {
|
|
|
135
122
|
for (const file of files) {
|
|
136
123
|
const srcPath = join(srcExtensionDir, file);
|
|
137
124
|
const destPath = join(extensionDir, file);
|
|
138
|
-
|
|
125
|
+
|
|
139
126
|
try {
|
|
140
127
|
const stat = readdirSync(srcPath);
|
|
141
128
|
mkdirSync(destPath, { recursive: true });
|
|
@@ -147,7 +134,7 @@ async function install() {
|
|
|
147
134
|
|
|
148
135
|
success(`Extension files copied to: ${extensionDir}`);
|
|
149
136
|
|
|
150
|
-
header("Step 3: Load Extension in
|
|
137
|
+
header("Step 3: Load Extension in Chrome");
|
|
151
138
|
|
|
152
139
|
log(`
|
|
153
140
|
Works with: ${color("cyan", "Chrome")}, ${color("cyan", "Brave")}, ${color("cyan", "Arc")}, ${color("cyan", "Edge")}, and other Chromium browsers.
|
|
@@ -164,239 +151,194 @@ To load the extension:
|
|
|
164
151
|
4. Select this folder:
|
|
165
152
|
${color("cyan", extensionDir)}
|
|
166
153
|
${os === "darwin" ? color("yellow", "Tip: Press Cmd+Shift+G and paste the path above") : ""}
|
|
167
|
-
|
|
168
|
-
5. Copy the ${color("bright", "Extension ID")} shown under the extension name
|
|
169
|
-
(looks like: abcdefghijklmnopqrstuvwxyz123456)
|
|
170
154
|
`);
|
|
171
155
|
|
|
172
|
-
|
|
173
|
-
const extensionId = await ask(color("bright", "Enter your Extension ID: "));
|
|
174
|
-
|
|
175
|
-
if (!extensionId) {
|
|
176
|
-
error("Extension ID is required");
|
|
177
|
-
process.exit(1);
|
|
178
|
-
}
|
|
179
|
-
|
|
180
|
-
if (!/^[a-z]{32}$/.test(extensionId)) {
|
|
181
|
-
warn("Extension ID format looks unusual (expected 32 lowercase letters)");
|
|
182
|
-
const proceed = await confirm("Continue anyway?");
|
|
183
|
-
if (!proceed) process.exit(1);
|
|
184
|
-
}
|
|
185
|
-
|
|
186
|
-
header("Step 4: Register Native Messaging Host");
|
|
187
|
-
|
|
188
|
-
const nativeHostDir = os === "darwin"
|
|
189
|
-
? join(homedir(), "Library", "Application Support", "Google", "Chrome", "NativeMessagingHosts")
|
|
190
|
-
: join(homedir(), ".config", "google-chrome", "NativeMessagingHosts");
|
|
191
|
-
|
|
192
|
-
mkdirSync(nativeHostDir, { recursive: true });
|
|
193
|
-
|
|
194
|
-
const nodePath = process.execPath;
|
|
195
|
-
const hostScriptPath = join(PACKAGE_ROOT, "src", "host.js");
|
|
156
|
+
await ask(color("bright", "Press Enter when you've loaded the extension..."));
|
|
196
157
|
|
|
197
|
-
|
|
198
|
-
const wrapperPath = join(wrapperDir, "host-wrapper.sh");
|
|
199
|
-
|
|
200
|
-
writeFileSync(wrapperPath, `#!/bin/bash
|
|
201
|
-
exec "${nodePath}" "${hostScriptPath}" "$@"
|
|
202
|
-
`, { mode: 0o755 });
|
|
158
|
+
header("Step 4: Configure OpenCode");
|
|
203
159
|
|
|
204
|
-
const
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
type: "stdio",
|
|
209
|
-
allowed_origins: [`chrome-extension://${extensionId}/`],
|
|
210
|
-
};
|
|
211
|
-
|
|
212
|
-
const manifestPath = join(nativeHostDir, "com.opencode.browser_automation.json");
|
|
213
|
-
writeFileSync(manifestPath, JSON.stringify(manifest, null, 2));
|
|
214
|
-
|
|
215
|
-
success(`Native host registered at: ${manifestPath}`);
|
|
216
|
-
|
|
217
|
-
const logsDir = join(homedir(), ".opencode-browser", "logs");
|
|
218
|
-
mkdirSync(logsDir, { recursive: true });
|
|
219
|
-
|
|
220
|
-
header("Step 5: Configure OpenCode");
|
|
221
|
-
|
|
222
|
-
const serverPath = join(PACKAGE_ROOT, "src", "server.js");
|
|
223
|
-
const mcpConfig = {
|
|
224
|
-
browser: {
|
|
225
|
-
type: "local",
|
|
226
|
-
command: ["node", serverPath],
|
|
227
|
-
enabled: true,
|
|
228
|
-
},
|
|
229
|
-
};
|
|
160
|
+
const pluginConfig = `{
|
|
161
|
+
"$schema": "https://opencode.ai/config.json",
|
|
162
|
+
"plugin": ["@different-ai/opencode-browser"]
|
|
163
|
+
}`;
|
|
230
164
|
|
|
231
165
|
log(`
|
|
232
|
-
Add
|
|
166
|
+
Add the plugin to your ${color("cyan", "opencode.json")}:
|
|
167
|
+
|
|
168
|
+
${color("bright", pluginConfig)}
|
|
233
169
|
|
|
234
|
-
|
|
170
|
+
Or if you already have an opencode.json, just add to the "plugin" array:
|
|
171
|
+
${color("bright", '"plugin": ["@different-ai/opencode-browser"]')}
|
|
235
172
|
`);
|
|
236
173
|
|
|
237
174
|
const opencodeJsonPath = join(process.cwd(), "opencode.json");
|
|
238
|
-
let shouldUpdateConfig = false;
|
|
239
175
|
|
|
240
176
|
if (existsSync(opencodeJsonPath)) {
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
if (
|
|
177
|
+
const shouldUpdate = await confirm(`Found opencode.json. Add plugin automatically?`);
|
|
178
|
+
|
|
179
|
+
if (shouldUpdate) {
|
|
244
180
|
try {
|
|
245
181
|
const config = JSON.parse(readFileSync(opencodeJsonPath, "utf-8"));
|
|
246
|
-
config.
|
|
247
|
-
config.
|
|
182
|
+
config.plugin = config.plugin || [];
|
|
183
|
+
if (!config.plugin.includes("@different-ai/opencode-browser")) {
|
|
184
|
+
config.plugin.push("@different-ai/opencode-browser");
|
|
185
|
+
}
|
|
186
|
+
// Remove old MCP config if present
|
|
187
|
+
if (config.mcp?.browser) {
|
|
188
|
+
delete config.mcp.browser;
|
|
189
|
+
if (Object.keys(config.mcp).length === 0) {
|
|
190
|
+
delete config.mcp;
|
|
191
|
+
}
|
|
192
|
+
warn("Removed old MCP browser config (replaced by plugin)");
|
|
193
|
+
}
|
|
248
194
|
writeFileSync(opencodeJsonPath, JSON.stringify(config, null, 2) + "\n");
|
|
249
|
-
success("Updated opencode.json with
|
|
195
|
+
success("Updated opencode.json with plugin");
|
|
250
196
|
} catch (e) {
|
|
251
197
|
error(`Failed to update opencode.json: ${e.message}`);
|
|
252
|
-
log("Please add the
|
|
198
|
+
log("Please add the plugin manually.");
|
|
253
199
|
}
|
|
254
200
|
}
|
|
255
201
|
} else {
|
|
256
|
-
const shouldCreate = await confirm(`No opencode.json found. Create one
|
|
257
|
-
|
|
202
|
+
const shouldCreate = await confirm(`No opencode.json found. Create one?`);
|
|
203
|
+
|
|
258
204
|
if (shouldCreate) {
|
|
259
205
|
try {
|
|
260
|
-
const config = {
|
|
206
|
+
const config = {
|
|
207
|
+
$schema: "https://opencode.ai/config.json",
|
|
208
|
+
plugin: ["@different-ai/opencode-browser"],
|
|
209
|
+
};
|
|
261
210
|
writeFileSync(opencodeJsonPath, JSON.stringify(config, null, 2) + "\n");
|
|
262
|
-
success("Created opencode.json with
|
|
263
|
-
shouldUpdateConfig = true;
|
|
211
|
+
success("Created opencode.json with plugin");
|
|
264
212
|
} catch (e) {
|
|
265
213
|
error(`Failed to create opencode.json: ${e.message}`);
|
|
266
214
|
}
|
|
267
|
-
} else {
|
|
268
|
-
log(`Add the config above to your project's opencode.json manually.`);
|
|
269
215
|
}
|
|
270
216
|
}
|
|
271
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.");
|
|
229
|
+
}
|
|
230
|
+
} else {
|
|
231
|
+
success("No old daemon to clean up");
|
|
232
|
+
}
|
|
233
|
+
|
|
272
234
|
header("Installation Complete!");
|
|
273
235
|
|
|
274
236
|
log(`
|
|
275
|
-
${color("green", "
|
|
276
|
-
${color("green", "
|
|
277
|
-
${shouldUpdateConfig ? color("green", "✓") + " opencode.json updated" : color("yellow", "○") + " Remember to update opencode.json"}
|
|
237
|
+
${color("green", "")} Extension: ${extensionDir}
|
|
238
|
+
${color("green", "")} Plugin: @different-ai/opencode-browser
|
|
278
239
|
|
|
279
|
-
${color("bright", "
|
|
280
|
-
1.
|
|
281
|
-
2.
|
|
282
|
-
3.
|
|
240
|
+
${color("bright", "How it works:")}
|
|
241
|
+
1. OpenCode loads the plugin on startup
|
|
242
|
+
2. Plugin starts WebSocket server on port 19222
|
|
243
|
+
3. Chrome extension connects automatically
|
|
244
|
+
4. Browser tools are available!
|
|
283
245
|
|
|
284
246
|
${color("bright", "Available tools:")}
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
${color("bright", "
|
|
298
|
-
|
|
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"')}
|
|
299
265
|
`);
|
|
300
266
|
}
|
|
301
267
|
|
|
302
|
-
async function
|
|
303
|
-
|
|
304
|
-
const daemonPath = join(PACKAGE_ROOT, "src", "daemon.js");
|
|
305
|
-
log("Starting daemon...");
|
|
306
|
-
const child = spawn(process.execPath, [daemonPath], { stdio: "inherit" });
|
|
307
|
-
child.on("exit", (code) => process.exit(code || 0));
|
|
308
|
-
}
|
|
268
|
+
async function status() {
|
|
269
|
+
header("Browser Lock Status");
|
|
309
270
|
|
|
310
|
-
|
|
311
|
-
header("Installing Background Daemon");
|
|
312
|
-
|
|
313
|
-
const os = platform();
|
|
314
|
-
if (os !== "darwin") {
|
|
315
|
-
error("Daemon auto-install currently supports macOS only");
|
|
316
|
-
log("On Linux, create a systemd service manually.");
|
|
317
|
-
process.exit(1);
|
|
318
|
-
}
|
|
319
|
-
|
|
320
|
-
const nodePath = process.execPath;
|
|
321
|
-
const daemonPath = join(PACKAGE_ROOT, "src", "daemon.js");
|
|
322
|
-
const logsDir = join(homedir(), ".opencode-browser", "logs");
|
|
323
|
-
|
|
324
|
-
mkdirSync(logsDir, { recursive: true });
|
|
325
|
-
|
|
326
|
-
const plist = `<?xml version="1.0" encoding="UTF-8"?>
|
|
327
|
-
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
|
|
328
|
-
<plist version="1.0">
|
|
329
|
-
<dict>
|
|
330
|
-
<key>Label</key>
|
|
331
|
-
<string>com.opencode.browser-daemon</string>
|
|
332
|
-
<key>ProgramArguments</key>
|
|
333
|
-
<array>
|
|
334
|
-
<string>${nodePath}</string>
|
|
335
|
-
<string>${daemonPath}</string>
|
|
336
|
-
</array>
|
|
337
|
-
<key>RunAtLoad</key>
|
|
338
|
-
<true/>
|
|
339
|
-
<key>KeepAlive</key>
|
|
340
|
-
<true/>
|
|
341
|
-
<key>StandardOutPath</key>
|
|
342
|
-
<string>${logsDir}/daemon.log</string>
|
|
343
|
-
<key>StandardErrorPath</key>
|
|
344
|
-
<string>${logsDir}/daemon.log</string>
|
|
345
|
-
</dict>
|
|
346
|
-
</plist>`;
|
|
347
|
-
|
|
348
|
-
const plistPath = join(homedir(), "Library", "LaunchAgents", "com.opencode.browser-daemon.plist");
|
|
349
|
-
writeFileSync(plistPath, plist);
|
|
350
|
-
success(`Created launchd plist: ${plistPath}`);
|
|
351
|
-
|
|
352
|
-
try {
|
|
353
|
-
execSync(`launchctl unload "${plistPath}" 2>/dev/null || true`);
|
|
354
|
-
execSync(`launchctl load "${plistPath}"`);
|
|
355
|
-
success("Daemon started");
|
|
356
|
-
} catch (e) {
|
|
357
|
-
error(`Failed to load daemon: ${e.message}`);
|
|
358
|
-
}
|
|
359
|
-
|
|
360
|
-
log(`
|
|
361
|
-
${color("green", "✓")} Daemon installed and running
|
|
271
|
+
const lockFile = join(homedir(), ".opencode-browser", "lock.json");
|
|
362
272
|
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
273
|
+
if (!existsSync(lockFile)) {
|
|
274
|
+
success("Browser available (no lock file)");
|
|
275
|
+
return;
|
|
276
|
+
}
|
|
366
277
|
|
|
367
|
-
|
|
278
|
+
try {
|
|
279
|
+
const lock = JSON.parse(readFileSync(lockFile, "utf-8"));
|
|
280
|
+
log(`
|
|
281
|
+
Lock file: ${lockFile}
|
|
368
282
|
|
|
369
|
-
${
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
283
|
+
PID: ${lock.pid}
|
|
284
|
+
Session: ${lock.sessionId}
|
|
285
|
+
Started: ${lock.startedAt}
|
|
286
|
+
Working directory: ${lock.cwd}
|
|
373
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.`);
|
|
295
|
+
}
|
|
296
|
+
} catch (e) {
|
|
297
|
+
error(`Could not read lock file: ${e.message}`);
|
|
298
|
+
}
|
|
374
299
|
}
|
|
375
300
|
|
|
376
301
|
async function uninstall() {
|
|
377
302
|
header("Uninstalling OpenCode Browser");
|
|
378
303
|
|
|
304
|
+
// Remove old daemon
|
|
379
305
|
const os = platform();
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
306
|
+
if (os === "darwin") {
|
|
307
|
+
const plistPath = join(homedir(), "Library", "LaunchAgents", "com.opencode.browser-daemon.plist");
|
|
308
|
+
if (existsSync(plistPath)) {
|
|
309
|
+
try {
|
|
310
|
+
execSync(`launchctl unload "${plistPath}" 2>/dev/null || true`, { stdio: "ignore" });
|
|
311
|
+
unlinkSync(plistPath);
|
|
312
|
+
success("Removed daemon plist");
|
|
313
|
+
} catch {}
|
|
314
|
+
}
|
|
315
|
+
}
|
|
383
316
|
|
|
384
|
-
|
|
317
|
+
// Remove native host registration (v1.x)
|
|
318
|
+
const nativeHostDir =
|
|
319
|
+
os === "darwin"
|
|
320
|
+
? join(homedir(), "Library", "Application Support", "Google", "Chrome", "NativeMessagingHosts")
|
|
321
|
+
: join(homedir(), ".config", "google-chrome", "NativeMessagingHosts");
|
|
385
322
|
|
|
323
|
+
const manifestPath = join(nativeHostDir, "com.opencode.browser_automation.json");
|
|
386
324
|
if (existsSync(manifestPath)) {
|
|
387
|
-
const { unlinkSync } = await import("fs");
|
|
388
325
|
unlinkSync(manifestPath);
|
|
389
326
|
success("Removed native host registration");
|
|
390
|
-
}
|
|
391
|
-
|
|
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");
|
|
392
334
|
}
|
|
393
335
|
|
|
394
336
|
log(`
|
|
395
|
-
${color("bright", "Note:")}
|
|
396
|
-
Remove
|
|
337
|
+
${color("bright", "Note:")} Extension files at ~/.opencode-browser/ were not removed.
|
|
338
|
+
Remove manually if needed:
|
|
397
339
|
rm -rf ~/.opencode-browser/
|
|
398
340
|
|
|
399
|
-
Also remove
|
|
341
|
+
Also remove "@different-ai/opencode-browser" from your opencode.json plugin array.
|
|
400
342
|
`);
|
|
401
343
|
}
|
|
402
344
|
|