@different-ai/opencode-browser 4.3.2 → 4.4.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.
@@ -21,9 +21,15 @@ metadata:
21
21
  3. Navigate with `browser_navigate` if needed
22
22
  4. Wait for UI using `browser_query` with `timeoutMs`
23
23
  5. Discover candidates using `browser_query` with `mode=list`
24
- 6. Click or type using `index`
24
+ 6. Click, type, or select using `index`
25
25
  7. Confirm using `browser_query` or `browser_snapshot`
26
26
 
27
+ ## Selecting options
28
+
29
+ - Use `browser_select` for native `<select>` elements
30
+ - Prefer `value` or `label`; use `optionIndex` when needed
31
+ - Example: `browser_select({ selector: "select", value: "plugin" })`
32
+
27
33
  ## Query modes
28
34
 
29
35
  - `text`: read visible text from a matched element
package/README.md CHANGED
@@ -1,6 +1,6 @@
1
1
  # OpenCode Browser
2
2
 
3
- Browser automation plugin for [OpenCode](https://github.com/opencode-ai/opencode).
3
+ Browser automation plugin for [OpenCode](https://opencode.ai).
4
4
 
5
5
  Control your real Chromium browser (Chrome/Brave/Arc/Edge) using your existing profile (logins, cookies, bookmarks). No DevTools Protocol, no security prompts.
6
6
 
@@ -34,11 +34,15 @@ The installer will:
34
34
 
35
35
  1. Copy the extension to `~/.opencode-browser/extension/`
36
36
  2. Walk you through loading + pinning it in `chrome://extensions`
37
- 3. Ask for the extension ID and install a **Native Messaging Host manifest**
37
+ 3. Resolve a fixed extension ID (no copy/paste) and install a **Native Messaging Host manifest**
38
38
  4. Update your `opencode.json` or `opencode.jsonc` to load the plugin
39
39
 
40
+ To override the extension ID, pass `--extension-id <id>` or set `OPENCODE_BROWSER_EXTENSION_ID`.
41
+
40
42
  ### Configure OpenCode
41
43
 
44
+ > Note: if you run the installer you'll be prompted to include this automatically. If you said "yes", you can skip this part.
45
+
42
46
  Your `opencode.json` or `opencode.jsonc` should contain:
43
47
 
44
48
  ```json
@@ -48,6 +52,12 @@ Your `opencode.json` or `opencode.jsonc` should contain:
48
52
  }
49
53
  ```
50
54
 
55
+ ### Update
56
+
57
+ ```bash
58
+ bunx @different-ai/opencode-browser@latest update
59
+ ```
60
+
51
61
  ## How it works
52
62
 
53
63
  ```
@@ -58,6 +68,47 @@ OpenCode Plugin <-> Local Broker (unix socket) <-> Native Host <-> Chrome Extens
58
68
  - The plugin talks to the broker over a local unix socket.
59
69
  - The broker forwards tool requests to the extension and enforces tab ownership.
60
70
 
71
+ ## Agent Browser mode (alpha)
72
+
73
+ This branch adds an alternate backend powered by `agent-browser` (Playwright). It runs headless and does **not** reuse your existing Chrome profile.
74
+
75
+ ### Enable locally
76
+
77
+ 1. Install `agent-browser` and Chromium:
78
+
79
+ ```bash
80
+ npm install -g agent-browser
81
+ agent-browser install
82
+ ```
83
+
84
+ 2. Set the backend mode:
85
+
86
+ ```bash
87
+ export OPENCODE_BROWSER_BACKEND=agent
88
+ ```
89
+
90
+ Optional overrides:
91
+ - `OPENCODE_BROWSER_AGENT_SESSION` (custom session name)
92
+ - `OPENCODE_BROWSER_AGENT_SOCKET` (unix socket path)
93
+ - `OPENCODE_BROWSER_AGENT_AUTOSTART=0` (disable auto-start)
94
+ - `OPENCODE_BROWSER_AGENT_DAEMON` (explicit daemon path)
95
+
96
+ ### Tailnet/remote host
97
+
98
+ On the host (e.g., `home-server.taild435d7.ts.net`), run the TCP gateway:
99
+
100
+ ```bash
101
+ OPENCODE_BROWSER_AGENT_GATEWAY_PORT=9833 node bin/agent-gateway.cjs
102
+ ```
103
+
104
+ On the client:
105
+
106
+ ```bash
107
+ export OPENCODE_BROWSER_BACKEND=agent
108
+ export OPENCODE_BROWSER_AGENT_HOST=home-server.taild435d7.ts.net
109
+ export OPENCODE_BROWSER_AGENT_PORT=9833
110
+ ```
111
+
61
112
  ## Per-tab ownership
62
113
 
63
114
  - First time a session touches a tab, the broker **auto-claims** it for that session.
@@ -72,12 +123,20 @@ Core primitives:
72
123
  - `browser_open_tab`
73
124
  - `browser_navigate`
74
125
  - `browser_query` (modes: `text`, `value`, `list`, `exists`, `page_text`; optional `timeoutMs`/`pollMs`)
75
- - `browser_click`
76
- - `browser_type`
77
- - `browser_select`
78
- - `browser_scroll`
126
+ - `browser_click` (optional `timeoutMs`/`pollMs`)
127
+ - `browser_type` (optional `timeoutMs`/`pollMs`)
128
+ - `browser_select` (optional `timeoutMs`/`pollMs`)
129
+ - `browser_scroll` (optional `timeoutMs`/`pollMs`)
79
130
  - `browser_wait`
80
131
 
132
+ Selector helpers (usable in `selector`):
133
+ - `label:Mailing Address: City`
134
+ - `aria:Principal Address: City`
135
+ - `placeholder:Search`, `name:email`, `role:button`, `text:Submit`
136
+ - `css:label:has(input)` to force CSS
137
+
138
+ Selector-based tools wait up to 2000ms by default; set `timeoutMs: 0` to disable.
139
+
81
140
  Diagnostics:
82
141
  - `browser_snapshot`
83
142
  - `browser_screenshot`
@@ -95,7 +154,7 @@ Diagnostics:
95
154
 
96
155
  **Extension says native host not available**
97
156
  - Re-run `npx @different-ai/opencode-browser install`
98
- - Confirm the extension ID you pasted matches the loaded extension in `chrome://extensions`
157
+ - If you loaded a custom extension ID, rerun with `--extension-id <id>`
99
158
 
100
159
  **Tab ownership errors**
101
160
  - Use `browser_status` to see current claims
@@ -0,0 +1,129 @@
1
+ #!/usr/bin/env node
2
+ "use strict";
3
+
4
+ const net = require("net");
5
+ const os = require("os");
6
+ const path = require("path");
7
+ const { spawn } = require("child_process");
8
+
9
+ const session =
10
+ (process.env.OPENCODE_BROWSER_AGENT_SESSION || process.env.AGENT_BROWSER_SESSION || "default").trim();
11
+ const socketPath =
12
+ process.env.OPENCODE_BROWSER_AGENT_SOCKET || path.join(os.tmpdir(), `agent-browser-${session}.sock`);
13
+
14
+ function getPortForSession(name) {
15
+ let hash = 0;
16
+ for (let i = 0; i < name.length; i++) {
17
+ hash = (hash << 5) - hash + name.charCodeAt(i);
18
+ hash |= 0;
19
+ }
20
+ return 49152 + (Math.abs(hash) % 16383);
21
+ }
22
+
23
+ const host = process.env.OPENCODE_BROWSER_AGENT_GATEWAY_HOST || process.env.OPENCODE_BROWSER_AGENT_HOST || "0.0.0.0";
24
+ const port =
25
+ Number(process.env.OPENCODE_BROWSER_AGENT_GATEWAY_PORT || process.env.OPENCODE_BROWSER_AGENT_PORT) ||
26
+ getPortForSession(session);
27
+
28
+ function resolveDaemonPath() {
29
+ const override = process.env.OPENCODE_BROWSER_AGENT_DAEMON;
30
+ if (override) return override;
31
+ try {
32
+ return require.resolve("agent-browser/dist/daemon.js");
33
+ } catch {
34
+ return null;
35
+ }
36
+ }
37
+
38
+ function shouldAutoStart() {
39
+ const autoStart = (process.env.OPENCODE_BROWSER_AGENT_AUTOSTART || "").toLowerCase();
40
+ return !["0", "false", "no"].includes(autoStart);
41
+ }
42
+
43
+ function startDaemon() {
44
+ if (!shouldAutoStart()) return;
45
+ const daemonPath = resolveDaemonPath();
46
+ if (!daemonPath) {
47
+ console.error("[agent-gateway] agent-browser dependency not found.");
48
+ return;
49
+ }
50
+ try {
51
+ const child = spawn(process.execPath, [daemonPath], {
52
+ detached: true,
53
+ stdio: "ignore",
54
+ env: {
55
+ ...process.env,
56
+ AGENT_BROWSER_SESSION: session,
57
+ AGENT_BROWSER_DAEMON: "1",
58
+ },
59
+ });
60
+ child.unref();
61
+ } catch (err) {
62
+ console.error("[agent-gateway] Failed to start daemon:", err?.message || err);
63
+ }
64
+ }
65
+
66
+ async function sleep(ms) {
67
+ return await new Promise((resolve) => setTimeout(resolve, ms));
68
+ }
69
+
70
+ async function connectAgentSocket() {
71
+ return await new Promise((resolve, reject) => {
72
+ const socket = net.createConnection(socketPath);
73
+ socket.once("connect", () => resolve(socket));
74
+ socket.once("error", (err) => reject(err));
75
+ });
76
+ }
77
+
78
+ async function createAgentConnection() {
79
+ try {
80
+ return await connectAgentSocket();
81
+ } catch {
82
+ startDaemon();
83
+ for (let attempt = 0; attempt < 20; attempt++) {
84
+ await sleep(100);
85
+ try {
86
+ return await connectAgentSocket();
87
+ } catch {}
88
+ }
89
+ throw new Error(`Could not connect to agent-browser socket at ${socketPath}`);
90
+ }
91
+ }
92
+
93
+ const server = net.createServer(async (client) => {
94
+ let upstream = null;
95
+ try {
96
+ upstream = await createAgentConnection();
97
+ } catch (err) {
98
+ client.end();
99
+ console.error("[agent-gateway] Connection failed:", err?.message || err);
100
+ return;
101
+ }
102
+
103
+ client.pipe(upstream);
104
+ upstream.pipe(client);
105
+
106
+ const close = () => {
107
+ try {
108
+ client.destroy();
109
+ } catch {}
110
+ try {
111
+ upstream.destroy();
112
+ } catch {}
113
+ };
114
+
115
+ client.on("error", close);
116
+ upstream.on("error", close);
117
+ client.on("close", close);
118
+ upstream.on("close", close);
119
+ });
120
+
121
+ server.on("error", (err) => {
122
+ console.error("[agent-gateway] Server error:", err?.message || err);
123
+ process.exit(1);
124
+ });
125
+
126
+ server.listen(port, host, () => {
127
+ console.log(`[agent-gateway] Listening on ${host}:${port}`);
128
+ console.log(`[agent-gateway] Proxying to ${socketPath}`);
129
+ });
package/bin/cli.js CHANGED
@@ -26,15 +26,16 @@ import { join, dirname } from "path";
26
26
  import { fileURLToPath } from "url";
27
27
  import { createInterface } from "readline";
28
28
  import { createConnection } from "net";
29
- import { execSync } from "child_process";
29
+ import { execSync, spawn } from "child_process";
30
+ import { createHash } from "crypto";
30
31
 
31
32
  const __filename = fileURLToPath(import.meta.url);
32
33
  const __dirname = dirname(__filename);
33
34
  const PACKAGE_ROOT = join(__dirname, "..");
34
- const PACKAGE_JSON = join(PACKAGE_ROOT, "package.json");
35
35
 
36
36
  const BASE_DIR = join(homedir(), ".opencode-browser");
37
37
  const EXTENSION_DIR = join(BASE_DIR, "extension");
38
+ const EXTENSION_MANIFEST_PATH = join(PACKAGE_ROOT, "extension", "manifest.json");
38
39
  const BROKER_DST = join(BASE_DIR, "broker.cjs");
39
40
  const NATIVE_HOST_DST = join(BASE_DIR, "native-host.cjs");
40
41
  const NATIVE_HOST_WRAPPER = join(BASE_DIR, "host-wrapper.sh");
@@ -93,6 +94,84 @@ async function confirm(question) {
93
94
  return answer.toLowerCase() === "y" || answer.toLowerCase() === "yes";
94
95
  }
95
96
 
97
+ function getFlagValue(flag) {
98
+ const index = process.argv.findIndex((arg) => arg === flag || arg.startsWith(`${flag}=`));
99
+ if (index === -1) return null;
100
+ const arg = process.argv[index];
101
+ if (arg.includes("=")) return arg.slice(arg.indexOf("=") + 1).trim() || null;
102
+ const next = process.argv[index + 1];
103
+ if (!next || next.startsWith("-")) return null;
104
+ return next.trim();
105
+ }
106
+
107
+ function getExtensionIdOverride() {
108
+ const cliValue = getFlagValue("--extension-id") || getFlagValue("-e");
109
+ if (cliValue) return cliValue;
110
+ const envValue = process.env.OPENCODE_BROWSER_EXTENSION_ID;
111
+ return envValue ? envValue.trim() : null;
112
+ }
113
+
114
+ function readExtensionManifest() {
115
+ try {
116
+ if (!existsSync(EXTENSION_MANIFEST_PATH)) return null;
117
+ return JSON.parse(readFileSync(EXTENSION_MANIFEST_PATH, "utf-8"));
118
+ } catch {
119
+ return null;
120
+ }
121
+ }
122
+
123
+ function computeExtensionIdFromKey(key) {
124
+ try {
125
+ const raw = String(key || "").trim();
126
+ if (!raw) return null;
127
+ const buffer = Buffer.from(raw, "base64");
128
+ if (!buffer.length) return null;
129
+ const hash = createHash("sha256").update(buffer).digest();
130
+ const bytes = hash.subarray(0, 16);
131
+ return Array.from(bytes)
132
+ .map((b) => {
133
+ const hi = b >> 4;
134
+ const lo = b & 15;
135
+ return String.fromCharCode(97 + hi) + String.fromCharCode(97 + lo);
136
+ })
137
+ .join("");
138
+ } catch {
139
+ return null;
140
+ }
141
+ }
142
+
143
+ function getExtensionIdFromManifest() {
144
+ const manifest = readExtensionManifest();
145
+ if (!manifest?.key) return null;
146
+ return computeExtensionIdFromKey(manifest.key);
147
+ }
148
+
149
+ async function resolveExtensionId({ allowPrompt = true, preferConfig = false } = {}) {
150
+ const override = getExtensionIdOverride();
151
+ if (override) return { id: override, source: "override" };
152
+
153
+ const config = loadConfig();
154
+ if (preferConfig && config?.extensionId) {
155
+ return { id: config.extensionId, source: "config" };
156
+ }
157
+
158
+ const manifestId = getExtensionIdFromManifest();
159
+ if (manifestId) {
160
+ return { id: manifestId, source: "manifest" };
161
+ }
162
+
163
+ if (!preferConfig && config?.extensionId) {
164
+ return { id: config.extensionId, source: "config" };
165
+ }
166
+
167
+ if (!allowPrompt) {
168
+ return { id: null, source: "missing" };
169
+ }
170
+
171
+ const extensionId = await ask(color("bright", "Paste Extension ID: "));
172
+ return { id: extensionId || null, source: extensionId ? "prompt" : "missing" };
173
+ }
174
+
96
175
  function ensureDir(p) {
97
176
  mkdirSync(p, { recursive: true });
98
177
  }
@@ -109,14 +188,6 @@ function resolveNodePath() {
109
188
  return process.execPath;
110
189
  }
111
190
 
112
- function getPackageVersion() {
113
- try {
114
- const pkg = JSON.parse(readFileSync(PACKAGE_JSON, "utf-8"));
115
- if (typeof pkg?.version === "string") return pkg.version;
116
- } catch {}
117
- return null;
118
- }
119
-
120
191
  function writeHostWrapper(nodePath) {
121
192
  ensureDir(BASE_DIR);
122
193
  const script = `#!/bin/sh\n"${nodePath}" "${NATIVE_HOST_DST}"\n`;
@@ -265,21 +336,38 @@ ${color("cyan", "Browser automation plugin (native messaging + per-tab ownership
265
336
 
266
337
  if (command === "install") {
267
338
  await install();
339
+ } else if (command === "update") {
340
+ await update();
268
341
  } else if (command === "uninstall") {
269
342
  await uninstall();
270
343
  } else if (command === "status") {
271
344
  await status();
345
+ } else if (command === "agent-install") {
346
+ await agentInstall();
347
+ } else if (command === "agent-gateway") {
348
+ await agentGateway();
272
349
  } else {
273
350
  log(`
274
351
  ${color("bright", "Usage:")}
275
352
  npx @different-ai/opencode-browser install
353
+ npx @different-ai/opencode-browser update
276
354
  npx @different-ai/opencode-browser status
277
355
  npx @different-ai/opencode-browser uninstall
356
+ npx @different-ai/opencode-browser agent-install
357
+ npx @different-ai/opencode-browser agent-gateway
358
+
359
+ ${color("bright", "Options:")}
360
+ --extension-id <id> (or OPENCODE_BROWSER_EXTENSION_ID)
278
361
 
279
362
  ${color("bright", "Quick Start:")}
280
363
  1. Run: npx @different-ai/opencode-browser install
281
364
  2. Restart OpenCode
282
365
  3. Use: browser_navigate / browser_click / browser_snapshot
366
+
367
+ ${color("bright", "Agent Mode:")}
368
+ 1. Run: npx @different-ai/opencode-browser agent-install
369
+ 2. Set OPENCODE_BROWSER_BACKEND=agent
370
+ 3. Optionally run: npx @different-ai/opencode-browser agent-gateway
283
371
  `);
284
372
  }
285
373
 
@@ -302,24 +390,6 @@ async function install() {
302
390
  ensureDir(BASE_DIR);
303
391
  const srcExtensionDir = join(PACKAGE_ROOT, "extension");
304
392
  copyDirRecursive(srcExtensionDir, EXTENSION_DIR);
305
-
306
- const packageVersion = getPackageVersion();
307
- if (packageVersion) {
308
- const manifestPath = join(EXTENSION_DIR, "manifest.json");
309
- if (existsSync(manifestPath)) {
310
- try {
311
- const manifest = JSON.parse(readFileSync(manifestPath, "utf-8"));
312
- if (manifest.version !== packageVersion) {
313
- manifest.version = packageVersion;
314
- writeFileSync(manifestPath, JSON.stringify(manifest, null, 2) + "\n");
315
- success(`Updated extension manifest version to ${packageVersion}`);
316
- }
317
- } catch (e) {
318
- warn(`Could not update extension manifest: ${e.message || String(e)}`);
319
- }
320
- }
321
- }
322
-
323
393
  success(`Extension files copied to: ${EXTENSION_DIR}`);
324
394
 
325
395
  header("Step 3: Load & Pin Extension");
@@ -338,9 +408,13 @@ After loading, ${color("bright", "pin the extension")}: open the Extensions menu
338
408
 
339
409
  await ask(color("bright", "Press Enter when you've loaded and pinned the extension..."));
340
410
 
341
- header("Step 4: Get Extension ID");
411
+ header("Step 4: Extension ID");
342
412
 
343
- log(`
413
+ let resolved = await resolveExtensionId({ allowPrompt: false, preferConfig: true });
414
+ let extensionId = resolved.id;
415
+
416
+ if (!extensionId) {
417
+ log(`
344
418
  We need the extension ID to register the native messaging host.
345
419
 
346
420
  Find it at ${color("cyan", "chrome://extensions")}:
@@ -349,7 +423,22 @@ Find it at ${color("cyan", "chrome://extensions")}:
349
423
  - Copy the ${color("bright", "ID")}
350
424
  `);
351
425
 
352
- const extensionId = await ask(color("bright", "Paste Extension ID: "));
426
+ resolved = await resolveExtensionId({ allowPrompt: true, preferConfig: false });
427
+ extensionId = resolved.id;
428
+ } else if (resolved.source === "manifest") {
429
+ success(`Using fixed extension ID from manifest: ${extensionId}`);
430
+ log(`If you already loaded a different ID, rerun with --extension-id to override.`);
431
+ } else if (resolved.source === "config") {
432
+ success(`Using extension ID from config.json: ${extensionId}`);
433
+ } else if (resolved.source === "override") {
434
+ success(`Using extension ID override: ${extensionId}`);
435
+ }
436
+
437
+ if (!extensionId) {
438
+ error("Extension ID is required to continue.");
439
+ process.exit(1);
440
+ }
441
+
353
442
  if (!/^[a-p]{32}$/i.test(extensionId)) {
354
443
  warn("That doesn't look like a Chrome extension ID (expected 32 chars a-p). Continuing anyway.");
355
444
  }
@@ -566,10 +655,114 @@ Open Chrome and:
566
655
  ${color("bright", "Try it:")}
567
656
  Restart OpenCode and run: ${color("cyan", "browser_get_tabs")}
568
657
  `);
658
+ }
659
+
660
+ async function update() {
661
+ header("Update: Check Platform");
662
+
663
+ const osName = platform();
664
+ if (osName !== "darwin" && osName !== "linux") {
665
+ error(`Unsupported platform: ${osName}`);
666
+ error("OpenCode Browser currently supports macOS and Linux only.");
667
+ process.exit(1);
668
+ }
669
+ success(`Platform: ${osName === "darwin" ? "macOS" : "Linux"}`);
670
+
671
+ header("Step 1: Copy Extension Files");
672
+
673
+ ensureDir(BASE_DIR);
674
+ const srcExtensionDir = join(PACKAGE_ROOT, "extension");
675
+ copyDirRecursive(srcExtensionDir, EXTENSION_DIR);
676
+ success(`Extension files copied to: ${EXTENSION_DIR}`);
677
+
678
+ header("Step 2: Resolve Extension ID");
679
+
680
+ let resolved = await resolveExtensionId({ allowPrompt: false, preferConfig: true });
681
+ let extensionId = resolved.id;
682
+
683
+ if (!extensionId) {
684
+ log(`
685
+ We need the extension ID to register the native messaging host.
686
+
687
+ Find it at ${color("cyan", "chrome://extensions")}:
688
+ - Locate ${color("bright", "OpenCode Browser Automation")}
689
+ - Click ${color("bright", "Details")}
690
+ - Copy the ${color("bright", "ID")}
691
+ `);
692
+
693
+ resolved = await resolveExtensionId({ allowPrompt: true, preferConfig: false });
694
+ extensionId = resolved.id;
695
+ } else if (resolved.source === "manifest") {
696
+ success(`Using fixed extension ID from manifest: ${extensionId}`);
697
+ } else if (resolved.source === "config") {
698
+ success(`Using extension ID from config.json: ${extensionId}`);
699
+ } else if (resolved.source === "override") {
700
+ success(`Using extension ID override: ${extensionId}`);
701
+ }
702
+
703
+ if (!extensionId) {
704
+ error("Extension ID is required to continue.");
705
+ process.exit(1);
706
+ }
707
+
708
+ if (!/^[a-p]{32}$/i.test(extensionId)) {
709
+ warn("That doesn't look like a Chrome extension ID (expected 32 chars a-p). Continuing anyway.");
710
+ }
711
+
712
+ const manifestId = getExtensionIdFromManifest();
713
+ if (resolved.source === "config" && manifestId && manifestId !== extensionId) {
714
+ warn(`Manifest key implies ${manifestId}, but config.json uses ${extensionId}. Run update with --extension-id ${manifestId} to switch.`);
715
+ }
716
+
717
+ header("Step 3: Install Local Host + Broker");
718
+
719
+ const brokerSrc = join(PACKAGE_ROOT, "bin", "broker.cjs");
720
+ const nativeHostSrc = join(PACKAGE_ROOT, "bin", "native-host.cjs");
721
+
722
+ copyFileSync(brokerSrc, BROKER_DST);
723
+ copyFileSync(nativeHostSrc, NATIVE_HOST_DST);
724
+
725
+ try {
726
+ chmodSync(BROKER_DST, 0o755);
727
+ } catch {}
728
+ try {
729
+ chmodSync(NATIVE_HOST_DST, 0o755);
730
+ } catch {}
731
+
732
+ success(`Updated broker: ${BROKER_DST}`);
733
+ success(`Updated native host: ${NATIVE_HOST_DST}`);
734
+
735
+ const nodePath = resolveNodePath();
736
+ if (!/node(\.exe)?$/.test(nodePath)) {
737
+ warn(`Node not detected; using ${nodePath}. Set OPENCODE_BROWSER_NODE if needed.`);
738
+ }
739
+ const hostPath = writeHostWrapper(nodePath);
740
+ success(`Updated host wrapper: ${hostPath}`);
741
+
742
+ saveConfig({ extensionId, installedAt: new Date().toISOString(), nodePath });
743
+
744
+ header("Step 4: Register Native Messaging Host");
745
+
746
+ const hostDirs = getNativeHostDirs(osName);
747
+ for (const dir of hostDirs) {
748
+ try {
749
+ writeNativeHostManifest(dir, extensionId, hostPath);
750
+ success(`Wrote native host manifest: ${nativeHostManifestPath(dir)}`);
751
+ } catch {
752
+ warn(`Could not write native host manifest to: ${dir}`);
753
+ }
754
+ }
755
+
756
+ header("Update Complete!");
757
+
758
+ log(`
759
+ Reload the extension in ${color("cyan", "chrome://extensions")} and restart OpenCode.
760
+ `);
569
761
  }
570
762
 
571
763
 
572
764
  async function status() {
765
+
573
766
  header("Status");
574
767
 
575
768
  success(`Base dir: ${BASE_DIR}`);
@@ -585,6 +778,11 @@ async function status() {
585
778
  warn("No config.json found (run install)");
586
779
  }
587
780
 
781
+ const manifestId = getExtensionIdFromManifest();
782
+ if (manifestId) {
783
+ success(`Fixed extension ID (manifest): ${manifestId}`);
784
+ }
785
+
588
786
  if (cfg?.nodePath) {
589
787
  success(`Node path: ${cfg.nodePath}`);
590
788
  }
@@ -604,6 +802,31 @@ async function status() {
604
802
  }
605
803
  }
606
804
 
805
+ async function agentInstall() {
806
+ header("Agent Browser Install");
807
+
808
+ const extraArgs = process.argv.slice(3).join(" ");
809
+ const command = `npx agent-browser install ${extraArgs}`.trim();
810
+ try {
811
+ execSync(command, { stdio: "inherit" });
812
+ success("agent-browser install completed.");
813
+ } catch (err) {
814
+ error(`agent-browser install failed: ${err?.message || err}`);
815
+ }
816
+ }
817
+
818
+ async function agentGateway() {
819
+ header("Agent Browser Gateway");
820
+
821
+ const gatewayPath = join(PACKAGE_ROOT, "bin", "agent-gateway.cjs");
822
+ success(`Starting gateway: ${gatewayPath}`);
823
+
824
+ await new Promise((resolve) => {
825
+ const child = spawn(process.execPath, [gatewayPath], { stdio: "inherit" });
826
+ child.on("exit", resolve);
827
+ });
828
+ }
829
+
607
830
  async function uninstall() {
608
831
  header("Uninstall");
609
832
 
@@ -0,0 +1,35 @@
1
+ import plugin from "../dist/plugin.js";
2
+
3
+ const toolName = process.argv[2] ?? "browser_status";
4
+ const rawArgs = process.argv[3];
5
+
6
+ let args: Record<string, unknown> = {};
7
+ if (rawArgs) {
8
+ try {
9
+ args = JSON.parse(rawArgs);
10
+ } catch (error) {
11
+ console.error("Args must be valid JSON.");
12
+ console.error(String(error));
13
+ process.exit(1);
14
+ }
15
+ }
16
+
17
+ try {
18
+ const pluginInstance = await (plugin as any)({});
19
+ const tool = pluginInstance?.tool?.[toolName];
20
+ if (!tool) {
21
+ console.error(`Tool not found: ${toolName}`);
22
+ process.exit(1);
23
+ }
24
+
25
+ const result = await tool.execute(args, {});
26
+ if (typeof result === "string") {
27
+ console.log(result);
28
+ } else {
29
+ console.log(JSON.stringify(result, null, 2));
30
+ }
31
+ process.exit(0);
32
+ } catch (error) {
33
+ console.error(String(error));
34
+ process.exit(1);
35
+ }