@different-ai/opencode-browser 4.3.2 → 4.5.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 +75 -11
- package/bin/agent-gateway.cjs +129 -0
- package/bin/broker.cjs +158 -11
- package/bin/cli.js +254 -31
- package/bin/tool-test.ts +35 -0
- package/dist/plugin.js +803 -46
- package/extension/background.js +180 -35
- package/extension/manifest.json +4 -1
- package/package.json +8 -2
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
|
+
}
|