@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.
- 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 -31
- 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,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:
|
|
411
|
+
header("Step 4: Extension ID");
|
|
342
412
|
|
|
343
|
-
|
|
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
|
-
|
|
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
|
|
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
|
+
}
|