/** * Control client: the MCP server's half of the link to Aseprite. * * Responsibilities beyond "send JSON": * - spawn the bridge if nothing owns the control port yet (zero-setup goal); * - reconnect with backoff, because the bridge may be restarted under us; * - track `pluginConnected` from bridge_state so `preflight` can answer * truthfully without a round-trip to Aseprite. */ import { HelloFrame } from "../lib/protocol.js"; import type { AsepriteLink } from "./link.js"; export interface LiveClientOptions { controlPort?: number; pluginPort?: number; /** Disable auto-spawning the bridge (useful in tests and CI). */ autoSpawnBridge?: boolean; timeoutMs?: number; log?: (msg: string) => void; } export declare class LiveClient implements AsepriteLink { readonly mode: "live"; private readonly controlPort; private readonly pluginPort; private readonly autoSpawn; private readonly timeoutMs; private readonly log; private socket; private pending; private seq; private reconnectDelay; private reconnectTimer; private closed; private lastSpawnAt; private spawnedPid; private state; constructor(opts?: LiveClientOptions); get bridgeConnected(): boolean; get pluginConnected(): boolean; get hello(): HelloFrame | null; /** * PID of a bridge this client started itself, or null if it attached to one * that was already running. `doctor` needs the difference: a diagnostic must * not leave a daemon behind, and must not report a bridge as healthy when the * only reason it is up is that the diagnostic started it. */ get spawnedBridgePid(): number | null; get features(): string[]; hasFeature(name: string): boolean; /** Wait until the bridge link is up (not necessarily Aseprite). */ waitForBridge(ms?: number): Promise; /** Wait until Aseprite itself is attached. */ waitForPlugin(ms?: number): Promise; /** * @param opts.expect Field names the caller goes on to read. Nothing else checks * them: `call` is a compile-time claim about data that crossed a socket * from a separately installed extension, and an extension older than this * server answers a command it knows with fields it does not have. Without * this the missing value becomes `undefined`, flows into the summary the * agent reads, and the run looks like it worked. * * Presence only — this is not schema validation, and it cannot catch an * argument name the two sides spell differently, because Lua defaults the * value and reports success. Only tests/extension.test.lua catches that. * * NEVER list a field the extension may leave unset. A Lua table cannot hold * nil, so `{sprite = nil}` does not reach the wire as `null` — the key is * simply absent, and is indistinguishable from an old extension omitting * it. Listing `sprite` here turned "no document is open", which is the * normal state of a freshly started editor, into a hard error telling the * user to reinstall a perfectly current extension. */ call(cmd: string, args?: Record, opts?: { expect?: readonly string[]; timeoutMs?: number; }): Promise; close(): void; private rejectPending; private connect; private scheduleReconnect; private onMessage; /** * Start the bridge as a detached child so it survives this MCP process. * Losing the bind race is normal and silent — it means a bridge already runs. */ private spawnBridge; }