From 42bb80b13436550ddffd2cbc40102b8a5b8ae549 Mon Sep 17 00:00:00 2001 From: BB84 Date: Tue, 3 Feb 2026 14:10:46 -0500 Subject: [PATCH] Add Windows support and named-pipe broker Add Windows support across CLI, broker, native host, and plugin by using a platform-aware broker socket (Unix socket or Windows named pipe) and safe pipe naming. Implement OPENCODE_BROWSER_BROKER_SOCKET override, create host-wrapper.cmd on Windows, and add registry registration/unregistration and status checks for Chrome/Edge/Brave/Chromium native messaging hosts. Improve plugin diagnostics and logging (plugin.log), include broker socket info in debug/status output, and enhance broker connection errors with underlying messages. Also update README to list supported OSes. Files changed: bin/cli.js, bin/broker.cjs, bin/native-host.cjs, src/plugin.ts, dist/plugin.js, README.md. --- README.md | 2 + bin/broker.cjs | 18 ++- bin/cli.js | 261 +++++++++++++++++++++++++++++++++++--------- bin/native-host.cjs | 18 ++- dist/plugin.js | 214 ++++++++++++++++++++---------------- src/plugin.ts | 77 +++++++++---- 6 files changed, 419 insertions(+), 171 deletions(-) diff --git a/README.md b/README.md index 7f41f85..7bab559 100644 --- a/README.md +++ b/README.md @@ -24,6 +24,8 @@ This version is optimized for reliability and predictable multi-session behavior bunx @different-ai/opencode-browser@latest install ``` +Supports macOS, Linux, and Windows (Chrome/Edge/Brave/Chromium). + https://github.com/user-attachments/assets/d5767362-fbf3-4023-858b-90f06d9f0b25 diff --git a/bin/broker.cjs b/bin/broker.cjs index d853d02..0b14a23 100644 --- a/bin/broker.cjs +++ b/bin/broker.cjs @@ -7,7 +7,23 @@ const os = require("os"); const path = require("path"); const BASE_DIR = path.join(os.homedir(), ".opencode-browser"); -const SOCKET_PATH = path.join(BASE_DIR, "broker.sock"); +const SOCKET_PATH = getBrokerSocketPath(); + +function getSafePipeName() { + try { + const username = os.userInfo().username || "user"; + return `opencode-browser-${username}`.replace(/[^a-zA-Z0-9._-]/g, "_"); + } catch { + return "opencode-browser"; + } +} + +function getBrokerSocketPath() { + const override = process.env.OPENCODE_BROWSER_BROKER_SOCKET; + if (override) return override; + if (process.platform === "win32") return `\\\\.\\pipe\\${getSafePipeName()}`; + return path.join(BASE_DIR, "broker.sock"); +} fs.mkdirSync(BASE_DIR, { recursive: true }); diff --git a/bin/cli.js b/bin/cli.js index d2c5392..49f3121 100755 --- a/bin/cli.js +++ b/bin/cli.js @@ -21,12 +21,12 @@ import { unlinkSync, chmodSync, } from "fs"; -import { homedir, platform } from "os"; +import { homedir, platform, userInfo } from "os"; import { join, dirname } from "path"; import { fileURLToPath } from "url"; import { createInterface } from "readline"; import { createConnection } from "net"; -import { execSync, spawn } from "child_process"; +import { execSync, spawn, spawnSync } from "child_process"; import { createHash } from "crypto"; const __filename = fileURLToPath(import.meta.url); @@ -38,11 +38,15 @@ const EXTENSION_DIR = join(BASE_DIR, "extension"); const EXTENSION_MANIFEST_PATH = join(PACKAGE_ROOT, "extension", "manifest.json"); const BROKER_DST = join(BASE_DIR, "broker.cjs"); const NATIVE_HOST_DST = join(BASE_DIR, "native-host.cjs"); -const NATIVE_HOST_WRAPPER = join(BASE_DIR, "host-wrapper.sh"); const CONFIG_DST = join(BASE_DIR, "config.json"); -const BROKER_SOCKET = join(BASE_DIR, "broker.sock"); const NATIVE_HOST_NAME = "com.opencode.browser_automation"; +const OS_NAME = platform(); +const NATIVE_HOST_WRAPPER = join( + BASE_DIR, + OS_NAME === "win32" ? "host-wrapper.cmd" : "host-wrapper.sh" +); +const BROKER_SOCKET = getBrokerSocketPath(); const COLORS = { reset: "\x1b[0m", @@ -57,6 +61,56 @@ function color(c, text) { return `${COLORS[c]}${text}${COLORS.reset}`; } +function isWindows() { + return OS_NAME === "win32"; +} + +function getSafePipeName() { + try { + const username = userInfo().username || "user"; + return `opencode-browser-${username}`.replace(/[^a-zA-Z0-9._-]/g, "_"); + } catch { + return "opencode-browser"; + } +} + +function getBrokerSocketPath() { + const override = process.env.OPENCODE_BROWSER_BROKER_SOCKET; + if (override) return override; + if (OS_NAME === "win32") return `\\\\.\\pipe\\${getSafePipeName()}`; + return join(BASE_DIR, "broker.sock"); +} + +function getWindowsRegistryTargets() { + return [ + { name: "Chrome", key: "HKCU\\Software\\Google\\Chrome\\NativeMessagingHosts" }, + { name: "Chromium", key: "HKCU\\Software\\Chromium\\NativeMessagingHosts" }, + { name: "Brave", key: "HKCU\\Software\\BraveSoftware\\Brave-Browser\\NativeMessagingHosts" }, + { name: "Edge", key: "HKCU\\Software\\Microsoft\\Edge\\NativeMessagingHosts" }, + ]; +} + +function runRegCommand(args) { + try { + const result = spawnSync("reg", args, { stdio: "ignore" }); + return result.status === 0; + } catch { + return false; + } +} + +function queryRegistryDefaultValue(key) { + try { + const result = spawnSync("reg", ["query", key, "/ve"], { encoding: "utf8" }); + if (result.status !== 0) return null; + const output = String(result.stdout || ""); + const match = output.match(/REG_SZ\s+(.+)\s*$/m); + return match ? match[1].trim() : null; + } catch { + return null; + } +} + function log(msg) { console.log(msg); } @@ -180,7 +234,8 @@ function resolveNodePath() { if (process.env.OPENCODE_BROWSER_NODE) return process.env.OPENCODE_BROWSER_NODE; if (process.execPath && /node(\.exe)?$/.test(process.execPath)) return process.execPath; try { - const output = execSync("which node", { stdio: ["ignore", "pipe", "ignore"] }) + const command = isWindows() ? "where node" : "which node"; + const output = execSync(command, { stdio: ["ignore", "pipe", "ignore"] }) .toString("utf8") .trim(); if (output) return output; @@ -190,6 +245,11 @@ function resolveNodePath() { function writeHostWrapper(nodePath) { ensureDir(BASE_DIR); + if (isWindows()) { + const script = `@echo off\r\n"${nodePath}" "${NATIVE_HOST_DST}"\r\n`; + writeFileSync(NATIVE_HOST_WRAPPER, script); + return NATIVE_HOST_WRAPPER; + } const script = `#!/bin/sh\n"${nodePath}" "${NATIVE_HOST_DST}"\n`; writeFileSync(NATIVE_HOST_WRAPPER, script, { mode: 0o755 }); chmodSync(NATIVE_HOST_WRAPPER, 0o755); @@ -276,6 +336,7 @@ function copyDirRecursive(srcDir, destDir) { } function getNativeHostDirs(osName) { + if (osName === "win32") return []; if (osName === "darwin") { const base = join(homedir(), "Library", "Application Support"); return [ @@ -312,6 +373,58 @@ function writeNativeHostManifest(dir, extensionId, hostPath) { writeFileSync(nativeHostManifestPath(dir), JSON.stringify(manifest, null, 2) + "\n"); } +function writeWindowsNativeHostManifest(extensionId, hostPath) { + const manifestPath = nativeHostManifestPath(BASE_DIR); + writeNativeHostManifest(BASE_DIR, extensionId, hostPath); + return manifestPath; +} + +function registerWindowsNativeHost(manifestPath) { + for (const target of getWindowsRegistryTargets()) { + const key = `${target.key}\\${NATIVE_HOST_NAME}`; + const ok = runRegCommand(["add", key, "/ve", "/t", "REG_SZ", "/d", manifestPath, "/f"]); + if (ok) { + success(`Registered native host for ${target.name}: ${key}`); + } else { + warn(`Could not register native host for ${target.name}: ${key}`); + } + } +} + +function unregisterWindowsNativeHost() { + for (const target of getWindowsRegistryTargets()) { + const key = `${target.key}\\${NATIVE_HOST_NAME}`; + const ok = runRegCommand(["delete", key, "/f"]); + if (ok) { + success(`Removed native host registry: ${key}`); + } else { + warn(`Could not remove native host registry: ${key}`); + } + } +} + +function reportWindowsNativeHostStatus() { + const manifestPath = nativeHostManifestPath(BASE_DIR); + if (existsSync(manifestPath)) { + success(`Native host manifest: ${manifestPath}`); + } else { + warn(`Native host manifest missing: ${manifestPath}`); + } + + let foundAny = false; + for (const target of getWindowsRegistryTargets()) { + const key = `${target.key}\\${NATIVE_HOST_NAME}`; + const value = queryRegistryDefaultValue(key); + if (value) { + foundAny = true; + success(`Registry (${target.name}): ${key}`); + } + } + if (!foundAny) { + warn("No native host registry entries found. Run: npx @different-ai/opencode-browser install"); + } +} + function loadConfig() { try { if (!existsSync(CONFIG_DST)) return null; @@ -377,13 +490,13 @@ ${color("bright", "Agent Mode:")} async function install() { header("Step 1: Check Platform"); - const osName = platform(); - if (osName !== "darwin" && osName !== "linux") { + const osName = OS_NAME; + if (osName !== "darwin" && osName !== "linux" && osName !== "win32") { error(`Unsupported platform: ${osName}`); - error("OpenCode Browser currently supports macOS and Linux only."); + error("OpenCode Browser currently supports macOS, Linux, and Windows only."); process.exit(1); } - success(`Platform: ${osName === "darwin" ? "macOS" : "Linux"}`); + success(`Platform: ${osName === "darwin" ? "macOS" : osName === "win32" ? "Windows" : "Linux"}`); header("Step 2: Copy Extension Files"); @@ -472,13 +585,19 @@ Find it at ${color("cyan", "chrome://extensions")}: header("Step 6: Register Native Messaging Host"); - const hostDirs = getNativeHostDirs(osName); - for (const dir of hostDirs) { - try { - writeNativeHostManifest(dir, extensionId, hostPath); - success(`Wrote native host manifest: ${nativeHostManifestPath(dir)}`); - } catch (e) { - warn(`Could not write native host manifest to: ${dir}`); + if (osName === "win32") { + const manifestPath = writeWindowsNativeHostManifest(extensionId, hostPath); + success(`Wrote native host manifest: ${manifestPath}`); + registerWindowsNativeHost(manifestPath); + } else { + const hostDirs = getNativeHostDirs(osName); + for (const dir of hostDirs) { + try { + writeNativeHostManifest(dir, extensionId, hostPath); + success(`Wrote native host manifest: ${nativeHostManifestPath(dir)}`); + } catch (e) { + warn(`Could not write native host manifest to: ${dir}`); + } } } @@ -509,9 +628,13 @@ Find it at ${color("cyan", "chrome://extensions")}: return jsonPath; } + const globalConfigLabel = + osName === "win32" + ? "2) Global (%USERPROFILE%\\.config\\opencode\\opencode.json)" + : "2) Global (~/.config/opencode/opencode.json)"; const configOptions = [ "1) Project (./opencode.json or opencode.jsonc)", - "2) Global (~/.config/opencode/opencode.json)", + globalConfigLabel, "3) Custom path", "4) Skip (does nothing)", ]; @@ -526,8 +649,12 @@ Find it at ${color("cyan", "chrome://extensions")}: configDir = process.cwd(); configPath = findOpenCodeConfigPath(configDir); } else if (selection === "2") { - const xdgConfig = process.env.XDG_CONFIG_HOME; - configDir = xdgConfig ? join(xdgConfig, "opencode") : join(homedir(), ".config", "opencode"); + if (osName === "win32") { + configDir = join(homedir(), ".config", "opencode"); + } else { + const xdgConfig = process.env.XDG_CONFIG_HOME; + configDir = xdgConfig ? join(xdgConfig, "opencode") : join(homedir(), ".config", "opencode"); + } configPath = findOpenCodeConfigPath(configDir); } else if (selection === "3") { const customPath = await ask("Enter full path to opencode.json or opencode.jsonc: "); @@ -660,13 +787,13 @@ Open Chrome and: async function update() { header("Update: Check Platform"); - const osName = platform(); - if (osName !== "darwin" && osName !== "linux") { + const osName = OS_NAME; + if (osName !== "darwin" && osName !== "linux" && osName !== "win32") { error(`Unsupported platform: ${osName}`); - error("OpenCode Browser currently supports macOS and Linux only."); + error("OpenCode Browser currently supports macOS, Linux, and Windows only."); process.exit(1); } - success(`Platform: ${osName === "darwin" ? "macOS" : "Linux"}`); + success(`Platform: ${osName === "darwin" ? "macOS" : osName === "win32" ? "Windows" : "Linux"}`); header("Step 1: Copy Extension Files"); @@ -743,13 +870,19 @@ Find it at ${color("cyan", "chrome://extensions")}: header("Step 4: Register Native Messaging Host"); - const hostDirs = getNativeHostDirs(osName); - for (const dir of hostDirs) { - try { - writeNativeHostManifest(dir, extensionId, hostPath); - success(`Wrote native host manifest: ${nativeHostManifestPath(dir)}`); - } catch { - warn(`Could not write native host manifest to: ${dir}`); + if (osName === "win32") { + const manifestPath = writeWindowsNativeHostManifest(extensionId, hostPath); + success(`Wrote native host manifest: ${manifestPath}`); + registerWindowsNativeHost(manifestPath); + } else { + const hostDirs = getNativeHostDirs(osName); + for (const dir of hostDirs) { + try { + writeNativeHostManifest(dir, extensionId, hostPath); + success(`Wrote native host manifest: ${nativeHostManifestPath(dir)}`); + } catch { + warn(`Could not write native host manifest to: ${dir}`); + } } } @@ -770,6 +903,7 @@ async function status() { success(`Broker installed: ${existsSync(BROKER_DST)}`); success(`Native host installed: ${existsSync(NATIVE_HOST_DST)}`); success(`Host wrapper installed: ${existsSync(NATIVE_HOST_WRAPPER)}`); + success(`Broker socket: ${BROKER_SOCKET}`); const cfg = loadConfig(); if (cfg?.extensionId) { @@ -787,18 +921,29 @@ async function status() { success(`Node path: ${cfg.nodePath}`); } - const osName = platform(); - const hostDirs = getNativeHostDirs(osName); - let foundAny = false; - for (const dir of hostDirs) { - const p = nativeHostManifestPath(dir); - if (existsSync(p)) { - foundAny = true; - success(`Native host manifest: ${p}`); + const osName = OS_NAME; + if (osName === "win32") { + reportWindowsNativeHostStatus(); + } else { + const hostDirs = getNativeHostDirs(osName); + let foundAny = false; + for (const dir of hostDirs) { + const p = nativeHostManifestPath(dir); + if (existsSync(p)) { + foundAny = true; + success(`Native host manifest: ${p}`); + } + } + if (!foundAny) { + warn("No native host manifest found. Run: npx @different-ai/opencode-browser install"); } } - if (!foundAny) { - warn("No native host manifest found. Run: npx @different-ai/opencode-browser install"); + + const brokerStatus = await getBrokerStatus(1000); + if (brokerStatus.ok) { + success(`Broker status: ok (hostConnected=${!!brokerStatus.data?.hostConnected})`); + } else { + warn(`Broker status: ${brokerStatus.error || "unavailable"}`); } } @@ -830,20 +975,34 @@ async function agentGateway() { async function uninstall() { header("Uninstall"); - const osName = platform(); - const hostDirs = getNativeHostDirs(osName); - for (const dir of hostDirs) { - const p = nativeHostManifestPath(dir); - if (!existsSync(p)) continue; - try { - unlinkSync(p); - success(`Removed native host manifest: ${p}`); - } catch { - warn(`Could not remove: ${p}`); + const osName = OS_NAME; + if (osName === "win32") { + unregisterWindowsNativeHost(); + const manifestPath = nativeHostManifestPath(BASE_DIR); + if (existsSync(manifestPath)) { + try { + unlinkSync(manifestPath); + success(`Removed native host manifest: ${manifestPath}`); + } catch { + warn(`Could not remove: ${manifestPath}`); + } + } + } else { + const hostDirs = getNativeHostDirs(osName); + for (const dir of hostDirs) { + const p = nativeHostManifestPath(dir); + if (!existsSync(p)) continue; + try { + unlinkSync(p); + success(`Removed native host manifest: ${p}`); + } catch { + warn(`Could not remove: ${p}`); + } } } - for (const p of [BROKER_DST, NATIVE_HOST_DST, CONFIG_DST, join(BASE_DIR, "broker.sock")]) { + const unixSocketPath = join(BASE_DIR, "broker.sock"); + for (const p of [BROKER_DST, NATIVE_HOST_DST, CONFIG_DST, unixSocketPath, BROKER_SOCKET]) { if (!existsSync(p)) continue; try { unlinkSync(p); diff --git a/bin/native-host.cjs b/bin/native-host.cjs index 6054973..80918f0 100644 --- a/bin/native-host.cjs +++ b/bin/native-host.cjs @@ -11,9 +11,25 @@ const path = require("path"); const { spawn } = require("child_process"); const BASE_DIR = path.join(os.homedir(), ".opencode-browser"); -const SOCKET_PATH = path.join(BASE_DIR, "broker.sock"); +const SOCKET_PATH = getBrokerSocketPath(); const BROKER_PATH = path.join(BASE_DIR, "broker.cjs"); +function getSafePipeName() { + try { + const username = os.userInfo().username || "user"; + return `opencode-browser-${username}`.replace(/[^a-zA-Z0-9._-]/g, "_"); + } catch { + return "opencode-browser"; + } +} + +function getBrokerSocketPath() { + const override = process.env.OPENCODE_BROWSER_BROKER_SOCKET; + if (override) return override; + if (process.platform === "win32") return `\\\\.\\pipe\\${getSafePipeName()}`; + return path.join(BASE_DIR, "broker.sock"); +} + fs.mkdirSync(BASE_DIR, { recursive: true }); function createJsonLineParser(onMessage) { diff --git a/dist/plugin.js b/dist/plugin.js index 15933a6..4c4802d 100644 --- a/dist/plugin.js +++ b/dist/plugin.js @@ -9,7 +9,7 @@ var __export = (target, all) => { }); }; -// node_modules/.pnpm/zod@4.1.8/node_modules/zod/v4/classic/external.js +// node_modules/zod/v4/classic/external.js var exports_external = {}; __export(exports_external, { xid: () => xid2, @@ -239,7 +239,7 @@ __export(exports_external, { $brand: () => $brand }); -// node_modules/.pnpm/zod@4.1.8/node_modules/zod/v4/core/index.js +// node_modules/zod/v4/core/index.js var exports_core2 = {}; __export(exports_core2, { version: () => version, @@ -503,7 +503,7 @@ __export(exports_core2, { $ZodAny: () => $ZodAny }); -// node_modules/.pnpm/zod@4.1.8/node_modules/zod/v4/core/core.js +// node_modules/zod/v4/core/core.js var NEVER = Object.freeze({ status: "aborted" }); @@ -570,7 +570,7 @@ function config(newConfig) { Object.assign(globalConfig, newConfig); return globalConfig; } -// node_modules/.pnpm/zod@4.1.8/node_modules/zod/v4/core/util.js +// node_modules/zod/v4/core/util.js var exports_util = {}; __export(exports_util, { unwrapMessage: () => unwrapMessage, @@ -1199,7 +1199,7 @@ class Class { constructor(..._args) {} } -// node_modules/.pnpm/zod@4.1.8/node_modules/zod/v4/core/errors.js +// node_modules/zod/v4/core/errors.js var initializer = (inst, def) => { inst.name = "$ZodError"; Object.defineProperty(inst, "_zod", { @@ -1342,7 +1342,7 @@ function prettifyError(error) { `); } -// node_modules/.pnpm/zod@4.1.8/node_modules/zod/v4/core/parse.js +// node_modules/zod/v4/core/parse.js var _parse = (_Err) => (schema, value, _ctx, _params) => { const ctx = _ctx ? Object.assign(_ctx, { async: false }) : { async: false }; const result = schema._zod.run({ value, issues: [] }, ctx); @@ -1429,7 +1429,7 @@ var _safeDecodeAsync = (_Err) => async (schema, value, _ctx) => { return _safeParseAsync(_Err)(schema, value, _ctx); }; var safeDecodeAsync = /* @__PURE__ */ _safeDecodeAsync($ZodRealError); -// node_modules/.pnpm/zod@4.1.8/node_modules/zod/v4/core/regexes.js +// node_modules/zod/v4/core/regexes.js var exports_regexes = {}; __export(exports_regexes, { xid: () => xid, @@ -1581,7 +1581,7 @@ var sha512_hex = /^[0-9a-fA-F]{128}$/; var sha512_base64 = /* @__PURE__ */ fixedBase64(86, "=="); var sha512_base64url = /* @__PURE__ */ fixedBase64url(86); -// node_modules/.pnpm/zod@4.1.8/node_modules/zod/v4/core/checks.js +// node_modules/zod/v4/core/checks.js var $ZodCheck = /* @__PURE__ */ $constructor("$ZodCheck", (inst, def) => { var _a; inst._zod ?? (inst._zod = {}); @@ -2122,7 +2122,7 @@ var $ZodCheckOverwrite = /* @__PURE__ */ $constructor("$ZodCheckOverwrite", (ins }; }); -// node_modules/.pnpm/zod@4.1.8/node_modules/zod/v4/core/doc.js +// node_modules/zod/v4/core/doc.js class Doc { constructor(args = []) { this.content = []; @@ -2160,14 +2160,14 @@ class Doc { } } -// node_modules/.pnpm/zod@4.1.8/node_modules/zod/v4/core/versions.js +// node_modules/zod/v4/core/versions.js var version = { major: 4, minor: 1, patch: 8 }; -// node_modules/.pnpm/zod@4.1.8/node_modules/zod/v4/core/schemas.js +// node_modules/zod/v4/core/schemas.js var $ZodType = /* @__PURE__ */ $constructor("$ZodType", (inst, def) => { var _a; inst ?? (inst = {}); @@ -3990,7 +3990,7 @@ function handleRefineResult(result, payload, input, inst) { payload.issues.push(issue(_iss)); } } -// node_modules/.pnpm/zod@4.1.8/node_modules/zod/v4/locales/index.js +// node_modules/zod/v4/locales/index.js var exports_locales = {}; __export(exports_locales, { zhTW: () => zh_TW_default, @@ -4041,7 +4041,7 @@ __export(exports_locales, { ar: () => ar_default }); -// node_modules/.pnpm/zod@4.1.8/node_modules/zod/v4/locales/ar.js +// node_modules/zod/v4/locales/ar.js var error = () => { const Sizable = { string: { unit: "حرف", verb: "أن يحوي" }, @@ -4157,7 +4157,7 @@ function ar_default() { localeError: error() }; } -// node_modules/.pnpm/zod@4.1.8/node_modules/zod/v4/locales/az.js +// node_modules/zod/v4/locales/az.js var error2 = () => { const Sizable = { string: { unit: "simvol", verb: "olmalıdır" }, @@ -4272,7 +4272,7 @@ function az_default() { localeError: error2() }; } -// node_modules/.pnpm/zod@4.1.8/node_modules/zod/v4/locales/be.js +// node_modules/zod/v4/locales/be.js function getBelarusianPlural(count, one, few, many) { const absCount = Math.abs(count); const lastDigit = absCount % 10; @@ -4436,7 +4436,7 @@ function be_default() { localeError: error3() }; } -// node_modules/.pnpm/zod@4.1.8/node_modules/zod/v4/locales/ca.js +// node_modules/zod/v4/locales/ca.js var error4 = () => { const Sizable = { string: { unit: "caràcters", verb: "contenir" }, @@ -4553,7 +4553,7 @@ function ca_default() { localeError: error4() }; } -// node_modules/.pnpm/zod@4.1.8/node_modules/zod/v4/locales/cs.js +// node_modules/zod/v4/locales/cs.js var error5 = () => { const Sizable = { string: { unit: "znaků", verb: "mít" }, @@ -4688,7 +4688,7 @@ function cs_default() { localeError: error5() }; } -// node_modules/.pnpm/zod@4.1.8/node_modules/zod/v4/locales/da.js +// node_modules/zod/v4/locales/da.js var error6 = () => { const Sizable = { string: { unit: "tegn", verb: "havde" }, @@ -4819,7 +4819,7 @@ function da_default() { localeError: error6() }; } -// node_modules/.pnpm/zod@4.1.8/node_modules/zod/v4/locales/de.js +// node_modules/zod/v4/locales/de.js var error7 = () => { const Sizable = { string: { unit: "Zeichen", verb: "zu haben" }, @@ -4935,7 +4935,7 @@ function de_default() { localeError: error7() }; } -// node_modules/.pnpm/zod@4.1.8/node_modules/zod/v4/locales/en.js +// node_modules/zod/v4/locales/en.js var parsedType = (data) => { const t = typeof data; switch (t) { @@ -5052,7 +5052,7 @@ function en_default() { localeError: error8() }; } -// node_modules/.pnpm/zod@4.1.8/node_modules/zod/v4/locales/eo.js +// node_modules/zod/v4/locales/eo.js var parsedType2 = (data) => { const t = typeof data; switch (t) { @@ -5168,7 +5168,7 @@ function eo_default() { localeError: error9() }; } -// node_modules/.pnpm/zod@4.1.8/node_modules/zod/v4/locales/es.js +// node_modules/zod/v4/locales/es.js var error10 = () => { const Sizable = { string: { unit: "caracteres", verb: "tener" }, @@ -5316,7 +5316,7 @@ function es_default() { localeError: error10() }; } -// node_modules/.pnpm/zod@4.1.8/node_modules/zod/v4/locales/fa.js +// node_modules/zod/v4/locales/fa.js var error11 = () => { const Sizable = { string: { unit: "کاراکتر", verb: "داشته باشد" }, @@ -5438,7 +5438,7 @@ function fa_default() { localeError: error11() }; } -// node_modules/.pnpm/zod@4.1.8/node_modules/zod/v4/locales/fi.js +// node_modules/zod/v4/locales/fi.js var error12 = () => { const Sizable = { string: { unit: "merkkiä", subject: "merkkijonon" }, @@ -5560,7 +5560,7 @@ function fi_default() { localeError: error12() }; } -// node_modules/.pnpm/zod@4.1.8/node_modules/zod/v4/locales/fr.js +// node_modules/zod/v4/locales/fr.js var error13 = () => { const Sizable = { string: { unit: "caractères", verb: "avoir" }, @@ -5676,7 +5676,7 @@ function fr_default() { localeError: error13() }; } -// node_modules/.pnpm/zod@4.1.8/node_modules/zod/v4/locales/fr-CA.js +// node_modules/zod/v4/locales/fr-CA.js var error14 = () => { const Sizable = { string: { unit: "caractères", verb: "avoir" }, @@ -5793,7 +5793,7 @@ function fr_CA_default() { localeError: error14() }; } -// node_modules/.pnpm/zod@4.1.8/node_modules/zod/v4/locales/he.js +// node_modules/zod/v4/locales/he.js var error15 = () => { const Sizable = { string: { unit: "אותיות", verb: "לכלול" }, @@ -5909,7 +5909,7 @@ function he_default() { localeError: error15() }; } -// node_modules/.pnpm/zod@4.1.8/node_modules/zod/v4/locales/hu.js +// node_modules/zod/v4/locales/hu.js var error16 = () => { const Sizable = { string: { unit: "karakter", verb: "legyen" }, @@ -6025,7 +6025,7 @@ function hu_default() { localeError: error16() }; } -// node_modules/.pnpm/zod@4.1.8/node_modules/zod/v4/locales/id.js +// node_modules/zod/v4/locales/id.js var error17 = () => { const Sizable = { string: { unit: "karakter", verb: "memiliki" }, @@ -6141,7 +6141,7 @@ function id_default() { localeError: error17() }; } -// node_modules/.pnpm/zod@4.1.8/node_modules/zod/v4/locales/is.js +// node_modules/zod/v4/locales/is.js var parsedType3 = (data) => { const t = typeof data; switch (t) { @@ -6258,7 +6258,7 @@ function is_default() { localeError: error18() }; } -// node_modules/.pnpm/zod@4.1.8/node_modules/zod/v4/locales/it.js +// node_modules/zod/v4/locales/it.js var error19 = () => { const Sizable = { string: { unit: "caratteri", verb: "avere" }, @@ -6374,7 +6374,7 @@ function it_default() { localeError: error19() }; } -// node_modules/.pnpm/zod@4.1.8/node_modules/zod/v4/locales/ja.js +// node_modules/zod/v4/locales/ja.js var error20 = () => { const Sizable = { string: { unit: "文字", verb: "である" }, @@ -6489,7 +6489,7 @@ function ja_default() { localeError: error20() }; } -// node_modules/.pnpm/zod@4.1.8/node_modules/zod/v4/locales/ka.js +// node_modules/zod/v4/locales/ka.js var parsedType4 = (data) => { const t = typeof data; switch (t) { @@ -6614,7 +6614,7 @@ function ka_default() { localeError: error21() }; } -// node_modules/.pnpm/zod@4.1.8/node_modules/zod/v4/locales/km.js +// node_modules/zod/v4/locales/km.js var error22 = () => { const Sizable = { string: { unit: "តួអក្សរ", verb: "គួរមាន" }, @@ -6732,11 +6732,11 @@ function km_default() { }; } -// node_modules/.pnpm/zod@4.1.8/node_modules/zod/v4/locales/kh.js +// node_modules/zod/v4/locales/kh.js function kh_default() { return km_default(); } -// node_modules/.pnpm/zod@4.1.8/node_modules/zod/v4/locales/ko.js +// node_modules/zod/v4/locales/ko.js var error23 = () => { const Sizable = { string: { unit: "문자", verb: "to have" }, @@ -6857,7 +6857,7 @@ function ko_default() { localeError: error23() }; } -// node_modules/.pnpm/zod@4.1.8/node_modules/zod/v4/locales/lt.js +// node_modules/zod/v4/locales/lt.js var parsedType5 = (data) => { const t = typeof data; return parsedTypeFromType(t, data); @@ -7086,7 +7086,7 @@ function lt_default() { localeError: error24() }; } -// node_modules/.pnpm/zod@4.1.8/node_modules/zod/v4/locales/mk.js +// node_modules/zod/v4/locales/mk.js var error25 = () => { const Sizable = { string: { unit: "знаци", verb: "да имаат" }, @@ -7203,7 +7203,7 @@ function mk_default() { localeError: error25() }; } -// node_modules/.pnpm/zod@4.1.8/node_modules/zod/v4/locales/ms.js +// node_modules/zod/v4/locales/ms.js var error26 = () => { const Sizable = { string: { unit: "aksara", verb: "mempunyai" }, @@ -7319,7 +7319,7 @@ function ms_default() { localeError: error26() }; } -// node_modules/.pnpm/zod@4.1.8/node_modules/zod/v4/locales/nl.js +// node_modules/zod/v4/locales/nl.js var error27 = () => { const Sizable = { string: { unit: "tekens" }, @@ -7436,7 +7436,7 @@ function nl_default() { localeError: error27() }; } -// node_modules/.pnpm/zod@4.1.8/node_modules/zod/v4/locales/no.js +// node_modules/zod/v4/locales/no.js var error28 = () => { const Sizable = { string: { unit: "tegn", verb: "å ha" }, @@ -7552,7 +7552,7 @@ function no_default() { localeError: error28() }; } -// node_modules/.pnpm/zod@4.1.8/node_modules/zod/v4/locales/ota.js +// node_modules/zod/v4/locales/ota.js var error29 = () => { const Sizable = { string: { unit: "harf", verb: "olmalıdır" }, @@ -7668,7 +7668,7 @@ function ota_default() { localeError: error29() }; } -// node_modules/.pnpm/zod@4.1.8/node_modules/zod/v4/locales/ps.js +// node_modules/zod/v4/locales/ps.js var error30 = () => { const Sizable = { string: { unit: "توکي", verb: "ولري" }, @@ -7790,7 +7790,7 @@ function ps_default() { localeError: error30() }; } -// node_modules/.pnpm/zod@4.1.8/node_modules/zod/v4/locales/pl.js +// node_modules/zod/v4/locales/pl.js var error31 = () => { const Sizable = { string: { unit: "znaków", verb: "mieć" }, @@ -7907,7 +7907,7 @@ function pl_default() { localeError: error31() }; } -// node_modules/.pnpm/zod@4.1.8/node_modules/zod/v4/locales/pt.js +// node_modules/zod/v4/locales/pt.js var error32 = () => { const Sizable = { string: { unit: "caracteres", verb: "ter" }, @@ -8023,7 +8023,7 @@ function pt_default() { localeError: error32() }; } -// node_modules/.pnpm/zod@4.1.8/node_modules/zod/v4/locales/ru.js +// node_modules/zod/v4/locales/ru.js function getRussianPlural(count, one, few, many) { const absCount = Math.abs(count); const lastDigit = absCount % 10; @@ -8187,7 +8187,7 @@ function ru_default() { localeError: error33() }; } -// node_modules/.pnpm/zod@4.1.8/node_modules/zod/v4/locales/sl.js +// node_modules/zod/v4/locales/sl.js var error34 = () => { const Sizable = { string: { unit: "znakov", verb: "imeti" }, @@ -8304,7 +8304,7 @@ function sl_default() { localeError: error34() }; } -// node_modules/.pnpm/zod@4.1.8/node_modules/zod/v4/locales/sv.js +// node_modules/zod/v4/locales/sv.js var error35 = () => { const Sizable = { string: { unit: "tecken", verb: "att ha" }, @@ -8422,7 +8422,7 @@ function sv_default() { localeError: error35() }; } -// node_modules/.pnpm/zod@4.1.8/node_modules/zod/v4/locales/ta.js +// node_modules/zod/v4/locales/ta.js var error36 = () => { const Sizable = { string: { unit: "எழுத்துக்கள்", verb: "கொண்டிருக்க வேண்டும்" }, @@ -8539,7 +8539,7 @@ function ta_default() { localeError: error36() }; } -// node_modules/.pnpm/zod@4.1.8/node_modules/zod/v4/locales/th.js +// node_modules/zod/v4/locales/th.js var error37 = () => { const Sizable = { string: { unit: "ตัวอักษร", verb: "ควรมี" }, @@ -8656,7 +8656,7 @@ function th_default() { localeError: error37() }; } -// node_modules/.pnpm/zod@4.1.8/node_modules/zod/v4/locales/tr.js +// node_modules/zod/v4/locales/tr.js var parsedType6 = (data) => { const t = typeof data; switch (t) { @@ -8771,7 +8771,7 @@ function tr_default() { localeError: error38() }; } -// node_modules/.pnpm/zod@4.1.8/node_modules/zod/v4/locales/uk.js +// node_modules/zod/v4/locales/uk.js var error39 = () => { const Sizable = { string: { unit: "символів", verb: "матиме" }, @@ -8888,11 +8888,11 @@ function uk_default() { }; } -// node_modules/.pnpm/zod@4.1.8/node_modules/zod/v4/locales/ua.js +// node_modules/zod/v4/locales/ua.js function ua_default() { return uk_default(); } -// node_modules/.pnpm/zod@4.1.8/node_modules/zod/v4/locales/ur.js +// node_modules/zod/v4/locales/ur.js var error40 = () => { const Sizable = { string: { unit: "حروف", verb: "ہونا" }, @@ -9009,7 +9009,7 @@ function ur_default() { localeError: error40() }; } -// node_modules/.pnpm/zod@4.1.8/node_modules/zod/v4/locales/vi.js +// node_modules/zod/v4/locales/vi.js var error41 = () => { const Sizable = { string: { unit: "ký tự", verb: "có" }, @@ -9125,7 +9125,7 @@ function vi_default() { localeError: error41() }; } -// node_modules/.pnpm/zod@4.1.8/node_modules/zod/v4/locales/zh-CN.js +// node_modules/zod/v4/locales/zh-CN.js var error42 = () => { const Sizable = { string: { unit: "字符", verb: "包含" }, @@ -9241,7 +9241,7 @@ function zh_CN_default() { localeError: error42() }; } -// node_modules/.pnpm/zod@4.1.8/node_modules/zod/v4/locales/zh-TW.js +// node_modules/zod/v4/locales/zh-TW.js var error43 = () => { const Sizable = { string: { unit: "字元", verb: "擁有" }, @@ -9358,7 +9358,7 @@ function zh_TW_default() { localeError: error43() }; } -// node_modules/.pnpm/zod@4.1.8/node_modules/zod/v4/locales/yo.js +// node_modules/zod/v4/locales/yo.js var error44 = () => { const Sizable = { string: { unit: "àmi", verb: "ní" }, @@ -9473,7 +9473,7 @@ function yo_default() { localeError: error44() }; } -// node_modules/.pnpm/zod@4.1.8/node_modules/zod/v4/core/registries.js +// node_modules/zod/v4/core/registries.js var $output = Symbol("ZodOutput"); var $input = Symbol("ZodInput"); @@ -9524,7 +9524,7 @@ function registry() { return new $ZodRegistry; } var globalRegistry = /* @__PURE__ */ registry(); -// node_modules/.pnpm/zod@4.1.8/node_modules/zod/v4/core/api.js +// node_modules/zod/v4/core/api.js function _string(Class2, params) { return new Class2({ type: "string", @@ -10402,7 +10402,7 @@ function _stringFormat(Class2, format, fnOrRegex, _params = {}) { const inst = new Class2(def); return inst; } -// node_modules/.pnpm/zod@4.1.8/node_modules/zod/v4/core/to-json-schema.js +// node_modules/zod/v4/core/to-json-schema.js class JSONSchemaGenerator { constructor(params) { this.counter = 0; @@ -11206,9 +11206,9 @@ function isTransforming(_schema, _ctx) { } throw new Error(`Unknown schema type: ${def.type}`); } -// node_modules/.pnpm/zod@4.1.8/node_modules/zod/v4/core/json-schema.js +// node_modules/zod/v4/core/json-schema.js var exports_json_schema = {}; -// node_modules/.pnpm/zod@4.1.8/node_modules/zod/v4/classic/iso.js +// node_modules/zod/v4/classic/iso.js var exports_iso = {}; __export(exports_iso, { time: () => time2, @@ -11249,7 +11249,7 @@ function duration2(params) { return _isoDuration(ZodISODuration, params); } -// node_modules/.pnpm/zod@4.1.8/node_modules/zod/v4/classic/errors.js +// node_modules/zod/v4/classic/errors.js var initializer2 = (inst, issues) => { $ZodError.init(inst, issues); inst.name = "ZodError"; @@ -11284,7 +11284,7 @@ var ZodRealError = $constructor("ZodError", initializer2, { Parent: Error }); -// node_modules/.pnpm/zod@4.1.8/node_modules/zod/v4/classic/parse.js +// node_modules/zod/v4/classic/parse.js var parse3 = /* @__PURE__ */ _parse(ZodRealError); var parseAsync2 = /* @__PURE__ */ _parseAsync(ZodRealError); var safeParse2 = /* @__PURE__ */ _safeParse(ZodRealError); @@ -11298,7 +11298,7 @@ var safeDecode2 = /* @__PURE__ */ _safeDecode(ZodRealError); var safeEncodeAsync2 = /* @__PURE__ */ _safeEncodeAsync(ZodRealError); var safeDecodeAsync2 = /* @__PURE__ */ _safeDecodeAsync(ZodRealError); -// node_modules/.pnpm/zod@4.1.8/node_modules/zod/v4/classic/schemas.js +// node_modules/zod/v4/classic/schemas.js var ZodType = /* @__PURE__ */ $constructor("ZodType", (inst, def) => { $ZodType.init(inst, def); inst.def = def; @@ -12273,7 +12273,7 @@ function json(params) { function preprocess(fn, schema) { return pipe(transform(fn), schema); } -// node_modules/.pnpm/zod@4.1.8/node_modules/zod/v4/classic/compat.js +// node_modules/zod/v4/classic/compat.js var ZodIssueCode = { invalid_type: "invalid_type", too_big: "too_big", @@ -12297,7 +12297,7 @@ function getErrorMap() { } var ZodFirstPartyTypeKind; (function(ZodFirstPartyTypeKind2) {})(ZodFirstPartyTypeKind || (ZodFirstPartyTypeKind = {})); -// node_modules/.pnpm/zod@4.1.8/node_modules/zod/v4/classic/coerce.js +// node_modules/zod/v4/classic/coerce.js var exports_coerce = {}; __export(exports_coerce, { string: () => string3, @@ -12322,9 +12322,9 @@ function date4(params) { return _coercedDate(ZodDate, params); } -// node_modules/.pnpm/zod@4.1.8/node_modules/zod/v4/classic/external.js +// node_modules/zod/v4/classic/external.js config(en_default()); -// node_modules/.pnpm/@opencode-ai+plugin@1.1.39/node_modules/@opencode-ai/plugin/dist/tool.js +// node_modules/@opencode-ai/plugin/dist/tool.js function tool(input) { return input; } @@ -13108,8 +13108,8 @@ function createAgentBackend(sessionId) { } // src/plugin.ts -import { existsSync, mkdirSync as mkdirSync2, readFileSync as readFileSync2, statSync } from "fs"; -import { homedir as homedir2 } from "os"; +import { appendFileSync, existsSync, mkdirSync as mkdirSync2, readFileSync as readFileSync2, statSync } from "fs"; +import { homedir as homedir2, userInfo } from "os"; import { basename as basename2, dirname as dirname2, isAbsolute as isAbsolute2, join as join2, resolve as resolve2 } from "path"; import { spawn as spawn2 } from "child_process"; import { fileURLToPath } from "url"; @@ -13132,8 +13132,32 @@ function getPackageVersion() { } var { schema } = tool; var BASE_DIR2 = join2(homedir2(), ".opencode-browser"); -var SOCKET_PATH = join2(BASE_DIR2, "broker.sock"); +var SOCKET_PATH = getBrokerSocketPath(); +var LOG_PATH = join2(BASE_DIR2, "plugin.log"); +function getSafePipeName() { + try { + const username = userInfo().username || "user"; + return `opencode-browser-${username}`.replace(/[^a-zA-Z0-9._-]/g, "_"); + } catch { + return "opencode-browser"; + } +} +function getBrokerSocketPath() { + const override = process.env.OPENCODE_BROWSER_BROKER_SOCKET; + if (override) + return override; + if (process.platform === "win32") + return `\\\\.\\pipe\\${getSafePipeName()}`; + return join2(BASE_DIR2, "broker.sock"); +} mkdirSync2(BASE_DIR2, { recursive: true }); +function logDebug(message) { + try { + appendFileSync(LOG_PATH, `[${new Date().toISOString()}] ${message} +`, "utf8"); + } catch {} +} +logDebug(`plugin loaded v${getPackageVersion()} pid=${process.pid} socket=${SOCKET_PATH}`); var DEFAULT_MAX_UPLOAD_BYTES = 512 * 1024; var MAX_UPLOAD_BYTES = (() => { const raw = process.env.OPENCODE_BROWSER_MAX_UPLOAD_BYTES; @@ -13197,7 +13221,11 @@ async function connectToBroker() { return await new Promise((resolve3, reject) => { const socket = net2.createConnection(SOCKET_PATH); socket.once("connect", () => resolve3(socket)); - socket.once("error", (err) => reject(err)); + socket.once("error", (err) => { + lastBrokerError = err instanceof Error ? err : new Error(String(err)); + logDebug(`broker connect error socket=${SOCKET_PATH} error=${lastBrokerError.message}`); + reject(err); + }); }); } async function sleep2(ms) { @@ -13206,6 +13234,7 @@ async function sleep2(ms) { var BACKEND_MODE = (process.env.OPENCODE_BROWSER_BACKEND ?? process.env.OPENCODE_BROWSER_MODE ?? "extension").toLowerCase().trim(); var USE_AGENT_BACKEND = ["agent", "agent-browser", "agentbrowser"].includes(BACKEND_MODE); var socket = null; +var lastBrokerError = null; var sessionId = Math.random().toString(36).slice(2); var reqId = 0; var pending = new Map; @@ -13226,9 +13255,11 @@ async function ensureBrokerSocket() { } } if (!socket || socket.destroyed) { - throw new Error("Could not connect to local broker. Run `npx @different-ai/opencode-browser install` and ensure the extension is loaded."); + const errorMessage = lastBrokerError?.message ? ` (${lastBrokerError.message})` : ""; + throw new Error(`Could not connect to local broker at ${SOCKET_PATH}${errorMessage}. ` + "Run `npx @different-ai/opencode-browser install` and ensure the extension is loaded."); } socket.setNoDelay(true); + logDebug(`broker connected socket=${SOCKET_PATH}`); socket.on("data", createJsonLineParser2((msg) => { if (msg?.type !== "response" || typeof msg.id !== "number") return; @@ -13309,25 +13340,20 @@ var plugin = async (ctx) => { description: "Debug plugin loading and connection status.", args: {}, async execute(args, ctx2) { - if (ctx2?.client?.app?.log) { - await ctx2.client.app.log({ - service: "opencode-browser", - level: "info", - message: "browser_debug called", - extra: { sessionId, pid: process.pid } - }); - } - return JSON.stringify({ - loaded: true, - sessionId, - pid: process.pid, - backend: USE_AGENT_BACKEND ? "agent-browser" : "extension", - agentSession: agentBackend?.session ?? null, - agentConnection: agentBackend?.connection ?? null, - agentBrowserVersion: agentBackend?.getVersion?.() ?? null, - pluginVersion: getPackageVersion(), - timestamp: new Date().toISOString() - }); + const lines = [ + "loaded: true", + `sessionId: ${sessionId}`, + `pid: ${process.pid}`, + `backend: ${USE_AGENT_BACKEND ? "agent-browser" : "extension"}`, + `brokerSocket: ${SOCKET_PATH}`, + `agentSession: ${agentBackend?.session ?? ""}`, + `agentConnection: ${JSON.stringify(agentBackend?.connection ?? null)}`, + `agentBrowserVersion: ${agentBackend?.getVersion?.() ?? ""}`, + `pluginVersion: ${getPackageVersion()}`, + `timestamp: ${new Date().toISOString()}` + ]; + return lines.join(` +`); } }), browser_version: tool({ diff --git a/src/plugin.ts b/src/plugin.ts index 4c7e2d5..0a9d6d5 100644 --- a/src/plugin.ts +++ b/src/plugin.ts @@ -2,8 +2,8 @@ import type { Plugin } from "@opencode-ai/plugin"; import { tool } from "@opencode-ai/plugin"; import net from "net"; import { createAgentBackend, type AgentBackend } from "./agent-backend.js"; -import { existsSync, mkdirSync, readFileSync, statSync } from "fs"; -import { homedir } from "os"; +import { appendFileSync, existsSync, mkdirSync, readFileSync, statSync } from "fs"; +import { homedir, userInfo } from "os"; import { basename, dirname, isAbsolute, join, resolve } from "path"; import { spawn } from "child_process"; import { fileURLToPath } from "url"; @@ -33,10 +33,37 @@ function getPackageVersion(): string { const { schema } = tool; const BASE_DIR = join(homedir(), ".opencode-browser"); -const SOCKET_PATH = join(BASE_DIR, "broker.sock"); +const SOCKET_PATH = getBrokerSocketPath(); +const LOG_PATH = join(BASE_DIR, "plugin.log"); + +function getSafePipeName(): string { + try { + const username = userInfo().username || "user"; + return `opencode-browser-${username}`.replace(/[^a-zA-Z0-9._-]/g, "_"); + } catch { + return "opencode-browser"; + } +} + +function getBrokerSocketPath(): string { + const override = process.env.OPENCODE_BROWSER_BROKER_SOCKET; + if (override) return override; + if (process.platform === "win32") return `\\\\.\\pipe\\${getSafePipeName()}`; + return join(BASE_DIR, "broker.sock"); +} mkdirSync(BASE_DIR, { recursive: true }); +function logDebug(message: string): void { + try { + appendFileSync(LOG_PATH, `[${new Date().toISOString()}] ${message}\n`, "utf8"); + } catch { + // ignore + } +} + +logDebug(`plugin loaded v${getPackageVersion()} pid=${process.pid} socket=${SOCKET_PATH}`); + const DEFAULT_MAX_UPLOAD_BYTES = 512 * 1024; const MAX_UPLOAD_BYTES = (() => { const raw = process.env.OPENCODE_BROWSER_MAX_UPLOAD_BYTES; @@ -114,7 +141,11 @@ async function connectToBroker(): Promise { return await new Promise((resolve, reject) => { const socket = net.createConnection(SOCKET_PATH); socket.once("connect", () => resolve(socket)); - socket.once("error", (err) => reject(err)); + socket.once("error", (err) => { + lastBrokerError = err instanceof Error ? err : new Error(String(err)); + logDebug(`broker connect error socket=${SOCKET_PATH} error=${lastBrokerError.message}`); + reject(err); + }); }); } @@ -128,6 +159,7 @@ const BACKEND_MODE = (process.env.OPENCODE_BROWSER_BACKEND ?? process.env.OPENCO const USE_AGENT_BACKEND = ["agent", "agent-browser", "agentbrowser"].includes(BACKEND_MODE); let socket: net.Socket | null = null; +let lastBrokerError: Error | null = null; let sessionId = Math.random().toString(36).slice(2); let reqId = 0; const pending = new Map void; reject: (e: Error) => void }>(); @@ -152,12 +184,15 @@ async function ensureBrokerSocket(): Promise { } if (!socket || socket.destroyed) { + const errorMessage = lastBrokerError?.message ? ` (${lastBrokerError.message})` : ""; throw new Error( - "Could not connect to local broker. Run `npx @different-ai/opencode-browser install` and ensure the extension is loaded." + `Could not connect to local broker at ${SOCKET_PATH}${errorMessage}. ` + + "Run `npx @different-ai/opencode-browser install` and ensure the extension is loaded." ); } socket.setNoDelay(true); + logDebug(`broker connected socket=${SOCKET_PATH}`); socket.on( "data", createJsonLineParser((msg) => { @@ -245,25 +280,19 @@ const plugin: Plugin = async (ctx) => { description: "Debug plugin loading and connection status.", args: {}, async execute(args, ctx) { - if (ctx?.client?.app?.log) { - await ctx.client.app.log({ - service: "opencode-browser", - level: "info", - message: "browser_debug called", - extra: { sessionId, pid: process.pid }, - }); - } - return JSON.stringify({ - loaded: true, - sessionId, - pid: process.pid, - backend: USE_AGENT_BACKEND ? "agent-browser" : "extension", - agentSession: agentBackend?.session ?? null, - agentConnection: agentBackend?.connection ?? null, - agentBrowserVersion: agentBackend?.getVersion?.() ?? null, - pluginVersion: getPackageVersion(), - timestamp: new Date().toISOString(), - }); + const lines = [ + "loaded: true", + `sessionId: ${sessionId}`, + `pid: ${process.pid}`, + `backend: ${USE_AGENT_BACKEND ? "agent-browser" : "extension"}`, + `brokerSocket: ${SOCKET_PATH}`, + `agentSession: ${agentBackend?.session ?? ""}`, + `agentConnection: ${JSON.stringify(agentBackend?.connection ?? null)}`, + `agentBrowserVersion: ${agentBackend?.getVersion?.() ?? ""}`, + `pluginVersion: ${getPackageVersion()}`, + `timestamp: ${new Date().toISOString()}`, + ]; + return lines.join("\n"); }, }),