diff --git a/bin/agent-browser b/bin/agent-browser index 96a04c1..e9239fe 100755 --- a/bin/agent-browser +++ b/bin/agent-browser @@ -1,4 +1,7 @@ #!/bin/sh +# agent-browser CLI wrapper +# Detects OS/arch and runs the appropriate native binary + SCRIPT="$0" while [ -L "$SCRIPT" ]; do SCRIPT_DIR="$(cd "$(dirname "$SCRIPT")" && pwd)" @@ -6,10 +9,18 @@ while [ -L "$SCRIPT" ]; do case "$SCRIPT" in /*) ;; *) SCRIPT="$SCRIPT_DIR/$SCRIPT" ;; esac done SCRIPT_DIR="$(cd "$(dirname "$SCRIPT")" && pwd)" + OS=$(uname -s | tr '[:upper:]' '[:lower:]') ARCH=$(uname -m) case "$OS" in darwin) OS="darwin" ;; linux) OS="linux" ;; mingw*|msys*|cygwin*) OS="win32" ;; esac case "$ARCH" in x86_64|amd64) ARCH="x64" ;; aarch64|arm64) ARCH="arm64" ;; esac + BINARY="$SCRIPT_DIR/agent-browser-${OS}-${ARCH}" -[ -f "$BINARY" ] && [ -x "$BINARY" ] && exec "$BINARY" "$@" -exec node "$SCRIPT_DIR/../dist/index.js" "$@" + +if [ -f "$BINARY" ] && [ -x "$BINARY" ]; then + exec "$BINARY" "$@" +fi + +echo "Error: No binary found for ${OS}-${ARCH}" >&2 +echo "Run 'npm run build:native' to build for your platform" >&2 +exit 1 diff --git a/package.json b/package.json index 189c448..d078f1c 100644 --- a/package.json +++ b/package.json @@ -3,7 +3,7 @@ "version": "0.3.7", "description": "Headless browser automation CLI for AI agents", "type": "module", - "main": "dist/index.js", + "main": "dist/daemon.js", "files": [ "dist", "bin", diff --git a/src/cli-light.ts b/src/cli-light.ts deleted file mode 100644 index 925b848..0000000 --- a/src/cli-light.ts +++ /dev/null @@ -1,457 +0,0 @@ -#!/usr/bin/env node -/** - * Lightweight CLI client for agent-browser - * - * This file contains ONLY the client logic (no Playwright imports). - * It can be compiled with Bun for fast startup times. - * - * The actual browser automation runs in a separate daemon process. - */ - -import * as net from 'net'; -import * as fs from 'fs'; -import * as os from 'os'; -import * as path from 'path'; -import { spawn } from 'child_process'; - -// ============================================================================ -// Configuration -// ============================================================================ - -const SESSION = process.env.AGENT_BROWSER_SESSION || 'default'; -const SOCKET_PATH = path.join(os.tmpdir(), `agent-browser-${SESSION}.sock`); -const PID_FILE = path.join(os.tmpdir(), `agent-browser-${SESSION}.pid`); - -// ============================================================================ -// Daemon Management -// ============================================================================ - -function isDaemonRunning(): boolean { - if (!fs.existsSync(PID_FILE)) return false; - try { - const pid = parseInt(fs.readFileSync(PID_FILE, 'utf8').trim(), 10); - process.kill(pid, 0); - return true; - } catch { - return false; - } -} - -async function ensureDaemon(): Promise { - if (isDaemonRunning() && fs.existsSync(SOCKET_PATH)) { - return; - } - - // Find the daemon script - look relative to this script - const scriptDir = path.dirname(process.argv[1]); - let daemonPath = path.join(scriptDir, 'daemon.js'); - - // Fallback paths - if (!fs.existsSync(daemonPath)) { - daemonPath = path.join(scriptDir, '../dist/daemon.js'); - } - if (!fs.existsSync(daemonPath)) { - daemonPath = path.join(process.cwd(), 'dist/daemon.js'); - } - - if (!fs.existsSync(daemonPath)) { - throw new Error(`Daemon not found. Looked in: ${daemonPath}`); - } - - const child = spawn('node', [daemonPath], { - detached: true, - stdio: 'ignore', - env: { ...process.env, AGENT_BROWSER_DAEMON: '1', AGENT_BROWSER_SESSION: SESSION }, - }); - child.unref(); - - // Wait for socket - for (let i = 0; i < 50; i++) { - if (fs.existsSync(SOCKET_PATH)) return; - await new Promise(r => setTimeout(r, 100)); - } - throw new Error('Failed to start daemon'); -} - -// ============================================================================ -// Command Execution -// ============================================================================ - -interface Response { - id: string; - success: boolean; - data?: unknown; - error?: string; -} - -async function sendCommand(cmd: Record): Promise { - return new Promise((resolve, reject) => { - let buffer = ''; - let resolved = false; - const socket = net.createConnection(SOCKET_PATH); - - const cleanup = () => { - socket.removeAllListeners(); - socket.destroy(); - }; - - socket.on('connect', () => { - socket.write(JSON.stringify(cmd) + '\n'); - }); - - socket.on('data', (data) => { - buffer += data.toString(); - const idx = buffer.indexOf('\n'); - if (idx !== -1 && !resolved) { - resolved = true; - try { - const response = JSON.parse(buffer.substring(0, idx)) as Response; - cleanup(); - resolve(response); - } catch { - cleanup(); - reject(new Error('Invalid JSON response')); - } - } - }); - - socket.on('error', (err) => { - if (!resolved) { - resolved = true; - cleanup(); - reject(err); - } - }); - - socket.on('close', () => { - if (!resolved && buffer.trim()) { - resolved = true; - try { - resolve(JSON.parse(buffer.trim()) as Response); - } catch { - reject(new Error('Connection closed')); - } - } - }); - - setTimeout(() => { - if (!resolved) { - resolved = true; - cleanup(); - reject(new Error('Timeout')); - } - }, 30000); - }); -} - -// ============================================================================ -// Command Parsing -// ============================================================================ - -function parseCommand(parts: string[]): Record | null { - if (parts.length === 0) return null; - - const command = parts[0]; - const rest = parts.slice(1); - const id = Math.random().toString(36).slice(2, 10); - - switch (command) { - case 'open': - case 'goto': - case 'navigate': - return { id, action: 'navigate', url: rest[0]?.startsWith('http') ? rest[0] : `https://${rest[0]}` }; - - case 'click': - return { id, action: 'click', selector: rest[0] }; - - case 'fill': - return { id, action: 'fill', selector: rest[0], value: rest.slice(1).join(' ') }; - - case 'type': - return { id, action: 'type', selector: rest[0], text: rest.slice(1).join(' ') }; - - case 'hover': - return { id, action: 'hover', selector: rest[0] }; - - case 'snapshot': { - const opts: Record = { id, action: 'snapshot' }; - // Parse snapshot options from rest args - for (let i = 0; i < rest.length; i++) { - const arg = rest[i]; - if (arg === '-i' || arg === '--interactive') { - opts.interactive = true; - } else if (arg === '-c' || arg === '--compact') { - opts.compact = true; - } else if (arg === '--depth' || arg === '-d') { - opts.maxDepth = parseInt(rest[++i], 10); - } else if (arg === '--selector' || arg === '-s') { - opts.selector = rest[++i]; - } - } - return opts; - } - - case 'screenshot': - return { id, action: 'screenshot', path: rest[0] }; - - case 'close': - case 'quit': - return { id, action: 'close' }; - - case 'get': - if (rest[0] === 'text') return { id, action: 'gettext', selector: rest[1] }; - if (rest[0] === 'url') return { id, action: 'url' }; - if (rest[0] === 'title') return { id, action: 'title' }; - return null; - - case 'press': - return { id, action: 'press', key: rest[0] }; - - case 'wait': - if (/^\d+$/.test(rest[0])) { - return { id, action: 'wait', timeout: parseInt(rest[0], 10) }; - } - return { id, action: 'wait', selector: rest[0] }; - - case 'back': - return { id, action: 'back' }; - - case 'forward': - return { id, action: 'forward' }; - - case 'reload': - return { id, action: 'reload' }; - - case 'eval': - return { id, action: 'evaluate', script: rest.join(' ') }; - - default: - return null; - } -} - -function parseBatchCommands(args: string[]): Record[] { - const commands: Record[] = []; - - // Each argument after 'batch' is a command string - for (const arg of args) { - // Split the command string into parts - const parts = arg.match(/(?:[^\s"]+|"[^"]*")+/g) || []; - const cleanParts = parts.map(p => p.replace(/^"|"$/g, '')); - - const cmd = parseCommand(cleanParts); - if (cmd) { - commands.push(cmd); - } - } - - return commands; -} - -// ============================================================================ -// Output Formatting -// ============================================================================ - -function formatResponse(response: Response): string { - if (!response.success) { - return `\x1b[31m✗ Error:\x1b[0m ${response.error}`; - } - - const data = response.data as Record; - - if (data?.url && data?.title) { - return `\x1b[32m✓\x1b[0m \x1b[1m${data.title}\x1b[0m\n\x1b[2m ${data.url}\x1b[0m`; - } else if (data?.snapshot) { - return String(data.snapshot); - } else if (data?.text !== undefined) { - return String(data.text); - } else if (data?.url) { - return String(data.url); - } else if (data?.title) { - return String(data.title); - } else if (data?.result !== undefined) { - return typeof data.result === 'object' ? JSON.stringify(data.result, null, 2) : String(data.result); - } else if (data?.closed) { - return '\x1b[32m✓\x1b[0m Browser closed'; - } else { - return '\x1b[32m✓\x1b[0m Done'; - } -} - -function printResponse(response: Response, json: boolean): void { - if (json) { - console.log(JSON.stringify(response)); - } else { - console.log(formatResponse(response)); - } -} - -// ============================================================================ -// Main -// ============================================================================ - -const HELP = ` -agent-browser - fast browser automation CLI - -Usage: - agent-browser [args] [--json] - agent-browser batch ... [--json] - -Commands: - open Navigate to URL - click Click element (use @ref from snapshot) - fill Fill input - type Type text - hover Hover element - snapshot [options] Get accessibility tree with refs - screenshot [path] Take screenshot - get text Get text content - get url Get current URL - get title Get page title - press Press keyboard key - wait Wait for time or element - eval Evaluate JavaScript - close Close browser - -Snapshot Options: - -i, --interactive Only show interactive elements (buttons, links, inputs) - -c, --compact Remove empty structural elements - -d, --depth Limit tree depth (e.g., --depth 3) - -s, --selector Scope snapshot to CSS selector - -Batch Mode: - batch ... Execute multiple commands in sequence - Each command is a quoted string - -Options: - --json Output JSON (for AI agents) - -Examples: - agent-browser open example.com - agent-browser snapshot - agent-browser click @e2 - agent-browser fill @e3 "hello" - - # Batch mode - execute multiple commands efficiently - agent-browser batch "open example.com" "snapshot" "click a" - agent-browser batch "open google.com" "snapshot" "get title" --json -`; - -async function runBatch(commands: Record[], json: boolean): Promise { - const results: Response[] = []; - let hasError = false; - - for (const cmd of commands) { - try { - const response = await sendCommand(cmd); - results.push(response); - - if (!json) { - // Print each result as we go for non-JSON mode - console.log(`\x1b[36m[${cmd.action}]\x1b[0m`); - console.log(formatResponse(response)); - console.log(); - } - - if (!response.success) { - hasError = true; - break; // Stop on first error - } - } catch (err) { - const message = err instanceof Error ? err.message : String(err); - const errorResponse: Response = { - id: String(cmd.id), - success: false, - error: message, - }; - results.push(errorResponse); - hasError = true; - - if (!json) { - console.log(`\x1b[36m[${cmd.action}]\x1b[0m`); - console.log(`\x1b[31m✗ Error:\x1b[0m ${message}`); - } - break; - } - } - - if (json) { - console.log(JSON.stringify({ - success: !hasError, - results, - completed: results.length, - total: commands.length, - })); - } else { - console.log(`\x1b[2m─────────────────────────────────────\x1b[0m`); - console.log(`Completed ${results.length}/${commands.length} commands`); - } - - process.exit(hasError ? 1 : 0); -} - -async function main(): Promise { - const args = process.argv.slice(2); - const json = args.includes('--json'); - const cleanArgs = args.filter(a => !a.startsWith('--')); - - if (cleanArgs.length === 0 || args.includes('--help') || args.includes('-h')) { - console.log(HELP); - process.exit(0); - } - - // Check for batch mode - if (cleanArgs[0] === 'batch') { - const batchArgs = cleanArgs.slice(1); - if (batchArgs.length === 0) { - console.error('\x1b[31mBatch mode requires at least one command\x1b[0m'); - console.log('\nExample: agent-browser batch "open example.com" "snapshot"'); - process.exit(1); - } - - const commands = parseBatchCommands(batchArgs); - if (commands.length === 0) { - console.error('\x1b[31mNo valid commands found\x1b[0m'); - process.exit(1); - } - - try { - await ensureDaemon(); - await runBatch(commands, json); - } catch (err) { - const message = err instanceof Error ? err.message : String(err); - if (json) { - console.log(JSON.stringify({ success: false, error: message })); - } else { - console.error('\x1b[31m✗ Error:\x1b[0m', message); - } - process.exit(1); - } - return; - } - - // Single command mode - const cmd = parseCommand(cleanArgs); - - if (!cmd) { - console.error('\x1b[31mUnknown command:\x1b[0m', cleanArgs[0]); - process.exit(1); - } - - try { - await ensureDaemon(); - const response = await sendCommand(cmd); - printResponse(response, json); - process.exit(response.success ? 0 : 1); - } catch (err) { - const message = err instanceof Error ? err.message : String(err); - if (json) { - console.log(JSON.stringify({ success: false, error: message })); - } else { - console.error('\x1b[31m✗ Error:\x1b[0m', message); - } - process.exit(1); - } -} - -main(); diff --git a/src/client.ts b/src/client.ts deleted file mode 100644 index 7028bf8..0000000 --- a/src/client.ts +++ /dev/null @@ -1,150 +0,0 @@ -import * as net from 'net'; -import { spawn } from 'child_process'; -import { fileURLToPath } from 'url'; -import * as path from 'path'; -import * as fs from 'fs'; -import { getSocketPath, isDaemonRunning, setSession, getSession } from './daemon.js'; -import type { Response } from './types.js'; - -const __dirname = path.dirname(fileURLToPath(import.meta.url)); - -let DEBUG = false; - -export function setDebug(enabled: boolean): void { - DEBUG = enabled; -} - -export { setSession, getSession }; - -function debug(...args: unknown[]): void { - if (DEBUG) { - console.error('[debug]', ...args); - } -} - -/** - * Wait for socket to exist - */ -async function waitForSocket(maxAttempts = 30): Promise { - const socketPath = getSocketPath(); - debug('Waiting for socket at', socketPath); - for (let i = 0; i < maxAttempts; i++) { - if (fs.existsSync(socketPath)) { - debug('Socket found after', i * 100, 'ms'); - return true; - } - await new Promise((r) => setTimeout(r, 100)); - } - debug('Socket not found after', maxAttempts * 100, 'ms'); - return false; -} - -/** - * Ensure daemon is running, start if not - */ -export async function ensureDaemon(): Promise { - const session = getSession(); - debug(`Checking if daemon is running for session "${session}"...`); - if (isDaemonRunning()) { - debug('Daemon already running'); - return; - } - - debug('Starting daemon...'); - const daemonPath = path.join(__dirname, 'daemon.js'); - const child = spawn(process.execPath, [daemonPath], { - detached: true, - stdio: 'ignore', - env: { ...process.env, AGENT_BROWSER_DAEMON: '1', AGENT_BROWSER_SESSION: session }, - }); - child.unref(); - - // Wait for socket to be created - const ready = await waitForSocket(); - if (!ready) { - throw new Error('Failed to start daemon'); - } - - debug(`Daemon started for session "${session}"`); -} - -/** - * Send a command to the daemon - */ -export async function sendCommand(command: Record): Promise { - const socketPath = getSocketPath(); - debug('Sending command:', JSON.stringify(command)); - - return new Promise((resolve, reject) => { - let resolved = false; - let buffer = ''; - const startTime = Date.now(); - - const socket = net.createConnection(socketPath); - - socket.on('connect', () => { - debug('Connected to daemon, sending command...'); - socket.write(JSON.stringify(command) + '\n'); - }); - - socket.on('data', (data) => { - buffer += data.toString(); - debug('Received data:', buffer.length, 'bytes'); - - // Try to parse complete JSON from buffer - const newlineIdx = buffer.indexOf('\n'); - if (newlineIdx !== -1) { - const jsonStr = buffer.substring(0, newlineIdx); - try { - const response = JSON.parse(jsonStr) as Response; - debug('Response received in', Date.now() - startTime, 'ms'); - resolved = true; - socket.end(); - resolve(response); - } catch (e) { - debug('JSON parse error:', e); - } - } - }); - - socket.on('error', (err) => { - debug('Socket error:', err.message); - if (!resolved) { - reject(new Error(`Connection error: ${err.message}`)); - } - }); - - socket.on('close', () => { - debug('Socket closed, resolved:', resolved, 'buffer:', buffer.length); - if (!resolved && buffer.trim()) { - try { - const response = JSON.parse(buffer.trim()) as Response; - resolve(response); - } catch { - reject(new Error('Invalid response from daemon')); - } - } else if (!resolved) { - reject(new Error('Connection closed without response')); - } - }); - - // Timeout after 15 seconds (allows for 10s Playwright timeout + overhead) - setTimeout(() => { - if (!resolved) { - debug('Command timeout after 15s'); - socket.destroy(); - reject(new Error('Command timeout')); - } - }, 15000); - }); -} - -/** - * Send a command, ensuring daemon is running first - */ -export async function send(command: Record): Promise { - const startTime = Date.now(); - await ensureDaemon(); - debug('ensureDaemon took', Date.now() - startTime, 'ms'); - return sendCommand(command); -} diff --git a/src/index.ts b/src/index.ts deleted file mode 100644 index c1b06ce..0000000 --- a/src/index.ts +++ /dev/null @@ -1,1185 +0,0 @@ -#!/usr/bin/env node -import * as fs from 'fs'; -import * as os from 'os'; -import * as path from 'path'; -import { execSync, spawnSync } from 'child_process'; -import { send, setDebug, setSession, getSession } from './client.js'; -import type { Response } from './types.js'; - -// ============================================================================ -// System Dependencies Installation -// ============================================================================ - -// Common dependencies needed for Playwright browsers on Linux -const LINUX_DEPS = { - // Shared libraries for Chromium/Firefox/WebKit - apt: [ - 'libxcb-shm0', - 'libx11-xcb1', - 'libx11-6', - 'libxcb1', - 'libxext6', - 'libxrandr2', - 'libxcomposite1', - 'libxcursor1', - 'libxdamage1', - 'libxfixes3', - 'libxi6', - 'libgtk-3-0', - 'libpangocairo-1.0-0', - 'libpango-1.0-0', - 'libatk1.0-0', - 'libcairo-gobject2', - 'libcairo2', - 'libgdk-pixbuf-2.0-0', - 'libxrender1', - 'libasound2', - 'libfreetype6', - 'libfontconfig1', - 'libdbus-1-3', - 'libnss3', - 'libnspr4', - 'libatk-bridge2.0-0', - 'libdrm2', - 'libxkbcommon0', - 'libatspi2.0-0', - 'libcups2', - 'libxshmfence1', - 'libgbm1', - ], - dnf: [ - 'libxcb', - 'libX11-xcb', - 'libX11', - 'libXext', - 'libXrandr', - 'libXcomposite', - 'libXcursor', - 'libXdamage', - 'libXfixes', - 'libXi', - 'gtk3', - 'pango', - 'atk', - 'cairo-gobject', - 'cairo', - 'gdk-pixbuf2', - 'libXrender', - 'alsa-lib', - 'freetype', - 'fontconfig', - 'dbus-libs', - 'nss', - 'nspr', - 'at-spi2-atk', - 'libdrm', - 'libxkbcommon', - 'at-spi2-core', - 'cups-libs', - 'libxshmfence', - 'mesa-libgbm', - 'libwayland-client', - 'libwayland-server', - ], - yum: [ - 'libxcb', - 'libX11-xcb', - 'libX11', - 'libXext', - 'libXrandr', - 'libXcomposite', - 'libXcursor', - 'libXdamage', - 'libXfixes', - 'libXi', - 'gtk3', - 'pango', - 'atk', - 'cairo-gobject', - 'cairo', - 'gdk-pixbuf2', - 'libXrender', - 'alsa-lib', - 'freetype', - 'fontconfig', - 'dbus-libs', - 'nss', - 'nspr', - 'at-spi2-atk', - 'libdrm', - 'libxkbcommon', - 'at-spi2-core', - 'cups-libs', - 'libxshmfence', - 'mesa-libgbm', - ], -}; - -function detectPackageManager(): 'apt' | 'dnf' | 'yum' | null { - const managers = ['apt-get', 'dnf', 'yum'] as const; - for (const mgr of managers) { - try { - execSync(`which ${mgr}`, { stdio: 'ignore' }); - return mgr === 'apt-get' ? 'apt' : mgr; - } catch { - // Not found, try next - } - } - return null; -} - -function installSystemDeps(): void { - if (os.platform() !== 'linux') { - console.log('System dependency installation is only needed on Linux'); - return; - } - - const pkgMgr = detectPackageManager(); - if (!pkgMgr) { - throw new Error('No supported package manager found (apt-get, dnf, or yum)'); - } - - const deps = LINUX_DEPS[pkgMgr]; - if (!deps || deps.length === 0) { - throw new Error(`No dependencies defined for package manager: ${pkgMgr}`); - } - - console.log(`Detected package manager: ${pkgMgr}`); - console.log(`Installing ${deps.length} dependencies...`); - - let cmd: string; - switch (pkgMgr) { - case 'apt': - cmd = `apt-get update && apt-get install -y ${deps.join(' ')}`; - break; - case 'dnf': - cmd = `dnf install -y ${deps.join(' ')}`; - break; - case 'yum': - cmd = `yum install -y ${deps.join(' ')}`; - break; - } - - // Run with sudo if not root - const isRoot = process.getuid?.() === 0; - if (!isRoot) { - cmd = `sudo ${cmd}`; - } - - execSync(cmd, { stdio: 'inherit' }); -} - -// ============================================================================ -// Utilities -// ============================================================================ - -function listSessions(): string[] { - const tmpDir = os.tmpdir(); - try { - const files = fs.readdirSync(tmpDir); - const sessions: string[] = []; - for (const file of files) { - const match = file.match(/^agent-browser-(.+)\.pid$/); - if (match) { - const pidFile = path.join(tmpDir, file); - try { - const pid = parseInt(fs.readFileSync(pidFile, 'utf8').trim(), 10); - process.kill(pid, 0); - sessions.push(match[1]); - } catch { - /* Process not running */ - } - } - } - return sessions; - } catch { - return []; - } -} - -const colors = { - reset: '\x1b[0m', - bold: '\x1b[1m', - dim: '\x1b[2m', - red: '\x1b[31m', - green: '\x1b[32m', - yellow: '\x1b[33m', - cyan: '\x1b[36m', -}; - -const c = (color: keyof typeof colors, text: string) => `${colors[color]}${text}${colors.reset}`; - -function genId(): string { - return Math.random().toString(36).slice(2, 10); -} - -function err(msg: string): never { - console.error(c('red', 'Error:'), msg); - process.exit(1); -} - -// ============================================================================ -// Help -// ============================================================================ - -function printHelp(): void { - console.log(` -${c('bold', 'agent-browser')} - headless browser automation for AI agents - -${c('yellow', 'Usage:')} agent-browser [options] - -${c('yellow', 'Core Commands:')} - ${c('cyan', 'open')} Navigate to URL - ${c('cyan', 'click')} Click element (or @ref) - ${c('cyan', 'type')} Type into element - ${c('cyan', 'fill')} Clear and fill - ${c('cyan', 'press')} Press key (Enter, Tab, Control+a) - ${c('cyan', 'hover')} Hover element - ${c('cyan', 'select')} Select dropdown option - ${c('cyan', 'scroll')} [px] Scroll (up/down/left/right) - ${c('cyan', 'wait')} Wait for element or time - ${c('cyan', 'screenshot')} [path] Take screenshot - ${c('cyan', 'snapshot')} Accessibility tree with refs (for AI) - ${c('cyan', 'eval')} Run JavaScript - ${c('cyan', 'close')} Close browser - -${c('yellow', 'Selectors:')} CSS, XPath, text=, or ${c('green', '@ref')} from snapshot - ${c('dim', 'CSS:')} "#id", ".class", "button" - ${c('dim', 'XPath:')} "xpath=//button" - ${c('dim', 'Text:')} "text=Submit" - ${c('dim', 'Ref:')} ${c('green', '@e1')}, ${c('green', '@e2')} (from snapshot output) - -${c('yellow', 'Get Info:')} agent-browser get [selector] - text, html, value, attr, title, url, count, box - -${c('yellow', 'Check State:')} agent-browser is - visible, enabled, checked - -${c('yellow', 'Find Elements:')} agent-browser find [value] - role, text, label, placeholder, alt, title, testid, first, last, nth - -${c('yellow', 'Mouse:')} agent-browser mouse [args] - move , down, up, wheel - -${c('yellow', 'Storage:')} - ${c('cyan', 'cookies')} [get|set|clear] Manage cookies - ${c('cyan', 'storage')} Manage web storage - -${c('yellow', 'Browser:')} agent-browser set [value] - viewport, device, geo, offline, headers, credentials - -${c('yellow', 'Network:')} agent-browser network - route, unroute, requests - -${c('yellow', 'Tabs:')} - ${c('cyan', 'tab')} [new|list|close|] Manage tabs - -${c('yellow', 'Debug:')} - ${c('cyan', 'trace')} start|stop Record trace - ${c('cyan', 'console')} View console logs - ${c('cyan', 'errors')} View page errors - -${c('yellow', 'Setup:')} - ${c('cyan', 'install')} Install browser binaries - ${c('cyan', 'install')} --with-deps Also install system dependencies (Linux) - -${c('yellow', 'Options:')} - --session Isolated session (or AGENT_BROWSER_SESSION env) - --json JSON output - --full, -f Full page screenshot - --headed Show browser window (not headless) - --debug Debug output - -${c('yellow', 'Examples:')} - agent-browser open example.com - agent-browser snapshot # Get tree with refs - agent-browser click @e2 # Click by ref from snapshot - agent-browser fill @e3 "test@example.com" # Fill by ref - agent-browser click "#submit" # CSS selector still works - agent-browser get text @e1 # Get text by ref - agent-browser find role button click --name Submit -`); -} - -// ============================================================================ -// Response Printing -// ============================================================================ - -function printResponse(response: Response, jsonMode: boolean): void { - if (jsonMode) { - console.log(JSON.stringify(response)); - return; - } - - if (!response.success) { - console.error(c('red', '✗ Error:'), response.error); - process.exit(1); - } - - const data = response.data as Record; - - if (data.url && data.title) { - console.log(c('green', '✓'), c('bold', data.title as string)); - console.log(c('dim', ` ${data.url}`)); - } else if (data.text !== undefined) { - console.log(data.text ?? c('dim', 'null')); - } else if (data.html !== undefined) { - console.log(data.html); - } else if (data.value !== undefined) { - console.log(data.value ?? c('dim', 'null')); - } else if (data.result !== undefined) { - const result = data.result; - console.log(typeof result === 'object' ? JSON.stringify(result, null, 2) : result); - } else if (data.snapshot) { - console.log(data.snapshot); - } else if (data.visible !== undefined) { - console.log(data.visible ? c('green', 'true') : c('red', 'false')); - } else if (data.enabled !== undefined) { - console.log(data.enabled ? c('green', 'true') : c('red', 'false')); - } else if (data.checked !== undefined) { - console.log(data.checked ? c('green', 'true') : c('red', 'false')); - } else if (data.count !== undefined) { - console.log(data.count); - } else if (data.box) { - const box = data.box as { x: number; y: number; width: number; height: number }; - console.log(`x:${box.x} y:${box.y} w:${box.width} h:${box.height}`); - } else if (data.url) { - console.log(data.url); - } else if (data.title) { - console.log(data.title); - } else if (data.base64) { - console.log(c('green', '✓'), 'Screenshot captured'); - } else if (data.path) { - console.log(c('green', '✓'), `Saved: ${data.path}`); - } else if (data.cookies) { - const cookies = data.cookies as Array<{ name: string; value: string }>; - if (cookies.length === 0) console.log(c('dim', 'No cookies')); - else cookies.forEach((ck) => console.log(`${c('cyan', ck.name)}: ${ck.value}`)); - } else if (data.tabs) { - const tabs = data.tabs as Array<{ index: number; url: string; title: string; active: boolean }>; - tabs.forEach((t) => { - const marker = t.active ? c('green', '→') : ' '; - console.log(`${marker} [${t.index}] ${t.title || c('dim', '(untitled)')}`); - if (t.url) console.log(c('dim', ` ${t.url}`)); - }); - } else if (data.index !== undefined && data.total !== undefined) { - console.log(c('green', '✓'), `Tab ${data.index} (${data.total} total)`); - } else if (data.messages) { - const msgs = data.messages as Array<{ type: string; text: string }>; - if (msgs.length === 0) console.log(c('dim', 'No messages')); - else - msgs.forEach((m) => { - const col = m.type === 'error' ? 'red' : m.type === 'warning' ? 'yellow' : 'dim'; - console.log(`${c(col, `[${m.type}]`)} ${m.text}`); - }); - } else if (data.errors) { - const errs = data.errors as Array<{ message: string }>; - if (errs.length === 0) console.log(c('dim', 'No errors')); - else errs.forEach((e) => console.log(c('red', '✗'), e.message)); - } else if (data.requests) { - const reqs = data.requests as Array<{ method: string; url: string }>; - if (reqs.length === 0) console.log(c('dim', 'No requests')); - else reqs.forEach((r) => console.log(`${c('cyan', r.method)} ${r.url}`)); - } else if (data.moved) { - console.log(c('green', '✓'), `Moved to (${data.x}, ${data.y})`); - } else if (data.body !== undefined && data.status !== undefined) { - // Response body - console.log(c('green', '✓'), `${data.status} ${data.url}`); - console.log(typeof data.body === 'object' ? JSON.stringify(data.body, null, 2) : data.body); - } else if (data.filename) { - // Download - console.log(c('green', '✓'), `Downloaded: ${data.filename}`); - console.log(c('dim', ` Path: ${data.path}`)); - } else if (data.inserted) { - console.log(c('green', '✓'), 'Text inserted'); - } else if (data.key) { - console.log(c('green', '✓'), `Key ${data.down ? 'down' : 'up'}: ${data.key}`); - } else if (data.note) { - console.log(c('yellow', '⚠'), data.note); - } else if (data.closed === true) { - console.log(c('green', '✓'), 'Browser closed'); - } else if (data.launched) { - console.log(c('green', '✓'), 'Browser launched'); - } else if (data.state) { - console.log(c('green', '✓'), `Load state: ${data.state}`); - } else if ( - Object.keys(data).some((k) => - [ - 'clicked', - 'typed', - 'filled', - 'pressed', - 'hovered', - 'scrolled', - 'selected', - 'waited', - 'checked', - 'unchecked', - 'focused', - 'set', - 'cleared', - 'started', - 'down', - 'up', - ].includes(k) - ) - ) { - console.log(c('green', '✓'), 'Done'); - } else { - console.log(c('green', '✓'), JSON.stringify(data)); - } -} - -// ============================================================================ -// Command Handlers -// ============================================================================ - -async function handleGet(args: string[], id: string): Promise> { - const what = args[0]; - const selector = args[1]; - - switch (what) { - case 'text': - if (!selector) err('Selector required: agent-browser get text '); - return { id, action: 'gettext', selector }; - case 'html': - if (!selector) err('Selector required: agent-browser get html '); - return { id, action: 'innerhtml', selector }; - case 'value': - if (!selector) err('Selector required: agent-browser get value '); - return { id, action: 'inputvalue', selector }; - case 'attr': - if (!selector || !args[2]) err('Usage: agent-browser get attr '); - return { id, action: 'getattribute', selector, attribute: args[2] }; - case 'title': - return { id, action: 'title' }; - case 'url': - return { id, action: 'url' }; - case 'count': - if (!selector) err('Selector required: agent-browser get count '); - return { id, action: 'count', selector }; - case 'box': - if (!selector) err('Selector required: agent-browser get box '); - return { id, action: 'boundingbox', selector }; - default: - err(`Unknown: agent-browser get ${what}. Options: text, html, value, attr, title, url, count, box`); - } -} - -async function handleIs(args: string[], id: string): Promise> { - const what = args[0]; - const selector = args[1]; - - if (!selector) err(`Selector required: agent-browser is ${what} `); - - switch (what) { - case 'visible': - return { id, action: 'isvisible', selector }; - case 'enabled': - return { id, action: 'isenabled', selector }; - case 'checked': - return { id, action: 'ischecked', selector }; - default: - err(`Unknown: agent-browser is ${what}. Options: visible, enabled, checked`); - } -} - -async function handleFind( - args: string[], - id: string, - flags: Flags -): Promise> { - const locator = args[0]; - const value = args[1]; - const subaction = args[2] || 'click'; - const fillValue = args[3]; - - if (!value) err(`Value required: agent-browser find ${locator} `); - - const exact = flags.exact; - const name = flags.name; - - switch (locator) { - case 'role': - return { id, action: 'getbyrole', role: value, subaction, value: fillValue, name, exact }; - case 'text': - return { id, action: 'getbytext', text: value, subaction, exact }; - case 'label': - return { id, action: 'getbylabel', label: value, subaction, value: fillValue, exact }; - case 'placeholder': - return { - id, - action: 'getbyplaceholder', - placeholder: value, - subaction, - value: fillValue, - exact, - }; - case 'alt': - return { id, action: 'getbyalttext', text: value, subaction, exact }; - case 'title': - return { id, action: 'getbytitle', text: value, subaction, exact }; - case 'testid': - return { id, action: 'getbytestid', testId: value, subaction, value: fillValue }; - case 'first': - return { id, action: 'nth', selector: value, index: 0, subaction, value: fillValue }; - case 'last': - return { id, action: 'nth', selector: value, index: -1, subaction, value: fillValue }; - case 'nth': { - const idx = parseInt(value, 10); - const sel = args[2]; - const act = args[3] || 'click'; - const val = args[4]; - if (isNaN(idx) || !sel) err('Usage: agent-browser find nth '); - return { id, action: 'nth', selector: sel, index: idx, subaction: act, value: val }; - } - default: - err( - `Unknown locator: ${locator}. Options: role, text, label, placeholder, alt, title, testid, first, last, nth` - ); - } -} - -async function handleMouse(args: string[], id: string): Promise> { - const action = args[0]; - - switch (action) { - case 'move': { - const x = parseInt(args[1], 10); - const y = parseInt(args[2], 10); - if (isNaN(x) || isNaN(y)) err('Usage: agent-browser mouse move '); - return { id, action: 'mousemove', x, y }; - } - case 'down': - return { id, action: 'mousedown', button: args[1] || 'left' }; - case 'up': - return { id, action: 'mouseup', button: args[1] || 'left' }; - case 'wheel': { - const dy = parseInt(args[1], 10) || 100; - const dx = parseInt(args[2], 10) || 0; - return { id, action: 'wheel', deltaY: dy, deltaX: dx }; - } - default: - err(`Unknown: agent-browser mouse ${action}. Options: move, down, up, wheel`); - } -} - -async function handleSet(args: string[], id: string): Promise> { - const setting = args[0]; - - switch (setting) { - case 'viewport': { - const w = parseInt(args[1], 10); - const h = parseInt(args[2], 10); - if (isNaN(w) || isNaN(h)) err('Usage: agent-browser set viewport '); - return { id, action: 'viewport', width: w, height: h }; - } - case 'device': - if (!args[1]) err('Usage: agent-browser set device '); - return { id, action: 'device', device: args[1] }; - case 'geo': - case 'geolocation': { - const lat = parseFloat(args[1]); - const lng = parseFloat(args[2]); - if (isNaN(lat) || isNaN(lng)) err('Usage: agent-browser set geo '); - return { id, action: 'geolocation', latitude: lat, longitude: lng }; - } - case 'offline': - return { id, action: 'offline', offline: args[1] !== 'off' && args[1] !== 'false' }; - case 'headers': - if (!args[1]) err('Usage: agent-browser set headers '); - try { - return { id, action: 'headers', headers: JSON.parse(args[1]) }; - } catch { - err('Invalid JSON for headers'); - } - break; - case 'credentials': - case 'auth': - if (!args[1] || !args[2]) err('Usage: agent-browser set credentials '); - return { id, action: 'credentials', username: args[1], password: args[2] }; - case 'media': { - const colorScheme = args.includes('dark') - ? 'dark' - : args.includes('light') - ? 'light' - : undefined; - const media = args.includes('print') - ? 'print' - : args.includes('screen') - ? 'screen' - : undefined; - return { id, action: 'emulatemedia', colorScheme, media }; - } - default: - err( - `Unknown: agent-browser set ${setting}. Options: viewport, device, geo, offline, headers, credentials, media` - ); - } - return {}; -} - -async function handleNetwork( - args: string[], - id: string, - allArgs: string[] -): Promise> { - const action = args[0]; - - switch (action) { - case 'route': { - const url = args[1]; - if (!url) err('Usage: agent-browser network route [--abort|--body ]'); - const abort = allArgs.includes('--abort'); - const bodyIdx = allArgs.indexOf('--body'); - const body = bodyIdx !== -1 ? allArgs[bodyIdx + 1] : undefined; - return { - id, - action: 'route', - url, - abort, - response: body ? { body, contentType: 'application/json' } : undefined, - }; - } - case 'unroute': - return { id, action: 'unroute', url: args[1] }; - case 'requests': { - const clear = allArgs.includes('--clear'); - const filterIdx = allArgs.indexOf('--filter'); - const filter = filterIdx !== -1 ? allArgs[filterIdx + 1] : undefined; - return { id, action: 'requests', clear, filter }; - } - default: - err(`Unknown: agent-browser network ${action}. Options: route, unroute, requests`); - } - return {}; -} - -async function handleStorage(args: string[], id: string): Promise> { - const type = args[0] as 'local' | 'session'; - const sub = args[1]; - - if (type !== 'local' && type !== 'session') { - err('Usage: agent-browser storage [get|set|clear] [key] [value]'); - } - - if (sub === 'set') { - if (!args[2] || !args[3]) err(`Usage: agent-browser storage ${type} set `); - return { id, action: 'storage_set', type, key: args[2], value: args[3] }; - } else if (sub === 'clear') { - return { id, action: 'storage_clear', type }; - } else { - // get (default) - return { id, action: 'storage_get', type, key: sub }; - } -} - -async function handleCookies(args: string[], id: string): Promise> { - const sub = args[0]; - - if (sub === 'set') { - if (!args[1]) err('Usage: agent-browser cookies set '); - try { - return { id, action: 'cookies_set', cookies: JSON.parse(args[1]) }; - } catch { - err('Invalid JSON for cookies'); - } - } else if (sub === 'clear') { - return { id, action: 'cookies_clear' }; - } else { - return { id, action: 'cookies_get' }; - } - return {}; -} - -async function handleTab(args: string[], id: string): Promise> { - const sub = args[0]; - - if (sub === 'new') { - return { id, action: 'tab_new' }; - } else if (sub === 'list' || sub === 'ls' || !sub) { - return { id, action: 'tab_list' }; - } else if (sub === 'close') { - const idx = args[1] !== undefined ? parseInt(args[1], 10) : undefined; - return { id, action: 'tab_close', index: idx }; - } else { - const idx = parseInt(sub, 10); - if (isNaN(idx)) err(`Unknown: agent-browser tab ${sub}. Options: new, list, close, `); - return { id, action: 'tab_switch', index: idx }; - } -} - -async function handleTrace(args: string[], id: string): Promise> { - const sub = args[0]; - - if (sub === 'start') { - return { id, action: 'trace_start', screenshots: true, snapshots: true }; - } else if (sub === 'stop') { - if (!args[1]) err('Usage: agent-browser trace stop '); - return { id, action: 'trace_stop', path: args[1] }; - } else { - err('Usage: agent-browser trace start|stop'); - } - return {}; -} - -async function handleState(args: string[], id: string): Promise> { - const sub = args[0]; - const path = args[1]; - - if (sub === 'save') { - if (!path) err('Usage: agent-browser state save '); - return { id, action: 'state_save', path }; - } else if (sub === 'load') { - if (!path) err('Usage: agent-browser state load '); - return { id, action: 'state_load', path }; - } else { - err('Usage: agent-browser state save|load '); - } - return {}; -} - -// ============================================================================ -// Flags Parser -// ============================================================================ - -interface Flags { - json: boolean; - full: boolean; - text: boolean; - debug: boolean; - headed: boolean; - session: string; - selector?: string; - name?: string; - exact: boolean; - url?: string; - load?: string; - fn?: string; -} - -function parseFlags(args: string[]): { flags: Flags; cleanArgs: string[] } { - const flags: Flags = { - json: false, - full: false, - text: false, - debug: false, - headed: false, - session: process.env.AGENT_BROWSER_SESSION || 'default', - exact: false, - }; - - const cleanArgs: string[] = []; - let i = 0; - - while (i < args.length) { - const arg = args[i]; - - if (arg === '--json') { - flags.json = true; - } else if (arg === '--full' || arg === '-f') { - flags.full = true; - } else if (arg === '--text' || arg === '-t') { - flags.text = true; - } else if (arg === '--debug') { - flags.debug = true; - } else if (arg === '--headed' || arg === '--head') { - flags.headed = true; - } else if (arg === '--exact') { - flags.exact = true; - } else if (arg === '--session' && args[i + 1]) { - flags.session = args[++i]; - } else if ((arg === '--selector' || arg === '-s') && args[i + 1]) { - flags.selector = args[++i]; - } else if ((arg === '--name' || arg === '-n') && args[i + 1]) { - flags.name = args[++i]; - } else if (arg === '--url' && args[i + 1]) { - flags.url = args[++i]; - } else if (arg === '--load' && args[i + 1]) { - flags.load = args[++i]; - } else if ((arg === '--fn' || arg === '--function') && args[i + 1]) { - flags.fn = args[++i]; - } else if (!arg.startsWith('-')) { - cleanArgs.push(arg); - } - i++; - } - - return { flags, cleanArgs }; -} - -// ============================================================================ -// Main -// ============================================================================ - -async function main(): Promise { - const rawArgs = process.argv.slice(2); - const { flags, cleanArgs } = parseFlags(rawArgs); - - if (flags.debug) setDebug(true); - setSession(flags.session); - - if (cleanArgs.length === 0 || rawArgs.includes('--help') || rawArgs.includes('-h')) { - printHelp(); - process.exit(0); - } - - const command = cleanArgs[0]; - const args = cleanArgs.slice(1); - const id = genId(); - - let cmd: Record; - - switch (command) { - // === Core Commands === - case 'open': - case 'goto': - case 'navigate': { - if (!args[0]) err('URL required'); - const url = args[0].startsWith('http') ? args[0] : `https://${args[0]}`; - // If --headed, launch with headless=false first - if (flags.headed) { - await send({ id: genId(), action: 'launch', headless: false }); - } - cmd = { id, action: 'navigate', url }; - break; - } - - case 'click': - if (!args[0]) err('Selector required'); - cmd = { id, action: 'click', selector: args[0] }; - break; - - case 'dblclick': - if (!args[0]) err('Selector required'); - cmd = { id, action: 'dblclick', selector: args[0] }; - break; - - case 'type': - if (!args[0] || !args[1]) err('Usage: agent-browser type '); - cmd = { id, action: 'type', selector: args[0], text: args.slice(1).join(' ') }; - break; - - case 'fill': - if (!args[0] || !args[1]) err('Usage: agent-browser fill '); - cmd = { id, action: 'fill', selector: args[0], value: args.slice(1).join(' ') }; - break; - - case 'press': - case 'key': - if (!args[0]) err('Key required'); - cmd = { id, action: 'press', key: args[0] }; - break; - - case 'keydown': - if (!args[0]) err('Key required'); - cmd = { id, action: 'keydown', key: args[0] }; - break; - - case 'keyup': - if (!args[0]) err('Key required'); - cmd = { id, action: 'keyup', key: args[0] }; - break; - - case 'hover': - if (!args[0]) err('Selector required'); - cmd = { id, action: 'hover', selector: args[0] }; - break; - - case 'focus': - if (!args[0]) err('Selector required'); - cmd = { id, action: 'focus', selector: args[0] }; - break; - - case 'check': - if (!args[0]) err('Selector required'); - cmd = { id, action: 'check', selector: args[0] }; - break; - - case 'uncheck': - if (!args[0]) err('Selector required'); - cmd = { id, action: 'uncheck', selector: args[0] }; - break; - - case 'select': - if (!args[0] || !args[1]) err('Usage: agent-browser select '); - cmd = { id, action: 'select', selector: args[0], value: args[1] }; - break; - - case 'drag': - if (!args[0] || !args[1]) err('Usage: agent-browser drag '); - cmd = { id, action: 'drag', source: args[0], target: args[1] }; - break; - - case 'upload': - if (!args[0] || !args[1]) err('Usage: agent-browser upload '); - cmd = { id, action: 'upload', selector: args[0], files: args.slice(1) }; - break; - - case 'scroll': { - const dir = args[0] || 'down'; - const amount = parseInt(args[1], 10) || 300; - cmd = { id, action: 'scroll', direction: dir, amount, selector: flags.selector }; - break; - } - - case 'wait': { - const target = args[0]; - // Check for flags - if (flags.fn) { - cmd = { id, action: 'waitforfunction', expression: flags.fn }; - } else if (flags.url) { - cmd = { id, action: 'waitforurl', url: flags.url }; - } else if (flags.load) { - cmd = { id, action: 'waitforloadstate', state: flags.load }; - } else if (flags.text) { - if (!target) err('Text required with --text flag'); - cmd = { id, action: 'wait', text: target }; - } else if (target && /^\d+$/.test(target)) { - cmd = { id, action: 'wait', timeout: parseInt(target, 10) }; - } else if (target) { - cmd = { id, action: 'wait', selector: target }; - } else { - err('Usage: agent-browser wait '); - } - break; - } - - case 'screenshot': { - const path = args[0]; - cmd = { id, action: 'screenshot', path, fullPage: flags.full, selector: flags.selector }; - break; - } - - case 'pdf': - if (!args[0]) err('Path required'); - cmd = { id, action: 'pdf', path: args[0] }; - break; - - case 'snapshot': - cmd = { id, action: 'snapshot' }; - break; - - case 'eval': - if (!args[0]) err('Script required'); - cmd = { id, action: 'evaluate', script: args.join(' ') }; - break; - - case 'close': - case 'quit': - case 'exit': - cmd = { id, action: 'close' }; - break; - - // === Navigation === - case 'back': - cmd = { id, action: 'back' }; - break; - - case 'forward': - cmd = { id, action: 'forward' }; - break; - - case 'reload': - cmd = { id, action: 'reload' }; - break; - - // === Grouped Commands === - case 'get': - cmd = await handleGet(args, id); - break; - - case 'is': - cmd = await handleIs(args, id); - break; - - case 'find': - cmd = await handleFind(args, id, flags); - break; - - case 'mouse': - cmd = await handleMouse(args, id); - break; - - case 'set': - cmd = await handleSet(args, id); - break; - - case 'network': - cmd = await handleNetwork(args, id, rawArgs); - break; - - case 'storage': - cmd = await handleStorage(args, id); - break; - - case 'cookies': - cmd = await handleCookies(args, id); - break; - - case 'tab': - cmd = await handleTab(args, id); - break; - - case 'window': - if (args[0] === 'new') { - cmd = { id, action: 'window_new' }; - } else { - err('Usage: agent-browser window new'); - } - break; - - case 'frame': - if (!args[0]) err('Selector required'); - if (args[0] === 'main') { - cmd = { id, action: 'mainframe' }; - } else { - cmd = { id, action: 'frame', selector: args[0] }; - } - break; - - case 'dialog': - if (args[0] === 'accept') { - cmd = { id, action: 'dialog', response: 'accept', promptText: args[1] }; - } else if (args[0] === 'dismiss') { - cmd = { id, action: 'dialog', response: 'dismiss' }; - } else { - err('Usage: agent-browser dialog accept|dismiss'); - } - break; - - case 'trace': - cmd = await handleTrace(args, id); - break; - - case 'state': - cmd = await handleState(args, id); - break; - - case 'console': - cmd = { id, action: 'console', clear: rawArgs.includes('--clear') }; - break; - - case 'errors': - cmd = { id, action: 'errors', clear: rawArgs.includes('--clear') }; - break; - - case 'highlight': - if (!args[0]) err('Selector required'); - cmd = { id, action: 'highlight', selector: args[0] }; - break; - - case 'scrollintoview': - case 'scrollinto': - if (!args[0]) err('Selector required'); - cmd = { id, action: 'scrollintoview', selector: args[0] }; - break; - - case 'initscript': - if (!args[0]) err('Script required'); - cmd = { id, action: 'addinitscript', script: args.join(' ') }; - break; - - case 'inserttext': - case 'insert': - if (!args[0]) err('Text required'); - cmd = { id, action: 'inserttext', text: args.join(' ') }; - break; - - case 'multiselect': - if (!args[0] || args.length < 2) - err('Usage: agent-browser multiselect [value2...]'); - cmd = { id, action: 'multiselect', selector: args[0], values: args.slice(1) }; - break; - - case 'download': - cmd = { id, action: 'waitfordownload', path: args[0] }; - break; - - case 'response': - if (!args[0]) err('URL pattern required'); - cmd = { id, action: 'responsebody', url: args[0] }; - break; - - case 'session': - if (args[0] === 'list' || args[0] === 'ls') { - const sessions = listSessions(); - const current = getSession(); - if (sessions.length === 0) { - console.log(c('dim', 'No active sessions')); - } else { - sessions.forEach((s) => { - const marker = s === current ? c('green', '→') : ' '; - console.log(`${marker} ${c('cyan', s)}`); - }); - } - process.exit(0); - } else { - console.log(c('cyan', getSession())); - process.exit(0); - } - - case 'install': { - const withDeps = rawArgs.includes('--with-deps') || rawArgs.includes('-d'); - - // Install system dependencies first if requested - if (withDeps) { - console.log(c('cyan', 'Installing system dependencies...')); - try { - installSystemDeps(); - console.log(c('green', '✓'), 'System dependencies installed'); - } catch (error) { - const msg = error instanceof Error ? error.message : String(error); - console.error(c('red', '✗'), 'Failed to install system dependencies:', msg); - process.exit(1); - } - } - - // Install browsers - console.log(c('cyan', 'Installing Playwright browsers...')); - try { - execSync('npx playwright install', { stdio: 'inherit' }); - console.log(c('green', '✓'), 'Browsers installed successfully'); - process.exit(0); - } catch (error) { - console.error(c('red', '✗'), 'Failed to install browsers'); - process.exit(1); - } - } - - // === Legacy aliases for backwards compatibility === - case 'url': - cmd = { id, action: 'url' }; - break; - case 'title': - cmd = { id, action: 'title' }; - break; - case 'gettext': - cmd = { id, action: 'gettext', selector: args[0] }; - break; - case 'extract': - cmd = { id, action: 'content', selector: args[0] }; - break; - - default: - console.error(c('red', 'Unknown command:'), command); - console.error(c('dim', 'Run: agent-browser --help')); - process.exit(1); - } - - try { - const response = await send(cmd); - printResponse(response, flags.json); - process.exit(0); - } catch (error) { - const message = error instanceof Error ? error.message : String(error); - if (flags.json) { - console.log(JSON.stringify({ id, success: false, error: message })); - } else { - console.error(c('red', '✗ Error:'), message); - } - process.exit(1); - } -} - -main();