/** * HTTP client for the chrome-devtools-axi bridge + bridge lifecycle management. */ import { AxiError } from "axi-sdk-js"; /** * Resolve the bridge readiness deadline in milliseconds. * * Honors `CHROME_DEVTOOLS_AXI_BRIDGE_TIMEOUT_MS` for systems where npx * bootstrap or Chrome launch is slow (>30s). Values below 1s are clamped to * 1s to avoid pathological retries. */ export declare function resolveBridgeTimeoutMs(): number; export type ErrorCode = "BRIDGE_NOT_READY" | "REF_NOT_FOUND" | "STALE_REF" | "TIMEOUT" | "BROWSER_ERROR" | "VALIDATION_ERROR" | "UNKNOWN"; export declare class CdpError extends AxiError { readonly code: ErrorCode; readonly suggestions: string[]; constructor(message: string, code: ErrorCode, suggestions?: string[]); } /** * Probe the bridge's `/health` endpoint. With `deep: true`, asks the bridge * to drive one CDP-backed MCP call (`list_pages`) so callers can distinguish * "MCP server is up but the attached browser is gone" from genuine readiness. * * With `expectedSession`, a bridge that reports a *different* session name is * treated as unhealthy, so a session never silently reuses another session's * bridge after a port collision (two sessions pinned to one port via a global * `CHROME_DEVTOOLS_AXI_PORT`). A bridge that omits the field (older version) is * accepted, since there is no mismatch to detect. * * With `notice`, a healthy *deep* probe writes the bridge's `pageIdentityChanged` * flag into that caller-owned holder (see {@link PageIdentityNotice}). * * Exported for tests; production code uses it via {@link ensureBridge}. */ export declare function checkBridgeHealth(port: number, opts?: { deep?: boolean; expectedSession?: string; notice?: PageIdentityNotice; }): Promise; /** * One {@link callTool} invocation's reconnect notice: whether the deep probe * that ran for *this* call reported that chrome-devtools-mcp had reissued every * page id *and* that this took a selection with it. The bridge gates the flag * on the clear having removed an id, so a session that never selected a page is * never told it lost one. * * `ensureBridge` deep-probes before every command, and that probe's * `list_pages` is what consumes chrome-devtools-mcp's one-shot reconnect marker * and clears the persisted selection - so without this relay the command that * follows finds no selection and blames the caller for never selecting a page. * * The holder is created by the caller and threaded through, never module state: * a `run` script can have several `callTool`s in flight at once, and a shared * slot would let one call's probe overwrite - or one call's resolution consume - * another's attribution, so a reconnect could be reported against the wrong * operation or dropped entirely. Per-invocation ownership also keeps the signal * one-shot in the same spirit as the marker it relays: it explains only the call * whose own probe consumed the marker, and is discarded with that call rather * than relabelling later no-selection errors in the same process. A call that * resolves no selection simply drops it - `pages` only calls `list_pages`, and * the home view probe carries no holder at all, since its own health check is * shallow and its `take_snapshot` carries the persisted id without coming * through here. */ export interface PageIdentityNotice { /** Written by the last deep probe of the owning `ensureBridge` call. */ pageIdentityChanged: boolean; } export declare function waitForProcessExit(pid: number, timeoutMs: number): Promise; /** * Terminate a bridge process and reap its detached process group. Sends * SIGTERM, polls up to ~2s for exit, then escalates to SIGKILL on the entire * process group so chrome-devtools-mcp / Chrome children can't survive as * orphans. Returns once the bridge PID is gone (or the SIGKILL grace window * expires). */ export declare function terminateBridgeProcess(pid: number, opts?: { killProcessGroup?: boolean; }): Promise; /** * Minimal view of the spawned bridge process that {@link ensureBridge} needs: * an `exit` notification so a bridge that dies before reporting healthy can be * detected. The default {@link spawnBridgeProcess} returns a `ChildProcess` * (which satisfies this); tests inject a fake. */ export interface SpawnedBridge { on(event: "exit", listener: (code: number | null, signal: NodeJS.Signals | null) => void): void; } /** * Build the error thrown when a freshly spawned bridge exits before it ever * reports healthy. Surfacing this the moment the child dies - rather than * polling the full readiness deadline - turns an early death into a fast, * actionable failure instead of a slow, generic "failed to start" timeout. * * The guidance is attributed by exit code. Only {@link BRIDGE_PORT_IN_USE_EXIT_CODE} * (the bridge's EADDRINUSE sentinel) gets the port-in-use explanation; any * other early death is a startup failure. Direct shared-MCP configuration gets * endpoint-specific guidance; proxy configuration gets `MCP_PATH`/`--serverUrl` * prerequisites; local mode covers npx resolution, a broken * `CHROME_DEVTOOLS_AXI_MCP_PATH`, or a Chrome launch failure. In either mode, a * single-session user with a broken install is not misdirected to port advice. */ export declare function buildBridgeEarlyExitError(sessionName: string, port: number, code: number | null, signal: NodeJS.Signals | null): CdpError; /** * Ensure the bridge is running, starting it if needed. Returns the port. * * Verifies a *deep* health check (one round-trip CDP-backed MCP call) before * declaring the bridge ready, so a bridge whose attached browser/Electron * target was killed while still answering local /health requests gets torn * down + restarted instead of being reused as a stale endpoint. * * `spawnBridge` is injectable for tests; production uses {@link spawnBridgeProcess}. * * `notice` is the caller's own {@link PageIdentityNotice} holder; every deep * probe this call makes writes its `pageIdentityChanged` flag there, so the * reconnect attribution belongs to this invocation and cannot cross a * concurrent one. */ export declare function ensureBridge(spawnBridge?: (port: number, sessionName: string) => SpawnedBridge, notice?: PageIdentityNotice): Promise; /** * The workspace roots a call needs: always the invoking cwd (so writes under it * pass), plus the nearest existing ancestor of any output path argument (so a * write outside cwd, e.g. `$HOME/a.png`, passes too). */ export declare function collectRootDirs(name: string, args: Record): string[]; /** * Call an MCP tool via the bridge. Returns the text result. * * Page-scoped tools (eval/snapshot/click/fill and the rest of * `PAGE_SCOPED_TOOLS` in `src/pages.ts`) get the session's last * `select_page` / url-matched `new_page` `pageId` injected so they satisfy * chrome-devtools-mcp 1.8+ defaults. `list_pages` is never consulted. */ export declare function callTool(name: string, args?: Record): Promise; /** * The reconnect already happened before this command ran: the deep probe * consumed the marker and dropped the routing it invalidated, so nothing was * sent to a wrong tab and there is nothing to retarget — the caller just has * to re-select. Keeps the `BROWSER_ERROR` code and the "no page" clause of the * plain no-selection message so `open`'s existing recovery still applies (it * creates a new tab; see AGENTS.md). * * Exported so the `open` / `page.open` recovery tests build their rejection * from the message this ships rather than a copy of it, which is what makes * them fail if a reword breaks that match. */ export declare function pageIdentityClearedError(): CdpError; export declare function mapErrorMessage(message: string): CdpError; /** * Get the current page snapshot without starting the bridge. * * Returns null if the bridge is not running or healthy. This is the ambient * home view / SessionStart probe, so it must stay cheap and never throw: an * invalid `CHROME_DEVTOOLS_AXI_SESSION` degrades to "no active session" (null) * here, while action commands (`ensureBridge` / `stopBridge`) still fail loudly. */ export declare function getSessionSnapshotIfRunning(): Promise; /** * Stop the bridge process. Waits for the bridge PID to actually exit (bounded * poll, ~2s) before escalating to SIGKILL on the entire detached process * group, so chrome-devtools-mcp + Chrome children get reaped together rather * than orphaned. Resolves once the bridge process is gone. */ export declare function stopBridge(): Promise;