/** * `agent-relay drive ` — interactive read-write take-over client. * * Attaches to a running agent, flips it into `manual_flush` inbound delivery mode so the * broker parks new relay messages in a per-worker queue, and forwards your * keystrokes to the worker's PTY. Message queue control is deliberately out of * band: use `local agent message flush`, `local agent message hold`, and * `local agent message auto` * from another terminal instead of terminal control chords that agent TUIs may * intercept. `Ctrl+C` detaches, restores the worker's previous inbound delivery * mode, and leaves the agent running under the broker — `drive` never kills the * worker. * * Sequence of operations on attach: * * 1. Discover broker connection (CLI flag → env → connection.json). * 2. `GET /api/spawned/{name}/delivery-mode` → remember the previous mode. * 3. `PUT /api/spawned/{name}/delivery-mode` → switch to `manual_flush`. * 4. `captureAndRenderSnapshot` → repaint the agent's current screen. * 5. `GET /api/spawned/{name}/pending` → seed the status-line counter. * 6. Open `/ws`, subscribe to events for this worker. * 7. Open the SDK PTY input stream, then switch local stdin to raw * mode and forward bytes through that stream. * * On detach (clean or abnormal), best-effort `PUT .../delivery-mode` restores the * previous mode so the queue doesn't fill up indefinitely. */ import { Buffer } from 'node:buffer'; import type { InboundDeliveryMode } from '@agent-relay/harness-driver'; import WebSocket from 'ws'; import { captureAndRenderSnapshot, type AttachSnapshotConnection, type AttachSnapshotDeps } from '../lib/attach.js'; import { type BrokerConnection } from '../lib/broker-connection.js'; import { type PtyInputStreamOptions, type PtyInputWriteResult } from '../lib/attach-broker.js'; import { type CreatePredictiveEchoOptions } from './predictive-echo-screen.js'; import type { PredictiveEcho } from '@agent-relay/harness-driver'; type ExitFn = (code: number) => never; /** Wire string for the broker's `InboundDeliveryMode` enum. */ export type { InboundDeliveryMode }; /** Minimal WebSocket surface we depend on — same shape as `view`'s. */ export interface DriveWebSocket { on(event: 'open', listener: () => void): unknown; on(event: 'message', listener: (data: WebSocket.RawData) => void): unknown; on(event: 'close', listener: (code: number, reason: Buffer) => void): unknown; on(event: 'error', listener: (err: Error) => void): unknown; close(code?: number, reason?: string): void; } export type DriveWebSocketFactory = (url: string, headers: Record) => DriveWebSocket; export interface DriveSignalRegistrar { (signal: NodeJS.Signals, handler: () => void | Promise): void | (() => void); } /** Stdin surface — tests provide a fake that never touches the real TTY. */ export interface DriveStdin { setRawMode?: (mode: boolean) => unknown; isTTY?: boolean; resume(): unknown; pause(): unknown; on(event: 'data', listener: (chunk: Buffer) => void): unknown; off?(event: 'data', listener: (chunk: Buffer) => void): unknown; removeListener?(event: 'data', listener: (chunk: Buffer) => void): unknown; } /** * Local terminal-size source. Wraps `process.stdout` in production so * the resize wiring reads the user's actual terminal dimensions and * gets a SIGWINCH-equivalent `'resize'` event for free. Tests inject a * controllable fake. */ export interface DriveTerminal { /** Current `(rows, cols)`. Returns `null` when stdout is not a TTY, * in which case resize forwarding is skipped entirely. */ getSize(): { rows: number; cols: number; } | null; /** Subscribe to local-terminal resize events. Returns an unsubscribe * function the client calls during teardown. */ onResize(handler: () => void): () => void; } export interface CliPtyInputStream { waitUntilOpen(): Promise; send(data: string): Promise; close(code?: number, reason?: string): void; /** Smoothed input→ack RTT (ms), or null before the first ack. */ readonly srttMs?: number | null; } export interface DriveDependencies { /** Reads `/connection.json` and returns parsed JSON, or null. */ readConnectionFile: (stateDir: string) => unknown; /** Project paths helper — used to pick the default state dir. */ getDefaultStateDir: () => string; /** Environment variables (so tests can inject). */ env: NodeJS.ProcessEnv; /** Factory for the WebSocket — overridden in tests with a mock. */ createWebSocket: DriveWebSocketFactory; /** Where the PTY chunks get written. Defaults to `process.stdout.write`. */ writeChunk: (chunk: string) => void; /** Signal registration (so tests can drive SIGINT without killing the test). */ onSignal: DriveSignalRegistrar; log: (...args: unknown[]) => void; error: (...args: unknown[]) => void; exit: ExitFn; /** HTTP client used for mode/pending/flush/resize calls. Defaults to global `fetch`. */ fetch: typeof globalThis.fetch; /** Override for the snapshot-on-attach helper (tests substitute a stub). */ captureAndRenderSnapshot: (connection: AttachSnapshotConnection, name: string, deps: AttachSnapshotDeps) => ReturnType; /** Stdin handle — defaults to `process.stdin`. */ stdin: DriveStdin; /** Local terminal size source — defaults to `process.stdout`. */ terminal: DriveTerminal; /** Opens the SDK PTY input stream used for raw human keystrokes. */ openInputStream: (connection: BrokerConnection, name: string, options?: PtyInputStreamOptions) => CliPtyInputStream; /** * Builds the adaptive predictive-echo engine, or returns null to disable * it (degenerate terminal). Omitted by tests that want plain pass-through. */ createPredictiveEcho?: (opts: CreatePredictiveEchoOptions) => PredictiveEcho | null; } /** ----- HTTP helpers ----- */ /** `GET /api/spawned/{name}/delivery-mode` → `'manual_flush' | 'auto_inject'` or `null` on failure. */ export declare function getInboundDeliveryMode(connection: BrokerConnection, name: string, fetchFn: typeof globalThis.fetch): Promise; /** Outcome of a `PUT /api/spawned/{name}/delivery-mode` call. */ export interface SetInboundDeliveryModeResult { ok: boolean; status: number; /** Server-reported number of pending messages drained on a `manual_flush→auto_inject` flip. */ flushed?: number; /** Human-readable error message when `ok` is false. */ message?: string; } export declare function setInboundDeliveryMode(connection: BrokerConnection, name: string, mode: InboundDeliveryMode, fetchFn: typeof globalThis.fetch): Promise; /** `GET /api/spawned/{name}/pending` → count, or `0` on failure (best-effort). */ export declare function getPendingCount(connection: BrokerConnection, name: string, fetchFn: typeof globalThis.fetch): Promise; /** `POST /api/spawned/{name}/flush` → server returns `{ flushed: N }`. */ export declare function flushPending(connection: BrokerConnection, name: string, fetchFn: typeof globalThis.fetch): Promise<{ ok: boolean; flushed?: number; message?: string; }>; /** `POST /api/input/{name}` body `{ data: "" }`. */ export declare function sendInput(connection: BrokerConnection, name: string, data: string, fetchFn: typeof globalThis.fetch): Promise<{ ok: boolean; message?: string; }>; /** Open the SDK-backed raw PTY input stream for interactive CLI sessions. */ export declare function openPtyInputStream(connection: BrokerConnection, name: string, fetchFn: typeof globalThis.fetch, options?: PtyInputStreamOptions): CliPtyInputStream; /** * `POST /api/resize/{name}` body `{ rows, cols }`. Forwards the * driver's local terminal dimensions so the agent's PTY (and any TUI * running in it) sees the size the human is actually looking at. * Called once on attach and again on every local-terminal resize. */ export declare function resizeWorker(connection: BrokerConnection, name: string, rows: number, cols: number, fetchFn: typeof globalThis.fetch): Promise<{ ok: boolean; message?: string; }>; /** Discriminated union of the broker events `drive` cares about. */ export type DriveWsEvent = { kind: 'worker_stream'; chunk: string; } | { kind: 'delivery_queued'; } | { kind: 'agent_pending_drained'; count?: number; } | { kind: 'other'; }; /** * Inspect a single WebSocket frame and classify it relative to the agent * we're driving. Non-matching / malformed frames return `{ kind: 'other' }` * so the caller can ignore them cheaply. * * Exported for unit testing the filter in isolation. */ export declare function classifyWsEvent(rawMessage: string, name: string): DriveWsEvent; /** ----- Keybind state machine ----- */ /** Outcome of feeding one chunk to the keybind parser. */ export interface KeybindOutcome { /** Bytes that should be forwarded to the agent (may be empty). */ forward: Buffer; /** Local actions the client should perform, in order. */ actions: KeybindAction[]; } export type KeybindAction = 'detach'; /** * Parser for the one local control byte drive keeps: `Ctrl+C` detaches. * * Semantics: * - `Ctrl+C` (0x03) → emit `detach`, never forwarded. * - Every other byte, including Ctrl+B and Ctrl+G, is forwarded to the agent. */ export declare class KeybindParser { /** Process one chunk; returns bytes to forward + actions to take. */ feed(chunk: Buffer): KeybindOutcome; /** Reset the parser (e.g. before tearing down). */ reset(): void; } /** ----- Status line rendering ----- */ /** * Render the bottom-of-terminal status line for `drive`. Uses ANSI * save-cursor / restore-cursor so the agent's output isn't disturbed. * * Exported for unit testing — `runDriveSession` calls it on every * pending-count change. */ export declare function renderStatusLine(opts: { name: string; mode: InboundDeliveryMode; pending: number; /** Terminal rows — defaults to 24 if unknown. The status line lands on row N. */ rows?: number; }): string; /** * Open a `drive` session. Resolves with the exit code the CLI should * propagate. Cleans up its own stdin raw-mode and best-effort restores * the worker's previous inbound delivery mode on any exit path. */ export declare function runDriveSession(agentName: string, options: { brokerUrl?: string; apiKey?: string; stateDir?: string; }, deps: DriveDependencies): Promise; /** Run a drive session with default dependencies. Used by `runtime agent attach --mode drive`. */ export declare function attachDrive(name: string, options: { brokerUrl?: string; apiKey?: string; stateDir?: string; }, overrides?: Partial): Promise; //# sourceMappingURL=attach-drive.d.ts.map