@different-ai/opencode-browser
Advanced tools
+158
-11
@@ -14,2 +14,16 @@ #!/usr/bin/env node | ||
| const DEFAULT_LEASE_TTL_MS = 5 * 60 * 1000; | ||
| const LEASE_TTL_MS = (() => { | ||
| const raw = process.env.OPENCODE_BROWSER_CLAIM_TTL_MS; | ||
| const value = Number(raw); | ||
| if (Number.isFinite(value) && value >= 0) return value; | ||
| return DEFAULT_LEASE_TTL_MS; | ||
| })(); | ||
| const LEASE_SWEEP_MS = | ||
| LEASE_TTL_MS > 0 ? Math.min(Math.max(10000, Math.floor(LEASE_TTL_MS / 2)), 60000) : 0; | ||
| function nowMs() { | ||
| return Date.now(); | ||
| } | ||
| function nowIso() { | ||
@@ -43,3 +57,3 @@ return new Date().toISOString(); | ||
| function wantsTab(toolName) { | ||
| return !["get_tabs", "get_active_tab"].includes(toolName); | ||
| return !["get_tabs", "get_active_tab", "open_tab"].includes(toolName); | ||
| } | ||
@@ -54,4 +68,6 @@ | ||
| // Tab ownership: tabId -> { sessionId, claimedAt } | ||
| // Tab ownership: tabId -> { sessionId, claimedAt, lastSeenAt } | ||
| const claims = new Map(); | ||
| // Session state: sessionId -> { defaultTabId, lastSeenAt } | ||
| const sessionState = new Map(); | ||
@@ -61,3 +77,8 @@ function listClaims() { | ||
| for (const [tabId, info] of claims.entries()) { | ||
| out.push({ tabId, ...info }); | ||
| out.push({ | ||
| tabId, | ||
| sessionId: info.sessionId, | ||
| claimedAt: info.claimedAt, | ||
| lastSeenAt: new Date(info.lastSeenAt).toISOString(), | ||
| }); | ||
| } | ||
@@ -68,2 +89,49 @@ out.sort((a, b) => a.tabId - b.tabId); | ||
| function sessionHasClaims(sessionId) { | ||
| for (const info of claims.values()) { | ||
| if (info.sessionId === sessionId) return true; | ||
| } | ||
| return false; | ||
| } | ||
| function getSessionState(sessionId) { | ||
| if (!sessionId) return null; | ||
| let state = sessionState.get(sessionId); | ||
| if (!state) { | ||
| state = { defaultTabId: null, lastSeenAt: nowMs() }; | ||
| sessionState.set(sessionId, state); | ||
| } | ||
| return state; | ||
| } | ||
| function touchSession(sessionId) { | ||
| const state = getSessionState(sessionId); | ||
| if (!state) return null; | ||
| state.lastSeenAt = nowMs(); | ||
| return state; | ||
| } | ||
| function setDefaultTab(sessionId, tabId) { | ||
| const state = getSessionState(sessionId); | ||
| if (!state) return; | ||
| state.defaultTabId = tabId; | ||
| state.lastSeenAt = nowMs(); | ||
| } | ||
| function clearDefaultTab(sessionId, tabId) { | ||
| const state = sessionState.get(sessionId); | ||
| if (!state) return; | ||
| if (tabId === undefined || state.defaultTabId === tabId) { | ||
| state.defaultTabId = null; | ||
| } | ||
| state.lastSeenAt = nowMs(); | ||
| } | ||
| function releaseClaim(tabId) { | ||
| const info = claims.get(tabId); | ||
| if (!info) return; | ||
| claims.delete(tabId); | ||
| clearDefaultTab(info.sessionId, tabId); | ||
| } | ||
| function releaseClaimsForSession(sessionId) { | ||
@@ -73,2 +141,4 @@ for (const [tabId, info] of claims.entries()) { | ||
| } | ||
| clearDefaultTab(sessionId); | ||
| sessionState.delete(sessionId); | ||
| } | ||
@@ -84,5 +154,35 @@ | ||
| function setClaim(tabId, sessionId) { | ||
| claims.set(tabId, { sessionId, claimedAt: nowIso() }); | ||
| const existing = claims.get(tabId); | ||
| claims.set(tabId, { | ||
| sessionId, | ||
| claimedAt: existing ? existing.claimedAt : nowIso(), | ||
| lastSeenAt: nowMs(), | ||
| }); | ||
| } | ||
| function touchClaim(tabId, sessionId) { | ||
| const existing = claims.get(tabId); | ||
| if (existing && existing.sessionId !== sessionId) return; | ||
| if (existing) { | ||
| existing.lastSeenAt = nowMs(); | ||
| } else { | ||
| setClaim(tabId, sessionId); | ||
| } | ||
| } | ||
| function cleanupStaleClaims() { | ||
| if (!LEASE_TTL_MS) return; | ||
| const now = nowMs(); | ||
| for (const [tabId, info] of claims.entries()) { | ||
| if (now - info.lastSeenAt > LEASE_TTL_MS) { | ||
| releaseClaim(tabId); | ||
| } | ||
| } | ||
| for (const [sessionId, state] of sessionState.entries()) { | ||
| if (!sessionHasClaims(sessionId) && now - state.lastSeenAt > LEASE_TTL_MS) { | ||
| sessionState.delete(sessionId); | ||
| } | ||
| } | ||
| } | ||
| function ensureHost() { | ||
@@ -127,7 +227,30 @@ if (host && host.socket && !host.socket.destroyed) return; | ||
| if (sessionId) touchSession(sessionId); | ||
| let tabId = args.tabId; | ||
| const toolArgs = { ...args }; | ||
| if (tool === "open_tab" && toolArgs.active !== false) { | ||
| const activeTabId = await resolveActiveTab(sessionId); | ||
| const claimCheck = checkClaim(activeTabId, sessionId); | ||
| if (!claimCheck.ok) { | ||
| toolArgs.active = false; | ||
| } | ||
| } | ||
| if (wantsTab(tool)) { | ||
| if (typeof tabId !== "number") { | ||
| tabId = await resolveActiveTab(sessionId); | ||
| const state = getSessionState(sessionId); | ||
| const defaultTabId = state && Number.isFinite(state.defaultTabId) ? state.defaultTabId : null; | ||
| if (Number.isFinite(defaultTabId)) { | ||
| tabId = defaultTabId; | ||
| } else { | ||
| const activeTabId = await resolveActiveTab(sessionId); | ||
| const claimCheck = checkClaim(activeTabId, sessionId); | ||
| if (!claimCheck.ok) { | ||
| throw new Error(`${claimCheck.error}. No default tab for session; open a new tab or claim one.`); | ||
| } | ||
| tabId = activeTabId; | ||
| setDefaultTab(sessionId, tabId); | ||
| } | ||
| } | ||
@@ -139,3 +262,3 @@ | ||
| const res = await callExtension(tool, { ...args, tabId }, sessionId); | ||
| const res = await callExtension(tool, { ...toolArgs, tabId }, sessionId); | ||
@@ -145,5 +268,4 @@ const usedTabId = | ||
| if (typeof usedTabId === "number") { | ||
| // Auto-claim on first touch | ||
| const existing = claims.get(usedTabId); | ||
| if (!existing) setClaim(usedTabId, sessionId); | ||
| touchClaim(usedTabId, sessionId); | ||
| setDefaultTab(sessionId, usedTabId); | ||
| } | ||
@@ -158,2 +280,3 @@ | ||
| client.sessionId = msg.sessionId; | ||
| if (client.sessionId) touchSession(client.sessionId); | ||
| if (client.role === "native-host") { | ||
@@ -188,2 +311,3 @@ host = { socket }; | ||
| const sessionId = msg.sessionId || client.sessionId; | ||
| if (sessionId) touchSession(sessionId); | ||
@@ -197,3 +321,17 @@ const replyOk = (data) => writeJsonLine(socket, { type: "response", id: requestId, ok: true, data }); | ||
| if (msg.op === "status") { | ||
| replyOk({ broker: true, hostConnected: !!host && !!host.socket && !host.socket.destroyed, claims: listClaims() }); | ||
| const state = sessionId ? sessionState.get(sessionId) : null; | ||
| const sessionInfo = state | ||
| ? { | ||
| sessionId, | ||
| defaultTabId: state.defaultTabId, | ||
| lastSeenAt: new Date(state.lastSeenAt).toISOString(), | ||
| } | ||
| : null; | ||
| replyOk({ | ||
| broker: true, | ||
| hostConnected: !!host && !!host.socket && !host.socket.destroyed, | ||
| claims: listClaims(), | ||
| leaseTtlMs: LEASE_TTL_MS, | ||
| session: sessionInfo, | ||
| }); | ||
| return; | ||
@@ -215,3 +353,7 @@ } | ||
| } | ||
| if (existing && existing.sessionId !== sessionId && force) { | ||
| clearDefaultTab(existing.sessionId, tabId); | ||
| } | ||
| setClaim(tabId, sessionId); | ||
| setDefaultTab(sessionId, tabId); | ||
| replyOk({ ok: true, tabId, sessionId }); | ||
@@ -232,3 +374,3 @@ return; | ||
| } | ||
| claims.delete(tabId); | ||
| releaseClaim(tabId); | ||
| replyOk({ ok: true, tabId, released: true }); | ||
@@ -305,2 +447,7 @@ return; | ||
| if (LEASE_TTL_MS > 0 && LEASE_SWEEP_MS > 0) { | ||
| const timer = setInterval(cleanupStaleClaims, LEASE_SWEEP_MS); | ||
| if (typeof timer.unref === "function") timer.unref(); | ||
| } | ||
| start(); |
+3
-1
| { | ||
| "name": "@different-ai/opencode-browser", | ||
| "version": "4.4.0", | ||
| "version": "4.5.0", | ||
| "description": "Browser automation plugin for OpenCode (native messaging + per-tab ownership).", | ||
@@ -23,2 +23,4 @@ "type": "module", | ||
| "build": "bun build src/plugin.ts --target=node --outfile=dist/plugin.js", | ||
| "prepublishOnly": "bun run build", | ||
| "publish": "node -e \"const argv=(() => { try { return JSON.parse(process.env.npm_config_argv || '{}').original || []; } catch { return []; } })(); if (argv.length === 1 && argv[0] === 'publish') process.exit(0); require('child_process').execSync('npm publish --access public', { stdio: 'inherit' });\"", | ||
| "install": "node bin/cli.js install", | ||
@@ -25,0 +27,0 @@ "uninstall": "node bin/cli.js uninstall", |
+9
-4
@@ -115,4 +115,6 @@ # OpenCode Browser | ||
| - First time a session touches a tab, the broker **auto-claims** it for that session. | ||
| - Other sessions attempting to use the same tab will get an error. | ||
| - Use `browser_status` to inspect claims if needed. | ||
| - Each session tracks a default tab; tools without `tabId` route to it. | ||
| - `browser_open_tab` always works; if another session owns the active tab, the new tab opens in the background. | ||
| - Claims expire after inactivity (`OPENCODE_BROWSER_CLAIM_TTL_MS`, default 5 minutes). | ||
| - Use `browser_status` or `browser_list_claims` to inspect claims if needed. | ||
@@ -124,2 +126,5 @@ ## Available tools | ||
| - `browser_get_tabs` | ||
| - `browser_list_claims` | ||
| - `browser_claim_tab` | ||
| - `browser_release_tab` | ||
| - `browser_open_tab` | ||
@@ -162,4 +167,4 @@ - `browser_navigate` | ||
| **Tab ownership errors** | ||
| - Use `browser_status` to see current claims | ||
| - Close the other OpenCode session to release ownership | ||
| - Use `browser_status` or `browser_list_claims` to see current claims | ||
| - Use `browser_release_tab` or close the other OpenCode session to release ownership | ||
@@ -166,0 +171,0 @@ ## Uninstall |
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 19 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
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 18 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
537365
1.16%15642
1.09%175
2.94%59
1.72%