@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 CHANGED
@@ -1,61 +1,47 @@
1
1
  # OpenCode Browser
2
2
 
3
- Browser automation for [OpenCode](https://github.com/opencode-ai/opencode) via Chrome extension + Native Messaging.
3
+ Browser automation plugin for [OpenCode](https://github.com/opencode-ai/opencode).
4
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.
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
- Get access to your fully credentialed chrome instance to perform privileged web operations.
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
- 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.
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. 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
- ```
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 ──MCP──> server.js ──Unix Socket──> host.js ──Native Messaging──> Chrome Extension
73
-
74
-
75
- chrome.tabs
76
- chrome.scripting
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
- - **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
98
+ 3. Restart OpenCode
82
99
 
83
- No DevTools Protocol = No security prompts.
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 and native messaging host for browser automation.
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 { createInterface } from "readline";
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", "" + msg));
39
+ console.log(color("green", " " + msg));
39
40
  }
40
41
 
41
42
  function warn(msg) {
42
- console.log(color("yellow", "" + msg));
43
+ console.log(color("yellow", " " + msg));
43
44
  }
44
45
 
45
46
  function error(msg) {
46
- console.log(color("red", "" + msg));
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", "".repeat(msg.length)));
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", 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", "╚═══════════════════════════════════════════════════════════╝"))}
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 === "daemon") {
89
- await startDaemon();
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 Install extension
100
- npx @different-ai/opencode-browser daemon-install Install background daemon
101
- npx @different-ai/opencode-browser daemon Run daemon (foreground)
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", "For scheduled jobs:")}
109
- Run 'daemon-install' to enable browser tools in background jobs.
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: Install Extension Directory");
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 Browser");
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
- log("");
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
- const wrapperDir = join(homedir(), ".opencode-browser");
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 manifest = {
205
- name: "com.opencode.browser_automation",
206
- description: "OpenCode Browser Automation Native Messaging Host",
207
- path: wrapperPath,
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 this to your ${color("cyan", "opencode.json")} under "mcp":
166
+ Add the plugin to your ${color("cyan", "opencode.json")}:
167
+
168
+ ${color("bright", pluginConfig)}
233
169
 
234
- ${color("bright", JSON.stringify(mcpConfig, null, 2))}
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
- shouldUpdateConfig = await confirm(`Found opencode.json in current directory. Add browser config automatically?`);
242
-
243
- if (shouldUpdateConfig) {
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.mcp = config.mcp || {};
247
- config.mcp.browser = mcpConfig.browser;
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 browser MCP config");
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 config manually.");
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 with browser config?`);
257
-
202
+ const shouldCreate = await confirm(`No opencode.json found. Create one?`);
203
+
258
204
  if (shouldCreate) {
259
205
  try {
260
- const config = { "$schema": "https://opencode.ai/config.json", mcp: mcpConfig };
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 browser MCP config");
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", "")} Extension installed at: ${extensionDir}
276
- ${color("green", "")} Native host registered
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", "Next steps:")}
280
- 1. ${color("cyan", "Restart Chrome")} (close all windows and reopen)
281
- 2. Click the extension icon to verify connection
282
- 3. Restart OpenCode to load the new MCP server
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
- browser_navigate - Go to a URL
286
- browser_click - Click an element
287
- browser_type - Type into an input
288
- browser_screenshot - Capture the page
289
- browser_snapshot - Get accessibility tree
290
- browser_get_tabs - List open tabs
291
- browser_scroll - Scroll the page
292
- browser_wait - Wait for duration
293
- browser_execute - Run JavaScript
294
-
295
- ${color("bright", "Logs:")} ~/.opencode-browser/logs/
296
-
297
- ${color("bright", "Test it out:")}
298
- Open OpenCode and try: ${color("cyan", '"Navigate to google.com and take a snapshot"')}
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 startDaemon() {
303
- const { spawn } = await import("child_process");
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
- async function installDaemon() {
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
- The daemon bridges Chrome extension ↔ MCP server.
364
- It runs automatically on login and enables browser
365
- tools in scheduled OpenCode jobs.
273
+ if (!existsSync(lockFile)) {
274
+ success("Browser available (no lock file)");
275
+ return;
276
+ }
366
277
 
367
- ${color("bright", "Logs:")} ${logsDir}/daemon.log
278
+ try {
279
+ const lock = JSON.parse(readFileSync(lockFile, "utf-8"));
280
+ log(`
281
+ Lock file: ${lockFile}
368
282
 
369
- ${color("bright", "Control:")}
370
- launchctl stop com.opencode.browser-daemon
371
- launchctl start com.opencode.browser-daemon
372
- launchctl unload ~/Library/LaunchAgents/com.opencode.browser-daemon.plist
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
- const nativeHostDir = os === "darwin"
381
- ? join(homedir(), "Library", "Application Support", "Google", "Chrome", "NativeMessagingHosts")
382
- : join(homedir(), ".config", "google-chrome", "NativeMessagingHosts");
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
- const manifestPath = join(nativeHostDir, "com.opencode.browser_automation.json");
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
- } else {
391
- warn("Native host manifest not found");
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:")} The extension files at ~/.opencode-browser/ were not removed.
396
- Remove them manually if needed:
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 the "browser" entry from your opencode.json.
341
+ Also remove "@different-ai/opencode-browser" from your opencode.json plugin array.
400
342
  `);
401
343
  }
402
344