You're Invited:Meet the Socket Team at RSAC and BSidesSF 2026, March 23–26.RSVP
Socket
Book a DemoSign in
Socket

@different-ai/opencode-browser

Package Overview
Dependencies
Maintainers
2
Versions
35
Alerts
File Explorer

Advanced tools

Socket logo

Install Socket

Detect and block malicious and high-risk dependencies

Install

@different-ai/opencode-browser - npm Package Compare versions

Comparing version
1.0.0
to
1.0.1
+207
src/daemon.js
#!/usr/bin/env node
/**
* Persistent Browser Bridge Daemon
*
* Runs as a background service and bridges:
* - Chrome extension (via WebSocket on localhost)
* - MCP server (via Unix socket)
*
* This allows scheduled jobs to use browser tools even if
* the OpenCode session that created the job isn't running.
*/
import { createServer as createNetServer } from "net";
import { WebSocketServer } from "ws";
import { existsSync, mkdirSync, unlinkSync, appendFileSync } from "fs";
import { homedir } from "os";
import { join } from "path";
const BASE_DIR = join(homedir(), ".opencode-browser");
const LOG_DIR = join(BASE_DIR, "logs");
const SOCKET_PATH = join(BASE_DIR, "browser.sock");
const WS_PORT = 19222;
if (!existsSync(LOG_DIR)) mkdirSync(LOG_DIR, { recursive: true });
const LOG_FILE = join(LOG_DIR, "daemon.log");
function log(...args) {
const timestamp = new Date().toISOString();
const message = `[${timestamp}] ${args.join(" ")}\n`;
appendFileSync(LOG_FILE, message);
console.error(message.trim());
}
log("Daemon starting...");
// State
let chromeConnection = null;
let mcpConnections = new Set();
let pendingRequests = new Map();
let requestId = 0;
// ============================================================================
// WebSocket Server for Chrome Extension
// ============================================================================
const wss = new WebSocketServer({ port: WS_PORT });
wss.on("connection", (ws) => {
log("Chrome extension connected via WebSocket");
chromeConnection = ws;
ws.on("message", (data) => {
try {
const message = JSON.parse(data.toString());
handleChromeMessage(message);
} catch (e) {
log("Failed to parse Chrome message:", e.message);
}
});
ws.on("close", () => {
log("Chrome extension disconnected");
chromeConnection = null;
});
ws.on("error", (err) => {
log("Chrome WebSocket error:", err.message);
});
});
wss.on("listening", () => {
log(`WebSocket server listening on port ${WS_PORT}`);
});
function sendToChrome(message) {
if (chromeConnection && chromeConnection.readyState === 1) {
chromeConnection.send(JSON.stringify(message));
return true;
}
return false;
}
function handleChromeMessage(message) {
log("From Chrome:", message.type);
if (message.type === "tool_response") {
const pending = pendingRequests.get(message.id);
if (pending) {
pendingRequests.delete(message.id);
sendToMcp(pending.socket, {
type: "tool_response",
id: pending.mcpId,
result: message.result,
error: message.error
});
}
} else if (message.type === "pong") {
log("Chrome ping OK");
}
}
// ============================================================================
// Unix Socket Server for MCP
// ============================================================================
try {
if (existsSync(SOCKET_PATH)) {
unlinkSync(SOCKET_PATH);
}
} catch {}
const unixServer = createNetServer((socket) => {
log("MCP server connected");
mcpConnections.add(socket);
let buffer = "";
socket.on("data", (data) => {
buffer += data.toString();
const lines = buffer.split("\n");
buffer = lines.pop() || "";
for (const line of lines) {
if (line.trim()) {
try {
const message = JSON.parse(line);
handleMcpMessage(socket, message);
} catch (e) {
log("Failed to parse MCP message:", e.message);
}
}
}
});
socket.on("close", () => {
log("MCP server disconnected");
mcpConnections.delete(socket);
});
socket.on("error", (err) => {
log("MCP socket error:", err.message);
mcpConnections.delete(socket);
});
});
unixServer.listen(SOCKET_PATH, () => {
log(`Unix socket listening at ${SOCKET_PATH}`);
});
function sendToMcp(socket, message) {
if (socket && !socket.destroyed) {
socket.write(JSON.stringify(message) + "\n");
}
}
function handleMcpMessage(socket, message) {
log("From MCP:", message.type, message.tool || "");
if (message.type === "tool_request") {
if (!chromeConnection) {
sendToMcp(socket, {
type: "tool_response",
id: message.id,
error: { content: "Chrome extension not connected. Open Chrome and ensure the OpenCode extension is enabled." }
});
return;
}
const id = ++requestId;
pendingRequests.set(id, { socket, mcpId: message.id });
sendToChrome({
type: "tool_request",
id,
tool: message.tool,
args: message.args
});
}
}
// ============================================================================
// Health Check
// ============================================================================
setInterval(() => {
if (chromeConnection) {
sendToChrome({ type: "ping" });
}
}, 30000);
// ============================================================================
// Graceful Shutdown
// ============================================================================
function shutdown() {
log("Shutting down...");
wss.close();
unixServer.close();
try { unlinkSync(SOCKET_PATH); } catch {}
process.exit(0);
}
process.on("SIGTERM", shutdown);
process.on("SIGINT", shutdown);
log("Daemon started successfully");
+89
-4

@@ -88,10 +88,21 @@ #!/usr/bin/env node

await uninstall();
} else if (command === "daemon") {
await startDaemon();
} else if (command === "daemon-install") {
await installDaemon();
} else if (command === "start") {
rl.close();
await import("../src/server.js");
return;
} else {
log(`
${color("bright", "Usage:")}
npx opencode-browser install Install extension and native host
npx opencode-browser uninstall Remove native host registration
npx @different-ai/opencode-browser install Install extension
npx @different-ai/opencode-browser daemon-install Install background daemon
npx @different-ai/opencode-browser daemon Run daemon (foreground)
npx @different-ai/opencode-browser start Run MCP server
npx @different-ai/opencode-browser uninstall Remove installation
${color("bright", "After installation:")}
The MCP server starts automatically when OpenCode connects.
${color("bright", "For scheduled jobs:")}
Run 'daemon-install' to enable browser tools in background jobs.
`);

@@ -287,2 +298,76 @@ }

async function startDaemon() {
const { spawn } = await import("child_process");
const daemonPath = join(PACKAGE_ROOT, "src", "daemon.js");
log("Starting daemon...");
const child = spawn(process.execPath, [daemonPath], { stdio: "inherit" });
child.on("exit", (code) => process.exit(code || 0));
}
async function installDaemon() {
header("Installing Background Daemon");
const os = platform();
if (os !== "darwin") {
error("Daemon auto-install currently supports macOS only");
log("On Linux, create a systemd service manually.");
process.exit(1);
}
const nodePath = process.execPath;
const daemonPath = join(PACKAGE_ROOT, "src", "daemon.js");
const logsDir = join(homedir(), ".opencode-browser", "logs");
mkdirSync(logsDir, { recursive: true });
const plist = `<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>Label</key>
<string>com.opencode.browser-daemon</string>
<key>ProgramArguments</key>
<array>
<string>${nodePath}</string>
<string>${daemonPath}</string>
</array>
<key>RunAtLoad</key>
<true/>
<key>KeepAlive</key>
<true/>
<key>StandardOutPath</key>
<string>${logsDir}/daemon.log</string>
<key>StandardErrorPath</key>
<string>${logsDir}/daemon.log</string>
</dict>
</plist>`;
const plistPath = join(homedir(), "Library", "LaunchAgents", "com.opencode.browser-daemon.plist");
writeFileSync(plistPath, plist);
success(`Created launchd plist: ${plistPath}`);
try {
execSync(`launchctl unload "${plistPath}" 2>/dev/null || true`);
execSync(`launchctl load "${plistPath}"`);
success("Daemon started");
} catch (e) {
error(`Failed to load daemon: ${e.message}`);
}
log(`
${color("green", "✓")} Daemon installed and running
The daemon bridges Chrome extension ↔ MCP server.
It runs automatically on login and enables browser
tools in scheduled OpenCode jobs.
${color("bright", "Logs:")} ${logsDir}/daemon.log
${color("bright", "Control:")}
launchctl stop com.opencode.browser-daemon
launchctl start com.opencode.browser-daemon
launchctl unload ~/Library/LaunchAgents/com.opencode.browser-daemon.plist
`);
}
async function uninstall() {

@@ -289,0 +374,0 @@ header("Uninstalling OpenCode Browser");

+130
-301

@@ -1,106 +0,83 @@

// OpenCode Browser Automation - Background Service Worker
// Native Messaging Host: com.opencode.browser_automation
const DAEMON_URL = "ws://localhost:19222";
const KEEPALIVE_ALARM = "keepalive";
const NATIVE_HOST_NAME = "com.opencode.browser_automation";
let nativePort = null;
let ws = null;
let isConnected = false;
// ============================================================================
// Native Messaging Connection
// ============================================================================
chrome.alarms.create(KEEPALIVE_ALARM, { periodInMinutes: 0.25 });
async function connectToNativeHost() {
if (nativePort) {
return true;
chrome.alarms.onAlarm.addListener((alarm) => {
if (alarm.name === KEEPALIVE_ALARM) {
if (!isConnected) {
console.log("[OpenCode] Alarm triggered reconnect");
connect();
}
}
});
function connect() {
if (ws && ws.readyState === WebSocket.OPEN) return;
if (ws) {
try { ws.close(); } catch {}
ws = null;
}
try {
nativePort = chrome.runtime.connectNative(NATIVE_HOST_NAME);
ws = new WebSocket(DAEMON_URL);
nativePort.onMessage.addListener(handleNativeMessage);
ws.onopen = () => {
console.log("[OpenCode] Connected to daemon");
isConnected = true;
updateBadge(true);
};
nativePort.onDisconnect.addListener(() => {
const error = chrome.runtime.lastError?.message;
console.log("[OpenCode] Native host disconnected:", error);
nativePort = null;
ws.onmessage = async (event) => {
try {
const message = JSON.parse(event.data);
await handleMessage(message);
} catch (e) {
console.error("[OpenCode] Parse error:", e);
}
};
ws.onclose = () => {
console.log("[OpenCode] Disconnected");
isConnected = false;
});
// Ping to verify connection
const connected = await new Promise((resolve) => {
const timeout = setTimeout(() => resolve(false), 5000);
const pingHandler = (msg) => {
if (msg.type === "pong") {
clearTimeout(timeout);
nativePort.onMessage.removeListener(pingHandler);
resolve(true);
}
};
nativePort.onMessage.addListener(pingHandler);
nativePort.postMessage({ type: "ping" });
});
if (connected) {
isConnected = true;
console.log("[OpenCode] Connected to native host");
return true;
} else {
nativePort.disconnect();
nativePort = null;
return false;
}
} catch (error) {
console.error("[OpenCode] Failed to connect:", error);
nativePort = null;
return false;
ws = null;
updateBadge(false);
};
ws.onerror = (err) => {
console.error("[OpenCode] WebSocket error");
isConnected = false;
updateBadge(false);
};
} catch (e) {
console.error("[OpenCode] Connect failed:", e);
isConnected = false;
updateBadge(false);
}
}
function disconnectNativeHost() {
if (nativePort) {
nativePort.disconnect();
nativePort = null;
isConnected = false;
}
function updateBadge(connected) {
chrome.action.setBadgeText({ text: connected ? "ON" : "" });
chrome.action.setBadgeBackgroundColor({ color: connected ? "#22c55e" : "#ef4444" });
}
// ============================================================================
// Message Handling from Native Host
// ============================================================================
async function handleNativeMessage(message) {
console.log("[OpenCode] Received from native:", message.type);
switch (message.type) {
case "tool_request":
await handleToolRequest(message);
break;
case "ping":
sendToNative({ type: "pong" });
break;
case "get_status":
sendToNative({
type: "status_response",
connected: isConnected,
version: chrome.runtime.getManifest().version
});
break;
function send(message) {
if (ws && ws.readyState === WebSocket.OPEN) {
ws.send(JSON.stringify(message));
return true;
}
return false;
}
function sendToNative(message) {
if (nativePort) {
nativePort.postMessage(message);
} else {
console.error("[OpenCode] Cannot send - not connected");
async function handleMessage(message) {
if (message.type === "tool_request") {
await handleToolRequest(message);
} else if (message.type === "ping") {
send({ type: "pong" });
}
}
// ============================================================================
// Tool Execution
// ============================================================================
async function handleToolRequest(request) {

@@ -111,13 +88,5 @@ const { id, tool, args } = request;

const result = await executeTool(tool, args || {});
sendToNative({
type: "tool_response",
id,
result: { content: result }
});
send({ type: "tool_response", id, result: { content: result } });
} catch (error) {
sendToNative({
type: "tool_response",
id,
error: { content: error.message || String(error) }
});
send({ type: "tool_response", id, error: { content: error.message || String(error) } });
}

@@ -127,30 +96,19 @@ }

async function executeTool(toolName, args) {
switch (toolName) {
case "navigate":
return await toolNavigate(args);
case "click":
return await toolClick(args);
case "type":
return await toolType(args);
case "screenshot":
return await toolScreenshot(args);
case "snapshot":
return await toolSnapshot(args);
case "get_tabs":
return await toolGetTabs(args);
case "execute_script":
return await toolExecuteScript(args);
case "scroll":
return await toolScroll(args);
case "wait":
return await toolWait(args);
default:
throw new Error(`Unknown tool: ${toolName}`);
}
const tools = {
navigate: toolNavigate,
click: toolClick,
type: toolType,
screenshot: toolScreenshot,
snapshot: toolSnapshot,
get_tabs: toolGetTabs,
execute_script: toolExecuteScript,
scroll: toolScroll,
wait: toolWait
};
const fn = tools[toolName];
if (!fn) throw new Error(`Unknown tool: ${toolName}`);
return await fn(args);
}
// ============================================================================
// Tool Implementations
// ============================================================================
async function getActiveTab() {

@@ -163,6 +121,3 @@ const [tab] = await chrome.tabs.query({ active: true, currentWindow: true });

async function getTabById(tabId) {
if (tabId) {
return await chrome.tabs.get(tabId);
}
return await getActiveTab();
return tabId ? await chrome.tabs.get(tabId) : await getActiveTab();
}

@@ -172,7 +127,5 @@

if (!url) throw new Error("URL is required");
const tab = await getTabById(tabId);
await chrome.tabs.update(tab.id, { url });
// Wait for page to load
await new Promise((resolve) => {

@@ -186,7 +139,3 @@ const listener = (updatedTabId, info) => {

chrome.tabs.onUpdated.addListener(listener);
// Timeout after 30 seconds
setTimeout(() => {
chrome.tabs.onUpdated.removeListener(listener);
resolve();
}, 30000);
setTimeout(() => { chrome.tabs.onUpdated.removeListener(listener); resolve(); }, 30000);
});

@@ -199,3 +148,2 @@

if (!selector) throw new Error("Selector is required");
const tab = await getTabById(tabId);

@@ -206,5 +154,5 @@

func: (sel) => {
const element = document.querySelector(sel);
if (!element) return { success: false, error: `Element not found: ${sel}` };
element.click();
const el = document.querySelector(sel);
if (!el) return { success: false, error: `Element not found: ${sel}` };
el.click();
return { success: true };

@@ -215,6 +163,3 @@ },

if (!result[0]?.result?.success) {
throw new Error(result[0]?.result?.error || "Click failed");
}
if (!result[0]?.result?.success) throw new Error(result[0]?.result?.error || "Click failed");
return `Clicked ${selector}`;

@@ -226,3 +171,2 @@ }

if (text === undefined) throw new Error("Text is required");
const tab = await getTabById(tabId);

@@ -233,19 +177,13 @@

func: (sel, txt, shouldClear) => {
const element = document.querySelector(sel);
if (!element) return { success: false, error: `Element not found: ${sel}` };
element.focus();
if (shouldClear) {
element.value = "";
}
// For input/textarea, set value directly
if (element.tagName === "INPUT" || element.tagName === "TEXTAREA") {
element.value = element.value + txt;
element.dispatchEvent(new Event("input", { bubbles: true }));
element.dispatchEvent(new Event("change", { bubbles: true }));
} else if (element.isContentEditable) {
const el = document.querySelector(sel);
if (!el) return { success: false, error: `Element not found: ${sel}` };
el.focus();
if (shouldClear) el.value = "";
if (el.tagName === "INPUT" || el.tagName === "TEXTAREA") {
el.value = el.value + txt;
el.dispatchEvent(new Event("input", { bubbles: true }));
el.dispatchEvent(new Event("change", { bubbles: true }));
} else if (el.isContentEditable) {
document.execCommand("insertText", false, txt);
}
return { success: true };

@@ -256,18 +194,9 @@ },

if (!result[0]?.result?.success) {
throw new Error(result[0]?.result?.error || "Type failed");
}
if (!result[0]?.result?.success) throw new Error(result[0]?.result?.error || "Type failed");
return `Typed "${text}" into ${selector}`;
}
async function toolScreenshot({ tabId, fullPage = false }) {
async function toolScreenshot({ tabId }) {
const tab = await getTabById(tabId);
// Capture visible area
const dataUrl = await chrome.tabs.captureVisibleTab(tab.windowId, {
format: "png"
});
return dataUrl;
return await chrome.tabs.captureVisibleTab(tab.windowId, { format: "png" });
}

@@ -281,65 +210,28 @@

func: () => {
// Build accessibility tree snapshot
function getAccessibleName(element) {
return element.getAttribute("aria-label") ||
element.getAttribute("alt") ||
element.getAttribute("title") ||
element.getAttribute("placeholder") ||
element.innerText?.slice(0, 100) ||
"";
function getName(el) {
return el.getAttribute("aria-label") || el.getAttribute("alt") ||
el.getAttribute("title") || el.getAttribute("placeholder") ||
el.innerText?.slice(0, 100) || "";
}
function getRole(element) {
return element.getAttribute("role") ||
element.tagName.toLowerCase();
}
function buildSnapshot(element, depth = 0, uid = 0) {
function build(el, depth = 0, uid = 0) {
if (depth > 10) return { nodes: [], nextUid: uid };
const nodes = [];
const style = window.getComputedStyle(element);
const style = window.getComputedStyle(el);
if (style.display === "none" || style.visibility === "hidden") return { nodes: [], nextUid: uid };
// Skip hidden elements
if (style.display === "none" || style.visibility === "hidden") {
return { nodes: [], nextUid: uid };
}
const isInteractive = ["A", "BUTTON", "INPUT", "TEXTAREA", "SELECT"].includes(el.tagName) ||
el.getAttribute("onclick") || el.getAttribute("role") === "button" || el.isContentEditable;
const rect = el.getBoundingClientRect();
const isInteractive =
element.tagName === "A" ||
element.tagName === "BUTTON" ||
element.tagName === "INPUT" ||
element.tagName === "TEXTAREA" ||
element.tagName === "SELECT" ||
element.getAttribute("onclick") ||
element.getAttribute("role") === "button" ||
element.isContentEditable;
const rect = element.getBoundingClientRect();
const isVisible = rect.width > 0 && rect.height > 0;
if (isVisible && (isInteractive || element.innerText?.trim())) {
const node = {
uid: `e${uid}`,
role: getRole(element),
name: getAccessibleName(element).slice(0, 200),
tag: element.tagName.toLowerCase()
};
if (element.tagName === "A" && element.href) {
node.href = element.href;
if (rect.width > 0 && rect.height > 0 && (isInteractive || el.innerText?.trim())) {
const node = { uid: `e${uid}`, role: el.getAttribute("role") || el.tagName.toLowerCase(),
name: getName(el).slice(0, 200), tag: el.tagName.toLowerCase() };
if (el.tagName === "A" && el.href) node.href = el.href;
if (el.tagName === "INPUT") { node.type = el.type; node.value = el.value; }
if (el.id) node.selector = `#${el.id}`;
else if (el.className && typeof el.className === "string") {
const cls = el.className.trim().split(/\s+/).slice(0, 2).join(".");
if (cls) node.selector = `${el.tagName.toLowerCase()}.${cls}`;
}
if (element.tagName === "INPUT") {
node.type = element.type;
node.value = element.value;
}
// Generate a selector
if (element.id) {
node.selector = `#${element.id}`;
} else if (element.className && typeof element.className === "string") {
const classes = element.className.trim().split(/\s+/).slice(0, 2).join(".");
if (classes) node.selector = `${element.tagName.toLowerCase()}.${classes}`;
}
nodes.push(node);

@@ -349,18 +241,11 @@ uid++;

for (const child of element.children) {
const childResult = buildSnapshot(child, depth + 1, uid);
nodes.push(...childResult.nodes);
uid = childResult.nextUid;
for (const child of el.children) {
const r = build(child, depth + 1, uid);
nodes.push(...r.nodes);
uid = r.nextUid;
}
return { nodes, nextUid: uid };
}
const { nodes } = buildSnapshot(document.body);
return {
url: window.location.href,
title: document.title,
nodes: nodes.slice(0, 500) // Limit to 500 nodes
};
return { url: location.href, title: document.title, nodes: build(document.body).nodes.slice(0, 500) };
}

@@ -374,9 +259,3 @@ });

const tabs = await chrome.tabs.query({});
return JSON.stringify(tabs.map(t => ({
id: t.id,
url: t.url,
title: t.title,
active: t.active,
windowId: t.windowId
})), null, 2);
return JSON.stringify(tabs.map(t => ({ id: t.id, url: t.url, title: t.title, active: t.active, windowId: t.windowId })), null, 2);
}

@@ -386,10 +265,4 @@

if (!code) throw new Error("Code is required");
const tab = await getTabById(tabId);
const result = await chrome.scripting.executeScript({
target: { tabId: tab.id },
func: new Function(code)
});
const result = await chrome.scripting.executeScript({ target: { tabId: tab.id }, func: new Function(code) });
return JSON.stringify(result[0]?.result);

@@ -400,4 +273,2 @@ }

const tab = await getTabById(tabId);
// Ensure selector is null (not undefined) for proper serialization
const sel = selector || null;

@@ -408,9 +279,3 @@

func: (scrollX, scrollY, sel) => {
if (sel) {
const element = document.querySelector(sel);
if (element) {
element.scrollIntoView({ behavior: "smooth", block: "center" });
return;
}
}
if (sel) { const el = document.querySelector(sel); if (el) { el.scrollIntoView({ behavior: "smooth", block: "center" }); return; } }
window.scrollBy(scrollX, scrollY);

@@ -429,46 +294,10 @@ },

// ============================================================================
// Extension Lifecycle
// ============================================================================
chrome.runtime.onInstalled.addListener(async () => {
console.log("[OpenCode] Extension installed");
await connectToNativeHost();
chrome.runtime.onInstalled.addListener(() => connect());
chrome.runtime.onStartup.addListener(() => connect());
chrome.action.onClicked.addListener(() => {
connect();
chrome.notifications.create({ type: "basic", iconUrl: "icons/icon128.png", title: "OpenCode Browser",
message: isConnected ? "Connected" : "Reconnecting..." });
});
chrome.runtime.onStartup.addListener(async () => {
console.log("[OpenCode] Extension started");
await connectToNativeHost();
});
// Auto-reconnect on action click
chrome.action.onClicked.addListener(async () => {
if (!isConnected) {
const connected = await connectToNativeHost();
if (connected) {
chrome.notifications.create({
type: "basic",
iconUrl: "icons/icon128.png",
title: "OpenCode Browser",
message: "Connected to native host"
});
} else {
chrome.notifications.create({
type: "basic",
iconUrl: "icons/icon128.png",
title: "OpenCode Browser",
message: "Failed to connect. Is the native host installed?"
});
}
} else {
chrome.notifications.create({
type: "basic",
iconUrl: "icons/icon128.png",
title: "OpenCode Browser",
message: "Already connected"
});
}
});
// Try to connect on load
connectToNativeHost();
connect();

@@ -12,3 +12,4 @@ {

"storage",
"notifications"
"notifications",
"alarms"
],

@@ -15,0 +16,0 @@ "host_permissions": [

{
"name": "@different-ai/opencode-browser",
"version": "1.0.0",
"version": "1.0.1",
"description": "Browser automation for OpenCode via Chrome extension + Native Messaging. Inspired by Claude in Chrome.",

@@ -33,4 +33,5 @@ "type": "module",

"dependencies": {
"@modelcontextprotocol/sdk": "^1.0.0"
"@modelcontextprotocol/sdk": "^1.0.0",
"ws": "^8.18.3"
}
}