browsirai 0.2.0 → 0.2.1
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.
- package/LICENSE +661 -78
- package/README.md +60 -5
- package/dist/bin.js +4 -2
- package/dist/bin.js.map +1 -1
- package/dist/cli/commands/act.js.map +1 -1
- package/dist/cli/commands/nav.js.map +1 -1
- package/dist/cli/commands/net.js.map +1 -1
- package/dist/cli/commands/obs.js.map +1 -1
- package/dist/cli/run.js +2 -1
- package/dist/cli/run.js.map +1 -1
- package/dist/cli.js +4 -2
- package/dist/cli.js.map +1 -1
- package/dist/server.js +4 -2
- package/dist/server.js.map +1 -1
- package/package.json +2 -2
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../../../src/cli/commands/obs.ts","../../../src/cli/run.ts","../../../src/chrome-launcher.ts","../../../src/tools/browser-snapshot.ts","../../../src/tools/browser-screenshot.ts","../../../src/tools/browser-html.ts","../../../src/tools/browser-eval.ts","../../../src/tools/browser-find.ts","../../../src/tools/browser-inspect-source.ts","../../../src/event-buffer.ts","../../../src/redactor.ts","../../../src/tools/browser-console-messages.ts","../../../src/tools/browser-network-requests.ts"],"sourcesContent":["/**\n * Observation CLI commands for browsirai.\n *\n * Commands: snapshot, screenshot, html, eval, find, source, console, network\n *\n * @module cli/commands/obs\n */\n\nimport { writeFileSync } from \"node:fs\";\nimport type { CLICommand } from \"../types.js\";\nimport { parseFlags } from \"../run.js\";\nimport { browserSnapshot } from \"../../tools/browser-snapshot.js\";\nimport { browserScreenshot } from \"../../tools/browser-screenshot.js\";\nimport { browserHtml } from \"../../tools/browser-html.js\";\nimport { browserEval } from \"../../tools/browser-eval.js\";\nimport { browserFind } from \"../../tools/browser-find.js\";\nimport { browserInspectSource } from \"../../tools/browser-inspect-source.js\";\nimport { browserConsoleMessages } from \"../../tools/browser-console-messages.js\";\nimport { browserNetworkRequests } from \"../../tools/browser-network-requests.js\";\n\n// ---------------------------------------------------------------------------\n// snapshot\n// ---------------------------------------------------------------------------\n\nconst snapshotCommand: CLICommand = {\n name: \"snapshot\",\n description: \"Capture the accessibility tree of the page\",\n usage: \"browsirai snapshot [-i] [-c] [-d N] [-s selector]\",\n run: async (cdp, args) => {\n const flags = parseFlags(args);\n\n const params: Record<string, unknown> = {};\n if (flags.compact === \"true\" || flags.c === \"true\") params.compact = true;\n if (flags.interactive === \"true\" || flags.i === \"true\") params.interactive = true;\n if (flags.selector) params.selector = flags.selector;\n if (flags.s) params.selector = flags.s;\n if (flags.depth) params.depth = parseInt(flags.depth, 10);\n if (flags.d) params.depth = parseInt(flags.d, 10);\n\n const result = await browserSnapshot(cdp, params);\n\n if (result.snapshot) {\n console.log(result.snapshot);\n } else {\n console.log(\"(empty snapshot)\");\n }\n\n if (result.truncated) {\n console.log(`\\n(truncated — ${result.totalElements} total elements)`);\n }\n },\n};\n\n// ---------------------------------------------------------------------------\n// screenshot\n// ---------------------------------------------------------------------------\n\nconst screenshotCommand: CLICommand = {\n name: \"screenshot\",\n description: \"Capture a screenshot of the page\",\n usage: \"browsirai screenshot [-o file.png] [--fullPage] [--selector=...] [--format=png]\",\n run: async (cdp, args) => {\n const flags = parseFlags(args);\n\n const params: Record<string, unknown> = {};\n if (flags.fullPage === \"true\") params.fullPage = true;\n if (flags.selector) params.selector = flags.selector;\n if (flags.format) params.format = flags.format;\n\n const result = await browserScreenshot(cdp, params);\n\n const output = flags.output ?? flags.o;\n if (output) {\n const buffer = Buffer.from(result.base64, \"base64\");\n writeFileSync(output, buffer);\n console.log(`Screenshot saved to ${output} (${buffer.length} bytes)`);\n } else {\n // Estimate dimensions from base64 data length\n const sizeKB = Math.round((result.base64.length * 3) / 4 / 1024);\n console.log(`Screenshot taken (~${sizeKB}KB)`);\n console.log(\"Use --output=file.png to save to disk\");\n }\n },\n};\n\n// ---------------------------------------------------------------------------\n// html\n// ---------------------------------------------------------------------------\n\nconst htmlCommand: CLICommand = {\n name: \"html\",\n description: \"Retrieve page or element HTML\",\n usage: \"browsirai html [--selector=...]\",\n run: async (cdp, args) => {\n const flags = parseFlags(args);\n\n const params: { selector?: string } = {};\n if (flags.selector) params.selector = flags.selector;\n\n const result = await browserHtml(cdp, params);\n\n if (result.error) {\n console.error(`Error: ${result.error}`);\n process.exit(1);\n }\n\n console.log(result.html);\n },\n};\n\n// ---------------------------------------------------------------------------\n// eval\n// ---------------------------------------------------------------------------\n\nconst evalCommand: CLICommand = {\n name: \"eval\",\n description: \"Evaluate a JavaScript expression in the browser\",\n usage: 'browsirai eval \"<expression>\"',\n run: async (cdp, args) => {\n const flags = parseFlags(args);\n\n // First positional arg is the JS expression\n const expression = flags._0;\n if (!expression) {\n console.error(\"Error: Missing expression argument\");\n console.error('Usage: browsirai eval \"<expression>\"');\n process.exit(1);\n }\n\n const result = await browserEval(cdp, { expression });\n\n if (result.error) {\n console.error(`Error: ${result.error}`);\n process.exit(1);\n }\n\n if (result.result === undefined) {\n console.log(\"undefined\");\n } else if (result.result === null) {\n console.log(\"null\");\n } else if (typeof result.result === \"object\") {\n console.log(JSON.stringify(result.result, null, 2));\n } else {\n console.log(String(result.result));\n }\n },\n};\n\n// ---------------------------------------------------------------------------\n// find\n// ---------------------------------------------------------------------------\n\nconst findCommand: CLICommand = {\n name: \"find\",\n description: \"Find elements by ARIA role, name, or text\",\n usage: \"browsirai find [--role=button] [--name=...] [--text=...]\",\n run: async (cdp, args) => {\n const flags = parseFlags(args);\n\n const params: Record<string, unknown> = {};\n if (flags.role) params.role = flags.role;\n if (flags.name) params.name = flags.name;\n if (flags.text) params.text = flags.text;\n if (flags.nth) params.nth = parseInt(flags.nth, 10);\n\n const result = await browserFind(cdp, params);\n\n if (result.found) {\n console.log(`Found ${result.ref} (role: ${result.role}, name: \"${result.name}\")`);\n if (result.count > 1) {\n console.log(` ${result.count} total matches`);\n }\n } else {\n console.log(\"Not found\");\n if (result.count > 0) {\n console.log(` ${result.count} matches exist but index out of range`);\n }\n }\n },\n};\n\n// ---------------------------------------------------------------------------\n// source\n// ---------------------------------------------------------------------------\n\nconst sourceCommand: CLICommand = {\n name: \"source\",\n description: \"Inspect source code location of an element\",\n usage: \"browsirai source [--ref=@e5] [--selector=h1]\",\n run: async (cdp, args) => {\n const flags = parseFlags(args);\n\n const params: { ref?: string; selector?: string } = {};\n if (flags.ref) params.ref = flags.ref;\n if (flags.selector) params.selector = flags.selector;\n\n if (!params.ref && !params.selector) {\n console.error(\"Error: Either --ref or --selector is required\");\n console.error(\"Usage: browsirai source [--ref=@e5] [--selector=h1]\");\n process.exit(1);\n }\n\n const result = await browserInspectSource(cdp, params);\n\n if (result.source) {\n const loc = result.source;\n const file = loc.filePath ?? \"(unknown file)\";\n const line = loc.lineNumber != null ? `:${loc.lineNumber}` : \"\";\n const col = loc.columnNumber != null ? `:${loc.columnNumber}` : \"\";\n const component = result.componentName ?? loc.componentName ?? \"(anonymous)\";\n console.log(`Component: ${component}`);\n console.log(`Source: ${file}${line}${col}`);\n } else {\n console.log(`Tag: <${result.tagName}>`);\n if (result.componentName) {\n console.log(`Component: ${result.componentName}`);\n }\n console.log(\"Source: not found (dev mode may not be enabled)\");\n }\n\n if (result.stack.length > 0) {\n console.log(\"\\nComponent stack:\");\n for (const loc of result.stack) {\n const name = loc.componentName ?? \"(anonymous)\";\n const line = loc.lineNumber != null ? `:${loc.lineNumber}` : \"\";\n console.log(` ${name} -> ${loc.filePath}${line}`);\n }\n }\n },\n};\n\n// ---------------------------------------------------------------------------\n// console\n// ---------------------------------------------------------------------------\n\nconst consoleCommand: CLICommand = {\n name: \"console\",\n description: \"View captured console messages\",\n usage: \"browsirai console [--level=error] [--limit=20]\",\n run: async (cdp, args) => {\n const flags = parseFlags(args);\n\n const params: Record<string, unknown> = {};\n if (flags.level) params.level = flags.level;\n if (flags.limit) params.limit = parseInt(flags.limit, 10);\n\n const result = await browserConsoleMessages(cdp, params);\n\n if (result.messages.length === 0) {\n console.log(\"No console messages captured\");\n return;\n }\n\n for (const msg of result.messages) {\n const level = msg.level.toUpperCase().padEnd(5);\n const ts = msg.timestamp\n ? new Date(msg.timestamp).toISOString().slice(11, 23)\n : \"\";\n const prefix = ts ? `[${ts}] ${level}` : level;\n console.log(`${prefix} ${msg.text}`);\n }\n\n console.log(`\\n(${result.messages.length} messages)`);\n },\n};\n\n// ---------------------------------------------------------------------------\n// network\n// ---------------------------------------------------------------------------\n\nconst networkCommand: CLICommand = {\n name: \"network\",\n description: \"View captured network requests\",\n usage: \"browsirai network [--filter=*api*] [--limit=10] [--includeHeaders]\",\n run: async (cdp, args) => {\n const flags = parseFlags(args);\n\n const params: Record<string, unknown> = {};\n if (flags.filter) params.filter = flags.filter;\n if (flags.limit) params.limit = parseInt(flags.limit, 10);\n if (flags.includeHeaders === \"true\") params.includeHeaders = true;\n\n const result = await browserNetworkRequests(cdp, params);\n\n if (result.requests.length === 0) {\n console.log(\"No network requests captured\");\n return;\n }\n\n for (const req of result.requests) {\n const status = req.status != null ? String(req.status) : \"...\";\n const type = req.type ? `[${req.type}]` : \"\";\n console.log(`${req.method.padEnd(6)} ${status.padEnd(3)} ${type.padEnd(10)} ${req.url}`);\n }\n\n console.log(`\\n(${result.requests.length} requests)`);\n },\n};\n\n// ---------------------------------------------------------------------------\n// Export\n// ---------------------------------------------------------------------------\n\nexport const obsCommands: CLICommand[] = [\n snapshotCommand,\n screenshotCommand,\n htmlCommand,\n evalCommand,\n findCommand,\n sourceCommand,\n consoleCommand,\n networkCommand,\n];\n","/**\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 * browser_snapshot tool — captures the accessibility tree and returns a\n * formatted text representation with @eN refs for each element.\n *\n * Exports:\n * - browserSnapshot(cdp, params?) — main entry point\n * - shouldShowAxNode(node, options?) — filtering predicate\n * - processAccessibilityTree(nodes, options?) — tree formatter\n *\n * @module browser-snapshot\n */\n\nimport type { CDPConnection } from \"../cdp/connection\";\n\n// ---------------------------------------------------------------------------\n// Types\n// ---------------------------------------------------------------------------\n\n/** Shape of an accessibility tree node from CDP. */\ninterface AXNode {\n nodeId: string;\n backendDOMNodeId?: number;\n role?: { type: string; value: string };\n name?: { type: string; value: string };\n description?: { type: string; value: string };\n value?: { type: string; value: string } | null;\n properties?: Array<{\n name: string;\n value: { type: string; value: unknown };\n }>;\n parentId?: string;\n children?: Array<{ nodeId: string; backendDOMNodeId?: number }>;\n childIds?: string[];\n}\n\n/** Options controlling which nodes appear in the snapshot. */\nexport interface SnapshotOptions {\n /** Only show interactive elements (button, link, textbox, etc.). */\n interactive?: boolean;\n /** Include elements with cursor:pointer. */\n cursor?: boolean;\n /** Compact mode — hides InlineTextBox and empty structural wrappers. */\n compact?: boolean;\n /** Maximum tree depth to include. */\n depth?: number;\n /** CSS selector to scope the snapshot. */\n selector?: string;\n}\n\n/** Options for processAccessibilityTree. */\nexport interface ProcessTreeOptions {\n maxDepth?: number;\n}\n\n/** Result returned by browserSnapshot. */\nexport interface SnapshotResult {\n snapshot: string;\n truncated?: boolean;\n totalElements?: number;\n}\n\n// ---------------------------------------------------------------------------\n// Interactive role set\n// ---------------------------------------------------------------------------\n\nconst INTERACTIVE_ROLES = new Set([\n \"button\",\n \"link\",\n \"textbox\",\n \"checkbox\",\n \"radio\",\n \"combobox\",\n \"listbox\",\n \"menuitem\",\n \"menuitemcheckbox\",\n \"menuitemradio\",\n \"option\",\n \"searchbox\",\n \"slider\",\n \"spinbutton\",\n \"switch\",\n \"tab\",\n \"treeitem\",\n]);\n\n// ---------------------------------------------------------------------------\n// shouldShowAxNode\n// ---------------------------------------------------------------------------\n\n/**\n * Determines whether an AX node should be included in the snapshot output.\n *\n * Filtering rules (from tests):\n * 1. role='none' → false\n * 2. role='generic' with empty name → false\n * 3. role='InlineTextBox' in compact mode → false\n * 4. Empty name AND empty/null value → false\n * 5. Otherwise → true\n */\nexport function shouldShowAxNode(\n node: AXNode,\n options?: { compact?: boolean },\n): boolean {\n const role = node.role?.value ?? \"\";\n\n // Rule 1: role='none'\n if (role === \"none\") {\n return false;\n }\n\n // Rule 2: role='generic' with empty name\n if (role === \"generic\") {\n const name = node.name?.value ?? \"\";\n if (!name) {\n return false;\n }\n }\n\n // Rule 3: InlineTextBox in compact mode\n if (options?.compact && role === \"InlineTextBox\") {\n return false;\n }\n\n // Rule 4: Empty name AND empty/null value\n const name = node.name?.value ?? \"\";\n const value = node.value?.value ?? null;\n if (!name && (value === null || value === undefined || value === \"\")) {\n return false;\n }\n\n return true;\n}\n\n// ---------------------------------------------------------------------------\n// processAccessibilityTree\n// ---------------------------------------------------------------------------\n\n/**\n * Processes a flat array of AX nodes into a formatted, indented text tree.\n *\n * - Uses 2-space indentation per depth level.\n * - Caps traversal at maxDepth (default 10).\n * - Prevents cycles via a visited set.\n * - Orders children via childIds[] when present, falling back to children[].\n */\nexport function processAccessibilityTree(\n nodes: AXNode[],\n options: ProcessTreeOptions,\n): string {\n const maxDepth = options.maxDepth ?? 10;\n\n // Build lookup map: nodeId -> AXNode\n const nodeMap = new Map<string, AXNode>();\n for (const node of nodes) {\n nodeMap.set(node.nodeId, node);\n }\n\n // Find root node (first node without parentId, or first node)\n let root: AXNode | undefined;\n for (const node of nodes) {\n if (!node.parentId) {\n root = node;\n break;\n }\n }\n\n if (!root) {\n return \"\";\n }\n\n const lines: string[] = [];\n const visited = new Set<string>();\n\n function traverse(node: AXNode, depth: number): void {\n // Cycle prevention\n if (visited.has(node.nodeId)) {\n return;\n }\n visited.add(node.nodeId);\n\n // Depth cap\n if (depth > maxDepth) {\n return;\n }\n\n // Format node line\n const indent = \" \".repeat(depth);\n const role = node.role?.value ?? \"unknown\";\n const name = node.name?.value ?? \"\";\n const attrs = formatNodeAttributes(node);\n\n let line = `${indent}${role}`;\n if (name) {\n line += ` \"${name}\"`;\n }\n if (attrs) {\n line += ` ${attrs}`;\n }\n lines.push(line);\n\n // Resolve children: prefer childIds, fall back to children\n const childNodeIds = getChildNodeIds(node);\n\n for (const childId of childNodeIds) {\n const childNode = nodeMap.get(childId);\n if (childNode) {\n traverse(childNode, depth + 1);\n }\n }\n }\n\n traverse(root, 0);\n\n return lines.join(\"\\n\");\n}\n\n// ---------------------------------------------------------------------------\n// Helper: get child node IDs\n// ---------------------------------------------------------------------------\n\nfunction getChildNodeIds(node: AXNode): string[] {\n if (node.childIds && node.childIds.length > 0) {\n return node.childIds;\n }\n if (node.children && node.children.length > 0) {\n return node.children.map((c) => c.nodeId);\n }\n return [];\n}\n\n// ---------------------------------------------------------------------------\n// Helper: format node attributes\n// ---------------------------------------------------------------------------\n\nfunction formatNodeAttributes(node: AXNode): string {\n const parts: string[] = [];\n\n // Value\n if (node.value?.value !== undefined && node.value.value !== \"\") {\n parts.push(`value=\"${node.value.value}\"`);\n }\n\n // Description\n if (node.description?.value) {\n parts.push(`description=\"${node.description.value}\"`);\n }\n\n // Properties\n if (node.properties) {\n for (const prop of node.properties) {\n switch (prop.name) {\n case \"level\":\n parts.push(`level=${prop.value.value}`);\n break;\n case \"checked\": {\n const val = prop.value.value;\n if (val === true || val === \"true\" || val === \"mixed\") {\n parts.push(\"checked\");\n }\n break;\n }\n case \"selected\":\n if (prop.value.value === true) {\n parts.push(\"selected\");\n }\n break;\n case \"expanded\":\n if (prop.value.value === true || prop.value.value === \"true\") {\n parts.push(\"expanded\");\n } else {\n parts.push(\"collapsed\");\n }\n break;\n default:\n // Skip other properties\n break;\n }\n }\n }\n\n return parts.join(\" \");\n}\n\n// ---------------------------------------------------------------------------\n// browserSnapshot\n// ---------------------------------------------------------------------------\n\n/**\n * Takes an accessibility tree snapshot from the page, assigns @eN refs,\n * and returns formatted text output.\n *\n * @param cdp - CDP connection\n * @param params - Snapshot options\n * @returns SnapshotResult with snapshot text\n */\nexport async function browserSnapshot(\n cdp: CDPConnection,\n params?: Record<string, unknown>,\n): Promise<SnapshotResult> {\n const options: SnapshotOptions = {\n interactive: params?.interactive as boolean | undefined,\n cursor: params?.cursor as boolean | undefined,\n compact: params?.compact as boolean | undefined,\n depth: params?.depth as number | undefined,\n selector: params?.selector as string | undefined,\n };\n\n // Enable accessibility\n await cdp.send(\"Accessibility.enable\");\n\n // Get the accessibility tree\n let axNodes: AXNode[];\n\n if (options.selector) {\n // Scoped snapshot via CSS selector\n const docResult = (await cdp.send(\"DOM.getDocument\")) as {\n root: { nodeId: number };\n };\n const queryResult = (await cdp.send(\"DOM.querySelector\", {\n nodeId: docResult.root.nodeId,\n selector: options.selector,\n })) as { nodeId: number };\n\n if (queryResult.nodeId === 0) {\n return { snapshot: `No element found for selector: ${options.selector}` };\n }\n\n const partialResult = (await cdp.send(\"Accessibility.getPartialAXTree\", {\n nodeId: queryResult.nodeId,\n fetchRelatives: true,\n })) as { nodes: AXNode[] };\n axNodes = partialResult.nodes;\n } else {\n // Full page snapshot\n const fullResult = (await cdp.send(\"Accessibility.getFullAXTree\")) as {\n nodes: AXNode[];\n };\n axNodes = fullResult.nodes;\n }\n\n // Count non-root elements (exclude WebArea root)\n const totalElements = axNodes.filter(\n (n) => n.role?.value !== \"WebArea\",\n ).length;\n\n // Build a node map for tree traversal\n const nodeMap = new Map<string, AXNode>();\n for (const node of axNodes) {\n nodeMap.set(node.nodeId, node);\n }\n\n // Find root node\n let root: AXNode | undefined;\n for (const node of axNodes) {\n if (!node.parentId) {\n root = node;\n break;\n }\n }\n\n if (!root) {\n // If no root found, use first node\n root = axNodes[0];\n }\n\n if (!root) {\n return { snapshot: \"\" };\n }\n\n // Traverse the tree, filter, assign refs, and format\n const lines: string[] = [];\n let refCounter = 0;\n const visited = new Set<string>();\n const maxDepth = options.depth ?? 100;\n\n function traverse(node: AXNode, depth: number): void {\n // Cycle prevention\n if (visited.has(node.nodeId)) {\n return;\n }\n visited.add(node.nodeId);\n\n // Depth limit\n if (depth > maxDepth) {\n return;\n }\n\n const role = node.role?.value ?? \"\";\n const isRoot = role === \"WebArea\";\n\n // Apply filters\n const showNode = isRoot || shouldShow(node, options);\n\n if (showNode && !isRoot) {\n refCounter++;\n // Use backendDOMNodeId as ref so tools can resolve it directly\n const ref = node.backendDOMNodeId ? `@e${node.backendDOMNodeId}` : `@e${refCounter}`;\n const indent = \" \".repeat(depth);\n const name = node.name?.value ?? \"\";\n const attrs = formatNodeAttributes(node);\n\n let line = `${indent}${ref} ${role}`;\n if (name) {\n line += ` \"${name}\"`;\n }\n if (attrs) {\n line += ` ${attrs}`;\n }\n lines.push(line);\n }\n\n // Process children\n const childNodeIds = getChildNodeIds(node);\n const nextDepth = isRoot ? depth : depth + 1;\n\n for (const childId of childNodeIds) {\n const childNode = nodeMap.get(childId);\n if (childNode) {\n traverse(childNode, nextDepth);\n }\n }\n }\n\n traverse(root, 0);\n\n const snapshot = lines.join(\"\\n\");\n const result: SnapshotResult = { snapshot };\n\n // Add truncation info for large pages\n if (totalElements > 1000) {\n result.truncated = true;\n result.totalElements = totalElements;\n }\n\n return result;\n}\n\n// ---------------------------------------------------------------------------\n// Internal show filter (combines shouldShowAxNode with interactive/cursor)\n// ---------------------------------------------------------------------------\n\nfunction shouldShow(node: AXNode, options: SnapshotOptions): boolean {\n // Base filtering via shouldShowAxNode\n if (!shouldShowAxNode(node, { compact: options.compact })) {\n // In compact mode, skip filtered nodes entirely.\n // In normal mode, also skip.\n return false;\n }\n\n const role = node.role?.value ?? \"\";\n\n // Interactive filter: only show interactive roles\n if (options.interactive) {\n if (INTERACTIVE_ROLES.has(role)) {\n return true;\n }\n // If cursor mode is also on, check for cursor:pointer property\n if (options.cursor) {\n return hasCursorPointer(node);\n }\n return false;\n }\n\n // Cursor filter: include elements with cursor:pointer even if role is generic\n if (options.cursor) {\n if (hasCursorPointer(node)) {\n return true;\n }\n }\n\n return true;\n}\n\nfunction hasCursorPointer(node: AXNode): boolean {\n if (!node.properties) return false;\n return node.properties.some(\n (p) => p.name === \"cursor\" && p.value.value === \"pointer\",\n );\n}\n","/**\n * browser_screenshot tool — captures viewport or full-page screenshots via CDP.\n *\n * Supports:\n * - Viewport / full-page capture (Page.captureScreenshot)\n * - DPR detection cascade (3-level fallback)\n * - Element screenshot by CSS selector or @eN ref\n * - Annotated screenshots with ref labels\n * - Custom format (png/jpeg) and quality\n *\n * @module browser-screenshot\n */\nimport type { CDPConnection } from \"../cdp/connection\";\n\n// ---------------------------------------------------------------------------\n// Types\n// ---------------------------------------------------------------------------\n\ninterface ScreenshotParams {\n /** Capture the full scrollable page, not just the viewport. */\n fullPage?: boolean;\n /** Screenshot format. Default: \"png\". */\n format?: \"png\" | \"jpeg\";\n /** JPEG quality (0-100). Only applies when format is \"jpeg\". */\n quality?: number;\n /** CSS selector to screenshot a specific element. */\n selector?: string;\n /** @eN ref to screenshot a specific element. */\n ref?: string;\n /** Annotate interactive elements with numbered labels. */\n annotate?: boolean;\n}\n\ninterface Annotation {\n /** The @eN ref string, e.g. \"@e1\" */\n ref: string;\n /** The numbered label on the screenshot, e.g. \"[1]\" */\n label: string;\n /** Accessibility role */\n role?: string;\n /** Accessible name */\n name?: string;\n}\n\ninterface ScreenshotResult {\n /** Base64-encoded screenshot data. */\n base64: string;\n /** Annotation labels when `annotate: true`. */\n annotations?: Annotation[];\n}\n\n// ---------------------------------------------------------------------------\n// DPR Detection Cascade\n// ---------------------------------------------------------------------------\n\n/**\n * Detect device pixel ratio using a 3-level cascade:\n * Level 1: Page.getLayoutMetrics (visualViewport vs cssVisualViewport)\n * Level 2: Emulation.getDeviceMetricsOverride (deviceScaleFactor)\n * Level 3: Runtime.evaluate(\"window.devicePixelRatio\")\n * Default: 1\n */\nasync function detectDPR(cdp: CDPConnection): Promise<number> {\n // Level 1: Page.getLayoutMetrics\n try {\n const metrics = (await cdp.send(\"Page.getLayoutMetrics\", {})) as {\n visualViewport?: { clientWidth: number };\n cssVisualViewport?: { clientWidth: number };\n layoutViewport?: { clientWidth: number };\n contentSize?: { width: number };\n };\n\n if (metrics.visualViewport && metrics.cssVisualViewport) {\n const physicalWidth = metrics.visualViewport.clientWidth;\n const cssWidth = metrics.cssVisualViewport.clientWidth;\n if (cssWidth > 0 && physicalWidth > 0) {\n const dpr = physicalWidth / cssWidth;\n if (dpr >= 1) {\n return dpr;\n }\n }\n }\n } catch {\n // Level 1 failed, try Level 2\n }\n\n // Level 2: Runtime.evaluate\n try {\n const evalResult = (await cdp.send(\"Runtime.evaluate\", {\n expression: \"window.devicePixelRatio\",\n returnByValue: true,\n })) as {\n result: { type: string; value: unknown };\n };\n\n if (evalResult.result.type === \"number\" && typeof evalResult.result.value === \"number\") {\n return evalResult.result.value;\n }\n } catch {\n // Level 3 failed, use default\n }\n\n // Default: DPR = 1\n return 1;\n}\n\n// ---------------------------------------------------------------------------\n// Element bounding box\n// ---------------------------------------------------------------------------\n\ninterface BoundingBox {\n x: number;\n y: number;\n width: number;\n height: number;\n}\n\n/**\n * Get the bounding box of an element by CSS selector.\n */\nasync function getElementBoxBySelector(\n cdp: CDPConnection,\n selector: string,\n): Promise<BoundingBox> {\n const doc = (await cdp.send(\"DOM.getDocument\", {})) as {\n root: { nodeId: number };\n };\n\n const queryResult = (await cdp.send(\"DOM.querySelector\", {\n nodeId: doc.root.nodeId,\n selector,\n })) as { nodeId: number };\n\n if (!queryResult.nodeId) {\n throw new Error(`Element not found: ${selector}`);\n }\n\n const boxModel = (await cdp.send(\"DOM.getBoxModel\", {\n nodeId: queryResult.nodeId,\n })) as {\n model: { content: number[]; width: number; height: number };\n };\n\n const content = boxModel.model.content;\n // content is [x1,y1, x2,y2, x3,y3, x4,y4] (quad)\n const x = content[0];\n const y = content[1];\n const width = content[2] - content[0];\n const height = content[5] - content[1];\n\n return { x, y, width, height };\n}\n\n/**\n * Get the bounding box of an element by @eN ref (backendNodeId).\n */\nasync function getElementBoxByRef(\n cdp: CDPConnection,\n ref: string,\n): Promise<BoundingBox> {\n // Parse @eN → extract numeric ref index\n const match = /^@e(\\d+)$/.exec(ref);\n if (!match) {\n throw new Error(`Invalid ref format: ${ref}`);\n }\n\n const backendNodeId = parseInt(match[1], 10);\n\n // Resolve the backendNodeId to a remote object\n const resolved = (await cdp.send(\"DOM.resolveNode\", {\n backendNodeId,\n })) as { object: { objectId: string } };\n\n // Get the box model for the node\n const boxModel = (await cdp.send(\"DOM.getBoxModel\", {\n backendNodeId,\n })) as {\n model: { content: number[]; width: number; height: number };\n };\n\n const content = boxModel.model.content;\n const x = content[0];\n const y = content[1];\n const width = content[2] - content[0];\n const height = content[5] - content[1];\n\n return { x, y, width, height };\n}\n\n// ---------------------------------------------------------------------------\n// Annotated screenshot\n// ---------------------------------------------------------------------------\n\n/**\n * Build annotations from the accessibility tree for interactive elements.\n */\nasync function buildAnnotations(cdp: CDPConnection): Promise<Annotation[]> {\n const axTree = (await cdp.send(\"Accessibility.getFullAXTree\", {}, { timeout: 10000 })) as {\n nodes: Array<{\n nodeId: string;\n backendDOMNodeId?: number;\n role?: { type: string; value: string };\n name?: { type: string; value: string };\n }>;\n };\n\n const annotations: Annotation[] = [];\n let counter = 0;\n\n for (const node of axTree.nodes) {\n const role = node.role?.value;\n\n // Skip the root WebArea node\n if (role === \"WebArea\") {\n continue;\n }\n\n counter++;\n // Use backendDOMNodeId as ref so tools can resolve it directly\n const ref = node.backendDOMNodeId ? `@e${node.backendDOMNodeId}` : `@e${counter}`;\n const label = `[${counter}]`;\n\n annotations.push({\n ref,\n label,\n role: role ?? \"unknown\",\n name: node.name?.value ?? \"\",\n });\n }\n\n return annotations;\n}\n\n// ---------------------------------------------------------------------------\n// Main export\n// ---------------------------------------------------------------------------\n\n/**\n * Capture a screenshot of the browser page or a specific element.\n *\n * @param cdp - CDP connection.\n * @param params - Screenshot parameters.\n * @returns Base64-encoded screenshot data and optional annotations.\n */\nexport async function browserScreenshot(\n cdp: CDPConnection,\n params: ScreenshotParams,\n): Promise<ScreenshotResult> {\n const format = params.format ?? \"png\";\n\n // Build the captureScreenshot parameters\n const captureParams: Record<string, unknown> = {\n format,\n };\n\n // Quality only applies to jpeg\n if (format === \"jpeg\" && params.quality !== undefined) {\n captureParams.quality = params.quality;\n }\n\n // Detect DPR (needed for various calculations)\n const _dpr = await detectDPR(cdp);\n\n // Element screenshot by selector\n if (params.selector) {\n const box = await getElementBoxBySelector(cdp, params.selector);\n captureParams.clip = {\n x: box.x,\n y: box.y,\n width: box.width,\n height: box.height,\n scale: 1,\n };\n }\n\n // Element screenshot by @eN ref\n if (params.ref) {\n const box = await getElementBoxByRef(cdp, params.ref);\n captureParams.clip = {\n x: box.x,\n y: box.y,\n width: box.width,\n height: box.height,\n scale: 1,\n };\n }\n\n // Full page screenshot\n if (params.fullPage) {\n const metrics = (await cdp.send(\"Page.getLayoutMetrics\", {})) as {\n contentSize: { width: number; height: number };\n cssContentSize?: { width: number; height: number };\n layoutViewport: { clientWidth: number; clientHeight: number };\n };\n\n const contentWidth = metrics.cssContentSize?.width ?? metrics.contentSize.width;\n const contentHeight = metrics.cssContentSize?.height ?? metrics.contentSize.height;\n\n captureParams.clip = {\n x: 0,\n y: 0,\n width: contentWidth,\n height: contentHeight,\n scale: 1,\n };\n }\n\n // Capture the screenshot\n const screenshot = (await cdp.send(\"Page.captureScreenshot\", captureParams)) as {\n data: string;\n };\n\n const result: ScreenshotResult = {\n base64: screenshot.data,\n };\n\n // Annotated screenshot\n if (params.annotate) {\n result.annotations = await buildAnnotations(cdp);\n }\n\n return result;\n}\n","/**\n * browser_html tool — retrieves page or element HTML via CDP.\n *\n * Supports:\n * - Full page HTML (document.documentElement.outerHTML)\n * - Element HTML by CSS selector (DOM.querySelector + DOM.getOuterHTML)\n * - Graceful handling of missing selectors\n *\n * @module browser-html\n */\nimport type { CDPConnection } from \"../cdp/connection\";\n\n// ---------------------------------------------------------------------------\n// Types\n// ---------------------------------------------------------------------------\n\ninterface HtmlParams {\n /** CSS selector to retrieve outerHTML for a specific element. */\n selector?: string;\n}\n\ninterface HtmlResult {\n /** The retrieved HTML string. */\n html: string;\n /** Error message when element is not found. */\n error?: string;\n}\n\ninterface MarkdownParams {\n /** CSS selector to scope markdown extraction. */\n selector?: string;\n}\n\ninterface MarkdownResult {\n /** Extracted markdown content. */\n markdown: string;\n}\n\n// ---------------------------------------------------------------------------\n// HTML retrieval\n// ---------------------------------------------------------------------------\n\n/**\n * Retrieve the outer HTML of the page or a specific element.\n *\n * Without a selector, returns document.documentElement.outerHTML via\n * Runtime.evaluate.\n *\n * With a selector, uses DOM.getDocument → DOM.querySelector → DOM.getOuterHTML.\n *\n * @param cdp - CDP connection.\n * @param params - Optional selector parameter.\n * @returns The HTML string, or an error if the element is not found.\n */\nexport async function browserHtml(\n cdp: CDPConnection,\n params: HtmlParams,\n): Promise<HtmlResult> {\n // No selector: return full page HTML\n if (!params.selector) {\n const evalResult = (await cdp.send(\"Runtime.evaluate\", {\n expression: \"document.documentElement.outerHTML\",\n returnByValue: true,\n })) as {\n result: { type: string; value: string };\n };\n\n return { html: evalResult.result.value };\n }\n\n // With selector: use DOM methods\n const doc = (await cdp.send(\"DOM.getDocument\", {})) as {\n root: { nodeId: number };\n };\n\n const queryResult = (await cdp.send(\"DOM.querySelector\", {\n nodeId: doc.root.nodeId,\n selector: params.selector,\n })) as { nodeId: number };\n\n // nodeId 0 means element not found\n if (!queryResult.nodeId) {\n return {\n html: \"\",\n error: `Element not found: ${params.selector}`,\n };\n }\n\n const outerResult = (await cdp.send(\"DOM.getOuterHTML\", {\n nodeId: queryResult.nodeId,\n })) as { outerHTML: string };\n\n return { html: outerResult.outerHTML };\n}\n\n// ---------------------------------------------------------------------------\n// HTML → Markdown conversion\n// ---------------------------------------------------------------------------\n\n/**\n * Minimal HTML-to-Markdown converter.\n *\n * Handles:\n * - Headings (h1-h6) → # syntax\n * - Code blocks (<pre><code>) → fenced ``` blocks\n * - Tables → markdown table syntax with separator row\n * - Navigation/sidebar exclusion (<nav>, <aside>)\n * - Paragraphs → plain text with newlines\n */\nfunction htmlToMarkdown(html: string): string {\n let content = html;\n\n // Strip <nav> and <aside> elements and their contents\n content = content.replace(/<nav\\b[^>]*>[\\s\\S]*?<\\/nav>/gi, \"\");\n content = content.replace(/<aside\\b[^>]*>[\\s\\S]*?<\\/aside>/gi, \"\");\n\n const lines: string[] = [];\n\n // Process code blocks first (before stripping tags)\n content = content.replace(\n /<pre[^>]*>\\s*<code(?:\\s+class=\"language-(\\w+)\")?[^>]*>([\\s\\S]*?)<\\/code>\\s*<\\/pre>/gi,\n (_match, lang, code) => {\n const language = lang ?? \"\";\n const decoded = decodeHtmlEntities(code.trim());\n return `\\n\\`\\`\\`${language}\\n${decoded}\\n\\`\\`\\`\\n`;\n },\n );\n\n // Process tables\n content = content.replace(\n /<table[^>]*>([\\s\\S]*?)<\\/table>/gi,\n (_match, tableContent: string) => {\n const rows: string[][] = [];\n\n // Extract thead rows\n const theadMatch = tableContent.match(/<thead[^>]*>([\\s\\S]*?)<\\/thead>/i);\n if (theadMatch) {\n const headerRows = extractTableRows(theadMatch[1]);\n rows.push(...headerRows);\n }\n\n // Extract tbody rows\n const tbodyMatch = tableContent.match(/<tbody[^>]*>([\\s\\S]*?)<\\/tbody>/i);\n if (tbodyMatch) {\n const bodyRows = extractTableRows(tbodyMatch[1]);\n // Insert separator after header\n if (rows.length > 0 && bodyRows.length > 0) {\n const colCount = rows[0].length;\n const separator = Array(colCount).fill(\"---\");\n rows.push(separator);\n }\n rows.push(...bodyRows);\n }\n\n // If no thead/tbody, extract all rows directly\n if (!theadMatch && !tbodyMatch) {\n const allRows = extractTableRows(tableContent);\n if (allRows.length > 1) {\n const colCount = allRows[0].length;\n const separator = Array(colCount).fill(\"---\");\n const result = [allRows[0], separator, ...allRows.slice(1)];\n return \"\\n\" + result.map((r) => \"| \" + r.join(\" | \") + \" |\").join(\"\\n\") + \"\\n\";\n }\n return \"\\n\" + allRows.map((r) => \"| \" + r.join(\" | \") + \" |\").join(\"\\n\") + \"\\n\";\n }\n\n return \"\\n\" + rows.map((r) => \"| \" + r.join(\" | \") + \" |\").join(\"\\n\") + \"\\n\";\n },\n );\n\n // Process headings\n content = content.replace(/<h1[^>]*>([\\s\\S]*?)<\\/h1>/gi, (_m, text) => `\\n# ${stripTags(text).trim()}\\n`);\n content = content.replace(/<h2[^>]*>([\\s\\S]*?)<\\/h2>/gi, (_m, text) => `\\n## ${stripTags(text).trim()}\\n`);\n content = content.replace(/<h3[^>]*>([\\s\\S]*?)<\\/h3>/gi, (_m, text) => `\\n### ${stripTags(text).trim()}\\n`);\n content = content.replace(/<h4[^>]*>([\\s\\S]*?)<\\/h4>/gi, (_m, text) => `\\n#### ${stripTags(text).trim()}\\n`);\n content = content.replace(/<h5[^>]*>([\\s\\S]*?)<\\/h5>/gi, (_m, text) => `\\n##### ${stripTags(text).trim()}\\n`);\n content = content.replace(/<h6[^>]*>([\\s\\S]*?)<\\/h6>/gi, (_m, text) => `\\n###### ${stripTags(text).trim()}\\n`);\n\n // Process paragraphs\n content = content.replace(/<p[^>]*>([\\s\\S]*?)<\\/p>/gi, (_m, text) => `\\n${stripTags(text).trim()}\\n`);\n\n // Strip remaining HTML tags\n content = stripTags(content);\n\n // Decode HTML entities\n content = decodeHtmlEntities(content);\n\n // Normalize whitespace: collapse multiple blank lines\n content = content.replace(/\\n{3,}/g, \"\\n\\n\");\n\n return content.trim();\n}\n\n/**\n * Extract rows from an HTML table section.\n */\nfunction extractTableRows(html: string): string[][] {\n const rows: string[][] = [];\n const rowRegex = /<tr[^>]*>([\\s\\S]*?)<\\/tr>/gi;\n let rowMatch: RegExpExecArray | null;\n\n while ((rowMatch = rowRegex.exec(html)) !== null) {\n const cells: string[] = [];\n const cellRegex = /<(?:td|th)[^>]*>([\\s\\S]*?)<\\/(?:td|th)>/gi;\n let cellMatch: RegExpExecArray | null;\n\n while ((cellMatch = cellRegex.exec(rowMatch[1])) !== null) {\n cells.push(stripTags(cellMatch[1]).trim());\n }\n\n if (cells.length > 0) {\n rows.push(cells);\n }\n }\n\n return rows;\n}\n\n/**\n * Strip all HTML tags from a string.\n */\nfunction stripTags(html: string): string {\n return html.replace(/<[^>]+>/g, \"\");\n}\n\n/**\n * Decode common HTML entities.\n */\nfunction decodeHtmlEntities(text: string): string {\n return text\n .replace(/&/g, \"&\")\n .replace(/</g, \"<\")\n .replace(/>/g, \">\")\n .replace(/"/g, '\"')\n .replace(/'/g, \"'\")\n .replace(/ /g, \" \");\n}\n\n// ---------------------------------------------------------------------------\n// extractContentAsMarkdown\n// ---------------------------------------------------------------------------\n\n/**\n * Extract the page content as Markdown.\n *\n * Fetches the full page HTML via Runtime.evaluate, then converts to markdown\n * using a lightweight HTML-to-Markdown converter that:\n * - Converts headings to # syntax\n * - Wraps code blocks in fenced markdown\n * - Converts tables to markdown tables\n * - Excludes nav/aside elements\n *\n * @param cdp - CDP connection.\n * @param params - Optional selector to scope extraction.\n * @returns Markdown string.\n */\nexport async function extractContentAsMarkdown(\n cdp: CDPConnection,\n params: MarkdownParams,\n): Promise<MarkdownResult> {\n // Get the HTML content\n const evalResult = (await cdp.send(\"Runtime.evaluate\", {\n expression: \"document.documentElement.outerHTML\",\n returnByValue: true,\n })) as {\n result: { type: string; value: string };\n };\n\n const html = evalResult.result.value;\n const markdown = htmlToMarkdown(html);\n\n return { markdown };\n}\n","/**\n * browser_eval tool — evaluates JavaScript expressions in the browser via CDP.\n *\n * Supports:\n * - Simple expression evaluation (Runtime.evaluate)\n * - Element-scoped evaluation via @eN refs (Runtime.callFunctionOn)\n * - Async expressions with awaitPromise\n * - Error handling (ReferenceError, TypeError, etc.)\n * - DOM node serialization to string description\n * - Primitive type handling (null, undefined, boolean)\n * - Multi-line expressions (stdin mode)\n * - Base64-encoded expressions\n *\n * @module browser-eval\n */\nimport type { CDPConnection } from \"../cdp/connection\";\n\n// ---------------------------------------------------------------------------\n// Types\n// ---------------------------------------------------------------------------\n\ninterface EvalParams {\n /** JavaScript expression to evaluate. */\n expression: string;\n /** @eN ref for element-scoped evaluation via callFunctionOn. */\n ref?: string;\n /** Whether the expression is multi-line (stdin mode). */\n stdin?: boolean;\n /** Whether the expression is base64-encoded. */\n base64?: boolean;\n}\n\ninterface EvalResult {\n /** The evaluation result value. */\n result?: unknown;\n /** Error message if evaluation failed. */\n error?: string;\n}\n\n// ---------------------------------------------------------------------------\n// CDP result parsing\n// ---------------------------------------------------------------------------\n\ninterface CDPRemoteObject {\n type: string;\n subtype?: string;\n className?: string;\n description?: string;\n value?: unknown;\n objectId?: string;\n}\n\ninterface CDPExceptionDetails {\n exceptionId: number;\n text: string;\n lineNumber: number;\n columnNumber: number;\n exception?: {\n type: string;\n subtype?: string;\n className?: string;\n description?: string;\n };\n}\n\n/**\n * Parse a CDP RemoteObject into a JavaScript value.\n *\n * Handles:\n * - Primitives (string, number, boolean)\n * - null (subtype \"null\")\n * - undefined (type \"undefined\")\n * - DOM nodes (subtype \"node\") → serialized to string description\n * - Objects → return the value directly\n */\nfunction parseRemoteObject(obj: CDPRemoteObject): unknown {\n // undefined\n if (obj.type === \"undefined\") {\n return undefined;\n }\n\n // null\n if (obj.type === \"object\" && obj.subtype === \"null\") {\n return null;\n }\n\n // DOM node\n if (obj.type === \"object\" && obj.subtype === \"node\") {\n return obj.description ?? `[${obj.className ?? \"Node\"}]`;\n }\n\n // Primitives and objects with value\n if (obj.value !== undefined) {\n return obj.value;\n }\n\n // Object without value (shouldn't happen with returnByValue: true)\n if (obj.description) {\n return obj.description;\n }\n\n return undefined;\n}\n\n/**\n * Extract error message from CDP exception details.\n */\nfunction formatException(details: CDPExceptionDetails): string {\n if (details.exception?.description) {\n return details.exception.description;\n }\n\n if (details.exception?.className) {\n return `${details.exception.className}: ${details.text}`;\n }\n\n return details.text;\n}\n\n// ---------------------------------------------------------------------------\n// Main export\n// ---------------------------------------------------------------------------\n\n/**\n * Evaluate a JavaScript expression in the browser context.\n *\n * Without a ref, uses `Runtime.evaluate` for global scope evaluation.\n * With a ref (@eN), uses `DOM.resolveNode` + `Runtime.callFunctionOn`\n * for element-scoped evaluation.\n *\n * @param cdp - CDP connection.\n * @param params - Expression and optional ref/flags.\n * @returns Evaluation result or error.\n */\nexport async function browserEval(\n cdp: CDPConnection,\n params: EvalParams,\n): Promise<EvalResult> {\n let expression = params.expression;\n\n // Decode base64-encoded expression\n if (params.base64) {\n expression = atob(expression);\n }\n\n // Element-scoped evaluation via @eN ref\n if (params.ref) {\n return evalWithRef(cdp, expression, params.ref);\n }\n\n // Global scope evaluation\n return evalGlobal(cdp, expression);\n}\n\n/**\n * Evaluate an expression in the global scope via Runtime.evaluate.\n */\nasync function evalGlobal(\n cdp: CDPConnection,\n expression: string,\n): Promise<EvalResult> {\n const response = (await cdp.send(\"Runtime.evaluate\", {\n expression,\n returnByValue: true,\n awaitPromise: true,\n })) as {\n result: CDPRemoteObject;\n exceptionDetails?: CDPExceptionDetails;\n };\n\n // Check for exceptions\n if (response.exceptionDetails) {\n return {\n error: formatException(response.exceptionDetails),\n };\n }\n\n const value = parseRemoteObject(response.result);\n\n return { result: value };\n}\n\n/**\n * Evaluate a function on a specific element via Runtime.callFunctionOn.\n *\n * Resolves the @eN ref to a backendNodeId, then resolves to a remote object,\n * and calls the function on it.\n */\nasync function evalWithRef(\n cdp: CDPConnection,\n functionDeclaration: string,\n ref: string,\n): Promise<EvalResult> {\n // Parse the @eN ref\n const match = /^@e(\\d+)$/.exec(ref);\n if (!match) {\n return { error: `Invalid ref format: ${ref}` };\n }\n\n const backendNodeId = parseInt(match[1], 10);\n\n // Resolve backendNodeId to a remote object\n const resolved = (await cdp.send(\"DOM.resolveNode\", {\n backendNodeId,\n })) as { object: { objectId: string } };\n\n const objectId = resolved.object.objectId;\n\n // Call the function on the element\n const response = (await cdp.send(\"Runtime.callFunctionOn\", {\n objectId,\n functionDeclaration,\n returnByValue: true,\n awaitPromise: true,\n })) as {\n result: CDPRemoteObject;\n exceptionDetails?: CDPExceptionDetails;\n };\n\n // Check for exceptions\n if (response.exceptionDetails) {\n return {\n error: formatException(response.exceptionDetails),\n };\n }\n\n const value = parseRemoteObject(response.result);\n\n return { result: value };\n}\n","/**\n * browser_find — Semantic element locators via CDP Accessibility and Runtime APIs.\n *\n * Provides Playwright-style locator functions: findByRole, findByText, findByLabel,\n * findByPlaceholder, findByAlt, findByTitle, findByTestId, findFirst, findLast, findNth.\n *\n * Also provides a unified `browserFind` function for the MCP tool that finds elements\n * by ARIA role, accessible name, or text content and returns @eN refs.\n *\n * Role and text-based locators use the Accessibility tree (Accessibility.getFullAXTree).\n * Attribute-based locators use Runtime.evaluate with CSS selectors.\n * Positional locators (first/last/nth) use querySelectorAll.\n */\nimport type { CDPConnection } from \"../cdp/connection.js\";\n\n// ---------------------------------------------------------------------------\n// Types\n// ---------------------------------------------------------------------------\n\nexport interface FindResult {\n /** Whether an element was found. */\n found: boolean;\n /** The backend node ID of the found element. */\n backendNodeId?: number;\n /** The index of the found element (for positional locators). */\n index?: number;\n}\n\nexport interface FindByRoleParams {\n /** ARIA role to search for (e.g., \"button\", \"link\"). */\n role: string;\n /** Accessible name to match. */\n name?: string;\n}\n\nexport interface FindByTextParams {\n /** Text content to search for. */\n text: string;\n /** If true, require exact text match (not substring). */\n exact?: boolean;\n}\n\nexport interface FindByLabelParams {\n /** Label text to search for. */\n label: string;\n}\n\nexport interface FindByPlaceholderParams {\n /** Placeholder text to search for. */\n placeholder: string;\n}\n\nexport interface FindByAltParams {\n /** Alt text to search for. */\n alt: string;\n}\n\nexport interface FindByTitleParams {\n /** Title attribute to search for. */\n title: string;\n}\n\nexport interface FindByTestIdParams {\n /** data-testid value to search for. */\n testId: string;\n}\n\nexport interface FindPositionalParams {\n /** CSS selector to match elements. */\n selector: string;\n}\n\nexport interface FindNthParams extends FindPositionalParams {\n /** Zero-based index of the element to select. */\n n: number;\n}\n\n// ---------------------------------------------------------------------------\n// Accessibility tree types\n// ---------------------------------------------------------------------------\n\ninterface AXNode {\n nodeId: string;\n role: { type: string; value: string };\n name?: { type: string; value: string };\n backendDOMNodeId?: number;\n}\n\n// ---------------------------------------------------------------------------\n// Exported functions\n// ---------------------------------------------------------------------------\n\n/**\n * Finds an element by ARIA role and optional accessible name.\n *\n * Uses Accessibility.getFullAXTree and filters by role/name.\n */\nexport async function findByRole(\n cdp: CDPConnection,\n params: FindByRoleParams,\n): Promise<FindResult> {\n const response = (await cdp.send(\"Accessibility.getFullAXTree\")) as {\n nodes: AXNode[];\n };\n\n const match = response.nodes.find((node) => {\n if (node.role.value !== params.role) return false;\n if (params.name && node.name?.value !== params.name) return false;\n return true;\n });\n\n if (!match) {\n return { found: false };\n }\n\n return {\n found: true,\n backendNodeId: match.backendDOMNodeId,\n };\n}\n\n/**\n * Finds an element by its text content.\n *\n * Uses Accessibility.getFullAXTree and filters by accessible name.\n */\nexport async function findByText(\n cdp: CDPConnection,\n params: FindByTextParams,\n): Promise<FindResult> {\n const response = (await cdp.send(\"Accessibility.getFullAXTree\")) as {\n nodes: AXNode[];\n };\n\n const match = response.nodes.find((node) => {\n if (!node.name?.value) return false;\n if (params.exact) {\n return node.name.value === params.text;\n }\n return node.name.value.includes(params.text);\n });\n\n if (!match) {\n return { found: false };\n }\n\n return {\n found: true,\n backendNodeId: match.backendDOMNodeId,\n };\n}\n\n/**\n * Finds an element by its associated label text.\n *\n * Uses Runtime.evaluate to find an element via label association.\n */\nexport async function findByLabel(\n cdp: CDPConnection,\n params: FindByLabelParams,\n): Promise<FindResult> {\n const escaped = JSON.stringify(params.label);\n\n const response = (await cdp.send(\"Runtime.evaluate\", {\n expression: `(() => {\n const labels = document.querySelectorAll('label');\n for (const label of labels) {\n if (label.textContent?.trim() === ${escaped}) {\n if (label.htmlFor) {\n return document.getElementById(label.htmlFor);\n }\n return label.querySelector('input, select, textarea');\n }\n }\n return null;\n })()`,\n returnByValue: false,\n })) as { result: { type: string; objectId?: string } };\n\n if (!response.result.objectId) {\n return { found: false };\n }\n\n return { found: true };\n}\n\n/**\n * Finds an element by its placeholder attribute.\n *\n * Uses Runtime.evaluate with an attribute selector.\n */\nexport async function findByPlaceholder(\n cdp: CDPConnection,\n params: FindByPlaceholderParams,\n): Promise<FindResult> {\n const escaped = JSON.stringify(params.placeholder);\n\n const response = (await cdp.send(\"Runtime.evaluate\", {\n expression: `document.querySelector('[placeholder=${escaped}]')`,\n returnByValue: false,\n })) as { result: { type: string; objectId?: string } };\n\n if (!response.result.objectId) {\n return { found: false };\n }\n\n return { found: true };\n}\n\n/**\n * Finds an element by its alt text attribute.\n *\n * Uses Runtime.evaluate with an attribute selector.\n */\nexport async function findByAlt(\n cdp: CDPConnection,\n params: FindByAltParams,\n): Promise<FindResult> {\n const escaped = JSON.stringify(params.alt);\n\n const response = (await cdp.send(\"Runtime.evaluate\", {\n expression: `document.querySelector('[alt=${escaped}]')`,\n returnByValue: false,\n })) as { result: { type: string; objectId?: string } };\n\n if (!response.result.objectId) {\n return { found: false };\n }\n\n return { found: true };\n}\n\n/**\n * Finds an element by its title attribute.\n *\n * Uses Runtime.evaluate with an attribute selector.\n */\nexport async function findByTitle(\n cdp: CDPConnection,\n params: FindByTitleParams,\n): Promise<FindResult> {\n const escaped = JSON.stringify(params.title);\n\n const response = (await cdp.send(\"Runtime.evaluate\", {\n expression: `document.querySelector('[title=${escaped}]')`,\n returnByValue: false,\n })) as { result: { type: string; objectId?: string } };\n\n if (!response.result.objectId) {\n return { found: false };\n }\n\n return { found: true };\n}\n\n/**\n * Finds an element by its data-testid attribute.\n *\n * Uses Runtime.evaluate with a data attribute selector.\n */\nexport async function findByTestId(\n cdp: CDPConnection,\n params: FindByTestIdParams,\n): Promise<FindResult> {\n const escaped = JSON.stringify(params.testId);\n\n const response = (await cdp.send(\"Runtime.evaluate\", {\n expression: `document.querySelector('[data-testid=${escaped}]')`,\n returnByValue: false,\n })) as { result: { type: string; objectId?: string } };\n\n if (!response.result.objectId) {\n return { found: false };\n }\n\n return { found: true };\n}\n\n/**\n * Finds the first element matching a CSS selector.\n *\n * @returns FindResult with index=0\n */\nexport async function findFirst(\n cdp: CDPConnection,\n params: FindPositionalParams,\n): Promise<FindResult> {\n const escaped = JSON.stringify(params.selector);\n\n const response = (await cdp.send(\"Runtime.evaluate\", {\n expression: `document.querySelectorAll(${escaped})[0]`,\n returnByValue: false,\n })) as { result: { type: string; objectId?: string } };\n\n if (!response.result.objectId) {\n return { found: false };\n }\n\n return { found: true, index: 0 };\n}\n\n/**\n * Finds the last element matching a CSS selector.\n *\n * @returns FindResult with the last element's index\n */\nexport async function findLast(\n cdp: CDPConnection,\n params: FindPositionalParams,\n): Promise<FindResult> {\n const escaped = JSON.stringify(params.selector);\n\n const response = (await cdp.send(\"Runtime.evaluate\", {\n expression: `(() => {\n const els = document.querySelectorAll(${escaped});\n return els.length > 0 ? els[els.length - 1] : null;\n })()`,\n returnByValue: false,\n })) as { result: { type: string; objectId?: string } };\n\n if (!response.result.objectId) {\n return { found: false };\n }\n\n return { found: true };\n}\n\n/**\n * Finds the nth element matching a CSS selector.\n *\n * @param params.n - Zero-based index\n * @returns FindResult with the nth element\n */\nexport async function findNth(\n cdp: CDPConnection,\n params: FindNthParams,\n): Promise<FindResult> {\n const escaped = JSON.stringify(params.selector);\n\n const response = (await cdp.send(\"Runtime.evaluate\", {\n expression: `document.querySelectorAll(${escaped})[${params.n}]`,\n returnByValue: false,\n })) as { result: { type: string; objectId?: string } };\n\n if (!response.result.objectId) {\n return { found: false };\n }\n\n return { found: true, index: params.n };\n}\n\n// ---------------------------------------------------------------------------\n// Unified browserFind — MCP tool entry point\n// ---------------------------------------------------------------------------\n\nexport interface BrowserFindParams {\n /** ARIA role to search for (e.g., \"button\", \"link\", \"heading\", \"textbox\"). */\n role?: string;\n /** Accessible name to match (substring, case-sensitive). */\n name?: string;\n /** Text content to search for (substring match on accessible name). */\n text?: string;\n /** Zero-based index to pick the nth match (default 0 = first). */\n nth?: number;\n}\n\nexport interface BrowserFindResult {\n /** Whether a matching element was found. */\n found: boolean;\n /** @eN ref for use with other browsirai tools, or null if not found. */\n ref: string | null;\n /** ARIA role of the matched element, or null if not found. */\n role: string | null;\n /** Accessible name of the matched element, or null if not found. */\n name: string | null;\n /** Total number of matching elements. */\n count: number;\n}\n\n/**\n * Finds elements by ARIA role, accessible name, or text content.\n *\n * Uses Accessibility.getFullAXTree to walk the AX tree, filters by\n * role (case-insensitive) and/or name (substring) and/or text content,\n * and returns the nth match (default first) with an @eN ref.\n */\nexport async function browserFind(\n cdp: CDPConnection,\n params: BrowserFindParams,\n): Promise<BrowserFindResult> {\n const nth = params.nth ?? 0;\n\n // Get the full accessibility tree\n const response = (await cdp.send(\"Accessibility.getFullAXTree\", undefined, {\n timeout: 10000,\n })) as {\n nodes: AXNode[];\n };\n\n // Filter nodes by role, name, and/or text\n const matches = response.nodes.filter((node) => {\n // Filter by role (case-insensitive)\n if (params.role) {\n const nodeRole = node.role?.value ?? \"\";\n if (nodeRole.toLowerCase() !== params.role.toLowerCase()) {\n return false;\n }\n }\n\n // Filter by name (substring match)\n if (params.name) {\n const nodeName = node.name?.value ?? \"\";\n if (!nodeName.includes(params.name)) {\n return false;\n }\n }\n\n // Filter by text content (substring match on accessible name)\n if (params.text) {\n const nodeName = node.name?.value ?? \"\";\n if (!nodeName.includes(params.text)) {\n return false;\n }\n }\n\n return true;\n });\n\n const count = matches.length;\n\n // Pick the nth match\n if (nth >= count || count === 0) {\n return {\n found: false,\n ref: null,\n role: null,\n name: null,\n count,\n };\n }\n\n const match = matches[nth];\n const ref = match.backendDOMNodeId ? `@e${match.backendDOMNodeId}` : null;\n\n return {\n found: true,\n ref,\n role: match.role?.value ?? null,\n name: match.name?.value ?? null,\n count,\n };\n}\n","/**\n * browser_inspect_source tool — maps DOM elements to their source code locations.\n *\n * CDP-native resolution:\n * - React: Walk Fiber tree, parse jsxDEV() calls in Function.toString()\n * to extract fileName/lineNumber embedded by Babel jsx-source plugin.\n * - Vue: Read __vueParentComponent.type.__file\n * - Svelte: Read __svelte_meta.loc\n *\n * Works with React, Vue, Svelte frameworks.\n */\nimport type { CDPConnection } from \"../cdp/connection\";\n\n// ---------------------------------------------------------------------------\n// Types\n// ---------------------------------------------------------------------------\n\nexport interface InspectSourceParams {\n /** @eN ref from accessibility snapshot */\n ref?: string;\n /** CSS selector to find the element */\n selector?: string;\n}\n\nexport interface SourceLocation {\n filePath: string;\n lineNumber: number | null;\n columnNumber: number | null;\n componentName: string | null;\n}\n\nexport interface InspectSourceResult {\n tagName: string;\n componentName: string | null;\n source: SourceLocation | null;\n stack: SourceLocation[];\n}\n\n// ---------------------------------------------------------------------------\n// Ref pattern\n// ---------------------------------------------------------------------------\n\nconst REF_PATTERN = /^@?e(\\d+)$/;\n\n// ---------------------------------------------------------------------------\n// Main export\n// ---------------------------------------------------------------------------\n\n/**\n * Inspects a DOM element and returns its source code location.\n *\n * Strategy:\n * 1. Resolve element via ref or selector\n * 2. Walk React Fiber tree → parse Function.toString() for fileName/lineNumber\n * 3. Check Vue (__vueParentComponent) and Svelte (__svelte_meta)\n */\nexport async function browserInspectSource(\n cdp: CDPConnection,\n params: InspectSourceParams,\n): Promise<InspectSourceResult> {\n // 1. Resolve element\n let objectId: string;\n\n if (params.ref) {\n const match = REF_PATTERN.exec(params.ref);\n if (!match) throw new Error(`Invalid ref format: ${params.ref}`);\n const backendNodeId = parseInt(match[1], 10);\n const resolved = (await cdp.send(\"DOM.resolveNode\", { backendNodeId })) as {\n object: { objectId: string };\n };\n objectId = resolved.object.objectId;\n } else if (params.selector) {\n const evalResult = (await cdp.send(\"Runtime.evaluate\", {\n expression: `document.querySelector(${JSON.stringify(params.selector)})`,\n returnByValue: false,\n })) as { result: { objectId?: string; subtype?: string } };\n\n if (!evalResult.result.objectId || evalResult.result.subtype === \"null\") {\n throw new Error(`Element not found: ${params.selector}`);\n }\n objectId = evalResult.result.objectId;\n } else {\n throw new Error(\"Either ref or selector must be provided\");\n }\n\n // 2. CDP-native: Walk Fiber tree + parse Function.toString()\n const cdpResult = (await cdp.send(\"Runtime.callFunctionOn\", {\n objectId,\n functionDeclaration: `function() {\n var el = this;\n var tagName = (el.tagName || '').toLowerCase();\n\n // Find React Fiber\n var fiberKey = Object.keys(el).find(function(k) { return k.startsWith('__reactFiber'); });\n\n // Also check Vue, Svelte\n var vueComp = el.__vueParentComponent;\n var svelteMeta = el.__svelte_meta;\n\n if (!fiberKey && !vueComp && !svelteMeta) {\n return JSON.stringify({ tagName: tagName, componentName: null, source: null, stack: [], framework: null });\n }\n\n // --- React path ---\n if (fiberKey) {\n var fiber = el[fiberKey];\n var stack = [];\n var current = fiber;\n var firstSource = null;\n var firstName = null;\n\n while (current && stack.length < 15) {\n if (typeof current.type === 'function' && current.type.name) {\n var fn = current.type;\n var fnStr = fn.toString();\n var fileName = null;\n var lineNumber = null;\n var columnNumber = null;\n\n // Parse jsxDEV calls for embedded fileName/lineNumber\n var fileMatch = fnStr.match(/fileName:\\\\s*\"([^\"]+)\"/);\n if (fileMatch) {\n fileName = fileMatch[1];\n var lineMatch = fnStr.match(/lineNumber:\\\\s*(\\\\d+)/);\n if (lineMatch) lineNumber = parseInt(lineMatch[1]);\n var colMatch = fnStr.match(/columnNumber:\\\\s*(\\\\d+)/);\n if (colMatch) columnNumber = parseInt(colMatch[1]);\n }\n\n var entry = {\n filePath: fileName,\n lineNumber: lineNumber,\n columnNumber: columnNumber,\n componentName: fn.name\n };\n\n stack.push(entry);\n\n if (fileName && !firstSource) {\n firstSource = entry;\n }\n if (!firstName && fn.name.length > 1) {\n firstName = fn.name;\n }\n }\n current = current.return;\n }\n\n return JSON.stringify({\n tagName: tagName,\n componentName: firstName || null,\n source: firstSource || (stack.length > 0 ? { filePath: null, lineNumber: null, columnNumber: null, componentName: stack[0].componentName } : null),\n stack: stack.filter(function(s) { return s.filePath; }),\n framework: 'react'\n });\n }\n\n // --- Svelte path ---\n if (svelteMeta) {\n var loc = svelteMeta.loc || {};\n return JSON.stringify({\n tagName: tagName,\n componentName: loc.char ? null : (svelteMeta.component || null),\n source: loc.file ? { filePath: loc.file, lineNumber: loc.line || null, columnNumber: (loc.column || 0) + 1, componentName: null } : null,\n stack: [],\n framework: 'svelte'\n });\n }\n\n // --- Vue path ---\n if (vueComp) {\n var comp = vueComp;\n var vueName = comp.type?.__name || comp.type?.name || null;\n var vueFile = comp.type?.__file || null;\n return JSON.stringify({\n tagName: tagName,\n componentName: vueName,\n source: vueFile ? { filePath: vueFile, lineNumber: null, columnNumber: null, componentName: vueName } : null,\n stack: [],\n framework: 'vue'\n });\n }\n\n return JSON.stringify({ tagName: tagName, componentName: null, source: null, stack: [], framework: null });\n }`,\n returnByValue: true,\n })) as { result: { value: string } };\n\n return JSON.parse(cdpResult.result.value) as InspectSourceResult;\n}\n","/**\n * Fixed-size circular (ring) buffer for browser events.\n *\n * When the buffer reaches capacity the oldest events are silently evicted,\n * keeping memory usage bounded while still giving callers access to the\n * most-recent N events in chronological order.\n */\nexport class EventBuffer<T = unknown> {\n private _buffer: (T | undefined)[];\n private _capacity: number;\n private _head: number; // next write index\n private _size: number;\n private _totalPushed: number;\n\n constructor(capacity: number = 500) {\n this._capacity = capacity;\n this._buffer = new Array(capacity);\n this._head = 0;\n this._size = 0;\n this._totalPushed = 0;\n }\n\n /** Append an event, evicting the oldest if at capacity. */\n push(event: T): void {\n this._buffer[this._head] = event;\n this._head = (this._head + 1) % this._capacity;\n if (this._size < this._capacity) {\n this._size++;\n }\n this._totalPushed++;\n }\n\n /**\n * Return the last `n` events in chronological (oldest-first) order.\n * Defaults to all events when `n` is omitted.\n */\n last(n?: number): T[] {\n const count = n === undefined ? this._size : Math.min(n, this._size);\n if (count === 0) return [];\n\n const result: T[] = new Array(count);\n // The oldest of the `count` events starts at:\n let start = (this._head - count + this._capacity) % this._capacity;\n for (let i = 0; i < count; i++) {\n result[i] = this._buffer[(start + i) % this._capacity] as T;\n }\n return result;\n }\n\n /** Remove all events from the buffer. */\n clear(): void {\n this._buffer = new Array(this._capacity);\n this._head = 0;\n this._size = 0;\n }\n\n /**\n * Return the last `n` events AND remove them from the buffer.\n * Defaults to all events when `n` is omitted.\n */\n drain(n?: number): T[] {\n const events = this.last(n);\n this.clear();\n return events;\n }\n\n /** Current number of events stored. */\n get size(): number {\n return this._size;\n }\n\n /** Maximum number of events this buffer can hold. */\n get capacity(): number {\n return this._capacity;\n }\n\n /** Total number of events ever pushed (including evicted ones). */\n get totalPushed(): number {\n return this._totalPushed;\n }\n\n /** Return events matching `predicate` without modifying the buffer. */\n filter(predicate: (event: T) => boolean): T[] {\n return this.last().filter(predicate);\n }\n\n /** Snapshot of buffer statistics. */\n get stats(): { size: number; capacity: number; totalPushed: number; evicted: number } {\n return {\n size: this._size,\n capacity: this._capacity,\n totalPushed: this._totalPushed,\n evicted: this._totalPushed - this._size,\n };\n }\n}\n","/**\n * Secret redaction utilities for browsirai.\n *\n * Redacts sensitive values from headers, JSON bodies, inline text,\n * and network event objects to prevent secret leakage in logs/output.\n */\n\n// ---------------------------------------------------------------------------\n// Types\n// ---------------------------------------------------------------------------\n\nexport interface RedactOptions {\n enabled?: boolean;\n}\n\nexport interface NetworkEvent {\n url: string;\n method: string;\n status?: number;\n headers?: Record<string, string>;\n responseHeaders?: Record<string, string>;\n body?: string;\n [key: string]: unknown;\n}\n\n// ---------------------------------------------------------------------------\n// Constants\n// ---------------------------------------------------------------------------\n\nconst SENSITIVE_HEADERS = new Set([\n \"authorization\",\n \"cookie\",\n \"set-cookie\",\n \"x-api-key\",\n \"x-auth-token\",\n \"x-csrf-token\",\n \"x-xsrf-token\",\n \"proxy-authorization\",\n \"x-access-token\",\n \"x-refresh-token\",\n \"x-secret\",\n \"x-token\",\n]);\n\nconst SENSITIVE_BODY_KEYS = new Set([\n \"password\",\n \"secret\",\n \"token\",\n \"api_key\",\n \"apiKey\",\n \"api-key\",\n \"access_token\",\n \"refresh_token\",\n \"client_secret\",\n \"private_key\",\n]);\n\nconst JWT_PATTERN =\n /eyJ[A-Za-z0-9_-]{10,}\\.[A-Za-z0-9_-]{10,}\\.[A-Za-z0-9_-]+/g;\n\nconst BEARER_PATTERN = /Bearer\\s+\\S+/gi;\n\nconst REDACTED = \"[REDACTED]\";\nconst REDACTED_JWT = \"[REDACTED_JWT]\";\n\n// ---------------------------------------------------------------------------\n// redactHeaders\n// ---------------------------------------------------------------------------\n\nexport function redactHeaders(\n headers: Record<string, string>,\n opts?: RedactOptions,\n): Record<string, string> {\n if (opts?.enabled === false) {\n return { ...headers };\n }\n\n const result: Record<string, string> = {};\n for (const [key, value] of Object.entries(headers)) {\n if (SENSITIVE_HEADERS.has(key.toLowerCase())) {\n result[key] = REDACTED;\n } else {\n result[key] = value;\n }\n }\n return result;\n}\n\n// ---------------------------------------------------------------------------\n// redactBody\n// ---------------------------------------------------------------------------\n\nfunction redactObjectKeys(obj: unknown): unknown {\n if (obj === null || obj === undefined || typeof obj !== \"object\") {\n return obj;\n }\n\n if (Array.isArray(obj)) {\n return obj.map((item) => redactObjectKeys(item));\n }\n\n const result: Record<string, unknown> = {};\n for (const [key, value] of Object.entries(obj as Record<string, unknown>)) {\n if (SENSITIVE_BODY_KEYS.has(key)) {\n result[key] = REDACTED;\n } else if (typeof value === \"object\" && value !== null) {\n result[key] = redactObjectKeys(value);\n } else {\n result[key] = value;\n }\n }\n return result;\n}\n\nexport function redactBody(\n body: string,\n opts?: RedactOptions,\n): string {\n if (opts?.enabled === false) {\n return body;\n }\n\n try {\n const parsed = JSON.parse(body);\n const redacted = redactObjectKeys(parsed);\n return JSON.stringify(redacted);\n } catch {\n // Not valid JSON — return as-is\n return body;\n }\n}\n\n// ---------------------------------------------------------------------------\n// redactInlineSecrets\n// ---------------------------------------------------------------------------\n\nexport function redactInlineSecrets(\n text: string,\n opts?: RedactOptions,\n): string {\n if (opts?.enabled === false) {\n return text;\n }\n\n // Redact JWTs first (before Bearer, since Bearer may contain a JWT)\n let result = text.replace(JWT_PATTERN, REDACTED_JWT);\n\n // Redact Bearer tokens — preserve the original casing of \"Bearer\"\n result = result.replace(BEARER_PATTERN, (match) => {\n const bearerWord = match.split(/\\s+/)[0];\n return `${bearerWord} ${REDACTED}`;\n });\n\n return result;\n}\n\n// ---------------------------------------------------------------------------\n// redactNetworkEvent\n// ---------------------------------------------------------------------------\n\nexport function redactNetworkEvent(\n event: NetworkEvent,\n opts?: RedactOptions,\n): NetworkEvent {\n if (opts?.enabled === false) {\n return { ...event };\n }\n\n const result: NetworkEvent = { ...event };\n\n if (event.headers) {\n result.headers = redactHeaders(event.headers, opts);\n }\n\n if (event.responseHeaders) {\n result.responseHeaders = redactHeaders(event.responseHeaders, opts);\n }\n\n if (event.body !== undefined) {\n result.body = redactBody(event.body, opts);\n }\n\n return result;\n}\n","/**\n * browser_console_messages tool — captures console messages via CDP events.\n *\n * Uses Runtime.consoleAPICalled CDP event to capture messages server-side\n * into a bounded EventBuffer. Messages survive page navigations and never\n * require JS injection.\n *\n * Supports:\n * - Filtering by log level\n * - Result limiting\n * - Secret redaction (JWT/Bearer tokens)\n *\n * @module browser-console-messages\n */\nimport { EventBuffer } from \"../event-buffer.js\";\nimport { redactInlineSecrets } from \"../redactor.js\";\n\n// ---------------------------------------------------------------------------\n// Types\n// ---------------------------------------------------------------------------\n\nexport interface ConsoleMessagesParams {\n /** Maximum number of messages to return. */\n limit?: number;\n /** Filter by log level. */\n level?: \"log\" | \"warn\" | \"error\" | \"info\";\n}\n\nexport interface ConsoleMessage {\n /** Log level: \"log\", \"warn\", \"error\", or \"info\". */\n level: string;\n /** Stringified message text. */\n text: string;\n /** Timestamp when the message was captured. */\n timestamp?: number;\n}\n\nexport interface ConsoleMessagesResult {\n /** List of captured console messages. */\n messages: ConsoleMessage[];\n}\n\n// ---------------------------------------------------------------------------\n// Supported console levels\n// ---------------------------------------------------------------------------\n\nconst SUPPORTED_LEVELS = new Set([\"log\", \"warn\", \"warning\", \"error\", \"info\"]);\n\n// ---------------------------------------------------------------------------\n// Module-level EventBuffer\n// ---------------------------------------------------------------------------\n\nconst consoleBuffer = new EventBuffer<ConsoleMessage>(500);\n\n// ---------------------------------------------------------------------------\n// Setup & Reset\n// ---------------------------------------------------------------------------\n\ninterface CDPEventSource {\n on(event: string, handler: (params: unknown) => void): void;\n}\n\n/**\n * Register a CDP event listener for Runtime.consoleAPICalled.\n * Call once after Runtime.enable.\n */\nexport function setupConsoleCapture(cdp: CDPEventSource): void {\n cdp.on(\"Runtime.consoleAPICalled\", (params: unknown) => {\n const p = params as {\n type: string;\n args?: Array<{ type: string; value?: unknown; description?: string }>;\n timestamp?: number;\n };\n\n if (!SUPPORTED_LEVELS.has(p.type)) return;\n\n const text = (p.args ?? [])\n .map((arg) => {\n if (arg.type === \"string\") return String(arg.value);\n if (arg.type === \"undefined\") return \"undefined\";\n if (arg.value !== undefined) return String(arg.value);\n if (arg.description) return arg.description;\n return \"\";\n })\n .join(\" \");\n\n consoleBuffer.push({\n level: p.type === \"warning\" ? \"warn\" : p.type,\n text,\n timestamp: p.timestamp ? Math.floor(p.timestamp) : Date.now(),\n });\n });\n}\n\n/** Clear the console buffer (call on reconnection). */\nexport function resetConsoleBuffer(): void {\n consoleBuffer.clear();\n}\n\n// ---------------------------------------------------------------------------\n// Main export\n// ---------------------------------------------------------------------------\n\n/**\n * Read console messages from the EventBuffer.\n *\n * @param _cdp - CDP connection (unused — buffer is populated by setupConsoleCapture).\n * @param params - Filter and limit parameters.\n * @returns List of console messages.\n */\nexport async function browserConsoleMessages(\n _cdp: unknown,\n params: ConsoleMessagesParams,\n): Promise<ConsoleMessagesResult> {\n let messages = consoleBuffer.last();\n\n // Redact secrets from message text\n messages = messages.map((m) => ({ ...m, text: redactInlineSecrets(m.text) }));\n\n // Filter by level\n if (params.level) {\n messages = messages.filter((m) => m.level === params.level);\n }\n\n // Apply limit (return most recent messages)\n const limit = params.limit ?? 100;\n if (messages.length > limit) {\n messages = messages.slice(-limit);\n }\n\n return { messages };\n}\n","/**\n * browser_network_requests tool — captures network requests via CDP events.\n *\n * Uses Network.requestWillBeSent and Network.responseReceived CDP events to\n * capture requests server-side into a bounded EventBuffer. Captures method,\n * status code, headers — data not available via the Performance API.\n *\n * Supports:\n * - URL filtering via substring match\n * - Static resource filtering (Image, Stylesheet, Font, Script)\n * - Result limiting\n * - Secret redaction (JWT/Bearer tokens in URLs)\n *\n * @module browser-network-requests\n */\nimport { EventBuffer } from \"../event-buffer.js\";\nimport { redactInlineSecrets } from \"../redactor.js\";\n\n// ---------------------------------------------------------------------------\n// Types\n// ---------------------------------------------------------------------------\n\nexport interface NetworkRequestsParams {\n /** Substring filter to match against request URLs. */\n filter?: string;\n /** Maximum number of requests to return. */\n limit?: number;\n /** Whether to include response headers. */\n includeHeaders?: boolean;\n /** Whether to include static resources (images, stylesheets, fonts, scripts). */\n includeStatic?: boolean;\n}\n\nexport interface NetworkRequest {\n /** The request URL. */\n url: string;\n /** HTTP method (GET, POST, etc.). */\n method: string;\n /** HTTP status code. */\n status?: number;\n /** Resource type (e.g. \"Fetch\", \"XHR\", \"Script\", \"Image\"). */\n type?: string;\n}\n\nexport interface NetworkRequestsResult {\n /** List of captured network requests. */\n requests: NetworkRequest[];\n}\n\n// ---------------------------------------------------------------------------\n// Static resource types (CDP uses PascalCase)\n// ---------------------------------------------------------------------------\n\nconst STATIC_TYPES = new Set([\n \"Image\",\n \"Stylesheet\",\n \"Font\",\n \"Script\",\n \"Media\",\n]);\n\n// ---------------------------------------------------------------------------\n// Internal buffer entry (mutable — response enriches it)\n// ---------------------------------------------------------------------------\n\ninterface BufferEntry {\n requestId: string;\n url: string;\n method: string;\n type: string;\n status?: number;\n timestamp: number;\n}\n\n// ---------------------------------------------------------------------------\n// Module-level state\n// ---------------------------------------------------------------------------\n\nconst networkBuffer = new EventBuffer<BufferEntry>(500);\n\n/** Map requestId → buffer index for response correlation */\nconst pendingRequests = new Map<string, BufferEntry>();\n\n// ---------------------------------------------------------------------------\n// Setup & Reset\n// ---------------------------------------------------------------------------\n\ninterface CDPEventSource {\n on(event: string, handler: (params: unknown) => void): void;\n}\n\n/**\n * Register CDP event listeners for Network.requestWillBeSent and\n * Network.responseReceived. Call once after Network.enable.\n */\nexport function setupNetworkCapture(cdp: CDPEventSource): void {\n cdp.on(\"Network.requestWillBeSent\", (params: unknown) => {\n const p = params as {\n requestId: string;\n request: { url: string; method: string };\n type?: string;\n timestamp?: number;\n };\n\n const entry: BufferEntry = {\n requestId: p.requestId,\n url: p.request.url,\n method: p.request.method,\n type: p.type ?? \"Other\",\n timestamp: p.timestamp ? Math.floor(p.timestamp * 1000) : Date.now(),\n };\n\n pendingRequests.set(p.requestId, entry);\n networkBuffer.push(entry);\n });\n\n cdp.on(\"Network.responseReceived\", (params: unknown) => {\n const p = params as {\n requestId: string;\n response: { url: string; status: number; headers?: Record<string, string> };\n };\n\n const entry = pendingRequests.get(p.requestId);\n if (entry) {\n entry.status = p.response.status;\n pendingRequests.delete(p.requestId);\n }\n });\n}\n\n/** Clear the network buffer (call on reconnection). */\nexport function resetNetworkBuffer(): void {\n networkBuffer.clear();\n pendingRequests.clear();\n}\n\n// ---------------------------------------------------------------------------\n// Main export\n// ---------------------------------------------------------------------------\n\n/**\n * Read network requests from the EventBuffer.\n *\n * @param _cdp - CDP connection (unused — buffer is populated by setupNetworkCapture).\n * @param params - Filter and limit parameters.\n * @returns List of network requests.\n */\nexport async function browserNetworkRequests(\n _cdp: unknown,\n params: NetworkRequestsParams,\n): Promise<NetworkRequestsResult> {\n let entries = networkBuffer.last();\n\n // Filter static resources unless includeStatic is true\n if (!params.includeStatic) {\n entries = entries.filter((e) => !STATIC_TYPES.has(e.type));\n }\n\n // Filter by URL substring\n if (params.filter) {\n const filterLower = params.filter.toLowerCase();\n entries = entries.filter((e) => e.url.toLowerCase().includes(filterLower));\n }\n\n // Apply limit\n const limit = params.limit ?? 100;\n entries = entries.slice(0, limit);\n\n // Map to NetworkRequest format — redact secrets from URLs\n const requests: NetworkRequest[] = entries.map((e) => ({\n url: redactInlineSecrets(e.url),\n method: e.method,\n status: e.status,\n type: e.type,\n }));\n\n return { requests };\n}\n"],"mappings":"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","names":["name"]}
|
|
1
|
+
{"version":3,"sources":["../../../src/cli/commands/obs.ts","../../../src/cli/run.ts","../../../src/chrome-launcher.ts","../../../src/tools/browser-snapshot.ts","../../../src/tools/browser-screenshot.ts","../../../src/tools/browser-html.ts","../../../src/tools/browser-eval.ts","../../../src/tools/browser-find.ts","../../../src/tools/browser-inspect-source.ts","../../../src/event-buffer.ts","../../../src/redactor.ts","../../../src/tools/browser-console-messages.ts","../../../src/tools/browser-network-requests.ts"],"sourcesContent":["/**\n * Observation CLI commands for browsirai.\n *\n * Commands: snapshot, screenshot, html, eval, find, source, console, network\n *\n * @module cli/commands/obs\n */\n\nimport { writeFileSync } from \"node:fs\";\nimport type { CLICommand } from \"../types.js\";\nimport { parseFlags } from \"../run.js\";\nimport { browserSnapshot } from \"../../tools/browser-snapshot.js\";\nimport { browserScreenshot } from \"../../tools/browser-screenshot.js\";\nimport { browserHtml } from \"../../tools/browser-html.js\";\nimport { browserEval } from \"../../tools/browser-eval.js\";\nimport { browserFind } from \"../../tools/browser-find.js\";\nimport { browserInspectSource } from \"../../tools/browser-inspect-source.js\";\nimport { browserConsoleMessages } from \"../../tools/browser-console-messages.js\";\nimport { browserNetworkRequests } from \"../../tools/browser-network-requests.js\";\n\n// ---------------------------------------------------------------------------\n// snapshot\n// ---------------------------------------------------------------------------\n\nconst snapshotCommand: CLICommand = {\n name: \"snapshot\",\n description: \"Capture the accessibility tree of the page\",\n usage: \"browsirai snapshot [-i] [-c] [-d N] [-s selector]\",\n run: async (cdp, args) => {\n const flags = parseFlags(args);\n\n const params: Record<string, unknown> = {};\n if (flags.compact === \"true\" || flags.c === \"true\") params.compact = true;\n if (flags.interactive === \"true\" || flags.i === \"true\") params.interactive = true;\n if (flags.selector) params.selector = flags.selector;\n if (flags.s) params.selector = flags.s;\n if (flags.depth) params.depth = parseInt(flags.depth, 10);\n if (flags.d) params.depth = parseInt(flags.d, 10);\n\n const result = await browserSnapshot(cdp, params);\n\n if (result.snapshot) {\n console.log(result.snapshot);\n } else {\n console.log(\"(empty snapshot)\");\n }\n\n if (result.truncated) {\n console.log(`\\n(truncated — ${result.totalElements} total elements)`);\n }\n },\n};\n\n// ---------------------------------------------------------------------------\n// screenshot\n// ---------------------------------------------------------------------------\n\nconst screenshotCommand: CLICommand = {\n name: \"screenshot\",\n description: \"Capture a screenshot of the page\",\n usage: \"browsirai screenshot [-o file.png] [--fullPage] [--selector=...] [--format=png]\",\n run: async (cdp, args) => {\n const flags = parseFlags(args);\n\n const params: Record<string, unknown> = {};\n if (flags.fullPage === \"true\") params.fullPage = true;\n if (flags.selector) params.selector = flags.selector;\n if (flags.format) params.format = flags.format;\n\n const result = await browserScreenshot(cdp, params);\n\n const output = flags.output ?? flags.o;\n if (output) {\n const buffer = Buffer.from(result.base64, \"base64\");\n writeFileSync(output, buffer);\n console.log(`Screenshot saved to ${output} (${buffer.length} bytes)`);\n } else {\n // Estimate dimensions from base64 data length\n const sizeKB = Math.round((result.base64.length * 3) / 4 / 1024);\n console.log(`Screenshot taken (~${sizeKB}KB)`);\n console.log(\"Use --output=file.png to save to disk\");\n }\n },\n};\n\n// ---------------------------------------------------------------------------\n// html\n// ---------------------------------------------------------------------------\n\nconst htmlCommand: CLICommand = {\n name: \"html\",\n description: \"Retrieve page or element HTML\",\n usage: \"browsirai html [--selector=...]\",\n run: async (cdp, args) => {\n const flags = parseFlags(args);\n\n const params: { selector?: string } = {};\n if (flags.selector) params.selector = flags.selector;\n\n const result = await browserHtml(cdp, params);\n\n if (result.error) {\n console.error(`Error: ${result.error}`);\n process.exit(1);\n }\n\n console.log(result.html);\n },\n};\n\n// ---------------------------------------------------------------------------\n// eval\n// ---------------------------------------------------------------------------\n\nconst evalCommand: CLICommand = {\n name: \"eval\",\n description: \"Evaluate a JavaScript expression in the browser\",\n usage: 'browsirai eval \"<expression>\"',\n run: async (cdp, args) => {\n const flags = parseFlags(args);\n\n // First positional arg is the JS expression\n const expression = flags._0;\n if (!expression) {\n console.error(\"Error: Missing expression argument\");\n console.error('Usage: browsirai eval \"<expression>\"');\n process.exit(1);\n }\n\n const result = await browserEval(cdp, { expression });\n\n if (result.error) {\n console.error(`Error: ${result.error}`);\n process.exit(1);\n }\n\n if (result.result === undefined) {\n console.log(\"undefined\");\n } else if (result.result === null) {\n console.log(\"null\");\n } else if (typeof result.result === \"object\") {\n console.log(JSON.stringify(result.result, null, 2));\n } else {\n console.log(String(result.result));\n }\n },\n};\n\n// ---------------------------------------------------------------------------\n// find\n// ---------------------------------------------------------------------------\n\nconst findCommand: CLICommand = {\n name: \"find\",\n description: \"Find elements by ARIA role, name, or text\",\n usage: \"browsirai find [--role=button] [--name=...] [--text=...]\",\n run: async (cdp, args) => {\n const flags = parseFlags(args);\n\n const params: Record<string, unknown> = {};\n if (flags.role) params.role = flags.role;\n if (flags.name) params.name = flags.name;\n if (flags.text) params.text = flags.text;\n if (flags.nth) params.nth = parseInt(flags.nth, 10);\n\n const result = await browserFind(cdp, params);\n\n if (result.found) {\n console.log(`Found ${result.ref} (role: ${result.role}, name: \"${result.name}\")`);\n if (result.count > 1) {\n console.log(` ${result.count} total matches`);\n }\n } else {\n console.log(\"Not found\");\n if (result.count > 0) {\n console.log(` ${result.count} matches exist but index out of range`);\n }\n }\n },\n};\n\n// ---------------------------------------------------------------------------\n// source\n// ---------------------------------------------------------------------------\n\nconst sourceCommand: CLICommand = {\n name: \"source\",\n description: \"Inspect source code location of an element\",\n usage: \"browsirai source [--ref=@e5] [--selector=h1]\",\n run: async (cdp, args) => {\n const flags = parseFlags(args);\n\n const params: { ref?: string; selector?: string } = {};\n if (flags.ref) params.ref = flags.ref;\n if (flags.selector) params.selector = flags.selector;\n\n if (!params.ref && !params.selector) {\n console.error(\"Error: Either --ref or --selector is required\");\n console.error(\"Usage: browsirai source [--ref=@e5] [--selector=h1]\");\n process.exit(1);\n }\n\n const result = await browserInspectSource(cdp, params);\n\n if (result.source) {\n const loc = result.source;\n const file = loc.filePath ?? \"(unknown file)\";\n const line = loc.lineNumber != null ? `:${loc.lineNumber}` : \"\";\n const col = loc.columnNumber != null ? `:${loc.columnNumber}` : \"\";\n const component = result.componentName ?? loc.componentName ?? \"(anonymous)\";\n console.log(`Component: ${component}`);\n console.log(`Source: ${file}${line}${col}`);\n } else {\n console.log(`Tag: <${result.tagName}>`);\n if (result.componentName) {\n console.log(`Component: ${result.componentName}`);\n }\n console.log(\"Source: not found (dev mode may not be enabled)\");\n }\n\n if (result.stack.length > 0) {\n console.log(\"\\nComponent stack:\");\n for (const loc of result.stack) {\n const name = loc.componentName ?? \"(anonymous)\";\n const line = loc.lineNumber != null ? `:${loc.lineNumber}` : \"\";\n console.log(` ${name} -> ${loc.filePath}${line}`);\n }\n }\n },\n};\n\n// ---------------------------------------------------------------------------\n// console\n// ---------------------------------------------------------------------------\n\nconst consoleCommand: CLICommand = {\n name: \"console\",\n description: \"View captured console messages\",\n usage: \"browsirai console [--level=error] [--limit=20]\",\n run: async (cdp, args) => {\n const flags = parseFlags(args);\n\n const params: Record<string, unknown> = {};\n if (flags.level) params.level = flags.level;\n if (flags.limit) params.limit = parseInt(flags.limit, 10);\n\n const result = await browserConsoleMessages(cdp, params);\n\n if (result.messages.length === 0) {\n console.log(\"No console messages captured\");\n return;\n }\n\n for (const msg of result.messages) {\n const level = msg.level.toUpperCase().padEnd(5);\n const ts = msg.timestamp\n ? new Date(msg.timestamp).toISOString().slice(11, 23)\n : \"\";\n const prefix = ts ? `[${ts}] ${level}` : level;\n console.log(`${prefix} ${msg.text}`);\n }\n\n console.log(`\\n(${result.messages.length} messages)`);\n },\n};\n\n// ---------------------------------------------------------------------------\n// network\n// ---------------------------------------------------------------------------\n\nconst networkCommand: CLICommand = {\n name: \"network\",\n description: \"View captured network requests\",\n usage: \"browsirai network [--filter=*api*] [--limit=10] [--includeHeaders]\",\n run: async (cdp, args) => {\n const flags = parseFlags(args);\n\n const params: Record<string, unknown> = {};\n if (flags.filter) params.filter = flags.filter;\n if (flags.limit) params.limit = parseInt(flags.limit, 10);\n if (flags.includeHeaders === \"true\") params.includeHeaders = true;\n\n const result = await browserNetworkRequests(cdp, params);\n\n if (result.requests.length === 0) {\n console.log(\"No network requests captured\");\n return;\n }\n\n for (const req of result.requests) {\n const status = req.status != null ? String(req.status) : \"...\";\n const type = req.type ? `[${req.type}]` : \"\";\n console.log(`${req.method.padEnd(6)} ${status.padEnd(3)} ${type.padEnd(10)} ${req.url}`);\n }\n\n console.log(`\\n(${result.requests.length} requests)`);\n },\n};\n\n// ---------------------------------------------------------------------------\n// Export\n// ---------------------------------------------------------------------------\n\nexport const obsCommands: CLICommand[] = [\n snapshotCommand,\n screenshotCommand,\n htmlCommand,\n evalCommand,\n findCommand,\n sourceCommand,\n consoleCommand,\n networkCommand,\n];\n","/**\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 \"--no-sandbox\",\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 \"--no-sandbox\",\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 * browser_snapshot tool — captures the accessibility tree and returns a\n * formatted text representation with @eN refs for each element.\n *\n * Exports:\n * - browserSnapshot(cdp, params?) — main entry point\n * - shouldShowAxNode(node, options?) — filtering predicate\n * - processAccessibilityTree(nodes, options?) — tree formatter\n *\n * @module browser-snapshot\n */\n\nimport type { CDPConnection } from \"../cdp/connection\";\n\n// ---------------------------------------------------------------------------\n// Types\n// ---------------------------------------------------------------------------\n\n/** Shape of an accessibility tree node from CDP. */\ninterface AXNode {\n nodeId: string;\n backendDOMNodeId?: number;\n role?: { type: string; value: string };\n name?: { type: string; value: string };\n description?: { type: string; value: string };\n value?: { type: string; value: string } | null;\n properties?: Array<{\n name: string;\n value: { type: string; value: unknown };\n }>;\n parentId?: string;\n children?: Array<{ nodeId: string; backendDOMNodeId?: number }>;\n childIds?: string[];\n}\n\n/** Options controlling which nodes appear in the snapshot. */\nexport interface SnapshotOptions {\n /** Only show interactive elements (button, link, textbox, etc.). */\n interactive?: boolean;\n /** Include elements with cursor:pointer. */\n cursor?: boolean;\n /** Compact mode — hides InlineTextBox and empty structural wrappers. */\n compact?: boolean;\n /** Maximum tree depth to include. */\n depth?: number;\n /** CSS selector to scope the snapshot. */\n selector?: string;\n}\n\n/** Options for processAccessibilityTree. */\nexport interface ProcessTreeOptions {\n maxDepth?: number;\n}\n\n/** Result returned by browserSnapshot. */\nexport interface SnapshotResult {\n snapshot: string;\n truncated?: boolean;\n totalElements?: number;\n}\n\n// ---------------------------------------------------------------------------\n// Interactive role set\n// ---------------------------------------------------------------------------\n\nconst INTERACTIVE_ROLES = new Set([\n \"button\",\n \"link\",\n \"textbox\",\n \"checkbox\",\n \"radio\",\n \"combobox\",\n \"listbox\",\n \"menuitem\",\n \"menuitemcheckbox\",\n \"menuitemradio\",\n \"option\",\n \"searchbox\",\n \"slider\",\n \"spinbutton\",\n \"switch\",\n \"tab\",\n \"treeitem\",\n]);\n\n// ---------------------------------------------------------------------------\n// shouldShowAxNode\n// ---------------------------------------------------------------------------\n\n/**\n * Determines whether an AX node should be included in the snapshot output.\n *\n * Filtering rules (from tests):\n * 1. role='none' → false\n * 2. role='generic' with empty name → false\n * 3. role='InlineTextBox' in compact mode → false\n * 4. Empty name AND empty/null value → false\n * 5. Otherwise → true\n */\nexport function shouldShowAxNode(\n node: AXNode,\n options?: { compact?: boolean },\n): boolean {\n const role = node.role?.value ?? \"\";\n\n // Rule 1: role='none'\n if (role === \"none\") {\n return false;\n }\n\n // Rule 2: role='generic' with empty name\n if (role === \"generic\") {\n const name = node.name?.value ?? \"\";\n if (!name) {\n return false;\n }\n }\n\n // Rule 3: InlineTextBox in compact mode\n if (options?.compact && role === \"InlineTextBox\") {\n return false;\n }\n\n // Rule 4: Empty name AND empty/null value\n const name = node.name?.value ?? \"\";\n const value = node.value?.value ?? null;\n if (!name && (value === null || value === undefined || value === \"\")) {\n return false;\n }\n\n return true;\n}\n\n// ---------------------------------------------------------------------------\n// processAccessibilityTree\n// ---------------------------------------------------------------------------\n\n/**\n * Processes a flat array of AX nodes into a formatted, indented text tree.\n *\n * - Uses 2-space indentation per depth level.\n * - Caps traversal at maxDepth (default 10).\n * - Prevents cycles via a visited set.\n * - Orders children via childIds[] when present, falling back to children[].\n */\nexport function processAccessibilityTree(\n nodes: AXNode[],\n options: ProcessTreeOptions,\n): string {\n const maxDepth = options.maxDepth ?? 10;\n\n // Build lookup map: nodeId -> AXNode\n const nodeMap = new Map<string, AXNode>();\n for (const node of nodes) {\n nodeMap.set(node.nodeId, node);\n }\n\n // Find root node (first node without parentId, or first node)\n let root: AXNode | undefined;\n for (const node of nodes) {\n if (!node.parentId) {\n root = node;\n break;\n }\n }\n\n if (!root) {\n return \"\";\n }\n\n const lines: string[] = [];\n const visited = new Set<string>();\n\n function traverse(node: AXNode, depth: number): void {\n // Cycle prevention\n if (visited.has(node.nodeId)) {\n return;\n }\n visited.add(node.nodeId);\n\n // Depth cap\n if (depth > maxDepth) {\n return;\n }\n\n // Format node line\n const indent = \" \".repeat(depth);\n const role = node.role?.value ?? \"unknown\";\n const name = node.name?.value ?? \"\";\n const attrs = formatNodeAttributes(node);\n\n let line = `${indent}${role}`;\n if (name) {\n line += ` \"${name}\"`;\n }\n if (attrs) {\n line += ` ${attrs}`;\n }\n lines.push(line);\n\n // Resolve children: prefer childIds, fall back to children\n const childNodeIds = getChildNodeIds(node);\n\n for (const childId of childNodeIds) {\n const childNode = nodeMap.get(childId);\n if (childNode) {\n traverse(childNode, depth + 1);\n }\n }\n }\n\n traverse(root, 0);\n\n return lines.join(\"\\n\");\n}\n\n// ---------------------------------------------------------------------------\n// Helper: get child node IDs\n// ---------------------------------------------------------------------------\n\nfunction getChildNodeIds(node: AXNode): string[] {\n if (node.childIds && node.childIds.length > 0) {\n return node.childIds;\n }\n if (node.children && node.children.length > 0) {\n return node.children.map((c) => c.nodeId);\n }\n return [];\n}\n\n// ---------------------------------------------------------------------------\n// Helper: format node attributes\n// ---------------------------------------------------------------------------\n\nfunction formatNodeAttributes(node: AXNode): string {\n const parts: string[] = [];\n\n // Value\n if (node.value?.value !== undefined && node.value.value !== \"\") {\n parts.push(`value=\"${node.value.value}\"`);\n }\n\n // Description\n if (node.description?.value) {\n parts.push(`description=\"${node.description.value}\"`);\n }\n\n // Properties\n if (node.properties) {\n for (const prop of node.properties) {\n switch (prop.name) {\n case \"level\":\n parts.push(`level=${prop.value.value}`);\n break;\n case \"checked\": {\n const val = prop.value.value;\n if (val === true || val === \"true\" || val === \"mixed\") {\n parts.push(\"checked\");\n }\n break;\n }\n case \"selected\":\n if (prop.value.value === true) {\n parts.push(\"selected\");\n }\n break;\n case \"expanded\":\n if (prop.value.value === true || prop.value.value === \"true\") {\n parts.push(\"expanded\");\n } else {\n parts.push(\"collapsed\");\n }\n break;\n default:\n // Skip other properties\n break;\n }\n }\n }\n\n return parts.join(\" \");\n}\n\n// ---------------------------------------------------------------------------\n// browserSnapshot\n// ---------------------------------------------------------------------------\n\n/**\n * Takes an accessibility tree snapshot from the page, assigns @eN refs,\n * and returns formatted text output.\n *\n * @param cdp - CDP connection\n * @param params - Snapshot options\n * @returns SnapshotResult with snapshot text\n */\nexport async function browserSnapshot(\n cdp: CDPConnection,\n params?: Record<string, unknown>,\n): Promise<SnapshotResult> {\n const options: SnapshotOptions = {\n interactive: params?.interactive as boolean | undefined,\n cursor: params?.cursor as boolean | undefined,\n compact: params?.compact as boolean | undefined,\n depth: params?.depth as number | undefined,\n selector: params?.selector as string | undefined,\n };\n\n // Enable accessibility\n await cdp.send(\"Accessibility.enable\");\n\n // Get the accessibility tree\n let axNodes: AXNode[];\n\n if (options.selector) {\n // Scoped snapshot via CSS selector\n const docResult = (await cdp.send(\"DOM.getDocument\")) as {\n root: { nodeId: number };\n };\n const queryResult = (await cdp.send(\"DOM.querySelector\", {\n nodeId: docResult.root.nodeId,\n selector: options.selector,\n })) as { nodeId: number };\n\n if (queryResult.nodeId === 0) {\n return { snapshot: `No element found for selector: ${options.selector}` };\n }\n\n const partialResult = (await cdp.send(\"Accessibility.getPartialAXTree\", {\n nodeId: queryResult.nodeId,\n fetchRelatives: true,\n })) as { nodes: AXNode[] };\n axNodes = partialResult.nodes;\n } else {\n // Full page snapshot\n const fullResult = (await cdp.send(\"Accessibility.getFullAXTree\")) as {\n nodes: AXNode[];\n };\n axNodes = fullResult.nodes;\n }\n\n // Count non-root elements (exclude WebArea root)\n const totalElements = axNodes.filter(\n (n) => n.role?.value !== \"WebArea\",\n ).length;\n\n // Build a node map for tree traversal\n const nodeMap = new Map<string, AXNode>();\n for (const node of axNodes) {\n nodeMap.set(node.nodeId, node);\n }\n\n // Find root node\n let root: AXNode | undefined;\n for (const node of axNodes) {\n if (!node.parentId) {\n root = node;\n break;\n }\n }\n\n if (!root) {\n // If no root found, use first node\n root = axNodes[0];\n }\n\n if (!root) {\n return { snapshot: \"\" };\n }\n\n // Traverse the tree, filter, assign refs, and format\n const lines: string[] = [];\n let refCounter = 0;\n const visited = new Set<string>();\n const maxDepth = options.depth ?? 100;\n\n function traverse(node: AXNode, depth: number): void {\n // Cycle prevention\n if (visited.has(node.nodeId)) {\n return;\n }\n visited.add(node.nodeId);\n\n // Depth limit\n if (depth > maxDepth) {\n return;\n }\n\n const role = node.role?.value ?? \"\";\n const isRoot = role === \"WebArea\";\n\n // Apply filters\n const showNode = isRoot || shouldShow(node, options);\n\n if (showNode && !isRoot) {\n refCounter++;\n // Use backendDOMNodeId as ref so tools can resolve it directly\n const ref = node.backendDOMNodeId ? `@e${node.backendDOMNodeId}` : `@e${refCounter}`;\n const indent = \" \".repeat(depth);\n const name = node.name?.value ?? \"\";\n const attrs = formatNodeAttributes(node);\n\n let line = `${indent}${ref} ${role}`;\n if (name) {\n line += ` \"${name}\"`;\n }\n if (attrs) {\n line += ` ${attrs}`;\n }\n lines.push(line);\n }\n\n // Process children\n const childNodeIds = getChildNodeIds(node);\n const nextDepth = isRoot ? depth : depth + 1;\n\n for (const childId of childNodeIds) {\n const childNode = nodeMap.get(childId);\n if (childNode) {\n traverse(childNode, nextDepth);\n }\n }\n }\n\n traverse(root, 0);\n\n const snapshot = lines.join(\"\\n\");\n const result: SnapshotResult = { snapshot };\n\n // Add truncation info for large pages\n if (totalElements > 1000) {\n result.truncated = true;\n result.totalElements = totalElements;\n }\n\n return result;\n}\n\n// ---------------------------------------------------------------------------\n// Internal show filter (combines shouldShowAxNode with interactive/cursor)\n// ---------------------------------------------------------------------------\n\nfunction shouldShow(node: AXNode, options: SnapshotOptions): boolean {\n // Base filtering via shouldShowAxNode\n if (!shouldShowAxNode(node, { compact: options.compact })) {\n // In compact mode, skip filtered nodes entirely.\n // In normal mode, also skip.\n return false;\n }\n\n const role = node.role?.value ?? \"\";\n\n // Interactive filter: only show interactive roles\n if (options.interactive) {\n if (INTERACTIVE_ROLES.has(role)) {\n return true;\n }\n // If cursor mode is also on, check for cursor:pointer property\n if (options.cursor) {\n return hasCursorPointer(node);\n }\n return false;\n }\n\n // Cursor filter: include elements with cursor:pointer even if role is generic\n if (options.cursor) {\n if (hasCursorPointer(node)) {\n return true;\n }\n }\n\n return true;\n}\n\nfunction hasCursorPointer(node: AXNode): boolean {\n if (!node.properties) return false;\n return node.properties.some(\n (p) => p.name === \"cursor\" && p.value.value === \"pointer\",\n );\n}\n","/**\n * browser_screenshot tool — captures viewport or full-page screenshots via CDP.\n *\n * Supports:\n * - Viewport / full-page capture (Page.captureScreenshot)\n * - DPR detection cascade (3-level fallback)\n * - Element screenshot by CSS selector or @eN ref\n * - Annotated screenshots with ref labels\n * - Custom format (png/jpeg) and quality\n *\n * @module browser-screenshot\n */\nimport type { CDPConnection } from \"../cdp/connection\";\n\n// ---------------------------------------------------------------------------\n// Types\n// ---------------------------------------------------------------------------\n\ninterface ScreenshotParams {\n /** Capture the full scrollable page, not just the viewport. */\n fullPage?: boolean;\n /** Screenshot format. Default: \"png\". */\n format?: \"png\" | \"jpeg\";\n /** JPEG quality (0-100). Only applies when format is \"jpeg\". */\n quality?: number;\n /** CSS selector to screenshot a specific element. */\n selector?: string;\n /** @eN ref to screenshot a specific element. */\n ref?: string;\n /** Annotate interactive elements with numbered labels. */\n annotate?: boolean;\n}\n\ninterface Annotation {\n /** The @eN ref string, e.g. \"@e1\" */\n ref: string;\n /** The numbered label on the screenshot, e.g. \"[1]\" */\n label: string;\n /** Accessibility role */\n role?: string;\n /** Accessible name */\n name?: string;\n}\n\ninterface ScreenshotResult {\n /** Base64-encoded screenshot data. */\n base64: string;\n /** Annotation labels when `annotate: true`. */\n annotations?: Annotation[];\n}\n\n// ---------------------------------------------------------------------------\n// DPR Detection Cascade\n// ---------------------------------------------------------------------------\n\n/**\n * Detect device pixel ratio using a 3-level cascade:\n * Level 1: Page.getLayoutMetrics (visualViewport vs cssVisualViewport)\n * Level 2: Emulation.getDeviceMetricsOverride (deviceScaleFactor)\n * Level 3: Runtime.evaluate(\"window.devicePixelRatio\")\n * Default: 1\n */\nasync function detectDPR(cdp: CDPConnection): Promise<number> {\n // Level 1: Page.getLayoutMetrics\n try {\n const metrics = (await cdp.send(\"Page.getLayoutMetrics\", {})) as {\n visualViewport?: { clientWidth: number };\n cssVisualViewport?: { clientWidth: number };\n layoutViewport?: { clientWidth: number };\n contentSize?: { width: number };\n };\n\n if (metrics.visualViewport && metrics.cssVisualViewport) {\n const physicalWidth = metrics.visualViewport.clientWidth;\n const cssWidth = metrics.cssVisualViewport.clientWidth;\n if (cssWidth > 0 && physicalWidth > 0) {\n const dpr = physicalWidth / cssWidth;\n if (dpr >= 1) {\n return dpr;\n }\n }\n }\n } catch {\n // Level 1 failed, try Level 2\n }\n\n // Level 2: Runtime.evaluate\n try {\n const evalResult = (await cdp.send(\"Runtime.evaluate\", {\n expression: \"window.devicePixelRatio\",\n returnByValue: true,\n })) as {\n result: { type: string; value: unknown };\n };\n\n if (evalResult.result.type === \"number\" && typeof evalResult.result.value === \"number\") {\n return evalResult.result.value;\n }\n } catch {\n // Level 3 failed, use default\n }\n\n // Default: DPR = 1\n return 1;\n}\n\n// ---------------------------------------------------------------------------\n// Element bounding box\n// ---------------------------------------------------------------------------\n\ninterface BoundingBox {\n x: number;\n y: number;\n width: number;\n height: number;\n}\n\n/**\n * Get the bounding box of an element by CSS selector.\n */\nasync function getElementBoxBySelector(\n cdp: CDPConnection,\n selector: string,\n): Promise<BoundingBox> {\n const doc = (await cdp.send(\"DOM.getDocument\", {})) as {\n root: { nodeId: number };\n };\n\n const queryResult = (await cdp.send(\"DOM.querySelector\", {\n nodeId: doc.root.nodeId,\n selector,\n })) as { nodeId: number };\n\n if (!queryResult.nodeId) {\n throw new Error(`Element not found: ${selector}`);\n }\n\n const boxModel = (await cdp.send(\"DOM.getBoxModel\", {\n nodeId: queryResult.nodeId,\n })) as {\n model: { content: number[]; width: number; height: number };\n };\n\n const content = boxModel.model.content;\n // content is [x1,y1, x2,y2, x3,y3, x4,y4] (quad)\n const x = content[0];\n const y = content[1];\n const width = content[2] - content[0];\n const height = content[5] - content[1];\n\n return { x, y, width, height };\n}\n\n/**\n * Get the bounding box of an element by @eN ref (backendNodeId).\n */\nasync function getElementBoxByRef(\n cdp: CDPConnection,\n ref: string,\n): Promise<BoundingBox> {\n // Parse @eN → extract numeric ref index\n const match = /^@e(\\d+)$/.exec(ref);\n if (!match) {\n throw new Error(`Invalid ref format: ${ref}`);\n }\n\n const backendNodeId = parseInt(match[1], 10);\n\n // Resolve the backendNodeId to a remote object\n const resolved = (await cdp.send(\"DOM.resolveNode\", {\n backendNodeId,\n })) as { object: { objectId: string } };\n\n // Get the box model for the node\n const boxModel = (await cdp.send(\"DOM.getBoxModel\", {\n backendNodeId,\n })) as {\n model: { content: number[]; width: number; height: number };\n };\n\n const content = boxModel.model.content;\n const x = content[0];\n const y = content[1];\n const width = content[2] - content[0];\n const height = content[5] - content[1];\n\n return { x, y, width, height };\n}\n\n// ---------------------------------------------------------------------------\n// Annotated screenshot\n// ---------------------------------------------------------------------------\n\n/**\n * Build annotations from the accessibility tree for interactive elements.\n */\nasync function buildAnnotations(cdp: CDPConnection): Promise<Annotation[]> {\n const axTree = (await cdp.send(\"Accessibility.getFullAXTree\", {}, { timeout: 10000 })) as {\n nodes: Array<{\n nodeId: string;\n backendDOMNodeId?: number;\n role?: { type: string; value: string };\n name?: { type: string; value: string };\n }>;\n };\n\n const annotations: Annotation[] = [];\n let counter = 0;\n\n for (const node of axTree.nodes) {\n const role = node.role?.value;\n\n // Skip the root WebArea node\n if (role === \"WebArea\") {\n continue;\n }\n\n counter++;\n // Use backendDOMNodeId as ref so tools can resolve it directly\n const ref = node.backendDOMNodeId ? `@e${node.backendDOMNodeId}` : `@e${counter}`;\n const label = `[${counter}]`;\n\n annotations.push({\n ref,\n label,\n role: role ?? \"unknown\",\n name: node.name?.value ?? \"\",\n });\n }\n\n return annotations;\n}\n\n// ---------------------------------------------------------------------------\n// Main export\n// ---------------------------------------------------------------------------\n\n/**\n * Capture a screenshot of the browser page or a specific element.\n *\n * @param cdp - CDP connection.\n * @param params - Screenshot parameters.\n * @returns Base64-encoded screenshot data and optional annotations.\n */\nexport async function browserScreenshot(\n cdp: CDPConnection,\n params: ScreenshotParams,\n): Promise<ScreenshotResult> {\n const format = params.format ?? \"png\";\n\n // Build the captureScreenshot parameters\n const captureParams: Record<string, unknown> = {\n format,\n };\n\n // Quality only applies to jpeg\n if (format === \"jpeg\" && params.quality !== undefined) {\n captureParams.quality = params.quality;\n }\n\n // Detect DPR (needed for various calculations)\n const _dpr = await detectDPR(cdp);\n\n // Element screenshot by selector\n if (params.selector) {\n const box = await getElementBoxBySelector(cdp, params.selector);\n captureParams.clip = {\n x: box.x,\n y: box.y,\n width: box.width,\n height: box.height,\n scale: 1,\n };\n }\n\n // Element screenshot by @eN ref\n if (params.ref) {\n const box = await getElementBoxByRef(cdp, params.ref);\n captureParams.clip = {\n x: box.x,\n y: box.y,\n width: box.width,\n height: box.height,\n scale: 1,\n };\n }\n\n // Full page screenshot\n if (params.fullPage) {\n const metrics = (await cdp.send(\"Page.getLayoutMetrics\", {})) as {\n contentSize: { width: number; height: number };\n cssContentSize?: { width: number; height: number };\n layoutViewport: { clientWidth: number; clientHeight: number };\n };\n\n const contentWidth = metrics.cssContentSize?.width ?? metrics.contentSize.width;\n const contentHeight = metrics.cssContentSize?.height ?? metrics.contentSize.height;\n\n captureParams.clip = {\n x: 0,\n y: 0,\n width: contentWidth,\n height: contentHeight,\n scale: 1,\n };\n }\n\n // Capture the screenshot\n const screenshot = (await cdp.send(\"Page.captureScreenshot\", captureParams)) as {\n data: string;\n };\n\n const result: ScreenshotResult = {\n base64: screenshot.data,\n };\n\n // Annotated screenshot\n if (params.annotate) {\n result.annotations = await buildAnnotations(cdp);\n }\n\n return result;\n}\n","/**\n * browser_html tool — retrieves page or element HTML via CDP.\n *\n * Supports:\n * - Full page HTML (document.documentElement.outerHTML)\n * - Element HTML by CSS selector (DOM.querySelector + DOM.getOuterHTML)\n * - Graceful handling of missing selectors\n *\n * @module browser-html\n */\nimport type { CDPConnection } from \"../cdp/connection\";\n\n// ---------------------------------------------------------------------------\n// Types\n// ---------------------------------------------------------------------------\n\ninterface HtmlParams {\n /** CSS selector to retrieve outerHTML for a specific element. */\n selector?: string;\n}\n\ninterface HtmlResult {\n /** The retrieved HTML string. */\n html: string;\n /** Error message when element is not found. */\n error?: string;\n}\n\ninterface MarkdownParams {\n /** CSS selector to scope markdown extraction. */\n selector?: string;\n}\n\ninterface MarkdownResult {\n /** Extracted markdown content. */\n markdown: string;\n}\n\n// ---------------------------------------------------------------------------\n// HTML retrieval\n// ---------------------------------------------------------------------------\n\n/**\n * Retrieve the outer HTML of the page or a specific element.\n *\n * Without a selector, returns document.documentElement.outerHTML via\n * Runtime.evaluate.\n *\n * With a selector, uses DOM.getDocument → DOM.querySelector → DOM.getOuterHTML.\n *\n * @param cdp - CDP connection.\n * @param params - Optional selector parameter.\n * @returns The HTML string, or an error if the element is not found.\n */\nexport async function browserHtml(\n cdp: CDPConnection,\n params: HtmlParams,\n): Promise<HtmlResult> {\n // No selector: return full page HTML\n if (!params.selector) {\n const evalResult = (await cdp.send(\"Runtime.evaluate\", {\n expression: \"document.documentElement.outerHTML\",\n returnByValue: true,\n })) as {\n result: { type: string; value: string };\n };\n\n return { html: evalResult.result.value };\n }\n\n // With selector: use DOM methods\n const doc = (await cdp.send(\"DOM.getDocument\", {})) as {\n root: { nodeId: number };\n };\n\n const queryResult = (await cdp.send(\"DOM.querySelector\", {\n nodeId: doc.root.nodeId,\n selector: params.selector,\n })) as { nodeId: number };\n\n // nodeId 0 means element not found\n if (!queryResult.nodeId) {\n return {\n html: \"\",\n error: `Element not found: ${params.selector}`,\n };\n }\n\n const outerResult = (await cdp.send(\"DOM.getOuterHTML\", {\n nodeId: queryResult.nodeId,\n })) as { outerHTML: string };\n\n return { html: outerResult.outerHTML };\n}\n\n// ---------------------------------------------------------------------------\n// HTML → Markdown conversion\n// ---------------------------------------------------------------------------\n\n/**\n * Minimal HTML-to-Markdown converter.\n *\n * Handles:\n * - Headings (h1-h6) → # syntax\n * - Code blocks (<pre><code>) → fenced ``` blocks\n * - Tables → markdown table syntax with separator row\n * - Navigation/sidebar exclusion (<nav>, <aside>)\n * - Paragraphs → plain text with newlines\n */\nfunction htmlToMarkdown(html: string): string {\n let content = html;\n\n // Strip <nav> and <aside> elements and their contents\n content = content.replace(/<nav\\b[^>]*>[\\s\\S]*?<\\/nav>/gi, \"\");\n content = content.replace(/<aside\\b[^>]*>[\\s\\S]*?<\\/aside>/gi, \"\");\n\n const lines: string[] = [];\n\n // Process code blocks first (before stripping tags)\n content = content.replace(\n /<pre[^>]*>\\s*<code(?:\\s+class=\"language-(\\w+)\")?[^>]*>([\\s\\S]*?)<\\/code>\\s*<\\/pre>/gi,\n (_match, lang, code) => {\n const language = lang ?? \"\";\n const decoded = decodeHtmlEntities(code.trim());\n return `\\n\\`\\`\\`${language}\\n${decoded}\\n\\`\\`\\`\\n`;\n },\n );\n\n // Process tables\n content = content.replace(\n /<table[^>]*>([\\s\\S]*?)<\\/table>/gi,\n (_match, tableContent: string) => {\n const rows: string[][] = [];\n\n // Extract thead rows\n const theadMatch = tableContent.match(/<thead[^>]*>([\\s\\S]*?)<\\/thead>/i);\n if (theadMatch) {\n const headerRows = extractTableRows(theadMatch[1]);\n rows.push(...headerRows);\n }\n\n // Extract tbody rows\n const tbodyMatch = tableContent.match(/<tbody[^>]*>([\\s\\S]*?)<\\/tbody>/i);\n if (tbodyMatch) {\n const bodyRows = extractTableRows(tbodyMatch[1]);\n // Insert separator after header\n if (rows.length > 0 && bodyRows.length > 0) {\n const colCount = rows[0].length;\n const separator = Array(colCount).fill(\"---\");\n rows.push(separator);\n }\n rows.push(...bodyRows);\n }\n\n // If no thead/tbody, extract all rows directly\n if (!theadMatch && !tbodyMatch) {\n const allRows = extractTableRows(tableContent);\n if (allRows.length > 1) {\n const colCount = allRows[0].length;\n const separator = Array(colCount).fill(\"---\");\n const result = [allRows[0], separator, ...allRows.slice(1)];\n return \"\\n\" + result.map((r) => \"| \" + r.join(\" | \") + \" |\").join(\"\\n\") + \"\\n\";\n }\n return \"\\n\" + allRows.map((r) => \"| \" + r.join(\" | \") + \" |\").join(\"\\n\") + \"\\n\";\n }\n\n return \"\\n\" + rows.map((r) => \"| \" + r.join(\" | \") + \" |\").join(\"\\n\") + \"\\n\";\n },\n );\n\n // Process headings\n content = content.replace(/<h1[^>]*>([\\s\\S]*?)<\\/h1>/gi, (_m, text) => `\\n# ${stripTags(text).trim()}\\n`);\n content = content.replace(/<h2[^>]*>([\\s\\S]*?)<\\/h2>/gi, (_m, text) => `\\n## ${stripTags(text).trim()}\\n`);\n content = content.replace(/<h3[^>]*>([\\s\\S]*?)<\\/h3>/gi, (_m, text) => `\\n### ${stripTags(text).trim()}\\n`);\n content = content.replace(/<h4[^>]*>([\\s\\S]*?)<\\/h4>/gi, (_m, text) => `\\n#### ${stripTags(text).trim()}\\n`);\n content = content.replace(/<h5[^>]*>([\\s\\S]*?)<\\/h5>/gi, (_m, text) => `\\n##### ${stripTags(text).trim()}\\n`);\n content = content.replace(/<h6[^>]*>([\\s\\S]*?)<\\/h6>/gi, (_m, text) => `\\n###### ${stripTags(text).trim()}\\n`);\n\n // Process paragraphs\n content = content.replace(/<p[^>]*>([\\s\\S]*?)<\\/p>/gi, (_m, text) => `\\n${stripTags(text).trim()}\\n`);\n\n // Strip remaining HTML tags\n content = stripTags(content);\n\n // Decode HTML entities\n content = decodeHtmlEntities(content);\n\n // Normalize whitespace: collapse multiple blank lines\n content = content.replace(/\\n{3,}/g, \"\\n\\n\");\n\n return content.trim();\n}\n\n/**\n * Extract rows from an HTML table section.\n */\nfunction extractTableRows(html: string): string[][] {\n const rows: string[][] = [];\n const rowRegex = /<tr[^>]*>([\\s\\S]*?)<\\/tr>/gi;\n let rowMatch: RegExpExecArray | null;\n\n while ((rowMatch = rowRegex.exec(html)) !== null) {\n const cells: string[] = [];\n const cellRegex = /<(?:td|th)[^>]*>([\\s\\S]*?)<\\/(?:td|th)>/gi;\n let cellMatch: RegExpExecArray | null;\n\n while ((cellMatch = cellRegex.exec(rowMatch[1])) !== null) {\n cells.push(stripTags(cellMatch[1]).trim());\n }\n\n if (cells.length > 0) {\n rows.push(cells);\n }\n }\n\n return rows;\n}\n\n/**\n * Strip all HTML tags from a string.\n */\nfunction stripTags(html: string): string {\n return html.replace(/<[^>]+>/g, \"\");\n}\n\n/**\n * Decode common HTML entities.\n */\nfunction decodeHtmlEntities(text: string): string {\n return text\n .replace(/&/g, \"&\")\n .replace(/</g, \"<\")\n .replace(/>/g, \">\")\n .replace(/"/g, '\"')\n .replace(/'/g, \"'\")\n .replace(/ /g, \" \");\n}\n\n// ---------------------------------------------------------------------------\n// extractContentAsMarkdown\n// ---------------------------------------------------------------------------\n\n/**\n * Extract the page content as Markdown.\n *\n * Fetches the full page HTML via Runtime.evaluate, then converts to markdown\n * using a lightweight HTML-to-Markdown converter that:\n * - Converts headings to # syntax\n * - Wraps code blocks in fenced markdown\n * - Converts tables to markdown tables\n * - Excludes nav/aside elements\n *\n * @param cdp - CDP connection.\n * @param params - Optional selector to scope extraction.\n * @returns Markdown string.\n */\nexport async function extractContentAsMarkdown(\n cdp: CDPConnection,\n params: MarkdownParams,\n): Promise<MarkdownResult> {\n // Get the HTML content\n const evalResult = (await cdp.send(\"Runtime.evaluate\", {\n expression: \"document.documentElement.outerHTML\",\n returnByValue: true,\n })) as {\n result: { type: string; value: string };\n };\n\n const html = evalResult.result.value;\n const markdown = htmlToMarkdown(html);\n\n return { markdown };\n}\n","/**\n * browser_eval tool — evaluates JavaScript expressions in the browser via CDP.\n *\n * Supports:\n * - Simple expression evaluation (Runtime.evaluate)\n * - Element-scoped evaluation via @eN refs (Runtime.callFunctionOn)\n * - Async expressions with awaitPromise\n * - Error handling (ReferenceError, TypeError, etc.)\n * - DOM node serialization to string description\n * - Primitive type handling (null, undefined, boolean)\n * - Multi-line expressions (stdin mode)\n * - Base64-encoded expressions\n *\n * @module browser-eval\n */\nimport type { CDPConnection } from \"../cdp/connection\";\n\n// ---------------------------------------------------------------------------\n// Types\n// ---------------------------------------------------------------------------\n\ninterface EvalParams {\n /** JavaScript expression to evaluate. */\n expression: string;\n /** @eN ref for element-scoped evaluation via callFunctionOn. */\n ref?: string;\n /** Whether the expression is multi-line (stdin mode). */\n stdin?: boolean;\n /** Whether the expression is base64-encoded. */\n base64?: boolean;\n}\n\ninterface EvalResult {\n /** The evaluation result value. */\n result?: unknown;\n /** Error message if evaluation failed. */\n error?: string;\n}\n\n// ---------------------------------------------------------------------------\n// CDP result parsing\n// ---------------------------------------------------------------------------\n\ninterface CDPRemoteObject {\n type: string;\n subtype?: string;\n className?: string;\n description?: string;\n value?: unknown;\n objectId?: string;\n}\n\ninterface CDPExceptionDetails {\n exceptionId: number;\n text: string;\n lineNumber: number;\n columnNumber: number;\n exception?: {\n type: string;\n subtype?: string;\n className?: string;\n description?: string;\n };\n}\n\n/**\n * Parse a CDP RemoteObject into a JavaScript value.\n *\n * Handles:\n * - Primitives (string, number, boolean)\n * - null (subtype \"null\")\n * - undefined (type \"undefined\")\n * - DOM nodes (subtype \"node\") → serialized to string description\n * - Objects → return the value directly\n */\nfunction parseRemoteObject(obj: CDPRemoteObject): unknown {\n // undefined\n if (obj.type === \"undefined\") {\n return undefined;\n }\n\n // null\n if (obj.type === \"object\" && obj.subtype === \"null\") {\n return null;\n }\n\n // DOM node\n if (obj.type === \"object\" && obj.subtype === \"node\") {\n return obj.description ?? `[${obj.className ?? \"Node\"}]`;\n }\n\n // Primitives and objects with value\n if (obj.value !== undefined) {\n return obj.value;\n }\n\n // Object without value (shouldn't happen with returnByValue: true)\n if (obj.description) {\n return obj.description;\n }\n\n return undefined;\n}\n\n/**\n * Extract error message from CDP exception details.\n */\nfunction formatException(details: CDPExceptionDetails): string {\n if (details.exception?.description) {\n return details.exception.description;\n }\n\n if (details.exception?.className) {\n return `${details.exception.className}: ${details.text}`;\n }\n\n return details.text;\n}\n\n// ---------------------------------------------------------------------------\n// Main export\n// ---------------------------------------------------------------------------\n\n/**\n * Evaluate a JavaScript expression in the browser context.\n *\n * Without a ref, uses `Runtime.evaluate` for global scope evaluation.\n * With a ref (@eN), uses `DOM.resolveNode` + `Runtime.callFunctionOn`\n * for element-scoped evaluation.\n *\n * @param cdp - CDP connection.\n * @param params - Expression and optional ref/flags.\n * @returns Evaluation result or error.\n */\nexport async function browserEval(\n cdp: CDPConnection,\n params: EvalParams,\n): Promise<EvalResult> {\n let expression = params.expression;\n\n // Decode base64-encoded expression\n if (params.base64) {\n expression = atob(expression);\n }\n\n // Element-scoped evaluation via @eN ref\n if (params.ref) {\n return evalWithRef(cdp, expression, params.ref);\n }\n\n // Global scope evaluation\n return evalGlobal(cdp, expression);\n}\n\n/**\n * Evaluate an expression in the global scope via Runtime.evaluate.\n */\nasync function evalGlobal(\n cdp: CDPConnection,\n expression: string,\n): Promise<EvalResult> {\n const response = (await cdp.send(\"Runtime.evaluate\", {\n expression,\n returnByValue: true,\n awaitPromise: true,\n })) as {\n result: CDPRemoteObject;\n exceptionDetails?: CDPExceptionDetails;\n };\n\n // Check for exceptions\n if (response.exceptionDetails) {\n return {\n error: formatException(response.exceptionDetails),\n };\n }\n\n const value = parseRemoteObject(response.result);\n\n return { result: value };\n}\n\n/**\n * Evaluate a function on a specific element via Runtime.callFunctionOn.\n *\n * Resolves the @eN ref to a backendNodeId, then resolves to a remote object,\n * and calls the function on it.\n */\nasync function evalWithRef(\n cdp: CDPConnection,\n functionDeclaration: string,\n ref: string,\n): Promise<EvalResult> {\n // Parse the @eN ref\n const match = /^@e(\\d+)$/.exec(ref);\n if (!match) {\n return { error: `Invalid ref format: ${ref}` };\n }\n\n const backendNodeId = parseInt(match[1], 10);\n\n // Resolve backendNodeId to a remote object\n const resolved = (await cdp.send(\"DOM.resolveNode\", {\n backendNodeId,\n })) as { object: { objectId: string } };\n\n const objectId = resolved.object.objectId;\n\n // Call the function on the element\n const response = (await cdp.send(\"Runtime.callFunctionOn\", {\n objectId,\n functionDeclaration,\n returnByValue: true,\n awaitPromise: true,\n })) as {\n result: CDPRemoteObject;\n exceptionDetails?: CDPExceptionDetails;\n };\n\n // Check for exceptions\n if (response.exceptionDetails) {\n return {\n error: formatException(response.exceptionDetails),\n };\n }\n\n const value = parseRemoteObject(response.result);\n\n return { result: value };\n}\n","/**\n * browser_find — Semantic element locators via CDP Accessibility and Runtime APIs.\n *\n * Provides Playwright-style locator functions: findByRole, findByText, findByLabel,\n * findByPlaceholder, findByAlt, findByTitle, findByTestId, findFirst, findLast, findNth.\n *\n * Also provides a unified `browserFind` function for the MCP tool that finds elements\n * by ARIA role, accessible name, or text content and returns @eN refs.\n *\n * Role and text-based locators use the Accessibility tree (Accessibility.getFullAXTree).\n * Attribute-based locators use Runtime.evaluate with CSS selectors.\n * Positional locators (first/last/nth) use querySelectorAll.\n */\nimport type { CDPConnection } from \"../cdp/connection.js\";\n\n// ---------------------------------------------------------------------------\n// Types\n// ---------------------------------------------------------------------------\n\nexport interface FindResult {\n /** Whether an element was found. */\n found: boolean;\n /** The backend node ID of the found element. */\n backendNodeId?: number;\n /** The index of the found element (for positional locators). */\n index?: number;\n}\n\nexport interface FindByRoleParams {\n /** ARIA role to search for (e.g., \"button\", \"link\"). */\n role: string;\n /** Accessible name to match. */\n name?: string;\n}\n\nexport interface FindByTextParams {\n /** Text content to search for. */\n text: string;\n /** If true, require exact text match (not substring). */\n exact?: boolean;\n}\n\nexport interface FindByLabelParams {\n /** Label text to search for. */\n label: string;\n}\n\nexport interface FindByPlaceholderParams {\n /** Placeholder text to search for. */\n placeholder: string;\n}\n\nexport interface FindByAltParams {\n /** Alt text to search for. */\n alt: string;\n}\n\nexport interface FindByTitleParams {\n /** Title attribute to search for. */\n title: string;\n}\n\nexport interface FindByTestIdParams {\n /** data-testid value to search for. */\n testId: string;\n}\n\nexport interface FindPositionalParams {\n /** CSS selector to match elements. */\n selector: string;\n}\n\nexport interface FindNthParams extends FindPositionalParams {\n /** Zero-based index of the element to select. */\n n: number;\n}\n\n// ---------------------------------------------------------------------------\n// Accessibility tree types\n// ---------------------------------------------------------------------------\n\ninterface AXNode {\n nodeId: string;\n role: { type: string; value: string };\n name?: { type: string; value: string };\n backendDOMNodeId?: number;\n}\n\n// ---------------------------------------------------------------------------\n// Exported functions\n// ---------------------------------------------------------------------------\n\n/**\n * Finds an element by ARIA role and optional accessible name.\n *\n * Uses Accessibility.getFullAXTree and filters by role/name.\n */\nexport async function findByRole(\n cdp: CDPConnection,\n params: FindByRoleParams,\n): Promise<FindResult> {\n const response = (await cdp.send(\"Accessibility.getFullAXTree\")) as {\n nodes: AXNode[];\n };\n\n const match = response.nodes.find((node) => {\n if (node.role.value !== params.role) return false;\n if (params.name && node.name?.value !== params.name) return false;\n return true;\n });\n\n if (!match) {\n return { found: false };\n }\n\n return {\n found: true,\n backendNodeId: match.backendDOMNodeId,\n };\n}\n\n/**\n * Finds an element by its text content.\n *\n * Uses Accessibility.getFullAXTree and filters by accessible name.\n */\nexport async function findByText(\n cdp: CDPConnection,\n params: FindByTextParams,\n): Promise<FindResult> {\n const response = (await cdp.send(\"Accessibility.getFullAXTree\")) as {\n nodes: AXNode[];\n };\n\n const match = response.nodes.find((node) => {\n if (!node.name?.value) return false;\n if (params.exact) {\n return node.name.value === params.text;\n }\n return node.name.value.includes(params.text);\n });\n\n if (!match) {\n return { found: false };\n }\n\n return {\n found: true,\n backendNodeId: match.backendDOMNodeId,\n };\n}\n\n/**\n * Finds an element by its associated label text.\n *\n * Uses Runtime.evaluate to find an element via label association.\n */\nexport async function findByLabel(\n cdp: CDPConnection,\n params: FindByLabelParams,\n): Promise<FindResult> {\n const escaped = JSON.stringify(params.label);\n\n const response = (await cdp.send(\"Runtime.evaluate\", {\n expression: `(() => {\n const labels = document.querySelectorAll('label');\n for (const label of labels) {\n if (label.textContent?.trim() === ${escaped}) {\n if (label.htmlFor) {\n return document.getElementById(label.htmlFor);\n }\n return label.querySelector('input, select, textarea');\n }\n }\n return null;\n })()`,\n returnByValue: false,\n })) as { result: { type: string; objectId?: string } };\n\n if (!response.result.objectId) {\n return { found: false };\n }\n\n return { found: true };\n}\n\n/**\n * Finds an element by its placeholder attribute.\n *\n * Uses Runtime.evaluate with an attribute selector.\n */\nexport async function findByPlaceholder(\n cdp: CDPConnection,\n params: FindByPlaceholderParams,\n): Promise<FindResult> {\n const escaped = JSON.stringify(params.placeholder);\n\n const response = (await cdp.send(\"Runtime.evaluate\", {\n expression: `document.querySelector('[placeholder=${escaped}]')`,\n returnByValue: false,\n })) as { result: { type: string; objectId?: string } };\n\n if (!response.result.objectId) {\n return { found: false };\n }\n\n return { found: true };\n}\n\n/**\n * Finds an element by its alt text attribute.\n *\n * Uses Runtime.evaluate with an attribute selector.\n */\nexport async function findByAlt(\n cdp: CDPConnection,\n params: FindByAltParams,\n): Promise<FindResult> {\n const escaped = JSON.stringify(params.alt);\n\n const response = (await cdp.send(\"Runtime.evaluate\", {\n expression: `document.querySelector('[alt=${escaped}]')`,\n returnByValue: false,\n })) as { result: { type: string; objectId?: string } };\n\n if (!response.result.objectId) {\n return { found: false };\n }\n\n return { found: true };\n}\n\n/**\n * Finds an element by its title attribute.\n *\n * Uses Runtime.evaluate with an attribute selector.\n */\nexport async function findByTitle(\n cdp: CDPConnection,\n params: FindByTitleParams,\n): Promise<FindResult> {\n const escaped = JSON.stringify(params.title);\n\n const response = (await cdp.send(\"Runtime.evaluate\", {\n expression: `document.querySelector('[title=${escaped}]')`,\n returnByValue: false,\n })) as { result: { type: string; objectId?: string } };\n\n if (!response.result.objectId) {\n return { found: false };\n }\n\n return { found: true };\n}\n\n/**\n * Finds an element by its data-testid attribute.\n *\n * Uses Runtime.evaluate with a data attribute selector.\n */\nexport async function findByTestId(\n cdp: CDPConnection,\n params: FindByTestIdParams,\n): Promise<FindResult> {\n const escaped = JSON.stringify(params.testId);\n\n const response = (await cdp.send(\"Runtime.evaluate\", {\n expression: `document.querySelector('[data-testid=${escaped}]')`,\n returnByValue: false,\n })) as { result: { type: string; objectId?: string } };\n\n if (!response.result.objectId) {\n return { found: false };\n }\n\n return { found: true };\n}\n\n/**\n * Finds the first element matching a CSS selector.\n *\n * @returns FindResult with index=0\n */\nexport async function findFirst(\n cdp: CDPConnection,\n params: FindPositionalParams,\n): Promise<FindResult> {\n const escaped = JSON.stringify(params.selector);\n\n const response = (await cdp.send(\"Runtime.evaluate\", {\n expression: `document.querySelectorAll(${escaped})[0]`,\n returnByValue: false,\n })) as { result: { type: string; objectId?: string } };\n\n if (!response.result.objectId) {\n return { found: false };\n }\n\n return { found: true, index: 0 };\n}\n\n/**\n * Finds the last element matching a CSS selector.\n *\n * @returns FindResult with the last element's index\n */\nexport async function findLast(\n cdp: CDPConnection,\n params: FindPositionalParams,\n): Promise<FindResult> {\n const escaped = JSON.stringify(params.selector);\n\n const response = (await cdp.send(\"Runtime.evaluate\", {\n expression: `(() => {\n const els = document.querySelectorAll(${escaped});\n return els.length > 0 ? els[els.length - 1] : null;\n })()`,\n returnByValue: false,\n })) as { result: { type: string; objectId?: string } };\n\n if (!response.result.objectId) {\n return { found: false };\n }\n\n return { found: true };\n}\n\n/**\n * Finds the nth element matching a CSS selector.\n *\n * @param params.n - Zero-based index\n * @returns FindResult with the nth element\n */\nexport async function findNth(\n cdp: CDPConnection,\n params: FindNthParams,\n): Promise<FindResult> {\n const escaped = JSON.stringify(params.selector);\n\n const response = (await cdp.send(\"Runtime.evaluate\", {\n expression: `document.querySelectorAll(${escaped})[${params.n}]`,\n returnByValue: false,\n })) as { result: { type: string; objectId?: string } };\n\n if (!response.result.objectId) {\n return { found: false };\n }\n\n return { found: true, index: params.n };\n}\n\n// ---------------------------------------------------------------------------\n// Unified browserFind — MCP tool entry point\n// ---------------------------------------------------------------------------\n\nexport interface BrowserFindParams {\n /** ARIA role to search for (e.g., \"button\", \"link\", \"heading\", \"textbox\"). */\n role?: string;\n /** Accessible name to match (substring, case-sensitive). */\n name?: string;\n /** Text content to search for (substring match on accessible name). */\n text?: string;\n /** Zero-based index to pick the nth match (default 0 = first). */\n nth?: number;\n}\n\nexport interface BrowserFindResult {\n /** Whether a matching element was found. */\n found: boolean;\n /** @eN ref for use with other browsirai tools, or null if not found. */\n ref: string | null;\n /** ARIA role of the matched element, or null if not found. */\n role: string | null;\n /** Accessible name of the matched element, or null if not found. */\n name: string | null;\n /** Total number of matching elements. */\n count: number;\n}\n\n/**\n * Finds elements by ARIA role, accessible name, or text content.\n *\n * Uses Accessibility.getFullAXTree to walk the AX tree, filters by\n * role (case-insensitive) and/or name (substring) and/or text content,\n * and returns the nth match (default first) with an @eN ref.\n */\nexport async function browserFind(\n cdp: CDPConnection,\n params: BrowserFindParams,\n): Promise<BrowserFindResult> {\n const nth = params.nth ?? 0;\n\n // Get the full accessibility tree\n const response = (await cdp.send(\"Accessibility.getFullAXTree\", undefined, {\n timeout: 10000,\n })) as {\n nodes: AXNode[];\n };\n\n // Filter nodes by role, name, and/or text\n const matches = response.nodes.filter((node) => {\n // Filter by role (case-insensitive)\n if (params.role) {\n const nodeRole = node.role?.value ?? \"\";\n if (nodeRole.toLowerCase() !== params.role.toLowerCase()) {\n return false;\n }\n }\n\n // Filter by name (substring match)\n if (params.name) {\n const nodeName = node.name?.value ?? \"\";\n if (!nodeName.includes(params.name)) {\n return false;\n }\n }\n\n // Filter by text content (substring match on accessible name)\n if (params.text) {\n const nodeName = node.name?.value ?? \"\";\n if (!nodeName.includes(params.text)) {\n return false;\n }\n }\n\n return true;\n });\n\n const count = matches.length;\n\n // Pick the nth match\n if (nth >= count || count === 0) {\n return {\n found: false,\n ref: null,\n role: null,\n name: null,\n count,\n };\n }\n\n const match = matches[nth];\n const ref = match.backendDOMNodeId ? `@e${match.backendDOMNodeId}` : null;\n\n return {\n found: true,\n ref,\n role: match.role?.value ?? null,\n name: match.name?.value ?? null,\n count,\n };\n}\n","/**\n * browser_inspect_source tool — maps DOM elements to their source code locations.\n *\n * CDP-native resolution:\n * - React: Walk Fiber tree, parse jsxDEV() calls in Function.toString()\n * to extract fileName/lineNumber embedded by Babel jsx-source plugin.\n * - Vue: Read __vueParentComponent.type.__file\n * - Svelte: Read __svelte_meta.loc\n *\n * Works with React, Vue, Svelte frameworks.\n */\nimport type { CDPConnection } from \"../cdp/connection\";\n\n// ---------------------------------------------------------------------------\n// Types\n// ---------------------------------------------------------------------------\n\nexport interface InspectSourceParams {\n /** @eN ref from accessibility snapshot */\n ref?: string;\n /** CSS selector to find the element */\n selector?: string;\n}\n\nexport interface SourceLocation {\n filePath: string;\n lineNumber: number | null;\n columnNumber: number | null;\n componentName: string | null;\n}\n\nexport interface InspectSourceResult {\n tagName: string;\n componentName: string | null;\n source: SourceLocation | null;\n stack: SourceLocation[];\n}\n\n// ---------------------------------------------------------------------------\n// Ref pattern\n// ---------------------------------------------------------------------------\n\nconst REF_PATTERN = /^@?e(\\d+)$/;\n\n// ---------------------------------------------------------------------------\n// Main export\n// ---------------------------------------------------------------------------\n\n/**\n * Inspects a DOM element and returns its source code location.\n *\n * Strategy:\n * 1. Resolve element via ref or selector\n * 2. Walk React Fiber tree → parse Function.toString() for fileName/lineNumber\n * 3. Check Vue (__vueParentComponent) and Svelte (__svelte_meta)\n */\nexport async function browserInspectSource(\n cdp: CDPConnection,\n params: InspectSourceParams,\n): Promise<InspectSourceResult> {\n // 1. Resolve element\n let objectId: string;\n\n if (params.ref) {\n const match = REF_PATTERN.exec(params.ref);\n if (!match) throw new Error(`Invalid ref format: ${params.ref}`);\n const backendNodeId = parseInt(match[1], 10);\n const resolved = (await cdp.send(\"DOM.resolveNode\", { backendNodeId })) as {\n object: { objectId: string };\n };\n objectId = resolved.object.objectId;\n } else if (params.selector) {\n const evalResult = (await cdp.send(\"Runtime.evaluate\", {\n expression: `document.querySelector(${JSON.stringify(params.selector)})`,\n returnByValue: false,\n })) as { result: { objectId?: string; subtype?: string } };\n\n if (!evalResult.result.objectId || evalResult.result.subtype === \"null\") {\n throw new Error(`Element not found: ${params.selector}`);\n }\n objectId = evalResult.result.objectId;\n } else {\n throw new Error(\"Either ref or selector must be provided\");\n }\n\n // 2. CDP-native: Walk Fiber tree + parse Function.toString()\n const cdpResult = (await cdp.send(\"Runtime.callFunctionOn\", {\n objectId,\n functionDeclaration: `function() {\n var el = this;\n var tagName = (el.tagName || '').toLowerCase();\n\n // Find React Fiber\n var fiberKey = Object.keys(el).find(function(k) { return k.startsWith('__reactFiber'); });\n\n // Also check Vue, Svelte\n var vueComp = el.__vueParentComponent;\n var svelteMeta = el.__svelte_meta;\n\n if (!fiberKey && !vueComp && !svelteMeta) {\n return JSON.stringify({ tagName: tagName, componentName: null, source: null, stack: [], framework: null });\n }\n\n // --- React path ---\n if (fiberKey) {\n var fiber = el[fiberKey];\n var stack = [];\n var current = fiber;\n var firstSource = null;\n var firstName = null;\n\n while (current && stack.length < 15) {\n if (typeof current.type === 'function' && current.type.name) {\n var fn = current.type;\n var fnStr = fn.toString();\n var fileName = null;\n var lineNumber = null;\n var columnNumber = null;\n\n // Parse jsxDEV calls for embedded fileName/lineNumber\n var fileMatch = fnStr.match(/fileName:\\\\s*\"([^\"]+)\"/);\n if (fileMatch) {\n fileName = fileMatch[1];\n var lineMatch = fnStr.match(/lineNumber:\\\\s*(\\\\d+)/);\n if (lineMatch) lineNumber = parseInt(lineMatch[1]);\n var colMatch = fnStr.match(/columnNumber:\\\\s*(\\\\d+)/);\n if (colMatch) columnNumber = parseInt(colMatch[1]);\n }\n\n var entry = {\n filePath: fileName,\n lineNumber: lineNumber,\n columnNumber: columnNumber,\n componentName: fn.name\n };\n\n stack.push(entry);\n\n if (fileName && !firstSource) {\n firstSource = entry;\n }\n if (!firstName && fn.name.length > 1) {\n firstName = fn.name;\n }\n }\n current = current.return;\n }\n\n return JSON.stringify({\n tagName: tagName,\n componentName: firstName || null,\n source: firstSource || (stack.length > 0 ? { filePath: null, lineNumber: null, columnNumber: null, componentName: stack[0].componentName } : null),\n stack: stack.filter(function(s) { return s.filePath; }),\n framework: 'react'\n });\n }\n\n // --- Svelte path ---\n if (svelteMeta) {\n var loc = svelteMeta.loc || {};\n return JSON.stringify({\n tagName: tagName,\n componentName: loc.char ? null : (svelteMeta.component || null),\n source: loc.file ? { filePath: loc.file, lineNumber: loc.line || null, columnNumber: (loc.column || 0) + 1, componentName: null } : null,\n stack: [],\n framework: 'svelte'\n });\n }\n\n // --- Vue path ---\n if (vueComp) {\n var comp = vueComp;\n var vueName = comp.type?.__name || comp.type?.name || null;\n var vueFile = comp.type?.__file || null;\n return JSON.stringify({\n tagName: tagName,\n componentName: vueName,\n source: vueFile ? { filePath: vueFile, lineNumber: null, columnNumber: null, componentName: vueName } : null,\n stack: [],\n framework: 'vue'\n });\n }\n\n return JSON.stringify({ tagName: tagName, componentName: null, source: null, stack: [], framework: null });\n }`,\n returnByValue: true,\n })) as { result: { value: string } };\n\n return JSON.parse(cdpResult.result.value) as InspectSourceResult;\n}\n","/**\n * Fixed-size circular (ring) buffer for browser events.\n *\n * When the buffer reaches capacity the oldest events are silently evicted,\n * keeping memory usage bounded while still giving callers access to the\n * most-recent N events in chronological order.\n */\nexport class EventBuffer<T = unknown> {\n private _buffer: (T | undefined)[];\n private _capacity: number;\n private _head: number; // next write index\n private _size: number;\n private _totalPushed: number;\n\n constructor(capacity: number = 500) {\n this._capacity = capacity;\n this._buffer = new Array(capacity);\n this._head = 0;\n this._size = 0;\n this._totalPushed = 0;\n }\n\n /** Append an event, evicting the oldest if at capacity. */\n push(event: T): void {\n this._buffer[this._head] = event;\n this._head = (this._head + 1) % this._capacity;\n if (this._size < this._capacity) {\n this._size++;\n }\n this._totalPushed++;\n }\n\n /**\n * Return the last `n` events in chronological (oldest-first) order.\n * Defaults to all events when `n` is omitted.\n */\n last(n?: number): T[] {\n const count = n === undefined ? this._size : Math.min(n, this._size);\n if (count === 0) return [];\n\n const result: T[] = new Array(count);\n // The oldest of the `count` events starts at:\n let start = (this._head - count + this._capacity) % this._capacity;\n for (let i = 0; i < count; i++) {\n result[i] = this._buffer[(start + i) % this._capacity] as T;\n }\n return result;\n }\n\n /** Remove all events from the buffer. */\n clear(): void {\n this._buffer = new Array(this._capacity);\n this._head = 0;\n this._size = 0;\n }\n\n /**\n * Return the last `n` events AND remove them from the buffer.\n * Defaults to all events when `n` is omitted.\n */\n drain(n?: number): T[] {\n const events = this.last(n);\n this.clear();\n return events;\n }\n\n /** Current number of events stored. */\n get size(): number {\n return this._size;\n }\n\n /** Maximum number of events this buffer can hold. */\n get capacity(): number {\n return this._capacity;\n }\n\n /** Total number of events ever pushed (including evicted ones). */\n get totalPushed(): number {\n return this._totalPushed;\n }\n\n /** Return events matching `predicate` without modifying the buffer. */\n filter(predicate: (event: T) => boolean): T[] {\n return this.last().filter(predicate);\n }\n\n /** Snapshot of buffer statistics. */\n get stats(): { size: number; capacity: number; totalPushed: number; evicted: number } {\n return {\n size: this._size,\n capacity: this._capacity,\n totalPushed: this._totalPushed,\n evicted: this._totalPushed - this._size,\n };\n }\n}\n","/**\n * Secret redaction utilities for browsirai.\n *\n * Redacts sensitive values from headers, JSON bodies, inline text,\n * and network event objects to prevent secret leakage in logs/output.\n */\n\n// ---------------------------------------------------------------------------\n// Types\n// ---------------------------------------------------------------------------\n\nexport interface RedactOptions {\n enabled?: boolean;\n}\n\nexport interface NetworkEvent {\n url: string;\n method: string;\n status?: number;\n headers?: Record<string, string>;\n responseHeaders?: Record<string, string>;\n body?: string;\n [key: string]: unknown;\n}\n\n// ---------------------------------------------------------------------------\n// Constants\n// ---------------------------------------------------------------------------\n\nconst SENSITIVE_HEADERS = new Set([\n \"authorization\",\n \"cookie\",\n \"set-cookie\",\n \"x-api-key\",\n \"x-auth-token\",\n \"x-csrf-token\",\n \"x-xsrf-token\",\n \"proxy-authorization\",\n \"x-access-token\",\n \"x-refresh-token\",\n \"x-secret\",\n \"x-token\",\n]);\n\nconst SENSITIVE_BODY_KEYS = new Set([\n \"password\",\n \"secret\",\n \"token\",\n \"api_key\",\n \"apiKey\",\n \"api-key\",\n \"access_token\",\n \"refresh_token\",\n \"client_secret\",\n \"private_key\",\n]);\n\nconst JWT_PATTERN =\n /eyJ[A-Za-z0-9_-]{10,}\\.[A-Za-z0-9_-]{10,}\\.[A-Za-z0-9_-]+/g;\n\nconst BEARER_PATTERN = /Bearer\\s+\\S+/gi;\n\nconst REDACTED = \"[REDACTED]\";\nconst REDACTED_JWT = \"[REDACTED_JWT]\";\n\n// ---------------------------------------------------------------------------\n// redactHeaders\n// ---------------------------------------------------------------------------\n\nexport function redactHeaders(\n headers: Record<string, string>,\n opts?: RedactOptions,\n): Record<string, string> {\n if (opts?.enabled === false) {\n return { ...headers };\n }\n\n const result: Record<string, string> = {};\n for (const [key, value] of Object.entries(headers)) {\n if (SENSITIVE_HEADERS.has(key.toLowerCase())) {\n result[key] = REDACTED;\n } else {\n result[key] = value;\n }\n }\n return result;\n}\n\n// ---------------------------------------------------------------------------\n// redactBody\n// ---------------------------------------------------------------------------\n\nfunction redactObjectKeys(obj: unknown): unknown {\n if (obj === null || obj === undefined || typeof obj !== \"object\") {\n return obj;\n }\n\n if (Array.isArray(obj)) {\n return obj.map((item) => redactObjectKeys(item));\n }\n\n const result: Record<string, unknown> = {};\n for (const [key, value] of Object.entries(obj as Record<string, unknown>)) {\n if (SENSITIVE_BODY_KEYS.has(key)) {\n result[key] = REDACTED;\n } else if (typeof value === \"object\" && value !== null) {\n result[key] = redactObjectKeys(value);\n } else {\n result[key] = value;\n }\n }\n return result;\n}\n\nexport function redactBody(\n body: string,\n opts?: RedactOptions,\n): string {\n if (opts?.enabled === false) {\n return body;\n }\n\n try {\n const parsed = JSON.parse(body);\n const redacted = redactObjectKeys(parsed);\n return JSON.stringify(redacted);\n } catch {\n // Not valid JSON — return as-is\n return body;\n }\n}\n\n// ---------------------------------------------------------------------------\n// redactInlineSecrets\n// ---------------------------------------------------------------------------\n\nexport function redactInlineSecrets(\n text: string,\n opts?: RedactOptions,\n): string {\n if (opts?.enabled === false) {\n return text;\n }\n\n // Redact JWTs first (before Bearer, since Bearer may contain a JWT)\n let result = text.replace(JWT_PATTERN, REDACTED_JWT);\n\n // Redact Bearer tokens — preserve the original casing of \"Bearer\"\n result = result.replace(BEARER_PATTERN, (match) => {\n const bearerWord = match.split(/\\s+/)[0];\n return `${bearerWord} ${REDACTED}`;\n });\n\n return result;\n}\n\n// ---------------------------------------------------------------------------\n// redactNetworkEvent\n// ---------------------------------------------------------------------------\n\nexport function redactNetworkEvent(\n event: NetworkEvent,\n opts?: RedactOptions,\n): NetworkEvent {\n if (opts?.enabled === false) {\n return { ...event };\n }\n\n const result: NetworkEvent = { ...event };\n\n if (event.headers) {\n result.headers = redactHeaders(event.headers, opts);\n }\n\n if (event.responseHeaders) {\n result.responseHeaders = redactHeaders(event.responseHeaders, opts);\n }\n\n if (event.body !== undefined) {\n result.body = redactBody(event.body, opts);\n }\n\n return result;\n}\n","/**\n * browser_console_messages tool — captures console messages via CDP events.\n *\n * Uses Runtime.consoleAPICalled CDP event to capture messages server-side\n * into a bounded EventBuffer. Messages survive page navigations and never\n * require JS injection.\n *\n * Supports:\n * - Filtering by log level\n * - Result limiting\n * - Secret redaction (JWT/Bearer tokens)\n *\n * @module browser-console-messages\n */\nimport { EventBuffer } from \"../event-buffer.js\";\nimport { redactInlineSecrets } from \"../redactor.js\";\n\n// ---------------------------------------------------------------------------\n// Types\n// ---------------------------------------------------------------------------\n\nexport interface ConsoleMessagesParams {\n /** Maximum number of messages to return. */\n limit?: number;\n /** Filter by log level. */\n level?: \"log\" | \"warn\" | \"error\" | \"info\";\n}\n\nexport interface ConsoleMessage {\n /** Log level: \"log\", \"warn\", \"error\", or \"info\". */\n level: string;\n /** Stringified message text. */\n text: string;\n /** Timestamp when the message was captured. */\n timestamp?: number;\n}\n\nexport interface ConsoleMessagesResult {\n /** List of captured console messages. */\n messages: ConsoleMessage[];\n}\n\n// ---------------------------------------------------------------------------\n// Supported console levels\n// ---------------------------------------------------------------------------\n\nconst SUPPORTED_LEVELS = new Set([\"log\", \"warn\", \"warning\", \"error\", \"info\"]);\n\n// ---------------------------------------------------------------------------\n// Module-level EventBuffer\n// ---------------------------------------------------------------------------\n\nconst consoleBuffer = new EventBuffer<ConsoleMessage>(500);\n\n// ---------------------------------------------------------------------------\n// Setup & Reset\n// ---------------------------------------------------------------------------\n\ninterface CDPEventSource {\n on(event: string, handler: (params: unknown) => void): void;\n}\n\n/**\n * Register a CDP event listener for Runtime.consoleAPICalled.\n * Call once after Runtime.enable.\n */\nexport function setupConsoleCapture(cdp: CDPEventSource): void {\n cdp.on(\"Runtime.consoleAPICalled\", (params: unknown) => {\n const p = params as {\n type: string;\n args?: Array<{ type: string; value?: unknown; description?: string }>;\n timestamp?: number;\n };\n\n if (!SUPPORTED_LEVELS.has(p.type)) return;\n\n const text = (p.args ?? [])\n .map((arg) => {\n if (arg.type === \"string\") return String(arg.value);\n if (arg.type === \"undefined\") return \"undefined\";\n if (arg.value !== undefined) return String(arg.value);\n if (arg.description) return arg.description;\n return \"\";\n })\n .join(\" \");\n\n consoleBuffer.push({\n level: p.type === \"warning\" ? \"warn\" : p.type,\n text,\n timestamp: p.timestamp ? Math.floor(p.timestamp) : Date.now(),\n });\n });\n}\n\n/** Clear the console buffer (call on reconnection). */\nexport function resetConsoleBuffer(): void {\n consoleBuffer.clear();\n}\n\n// ---------------------------------------------------------------------------\n// Main export\n// ---------------------------------------------------------------------------\n\n/**\n * Read console messages from the EventBuffer.\n *\n * @param _cdp - CDP connection (unused — buffer is populated by setupConsoleCapture).\n * @param params - Filter and limit parameters.\n * @returns List of console messages.\n */\nexport async function browserConsoleMessages(\n _cdp: unknown,\n params: ConsoleMessagesParams,\n): Promise<ConsoleMessagesResult> {\n let messages = consoleBuffer.last();\n\n // Redact secrets from message text\n messages = messages.map((m) => ({ ...m, text: redactInlineSecrets(m.text) }));\n\n // Filter by level\n if (params.level) {\n messages = messages.filter((m) => m.level === params.level);\n }\n\n // Apply limit (return most recent messages)\n const limit = params.limit ?? 100;\n if (messages.length > limit) {\n messages = messages.slice(-limit);\n }\n\n return { messages };\n}\n","/**\n * browser_network_requests tool — captures network requests via CDP events.\n *\n * Uses Network.requestWillBeSent and Network.responseReceived CDP events to\n * capture requests server-side into a bounded EventBuffer. Captures method,\n * status code, headers — data not available via the Performance API.\n *\n * Supports:\n * - URL filtering via substring match\n * - Static resource filtering (Image, Stylesheet, Font, Script)\n * - Result limiting\n * - Secret redaction (JWT/Bearer tokens in URLs)\n *\n * @module browser-network-requests\n */\nimport { EventBuffer } from \"../event-buffer.js\";\nimport { redactInlineSecrets } from \"../redactor.js\";\n\n// ---------------------------------------------------------------------------\n// Types\n// ---------------------------------------------------------------------------\n\nexport interface NetworkRequestsParams {\n /** Substring filter to match against request URLs. */\n filter?: string;\n /** Maximum number of requests to return. */\n limit?: number;\n /** Whether to include response headers. */\n includeHeaders?: boolean;\n /** Whether to include static resources (images, stylesheets, fonts, scripts). */\n includeStatic?: boolean;\n}\n\nexport interface NetworkRequest {\n /** The request URL. */\n url: string;\n /** HTTP method (GET, POST, etc.). */\n method: string;\n /** HTTP status code. */\n status?: number;\n /** Resource type (e.g. \"Fetch\", \"XHR\", \"Script\", \"Image\"). */\n type?: string;\n}\n\nexport interface NetworkRequestsResult {\n /** List of captured network requests. */\n requests: NetworkRequest[];\n}\n\n// ---------------------------------------------------------------------------\n// Static resource types (CDP uses PascalCase)\n// ---------------------------------------------------------------------------\n\nconst STATIC_TYPES = new Set([\n \"Image\",\n \"Stylesheet\",\n \"Font\",\n \"Script\",\n \"Media\",\n]);\n\n// ---------------------------------------------------------------------------\n// Internal buffer entry (mutable — response enriches it)\n// ---------------------------------------------------------------------------\n\ninterface BufferEntry {\n requestId: string;\n url: string;\n method: string;\n type: string;\n status?: number;\n timestamp: number;\n}\n\n// ---------------------------------------------------------------------------\n// Module-level state\n// ---------------------------------------------------------------------------\n\nconst networkBuffer = new EventBuffer<BufferEntry>(500);\n\n/** Map requestId → buffer index for response correlation */\nconst pendingRequests = new Map<string, BufferEntry>();\n\n// ---------------------------------------------------------------------------\n// Setup & Reset\n// ---------------------------------------------------------------------------\n\ninterface CDPEventSource {\n on(event: string, handler: (params: unknown) => void): void;\n}\n\n/**\n * Register CDP event listeners for Network.requestWillBeSent and\n * Network.responseReceived. Call once after Network.enable.\n */\nexport function setupNetworkCapture(cdp: CDPEventSource): void {\n cdp.on(\"Network.requestWillBeSent\", (params: unknown) => {\n const p = params as {\n requestId: string;\n request: { url: string; method: string };\n type?: string;\n timestamp?: number;\n };\n\n const entry: BufferEntry = {\n requestId: p.requestId,\n url: p.request.url,\n method: p.request.method,\n type: p.type ?? \"Other\",\n timestamp: p.timestamp ? Math.floor(p.timestamp * 1000) : Date.now(),\n };\n\n pendingRequests.set(p.requestId, entry);\n networkBuffer.push(entry);\n });\n\n cdp.on(\"Network.responseReceived\", (params: unknown) => {\n const p = params as {\n requestId: string;\n response: { url: string; status: number; headers?: Record<string, string> };\n };\n\n const entry = pendingRequests.get(p.requestId);\n if (entry) {\n entry.status = p.response.status;\n pendingRequests.delete(p.requestId);\n }\n });\n}\n\n/** Clear the network buffer (call on reconnection). */\nexport function resetNetworkBuffer(): void {\n networkBuffer.clear();\n pendingRequests.clear();\n}\n\n// ---------------------------------------------------------------------------\n// Main export\n// ---------------------------------------------------------------------------\n\n/**\n * Read network requests from the EventBuffer.\n *\n * @param _cdp - CDP connection (unused — buffer is populated by setupNetworkCapture).\n * @param params - Filter and limit parameters.\n * @returns List of network requests.\n */\nexport async function browserNetworkRequests(\n _cdp: unknown,\n params: NetworkRequestsParams,\n): Promise<NetworkRequestsResult> {\n let entries = networkBuffer.last();\n\n // Filter static resources unless includeStatic is true\n if (!params.includeStatic) {\n entries = entries.filter((e) => !STATIC_TYPES.has(e.type));\n }\n\n // Filter by URL substring\n if (params.filter) {\n const filterLower = params.filter.toLowerCase();\n entries = entries.filter((e) => e.url.toLowerCase().includes(filterLower));\n }\n\n // Apply limit\n const limit = params.limit ?? 100;\n entries = entries.slice(0, limit);\n\n // Map to NetworkRequest format — redact secrets from URLs\n const requests: NetworkRequest[] = entries.map((e) => ({\n url: redactInlineSecrets(e.url),\n method: e.method,\n status: e.status,\n type: e.type,\n }));\n\n return { requests };\n}\n"],"mappings":"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","names":["name"]}
|