#!/usr/bin/env node import * as fs from 'fs'; import * as os from 'os'; import * as path from 'path'; import { execSync } from 'child_process'; import { send, setDebug, setSession, getSession } from './client.js'; import type { Response } from './types.js'; // ============================================================================ // 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 ${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 (for AI) ${c('cyan', 'eval')} Run JavaScript ${c('cyan', 'close')} Close browser ${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('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 click "#submit" agent-browser fill "#email" "test@example.com" agent-browser get text "h1" agent-browser is visible ".modal" agent-browser find role button click --name Submit agent-browser wait 2000 agent-browser wait --load networkidle `); } // ============================================================================ // 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': { 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();