import type { DriverEventEnvelope, RuntimeDriver, RuntimeDriverRunHandle, RuntimeDriverRunInput } from "../types.js"; /** * `claude-interactive` driver — drives the REAL Claude Code interactive TUI. * * Unlike `claude-cli` / `claude-sdk` (which spawn a fresh one-shot process per * turn and fake continuity via `--resume`), this driver keeps ONE long-lived * interactive `claude` process alive per session, inside a PTY, and types each * turn's prompt into it. Context stays hot in the live process — no per-turn * cold start, no transcript reload. * * The hard problems of driving a TUI (no structured stdout, fragile * turn-completion detection) are NOT solved by scraping the rendered screen. * Instead we observe the session through two structured side channels that * Claude Code itself writes: * * 1. OUTPUT — the session transcript JSONL. Claude writes every user / * assistant / tool message to `/projects//.jsonl`. * The filename IS the session id, so we locate it by glob on the id we * pre-assigned via `--session-id` (no need to derive Claude's cwd slug). * Each new line is parsed by the SAME `parseClaudeSdkMessage` used by the * SDK/CLI drivers, so message fidelity is identical — typed tool_use / * tool_result / usage blocks, not scraped text. * * 2. TURN-END — a `Stop` hook. We inject a per-session `--settings` file * carrying ONLY a Stop hook that appends a line to a signal file when * Claude finishes a turn. This is a deterministic turn boundary, not a * debounce on a redrawing screen. The settings file is per-invocation so * it never touches the user's `~/.claude/settings.json` — other running * Claude sessions are unaffected. * * The PTY's only jobs: keep the TUI alive, type input (bracketed paste), * send ESC to cancel a turn, and send `/quit` to tear the session down. */ export interface PtyProcessLike { readonly pid: number; write(data: string): void; onData(listener: (data: string) => void): void; onExit(listener: (event: { exitCode: number; signal?: number; }) => void): void; kill(signal?: string): void; } export type PtySpawnFn = (file: string, args: string[], options: { cwd: string; env: Record; cols: number; rows: number; name: string; }) => PtyProcessLike; export interface ClaudeInteractiveDriverOptions { /** Inject a PTY spawner (tests). Defaults to a lazy `node-pty` import. */ ptySpawn?: PtySpawnFn; executable?: string; /** Override where Claude writes transcripts/config. Defaults to env / ~/.claude. */ configDir?: string; /** Quiet window after spawn before the TUI is considered ready for input. */ readyQuietMs?: number; /** Hard cap waiting for the TUI to settle after spawn. */ readyTimeoutMs?: number; /** Delay between bracketed-paste of the prompt and the submitting Enter. */ submitDelayMs?: number; /** Poll cadence for the JSONL transcript + turn-end signal file. */ pollIntervalMs?: number; /** Hard cap for a single turn before giving up. */ turnTimeoutMs?: number; } export declare class ClaudeInteractiveDriver implements RuntimeDriver { readonly name: "claude-interactive"; private readonly executable; private readonly injectedPtySpawn?; private readonly configDirOverride?; private readonly opts; private readonly sessions; constructor(options?: ClaudeInteractiveDriverOptions); run(input: RuntimeDriverRunInput, onEventRaw: (event: DriverEventEnvelope) => Promise | void): RuntimeDriverRunHandle; /** * Graceful teardown of a session's interactive process. The runtime's * `stop()` calls this. We type `/quit` (a real interactive command) so the * TUI shuts down cleanly, flushing its transcript, then drop the session. */ dispose(sessionId: string): Promise; private ensureSession; /** Read newly written transcript lines, parse, and emit them as messages. */ private pumpTranscript; /** True if the Stop hook appended at least one new line since last checked. */ private consumeTurnEnd; private loadNodePty; }