/** * PTY Session Module * * Implements the ISession interface using node-pty for direct PTY access. * This provides better input reliability and Windows support compared to tmux. * * @module pty-session */ import * as pty from 'node-pty'; import type { ISession, SessionOptions } from '../session-backend.interface.js'; export declare function _setPtySpawnImplForTesting(impl: typeof pty.spawn): () => void; /** * Wraps `pty.spawn` with retry-on-transient + actionable error. * * On the 4th and final failure, throws `PtySpawnExhaustedError` with a * human-readable message + machine-readable diagnostics. Callers should * surface the .message to the user (e.g. ORC → Slack). */ declare class PtySpawnExhaustedError extends Error { readonly diagnostics: { attempts: number; lastError: string; userProcessCount: number | null; maxProcPerUid: number | null; command: string; platform: string; }; constructor(message: string, diagnostics: { attempts: number; lastError: string; userProcessCount: number | null; maxProcPerUid: number | null; command: string; platform: string; }); } /** Test affordance: forget the once-per-process spawn-helper check. */ export declare function _resetSpawnHelperCheckForTesting(): void; export { PtySpawnExhaustedError }; /** * PTY Session implementation using node-pty. * * Provides a direct PTY-based terminal session with the following capabilities: * - Spawn shell processes with configurable environment * - Write input and receive output via callbacks * - Resize terminal dimensions * - Graceful termination * * @example * ```typescript * const session = new PtySession('my-session', '/home/user', { * cwd: '/home/user', * command: '/bin/bash', * cols: 120, * rows: 40, * }); * * session.onData((data) => console.log(data)); * session.write('echo hello\n'); * session.kill(); * ``` */ export declare class PtySession implements ISession { readonly name: string; readonly cwd: string; /** * The underlying node-pty process */ private ptyProcess; /** * Set of data listeners for output events (O(1) add/remove) */ private dataListeners; /** * Set of exit listeners for process termination events (O(1) add/remove) */ private exitListeners; /** * Flag indicating whether the session has been killed */ private killed; /** * Flag indicating whether the underlying PTY file descriptors have been * released via {@link closePty}. Guards against double-close and makes * teardown idempotent. */ private ptyClosed; /** * Logger for this session */ private logger; /** * Create a new PTY session. * * @param name - Unique name for this session * @param cwd - Current working directory for the session * @param options - Session configuration options * @throws Error if PTY process spawn fails * * @example * ```typescript * const session = new PtySession('dev-session', '/home/user/project', { * cwd: '/home/user/project', * command: '/bin/bash', * args: ['--login'], * env: { NODE_ENV: 'development' }, * cols: 120, * rows: 40, * }); * ``` */ constructor(name: string, cwd: string, options: SessionOptions); /** * The process ID of the underlying PTY process. */ get pid(): number; /** * Subscribe to data output from the session. * * @param callback - Function called when data is received * @returns Unsubscribe function to remove the listener * @throws Error if maximum listener count is exceeded * * @example * ```typescript * const unsubscribe = session.onData((data) => { * process.stdout.write(data); * }); * // Later: unsubscribe(); * ``` */ onData(callback: (data: string) => void): () => void; /** * Subscribe to the session exit event. * * @param callback - Function called when the process exits * @returns Unsubscribe function to remove the listener * @throws Error if maximum listener count is exceeded * * @example * ```typescript * const unsubscribe = session.onExit((code) => { * console.log(`Session exited with code ${code}`); * }); * ``` */ onExit(callback: (code: number) => void): () => void; /** * Write data to the session's stdin. * * @param data - String data to write to the session * @throws Error if session has been killed * * @example * ```typescript * session.write('ls -la\n'); * session.write('\x03'); // Send Ctrl+C * ``` */ write(data: string): void; /** * Resize the terminal dimensions. * * @param cols - Number of columns (width) * @param rows - Number of rows (height) * @throws Error if session has been killed * * @example * ```typescript * session.resize(120, 40); * ``` */ resize(cols: number, rows: number): void; /** * Kill the session and terminate the underlying process. * * After calling kill(), the session cannot be used anymore. * All listeners will be cleared and subsequent write/resize calls will throw. * * @param signal - Optional signal to send (e.g. 'SIGTERM', 'SIGKILL'). Defaults to node-pty default (SIGHUP). * * @example * ```typescript * session.kill(); * session.kill('SIGKILL'); * ``` */ kill(signal?: string): void; /** * Release the underlying node-pty file descriptors (PTY master + slave). * * This is the missing piece that fixes the FD leak. node-pty's `kill(signal)` * only forwards a signal to the child shell process — it does NOT close the * master fd that node-pty opened on `/dev/ptmx` (and the associated * `/dev/ttysNNN` slave). On Unix, `node-pty`'s concrete `UnixTerminal` exposes * a `destroy()` method that closes its internal read-stream socket (and thus * the master fd) before SIGHUP-ing the shell. That method is intentionally * absent from the public `IPty` typings, so we feature-detect it at runtime. * * Idempotent and exception-safe: safe to call multiple times and after the * process has already exited. Any error (already-closed socket, missing * `destroy` on a future node-pty, Windows ConPTY) is swallowed — the worst * case is the pre-existing behaviour (no extra close), never a throw. * * @example * ```typescript * session.closePty(); // releases /dev/ptmx + /dev/ttysNNN FDs * ``` */ closePty(): void; /** * Forcefully kill the session with SIGTERM → SIGKILL escalation. * * Sends SIGTERM first, then after a delay escalates to SIGKILL on both * the process and its process group to ensure all child processes are terminated. * This is necessary because Claude Code and Gemini CLI may catch/ignore SIGHUP and SIGTERM. * * @returns Promise that resolves after the kill sequence completes * * @example * ```typescript * await session.forceKill(); * ``` */ forceKill(): Promise; /** * Check if the session has been killed. * * @returns true if the session has been killed */ isKilled(): boolean; /** * Check if a child process (e.g. Claude Code) is alive inside this PTY shell. * * Uses `pgrep -P ` to detect child processes of the shell. * Returns false if the shell has no children (Claude Code exited) or if * the check itself fails (shell already dead). * * @returns true if at least one child process is running inside the shell */ isChildProcessAlive(): boolean; /** * Set up event handlers for the PTY process. */ private setupEventHandlers; /** * Sanitize environment variables, removing undefined values and the * owner API token. * * `CREWLY_API_TOKEN` is the owner's credential (it unlocks OKR approvals * and remote API access). Agent sessions must never inherit it, otherwise * an agent could approve its own proposals or hand the token to a * remote caller. * * Backend-only integration secrets (API_SECURITY_CONSTANTS.AGENT_ENV_DENYLIST, * e.g. the Slack bot token and signing secret) are dropped too: nothing in * an agent reads them, and an agent running `env` printed them into its * session log. * * @param env - Environment variables object * @returns Sanitized environment object with string values only */ private sanitizeEnv; } /** * Whether an environment variable marks an IDE's integrated terminal * (VS Code and its forks), and so must not reach an agent: * - every `VSCODE_*` variable (this includes `VSCODE_GIT_*`); * - `TERM_PROGRAM` / `TERM_PROGRAM_VERSION` when `TERM_PROGRAM` names such * an IDE; * - `GIT_ASKPASS` / `SSH_ASKPASS` that point into an IDE, or any askpass * helper inherited from an IDE terminal: VS Code's askpass script talks to * the editor over the `VSCODE_GIT_*` socket we drop, so HTTPS git auth in * the agent would fail instead of using the normal credential helper. * * @param key - Variable name * @param env - The whole environment (to read `TERM_PROGRAM` and the IDE markers) * @returns True when the variable should not reach an agent */ export declare function isIdeTerminalEnv(key: string, env: Record): boolean; //# sourceMappingURL=pty-session.d.ts.map