@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/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 output = execSync("which node", { stdio: ["ignore", "pipe", "ignore"] })
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 = platform();
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 Linux only.");
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
- const hostDirs = getNativeHostDirs(osName);
476
- for (const dir of hostDirs) {
477
- try {
478
- writeNativeHostManifest(dir, extensionId, hostPath);
479
- success(`Wrote native host manifest: ${nativeHostManifestPath(dir)}`);
480
- } catch (e) {
481
- warn(`Could not write native host manifest to: ${dir}`);
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
- "2) Global (~/.config/opencode/opencode.json)",
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
- const xdgConfig = process.env.XDG_CONFIG_HOME;
530
- configDir = xdgConfig ? join(xdgConfig, "opencode") : join(homedir(), ".config", "opencode");
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 = platform();
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 Linux only.");
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
- 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}`);
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 = platform();
791
- const hostDirs = getNativeHostDirs(osName);
792
- let foundAny = false;
793
- for (const dir of hostDirs) {
794
- const p = nativeHostManifestPath(dir);
795
- if (existsSync(p)) {
796
- foundAny = true;
797
- success(`Native host manifest: ${p}`);
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
- if (!foundAny) {
801
- warn("No native host manifest found. Run: npx @different-ai/opencode-browser install");
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 = platform();
834
- const hostDirs = getNativeHostDirs(osName);
835
- for (const dir of hostDirs) {
836
- const p = nativeHostManifestPath(dir);
837
- if (!existsSync(p)) continue;
838
- try {
839
- unlinkSync(p);
840
- success(`Removed native host manifest: ${p}`);
841
- } catch {
842
- warn(`Could not remove: ${p}`);
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
- for (const p of [BROKER_DST, NATIVE_HOST_DST, CONFIG_DST, join(BASE_DIR, "broker.sock")]) {
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);
@@ -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 = path.join(BASE_DIR, "broker.sock");
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) {