@different-ai/opencode-browser 4.3.1 → 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.
- package/.opencode/skill/browser-automation/SKILL.md +7 -1
- package/README.md +66 -7
- package/bin/agent-gateway.cjs +129 -0
- package/bin/cli.js +254 -4
- package/bin/tool-test.ts +35 -0
- package/dist/plugin.js +768 -46
- package/extension/background.js +180 -35
- package/extension/manifest.json +4 -1
- package/package.json +6 -2
|
@@ -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
|
|
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://
|
|
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.
|
|
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
|
-
-
|
|
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,7 +26,8 @@ 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);
|
|
@@ -34,6 +35,7 @@ const PACKAGE_ROOT = join(__dirname, "..");
|
|
|
34
35
|
|
|
35
36
|
const BASE_DIR = join(homedir(), ".opencode-browser");
|
|
36
37
|
const EXTENSION_DIR = join(BASE_DIR, "extension");
|
|
38
|
+
const EXTENSION_MANIFEST_PATH = join(PACKAGE_ROOT, "extension", "manifest.json");
|
|
37
39
|
const BROKER_DST = join(BASE_DIR, "broker.cjs");
|
|
38
40
|
const NATIVE_HOST_DST = join(BASE_DIR, "native-host.cjs");
|
|
39
41
|
const NATIVE_HOST_WRAPPER = join(BASE_DIR, "host-wrapper.sh");
|
|
@@ -92,6 +94,84 @@ async function confirm(question) {
|
|
|
92
94
|
return answer.toLowerCase() === "y" || answer.toLowerCase() === "yes";
|
|
93
95
|
}
|
|
94
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
|
+
|
|
95
175
|
function ensureDir(p) {
|
|
96
176
|
mkdirSync(p, { recursive: true });
|
|
97
177
|
}
|
|
@@ -256,21 +336,38 @@ ${color("cyan", "Browser automation plugin (native messaging + per-tab ownership
|
|
|
256
336
|
|
|
257
337
|
if (command === "install") {
|
|
258
338
|
await install();
|
|
339
|
+
} else if (command === "update") {
|
|
340
|
+
await update();
|
|
259
341
|
} else if (command === "uninstall") {
|
|
260
342
|
await uninstall();
|
|
261
343
|
} else if (command === "status") {
|
|
262
344
|
await status();
|
|
345
|
+
} else if (command === "agent-install") {
|
|
346
|
+
await agentInstall();
|
|
347
|
+
} else if (command === "agent-gateway") {
|
|
348
|
+
await agentGateway();
|
|
263
349
|
} else {
|
|
264
350
|
log(`
|
|
265
351
|
${color("bright", "Usage:")}
|
|
266
352
|
npx @different-ai/opencode-browser install
|
|
353
|
+
npx @different-ai/opencode-browser update
|
|
267
354
|
npx @different-ai/opencode-browser status
|
|
268
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)
|
|
269
361
|
|
|
270
362
|
${color("bright", "Quick Start:")}
|
|
271
363
|
1. Run: npx @different-ai/opencode-browser install
|
|
272
364
|
2. Restart OpenCode
|
|
273
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
|
|
274
371
|
`);
|
|
275
372
|
}
|
|
276
373
|
|
|
@@ -311,9 +408,13 @@ After loading, ${color("bright", "pin the extension")}: open the Extensions menu
|
|
|
311
408
|
|
|
312
409
|
await ask(color("bright", "Press Enter when you've loaded and pinned the extension..."));
|
|
313
410
|
|
|
314
|
-
header("Step 4:
|
|
411
|
+
header("Step 4: Extension ID");
|
|
315
412
|
|
|
316
|
-
|
|
413
|
+
let resolved = await resolveExtensionId({ allowPrompt: false, preferConfig: true });
|
|
414
|
+
let extensionId = resolved.id;
|
|
415
|
+
|
|
416
|
+
if (!extensionId) {
|
|
417
|
+
log(`
|
|
317
418
|
We need the extension ID to register the native messaging host.
|
|
318
419
|
|
|
319
420
|
Find it at ${color("cyan", "chrome://extensions")}:
|
|
@@ -322,7 +423,22 @@ Find it at ${color("cyan", "chrome://extensions")}:
|
|
|
322
423
|
- Copy the ${color("bright", "ID")}
|
|
323
424
|
`);
|
|
324
425
|
|
|
325
|
-
|
|
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
|
+
|
|
326
442
|
if (!/^[a-p]{32}$/i.test(extensionId)) {
|
|
327
443
|
warn("That doesn't look like a Chrome extension ID (expected 32 chars a-p). Continuing anyway.");
|
|
328
444
|
}
|
|
@@ -539,10 +655,114 @@ Open Chrome and:
|
|
|
539
655
|
${color("bright", "Try it:")}
|
|
540
656
|
Restart OpenCode and run: ${color("cyan", "browser_get_tabs")}
|
|
541
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
|
+
`);
|
|
542
761
|
}
|
|
543
762
|
|
|
544
763
|
|
|
545
764
|
async function status() {
|
|
765
|
+
|
|
546
766
|
header("Status");
|
|
547
767
|
|
|
548
768
|
success(`Base dir: ${BASE_DIR}`);
|
|
@@ -558,6 +778,11 @@ async function status() {
|
|
|
558
778
|
warn("No config.json found (run install)");
|
|
559
779
|
}
|
|
560
780
|
|
|
781
|
+
const manifestId = getExtensionIdFromManifest();
|
|
782
|
+
if (manifestId) {
|
|
783
|
+
success(`Fixed extension ID (manifest): ${manifestId}`);
|
|
784
|
+
}
|
|
785
|
+
|
|
561
786
|
if (cfg?.nodePath) {
|
|
562
787
|
success(`Node path: ${cfg.nodePath}`);
|
|
563
788
|
}
|
|
@@ -577,6 +802,31 @@ async function status() {
|
|
|
577
802
|
}
|
|
578
803
|
}
|
|
579
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
|
+
|
|
580
830
|
async function uninstall() {
|
|
581
831
|
header("Uninstall");
|
|
582
832
|
|
package/bin/tool-test.ts
ADDED
|
@@ -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
|
+
}
|