/** * `agent-relay passthrough ` — read-write attach in passthrough session. * * The broker auto-injects inbound relay messages into the agent's PTY * while the human also types; both writers race. That's the point — * passthrough is for observe-and-occasionally-nudge sessions * while the broker does its coordination thing. For exclusive * deterministic control with no auto-inject, use `drive` instead. * * On attach, ensures the worker is in `auto_inject` delivery mode (it's the * broker default, but if someone left a `drive` session the worker may * be in `manual_flush` mode — `passthrough` flips it back for the session's * duration and restores the prior mode on detach). On detach, restores * the prior mode and leaves the agent running. * * The session loop (snapshot-on-attach, raw stdin, resize forwarding, * detach keybind, Ctrl+C-as-detach safety alias) mirrors the shape of * `drive.ts` minus the pending-queue UI and `Ctrl+G` flush binding * (there's no queue in passthrough session). `drive.ts` is the more * heavily-commented version of the shared shape; this module * duplicates rather than abstracts because the trimmed surface is * small enough that an extra layer of indirection would cost more * clarity than it saves. */ import { Buffer } from 'node:buffer'; import { Command } from 'commander'; import WebSocket from 'ws'; import { captureAndRenderSnapshot, type AttachSnapshotConnection, type AttachSnapshotDeps } from '../lib/attach.js'; import { type BrokerConnection } from '../lib/broker-connection.js'; import { type CliPtyInputStream, type InboundDeliveryMode } from './drive.js'; type ExitFn = (code: number) => never; /** Minimal WebSocket surface we depend on — same shape as `drive`'s. */ export interface PassthroughWebSocket { 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 PassthroughWebSocketFactory = (url: string, headers: Record) => PassthroughWebSocket; export interface PassthroughSignalRegistrar { (signal: NodeJS.Signals, handler: () => void | Promise): void; } export interface PassthroughStdin { 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; } export interface PassthroughTerminal { getSize(): { rows: number; cols: number; } | null; onResize(handler: () => void): () => void; } export interface PassthroughDependencies { readConnectionFile: (stateDir: string) => unknown; getDefaultStateDir: () => string; env: NodeJS.ProcessEnv; createWebSocket: PassthroughWebSocketFactory; writeChunk: (chunk: string) => void; onSignal: PassthroughSignalRegistrar; log: (...args: unknown[]) => void; error: (...args: unknown[]) => void; exit: ExitFn; fetch: typeof globalThis.fetch; captureAndRenderSnapshot: (connection: AttachSnapshotConnection, name: string, deps: AttachSnapshotDeps) => ReturnType; stdin: PassthroughStdin; terminal: PassthroughTerminal; /** Opens the SDK PTY input stream used for raw human keystrokes. */ openInputStream: (connection: BrokerConnection, name: string) => CliPtyInputStream; } /** Discriminated union of broker events the `passthrough` client cares * about. No `delivery_queued` / `agent_pending_drained` — there's no * queue in passthrough session, so those events (which the broker doesn't * emit while the worker is in `auto_inject`) would be `other`. */ export type PassthroughWsEvent = { kind: 'worker_stream'; chunk: string; } | { kind: 'other'; }; /** * Inspect a single WebSocket frame and classify it relative to the * agent we're following. Non-matching / malformed frames return * `{ kind: 'other' }` so the caller can ignore them cheaply. */ export declare function classifyWsEvent(rawMessage: string, name: string): PassthroughWsEvent; /** ----- Keybind state machine ----- */ export interface PassthroughKeybindOutcome { forward: Buffer; actions: PassthroughKeybindAction[]; } export type PassthroughKeybindAction = 'detach' | 'toggle_help'; /** * Stateful parser for the passthrough client's keybind vocabulary. * Smaller than `drive`'s because there's no queue to flush — no * `Ctrl+G` binding. * * Semantics: * - `Ctrl+C` (0x03) → emit `detach`, never forwarded. * - `Ctrl+B` (0x02) → swallow, arm the prefix state. * Next byte: * - 'd' / 'D' / 0x04 (Ctrl+D) → emit `detach`. * - '?' → emit `toggle_help`. * - anything else → forward `Ctrl+B` + the byte so * TUI apps using `Ctrl+B` themselves * aren't deprived. */ export declare class PassthroughKeybindParser { private pendingPrefix; feed(chunk: Buffer): PassthroughKeybindOutcome; reset(): void; } /** ----- Status line rendering ----- */ /** * Render the bottom-of-terminal status line for `passthrough`. Same * save/restore-cursor trick as `drive`, no pending counter (there * isn't one in passthrough session). */ export declare function renderStatusLine(opts: { name: string; mode: InboundDeliveryMode; showHelp: boolean; rows?: number; }): string; /** ----- Main session runner ----- */ /** * Open a `passthrough` 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 runPassthroughSession(agentName: string, options: { brokerUrl?: string; apiKey?: string; stateDir?: string; }, deps: PassthroughDependencies): Promise; /** Register `agent-relay passthrough ` on the supplied commander program. */ export declare function registerPassthroughCommands(program: Command, overrides?: Partial): void; export {}; //# sourceMappingURL=passthrough.d.ts.map