@different-ai/opencode-browser 4.5.1 → 4.6.1
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 +14 -1
- package/README.md +53 -2
- package/bin/broker.cjs +18 -2
- package/bin/cli.js +395 -52
- package/bin/native-host.cjs +17 -1
- package/dist/plugin.js +374 -103
- package/extension/background.js +627 -20
- package/extension/manifest.json +4 -2
- package/package.json +15 -9
- package/.opencode/skill/github-release/SKILL.md +0 -12
package/bin/cli.js
CHANGED
|
@@ -21,12 +21,12 @@ import {
|
|
|
21
21
|
unlinkSync,
|
|
22
22
|
chmodSync,
|
|
23
23
|
} from "fs";
|
|
24
|
-
import { homedir, platform } from "os";
|
|
24
|
+
import { homedir, platform, userInfo } from "os";
|
|
25
25
|
import { join, dirname } from "path";
|
|
26
|
-
import { fileURLToPath } from "url";
|
|
26
|
+
import { fileURLToPath, pathToFileURL } from "url";
|
|
27
27
|
import { createInterface } from "readline";
|
|
28
28
|
import { createConnection } from "net";
|
|
29
|
-
import { execSync, spawn } from "child_process";
|
|
29
|
+
import { execSync, spawn, spawnSync } from "child_process";
|
|
30
30
|
import { createHash } from "crypto";
|
|
31
31
|
|
|
32
32
|
const __filename = fileURLToPath(import.meta.url);
|
|
@@ -38,11 +38,15 @@ const EXTENSION_DIR = join(BASE_DIR, "extension");
|
|
|
38
38
|
const EXTENSION_MANIFEST_PATH = join(PACKAGE_ROOT, "extension", "manifest.json");
|
|
39
39
|
const BROKER_DST = join(BASE_DIR, "broker.cjs");
|
|
40
40
|
const NATIVE_HOST_DST = join(BASE_DIR, "native-host.cjs");
|
|
41
|
-
const NATIVE_HOST_WRAPPER = join(BASE_DIR, "host-wrapper.sh");
|
|
42
41
|
const CONFIG_DST = join(BASE_DIR, "config.json");
|
|
43
|
-
const BROKER_SOCKET = join(BASE_DIR, "broker.sock");
|
|
44
42
|
|
|
45
43
|
const NATIVE_HOST_NAME = "com.opencode.browser_automation";
|
|
44
|
+
const OS_NAME = platform();
|
|
45
|
+
const NATIVE_HOST_WRAPPER = join(
|
|
46
|
+
BASE_DIR,
|
|
47
|
+
OS_NAME === "win32" ? "host-wrapper.cmd" : "host-wrapper.sh"
|
|
48
|
+
);
|
|
49
|
+
const BROKER_SOCKET = getBrokerSocketPath();
|
|
46
50
|
|
|
47
51
|
const COLORS = {
|
|
48
52
|
reset: "\x1b[0m",
|
|
@@ -57,6 +61,56 @@ function color(c, text) {
|
|
|
57
61
|
return `${COLORS[c]}${text}${COLORS.reset}`;
|
|
58
62
|
}
|
|
59
63
|
|
|
64
|
+
function isWindows() {
|
|
65
|
+
return OS_NAME === "win32";
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
function getSafePipeName() {
|
|
69
|
+
try {
|
|
70
|
+
const username = userInfo().username || "user";
|
|
71
|
+
return `opencode-browser-${username}`.replace(/[^a-zA-Z0-9._-]/g, "_");
|
|
72
|
+
} catch {
|
|
73
|
+
return "opencode-browser";
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
function getBrokerSocketPath() {
|
|
78
|
+
const override = process.env.OPENCODE_BROWSER_BROKER_SOCKET;
|
|
79
|
+
if (override) return override;
|
|
80
|
+
if (OS_NAME === "win32") return `\\\\.\\pipe\\${getSafePipeName()}`;
|
|
81
|
+
return join(BASE_DIR, "broker.sock");
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
function getWindowsRegistryTargets() {
|
|
85
|
+
return [
|
|
86
|
+
{ name: "Chrome", key: "HKCU\\Software\\Google\\Chrome\\NativeMessagingHosts" },
|
|
87
|
+
{ name: "Chromium", key: "HKCU\\Software\\Chromium\\NativeMessagingHosts" },
|
|
88
|
+
{ name: "Brave", key: "HKCU\\Software\\BraveSoftware\\Brave-Browser\\NativeMessagingHosts" },
|
|
89
|
+
{ name: "Edge", key: "HKCU\\Software\\Microsoft\\Edge\\NativeMessagingHosts" },
|
|
90
|
+
];
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
function runRegCommand(args) {
|
|
94
|
+
try {
|
|
95
|
+
const result = spawnSync("reg", args, { stdio: "ignore" });
|
|
96
|
+
return result.status === 0;
|
|
97
|
+
} catch {
|
|
98
|
+
return false;
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
function queryRegistryDefaultValue(key) {
|
|
103
|
+
try {
|
|
104
|
+
const result = spawnSync("reg", ["query", key, "/ve"], { encoding: "utf8" });
|
|
105
|
+
if (result.status !== 0) return null;
|
|
106
|
+
const output = String(result.stdout || "");
|
|
107
|
+
const match = output.match(/REG_SZ\s+(.+)\s*$/m);
|
|
108
|
+
return match ? match[1].trim() : null;
|
|
109
|
+
} catch {
|
|
110
|
+
return null;
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
|
|
60
114
|
function log(msg) {
|
|
61
115
|
console.log(msg);
|
|
62
116
|
}
|
|
@@ -180,7 +234,8 @@ function resolveNodePath() {
|
|
|
180
234
|
if (process.env.OPENCODE_BROWSER_NODE) return process.env.OPENCODE_BROWSER_NODE;
|
|
181
235
|
if (process.execPath && /node(\.exe)?$/.test(process.execPath)) return process.execPath;
|
|
182
236
|
try {
|
|
183
|
-
const
|
|
237
|
+
const command = isWindows() ? "where node" : "which node";
|
|
238
|
+
const output = execSync(command, { stdio: ["ignore", "pipe", "ignore"] })
|
|
184
239
|
.toString("utf8")
|
|
185
240
|
.trim();
|
|
186
241
|
if (output) return output;
|
|
@@ -190,6 +245,11 @@ function resolveNodePath() {
|
|
|
190
245
|
|
|
191
246
|
function writeHostWrapper(nodePath) {
|
|
192
247
|
ensureDir(BASE_DIR);
|
|
248
|
+
if (isWindows()) {
|
|
249
|
+
const script = `@echo off\r\n"${nodePath}" "${NATIVE_HOST_DST}"\r\n`;
|
|
250
|
+
writeFileSync(NATIVE_HOST_WRAPPER, script);
|
|
251
|
+
return NATIVE_HOST_WRAPPER;
|
|
252
|
+
}
|
|
193
253
|
const script = `#!/bin/sh\n"${nodePath}" "${NATIVE_HOST_DST}"\n`;
|
|
194
254
|
writeFileSync(NATIVE_HOST_WRAPPER, script, { mode: 0o755 });
|
|
195
255
|
chmodSync(NATIVE_HOST_WRAPPER, 0o755);
|
|
@@ -276,6 +336,7 @@ function copyDirRecursive(srcDir, destDir) {
|
|
|
276
336
|
}
|
|
277
337
|
|
|
278
338
|
function getNativeHostDirs(osName) {
|
|
339
|
+
if (osName === "win32") return [];
|
|
279
340
|
if (osName === "darwin") {
|
|
280
341
|
const base = join(homedir(), "Library", "Application Support");
|
|
281
342
|
return [
|
|
@@ -312,6 +373,58 @@ function writeNativeHostManifest(dir, extensionId, hostPath) {
|
|
|
312
373
|
writeFileSync(nativeHostManifestPath(dir), JSON.stringify(manifest, null, 2) + "\n");
|
|
313
374
|
}
|
|
314
375
|
|
|
376
|
+
function writeWindowsNativeHostManifest(extensionId, hostPath) {
|
|
377
|
+
const manifestPath = nativeHostManifestPath(BASE_DIR);
|
|
378
|
+
writeNativeHostManifest(BASE_DIR, extensionId, hostPath);
|
|
379
|
+
return manifestPath;
|
|
380
|
+
}
|
|
381
|
+
|
|
382
|
+
function registerWindowsNativeHost(manifestPath) {
|
|
383
|
+
for (const target of getWindowsRegistryTargets()) {
|
|
384
|
+
const key = `${target.key}\\${NATIVE_HOST_NAME}`;
|
|
385
|
+
const ok = runRegCommand(["add", key, "/ve", "/t", "REG_SZ", "/d", manifestPath, "/f"]);
|
|
386
|
+
if (ok) {
|
|
387
|
+
success(`Registered native host for ${target.name}: ${key}`);
|
|
388
|
+
} else {
|
|
389
|
+
warn(`Could not register native host for ${target.name}: ${key}`);
|
|
390
|
+
}
|
|
391
|
+
}
|
|
392
|
+
}
|
|
393
|
+
|
|
394
|
+
function unregisterWindowsNativeHost() {
|
|
395
|
+
for (const target of getWindowsRegistryTargets()) {
|
|
396
|
+
const key = `${target.key}\\${NATIVE_HOST_NAME}`;
|
|
397
|
+
const ok = runRegCommand(["delete", key, "/f"]);
|
|
398
|
+
if (ok) {
|
|
399
|
+
success(`Removed native host registry: ${key}`);
|
|
400
|
+
} else {
|
|
401
|
+
warn(`Could not remove native host registry: ${key}`);
|
|
402
|
+
}
|
|
403
|
+
}
|
|
404
|
+
}
|
|
405
|
+
|
|
406
|
+
function reportWindowsNativeHostStatus() {
|
|
407
|
+
const manifestPath = nativeHostManifestPath(BASE_DIR);
|
|
408
|
+
if (existsSync(manifestPath)) {
|
|
409
|
+
success(`Native host manifest: ${manifestPath}`);
|
|
410
|
+
} else {
|
|
411
|
+
warn(`Native host manifest missing: ${manifestPath}`);
|
|
412
|
+
}
|
|
413
|
+
|
|
414
|
+
let foundAny = false;
|
|
415
|
+
for (const target of getWindowsRegistryTargets()) {
|
|
416
|
+
const key = `${target.key}\\${NATIVE_HOST_NAME}`;
|
|
417
|
+
const value = queryRegistryDefaultValue(key);
|
|
418
|
+
if (value) {
|
|
419
|
+
foundAny = true;
|
|
420
|
+
success(`Registry (${target.name}): ${key}`);
|
|
421
|
+
}
|
|
422
|
+
}
|
|
423
|
+
if (!foundAny) {
|
|
424
|
+
warn("No native host registry entries found. Run: npx @different-ai/opencode-browser install");
|
|
425
|
+
}
|
|
426
|
+
}
|
|
427
|
+
|
|
315
428
|
function loadConfig() {
|
|
316
429
|
try {
|
|
317
430
|
if (!existsSync(CONFIG_DST)) return null;
|
|
@@ -326,6 +439,179 @@ function saveConfig(config) {
|
|
|
326
439
|
writeFileSync(CONFIG_DST, JSON.stringify(config, null, 2) + "\n");
|
|
327
440
|
}
|
|
328
441
|
|
|
442
|
+
async function loadPluginTools() {
|
|
443
|
+
const pluginPath = join(PACKAGE_ROOT, "dist", "plugin.js");
|
|
444
|
+
if (!existsSync(pluginPath)) {
|
|
445
|
+
throw new Error("dist/plugin.js is missing. Run `bun run build` first.");
|
|
446
|
+
}
|
|
447
|
+
|
|
448
|
+
const mod = await import(pathToFileURL(pluginPath).href);
|
|
449
|
+
const factory = mod?.default;
|
|
450
|
+
if (typeof factory !== "function") {
|
|
451
|
+
throw new Error("Could not load plugin factory from dist/plugin.js");
|
|
452
|
+
}
|
|
453
|
+
|
|
454
|
+
const pluginInstance = await factory({});
|
|
455
|
+
const tools = pluginInstance?.tool;
|
|
456
|
+
if (!tools || typeof tools !== "object") {
|
|
457
|
+
throw new Error("Plugin did not expose any tools");
|
|
458
|
+
}
|
|
459
|
+
return tools;
|
|
460
|
+
}
|
|
461
|
+
|
|
462
|
+
function parseJsonArg(raw, fallback = {}) {
|
|
463
|
+
if (!raw) return fallback;
|
|
464
|
+
try {
|
|
465
|
+
return JSON.parse(raw);
|
|
466
|
+
} catch (err) {
|
|
467
|
+
throw new Error(`Expected JSON args. Received: ${raw}`);
|
|
468
|
+
}
|
|
469
|
+
}
|
|
470
|
+
|
|
471
|
+
function parseMaybeJson(value) {
|
|
472
|
+
if (typeof value !== "string") return value;
|
|
473
|
+
const trimmed = value.trim();
|
|
474
|
+
if (!trimmed) return value;
|
|
475
|
+
if (!["{", "[", '"'].includes(trimmed[0])) return value;
|
|
476
|
+
try {
|
|
477
|
+
return JSON.parse(trimmed);
|
|
478
|
+
} catch {
|
|
479
|
+
return value;
|
|
480
|
+
}
|
|
481
|
+
}
|
|
482
|
+
|
|
483
|
+
function getToolArgJson() {
|
|
484
|
+
const byFlag = getFlagValue("--args");
|
|
485
|
+
if (byFlag != null) return byFlag;
|
|
486
|
+
return process.argv[4] || null;
|
|
487
|
+
}
|
|
488
|
+
|
|
489
|
+
async function executeTool(toolName, args = {}) {
|
|
490
|
+
const tools = await loadPluginTools();
|
|
491
|
+
const tool = tools?.[toolName];
|
|
492
|
+
if (!tool || typeof tool.execute !== "function") {
|
|
493
|
+
const available = Object.keys(tools || {})
|
|
494
|
+
.sort()
|
|
495
|
+
.join(", ");
|
|
496
|
+
throw new Error(`Unknown tool: ${toolName}. Available: ${available}`);
|
|
497
|
+
}
|
|
498
|
+
|
|
499
|
+
return await tool.execute(args, {});
|
|
500
|
+
}
|
|
501
|
+
|
|
502
|
+
async function listTools() {
|
|
503
|
+
header("Browser Tools");
|
|
504
|
+
const tools = await loadPluginTools();
|
|
505
|
+
const names = Object.keys(tools).sort();
|
|
506
|
+
if (!names.length) {
|
|
507
|
+
warn("No tools found in plugin.");
|
|
508
|
+
return;
|
|
509
|
+
}
|
|
510
|
+
|
|
511
|
+
log(`Found ${names.length} tools:\n`);
|
|
512
|
+
for (const name of names) {
|
|
513
|
+
const description = tools[name]?.description || "(no description)";
|
|
514
|
+
log(`- ${name}: ${description}`);
|
|
515
|
+
}
|
|
516
|
+
}
|
|
517
|
+
|
|
518
|
+
async function runToolCommand() {
|
|
519
|
+
const toolName = process.argv[3];
|
|
520
|
+
if (!toolName) {
|
|
521
|
+
throw new Error("Usage: npx @different-ai/opencode-browser tool <toolName> [argsJson]");
|
|
522
|
+
}
|
|
523
|
+
|
|
524
|
+
const args = parseJsonArg(getToolArgJson(), {});
|
|
525
|
+
const result = await executeTool(toolName, args);
|
|
526
|
+
|
|
527
|
+
if (typeof result === "string") {
|
|
528
|
+
log(result);
|
|
529
|
+
return;
|
|
530
|
+
}
|
|
531
|
+
log(JSON.stringify(result, null, 2));
|
|
532
|
+
}
|
|
533
|
+
|
|
534
|
+
function asNumber(value, fallback = 0) {
|
|
535
|
+
const n = Number(value);
|
|
536
|
+
return Number.isFinite(n) ? n : fallback;
|
|
537
|
+
}
|
|
538
|
+
|
|
539
|
+
function readTabId(value) {
|
|
540
|
+
const parsed = parseMaybeJson(value);
|
|
541
|
+
if (parsed && Number.isFinite(parsed.tabId)) return parsed.tabId;
|
|
542
|
+
if (parsed?.content && Number.isFinite(parsed.content.tabId)) return parsed.content.tabId;
|
|
543
|
+
return null;
|
|
544
|
+
}
|
|
545
|
+
|
|
546
|
+
async function selfTest() {
|
|
547
|
+
header("CLI Self-Test");
|
|
548
|
+
log("Running extension-backed smoke test via plugin tools...");
|
|
549
|
+
|
|
550
|
+
const statusRaw = await executeTool("browser_status", {});
|
|
551
|
+
const status = parseMaybeJson(statusRaw);
|
|
552
|
+
if (!status || status.broker !== true || status.hostConnected !== true) {
|
|
553
|
+
throw new Error(
|
|
554
|
+
"browser_status indicates the extension is not connected. Run `npx @different-ai/opencode-browser install` and click the extension icon in Chrome."
|
|
555
|
+
);
|
|
556
|
+
}
|
|
557
|
+
|
|
558
|
+
const fixtureUrl = "https://www.w3.org/WAI/ARIA/apg/patterns/listbox/examples/listbox-scrollable/";
|
|
559
|
+
const openRaw = await executeTool("browser_open_tab", { url: fixtureUrl, active: false });
|
|
560
|
+
const tabId = readTabId(openRaw);
|
|
561
|
+
if (!Number.isFinite(tabId)) {
|
|
562
|
+
throw new Error("Failed to read tabId from browser_open_tab output");
|
|
563
|
+
}
|
|
564
|
+
await executeTool("browser_wait", { ms: 250 });
|
|
565
|
+
|
|
566
|
+
const beforeRaw = await executeTool("browser_query", {
|
|
567
|
+
selector: "[role='listbox']",
|
|
568
|
+
mode: "property",
|
|
569
|
+
property: "scrollTop",
|
|
570
|
+
tabId,
|
|
571
|
+
});
|
|
572
|
+
const before = asNumber(parseMaybeJson(beforeRaw)?.value, 0);
|
|
573
|
+
|
|
574
|
+
await executeTool("browser_click", {
|
|
575
|
+
selector: "text:Neptunium",
|
|
576
|
+
tabId,
|
|
577
|
+
timeoutMs: 3000,
|
|
578
|
+
pollMs: 150,
|
|
579
|
+
});
|
|
580
|
+
|
|
581
|
+
const selectedRaw = await executeTool("browser_query", {
|
|
582
|
+
selector: "[aria-selected='true']",
|
|
583
|
+
mode: "text",
|
|
584
|
+
tabId,
|
|
585
|
+
});
|
|
586
|
+
const selectedText = String(parseMaybeJson(selectedRaw) || "");
|
|
587
|
+
if (!selectedText.toLowerCase().includes("neptunium")) {
|
|
588
|
+
throw new Error(`Click verification failed. Expected selected text to include Neptunium, got: ${selectedText}`);
|
|
589
|
+
}
|
|
590
|
+
|
|
591
|
+
await executeTool("browser_scroll", {
|
|
592
|
+
selector: "[role='listbox']",
|
|
593
|
+
y: 320,
|
|
594
|
+
tabId,
|
|
595
|
+
timeoutMs: 2000,
|
|
596
|
+
pollMs: 100,
|
|
597
|
+
});
|
|
598
|
+
await executeTool("browser_wait", { ms: 250 });
|
|
599
|
+
|
|
600
|
+
const afterRaw = await executeTool("browser_query", {
|
|
601
|
+
selector: "[role='listbox']",
|
|
602
|
+
mode: "property",
|
|
603
|
+
property: "scrollTop",
|
|
604
|
+
tabId,
|
|
605
|
+
});
|
|
606
|
+
const after = asNumber(parseMaybeJson(afterRaw)?.value, 0);
|
|
607
|
+
|
|
608
|
+
if (after <= before) {
|
|
609
|
+
throw new Error(`Scroll verification failed. Expected scrollTop to increase (before=${before}, after=${after}).`);
|
|
610
|
+
}
|
|
611
|
+
|
|
612
|
+
success("Self-test passed: click + selector text + container scroll are working.");
|
|
613
|
+
}
|
|
614
|
+
|
|
329
615
|
async function main() {
|
|
330
616
|
const command = process.argv[2];
|
|
331
617
|
|
|
@@ -338,6 +624,12 @@ ${color("cyan", "Browser automation plugin (native messaging + per-tab ownership
|
|
|
338
624
|
await install();
|
|
339
625
|
} else if (command === "update") {
|
|
340
626
|
await update();
|
|
627
|
+
} else if (command === "tools") {
|
|
628
|
+
await listTools();
|
|
629
|
+
} else if (command === "tool") {
|
|
630
|
+
await runToolCommand();
|
|
631
|
+
} else if (command === "self-test") {
|
|
632
|
+
await selfTest();
|
|
341
633
|
} else if (command === "uninstall") {
|
|
342
634
|
await uninstall();
|
|
343
635
|
} else if (command === "status") {
|
|
@@ -353,11 +645,15 @@ ${color("bright", "Usage:")}
|
|
|
353
645
|
npx @different-ai/opencode-browser update
|
|
354
646
|
npx @different-ai/opencode-browser status
|
|
355
647
|
npx @different-ai/opencode-browser uninstall
|
|
648
|
+
npx @different-ai/opencode-browser tools
|
|
649
|
+
npx @different-ai/opencode-browser tool <toolName> [argsJson]
|
|
650
|
+
npx @different-ai/opencode-browser self-test
|
|
356
651
|
npx @different-ai/opencode-browser agent-install
|
|
357
652
|
npx @different-ai/opencode-browser agent-gateway
|
|
358
653
|
|
|
359
654
|
${color("bright", "Options:")}
|
|
360
655
|
--extension-id <id> (or OPENCODE_BROWSER_EXTENSION_ID)
|
|
656
|
+
--args '{"selector":"text:Inbox"}' (for tool command)
|
|
361
657
|
|
|
362
658
|
${color("bright", "Quick Start:")}
|
|
363
659
|
1. Run: npx @different-ai/opencode-browser install
|
|
@@ -372,18 +668,19 @@ ${color("bright", "Agent Mode:")}
|
|
|
372
668
|
}
|
|
373
669
|
|
|
374
670
|
rl.close();
|
|
671
|
+
process.exit(0);
|
|
375
672
|
}
|
|
376
673
|
|
|
377
674
|
async function install() {
|
|
378
675
|
header("Step 1: Check Platform");
|
|
379
676
|
|
|
380
|
-
const osName =
|
|
381
|
-
if (osName !== "darwin" && osName !== "linux") {
|
|
677
|
+
const osName = OS_NAME;
|
|
678
|
+
if (osName !== "darwin" && osName !== "linux" && osName !== "win32") {
|
|
382
679
|
error(`Unsupported platform: ${osName}`);
|
|
383
|
-
error("OpenCode Browser currently supports macOS and
|
|
680
|
+
error("OpenCode Browser currently supports macOS, Linux, and Windows only.");
|
|
384
681
|
process.exit(1);
|
|
385
682
|
}
|
|
386
|
-
success(`Platform: ${osName === "darwin" ? "macOS" : "Linux"}`);
|
|
683
|
+
success(`Platform: ${osName === "darwin" ? "macOS" : osName === "win32" ? "Windows" : "Linux"}`);
|
|
387
684
|
|
|
388
685
|
header("Step 2: Copy Extension Files");
|
|
389
686
|
|
|
@@ -472,13 +769,19 @@ Find it at ${color("cyan", "chrome://extensions")}:
|
|
|
472
769
|
|
|
473
770
|
header("Step 6: Register Native Messaging Host");
|
|
474
771
|
|
|
475
|
-
|
|
476
|
-
|
|
477
|
-
|
|
478
|
-
|
|
479
|
-
|
|
480
|
-
|
|
481
|
-
|
|
772
|
+
if (osName === "win32") {
|
|
773
|
+
const manifestPath = writeWindowsNativeHostManifest(extensionId, hostPath);
|
|
774
|
+
success(`Wrote native host manifest: ${manifestPath}`);
|
|
775
|
+
registerWindowsNativeHost(manifestPath);
|
|
776
|
+
} else {
|
|
777
|
+
const hostDirs = getNativeHostDirs(osName);
|
|
778
|
+
for (const dir of hostDirs) {
|
|
779
|
+
try {
|
|
780
|
+
writeNativeHostManifest(dir, extensionId, hostPath);
|
|
781
|
+
success(`Wrote native host manifest: ${nativeHostManifestPath(dir)}`);
|
|
782
|
+
} catch (e) {
|
|
783
|
+
warn(`Could not write native host manifest to: ${dir}`);
|
|
784
|
+
}
|
|
482
785
|
}
|
|
483
786
|
}
|
|
484
787
|
|
|
@@ -509,9 +812,13 @@ Find it at ${color("cyan", "chrome://extensions")}:
|
|
|
509
812
|
return jsonPath;
|
|
510
813
|
}
|
|
511
814
|
|
|
815
|
+
const globalConfigLabel =
|
|
816
|
+
osName === "win32"
|
|
817
|
+
? "2) Global (%USERPROFILE%\\.config\\opencode\\opencode.json)"
|
|
818
|
+
: "2) Global (~/.config/opencode/opencode.json)";
|
|
512
819
|
const configOptions = [
|
|
513
820
|
"1) Project (./opencode.json or opencode.jsonc)",
|
|
514
|
-
|
|
821
|
+
globalConfigLabel,
|
|
515
822
|
"3) Custom path",
|
|
516
823
|
"4) Skip (does nothing)",
|
|
517
824
|
];
|
|
@@ -526,8 +833,12 @@ Find it at ${color("cyan", "chrome://extensions")}:
|
|
|
526
833
|
configDir = process.cwd();
|
|
527
834
|
configPath = findOpenCodeConfigPath(configDir);
|
|
528
835
|
} else if (selection === "2") {
|
|
529
|
-
|
|
530
|
-
|
|
836
|
+
if (osName === "win32") {
|
|
837
|
+
configDir = join(homedir(), ".config", "opencode");
|
|
838
|
+
} else {
|
|
839
|
+
const xdgConfig = process.env.XDG_CONFIG_HOME;
|
|
840
|
+
configDir = xdgConfig ? join(xdgConfig, "opencode") : join(homedir(), ".config", "opencode");
|
|
841
|
+
}
|
|
531
842
|
configPath = findOpenCodeConfigPath(configDir);
|
|
532
843
|
} else if (selection === "3") {
|
|
533
844
|
const customPath = await ask("Enter full path to opencode.json or opencode.jsonc: ");
|
|
@@ -660,13 +971,13 @@ Open Chrome and:
|
|
|
660
971
|
async function update() {
|
|
661
972
|
header("Update: Check Platform");
|
|
662
973
|
|
|
663
|
-
const osName =
|
|
664
|
-
if (osName !== "darwin" && osName !== "linux") {
|
|
974
|
+
const osName = OS_NAME;
|
|
975
|
+
if (osName !== "darwin" && osName !== "linux" && osName !== "win32") {
|
|
665
976
|
error(`Unsupported platform: ${osName}`);
|
|
666
|
-
error("OpenCode Browser currently supports macOS and
|
|
977
|
+
error("OpenCode Browser currently supports macOS, Linux, and Windows only.");
|
|
667
978
|
process.exit(1);
|
|
668
979
|
}
|
|
669
|
-
success(`Platform: ${osName === "darwin" ? "macOS" : "Linux"}`);
|
|
980
|
+
success(`Platform: ${osName === "darwin" ? "macOS" : osName === "win32" ? "Windows" : "Linux"}`);
|
|
670
981
|
|
|
671
982
|
header("Step 1: Copy Extension Files");
|
|
672
983
|
|
|
@@ -743,13 +1054,19 @@ Find it at ${color("cyan", "chrome://extensions")}:
|
|
|
743
1054
|
|
|
744
1055
|
header("Step 4: Register Native Messaging Host");
|
|
745
1056
|
|
|
746
|
-
|
|
747
|
-
|
|
748
|
-
|
|
749
|
-
|
|
750
|
-
|
|
751
|
-
|
|
752
|
-
|
|
1057
|
+
if (osName === "win32") {
|
|
1058
|
+
const manifestPath = writeWindowsNativeHostManifest(extensionId, hostPath);
|
|
1059
|
+
success(`Wrote native host manifest: ${manifestPath}`);
|
|
1060
|
+
registerWindowsNativeHost(manifestPath);
|
|
1061
|
+
} else {
|
|
1062
|
+
const hostDirs = getNativeHostDirs(osName);
|
|
1063
|
+
for (const dir of hostDirs) {
|
|
1064
|
+
try {
|
|
1065
|
+
writeNativeHostManifest(dir, extensionId, hostPath);
|
|
1066
|
+
success(`Wrote native host manifest: ${nativeHostManifestPath(dir)}`);
|
|
1067
|
+
} catch {
|
|
1068
|
+
warn(`Could not write native host manifest to: ${dir}`);
|
|
1069
|
+
}
|
|
753
1070
|
}
|
|
754
1071
|
}
|
|
755
1072
|
|
|
@@ -770,6 +1087,7 @@ async function status() {
|
|
|
770
1087
|
success(`Broker installed: ${existsSync(BROKER_DST)}`);
|
|
771
1088
|
success(`Native host installed: ${existsSync(NATIVE_HOST_DST)}`);
|
|
772
1089
|
success(`Host wrapper installed: ${existsSync(NATIVE_HOST_WRAPPER)}`);
|
|
1090
|
+
success(`Broker socket: ${BROKER_SOCKET}`);
|
|
773
1091
|
|
|
774
1092
|
const cfg = loadConfig();
|
|
775
1093
|
if (cfg?.extensionId) {
|
|
@@ -787,18 +1105,29 @@ async function status() {
|
|
|
787
1105
|
success(`Node path: ${cfg.nodePath}`);
|
|
788
1106
|
}
|
|
789
1107
|
|
|
790
|
-
const osName =
|
|
791
|
-
|
|
792
|
-
|
|
793
|
-
|
|
794
|
-
const
|
|
795
|
-
|
|
796
|
-
|
|
797
|
-
|
|
1108
|
+
const osName = OS_NAME;
|
|
1109
|
+
if (osName === "win32") {
|
|
1110
|
+
reportWindowsNativeHostStatus();
|
|
1111
|
+
} else {
|
|
1112
|
+
const hostDirs = getNativeHostDirs(osName);
|
|
1113
|
+
let foundAny = false;
|
|
1114
|
+
for (const dir of hostDirs) {
|
|
1115
|
+
const p = nativeHostManifestPath(dir);
|
|
1116
|
+
if (existsSync(p)) {
|
|
1117
|
+
foundAny = true;
|
|
1118
|
+
success(`Native host manifest: ${p}`);
|
|
1119
|
+
}
|
|
1120
|
+
}
|
|
1121
|
+
if (!foundAny) {
|
|
1122
|
+
warn("No native host manifest found. Run: npx @different-ai/opencode-browser install");
|
|
798
1123
|
}
|
|
799
1124
|
}
|
|
800
|
-
|
|
801
|
-
|
|
1125
|
+
|
|
1126
|
+
const brokerStatus = await getBrokerStatus(1000);
|
|
1127
|
+
if (brokerStatus.ok) {
|
|
1128
|
+
success(`Broker status: ok (hostConnected=${!!brokerStatus.data?.hostConnected})`);
|
|
1129
|
+
} else {
|
|
1130
|
+
warn(`Broker status: ${brokerStatus.error || "unavailable"}`);
|
|
802
1131
|
}
|
|
803
1132
|
}
|
|
804
1133
|
|
|
@@ -830,20 +1159,34 @@ async function agentGateway() {
|
|
|
830
1159
|
async function uninstall() {
|
|
831
1160
|
header("Uninstall");
|
|
832
1161
|
|
|
833
|
-
const osName =
|
|
834
|
-
|
|
835
|
-
|
|
836
|
-
const
|
|
837
|
-
if (
|
|
838
|
-
|
|
839
|
-
|
|
840
|
-
|
|
841
|
-
|
|
842
|
-
|
|
1162
|
+
const osName = OS_NAME;
|
|
1163
|
+
if (osName === "win32") {
|
|
1164
|
+
unregisterWindowsNativeHost();
|
|
1165
|
+
const manifestPath = nativeHostManifestPath(BASE_DIR);
|
|
1166
|
+
if (existsSync(manifestPath)) {
|
|
1167
|
+
try {
|
|
1168
|
+
unlinkSync(manifestPath);
|
|
1169
|
+
success(`Removed native host manifest: ${manifestPath}`);
|
|
1170
|
+
} catch {
|
|
1171
|
+
warn(`Could not remove: ${manifestPath}`);
|
|
1172
|
+
}
|
|
1173
|
+
}
|
|
1174
|
+
} else {
|
|
1175
|
+
const hostDirs = getNativeHostDirs(osName);
|
|
1176
|
+
for (const dir of hostDirs) {
|
|
1177
|
+
const p = nativeHostManifestPath(dir);
|
|
1178
|
+
if (!existsSync(p)) continue;
|
|
1179
|
+
try {
|
|
1180
|
+
unlinkSync(p);
|
|
1181
|
+
success(`Removed native host manifest: ${p}`);
|
|
1182
|
+
} catch {
|
|
1183
|
+
warn(`Could not remove: ${p}`);
|
|
1184
|
+
}
|
|
843
1185
|
}
|
|
844
1186
|
}
|
|
845
1187
|
|
|
846
|
-
|
|
1188
|
+
const unixSocketPath = join(BASE_DIR, "broker.sock");
|
|
1189
|
+
for (const p of [BROKER_DST, NATIVE_HOST_DST, CONFIG_DST, unixSocketPath, BROKER_SOCKET]) {
|
|
847
1190
|
if (!existsSync(p)) continue;
|
|
848
1191
|
try {
|
|
849
1192
|
unlinkSync(p);
|
package/bin/native-host.cjs
CHANGED
|
@@ -11,9 +11,25 @@ const path = require("path");
|
|
|
11
11
|
const { spawn } = require("child_process");
|
|
12
12
|
|
|
13
13
|
const BASE_DIR = path.join(os.homedir(), ".opencode-browser");
|
|
14
|
-
const SOCKET_PATH =
|
|
14
|
+
const SOCKET_PATH = getBrokerSocketPath();
|
|
15
15
|
const BROKER_PATH = path.join(BASE_DIR, "broker.cjs");
|
|
16
16
|
|
|
17
|
+
function getSafePipeName() {
|
|
18
|
+
try {
|
|
19
|
+
const username = os.userInfo().username || "user";
|
|
20
|
+
return `opencode-browser-${username}`.replace(/[^a-zA-Z0-9._-]/g, "_");
|
|
21
|
+
} catch {
|
|
22
|
+
return "opencode-browser";
|
|
23
|
+
}
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
function getBrokerSocketPath() {
|
|
27
|
+
const override = process.env.OPENCODE_BROWSER_BROKER_SOCKET;
|
|
28
|
+
if (override) return override;
|
|
29
|
+
if (process.platform === "win32") return `\\\\.\\pipe\\${getSafePipeName()}`;
|
|
30
|
+
return path.join(BASE_DIR, "broker.sock");
|
|
31
|
+
}
|
|
32
|
+
|
|
17
33
|
fs.mkdirSync(BASE_DIR, { recursive: true });
|
|
18
34
|
|
|
19
35
|
function createJsonLineParser(onMessage) {
|