browsirai 0.1.0 → 0.2.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +60 -1
- package/dist/bin.js +44 -43
- package/dist/bin.js.map +1 -1
- package/dist/cli/commands/act.js +1226 -0
- package/dist/cli/commands/act.js.map +1 -0
- package/dist/cli/commands/nav.js +739 -0
- package/dist/cli/commands/nav.js.map +1 -0
- package/dist/cli/commands/net.js +556 -0
- package/dist/cli/commands/net.js.map +1 -0
- package/dist/cli/commands/obs.js +1049 -0
- package/dist/cli/commands/obs.js.map +1 -0
- package/dist/cli/run.js +727 -0
- package/dist/cli/run.js.map +1 -0
- package/dist/cli.js +44 -43
- package/dist/cli.js.map +1 -1
- package/package.json +2 -2
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../../../src/cli/commands/net.ts","../../../src/cli/run.ts","../../../src/chrome-launcher.ts","../../../src/tools/browser-intercept.ts","../../../src/tools/browser-session-state.ts","../../../src/tools/browser-diff.ts"],"sourcesContent":["/**\n * Network intercept & state persistence CLI commands.\n *\n * Commands: route, abort, unroute, save, load, diff\n */\n\nimport pc from \"picocolors\";\nimport { writeFileSync } from \"node:fs\";\nimport type { CLICommand } from \"../types.js\";\nimport { parseFlags } from \"../run.js\";\nimport {\n browserRoute,\n browserAbort,\n browserUnroute,\n} from \"../../tools/browser-intercept.js\";\nimport {\n browserSaveState,\n browserLoadState,\n} from \"../../tools/browser-session-state.js\";\nimport { browserDiff } from \"../../tools/browser-diff.js\";\n\n// ---------------------------------------------------------------------------\n// route\n// ---------------------------------------------------------------------------\n\nconst routeCommand: CLICommand = {\n name: \"route\",\n description: \"Intercept matching requests with a custom response\",\n usage: \"browsirai route <urlPattern> [--status=200] [--body=\\\"...\\\"] [--contentType=application/json]\",\n run: async (cdp, args) => {\n const flags = parseFlags(args);\n const urlPattern = flags._0;\n\n if (!urlPattern) {\n console.error(pc.red(\"Error: URL pattern is required.\"));\n console.log(pc.dim(`Usage: ${routeCommand.usage}`));\n process.exit(1);\n }\n\n const status = flags.status ? parseInt(flags.status, 10) : 200;\n const body = flags.body ?? \"{}\";\n const contentType = flags.contentType ?? \"application/json\";\n\n const result = await browserRoute(cdp, {\n url: urlPattern,\n body,\n status,\n headers: { \"Content-Type\": contentType },\n });\n\n console.log(\n pc.green(`Routing ${pc.bold(result.url)} → ${result.status} (custom response)`),\n );\n console.log(pc.dim(`Active routes: ${result.activeRoutes}`));\n },\n};\n\n// ---------------------------------------------------------------------------\n// abort\n// ---------------------------------------------------------------------------\n\nconst abortCommand: CLICommand = {\n name: \"abort\",\n description: \"Block matching requests\",\n usage: \"browsirai abort <urlPattern>\",\n run: async (cdp, args) => {\n const flags = parseFlags(args);\n const urlPattern = flags._0;\n\n if (!urlPattern) {\n console.error(pc.red(\"Error: URL pattern is required.\"));\n console.log(pc.dim(`Usage: ${abortCommand.usage}`));\n process.exit(1);\n }\n\n const result = await browserAbort(cdp, { url: urlPattern });\n\n console.log(\n pc.green(`Blocking requests matching ${pc.bold(result.url)}`),\n );\n console.log(pc.dim(`Active abort rules: ${result.activeAborts}`));\n },\n};\n\n// ---------------------------------------------------------------------------\n// unroute\n// ---------------------------------------------------------------------------\n\nconst unrouteCommand: CLICommand = {\n name: \"unroute\",\n description: \"Remove intercept rules\",\n usage: \"browsirai unroute [urlPattern] [--all]\",\n run: async (cdp, args) => {\n const flags = parseFlags(args);\n const urlPattern = flags._0;\n const removeAll = flags.all === \"true\";\n\n if (!urlPattern && !removeAll) {\n console.error(pc.red(\"Error: Provide a URL pattern or --all.\"));\n console.log(pc.dim(`Usage: ${unrouteCommand.usage}`));\n process.exit(1);\n }\n\n const result = await browserUnroute(cdp, {\n url: urlPattern,\n all: removeAll,\n });\n\n if (removeAll) {\n console.log(pc.green(`Removed all routes (${result.removed} rules cleared)`));\n } else {\n console.log(pc.green(`Removed route for ${pc.bold(urlPattern!)} (${result.removed} removed)`));\n }\n\n console.log(pc.dim(`Remaining rules: ${result.remaining}`));\n },\n};\n\n// ---------------------------------------------------------------------------\n// save\n// ---------------------------------------------------------------------------\n\nconst saveCommand: CLICommand = {\n name: \"save\",\n description: \"Save browser session state (cookies, storage)\",\n usage: \"browsirai save <name>\",\n run: async (cdp, args) => {\n const flags = parseFlags(args);\n const name = flags._0;\n\n if (!name) {\n console.error(pc.red(\"Error: State name is required.\"));\n console.log(pc.dim(`Usage: ${saveCommand.usage}`));\n process.exit(1);\n }\n\n const result = await browserSaveState(cdp, { name });\n\n console.log(pc.green(`State saved as '${pc.bold(result.name)}'`));\n console.log(pc.dim(` Path: ${result.path}`));\n console.log(pc.dim(` Cookies: ${result.cookies}`));\n console.log(pc.dim(` localStorage: ${result.localStorage}`));\n console.log(pc.dim(` sessionStorage: ${result.sessionStorage}`));\n },\n};\n\n// ---------------------------------------------------------------------------\n// load\n// ---------------------------------------------------------------------------\n\nconst loadCommand: CLICommand = {\n name: \"load\",\n description: \"Load a saved browser session state\",\n usage: \"browsirai load <name> [--url=https://...]\",\n run: async (cdp, args) => {\n const flags = parseFlags(args);\n const name = flags._0;\n\n if (!name) {\n console.error(pc.red(\"Error: State name is required.\"));\n console.log(pc.dim(`Usage: ${loadCommand.usage}`));\n process.exit(1);\n }\n\n const result = await browserLoadState(cdp, {\n name,\n url: flags.url,\n });\n\n console.log(pc.green(`State '${pc.bold(result.name)}' loaded`));\n console.log(pc.dim(` Cookies: ${result.cookies}`));\n console.log(pc.dim(` localStorage: ${result.localStorage}`));\n console.log(pc.dim(` sessionStorage: ${result.sessionStorage}`));\n },\n};\n\n// ---------------------------------------------------------------------------\n// diff\n// ---------------------------------------------------------------------------\n\nconst diffCommand: CLICommand = {\n name: \"diff\",\n description: \"Pixel-by-pixel screenshot comparison\",\n usage: \"browsirai diff [--selector=...] [--threshold=30] [--output=diff.png]\",\n run: async (cdp, args) => {\n const flags = parseFlags(args);\n const selector = flags.selector;\n const threshold = flags.threshold ? parseInt(flags.threshold, 10) : 30;\n const output = flags.output;\n\n const result = await browserDiff(cdp, {\n before: \"current\",\n after: \"current\",\n selector,\n threshold,\n });\n\n // Save diff image if --output provided\n if (output) {\n const imageBuffer = Buffer.from(result.diffImage, \"base64\");\n writeFileSync(output, imageBuffer);\n console.log(pc.dim(`Diff image saved to ${output}`));\n }\n\n const pct = result.diffPercentage.toFixed(2);\n const status = result.identical\n ? pc.green(\"identical\")\n : pc.yellow(`${pct}% changed`);\n\n console.log(\n `Diff: ${status} (${result.diffPixels.toLocaleString()} pixels, ${result.width}x${result.height})`,\n );\n },\n};\n\n// ---------------------------------------------------------------------------\n// Export\n// ---------------------------------------------------------------------------\n\nexport const netCommands: CLICommand[] = [\n routeCommand,\n abortCommand,\n unrouteCommand,\n saveCommand,\n loadCommand,\n diffCommand,\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_route, browser_abort, browser_unroute tools — CDP Fetch domain request interception.\n *\n * Manages shared intercept state:\n * - Route rules: respond with custom body/status/headers for matching URLs\n * - Abort rules: block matching requests with BlockedByClient\n * - Unroute: remove specific or all intercept rules\n *\n * Uses glob pattern matching (** for any path, * for single segment).\n */\nimport type { CDPConnection } from \"../cdp/connection\";\n\n// ---------------------------------------------------------------------------\n// Types\n// ---------------------------------------------------------------------------\n\ninterface RouteRule {\n urlPattern: string;\n body: string;\n status: number;\n headers: Record<string, string>;\n}\n\ninterface AbortRule {\n urlPattern: string;\n}\n\nexport interface RouteParams {\n url: string;\n body: string | Record<string, unknown>;\n status?: number;\n headers?: Record<string, string>;\n}\n\nexport interface RouteResult {\n url: string;\n status: number;\n activeRoutes: number;\n}\n\nexport interface AbortParams {\n url: string;\n}\n\nexport interface AbortResult {\n url: string;\n activeAborts: number;\n}\n\nexport interface UnrouteParams {\n url?: string;\n all?: boolean;\n}\n\nexport interface UnrouteResult {\n removed: number;\n remaining: number;\n}\n\n// ---------------------------------------------------------------------------\n// Module-level state\n// ---------------------------------------------------------------------------\n\nconst activeRoutes: Map<string, RouteRule> = new Map();\nconst activeAborts: Map<string, AbortRule> = new Map();\nlet fetchEnabled = false;\nlet handlerAttached = false;\n\n// ---------------------------------------------------------------------------\n// Helpers\n// ---------------------------------------------------------------------------\n\n/**\n * Glob matching: convert ** to .* and * to [^/]* for regex matching.\n */\nfunction matchGlob(pattern: string, url: string): boolean {\n const regex = pattern\n .replace(/[.+^${}()|[\\]\\\\]/g, \"\\\\$&\")\n .replace(/\\*\\*/g, \"___DOUBLESTAR___\")\n .replace(/\\*/g, \"[^/]*\")\n .replace(/___DOUBLESTAR___/g, \".*\");\n return new RegExp(`^${regex}$`).test(url);\n}\n\n/**\n * Sync Fetch domain patterns with CDP.\n * Enables/disables Fetch domain and attaches the requestPaused handler once.\n */\nasync function syncFetchPatterns(cdp: CDPConnection): Promise<void> {\n const patterns = [\n ...Array.from(activeRoutes.keys()),\n ...Array.from(activeAborts.keys()),\n ].map((p) => ({ urlPattern: p }));\n\n if (patterns.length === 0) {\n if (fetchEnabled) {\n await cdp.send(\"Fetch.disable\");\n fetchEnabled = false;\n }\n return;\n }\n\n await cdp.send(\"Fetch.enable\", { patterns });\n fetchEnabled = true;\n\n // Attach handler once\n if (!handlerAttached) {\n cdp.on(\"Fetch.requestPaused\", async (params: any) => {\n const url = params.request.url;\n const requestId = params.requestId;\n\n try {\n // Check abort rules first\n for (const [pattern] of activeAborts) {\n if (matchGlob(pattern, url)) {\n await cdp.send(\"Fetch.failRequest\", {\n requestId,\n reason: \"BlockedByClient\",\n });\n return;\n }\n }\n\n // Check route rules\n for (const [pattern, rule] of activeRoutes) {\n if (matchGlob(pattern, url)) {\n const responseHeaders = Object.entries(rule.headers).map(\n ([name, value]) => ({ name, value }),\n );\n await cdp.send(\"Fetch.fulfillRequest\", {\n requestId,\n responseCode: rule.status,\n body: btoa(rule.body),\n responseHeaders,\n });\n return;\n }\n }\n\n // No match: continue request\n await cdp.send(\"Fetch.continueRequest\", { requestId });\n } catch {\n // Request may have been cancelled or navigation occurred — ignore\n }\n });\n handlerAttached = true;\n }\n}\n\n// ---------------------------------------------------------------------------\n// Tool exports\n// ---------------------------------------------------------------------------\n\n/**\n * Intercept matching requests and respond with a custom body/status/headers.\n */\nexport async function browserRoute(\n cdp: CDPConnection,\n params: RouteParams,\n): Promise<RouteResult> {\n const body =\n typeof params.body === \"object\" && params.body !== null\n ? JSON.stringify(params.body)\n : String(params.body);\n\n const status = params.status ?? 200;\n const headers = params.headers ?? { \"Content-Type\": \"application/json\" };\n\n const rule: RouteRule = {\n urlPattern: params.url,\n body,\n status,\n headers,\n };\n\n activeRoutes.set(params.url, rule);\n await syncFetchPatterns(cdp);\n\n return {\n url: params.url,\n status,\n activeRoutes: activeRoutes.size,\n };\n}\n\n/**\n * Block matching requests with BlockedByClient error.\n */\nexport async function browserAbort(\n cdp: CDPConnection,\n params: AbortParams,\n): Promise<AbortResult> {\n const rule: AbortRule = {\n urlPattern: params.url,\n };\n\n activeAborts.set(params.url, rule);\n await syncFetchPatterns(cdp);\n\n return {\n url: params.url,\n activeAborts: activeAborts.size,\n };\n}\n\n/**\n * Remove intercept rules — specific pattern or all.\n */\nexport async function browserUnroute(\n cdp: CDPConnection,\n params: UnrouteParams,\n): Promise<UnrouteResult> {\n let removed = 0;\n\n if (params.all) {\n removed = activeRoutes.size + activeAborts.size;\n activeRoutes.clear();\n activeAborts.clear();\n } else if (params.url) {\n if (activeRoutes.delete(params.url)) removed++;\n if (activeAborts.delete(params.url)) removed++;\n }\n\n await syncFetchPatterns(cdp);\n\n return {\n removed,\n remaining: activeRoutes.size + activeAborts.size,\n };\n}\n\n/**\n * Reset all intercept state — for testing purposes.\n */\nexport function resetInterceptState(): void {\n activeRoutes.clear();\n activeAborts.clear();\n fetchEnabled = false;\n handlerAttached = false;\n}\n","/**\n * browser_save_state / browser_load_state — persist and restore browser session state.\n *\n * Saves cookies, localStorage, and sessionStorage to a named JSON file\n * under ~/.browsirai/states/. Loading restores all three and navigates\n * to the saved (or custom) URL.\n */\nimport type { CDPConnection } from \"../cdp/connection\";\nimport { writeFileSync, readFileSync, existsSync, mkdirSync } from \"node:fs\";\nimport { join } from \"node:path\";\nimport { homedir } from \"node:os\";\n\n// ---------------------------------------------------------------------------\n// Types\n// ---------------------------------------------------------------------------\n\nexport interface SaveStateParams {\n name: string;\n}\n\nexport interface SaveStateResult {\n name: string;\n path: string;\n cookies: number;\n localStorage: number;\n sessionStorage: number;\n}\n\nexport interface LoadStateParams {\n name: string;\n url?: string;\n}\n\nexport interface LoadStateResult {\n name: string;\n cookies: number;\n localStorage: number;\n sessionStorage: number;\n}\n\ninterface StateFile {\n version: 1;\n savedAt: string;\n url: string;\n cookies: unknown[];\n localStorage: Record<string, string>;\n sessionStorage: Record<string, string>;\n}\n\n// ---------------------------------------------------------------------------\n// Helpers\n// ---------------------------------------------------------------------------\n\nfunction getStatesDir(): string {\n return join(homedir(), \".browsirai\", \"states\");\n}\n\nfunction ensureStatesDir(): string {\n const dir = getStatesDir();\n if (!existsSync(dir)) {\n mkdirSync(dir, { recursive: true });\n }\n return dir;\n}\n\nfunction getStatePath(name: string): string {\n return join(getStatesDir(), `${name}.json`);\n}\n\n// ---------------------------------------------------------------------------\n// browser_save_state\n// ---------------------------------------------------------------------------\n\nexport async function browserSaveState(\n cdp: CDPConnection,\n params: SaveStateParams,\n): Promise<SaveStateResult> {\n const { name } = params;\n\n // 1. Get cookies\n const cookieResponse = (await cdp.send(\"Network.getAllCookies\")) as {\n cookies: unknown[];\n };\n const cookies = cookieResponse.cookies ?? [];\n\n // 2. Get current URL\n const urlResponse = (await cdp.send(\"Runtime.evaluate\", {\n expression: \"window.location.href\",\n returnByValue: true,\n })) as { result: { value?: string } };\n const url = urlResponse.result.value ?? \"\";\n\n // 3. Get localStorage\n const localStorageResponse = (await cdp.send(\"Runtime.evaluate\", {\n expression: \"JSON.stringify(Object.entries(localStorage))\",\n returnByValue: true,\n })) as { result: { value?: string } };\n const localStorageEntries: Array<[string, string]> = JSON.parse(\n localStorageResponse.result.value ?? \"[]\",\n );\n const localStorage: Record<string, string> = Object.fromEntries(localStorageEntries);\n\n // 4. Get sessionStorage\n const sessionStorageResponse = (await cdp.send(\"Runtime.evaluate\", {\n expression: \"JSON.stringify(Object.entries(sessionStorage))\",\n returnByValue: true,\n })) as { result: { value?: string } };\n const sessionStorageEntries: Array<[string, string]> = JSON.parse(\n sessionStorageResponse.result.value ?? \"[]\",\n );\n const sessionStorage: Record<string, string> = Object.fromEntries(sessionStorageEntries);\n\n // 5. Write state file\n const dir = ensureStatesDir();\n const filePath = join(dir, `${name}.json`);\n\n const stateFile: StateFile = {\n version: 1,\n savedAt: new Date().toISOString(),\n url,\n cookies,\n localStorage,\n sessionStorage,\n };\n\n writeFileSync(filePath, JSON.stringify(stateFile, null, 2), \"utf-8\");\n\n return {\n name,\n path: filePath,\n cookies: cookies.length,\n localStorage: localStorageEntries.length,\n sessionStorage: sessionStorageEntries.length,\n };\n}\n\n// ---------------------------------------------------------------------------\n// browser_load_state\n// ---------------------------------------------------------------------------\n\nexport async function browserLoadState(\n cdp: CDPConnection,\n params: LoadStateParams,\n): Promise<LoadStateResult> {\n const { name, url: customUrl } = params;\n\n // 1. Read state file\n const filePath = getStatePath(name);\n if (!existsSync(filePath)) {\n throw new Error(`State file not found: ${filePath}`);\n }\n\n const stateFile: StateFile = JSON.parse(readFileSync(filePath, \"utf-8\"));\n\n // 2. Navigate to URL (custom or saved)\n const targetUrl = customUrl ?? stateFile.url;\n if (targetUrl) {\n await cdp.send(\"Page.enable\");\n await cdp.send(\"Page.navigate\", { url: targetUrl });\n }\n\n // 3. Set cookies\n if (stateFile.cookies.length > 0) {\n await cdp.send(\"Network.setCookies\", { cookies: stateFile.cookies });\n }\n\n // 4. Set localStorage\n const localEntries = Object.entries(stateFile.localStorage);\n if (localEntries.length > 0) {\n const localScript = localEntries\n .map(([k, v]) => `localStorage.setItem(${JSON.stringify(k)}, ${JSON.stringify(v)})`)\n .join(\";\");\n await cdp.send(\"Runtime.evaluate\", {\n expression: localScript,\n returnByValue: true,\n });\n }\n\n // 5. Set sessionStorage\n const sessionEntries = Object.entries(stateFile.sessionStorage);\n if (sessionEntries.length > 0) {\n const sessionScript = sessionEntries\n .map(([k, v]) => `sessionStorage.setItem(${JSON.stringify(k)}, ${JSON.stringify(v)})`)\n .join(\";\");\n await cdp.send(\"Runtime.evaluate\", {\n expression: sessionScript,\n returnByValue: true,\n });\n }\n\n // 6. Reload page to apply state\n await cdp.send(\"Page.reload\");\n\n return {\n name,\n cookies: stateFile.cookies.length,\n localStorage: localEntries.length,\n sessionStorage: sessionEntries.length,\n };\n}\n","/**\n * browser_diff tool — pixel-by-pixel comparison of two screenshots via CDP.\n *\n * Supports:\n * - Comparing two base64 PNG screenshots\n * - Capturing \"current\" page state as before/after\n * - Element-scoped comparison via CSS selector\n * - Configurable pixel difference threshold\n * - Visual diff image with red-highlighted changes\n *\n * @module browser-diff\n */\nimport type { CDPConnection } from \"../cdp/connection\";\n\n// ---------------------------------------------------------------------------\n// Types\n// ---------------------------------------------------------------------------\n\nexport interface DiffParams {\n /** First screenshot - base64 PNG or \"current\" to capture now */\n before: string;\n /** Second screenshot - base64 PNG or \"current\" to capture now */\n after?: string;\n /** CSS selector to scope comparison */\n selector?: string;\n /** Pixel difference threshold (0-255, default 30) */\n threshold?: number;\n}\n\nexport interface DiffResult {\n /** Percentage of pixels that differ (0-100) */\n diffPercentage: number;\n /** Total pixels compared */\n totalPixels: number;\n /** Number of different pixels */\n diffPixels: number;\n /** Whether images are considered identical (diffPercentage < 0.1) */\n identical: boolean;\n /** Base64 diff image (red highlights on differences) */\n diffImage: string;\n /** Dimensions */\n width: number;\n height: number;\n}\n\n// ---------------------------------------------------------------------------\n// Helpers\n// ---------------------------------------------------------------------------\n\n/**\n * Capture a screenshot of the current page or a specific element.\n */\nasync function captureScreenshot(\n cdp: CDPConnection,\n selector?: string,\n): Promise<string> {\n const captureParams: Record<string, unknown> = { format: \"png\" };\n\n if (selector) {\n const doc = (await cdp.send(\"DOM.getDocument\", {})) as {\n root: { nodeId: number };\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[] };\n };\n\n const content = boxModel.model.content;\n captureParams.clip = {\n x: content[0],\n y: content[1],\n width: content[2] - content[0],\n height: content[5] - content[1],\n scale: 1,\n };\n }\n\n const screenshot = (await cdp.send(\n \"Page.captureScreenshot\",\n captureParams,\n )) as { data: string };\n\n return screenshot.data;\n}\n\n/**\n * Build the JavaScript expression for in-browser pixel comparison.\n * Uses string concatenation to avoid template literal escaping issues\n * with large base64 strings injected into CDP Runtime.evaluate.\n */\nfunction buildComparisonExpression(threshold: number): string {\n return [\n \"(async () => {\",\n \" const beforeSrc = window._diffBefore;\",\n \" const afterSrc = window._diffAfter;\",\n \"\",\n \" const loadImg = (src) => new Promise((res, rej) => {\",\n \" const img = new Image();\",\n \" img.onload = () => res(img);\",\n \" img.onerror = (e) => rej(new Error('Failed to load image'));\",\n \" img.src = src;\",\n \" });\",\n \"\",\n \" const img1 = await loadImg('data:image/png;base64,' + beforeSrc);\",\n \" const img2 = await loadImg('data:image/png;base64,' + afterSrc);\",\n \"\",\n \" const w = Math.max(img1.width, img2.width);\",\n \" const h = Math.max(img1.height, img2.height);\",\n \"\",\n \" const c1 = document.createElement('canvas');\",\n \" c1.width = w; c1.height = h;\",\n \" const ctx1 = c1.getContext('2d');\",\n \" ctx1.drawImage(img1, 0, 0);\",\n \"\",\n \" const c2 = document.createElement('canvas');\",\n \" c2.width = w; c2.height = h;\",\n \" const ctx2 = c2.getContext('2d');\",\n \" ctx2.drawImage(img2, 0, 0);\",\n \"\",\n \" const d1 = ctx1.getImageData(0, 0, w, h).data;\",\n \" const d2 = ctx2.getImageData(0, 0, w, h).data;\",\n \"\",\n \" const diff = document.createElement('canvas');\",\n \" diff.width = w; diff.height = h;\",\n \" const dCtx = diff.getContext('2d');\",\n \" dCtx.drawImage(img2, 0, 0);\",\n \" const dData = dCtx.getImageData(0, 0, w, h);\",\n \"\",\n \" let diffCount = 0;\",\n \" const threshold = \" + threshold + \";\",\n \" for (let i = 0; i < d1.length; i += 4) {\",\n \" const dr = Math.abs(d1[i] - d2[i]);\",\n \" const dg = Math.abs(d1[i+1] - d2[i+1]);\",\n \" const db = Math.abs(d1[i+2] - d2[i+2]);\",\n \" if (dr > threshold || dg > threshold || db > threshold) {\",\n \" diffCount++;\",\n \" dData.data[i] = 255;\",\n \" dData.data[i+1] = 0;\",\n \" dData.data[i+2] = 0;\",\n \" dData.data[i+3] = 200;\",\n \" }\",\n \" }\",\n \"\",\n \" dCtx.putImageData(dData, 0, 0);\",\n \" const diffBase64 = diff.toDataURL('image/png').split(',')[1];\",\n \"\",\n \" const total = w * h;\",\n \" return JSON.stringify({\",\n \" diffPercentage: parseFloat((diffCount / total * 100).toFixed(4)),\",\n \" totalPixels: total,\",\n \" diffPixels: diffCount,\",\n \" identical: (diffCount / total) < 0.001,\",\n \" diffImage: diffBase64,\",\n \" width: w,\",\n \" height: h\",\n \" });\",\n \"})()\",\n ].join(\"\\n\");\n}\n\n// ---------------------------------------------------------------------------\n// Main export\n// ---------------------------------------------------------------------------\n\n/**\n * Compare two screenshots pixel-by-pixel.\n *\n * If `before` is \"current\", captures the page now.\n * If `after` is not provided or is \"current\", captures the page now.\n *\n * @param cdp - CDP connection.\n * @param params - Diff parameters.\n * @returns Diff result with percentage, pixel count, and visual diff image.\n */\nexport async function browserDiff(\n cdp: CDPConnection,\n params: DiffParams,\n): Promise<DiffResult> {\n const threshold = params.threshold ?? 30;\n\n // Resolve before image\n let beforeBase64: string;\n if (params.before === \"current\") {\n beforeBase64 = await captureScreenshot(cdp, params.selector);\n } else {\n beforeBase64 = params.before;\n }\n\n // Resolve after image\n let afterBase64: string;\n if (!params.after || params.after === \"current\") {\n afterBase64 = await captureScreenshot(cdp, params.selector);\n } else {\n afterBase64 = params.after;\n }\n\n // Store images in page context to avoid escaping issues with large base64 strings\n await cdp.send(\"Runtime.evaluate\", {\n expression: \"window._diffBefore = \" + JSON.stringify(beforeBase64) + \";\",\n returnByValue: true,\n });\n\n await cdp.send(\"Runtime.evaluate\", {\n expression: \"window._diffAfter = \" + JSON.stringify(afterBase64) + \";\",\n returnByValue: true,\n });\n\n // Run pixel comparison in the browser\n const result = (await cdp.send(\"Runtime.evaluate\", {\n expression: buildComparisonExpression(threshold),\n awaitPromise: true,\n returnByValue: true,\n })) as {\n result: { type: string; value: string };\n exceptionDetails?: { text: string };\n };\n\n if (result.exceptionDetails) {\n throw new Error(\n \"Diff comparison failed: \" + result.exceptionDetails.text,\n );\n }\n\n // Clean up global variables\n await cdp.send(\"Runtime.evaluate\", {\n expression: \"delete window._diffBefore; delete window._diffAfter;\",\n returnByValue: true,\n });\n\n return JSON.parse(result.result.value) as DiffResult;\n}\n"],"mappings":";AAMA,OAAOA,SAAQ;AACf,SAAS,iBAAAC,sBAAqB;;;ACA9B,OAAO,QAAQ;;;ACMf,SAAS,UAAU,aAAa;AAChC,SAAS,YAAY,cAAc,WAAW,cAAc,aAAa,gBAAgB;AACzF,OAAO,UAAU;AACjB,SAAS,YAAY;AACrB,SAAS,SAAS,cAAc;AAChC,SAAS,wBAAwB;;;ADY1B,SAAS,WAAW,MAAwC;AACjE,QAAM,QAAgC,CAAC;AACvC,MAAI,kBAAkB;AAEtB,WAAS,IAAI,GAAG,IAAI,KAAK,QAAQ,KAAK;AACpC,UAAM,MAAM,KAAK,CAAC;AAElB,QAAI,IAAI,WAAW,IAAI,GAAG;AACxB,YAAM,QAAQ,IAAI,QAAQ,GAAG;AAC7B,UAAI,UAAU,IAAI;AAEhB,cAAM,MAAM,IAAI,MAAM,GAAG,KAAK;AAC9B,cAAM,QAAQ,IAAI,MAAM,QAAQ,CAAC;AACjC,cAAM,GAAG,IAAI;AAAA,MACf,OAAO;AACL,cAAM,MAAM,IAAI,MAAM,CAAC;AACvB,cAAM,OAAO,KAAK,IAAI,CAAC;AACvB,YAAI,QAAQ,CAAC,KAAK,WAAW,GAAG,GAAG;AAEjC,gBAAM,GAAG,IAAI;AACb;AAAA,QACF,OAAO;AAEL,gBAAM,GAAG,IAAI;AAAA,QACf;AAAA,MACF;AAAA,IACF,WAAW,IAAI,WAAW,GAAG,KAAK,IAAI,SAAS,KAAK,CAAC,OAAO,KAAK,GAAG,GAAG;AAErE,YAAM,QAAQ,IAAI,MAAM,CAAC;AACzB,UAAI,MAAM,WAAW,GAAG;AAEtB,cAAM,OAAO,KAAK,IAAI,CAAC;AACvB,YAAI,QAAQ,CAAC,KAAK,WAAW,GAAG,GAAG;AACjC,gBAAM,KAAK,IAAI;AACf;AAAA,QACF,OAAO;AACL,gBAAM,KAAK,IAAI;AAAA,QACjB;AAAA,MACF,OAAO;AAEL,mBAAW,MAAM,OAAO;AACtB,gBAAM,EAAE,IAAI;AAAA,QACd;AAAA,MACF;AAAA,IACF,OAAO;AACL,YAAM,IAAI,eAAe,EAAE,IAAI;AAC/B;AAAA,IACF;AAAA,EACF;AAEA,SAAO;AACT;;;AElBA,IAAM,eAAuC,oBAAI,IAAI;AACrD,IAAM,eAAuC,oBAAI,IAAI;AACrD,IAAI,eAAe;AACnB,IAAI,kBAAkB;AAStB,SAAS,UAAU,SAAiB,KAAsB;AACxD,QAAM,QAAQ,QACX,QAAQ,qBAAqB,MAAM,EACnC,QAAQ,SAAS,kBAAkB,EACnC,QAAQ,OAAO,OAAO,EACtB,QAAQ,qBAAqB,IAAI;AACpC,SAAO,IAAI,OAAO,IAAI,KAAK,GAAG,EAAE,KAAK,GAAG;AAC1C;AAMA,eAAe,kBAAkB,KAAmC;AAClE,QAAM,WAAW;AAAA,IACf,GAAG,MAAM,KAAK,aAAa,KAAK,CAAC;AAAA,IACjC,GAAG,MAAM,KAAK,aAAa,KAAK,CAAC;AAAA,EACnC,EAAE,IAAI,CAAC,OAAO,EAAE,YAAY,EAAE,EAAE;AAEhC,MAAI,SAAS,WAAW,GAAG;AACzB,QAAI,cAAc;AAChB,YAAM,IAAI,KAAK,eAAe;AAC9B,qBAAe;AAAA,IACjB;AACA;AAAA,EACF;AAEA,QAAM,IAAI,KAAK,gBAAgB,EAAE,SAAS,CAAC;AAC3C,iBAAe;AAGf,MAAI,CAAC,iBAAiB;AACpB,QAAI,GAAG,uBAAuB,OAAO,WAAgB;AACnD,YAAM,MAAM,OAAO,QAAQ;AAC3B,YAAM,YAAY,OAAO;AAEzB,UAAI;AAEF,mBAAW,CAAC,OAAO,KAAK,cAAc;AACpC,cAAI,UAAU,SAAS,GAAG,GAAG;AAC3B,kBAAM,IAAI,KAAK,qBAAqB;AAAA,cAClC;AAAA,cACA,QAAQ;AAAA,YACV,CAAC;AACD;AAAA,UACF;AAAA,QACF;AAGA,mBAAW,CAAC,SAAS,IAAI,KAAK,cAAc;AAC1C,cAAI,UAAU,SAAS,GAAG,GAAG;AAC3B,kBAAM,kBAAkB,OAAO,QAAQ,KAAK,OAAO,EAAE;AAAA,cACnD,CAAC,CAAC,MAAM,KAAK,OAAO,EAAE,MAAM,MAAM;AAAA,YACpC;AACA,kBAAM,IAAI,KAAK,wBAAwB;AAAA,cACrC;AAAA,cACA,cAAc,KAAK;AAAA,cACnB,MAAM,KAAK,KAAK,IAAI;AAAA,cACpB;AAAA,YACF,CAAC;AACD;AAAA,UACF;AAAA,QACF;AAGA,cAAM,IAAI,KAAK,yBAAyB,EAAE,UAAU,CAAC;AAAA,MACvD,QAAQ;AAAA,MAER;AAAA,IACF,CAAC;AACD,sBAAkB;AAAA,EACpB;AACF;AASA,eAAsB,aACpB,KACA,QACsB;AACtB,QAAM,OACJ,OAAO,OAAO,SAAS,YAAY,OAAO,SAAS,OAC/C,KAAK,UAAU,OAAO,IAAI,IAC1B,OAAO,OAAO,IAAI;AAExB,QAAM,SAAS,OAAO,UAAU;AAChC,QAAM,UAAU,OAAO,WAAW,EAAE,gBAAgB,mBAAmB;AAEvE,QAAM,OAAkB;AAAA,IACtB,YAAY,OAAO;AAAA,IACnB;AAAA,IACA;AAAA,IACA;AAAA,EACF;AAEA,eAAa,IAAI,OAAO,KAAK,IAAI;AACjC,QAAM,kBAAkB,GAAG;AAE3B,SAAO;AAAA,IACL,KAAK,OAAO;AAAA,IACZ;AAAA,IACA,cAAc,aAAa;AAAA,EAC7B;AACF;AAKA,eAAsB,aACpB,KACA,QACsB;AACtB,QAAM,OAAkB;AAAA,IACtB,YAAY,OAAO;AAAA,EACrB;AAEA,eAAa,IAAI,OAAO,KAAK,IAAI;AACjC,QAAM,kBAAkB,GAAG;AAE3B,SAAO;AAAA,IACL,KAAK,OAAO;AAAA,IACZ,cAAc,aAAa;AAAA,EAC7B;AACF;AAKA,eAAsB,eACpB,KACA,QACwB;AACxB,MAAI,UAAU;AAEd,MAAI,OAAO,KAAK;AACd,cAAU,aAAa,OAAO,aAAa;AAC3C,iBAAa,MAAM;AACnB,iBAAa,MAAM;AAAA,EACrB,WAAW,OAAO,KAAK;AACrB,QAAI,aAAa,OAAO,OAAO,GAAG,EAAG;AACrC,QAAI,aAAa,OAAO,OAAO,GAAG,EAAG;AAAA,EACvC;AAEA,QAAM,kBAAkB,GAAG;AAE3B,SAAO;AAAA,IACL;AAAA,IACA,WAAW,aAAa,OAAO,aAAa;AAAA,EAC9C;AACF;;;AC7NA,SAAS,eAAe,gBAAAC,eAAc,cAAAC,aAAY,aAAAC,kBAAiB;AACnE,SAAS,QAAAC,aAAY;AACrB,SAAS,WAAAC,gBAAe;AA2CxB,SAAS,eAAuB;AAC9B,SAAOD,MAAKC,SAAQ,GAAG,cAAc,QAAQ;AAC/C;AAEA,SAAS,kBAA0B;AACjC,QAAM,MAAM,aAAa;AACzB,MAAI,CAACH,YAAW,GAAG,GAAG;AACpB,IAAAC,WAAU,KAAK,EAAE,WAAW,KAAK,CAAC;AAAA,EACpC;AACA,SAAO;AACT;AAEA,SAAS,aAAa,MAAsB;AAC1C,SAAOC,MAAK,aAAa,GAAG,GAAG,IAAI,OAAO;AAC5C;AAMA,eAAsB,iBACpB,KACA,QAC0B;AAC1B,QAAM,EAAE,KAAK,IAAI;AAGjB,QAAM,iBAAkB,MAAM,IAAI,KAAK,uBAAuB;AAG9D,QAAM,UAAU,eAAe,WAAW,CAAC;AAG3C,QAAM,cAAe,MAAM,IAAI,KAAK,oBAAoB;AAAA,IACtD,YAAY;AAAA,IACZ,eAAe;AAAA,EACjB,CAAC;AACD,QAAM,MAAM,YAAY,OAAO,SAAS;AAGxC,QAAM,uBAAwB,MAAM,IAAI,KAAK,oBAAoB;AAAA,IAC/D,YAAY;AAAA,IACZ,eAAe;AAAA,EACjB,CAAC;AACD,QAAM,sBAA+C,KAAK;AAAA,IACxD,qBAAqB,OAAO,SAAS;AAAA,EACvC;AACA,QAAM,eAAuC,OAAO,YAAY,mBAAmB;AAGnF,QAAM,yBAA0B,MAAM,IAAI,KAAK,oBAAoB;AAAA,IACjE,YAAY;AAAA,IACZ,eAAe;AAAA,EACjB,CAAC;AACD,QAAM,wBAAiD,KAAK;AAAA,IAC1D,uBAAuB,OAAO,SAAS;AAAA,EACzC;AACA,QAAM,iBAAyC,OAAO,YAAY,qBAAqB;AAGvF,QAAM,MAAM,gBAAgB;AAC5B,QAAM,WAAWA,MAAK,KAAK,GAAG,IAAI,OAAO;AAEzC,QAAM,YAAuB;AAAA,IAC3B,SAAS;AAAA,IACT,UAAS,oBAAI,KAAK,GAAE,YAAY;AAAA,IAChC;AAAA,IACA;AAAA,IACA;AAAA,IACA;AAAA,EACF;AAEA,gBAAc,UAAU,KAAK,UAAU,WAAW,MAAM,CAAC,GAAG,OAAO;AAEnE,SAAO;AAAA,IACL;AAAA,IACA,MAAM;AAAA,IACN,SAAS,QAAQ;AAAA,IACjB,cAAc,oBAAoB;AAAA,IAClC,gBAAgB,sBAAsB;AAAA,EACxC;AACF;AAMA,eAAsB,iBACpB,KACA,QAC0B;AAC1B,QAAM,EAAE,MAAM,KAAK,UAAU,IAAI;AAGjC,QAAM,WAAW,aAAa,IAAI;AAClC,MAAI,CAACF,YAAW,QAAQ,GAAG;AACzB,UAAM,IAAI,MAAM,yBAAyB,QAAQ,EAAE;AAAA,EACrD;AAEA,QAAM,YAAuB,KAAK,MAAMD,cAAa,UAAU,OAAO,CAAC;AAGvE,QAAM,YAAY,aAAa,UAAU;AACzC,MAAI,WAAW;AACb,UAAM,IAAI,KAAK,aAAa;AAC5B,UAAM,IAAI,KAAK,iBAAiB,EAAE,KAAK,UAAU,CAAC;AAAA,EACpD;AAGA,MAAI,UAAU,QAAQ,SAAS,GAAG;AAChC,UAAM,IAAI,KAAK,sBAAsB,EAAE,SAAS,UAAU,QAAQ,CAAC;AAAA,EACrE;AAGA,QAAM,eAAe,OAAO,QAAQ,UAAU,YAAY;AAC1D,MAAI,aAAa,SAAS,GAAG;AAC3B,UAAM,cAAc,aACjB,IAAI,CAAC,CAAC,GAAG,CAAC,MAAM,wBAAwB,KAAK,UAAU,CAAC,CAAC,KAAK,KAAK,UAAU,CAAC,CAAC,GAAG,EAClF,KAAK,GAAG;AACX,UAAM,IAAI,KAAK,oBAAoB;AAAA,MACjC,YAAY;AAAA,MACZ,eAAe;AAAA,IACjB,CAAC;AAAA,EACH;AAGA,QAAM,iBAAiB,OAAO,QAAQ,UAAU,cAAc;AAC9D,MAAI,eAAe,SAAS,GAAG;AAC7B,UAAM,gBAAgB,eACnB,IAAI,CAAC,CAAC,GAAG,CAAC,MAAM,0BAA0B,KAAK,UAAU,CAAC,CAAC,KAAK,KAAK,UAAU,CAAC,CAAC,GAAG,EACpF,KAAK,GAAG;AACX,UAAM,IAAI,KAAK,oBAAoB;AAAA,MACjC,YAAY;AAAA,MACZ,eAAe;AAAA,IACjB,CAAC;AAAA,EACH;AAGA,QAAM,IAAI,KAAK,aAAa;AAE5B,SAAO;AAAA,IACL;AAAA,IACA,SAAS,UAAU,QAAQ;AAAA,IAC3B,cAAc,aAAa;AAAA,IAC3B,gBAAgB,eAAe;AAAA,EACjC;AACF;;;ACnJA,eAAe,kBACb,KACA,UACiB;AACjB,QAAM,gBAAyC,EAAE,QAAQ,MAAM;AAE/D,MAAI,UAAU;AACZ,UAAM,MAAO,MAAM,IAAI,KAAK,mBAAmB,CAAC,CAAC;AAGjD,UAAM,cAAe,MAAM,IAAI,KAAK,qBAAqB;AAAA,MACvD,QAAQ,IAAI,KAAK;AAAA,MACjB;AAAA,IACF,CAAC;AAED,QAAI,CAAC,YAAY,QAAQ;AACvB,YAAM,IAAI,MAAM,sBAAsB,QAAQ,EAAE;AAAA,IAClD;AAEA,UAAM,WAAY,MAAM,IAAI,KAAK,mBAAmB;AAAA,MAClD,QAAQ,YAAY;AAAA,IACtB,CAAC;AAID,UAAM,UAAU,SAAS,MAAM;AAC/B,kBAAc,OAAO;AAAA,MACnB,GAAG,QAAQ,CAAC;AAAA,MACZ,GAAG,QAAQ,CAAC;AAAA,MACZ,OAAO,QAAQ,CAAC,IAAI,QAAQ,CAAC;AAAA,MAC7B,QAAQ,QAAQ,CAAC,IAAI,QAAQ,CAAC;AAAA,MAC9B,OAAO;AAAA,IACT;AAAA,EACF;AAEA,QAAM,aAAc,MAAM,IAAI;AAAA,IAC5B;AAAA,IACA;AAAA,EACF;AAEA,SAAO,WAAW;AACpB;AAOA,SAAS,0BAA0B,WAA2B;AAC5D,SAAO;AAAA,IACL;AAAA,IACA;AAAA,IACA;AAAA,IACA;AAAA,IACA;AAAA,IACA;AAAA,IACA;AAAA,IACA;AAAA,IACA;AAAA,IACA;AAAA,IACA;AAAA,IACA;AAAA,IACA;AAAA,IACA;AAAA,IACA;AAAA,IACA;AAAA,IACA;AAAA,IACA;AAAA,IACA;AAAA,IACA;AAAA,IACA;AAAA,IACA;AAAA,IACA;AAAA,IACA;AAAA,IACA;AAAA,IACA;AAAA,IACA;AAAA,IACA;AAAA,IACA;AAAA,IACA;AAAA,IACA;AAAA,IACA;AAAA,IACA;AAAA,IACA;AAAA,IACA;AAAA,IACA;AAAA,IACA;AAAA,IACA,yBAAyB,YAAY;AAAA,IACrC;AAAA,IACA;AAAA,IACA;AAAA,IACA;AAAA,IACA;AAAA,IACA;AAAA,IACA;AAAA,IACA;AAAA,IACA;AAAA,IACA;AAAA,IACA;AAAA,IACA;AAAA,IACA;AAAA,IACA;AAAA,IACA;AAAA,IACA;AAAA,IACA;AAAA,IACA;AAAA,IACA;AAAA,IACA;AAAA,IACA;AAAA,IACA;AAAA,IACA;AAAA,IACA;AAAA,IACA;AAAA,IACA;AAAA,IACA;AAAA,EACF,EAAE,KAAK,IAAI;AACb;AAgBA,eAAsB,YACpB,KACA,QACqB;AACrB,QAAM,YAAY,OAAO,aAAa;AAGtC,MAAI;AACJ,MAAI,OAAO,WAAW,WAAW;AAC/B,mBAAe,MAAM,kBAAkB,KAAK,OAAO,QAAQ;AAAA,EAC7D,OAAO;AACL,mBAAe,OAAO;AAAA,EACxB;AAGA,MAAI;AACJ,MAAI,CAAC,OAAO,SAAS,OAAO,UAAU,WAAW;AAC/C,kBAAc,MAAM,kBAAkB,KAAK,OAAO,QAAQ;AAAA,EAC5D,OAAO;AACL,kBAAc,OAAO;AAAA,EACvB;AAGA,QAAM,IAAI,KAAK,oBAAoB;AAAA,IACjC,YAAY,0BAA0B,KAAK,UAAU,YAAY,IAAI;AAAA,IACrE,eAAe;AAAA,EACjB,CAAC;AAED,QAAM,IAAI,KAAK,oBAAoB;AAAA,IACjC,YAAY,yBAAyB,KAAK,UAAU,WAAW,IAAI;AAAA,IACnE,eAAe;AAAA,EACjB,CAAC;AAGD,QAAM,SAAU,MAAM,IAAI,KAAK,oBAAoB;AAAA,IACjD,YAAY,0BAA0B,SAAS;AAAA,IAC/C,cAAc;AAAA,IACd,eAAe;AAAA,EACjB,CAAC;AAKD,MAAI,OAAO,kBAAkB;AAC3B,UAAM,IAAI;AAAA,MACR,6BAA6B,OAAO,iBAAiB;AAAA,IACvD;AAAA,EACF;AAGA,QAAM,IAAI,KAAK,oBAAoB;AAAA,IACjC,YAAY;AAAA,IACZ,eAAe;AAAA,EACjB,CAAC;AAED,SAAO,KAAK,MAAM,OAAO,OAAO,KAAK;AACvC;;;ALvNA,IAAM,eAA2B;AAAA,EAC/B,MAAM;AAAA,EACN,aAAa;AAAA,EACb,OAAO;AAAA,EACP,KAAK,OAAO,KAAK,SAAS;AACxB,UAAM,QAAQ,WAAW,IAAI;AAC7B,UAAM,aAAa,MAAM;AAEzB,QAAI,CAAC,YAAY;AACf,cAAQ,MAAMK,IAAG,IAAI,iCAAiC,CAAC;AACvD,cAAQ,IAAIA,IAAG,IAAI,UAAU,aAAa,KAAK,EAAE,CAAC;AAClD,cAAQ,KAAK,CAAC;AAAA,IAChB;AAEA,UAAM,SAAS,MAAM,SAAS,SAAS,MAAM,QAAQ,EAAE,IAAI;AAC3D,UAAM,OAAO,MAAM,QAAQ;AAC3B,UAAM,cAAc,MAAM,eAAe;AAEzC,UAAM,SAAS,MAAM,aAAa,KAAK;AAAA,MACrC,KAAK;AAAA,MACL;AAAA,MACA;AAAA,MACA,SAAS,EAAE,gBAAgB,YAAY;AAAA,IACzC,CAAC;AAED,YAAQ;AAAA,MACNA,IAAG,MAAM,WAAWA,IAAG,KAAK,OAAO,GAAG,CAAC,WAAM,OAAO,MAAM,oBAAoB;AAAA,IAChF;AACA,YAAQ,IAAIA,IAAG,IAAI,kBAAkB,OAAO,YAAY,EAAE,CAAC;AAAA,EAC7D;AACF;AAMA,IAAM,eAA2B;AAAA,EAC/B,MAAM;AAAA,EACN,aAAa;AAAA,EACb,OAAO;AAAA,EACP,KAAK,OAAO,KAAK,SAAS;AACxB,UAAM,QAAQ,WAAW,IAAI;AAC7B,UAAM,aAAa,MAAM;AAEzB,QAAI,CAAC,YAAY;AACf,cAAQ,MAAMA,IAAG,IAAI,iCAAiC,CAAC;AACvD,cAAQ,IAAIA,IAAG,IAAI,UAAU,aAAa,KAAK,EAAE,CAAC;AAClD,cAAQ,KAAK,CAAC;AAAA,IAChB;AAEA,UAAM,SAAS,MAAM,aAAa,KAAK,EAAE,KAAK,WAAW,CAAC;AAE1D,YAAQ;AAAA,MACNA,IAAG,MAAM,8BAA8BA,IAAG,KAAK,OAAO,GAAG,CAAC,EAAE;AAAA,IAC9D;AACA,YAAQ,IAAIA,IAAG,IAAI,uBAAuB,OAAO,YAAY,EAAE,CAAC;AAAA,EAClE;AACF;AAMA,IAAM,iBAA6B;AAAA,EACjC,MAAM;AAAA,EACN,aAAa;AAAA,EACb,OAAO;AAAA,EACP,KAAK,OAAO,KAAK,SAAS;AACxB,UAAM,QAAQ,WAAW,IAAI;AAC7B,UAAM,aAAa,MAAM;AACzB,UAAM,YAAY,MAAM,QAAQ;AAEhC,QAAI,CAAC,cAAc,CAAC,WAAW;AAC7B,cAAQ,MAAMA,IAAG,IAAI,wCAAwC,CAAC;AAC9D,cAAQ,IAAIA,IAAG,IAAI,UAAU,eAAe,KAAK,EAAE,CAAC;AACpD,cAAQ,KAAK,CAAC;AAAA,IAChB;AAEA,UAAM,SAAS,MAAM,eAAe,KAAK;AAAA,MACvC,KAAK;AAAA,MACL,KAAK;AAAA,IACP,CAAC;AAED,QAAI,WAAW;AACb,cAAQ,IAAIA,IAAG,MAAM,uBAAuB,OAAO,OAAO,iBAAiB,CAAC;AAAA,IAC9E,OAAO;AACL,cAAQ,IAAIA,IAAG,MAAM,qBAAqBA,IAAG,KAAK,UAAW,CAAC,KAAK,OAAO,OAAO,WAAW,CAAC;AAAA,IAC/F;AAEA,YAAQ,IAAIA,IAAG,IAAI,oBAAoB,OAAO,SAAS,EAAE,CAAC;AAAA,EAC5D;AACF;AAMA,IAAM,cAA0B;AAAA,EAC9B,MAAM;AAAA,EACN,aAAa;AAAA,EACb,OAAO;AAAA,EACP,KAAK,OAAO,KAAK,SAAS;AACxB,UAAM,QAAQ,WAAW,IAAI;AAC7B,UAAM,OAAO,MAAM;AAEnB,QAAI,CAAC,MAAM;AACT,cAAQ,MAAMA,IAAG,IAAI,gCAAgC,CAAC;AACtD,cAAQ,IAAIA,IAAG,IAAI,UAAU,YAAY,KAAK,EAAE,CAAC;AACjD,cAAQ,KAAK,CAAC;AAAA,IAChB;AAEA,UAAM,SAAS,MAAM,iBAAiB,KAAK,EAAE,KAAK,CAAC;AAEnD,YAAQ,IAAIA,IAAG,MAAM,mBAAmBA,IAAG,KAAK,OAAO,IAAI,CAAC,GAAG,CAAC;AAChE,YAAQ,IAAIA,IAAG,IAAI,WAAW,OAAO,IAAI,EAAE,CAAC;AAC5C,YAAQ,IAAIA,IAAG,IAAI,cAAc,OAAO,OAAO,EAAE,CAAC;AAClD,YAAQ,IAAIA,IAAG,IAAI,mBAAmB,OAAO,YAAY,EAAE,CAAC;AAC5D,YAAQ,IAAIA,IAAG,IAAI,qBAAqB,OAAO,cAAc,EAAE,CAAC;AAAA,EAClE;AACF;AAMA,IAAM,cAA0B;AAAA,EAC9B,MAAM;AAAA,EACN,aAAa;AAAA,EACb,OAAO;AAAA,EACP,KAAK,OAAO,KAAK,SAAS;AACxB,UAAM,QAAQ,WAAW,IAAI;AAC7B,UAAM,OAAO,MAAM;AAEnB,QAAI,CAAC,MAAM;AACT,cAAQ,MAAMA,IAAG,IAAI,gCAAgC,CAAC;AACtD,cAAQ,IAAIA,IAAG,IAAI,UAAU,YAAY,KAAK,EAAE,CAAC;AACjD,cAAQ,KAAK,CAAC;AAAA,IAChB;AAEA,UAAM,SAAS,MAAM,iBAAiB,KAAK;AAAA,MACzC;AAAA,MACA,KAAK,MAAM;AAAA,IACb,CAAC;AAED,YAAQ,IAAIA,IAAG,MAAM,UAAUA,IAAG,KAAK,OAAO,IAAI,CAAC,UAAU,CAAC;AAC9D,YAAQ,IAAIA,IAAG,IAAI,cAAc,OAAO,OAAO,EAAE,CAAC;AAClD,YAAQ,IAAIA,IAAG,IAAI,mBAAmB,OAAO,YAAY,EAAE,CAAC;AAC5D,YAAQ,IAAIA,IAAG,IAAI,qBAAqB,OAAO,cAAc,EAAE,CAAC;AAAA,EAClE;AACF;AAMA,IAAM,cAA0B;AAAA,EAC9B,MAAM;AAAA,EACN,aAAa;AAAA,EACb,OAAO;AAAA,EACP,KAAK,OAAO,KAAK,SAAS;AACxB,UAAM,QAAQ,WAAW,IAAI;AAC7B,UAAM,WAAW,MAAM;AACvB,UAAM,YAAY,MAAM,YAAY,SAAS,MAAM,WAAW,EAAE,IAAI;AACpE,UAAM,SAAS,MAAM;AAErB,UAAM,SAAS,MAAM,YAAY,KAAK;AAAA,MACpC,QAAQ;AAAA,MACR,OAAO;AAAA,MACP;AAAA,MACA;AAAA,IACF,CAAC;AAGD,QAAI,QAAQ;AACV,YAAM,cAAc,OAAO,KAAK,OAAO,WAAW,QAAQ;AAC1D,MAAAC,eAAc,QAAQ,WAAW;AACjC,cAAQ,IAAID,IAAG,IAAI,uBAAuB,MAAM,EAAE,CAAC;AAAA,IACrD;AAEA,UAAM,MAAM,OAAO,eAAe,QAAQ,CAAC;AAC3C,UAAM,SAAS,OAAO,YAClBA,IAAG,MAAM,WAAW,IACpBA,IAAG,OAAO,GAAG,GAAG,WAAW;AAE/B,YAAQ;AAAA,MACN,SAAS,MAAM,KAAK,OAAO,WAAW,eAAe,CAAC,YAAY,OAAO,KAAK,IAAI,OAAO,MAAM;AAAA,IACjG;AAAA,EACF;AACF;AAMO,IAAM,cAA4B;AAAA,EACvC;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AACF;","names":["pc","writeFileSync","readFileSync","existsSync","mkdirSync","join","homedir","pc","writeFileSync"]}
|