@different-ai/opencode-browser
Advanced tools
@@ -16,2 +16,3 @@ --- | ||
| - Confirm state changes after each action | ||
| - Support CLI-first debugging with `opencode-browser tool` commands | ||
@@ -28,2 +29,12 @@ ## Best-practice workflow | ||
| ## CLI-first debugging | ||
| - List all available tools: `npx @different-ai/opencode-browser tools` | ||
| - Run one tool directly: `npx @different-ai/opencode-browser tool browser_status` | ||
| - Pass JSON args: `npx @different-ai/opencode-browser tool browser_query --args '{"mode":"page_text"}'` | ||
| - Run smoke test: `npx @different-ai/opencode-browser self-test` | ||
| - After `update`, reload the unpacked extension in `chrome://extensions` | ||
| This path is useful for reproducing selector/scroll issues quickly before running a full OpenCode session. | ||
| ## Selecting options | ||
@@ -51,3 +62,5 @@ | ||
| - If a selector fails, run `browser_query` with `mode=page_text` to confirm the content exists | ||
| - Use `mode=list` on broad selectors (`button`, `a`, `*[role="button"]`) and choose by index | ||
| - Use `mode=list` on broad selectors (`button`, `a`, `*[role="button"]`, `*[role="listitem"]`) and choose by index | ||
| - For inbox/chat panes, try text selectors first (`text:Subject line`) then verify selection with `browser_query` | ||
| - For scrollable containers, pass both `selector` and `x`/`y` to `browser_scroll` and then verify `scrollTop` | ||
| - Confirm results after each action |
+17
-1
@@ -10,4 +10,20 @@ #!/usr/bin/env node | ||
| 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 }); | ||
@@ -14,0 +30,0 @@ |
+395
-52
@@ -24,8 +24,8 @@ #!/usr/bin/env node | ||
| } from "fs"; | ||
| import { homedir, platform } from "os"; | ||
| import { homedir, platform, userInfo } from "os"; | ||
| import { join, dirname } from "path"; | ||
| import { fileURLToPath } from "url"; | ||
| import { fileURLToPath, pathToFileURL } 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"; | ||
@@ -42,7 +42,11 @@ | ||
| 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(); | ||
@@ -62,2 +66,52 @@ const COLORS = { | ||
| 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) { | ||
@@ -186,3 +240,4 @@ console.log(msg); | ||
| 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") | ||
@@ -197,2 +252,7 @@ .trim(); | ||
| 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`; | ||
@@ -284,2 +344,3 @@ writeFileSync(NATIVE_HOST_WRAPPER, script, { mode: 0o755 }); | ||
| function getNativeHostDirs(osName) { | ||
| if (osName === "win32") return []; | ||
| if (osName === "darwin") { | ||
@@ -321,2 +382,54 @@ const base = join(homedir(), "Library", "Application Support"); | ||
| 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() { | ||
@@ -336,2 +449,175 @@ try { | ||
| async function loadPluginTools() { | ||
| const pluginPath = join(PACKAGE_ROOT, "dist", "plugin.js"); | ||
| if (!existsSync(pluginPath)) { | ||
| throw new Error("dist/plugin.js is missing. Run `bun run build` first."); | ||
| } | ||
| const mod = await import(pathToFileURL(pluginPath).href); | ||
| const factory = mod?.default; | ||
| if (typeof factory !== "function") { | ||
| throw new Error("Could not load plugin factory from dist/plugin.js"); | ||
| } | ||
| const pluginInstance = await factory({}); | ||
| const tools = pluginInstance?.tool; | ||
| if (!tools || typeof tools !== "object") { | ||
| throw new Error("Plugin did not expose any tools"); | ||
| } | ||
| return tools; | ||
| } | ||
| function parseJsonArg(raw, fallback = {}) { | ||
| if (!raw) return fallback; | ||
| try { | ||
| return JSON.parse(raw); | ||
| } catch (err) { | ||
| throw new Error(`Expected JSON args. Received: ${raw}`); | ||
| } | ||
| } | ||
| function parseMaybeJson(value) { | ||
| if (typeof value !== "string") return value; | ||
| const trimmed = value.trim(); | ||
| if (!trimmed) return value; | ||
| if (!["{", "[", '"'].includes(trimmed[0])) return value; | ||
| try { | ||
| return JSON.parse(trimmed); | ||
| } catch { | ||
| return value; | ||
| } | ||
| } | ||
| function getToolArgJson() { | ||
| const byFlag = getFlagValue("--args"); | ||
| if (byFlag != null) return byFlag; | ||
| return process.argv[4] || null; | ||
| } | ||
| async function executeTool(toolName, args = {}) { | ||
| const tools = await loadPluginTools(); | ||
| const tool = tools?.[toolName]; | ||
| if (!tool || typeof tool.execute !== "function") { | ||
| const available = Object.keys(tools || {}) | ||
| .sort() | ||
| .join(", "); | ||
| throw new Error(`Unknown tool: ${toolName}. Available: ${available}`); | ||
| } | ||
| return await tool.execute(args, {}); | ||
| } | ||
| async function listTools() { | ||
| header("Browser Tools"); | ||
| const tools = await loadPluginTools(); | ||
| const names = Object.keys(tools).sort(); | ||
| if (!names.length) { | ||
| warn("No tools found in plugin."); | ||
| return; | ||
| } | ||
| log(`Found ${names.length} tools:\n`); | ||
| for (const name of names) { | ||
| const description = tools[name]?.description || "(no description)"; | ||
| log(`- ${name}: ${description}`); | ||
| } | ||
| } | ||
| async function runToolCommand() { | ||
| const toolName = process.argv[3]; | ||
| if (!toolName) { | ||
| throw new Error("Usage: npx @different-ai/opencode-browser tool <toolName> [argsJson]"); | ||
| } | ||
| const args = parseJsonArg(getToolArgJson(), {}); | ||
| const result = await executeTool(toolName, args); | ||
| if (typeof result === "string") { | ||
| log(result); | ||
| return; | ||
| } | ||
| log(JSON.stringify(result, null, 2)); | ||
| } | ||
| function asNumber(value, fallback = 0) { | ||
| const n = Number(value); | ||
| return Number.isFinite(n) ? n : fallback; | ||
| } | ||
| function readTabId(value) { | ||
| const parsed = parseMaybeJson(value); | ||
| if (parsed && Number.isFinite(parsed.tabId)) return parsed.tabId; | ||
| if (parsed?.content && Number.isFinite(parsed.content.tabId)) return parsed.content.tabId; | ||
| return null; | ||
| } | ||
| async function selfTest() { | ||
| header("CLI Self-Test"); | ||
| log("Running extension-backed smoke test via plugin tools..."); | ||
| const statusRaw = await executeTool("browser_status", {}); | ||
| const status = parseMaybeJson(statusRaw); | ||
| if (!status || status.broker !== true || status.hostConnected !== true) { | ||
| throw new Error( | ||
| "browser_status indicates the extension is not connected. Run `npx @different-ai/opencode-browser install` and click the extension icon in Chrome." | ||
| ); | ||
| } | ||
| const fixtureUrl = "https://www.w3.org/WAI/ARIA/apg/patterns/listbox/examples/listbox-scrollable/"; | ||
| const openRaw = await executeTool("browser_open_tab", { url: fixtureUrl, active: false }); | ||
| const tabId = readTabId(openRaw); | ||
| if (!Number.isFinite(tabId)) { | ||
| throw new Error("Failed to read tabId from browser_open_tab output"); | ||
| } | ||
| await executeTool("browser_wait", { ms: 250 }); | ||
| const beforeRaw = await executeTool("browser_query", { | ||
| selector: "[role='listbox']", | ||
| mode: "property", | ||
| property: "scrollTop", | ||
| tabId, | ||
| }); | ||
| const before = asNumber(parseMaybeJson(beforeRaw)?.value, 0); | ||
| await executeTool("browser_click", { | ||
| selector: "text:Neptunium", | ||
| tabId, | ||
| timeoutMs: 3000, | ||
| pollMs: 150, | ||
| }); | ||
| const selectedRaw = await executeTool("browser_query", { | ||
| selector: "[aria-selected='true']", | ||
| mode: "text", | ||
| tabId, | ||
| }); | ||
| const selectedText = String(parseMaybeJson(selectedRaw) || ""); | ||
| if (!selectedText.toLowerCase().includes("neptunium")) { | ||
| throw new Error(`Click verification failed. Expected selected text to include Neptunium, got: ${selectedText}`); | ||
| } | ||
| await executeTool("browser_scroll", { | ||
| selector: "[role='listbox']", | ||
| y: 320, | ||
| tabId, | ||
| timeoutMs: 2000, | ||
| pollMs: 100, | ||
| }); | ||
| await executeTool("browser_wait", { ms: 250 }); | ||
| const afterRaw = await executeTool("browser_query", { | ||
| selector: "[role='listbox']", | ||
| mode: "property", | ||
| property: "scrollTop", | ||
| tabId, | ||
| }); | ||
| const after = asNumber(parseMaybeJson(afterRaw)?.value, 0); | ||
| if (after <= before) { | ||
| throw new Error(`Scroll verification failed. Expected scrollTop to increase (before=${before}, after=${after}).`); | ||
| } | ||
| success("Self-test passed: click + selector text + container scroll are working."); | ||
| } | ||
| async function main() { | ||
@@ -349,2 +635,8 @@ const command = process.argv[2]; | ||
| await update(); | ||
| } else if (command === "tools") { | ||
| await listTools(); | ||
| } else if (command === "tool") { | ||
| await runToolCommand(); | ||
| } else if (command === "self-test") { | ||
| await selfTest(); | ||
| } else if (command === "uninstall") { | ||
@@ -365,2 +657,5 @@ await uninstall(); | ||
| npx @different-ai/opencode-browser uninstall | ||
| npx @different-ai/opencode-browser tools | ||
| npx @different-ai/opencode-browser tool <toolName> [argsJson] | ||
| npx @different-ai/opencode-browser self-test | ||
| npx @different-ai/opencode-browser agent-install | ||
@@ -371,2 +666,3 @@ npx @different-ai/opencode-browser agent-gateway | ||
| --extension-id <id> (or OPENCODE_BROWSER_EXTENSION_ID) | ||
| --args '{"selector":"text:Inbox"}' (for tool command) | ||
@@ -386,2 +682,3 @@ ${color("bright", "Quick Start:")} | ||
| rl.close(); | ||
| process.exit(0); | ||
| } | ||
@@ -392,9 +689,9 @@ | ||
| 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"}`); | ||
@@ -488,9 +785,15 @@ header("Step 2: Copy Extension Files"); | ||
| 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}`); | ||
| } | ||
| } | ||
@@ -526,5 +829,9 @@ } | ||
| 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", | ||
@@ -544,4 +851,8 @@ "4) Skip (does nothing)", | ||
| } 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); | ||
@@ -679,9 +990,9 @@ } else if (selection === "3") { | ||
| 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"}`); | ||
@@ -763,9 +1074,15 @@ header("Step 1: Copy Extension Files"); | ||
| 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}`); | ||
| } | ||
| } | ||
@@ -791,2 +1108,3 @@ } | ||
| success(`Host wrapper installed: ${existsSync(NATIVE_HOST_WRAPPER)}`); | ||
| success(`Broker socket: ${BROKER_SOCKET}`); | ||
@@ -809,14 +1127,25 @@ const cfg = loadConfig(); | ||
| 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"}`); | ||
| } | ||
@@ -853,16 +1182,30 @@ } | ||
| 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; | ||
@@ -869,0 +1212,0 @@ try { |
+17
-1
@@ -14,5 +14,21 @@ #!/usr/bin/env node | ||
| 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 }); | ||
@@ -19,0 +35,0 @@ |
@@ -525,3 +525,3 @@ const NATIVE_HOST_NAME = "com.opencode.browser_automation" | ||
| const candidates = deepQuerySelectorAll( | ||
| "button, a, label, option, summary, [role='button'], [role='link'], [role='tab'], [role='menuitem']", | ||
| "button, a, label, option, summary, [role='button'], [role='link'], [role='tab'], [role='menuitem'], [role='option'], [role='listitem'], [role='row'], [tabindex]", | ||
| document | ||
@@ -536,2 +536,19 @@ ) | ||
| } | ||
| const generic = deepQuerySelectorAll("div, span, li, article", document) | ||
| for (const el of generic) { | ||
| if (!matchesText(el.innerText || el.textContent || "", target)) continue | ||
| const style = window.getComputedStyle(el) | ||
| const likelyInteractive = | ||
| !!el.getAttribute("onclick") || | ||
| !!el.getAttribute("role") || | ||
| el.tabIndex >= 0 || | ||
| style.cursor === "pointer" | ||
| if (!likelyInteractive) continue | ||
| if (!seen.has(el)) { | ||
| seen.add(el) | ||
| results.push(el) | ||
| } | ||
| } | ||
| const inputs = deepQuerySelectorAll("input[type='button'], input[type='submit'], input[type='reset']", document) | ||
@@ -896,2 +913,17 @@ for (const el of inputs) { | ||
| } | ||
| if (scrollX || scrollY) { | ||
| try { | ||
| if (typeof match.chosen.scrollBy === "function") { | ||
| match.chosen.scrollBy({ left: scrollX, top: scrollY, behavior: "smooth" }) | ||
| } else { | ||
| match.chosen.scrollLeft = Number(match.chosen.scrollLeft || 0) + scrollX | ||
| match.chosen.scrollTop = Number(match.chosen.scrollTop || 0) + scrollY | ||
| } | ||
| } catch { | ||
| match.chosen.scrollLeft = Number(match.chosen.scrollLeft || 0) + scrollX | ||
| match.chosen.scrollTop = Number(match.chosen.scrollTop || 0) + scrollY | ||
| } | ||
| return { ok: true, selectorUsed: match.selectorUsed, elementScroll: { x: scrollX, y: scrollY } } | ||
| } | ||
| try { | ||
@@ -898,0 +930,0 @@ match.chosen.scrollIntoView({ behavior: "smooth", block: "center" }) |
+3
-1
| { | ||
| "name": "@different-ai/opencode-browser", | ||
| "version": "4.6.0", | ||
| "version": "4.6.1", | ||
| "description": "Browser automation plugin for OpenCode (native messaging + per-tab ownership).", | ||
@@ -30,2 +30,4 @@ "type": "module", | ||
| "status": "node bin/cli.js status", | ||
| "tools": "node bin/cli.js tools", | ||
| "self-test": "node bin/cli.js self-test", | ||
| "tool-test": "bun bin/tool-test.ts" | ||
@@ -32,0 +34,0 @@ }, |
+21
-0
@@ -27,3 +27,5 @@ # OpenCode Browser | ||
| Supports macOS, Linux, and Windows (Chrome/Edge/Brave/Chromium). | ||
| https://github.com/user-attachments/assets/d5767362-fbf3-4023-858b-90f06d9f0b25 | ||
@@ -62,2 +64,21 @@ | ||
| ## CLI tool runner (for local debugging) | ||
| Run plugin tools directly from the package CLI (without starting an OpenCode session): | ||
| ```bash | ||
| # list available browser_* tools | ||
| npx @different-ai/opencode-browser tools | ||
| # run a single tool | ||
| npx @different-ai/opencode-browser tool browser_status | ||
| npx @different-ai/opencode-browser tool browser_query --args '{"mode":"page_text"}' | ||
| # run built-in end-to-end smoke test (click + text selector + container scroll) | ||
| npx @different-ai/opencode-browser self-test | ||
| ``` | ||
| This is useful for debugging issue reports (for example inbox/chat UIs) before involving a full OpenCode workflow. | ||
| After `update`, reload the unpacked extension in `chrome://extensions` before running `self-test`. | ||
| ## Chrome Web Store maintainer flow | ||
@@ -64,0 +85,0 @@ |
Sorry, the diff of this file is too big to display
Network access
Supply chain riskThis module accesses the network.
Found 1 instance in 1 package
Shell access
Supply chain riskThis module accesses the system shell. Accessing the system shell increases the risk of executing arbitrary code.
Found 1 instance in 1 package
Debug access
Supply chain riskUses debug, reflection and dynamic code execution features.
Found 1 instance in 1 package
Environment variable access
Supply chain riskPackage accesses environment variables, which may be a sign of credential stuffing or data theft.
Found 22 instances in 1 package
Filesystem access
Supply chain riskAccesses the file system, and could potentially read sensitive data.
Found 1 instance in 1 package
Long strings
Supply chain riskContains long string literals, which may be a sign of obfuscated or packed code.
Found 1 instance in 1 package
URL strings
Supply chain riskPackage contains fragments of external URLs or IP addresses, which the package may be accessing at runtime.
Found 1 instance in 1 package
Network access
Supply chain riskThis module accesses the network.
Found 1 instance in 1 package
Shell access
Supply chain riskThis module accesses the system shell. Accessing the system shell increases the risk of executing arbitrary code.
Found 1 instance in 1 package
Debug access
Supply chain riskUses debug, reflection and dynamic code execution features.
Found 1 instance in 1 package
Environment variable access
Supply chain riskPackage accesses environment variables, which may be a sign of credential stuffing or data theft.
Found 21 instances in 1 package
Filesystem access
Supply chain riskAccesses the file system, and could potentially read sensitive data.
Found 1 instance in 1 package
Long strings
Supply chain riskContains long string literals, which may be a sign of obfuscated or packed code.
Found 1 instance in 1 package
URL strings
Supply chain riskPackage contains fragments of external URLs or IP addresses, which the package may be accessing at runtime.
Found 1 instance in 1 package
580501
2.77%16787
2.37%228
10.14%66
6.45%