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
4.6.0
to
4.6.1
+14
-1
.opencode/skill/browser-automation/SKILL.md

@@ -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

@@ -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 {

@@ -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 @@ },

@@ -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