browsirai 0.1.0 → 0.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../../src/cli/run.ts","../../src/chrome-launcher.ts","../../src/cdp/connection.ts"],"sourcesContent":["/**\n * CLI runner for browsirai.\n *\n * Parses `browsirai <command> [args...]`, connects to Chrome via CDP,\n * looks up the command in a registry, and executes it.\n */\n\nimport pc from \"picocolors\";\nimport { connectChrome } from \"../chrome-launcher.js\";\nimport { CDPConnection } from \"../cdp/connection.js\";\nimport type { CLICommand } from \"./types.js\";\n\n// ---------------------------------------------------------------------------\n// Flag parsing utility\n// ---------------------------------------------------------------------------\n\n/**\n * Parses CLI flags from an args array.\n *\n * Supports:\n * --key=value → { key: \"value\" }\n * --key value → { key: \"value\" }\n * --flag → { flag: \"true\" }\n * -i → { i: \"true\" } (short boolean)\n * -d 5 → { d: \"5\" } (short with value)\n * -ic → { i: \"true\", c: \"true\" } (combined short booleans)\n * positional → { _0: \"positional\", _1: ... }\n *\n * @returns Record of parsed flags and positional args keyed as _0, _1, etc.\n */\nexport function parseFlags(args: string[]): Record<string, string> {\n const flags: Record<string, string> = {};\n let positionalIndex = 0;\n\n for (let i = 0; i < args.length; i++) {\n const arg = args[i]!;\n\n if (arg.startsWith(\"--\")) {\n const eqIdx = arg.indexOf(\"=\");\n if (eqIdx !== -1) {\n // --key=value\n const key = arg.slice(2, eqIdx);\n const value = arg.slice(eqIdx + 1);\n flags[key] = value;\n } else {\n const key = arg.slice(2);\n const next = args[i + 1];\n if (next && !next.startsWith(\"-\")) {\n // --key value\n flags[key] = next;\n i++;\n } else {\n // --flag (boolean)\n flags[key] = \"true\";\n }\n }\n } else if (arg.startsWith(\"-\") && arg.length > 1 && !/^-\\d/.test(arg)) {\n // Short flags: -i, -c, -d 5, -ic\n const chars = arg.slice(1);\n if (chars.length === 1) {\n // Single short flag: -i or -d 5\n const next = args[i + 1];\n if (next && !next.startsWith(\"-\")) {\n flags[chars] = next;\n i++;\n } else {\n flags[chars] = \"true\";\n }\n } else {\n // Combined short flags: -ic → i=true, c=true\n for (const ch of chars) {\n flags[ch] = \"true\";\n }\n }\n } else {\n flags[`_${positionalIndex}`] = arg;\n positionalIndex++;\n }\n }\n\n return flags;\n}\n\n// ---------------------------------------------------------------------------\n// Result printer\n// ---------------------------------------------------------------------------\n\n/**\n * Pretty-prints a command result to stdout.\n * Objects/arrays are JSON-formatted; primitives are printed as-is.\n */\nexport function printResult(data: unknown): void {\n if (data === undefined || data === null) return;\n\n if (typeof data === \"string\") {\n console.log(data);\n } else if (typeof data === \"object\") {\n console.log(JSON.stringify(data, null, 2));\n } else {\n console.log(String(data));\n }\n}\n\n// ---------------------------------------------------------------------------\n// Command registry\n// ---------------------------------------------------------------------------\n\ninterface CommandCategory {\n name: string;\n commands: CLICommand[];\n}\n\nasync function loadCommands(): Promise<CommandCategory[]> {\n const categories: CommandCategory[] = [];\n\n const imports: Array<{\n name: string;\n path: string;\n key: string;\n }> = [\n { name: \"Navigation\", path: \"./commands/nav.js\", key: \"navCommands\" },\n { name: \"Observation\", path: \"./commands/obs.js\", key: \"obsCommands\" },\n { name: \"Actions\", path: \"./commands/act.js\", key: \"actCommands\" },\n { name: \"Network\", path: \"./commands/net.js\", key: \"netCommands\" },\n ];\n\n const base = new URL(\".\", import.meta.url);\n for (const entry of imports) {\n try {\n const url = new URL(entry.path, base).href;\n const mod = (await import(url)) as Record<string, CLICommand[]>;\n const commands = mod[entry.key];\n if (commands && Array.isArray(commands) && commands.length > 0) {\n categories.push({ name: entry.name, commands });\n }\n } catch {\n // Command file not yet created — skip silently\n }\n }\n\n return categories;\n}\n\nfunction buildRegistry(\n categories: CommandCategory[],\n): Map<string, CLICommand> {\n const registry = new Map<string, CLICommand>();\n for (const cat of categories) {\n for (const cmd of cat.commands) {\n registry.set(cmd.name, cmd);\n if (cmd.aliases) {\n for (const alias of cmd.aliases) {\n registry.set(alias, cmd);\n }\n }\n }\n }\n return registry;\n}\n\n// ---------------------------------------------------------------------------\n// Help output\n// ---------------------------------------------------------------------------\n\nfunction printHelp(categories: CommandCategory[]): void {\n console.log();\n console.log(pc.bold(\"browsirai\") + \" — Browser automation from the terminal\");\n console.log();\n console.log(pc.dim(\"Usage:\") + \" browsirai <command> [args...] [--flags]\");\n console.log();\n\n if (categories.length === 0) {\n console.log(\n pc.yellow(\" No commands available yet. Command modules have not been installed.\"),\n );\n console.log();\n return;\n }\n\n for (const cat of categories) {\n console.log(pc.cyan(pc.bold(` ${cat.name}`)));\n for (const cmd of cat.commands) {\n const aliasStr = cmd.aliases?.length\n ? pc.dim(` (${cmd.aliases.join(\", \")})`)\n : \"\";\n const name = pc.green(cmd.name.padEnd(20));\n console.log(` ${name} ${pc.dim(cmd.description)}${aliasStr}`);\n }\n console.log();\n }\n\n console.log(pc.dim(\" Examples:\"));\n console.log(pc.dim(' browsirai open example.com'));\n console.log(pc.dim(' browsirai snapshot -i'));\n console.log(pc.dim(' browsirai click @e5'));\n console.log(pc.dim(' browsirai fill @e2 \"hello world\"'));\n console.log(pc.dim(' browsirai press Enter'));\n console.log(pc.dim(' browsirai eval \"document.title\"'));\n console.log();\n}\n\n// ---------------------------------------------------------------------------\n// CDP connection helper\n// ---------------------------------------------------------------------------\n\nasync function connectCDP(): Promise<CDPConnection> {\n const result = await connectChrome({ autoLaunch: true });\n\n if (!result.success) {\n const msg = result.error ?? \"Could not connect to Chrome via CDP.\";\n throw new Error(msg);\n }\n\n const wsUrl = result.wsEndpoint ?? `ws://127.0.0.1:${result.port}/devtools/browser`;\n const browser = new CDPConnection(wsUrl);\n await browser.connect();\n\n // Find a page target and attach to it (same as MCP server)\n const targets = await browser.send(\"Target.getTargets\") as {\n targetInfos: Array<{ targetId: string; type: string; url: string }>;\n };\n\n let page = targets.targetInfos.find(\n (t) => t.type === \"page\" && !t.url.startsWith(\"chrome://\")\n ) ?? targets.targetInfos.find(\n (t) => t.type === \"page\"\n );\n\n if (!page) {\n const created = await browser.send(\"Target.createTarget\", { url: \"about:blank\" }) as { targetId: string };\n page = { targetId: created.targetId, type: \"page\", url: \"about:blank\" };\n }\n\n const attached = await browser.send(\"Target.attachToTarget\", {\n targetId: page.targetId,\n flatten: true,\n }) as { sessionId: string };\n\n // Return a session-scoped proxy that sends commands with the sessionId\n const sessionId = attached.sessionId;\n const session = Object.create(browser) as CDPConnection;\n const originalSend = browser.send.bind(browser);\n session.send = (method: string, params?: Record<string, unknown>, options?: { timeout?: number; sessionId?: string }) => {\n return originalSend(method, params, {\n ...options,\n sessionId: options?.sessionId ?? sessionId,\n });\n };\n session.close = () => browser.close();\n\n await Promise.all([\n session.send(\"Page.enable\"),\n session.send(\"Runtime.enable\"),\n ]).catch(() => {});\n\n return session;\n}\n\n\n// ---------------------------------------------------------------------------\n// Main CLI runner\n// ---------------------------------------------------------------------------\n\nexport async function runCLI(args: string[]): Promise<void> {\n const commandName = args[0];\n const remainingArgs = args.slice(1);\n\n // Load available commands\n const categories = await loadCommands();\n const registry = buildRegistry(categories);\n\n // No command or --help → show help\n if (!commandName || commandName === \"--help\" || commandName === \"-h\") {\n printHelp(categories);\n return;\n }\n\n // Look up the command\n const command = registry.get(commandName);\n if (!command) {\n console.error(\n pc.red(`Unknown command: ${pc.bold(commandName)}`),\n );\n console.log();\n console.log(\n pc.dim(\"Run \") + pc.bold(\"browsirai --help\") + pc.dim(\" to see available commands.\"),\n );\n\n // Suggest similar commands\n const similar = findSimilar(commandName, registry);\n if (similar.length > 0) {\n console.log();\n console.log(pc.dim(\"Did you mean?\"));\n for (const s of similar) {\n console.log(` ${pc.green(s)}`);\n }\n }\n\n console.log();\n process.exit(1);\n }\n\n // Connect to Chrome and run the command\n let cdp: CDPConnection | null = null;\n try {\n cdp = await connectCDP();\n await command.run(cdp, remainingArgs);\n } catch (err) {\n const message =\n err instanceof Error ? err.message : String(err);\n console.error(pc.red(`Error: ${message}`));\n process.exit(1);\n } finally {\n if (cdp?.isConnected) {\n cdp.close();\n }\n }\n}\n\n// ---------------------------------------------------------------------------\n// Fuzzy matching helper\n// ---------------------------------------------------------------------------\n\nfunction findSimilar(\n input: string,\n registry: Map<string, CLICommand>,\n): string[] {\n const names = Array.from(registry.keys());\n return names\n .filter((name) => {\n // Simple substring or prefix match\n return (\n name.includes(input) ||\n input.includes(name) ||\n levenshtein(input, name) <= 3\n );\n })\n .slice(0, 3);\n}\n\nfunction levenshtein(a: string, b: string): number {\n const m = a.length;\n const n = b.length;\n const dp: number[][] = Array.from({ length: m + 1 }, () =>\n Array(n + 1).fill(0) as number[],\n );\n\n for (let i = 0; i <= m; i++) dp[i]![0] = i;\n for (let j = 0; j <= n; j++) dp[0]![j] = j;\n\n for (let i = 1; i <= m; i++) {\n for (let j = 1; j <= n; j++) {\n const cost = a[i - 1] === b[j - 1] ? 0 : 1;\n dp[i]![j] = Math.min(\n dp[i - 1]![j]! + 1,\n dp[i]![j - 1]! + 1,\n dp[i - 1]![j - 1]! + cost,\n );\n }\n }\n\n return dp[m]![n]!;\n}\n","/**\n * Chrome connection — connects to Chrome via CDP.\n *\n * Strategy (ordered by preference):\n * 1. If Chrome is already running with --remote-debugging-port → connect via DevToolsActivePort\n * 2. If Chrome is running without debugging → quit & relaunch with --remote-debugging-port\n * 3. If Chrome is not running → launch with --remote-debugging-port\n *\n * Using --remote-debugging-port avoids the Chrome M144 \"Allow remote debugging?\" modal\n * entirely. The default user data directory is preserved, so cookies, logins, tabs,\n * and extensions remain intact.\n */\n\nimport { execSync, spawn } from \"node:child_process\";\nimport { existsSync, readFileSync, mkdirSync, copyFileSync, readdirSync, statSync } from \"node:fs\";\nimport http from \"node:http\";\nimport { join } from \"node:path\";\nimport { homedir, tmpdir } from \"node:os\";\nimport { createConnection } from \"node:net\";\n\n// ---------------------------------------------------------------------------\n// Types\n// ---------------------------------------------------------------------------\n\nexport interface ConnectOptions {\n /** CDP port override (normally read from DevToolsActivePort) */\n port?: number;\n /** If true, auto-launch Chrome with --remote-debugging-port when not connected */\n autoLaunch?: boolean;\n /** If true, launch Chrome in headless mode (no visible window) */\n headless?: boolean;\n}\n\nexport interface ConnectResult {\n /** Whether connection to Chrome succeeded */\n success: boolean;\n /** Port Chrome is listening on */\n port: number;\n /** Full WebSocket endpoint URL */\n wsEndpoint?: string;\n /** Whether DevToolsActivePort file was found */\n activePortFound: boolean;\n /** Error message if connection failed */\n error?: string;\n}\n\n// ---------------------------------------------------------------------------\n// Well-known Chrome paths per platform\n// ---------------------------------------------------------------------------\n\nconst CHROME_PATHS: Record<string, string[]> = {\n darwin: [\n \"/Applications/Google Chrome.app/Contents/MacOS/Google Chrome\",\n \"/Applications/Google Chrome Canary.app/Contents/MacOS/Google Chrome Canary\",\n \"/Applications/Chromium.app/Contents/MacOS/Chromium\",\n \"/Applications/Microsoft Edge.app/Contents/MacOS/Microsoft Edge\",\n \"/Applications/Brave Browser.app/Contents/MacOS/Brave Browser\",\n ],\n linux: [\n \"google-chrome\",\n \"google-chrome-stable\",\n \"chromium\",\n \"chromium-browser\",\n \"microsoft-edge\",\n \"brave-browser\",\n ],\n win32: [\n \"C:\\\\Program Files\\\\Google\\\\Chrome\\\\Application\\\\chrome.exe\",\n \"C:\\\\Program Files (x86)\\\\Google\\\\Chrome\\\\Application\\\\chrome.exe\",\n \"C:\\\\Program Files\\\\Microsoft\\\\Edge\\\\Application\\\\msedge.exe\",\n \"C:\\\\Program Files\\\\BraveSoftware\\\\Brave-Browser\\\\Application\\\\brave.exe\",\n ],\n};\n\n// ---------------------------------------------------------------------------\n// Default Chrome data directory per platform\n// ---------------------------------------------------------------------------\n\nexport function getDefaultChromeDataDir(): string {\n const home = homedir();\n switch (process.platform) {\n case \"darwin\":\n return join(home, \"Library\", \"Application Support\", \"Google\", \"Chrome\");\n case \"win32\":\n return join(home, \"AppData\", \"Local\", \"Google\", \"Chrome\", \"User Data\");\n default: // linux\n return join(home, \".config\", \"google-chrome\");\n }\n}\n\n// ---------------------------------------------------------------------------\n// Find Chrome\n// ---------------------------------------------------------------------------\n\nexport function findChrome(): string | null {\n const platform = process.platform;\n const candidates = CHROME_PATHS[platform] ?? [];\n\n for (const candidate of candidates) {\n if (platform === \"darwin\" || platform === \"win32\") {\n if (existsSync(candidate)) return candidate;\n } else {\n try {\n const result = execSync(`which ${candidate}`, { stdio: \"pipe\" });\n const path = result.toString().trim();\n if (path) return path;\n } catch {\n // try next\n }\n }\n }\n return null;\n}\n\n// ---------------------------------------------------------------------------\n// Port check\n// ---------------------------------------------------------------------------\n\nexport function isPortReachable(port: number, host = \"127.0.0.1\"): Promise<boolean> {\n return new Promise((resolve) => {\n const socket = createConnection({ port, host });\n socket.setTimeout(2000);\n socket.on(\"connect\", () => { socket.end(); resolve(true); });\n socket.on(\"error\", () => { socket.destroy(); resolve(false); });\n socket.on(\"timeout\", () => { socket.destroy(); resolve(false); });\n });\n}\n\n/**\n * Verifies CDP is truly usable by hitting /json/version.\n * Chrome's M144 approach (chrome://inspect) opens the port but returns 404 on /json/version\n * and 403 on WebSocket connections. Only --remote-debugging-port gives real CDP access.\n */\nexport function isCDPHealthy(port: number, host = \"127.0.0.1\"): Promise<boolean> {\n return new Promise((resolve) => {\n const req = http.get(`http://${host}:${port}/json/version`, (res) => {\n resolve(res.statusCode === 200);\n res.resume();\n });\n req.setTimeout(3000, () => { req.destroy(); resolve(false); });\n req.on(\"error\", () => resolve(false));\n });\n}\n\n// ---------------------------------------------------------------------------\n// DevToolsActivePort reader\n// ---------------------------------------------------------------------------\n\nexport interface ActivePortInfo {\n /** The debug port Chrome is listening on */\n port: number;\n /** The WebSocket path (e.g. /devtools/browser/...) */\n wsPath: string;\n /** Full WebSocket endpoint: ws://127.0.0.1:{port}{wsPath} */\n wsEndpoint: string;\n}\n\n/**\n * Reads the DevToolsActivePort file from Chrome's data directory.\n *\n * Chrome writes this file when remote debugging is enabled via\n * chrome://inspect/#remote-debugging (Chrome M144+).\n *\n * File format:\n * Line 1: port number (e.g. \"9222\")\n * Line 2: WebSocket path (e.g. \"/devtools/browser/abc-123\")\n *\n * @param chromeDataDir - Override Chrome data dir (for testing)\n */\nexport function readDevToolsActivePort(chromeDataDir?: string): ActivePortInfo | null {\n const dataDir = chromeDataDir ?? getDefaultChromeDataDir();\n const portFile = join(dataDir, \"DevToolsActivePort\");\n\n if (!existsSync(portFile)) {\n return null;\n }\n\n try {\n const content = readFileSync(portFile, \"utf-8\");\n const lines = content.split(\"\\n\").map(l => l.trim()).filter(l => l.length > 0);\n\n if (lines.length < 2) return null;\n\n const port = parseInt(lines[0]!, 10);\n const wsPath = lines[1]!;\n\n if (isNaN(port) || !wsPath.startsWith(\"/\")) return null;\n\n return {\n port,\n wsPath,\n wsEndpoint: `ws://127.0.0.1:${port}${wsPath}`,\n };\n } catch {\n return null;\n }\n}\n\n// ---------------------------------------------------------------------------\n// Open chrome://inspect to guide user\n// ---------------------------------------------------------------------------\n\n/**\n * Opens chrome://inspect/#remote-debugging in the user's Chrome.\n * On macOS uses `open`, on Linux uses `xdg-open`, on Windows uses `start`.\n */\nexport function openChromeInspect(): boolean {\n const url = \"chrome://inspect/#remote-debugging\";\n try {\n if (process.platform === \"darwin\") {\n execSync(`open -a \"Google Chrome\" \"${url}\"`, { stdio: \"pipe\", timeout: 5000 });\n } else if (process.platform === \"win32\") {\n execSync(`start chrome \"${url}\"`, { stdio: \"pipe\", timeout: 5000 });\n } else {\n execSync(`google-chrome \"${url}\" || chromium \"${url}\"`, { stdio: \"pipe\", timeout: 5000 });\n }\n return true;\n } catch {\n return false;\n }\n}\n\n// ---------------------------------------------------------------------------\n// Launch Chrome with remote debugging (zero modals)\n// ---------------------------------------------------------------------------\n\nexport interface LaunchResult {\n success: boolean;\n port: number;\n wsEndpoint?: string;\n error?: string;\n}\n\n/**\n * Checks if Chrome is currently running.\n */\nexport function isChromeRunning(): boolean {\n try {\n if (process.platform === \"win32\") {\n const r = execSync('tasklist /FI \"IMAGENAME eq chrome.exe\" /NH', { stdio: \"pipe\" }).toString();\n return r.includes(\"chrome.exe\");\n }\n const r = execSync(\"pgrep -x 'Google Chrome' || pgrep -x chrome || pgrep -x chromium\", { stdio: \"pipe\" }).toString().trim();\n return r.length > 0;\n } catch {\n return false;\n }\n}\n\n/**\n * Quits Chrome gracefully, waits for it to fully exit.\n * Falls back to force kill if graceful quit doesn't work (e.g. macOS session restore).\n */\nexport async function quitChrome(): Promise<void> {\n try {\n if (process.platform === \"darwin\") {\n execSync('osascript -e \\'tell application \"Google Chrome\" to quit\\'', { stdio: \"pipe\", timeout: 5000 });\n } else if (process.platform === \"win32\") {\n execSync(\"taskkill /IM chrome.exe\", { stdio: \"pipe\", timeout: 5000 });\n } else {\n execSync(\"pkill -TERM chrome || pkill -TERM chromium\", { stdio: \"pipe\", timeout: 5000 });\n }\n } catch {\n // May not be running\n }\n\n // Wait for Chrome to fully exit (up to 3 seconds)\n for (let i = 0; i < 15; i++) {\n if (!isChromeRunning()) break;\n await new Promise(r => setTimeout(r, 200));\n }\n\n // Force kill if still running (macOS session restore can relaunch Chrome)\n if (isChromeRunning()) {\n try {\n if (process.platform === \"win32\") {\n execSync(\"taskkill /F /IM chrome.exe\", { stdio: \"pipe\", timeout: 5000 });\n } else {\n execSync(\"pkill -9 'Google Chrome' || pkill -9 chrome || pkill -9 chromium\", { stdio: \"pipe\", timeout: 5000 });\n }\n } catch {\n // best effort\n }\n\n // Wait for force kill to take effect\n for (let i = 0; i < 15; i++) {\n if (!isChromeRunning()) break;\n await new Promise(r => setTimeout(r, 200));\n }\n }\n\n // Small delay for profile lock release\n await new Promise(r => setTimeout(r, 500));\n}\n\nconst SEPARATE_PORT = 9444;\n\n/**\n * Launches Chrome with --remote-debugging-port to avoid the M144 \"Allow?\" modal.\n *\n * NEVER quits the user's running Chrome. If Chrome is already running without\n * CDP, a separate instance is launched with a temp profile + cookie sync\n * (same strategy as headless, but with a visible window).\n *\n * @param port - CDP port (default 9222)\n * @returns LaunchResult with wsEndpoint if successful\n */\nexport async function launchChromeWithDebugging(port = 9222, headless = false): Promise<LaunchResult> {\n // Already healthy (launched with --remote-debugging-port)? Don't touch Chrome.\n const healthy = await isCDPHealthy(port);\n if (healthy) {\n const ws = await getWsEndpoint(port);\n return { success: true, port, wsEndpoint: ws };\n }\n\n // Check if a separate browsirai instance is already running\n const sepHealthy = await isCDPHealthy(SEPARATE_PORT);\n if (sepHealthy) {\n const ws = await getWsEndpoint(SEPARATE_PORT);\n return { success: true, port: SEPARATE_PORT, wsEndpoint: ws };\n }\n\n const chromePath = findChrome();\n if (!chromePath) {\n return { success: false, port, error: \"Chrome not found. Install Chrome and try again.\" };\n }\n\n // If Chrome is running without CDP, launch a SEPARATE instance.\n // NEVER quit the user's Chrome — their tabs, work, and session are sacred.\n const usesSeparateInstance = isChromeRunning();\n const targetPort = usesSeparateInstance ? SEPARATE_PORT : port;\n\n const dataDir = usesSeparateInstance\n ? join(tmpdir(), \"browsirai-normal\")\n : undefined; // use default Chrome profile when no Chrome is running\n\n if (dataDir) {\n mkdirSync(dataDir, { recursive: true });\n syncCookiesToHeadless(dataDir); // reuse cookie sync for the separate instance\n }\n\n const args = [\n `--remote-debugging-port=${targetPort}`,\n \"--remote-allow-origins=*\",\n ];\n\n if (dataDir) {\n args.push(`--user-data-dir=${dataDir}`, \"--no-first-run\", \"--no-default-browser-check\", \"--disable-extensions\");\n }\n\n if (headless) {\n args.push(\"--headless=new\");\n }\n\n const child = spawn(chromePath, args, {\n detached: true,\n stdio: \"ignore\",\n });\n child.unref();\n\n // Wait for CDP to become healthy (up to 15 seconds)\n for (let i = 0; i < 75; i++) {\n await new Promise(r => setTimeout(r, 200));\n const ok = await isCDPHealthy(targetPort);\n if (ok) {\n const ws = await getWsEndpoint(targetPort);\n return { success: true, port: targetPort, wsEndpoint: ws };\n }\n }\n\n return {\n success: false,\n port: targetPort,\n error: \"Chrome launched but CDP port not reachable after 15s. Check if another Chrome instance is blocking the profile.\",\n };\n}\n\n// ---------------------------------------------------------------------------\n// Headless Chrome — separate instance, doesn't touch user's Chrome\n// ---------------------------------------------------------------------------\n\nconst HEADLESS_PORT = 9333;\n\n/**\n * Fetches the webSocketDebuggerUrl from /json/version.\n */\nasync function getWsEndpoint(port: number): Promise<string | undefined> {\n return new Promise((resolve) => {\n const req = http.get(`http://127.0.0.1:${port}/json/version`, (res) => {\n let body = \"\";\n res.on(\"data\", (c: Buffer) => { body += c.toString(); });\n res.on(\"end\", () => {\n try {\n const data = JSON.parse(body) as { webSocketDebuggerUrl?: string };\n resolve(data.webSocketDebuggerUrl);\n } catch { resolve(undefined); }\n });\n });\n req.setTimeout(3000, () => { req.destroy(); resolve(undefined); });\n req.on(\"error\", () => resolve(undefined));\n });\n}\n\n// ---------------------------------------------------------------------------\n// Cookie sync state — tracks last sync for navigate-hook resync detection\n// ---------------------------------------------------------------------------\n\ninterface CookieSyncState {\n profileName: string;\n cookieMtime: number;\n}\n\nlet cookieSyncState: CookieSyncState | null = null;\n\n/**\n * Returns the current cookie sync state (profile name + cookie file mtime).\n * Returns null if no sync has been performed yet.\n */\nexport function getCookieSyncState(): CookieSyncState | null {\n return cookieSyncState;\n}\n\n/**\n * Checks if cookies need re-syncing by comparing current cookie file mtime\n * and active profile against the last sync state.\n * Returns true if: cookie file modified, profile switched, or no prior sync.\n * Returns false if: nothing changed or Chrome data dir doesn't exist.\n */\nexport function needsCookieResync(chromeDataDir?: string): boolean {\n if (!cookieSyncState) return false; // no prior sync → nothing to compare\n\n const dataDir = chromeDataDir ?? getDefaultChromeDataDir();\n const localStatePath = join(dataDir, \"Local State\");\n if (!existsSync(localStatePath)) return false;\n\n try {\n const localState = JSON.parse(readFileSync(localStatePath, \"utf-8\")) as {\n profile?: { last_used?: string };\n };\n const profileName = localState.profile?.last_used ?? \"Default\";\n\n // Profile changed?\n if (profileName !== cookieSyncState.profileName) return true;\n\n // Cookie file mtime changed?\n const cookiePath = join(dataDir, profileName, \"Cookies\");\n if (!existsSync(cookiePath)) return false;\n const mtime = statSync(cookiePath).mtimeMs;\n return mtime !== cookieSyncState.cookieMtime;\n } catch {\n return false;\n }\n}\n\n/**\n * Detects the user's active Chrome profile, copies cookies to the dest profile,\n * and tracks sync state (mtime + profile name) for later resync detection.\n */\nexport function syncCookiesAndTrack(destDataDir: string, chromeDataDir?: string): void {\n const dataDir = chromeDataDir ?? getDefaultChromeDataDir();\n try {\n const localStatePath = join(dataDir, \"Local State\");\n if (!existsSync(localStatePath)) return;\n\n const localState = JSON.parse(readFileSync(localStatePath, \"utf-8\")) as {\n profile?: { last_used?: string };\n };\n const profileName = localState.profile?.last_used ?? \"Default\";\n const srcProfileDir = join(dataDir, profileName);\n\n if (!existsSync(join(srcProfileDir, \"Cookies\"))) return;\n\n // Ensure Default profile dir exists in dest data dir\n const destProfileDir = join(destDataDir, \"Default\");\n mkdirSync(destProfileDir, { recursive: true });\n\n // Copy all Cookies-related files\n const files = readdirSync(srcProfileDir).filter(f => f.startsWith(\"Cookies\"));\n for (const file of files) {\n copyFileSync(join(srcProfileDir, file), join(destProfileDir, file));\n }\n\n // Track sync state\n const mtime = statSync(join(srcProfileDir, \"Cookies\")).mtimeMs;\n cookieSyncState = { profileName, cookieMtime: mtime };\n } catch {\n // Best-effort — don't fail launch\n }\n}\n\n/** @deprecated Use syncCookiesAndTrack instead. Kept for internal compatibility. */\nfunction syncCookiesToHeadless(headlessDataDir: string): void {\n syncCookiesAndTrack(headlessDataDir);\n}\n\n/**\n * Launches a separate headless Chrome on port 9333 with a temp profile.\n * Does NOT quit or affect the user's running Chrome.\n */\nexport async function launchHeadlessChrome(): Promise<LaunchResult> {\n // Already running?\n const healthy = await isCDPHealthy(HEADLESS_PORT);\n if (healthy) {\n const ws = await getWsEndpoint(HEADLESS_PORT);\n return { success: true, port: HEADLESS_PORT, wsEndpoint: ws };\n }\n\n const chromePath = findChrome();\n if (!chromePath) {\n return { success: false, port: HEADLESS_PORT, error: \"Chrome not found.\" };\n }\n\n const dataDir = join(tmpdir(), \"browsirai-headless\");\n mkdirSync(dataDir, { recursive: true });\n\n // Copy user's cookies to headless profile before launch\n syncCookiesToHeadless(dataDir);\n\n const child = spawn(chromePath, [\n \"--headless=new\",\n `--remote-debugging-port=${HEADLESS_PORT}`,\n \"--remote-allow-origins=*\",\n `--user-data-dir=${dataDir}`,\n \"--no-first-run\",\n \"--no-default-browser-check\",\n \"--disable-extensions\",\n \"--disable-gpu\",\n ], {\n detached: true,\n stdio: \"ignore\",\n });\n child.unref();\n\n // Wait for CDP to become healthy\n for (let i = 0; i < 75; i++) {\n await new Promise(r => setTimeout(r, 200));\n if (await isCDPHealthy(HEADLESS_PORT)) {\n const ws = await getWsEndpoint(HEADLESS_PORT);\n return { success: true, port: HEADLESS_PORT, wsEndpoint: ws };\n }\n }\n\n return { success: false, port: HEADLESS_PORT, error: \"Headless Chrome did not start in 15s.\" };\n}\n\n// ---------------------------------------------------------------------------\n// Connect to Chrome\n// ---------------------------------------------------------------------------\n\n/**\n * Connects to Chrome via CDP.\n *\n * Strategy:\n * 1. Try DevToolsActivePort (Chrome already has debugging enabled)\n * 2. Try manual port override or default port 9222\n * 3. If autoLaunch is true, quit Chrome and relaunch with --remote-debugging-port\n *\n * @returns ConnectResult with wsEndpoint if successful\n */\nexport async function connectChrome(options: ConnectOptions = {}): Promise<ConnectResult> {\n const targetPort = options.port ?? 9222;\n\n // 1. Try DevToolsActivePort first (must be CDP-healthy, not just TCP-reachable)\n const activePort = readDevToolsActivePort();\n\n if (activePort) {\n const healthy = await isCDPHealthy(activePort.port);\n if (healthy) {\n return {\n success: true,\n port: activePort.port,\n wsEndpoint: activePort.wsEndpoint,\n activePortFound: true,\n };\n }\n }\n\n // 2. Try port directly (must be CDP-healthy to avoid M144 modal)\n const healthy = await isCDPHealthy(targetPort);\n if (healthy) {\n return {\n success: true,\n port: targetPort,\n activePortFound: false,\n };\n }\n\n // 3. Auto-launch if enabled\n if (options.autoLaunch) {\n const launch = await launchChromeWithDebugging(targetPort, options.headless);\n if (launch.success) {\n return {\n success: true,\n port: launch.port,\n wsEndpoint: launch.wsEndpoint,\n activePortFound: false,\n };\n }\n return {\n success: false,\n port: targetPort,\n activePortFound: false,\n error: launch.error,\n };\n }\n\n // 4. Not connected\n return {\n success: false,\n port: targetPort,\n activePortFound: activePort !== null,\n error: \"Chrome remote debugging is not enabled. Enable it at chrome://inspect/#remote-debugging\",\n };\n}\n","/**\n * CDP Connection — WebSocket-based Chrome DevTools Protocol client.\n *\n * Handles JSON-RPC command/response correlation, CDP event dispatch,\n * timeouts, reconnection on crash, and clean shutdown.\n *\n * @module\n */\n\nexport { waitForDocumentReady } from \"./wait-ready.js\";\n\n// ---------------------------------------------------------------------------\n// Constants\n// ---------------------------------------------------------------------------\n\n/** Default command timeout in milliseconds. */\nexport const TIMEOUT = 15_000;\n\n/** Navigation timeout in milliseconds. */\nexport const NAVIGATION_TIMEOUT = 30_000;\n\n/** Idle timeout in milliseconds (20 minutes). */\nexport const IDLE_TIMEOUT = 1_200_000;\n\n/** Maximum reconnection retries when connecting via daemon. */\nexport const DAEMON_CONNECT_RETRIES = 20;\n\n/** Delay between daemon connection retries in milliseconds. */\nexport const DAEMON_CONNECT_DELAY = 300;\n\n// ---------------------------------------------------------------------------\n// Types\n// ---------------------------------------------------------------------------\n\n/** Options for sending a CDP command. */\nexport interface CDPCommandOptions {\n /** Timeout in ms for this specific command. */\n timeout?: number;\n /** Session ID for target-scoped commands. */\n sessionId?: string;\n}\n\n/** Internal pending command tracker. */\ninterface PendingCommand {\n resolve: (result: unknown) => void;\n reject: (error: Error) => void;\n method: string;\n timer: ReturnType<typeof setTimeout>;\n}\n\n/** Shape of a parsed incoming CDP message. */\ninterface CDPMessage {\n id?: number;\n method?: string;\n params?: Record<string, unknown>;\n result?: unknown;\n error?: { code: number; message: string };\n}\n\n// ---------------------------------------------------------------------------\n// Minimal WebSocket interface (Node.js 22 built-in + browser compatible)\n// ---------------------------------------------------------------------------\n\ninterface MinimalWebSocket {\n readyState: number;\n send(data: string): void;\n close(): void;\n addEventListener(event: string, handler: (...args: unknown[]) => void): void;\n removeEventListener(event: string, handler: (...args: unknown[]) => void): void;\n}\n\n// ---------------------------------------------------------------------------\n// CDPConnection\n// ---------------------------------------------------------------------------\n\ntype EventHandler = (...args: unknown[]) => void;\n\n/**\n * WebSocket-based CDP client.\n *\n * ```ts\n * const conn = new CDPConnection(\"ws://127.0.0.1:9222/devtools/browser/abc\");\n * await conn.connect();\n * const result = await conn.send(\"Target.getTargets\");\n * conn.close();\n * ```\n */\nexport class CDPConnection {\n private readonly wsUrl: string;\n private ws: MinimalWebSocket | null = null;\n private nextId = 1;\n private readonly pending = new Map<number, PendingCommand>();\n private readonly eventHandlers = new Map<string, Set<EventHandler>>();\n private closed = false;\n private reconnecting = false;\n private _connected = false;\n\n // Bound listener references for cleanup\n private boundOnMessage: ((...args: unknown[]) => void) | null = null;\n private boundOnClose: ((...args: unknown[]) => void) | null = null;\n private boundOnError: ((...args: unknown[]) => void) | null = null;\n\n constructor(wsUrl: string) {\n this.wsUrl = wsUrl;\n }\n\n // -----------------------------------------------------------------------\n // Public API\n // -----------------------------------------------------------------------\n\n /** Whether the underlying WebSocket is currently open. */\n get isConnected(): boolean {\n return this._connected;\n }\n\n /**\n * Open the WebSocket connection.\n * Resolves on the `open` event; rejects on `error`.\n */\n async connect(): Promise<void> {\n // Use native WebSocket (Node 22+) or fall back to `ws` package\n // eslint-disable-next-line @typescript-eslint/no-explicit-any\n let WS = (globalThis as any).WebSocket;\n if (!WS) {\n try {\n // Dynamic import so it's not required when native WebSocket exists\n const wsModule = await import(\"ws\");\n WS = wsModule.default ?? wsModule.WebSocket ?? wsModule;\n } catch {\n throw new Error(\n \"No WebSocket implementation found. Install the `ws` package or use Node 22+.\"\n );\n }\n }\n const ws: MinimalWebSocket = new WS(this.wsUrl);\n this.ws = ws;\n\n await new Promise<void>((resolve, reject) => {\n const onOpen = () => {\n ws.removeEventListener(\"open\", onOpen);\n ws.removeEventListener(\"error\", onError);\n resolve();\n };\n\n const onError = (ev: unknown) => {\n ws.removeEventListener(\"open\", onOpen);\n ws.removeEventListener(\"error\", onError);\n const msg =\n (ev as { message?: string })?.message ?? \"WebSocket error\";\n reject(new Error(msg));\n };\n\n ws.addEventListener(\"open\", onOpen);\n ws.addEventListener(\"error\", onError);\n });\n\n this._connected = true;\n this.attachListeners(ws);\n }\n\n /**\n * Send a CDP command and await its result.\n *\n * @param method CDP method (e.g. `\"Runtime.evaluate\"`)\n * @param params Optional method parameters\n * @param options Optional timeout / sessionId overrides\n */\n send(\n method: string,\n params?: Record<string, unknown>,\n options?: CDPCommandOptions,\n ): Promise<unknown> {\n if (this.closed || !this._connected || !this.ws) {\n return Promise.reject(\n new Error(`Connection closed — cannot send ${method}`),\n );\n }\n\n const id = this.nextId++;\n const timeout = options?.timeout ?? TIMEOUT;\n\n // Build the JSON-RPC message\n const message: Record<string, unknown> = { id, method };\n if (params !== undefined) {\n message.params = params;\n }\n if (options?.sessionId !== undefined) {\n message.sessionId = options.sessionId;\n }\n\n this.ws.send(JSON.stringify(message));\n\n return new Promise<unknown>((resolve, reject) => {\n const timer = setTimeout(() => {\n this.pending.delete(id);\n reject(new Error(`CDP command timeout: ${method} (${timeout}ms)`));\n }, timeout);\n\n this.pending.set(id, { resolve, reject, method, timer });\n });\n }\n\n /**\n * Register an event handler for a CDP event or lifecycle event.\n *\n * CDP events are dispatched as `handler(params, { method })`.\n * Lifecycle events: `disconnected`, `browserCrashed`, `reconnected`,\n * `reconnectionFailed`.\n */\n on(event: string, handler: EventHandler): void {\n let handlers = this.eventHandlers.get(event);\n if (!handlers) {\n handlers = new Set();\n this.eventHandlers.set(event, handlers);\n }\n handlers.add(handler);\n }\n\n /** Remove a previously registered event handler. */\n off(event: string, handler: EventHandler): void {\n this.eventHandlers.get(event)?.delete(handler);\n }\n\n /**\n * Close the connection. Suppresses reconnection.\n * Safe to call multiple times.\n */\n close(): void {\n this.closed = true;\n this._connected = false;\n\n this.rejectAllPending(new Error(\"Connection closed\"));\n\n if (this.ws) {\n this.detachListeners(this.ws);\n try {\n this.ws.close();\n } catch {\n // Already closed or errored — ignore.\n }\n this.ws = null;\n }\n }\n\n // -----------------------------------------------------------------------\n // Private\n // -----------------------------------------------------------------------\n\n /** Attach message / close / error listeners to the WebSocket. */\n private attachListeners(ws: MinimalWebSocket): void {\n this.boundOnMessage = (event: unknown) => {\n const data = (event as { data?: string })?.data;\n if (typeof data !== \"string\") return;\n\n let msg: CDPMessage;\n try {\n msg = JSON.parse(data) as CDPMessage;\n } catch {\n return;\n }\n\n // --- Command response (has `id`) ---\n if (msg.id !== undefined) {\n const entry = this.pending.get(msg.id);\n if (entry) {\n this.pending.delete(msg.id);\n clearTimeout(entry.timer);\n if (msg.error) {\n entry.reject(new Error(msg.error.message));\n } else {\n entry.resolve(msg.result);\n }\n }\n return;\n }\n\n // --- CDP event (has `method`, no `id`) ---\n if (msg.method) {\n this.emit(msg.method, msg.params ?? {}, { method: msg.method });\n }\n };\n\n this.boundOnClose = (event: unknown) => {\n const code = (event as { code?: number })?.code ?? 1006;\n this._connected = false;\n\n // Reject all in-flight commands\n this.rejectAllPending(new Error(\"WebSocket disconnected — connection closed\"));\n\n // Abnormal close → browser crash\n if (code !== 1000) {\n this.emit(\"browserCrashed\");\n }\n\n // Always emit disconnected\n this.emit(\"disconnected\");\n\n // Reconnect on abnormal close unless user called close()\n if (!this.closed && code !== 1000) {\n this.attemptReconnection().catch(() => {\n // Swallow — reconnectionFailed event already emitted\n });\n }\n };\n\n this.boundOnError = () => {\n // Errors during an established connection surface via the close event.\n };\n\n ws.addEventListener(\"message\", this.boundOnMessage);\n ws.addEventListener(\"close\", this.boundOnClose);\n ws.addEventListener(\"error\", this.boundOnError);\n }\n\n /** Detach WebSocket listeners. */\n private detachListeners(ws: MinimalWebSocket): void {\n if (this.boundOnMessage) ws.removeEventListener(\"message\", this.boundOnMessage);\n if (this.boundOnClose) ws.removeEventListener(\"close\", this.boundOnClose);\n if (this.boundOnError) ws.removeEventListener(\"error\", this.boundOnError);\n }\n\n /** Reject every pending command. */\n private rejectAllPending(error: Error): void {\n for (const [id, entry] of this.pending) {\n clearTimeout(entry.timer);\n entry.reject(error);\n this.pending.delete(id);\n }\n }\n\n /** Dispatch an event to all registered handlers. */\n private emit(event: string, ...args: unknown[]): void {\n const handlers = this.eventHandlers.get(event);\n if (!handlers) return;\n for (const handler of handlers) {\n try {\n handler(...args);\n } catch {\n // Swallow handler errors.\n }\n }\n }\n\n /** Attempt reconnection with retries after abnormal close. */\n private async attemptReconnection(): Promise<void> {\n if (this.reconnecting || this.closed) return;\n this.reconnecting = true;\n\n for (let attempt = 0; attempt < DAEMON_CONNECT_RETRIES; attempt++) {\n if (this.closed) {\n this.reconnecting = false;\n return;\n }\n\n await this.delay(DAEMON_CONNECT_DELAY);\n\n if (this.closed) {\n this.reconnecting = false;\n return;\n }\n\n try {\n await this.connect();\n this.reconnecting = false;\n this.emit(\"reconnected\");\n return;\n } catch {\n // Will retry on next iteration.\n }\n }\n\n this.reconnecting = false;\n this.emit(\"reconnectionFailed\");\n }\n\n /** Promise-based delay. */\n private delay(ms: number): Promise<void> {\n return new Promise((resolve) => setTimeout(resolve, ms));\n }\n}\n"],"mappings":"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","names":["r","healthy"]}
package/dist/cli.js CHANGED
@@ -4601,6 +4601,8 @@ __export(doctor_exports, {
4601
4601
  });
4602
4602
  import { execSync as execSync3 } from "child_process";
4603
4603
  import { existsSync as existsSync4, readFileSync as readFileSync4 } from "fs";
4604
+ import { homedir as homedir4 } from "os";
4605
+ import { join as join4 } from "path";
4604
4606
  function findChromePath() {
4605
4607
  const whichCommands = process.platform === "win32" ? ["where chrome", "where chromium", "where msedge"] : ["which google-chrome", "which chromium", "which chromium-browser", "which chrome"];
4606
4608
  for (const cmd of whichCommands) {
@@ -4637,41 +4639,52 @@ function checkNodeVersion() {
4637
4639
  message: ok ? `v${versionStr} (>= 18 required)` : `v${versionStr} \u2014 Node.js >= 18 is required`
4638
4640
  };
4639
4641
  }
4640
- function checkPlatformConfig() {
4641
- const detection = detectPlatform();
4642
- const config = getInstallConfig(detection.platform);
4643
- const configPath = config.configPath;
4644
- if (!existsSync4(configPath)) {
4645
- return {
4646
- ok: false,
4647
- label: "Platform config",
4648
- message: `Config file not found: ${configPath} (platform: ${detection.platform})`
4649
- };
4650
- }
4642
+ function resolvePath(p) {
4643
+ if (p.startsWith("~/")) return join4(homedir4(), p.slice(2));
4644
+ return p;
4645
+ }
4646
+ function checkConfigFile(filePath, configKey, platform) {
4647
+ const resolved = resolvePath(filePath);
4648
+ if (!existsSync4(resolved)) return null;
4651
4649
  try {
4652
- const content = readFileSync4(configPath, "utf-8");
4650
+ const content = readFileSync4(resolved, "utf-8");
4653
4651
  const parsed = JSON.parse(content);
4654
- const servers = parsed[config.configKey];
4652
+ const servers = parsed[configKey];
4655
4653
  if (servers && typeof servers === "object" && "browsirai" in servers) {
4656
4654
  return {
4657
4655
  ok: true,
4658
4656
  label: "Platform config",
4659
- message: `browsirai found in ${configPath} (platform: ${detection.platform})`
4657
+ message: `browsirai found in ${resolved} (platform: ${platform})`
4660
4658
  };
4661
4659
  }
4662
- return {
4663
- ok: false,
4664
- label: "Platform config",
4665
- message: `browsirai not found in ${configPath} under "${config.configKey}"`
4666
- };
4660
+ return null;
4667
4661
  } catch {
4668
- return {
4669
- ok: false,
4670
- label: "Platform config",
4671
- message: `Failed to parse config file: ${configPath}`
4672
- };
4662
+ return null;
4673
4663
  }
4674
4664
  }
4665
+ function checkPlatformConfig() {
4666
+ const detection = detectPlatform();
4667
+ const config = getInstallConfig(detection.platform);
4668
+ const detected = checkConfigFile(config.configPath, config.configKey, detection.platform);
4669
+ if (detected) return detected;
4670
+ const knownPaths = [
4671
+ { path: "~/.mcp.json", key: "mcpServers", platform: "claude-code (global)" },
4672
+ { path: ".mcp.json", key: "mcpServers", platform: "claude-code" },
4673
+ { path: ".cursor/mcp.json", key: "mcpServers", platform: "cursor" },
4674
+ { path: "~/.gemini/settings.json", key: "mcpServers", platform: "gemini-cli" },
4675
+ { path: ".vscode/mcp.json", key: "servers", platform: "vscode-copilot" },
4676
+ { path: "~/.codeium/windsurf/mcp_config.json", key: "mcpServers", platform: "windsurf" }
4677
+ ];
4678
+ for (const entry of knownPaths) {
4679
+ const result = checkConfigFile(entry.path, entry.key, entry.platform);
4680
+ if (result) return result;
4681
+ }
4682
+ return {
4683
+ ok: false,
4684
+ label: "Platform config",
4685
+ message: `browsirai not found in any known config file (detected: ${detection.platform})`
4686
+ };
4687
+ }
4675
4688
  async function runDoctor() {
4676
4689
  const checks = [];
4677
4690
  const method = getInstallMethod();
@@ -4749,13 +4762,13 @@ __export(install_exports, {
4749
4762
  import { intro, select, confirm, spinner, outro, isCancel, cancel, log } from "@clack/prompts";
4750
4763
  import { existsSync as existsSync5, readFileSync as readFileSync5, writeFileSync as writeFileSync3, mkdirSync as mkdirSync4 } from "fs";
4751
4764
  import { resolve, dirname as dirname2 } from "path";
4752
- import { homedir as homedir4 } from "os";
4765
+ import { homedir as homedir5 } from "os";
4753
4766
  function resolveConfigPath(configPath, scope) {
4754
4767
  if (configPath.startsWith("~")) {
4755
- return resolve(homedir4(), configPath.slice(2));
4768
+ return resolve(homedir5(), configPath.slice(2));
4756
4769
  }
4757
4770
  if (scope === "global") {
4758
- return resolve(homedir4(), configPath);
4771
+ return resolve(homedir5(), configPath);
4759
4772
  }
4760
4773
  return resolve(process.cwd(), configPath);
4761
4774
  }
@@ -4767,7 +4780,7 @@ async function runInstall() {
4767
4780
  const config2 = getInstallConfig(opt.value);
4768
4781
  const paths = [
4769
4782
  resolve(process.cwd(), config2.configPath),
4770
- config2.configPath.startsWith("~") ? resolve(homedir4(), config2.configPath.slice(2)) : resolve(homedir4(), config2.configPath)
4783
+ config2.configPath.startsWith("~") ? resolve(homedir5(), config2.configPath.slice(2)) : resolve(homedir5(), config2.configPath)
4771
4784
  ];
4772
4785
  for (const filePath2 of paths) {
4773
4786
  if (existsSync5(filePath2)) {
@@ -4900,21 +4913,9 @@ async function runCli(args) {
4900
4913
  break;
4901
4914
  }
4902
4915
  default: {
4903
- console.error(`Unknown command: ${command}`);
4904
- console.log(
4905
- [
4906
- `browsirai v${VERSION}`,
4907
- "",
4908
- "Usage: browsirai [command]",
4909
- "",
4910
- "Commands:",
4911
- " (none) Start the MCP server (default)",
4912
- " doctor Run diagnostics",
4913
- " install Install browser integration",
4914
- " --version Print version",
4915
- ""
4916
- ].join("\n")
4917
- );
4916
+ const cliUrl = new URL("./cli/run.js", import.meta.url);
4917
+ const { runCLI } = await import(cliUrl.href);
4918
+ await runCLI(args);
4918
4919
  break;
4919
4920
  }
4920
4921
  }