/** * Visual-capture DEMAND contract — the cross-process rendezvous between the * MCP server (which asks for a frame) and the browser-capture runner (which * owns Chrome). * * Why a file and not the event bus: the bus is ONE-WAY. Producers (the Vite * plugin, the NestJS instrumentation, the capture runner) append to * `LENSMCP_EVENT_FILE` / the UDS socket and the MCP session tails it — nothing * flows back. So `visual.capture_frame` publishing a `visual-capture-request` * onto the MCP's in-process bus was a DEAD LETTER: the runner never saw it, * which is why capture had to poll a live Chrome forever (measured on a real * machine, 2026-07-29: 2 runners + >1GB of Chrome resident for 1d8h with * nobody capturing). Both processes already share the event-file directory, so * a single-line request file there is the cheapest signal that actually * crosses the boundary — the runner stats one small file instead of holding a * browser open on the chance someone asks. * * This module is deliberately node-free (no `node:path`/`node:fs`): protocol * types are bundled into the BROWSER client runtime, so it exports only the * filename + payload shape. Each side does its own path join / IO. */ import type { VisualFrameCause } from './attributes.js'; /** Request-file name, resolved as a sibling of `LENSMCP_EVENT_FILE`. */ export declare const CAPTURE_REQUEST_FILENAME = "capture-request.json"; /** * Presence-file name (same directory). The runner writes it at startup and * removes it on a clean stop. * * Its job is to keep the requester honest: `visual.capture_frame` now WAITS for * the fresh frame it asked for (a cold browser needs ~1s), and without a way to * tell "a runner is warming up" from "no runner exists" that wait would also be * paid by every workspace running with capture disabled or no Chrome installed * — an 8s stall for a frame that is never coming. With this, the tool waits * only when a live runner is actually listening, and can say so when it isn't. */ export declare const CAPTURE_PRESENCE_FILENAME = "capture-runner.json"; /** Advertised by a live capture runner. `pid` is checkable with `kill(pid, 0)`, * so a runner that was SIGKILLed leaves a file that readers can tell is dead. */ export interface CapturePresence { pid: number; /** `Date.now()` at runner startup. */ startedAt: number; /** Dev URL this runner captures. */ url: string; } /** One capture request. Written whole (single `writeFileSync`) so a reader * never sees a half-written line; `requestedAt` is the demand clock the * runner's idle timer works off. */ export interface CaptureRequest { /** `Date.now()` when the request was made. */ requestedAt: number; /** Why the frame was asked for — becomes the frame's `causedBy`. Validate on * read (`VisualFrameCauseSchema`): this arrives from another process, and a * hand-edited file must not put a bogus cause on a published frame. */ cause: VisualFrameCause; /** Whether to treat the captured frame as the stable final state. */ stable: boolean; /** Opaque id, so a reader can tell two same-millisecond requests apart. */ requestId?: string; }