@different-ai/opencode-browser
Advanced tools
+1
-1
| { | ||
| "name": "@different-ai/opencode-browser", | ||
| "version": "2.0.2", | ||
| "version": "2.1.0", | ||
| "description": "Browser automation plugin for OpenCode. Control your real Chrome browser with existing logins and cookies.", | ||
@@ -5,0 +5,0 @@ "type": "module", |
+6
-2
@@ -44,3 +44,5 @@ # OpenCode Browser | ||
| | `browser_status` | Check if browser is available or locked | | ||
| | `browser_kill_session` | Take over from another OpenCode session | | ||
| | `browser_kill_session` | Request other session release + take over (no kill) | | ||
| | `browser_release` | Release lock and stop server | | ||
| | `browser_force_kill_session` | (Last resort) kill other OpenCode process | | ||
| | `browser_navigate` | Navigate to a URL | | ||
@@ -61,3 +63,5 @@ | `browser_click` | Click an element by CSS selector | | ||
| - `browser_status` - Check who has the lock | ||
| - `browser_kill_session` - Kill the other session and take over | ||
| - `browser_kill_session` - Request the other session to release (no kill) | ||
| - `browser_release` - Release lock/server for this session | ||
| - `browser_force_kill_session` - (Last resort) kill the other OpenCode process and take over | ||
@@ -64,0 +68,0 @@ In your prompts, you can say: |
+327
-170
| /** | ||
| * OpenCode Browser Plugin | ||
| * | ||
| * A simple plugin that provides browser automation tools. | ||
| * Connects to Chrome extension via WebSocket. | ||
| * OpenCode Plugin (this) <--WebSocket:19222--> Chrome Extension | ||
| * | ||
| * Architecture: | ||
| * OpenCode Plugin (this) <--WebSocket:19222--> Chrome Extension | ||
| * | ||
| * Lock file ensures only one OpenCode session uses browser at a time. | ||
| * Notes | ||
| * - Uses a lock file so only one OpenCode session owns the browser. | ||
| * - Supports a *soft takeover* (SIGUSR1) so we don't have to kill OpenCode. | ||
| */ | ||
@@ -15,3 +13,3 @@ | ||
| import { tool } from "@opencode-ai/plugin"; | ||
| import { existsSync, readFileSync, writeFileSync, unlinkSync, mkdirSync } from "fs"; | ||
| import { existsSync, mkdirSync, readFileSync, unlinkSync, writeFileSync } from "fs"; | ||
| import { homedir } from "os"; | ||
@@ -25,3 +23,5 @@ import { join } from "path"; | ||
| // Ensure directories exist | ||
| // If a session hasn't used the browser in this long, allow soft takeover by default. | ||
| const LOCK_TTL_MS = 2 * 60 * 60 * 1000; // 2 hours | ||
| mkdirSync(BASE_DIR, { recursive: true }); | ||
@@ -38,9 +38,3 @@ mkdirSync(SCREENSHOTS_DIR, { recursive: true }); | ||
| let requestId = 0; | ||
| let hasLock = false; | ||
| let serverFailed = false; | ||
| // ============================================================================ | ||
| // Lock File Management | ||
| // ============================================================================ | ||
| interface LockInfo { | ||
@@ -50,5 +44,10 @@ pid: number; | ||
| startedAt: string; | ||
| lastUsedAt: string; | ||
| cwd: string; | ||
| } | ||
| function nowIso(): string { | ||
| return new Date().toISOString(); | ||
| } | ||
| function readLock(): LockInfo | null { | ||
@@ -66,12 +65,38 @@ try { | ||
| LOCK_FILE, | ||
| JSON.stringify({ | ||
| pid, | ||
| sessionId, | ||
| startedAt: new Date().toISOString(), | ||
| cwd: process.cwd(), | ||
| } satisfies LockInfo) | ||
| JSON.stringify( | ||
| { | ||
| pid, | ||
| sessionId, | ||
| startedAt: nowIso(), | ||
| lastUsedAt: nowIso(), | ||
| cwd: process.cwd(), | ||
| } satisfies LockInfo, | ||
| null, | ||
| 2 | ||
| ) + "\n" | ||
| ); | ||
| hasLock = true; | ||
| } | ||
| function touchLock(): void { | ||
| const lock = readLock(); | ||
| if (!lock) return; | ||
| if (lock.sessionId !== sessionId) return; | ||
| try { | ||
| writeFileSync( | ||
| LOCK_FILE, | ||
| JSON.stringify( | ||
| { | ||
| ...lock, | ||
| lastUsedAt: nowIso(), | ||
| } satisfies LockInfo, | ||
| null, | ||
| 2 | ||
| ) + "\n" | ||
| ); | ||
| } catch { | ||
| // Ignore | ||
| } | ||
| } | ||
| function releaseLock(): void { | ||
@@ -83,4 +108,5 @@ try { | ||
| } | ||
| } catch {} | ||
| hasLock = false; | ||
| } catch { | ||
| // Ignore | ||
| } | ||
| } | ||
@@ -97,74 +123,45 @@ | ||
| function tryAcquireLock(): { success: boolean; error?: string; lock?: LockInfo } { | ||
| const existingLock = readLock(); | ||
| if (!existingLock) { | ||
| writeLock(); | ||
| return { success: true }; | ||
| } | ||
| if (existingLock.sessionId === sessionId) { | ||
| return { success: true }; | ||
| } | ||
| if (!isProcessAlive(existingLock.pid)) { | ||
| // Stale lock, take it | ||
| writeLock(); | ||
| return { success: true }; | ||
| } | ||
| return { | ||
| success: false, | ||
| error: `Browser locked by another session (PID ${existingLock.pid})`, | ||
| lock: existingLock, | ||
| }; | ||
| function lockAgeMs(lock: LockInfo): number { | ||
| const ts = lock.lastUsedAt || lock.startedAt; | ||
| const n = Date.parse(ts); | ||
| if (Number.isNaN(n)) return Number.POSITIVE_INFINITY; | ||
| return Date.now() - n; | ||
| } | ||
| function sleep(ms: number): Promise<void> { | ||
| return new Promise((resolve) => setTimeout(resolve, ms)); | ||
| function isLockExpired(lock: LockInfo): boolean { | ||
| return lockAgeMs(lock) > LOCK_TTL_MS; | ||
| } | ||
| async function killSession(targetPid: number): Promise<{ success: boolean; error?: string }> { | ||
| function isPortFree(port: number): boolean { | ||
| try { | ||
| process.kill(targetPid, "SIGTERM"); | ||
| // Wait for process to die | ||
| let attempts = 0; | ||
| while (isProcessAlive(targetPid) && attempts < 10) { | ||
| await sleep(100); | ||
| attempts++; | ||
| } | ||
| if (isProcessAlive(targetPid)) { | ||
| process.kill(targetPid, "SIGKILL"); | ||
| } | ||
| // Remove lock and acquire | ||
| try { unlinkSync(LOCK_FILE); } catch {} | ||
| writeLock(); | ||
| return { success: true }; | ||
| // If we can connect, something is already listening. | ||
| const testSocket = Bun.connect({ port, timeout: 300 }); | ||
| testSocket.end(); | ||
| return false; | ||
| } catch (e) { | ||
| return { success: false, error: e instanceof Error ? e.message : String(e) }; | ||
| if ((e as any).code === "ECONNREFUSED") return true; | ||
| return false; | ||
| } | ||
| } | ||
| // ============================================================================ | ||
| // WebSocket Server | ||
| // ============================================================================ | ||
| function stopBrowserServer(): void { | ||
| try { | ||
| (ws as any)?.close?.(); | ||
| } catch { | ||
| // Ignore | ||
| } | ||
| ws = null; | ||
| isConnected = false; | ||
| function checkPortAvailable(): boolean { | ||
| try { | ||
| const testSocket = Bun.connect({ port: WS_PORT, timeout: 1000 }); | ||
| testSocket.end(); | ||
| return true; | ||
| } catch (e) { | ||
| if ((e as any).code === "ECONNREFUSED") { | ||
| return false; | ||
| } | ||
| return true; | ||
| server?.stop(); | ||
| } catch { | ||
| // Ignore | ||
| } | ||
| server = null; | ||
| } | ||
| function startServer(): boolean { | ||
| if (server) { | ||
| console.error(`[browser-plugin] Server already running`); | ||
| return true; | ||
| } | ||
| if (server) return true; | ||
| if (!isPortFree(WS_PORT)) return false; | ||
@@ -189,3 +186,3 @@ try { | ||
| }, | ||
| message(wsClient, data) { | ||
| message(_wsClient, data) { | ||
| try { | ||
@@ -200,4 +197,4 @@ const message = JSON.parse(data.toString()); | ||
| }); | ||
| console.error(`[browser-plugin] WebSocket server listening on port ${WS_PORT}`); | ||
| serverFailed = false; | ||
| return true; | ||
@@ -210,15 +207,70 @@ } catch (e) { | ||
| function sleep(ms: number): Promise<void> { | ||
| return new Promise((resolve) => setTimeout(resolve, ms)); | ||
| } | ||
| async function waitForExtensionConnection(timeoutMs: number): Promise<boolean> { | ||
| const start = Date.now(); | ||
| while (Date.now() - start < timeoutMs) { | ||
| if (isConnected) return true; | ||
| await sleep(100); | ||
| } | ||
| return isConnected; | ||
| } | ||
| async function requestSessionRelease(targetPid: number, opts?: { timeoutMs?: number }): Promise<{ success: boolean; error?: string }> { | ||
| const timeoutMs = opts?.timeoutMs ?? 3000; | ||
| try { | ||
| // SIGUSR1 is treated as "release browser lock + stop server". | ||
| // This does NOT terminate OpenCode. | ||
| process.kill(targetPid, "SIGUSR1"); | ||
| } catch (e) { | ||
| return { success: false, error: e instanceof Error ? e.message : String(e) }; | ||
| } | ||
| const start = Date.now(); | ||
| while (Date.now() - start < timeoutMs) { | ||
| const lock = readLock(); | ||
| const lockCleared = !lock || lock.pid !== targetPid; | ||
| const portCleared = isPortFree(WS_PORT); | ||
| if (lockCleared && portCleared) return { success: true }; | ||
| await sleep(100); | ||
| } | ||
| return { | ||
| success: false, | ||
| error: `Timed out waiting for PID ${targetPid} to release browser`, | ||
| }; | ||
| } | ||
| async function forceKillSession(targetPid: number): Promise<{ success: boolean; error?: string }> { | ||
| try { | ||
| process.kill(targetPid, "SIGTERM"); | ||
| let attempts = 0; | ||
| while (isProcessAlive(targetPid) && attempts < 20) { | ||
| await sleep(100); | ||
| attempts++; | ||
| } | ||
| if (isProcessAlive(targetPid)) { | ||
| process.kill(targetPid, "SIGKILL"); | ||
| } | ||
| return { success: true }; | ||
| } catch (e) { | ||
| return { success: false, error: e instanceof Error ? e.message : String(e) }; | ||
| } | ||
| } | ||
| function handleMessage(message: { type: string; id?: number; result?: any; error?: any }): void { | ||
| if (message.type === "tool_response" && message.id !== undefined) { | ||
| const pending = pendingRequests.get(message.id); | ||
| if (pending) { | ||
| pendingRequests.delete(message.id); | ||
| if (message.error) { | ||
| pending.reject(new Error(message.error.content || String(message.error))); | ||
| } else { | ||
| pending.resolve(message.result?.content); | ||
| } | ||
| if (!pending) return; | ||
| pendingRequests.delete(message.id); | ||
| if (message.error) { | ||
| pending.reject(new Error(message.error.content || String(message.error))); | ||
| } else { | ||
| pending.resolve(message.result?.content); | ||
| } | ||
| } else if (message.type === "pong") { | ||
| // Heartbeat response, ignore | ||
| } | ||
@@ -235,13 +287,83 @@ } | ||
| async function executeCommand(tool: string, args: Record<string, any>): Promise<any> { | ||
| // Check lock and start server if needed | ||
| const lockResult = tryAcquireLock(); | ||
| if (!lockResult.success) { | ||
| throw new Error( | ||
| `${lockResult.error}. Use browser_kill_session to take over, or browser_status to see details.` | ||
| async function performTakeover(): Promise<string> { | ||
| const lock = readLock(); | ||
| if (!lock) { | ||
| writeLock(); | ||
| } else if (lock.sessionId === sessionId) { | ||
| // Already ours. | ||
| } else if (!isProcessAlive(lock.pid)) { | ||
| // Dead PID -> stale. | ||
| console.error(`[browser-plugin] Cleaning stale lock from dead PID ${lock.pid}`); | ||
| writeLock(); | ||
| } else { | ||
| const ageMinutes = Math.round(lockAgeMs(lock) / 60000); | ||
| console.error( | ||
| `[browser-plugin] Requesting release from PID ${lock.pid} (last used ${ageMinutes}m ago)...` | ||
| ); | ||
| const released = await requestSessionRelease(lock.pid, { timeoutMs: 4000 }); | ||
| if (!released.success) { | ||
| throw new Error( | ||
| `Failed to takeover without killing OpenCode: ${released.error}. ` + | ||
| `Try again, or use browser_force_kill_session as last resort.` | ||
| ); | ||
| } | ||
| console.error(`[browser-plugin] Previous session released gracefully.`); | ||
| writeLock(); | ||
| } | ||
| touchLock(); | ||
| if (!server) { | ||
| if (!startServer()) { | ||
| throw new Error("Failed to start WebSocket server after takeover."); | ||
| } | ||
| } | ||
| const ok = await waitForExtensionConnection(3000); | ||
| if (!ok) { | ||
| throw new Error("Took over lock but Chrome extension did not connect."); | ||
| } | ||
| return "Browser now connected to this session."; | ||
| } | ||
| async function ensureLockAndServer(): Promise<void> { | ||
| const existingLock = readLock(); | ||
| if (!existingLock) { | ||
| writeLock(); | ||
| } else if (existingLock.sessionId === sessionId) { | ||
| // Already ours. | ||
| } else if (!isProcessAlive(existingLock.pid)) { | ||
| // Stale lock (dead PID). | ||
| console.error(`[browser-plugin] Cleaning stale lock from dead PID ${existingLock.pid}`); | ||
| writeLock(); | ||
| } else { | ||
| // Another session holds the lock - attempt automatic soft takeover | ||
| const ageMinutes = Math.round(lockAgeMs(existingLock) / 60000); | ||
| console.error( | ||
| `[browser-plugin] Browser locked by PID ${existingLock.pid} (last used ${ageMinutes}m ago). Attempting auto-takeover...` | ||
| ); | ||
| const released = await requestSessionRelease(existingLock.pid, { timeoutMs: 4000 }); | ||
| if (released.success) { | ||
| console.error(`[browser-plugin] Auto-takeover succeeded. Previous session released gracefully.`); | ||
| writeLock(); | ||
| } else { | ||
| // Soft takeover failed - provide helpful error | ||
| const expired = isLockExpired(existingLock); | ||
| const why = expired ? "expired" : "active"; | ||
| throw new Error( | ||
| `Browser locked by another session (PID ${existingLock.pid}, ${why}). ` + | ||
| `Auto-takeover failed: ${released.error}. ` + | ||
| `Use browser_force_kill_session as last resort, or browser_status for details.` | ||
| ); | ||
| } | ||
| } | ||
| touchLock(); | ||
| if (!server) { | ||
| if (!startServer()) { | ||
| throw new Error("Failed to start WebSocket server. Port may be in use."); | ||
@@ -252,8 +374,16 @@ } | ||
| if (!isConnected) { | ||
| throw new Error( | ||
| "Chrome extension not connected. Make sure Chrome is running with the OpenCode Browser extension enabled." | ||
| ); | ||
| const ok = await waitForExtensionConnection(3000); | ||
| if (!ok) { | ||
| throw new Error( | ||
| "Chrome extension not connected. Make sure Chrome is running with the OpenCode Browser extension enabled." | ||
| ); | ||
| } | ||
| } | ||
| } | ||
| async function executeCommand(toolName: string, args: Record<string, any>): Promise<any> { | ||
| await ensureLockAndServer(); | ||
| const id = ++requestId; | ||
| touchLock(); | ||
@@ -266,12 +396,10 @@ return new Promise((resolve, reject) => { | ||
| id, | ||
| tool, | ||
| tool: toolName, | ||
| args, | ||
| }); | ||
| // Timeout after 60 seconds | ||
| setTimeout(() => { | ||
| if (pendingRequests.has(id)) { | ||
| pendingRequests.delete(id); | ||
| reject(new Error("Tool execution timed out after 60 seconds")); | ||
| } | ||
| if (!pendingRequests.has(id)) return; | ||
| pendingRequests.delete(id); | ||
| reject(new Error("Tool execution timed out after 60 seconds")); | ||
| }, 60000); | ||
@@ -282,8 +410,15 @@ }); | ||
| // ============================================================================ | ||
| // Cleanup on exit | ||
| // Cleanup / Signals | ||
| // ============================================================================ | ||
| // Soft release: do NOT exit the OpenCode process. | ||
| process.on("SIGUSR1", () => { | ||
| console.error(`[browser-plugin] SIGUSR1: releasing lock + stopping server`); | ||
| releaseLock(); | ||
| stopBrowserServer(); | ||
| }); | ||
| process.on("SIGTERM", () => { | ||
| releaseLock(); | ||
| server?.stop(); | ||
| stopBrowserServer(); | ||
| process.exit(0); | ||
@@ -294,3 +429,3 @@ }); | ||
| releaseLock(); | ||
| server?.stop(); | ||
| stopBrowserServer(); | ||
| process.exit(0); | ||
@@ -307,34 +442,5 @@ }); | ||
| export const BrowserPlugin: Plugin = async (ctx) => { | ||
| export const BrowserPlugin: Plugin = async (_ctx) => { | ||
| console.error(`[browser-plugin] Initializing (session ${sessionId})`); | ||
| // Check port availability on load, don't try to acquire lock yet | ||
| checkPortAvailable(); | ||
| // Check lock status and set appropriate state | ||
| const lock = readLock(); | ||
| if (!lock) { | ||
| // No lock - just check if we can start server | ||
| console.error(`[browser-plugin] No lock file, checking port...`); | ||
| if (!startServer()) { | ||
| serverFailed = true; | ||
| } | ||
| } else if (lock.sessionId === sessionId) { | ||
| // We own the lock - start server | ||
| console.error(`[browser-plugin] Already have lock, starting server...`); | ||
| if (!startServer()) { | ||
| serverFailed = true; | ||
| } | ||
| } else if (!isProcessAlive(lock.pid)) { | ||
| // Stale lock - take it and start server | ||
| console.error(`[browser-plugin] Stale lock from dead PID ${lock.pid}, taking over...`); | ||
| writeLock(); | ||
| if (!startServer()) { | ||
| serverFailed = true; | ||
| } | ||
| } else { | ||
| // Another session has the lock | ||
| console.error(`[browser-plugin] Lock held by PID ${lock.pid}, tools will fail until lock is released`); | ||
| } | ||
| return { | ||
@@ -354,29 +460,73 @@ tool: { | ||
| if (lock.sessionId === sessionId) { | ||
| return `Browser connected (this session)\nPID: ${pid}\nStarted: ${lock.startedAt}\nExtension: ${isConnected ? "connected" : "not connected"}`; | ||
| return ( | ||
| `Browser connected (this session)\n` + | ||
| `PID: ${pid}\n` + | ||
| `Started: ${lock.startedAt}\n` + | ||
| `Last used: ${lock.lastUsedAt}\n` + | ||
| `Extension: ${isConnected ? "connected" : "not connected"}` | ||
| ); | ||
| } | ||
| if (!isProcessAlive(lock.pid)) { | ||
| return `Browser available (stale lock from dead PID ${lock.pid} will be auto-cleaned)`; | ||
| const alive = isProcessAlive(lock.pid); | ||
| const ageMinutes = Math.round(lockAgeMs(lock) / 60000); | ||
| const expired = isLockExpired(lock); | ||
| if (!alive) { | ||
| return `Browser available (stale lock from dead PID ${lock.pid} will be auto-cleaned on next command)`; | ||
| } | ||
| return `Browser locked by another session\nPID: ${lock.pid}\nSession: ${lock.sessionId}\nStarted: ${lock.startedAt}\nWorking directory: ${lock.cwd}\n\nUse browser_kill_session to take over.`; | ||
| return ( | ||
| `Browser locked by another session\n` + | ||
| `PID: ${lock.pid}\n` + | ||
| `Session: ${lock.sessionId}\n` + | ||
| `Started: ${lock.startedAt}\n` + | ||
| `Last used: ${lock.lastUsedAt} (~${ageMinutes}m ago)${expired ? " [expired]" : ""}\n` + | ||
| `Working directory: ${lock.cwd}\n\n` + | ||
| `Use browser_takeover to request release (no kill), or browser_force_kill_session as last resort.` | ||
| ); | ||
| }, | ||
| }), | ||
| browser_release: tool({ | ||
| description: "Release browser lock and stop the server for this session.", | ||
| args: {}, | ||
| async execute() { | ||
| const lock = readLock(); | ||
| if (lock && lock.sessionId !== sessionId) { | ||
| throw new Error("This session does not own the browser lock."); | ||
| } | ||
| releaseLock(); | ||
| stopBrowserServer(); | ||
| return "Released browser lock for this session."; | ||
| }, | ||
| }), | ||
| browser_takeover: tool({ | ||
| description: | ||
| "Request the session holding the browser lock to release it (no process kill), then take over.", | ||
| args: {}, | ||
| async execute() { | ||
| return await performTakeover(); | ||
| }, | ||
| }), | ||
| browser_kill_session: tool({ | ||
| description: | ||
| "Kill the session that currently holds the browser lock and take over. Use when browser_status shows another session has the lock.", | ||
| "(Deprecated name) Soft takeover without killing OpenCode. Prefer browser_takeover.", | ||
| args: {}, | ||
| async execute() { | ||
| // Keep backward compatibility: old callers use this. | ||
| return await performTakeover(); | ||
| }, | ||
| }), | ||
| browser_force_kill_session: tool({ | ||
| description: "Force kill the session holding the browser lock (last resort).", | ||
| args: {}, | ||
| async execute() { | ||
| const lock = readLock(); | ||
| if (!lock) { | ||
| // No lock, just acquire | ||
| writeLock(); | ||
| // Start server if needed | ||
| if (!server) { | ||
| if (!startServer()) { | ||
| throw new Error("Failed to start WebSocket server after acquiring lock."); | ||
| } | ||
| } | ||
| return "No active session. Browser now connected to this session."; | ||
@@ -390,25 +540,32 @@ } | ||
| if (!isProcessAlive(lock.pid)) { | ||
| // Stale lock | ||
| writeLock(); | ||
| // Start server if needed | ||
| if (!server) { | ||
| if (!startServer()) { | ||
| throw new Error("Failed to start WebSocket server after cleaning stale lock."); | ||
| } | ||
| } | ||
| return `Cleaned stale lock (PID ${lock.pid} was dead). Browser now connected to this session.`; | ||
| } | ||
| // Kill other session and wait for port to be free | ||
| const result = await killSession(lock.pid); | ||
| if (result.success) { | ||
| if (!server) { | ||
| if (!startServer()) { | ||
| throw new Error("Failed to start WebSocket server after killing other session."); | ||
| } | ||
| const result = await forceKillSession(lock.pid); | ||
| if (!result.success) { | ||
| throw new Error(`Failed to force kill session: ${result.error}`); | ||
| } | ||
| // Best-effort cleanup; then take lock. | ||
| try { | ||
| unlinkSync(LOCK_FILE); | ||
| } catch { | ||
| // Ignore | ||
| } | ||
| writeLock(); | ||
| if (!server) { | ||
| if (!startServer()) { | ||
| throw new Error("Failed to start WebSocket server after force kill."); | ||
| } | ||
| return `Killed session ${lock.sessionId} (PID ${lock.pid}). Browser now connected to this session.`; | ||
| } else { | ||
| throw new Error(`Failed to kill session: ${result.error}`); | ||
| } | ||
| const ok = await waitForExtensionConnection(3000); | ||
| if (!ok) { | ||
| throw new Error("Force-killed lock holder but Chrome extension did not connect."); | ||
| } | ||
| return `Force-killed session ${lock.sessionId} (PID ${lock.pid}). Browser now connected to this session.`; | ||
| }, | ||
@@ -460,6 +617,6 @@ }), | ||
| }, | ||
| async execute(args) { | ||
| async execute(args: { tabId?: number; name?: string }) { | ||
| const result = await executeCommand("screenshot", args); | ||
| if (result && result.startsWith("data:image")) { | ||
| if (result && typeof result === "string" && result.startsWith("data:image")) { | ||
| const base64Data = result.replace(/^data:image\/\w+;base64,/, ""); | ||
@@ -466,0 +623,0 @@ const timestamp = new Date().toISOString().replace(/[:.]/g, "-"); |
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
Uses eval
Supply chain riskPackage uses dynamic code execution (e.g., eval()), which is a dangerous practice. This can prevent the code from running in certain environments and increases the risk that the code may contain exploits or malicious behavior.
Found 1 instance 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
Uses eval
Supply chain riskPackage uses dynamic code execution (e.g., eval()), which is a dangerous practice. This can prevent the code from running in certain environments and increases the risk that the code may contain exploits or malicious behavior.
Found 1 instance 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
46136
10.69%1142
12.73%140
2.94%