UNPKG

@wonderwhy-er/desktop-commander

Version:

MCP server for terminal operations and file editing

222 lines (221 loc) • 9.82 kB
import { TerminalSession, CommandExecutionResult, ActiveSession } from './types.js'; import { type ProcessState } from './utils/process-detection.js'; /** A child process's output pipes. */ type OutputStream = 'stdout' | 'stderr'; /** The end of one output pipe, kept for process-state detection (see getProcessState). */ interface StreamTail { text: string; chars: number; turn: number; } type StreamTails = Record<OutputStream, StreamTail>; /** An active session plus the per-stream tails that state detection reads. */ interface ManagedSession extends TerminalSession { streams: StreamTails; termination?: Promise<boolean>; } /** Outcome of forceTerminate */ export type TerminateOutcome = 'terminated' | 'failed' | 'not_found'; interface CompletedSession { session: ManagedSession; exitCode: number | null; signal: NodeJS.Signals | null; endTime: Date; } /** * A position in a session's output, absolute since process start (evicted * output included), so it stays valid while the buffer cap evicts lines. */ export interface OutputSnapshot { totalChars: number; lineCount: number; streamChars: Record<OutputStream, number>; } /** * Ceiling for a single blocking process wait (start_process's initial wait, * interact_with_process, read_process_output). The wait is otherwise bounded * only by the caller's timeout_ms, and MCP clients abandon a tool call after * ~4 minutes (Claude Desktop: "No result received ... after 4 minutes"), so a * large timeout_ms on a long, prompt-less command used to outlive the client. * The ceiling is MAX_PROCESS_WAIT_MS; callers other than the tools (tests) may * pass a smaller one. */ export interface ProcessWaitLimit { waitMs: number; capMs: number; capped: boolean; } /** * The one place that turns a caller's timeout_ms into the time a process wait * may actually block. When `capped`, the call returns at the ceiling with the * process still running and the caller continues with read_process_output. */ export declare function getProcessWaitLimit(timeoutMs: number, capMs?: number): ProcessWaitLimit; /** executeCommand result: CommandExecutionResult plus the process state and whether the wait ceiling ended the wait. */ export interface ProcessStartResult extends CommandExecutionResult { processState: ProcessState; waitCappedAtMs?: number; } /** * Output buffering caps. Without a cap, a process emitting enough output makes * string concatenation throw "RangeError: Invalid string length" at V8's max * string size (~536M chars) inside a stdout 'data' handler — an uncaught * exception that kills the whole server (index.ts exits on uncaughtException). * The cap also bounds the cost of the reads that are O(total output), such as * since-start snapshot reads. */ export declare const MAX_BUFFERED_OUTPUT_CHARS: number; export interface PaginatedOutputResult { lines: string[]; totalLines: number; readFrom: number; readCount: number; remaining: number; isComplete: boolean; exitCode?: number | null; signal?: NodeJS.Signals | null; runtimeMs?: number; evictedLines?: number; } export declare class TerminalManager { private sessions; private completedSessions; private deliveryTurn; private deliveryTurnOpen; /** * Send input to a running process * @param pid Process ID * @param input Text to send to the process * @returns Whether input was successfully sent */ sendInputToProcess(pid: number, input: string): boolean; executeCommand(command: string, timeoutMs?: number, shell?: string, collectTiming?: boolean, maxWaitMs?: number): Promise<ProcessStartResult>; /** * Add a chunk of one stream's output to the session: to the merged line * buffer, and to that stream's tail for state detection. */ private recordOutput; /** * Number of the event-loop turn now delivering output; every chunk * dispatched before the next check phase (setImmediate) shares it. * * stdout and stderr are separate pipes. Each turn delivers what the OS had * ready when the loop polled, so a chunk delivered in a later turn was * written after the chunks of earlier turns. Within one turn the pipes come * in either order: when python -i's final stdout newline and its stderr * ">>> " prompt are both waiting, the prompt is often delivered first. */ private currentDeliveryTurn; /** * Append text to a session's line buffer * Handles partial lines and newline splitting */ private appendToLineBuffer; /** * Read process output with pagination (like file reading) * @param pid Process ID * @param offset Line offset: 0=from lastReadIndex, positive=absolute, negative=tail * @param length Max lines to return * @param updateReadIndex Whether to update lastReadIndex (default: true for offset=0) */ readOutputPaginated(pid: number, offset?: number, length?: number): PaginatedOutputResult | null; /** * Whether a default read (offset 0) has anything to return: complete lines * past the read position, or an unfinished last line with text a read * hasn't returned (see readFromLineBuffer). Line counts alone can't tell * the second: text appended to that line adds no line. */ hasUnreadOutput(pid: number): boolean; /** * Whether the unfinished last line has text that a default read hasn't * returned. Empty, it has nothing to return. Its text is compared with what * a read last returned only while it is the line that read stopped at: a * later line is new even when its text is the same (a REPL's next ">>> "). */ private static openLineHasNewText; /** * Internal helper to read from a session's line buffer with offset/length */ private readFromLineBuffer; /** * Get total line count for a process */ getOutputLineCount(pid: number): number | null; /** * Legacy method for backward compatibility * Returns all new output since last read * @param maxLines Maximum lines to return (default: 1000 for context protection) * @deprecated Use readOutputPaginated instead */ getNewOutput(pid: number, maxLines?: number): string | null; /** * Capture a snapshot of current output state for interaction tracking. * Used by interactWithProcess to know what output existed before sending input. */ captureOutputSnapshot(pid: number): OutputSnapshot | null; /** * Get output that appeared since a snapshot was taken. * This handles the case where output is appended to the last line (REPL prompts). * Also checks completed sessions in case process finished between snapshot and poll. */ getOutputSinceSnapshot(pid: number, snapshot: OutputSnapshot): string | null; /** * Output that appeared since `snapshot`, plus the snapshot at the current end * of output. A poller passes `next` back in to read only the output that * arrived since its previous poll. Costs O(output since snapshot), never * O(whole buffer), so polling stays cheap however much history the session * has retained. Also checks completed sessions in case the process finished * between snapshot and poll. */ readOutputSince(pid: number, snapshot: OutputSnapshot): { output: string; next: OutputSnapshot; } | null; /** * Snapshot at the current end of a buffer, in O(1): bufferedChars is the * joined length of the retained lines, so nothing is joined here. */ private static endOfOutput; /** * The process's state: finished once it has exited, whatever its output * says; otherwise waiting for input or running, judged from the end of the * output it wrote since `since` (default: since it started). Reads only the * per-stream tails, so it costs the same however much output the process * has produced. The one place start_process, interact_with_process, * read_process_output and list_sessions get the state from. */ getProcessState(pid: number, since?: OutputSnapshot): ProcessState | null; /** * The output since `since` of each stream that may have written last: the * streams delivered in the latest turn that delivered anything since then. * Their relative order within that turn is unknown (see * currentDeliveryTurn), so each is judged on its own; streams last * delivered in an earlier turn wrote before them and can't end the output. */ private static lastWrittenOutput; /** * New output since a snapshot, in absolute (since process start) offsets: * the last (end - snapshot) chars of the joined buffer. Walks back from the * newest line only until the snapshot position is covered and joins just * those lines. If eviction dropped part of the unseen output, returns what * the buffer still holds — the oldest unseen chars are lost to the cap. */ private static outputSinceSnapshot; /** * Get a session by PID * @param pid Process ID * @returns The session or undefined if not found */ getSession(pid: number): TerminalSession | undefined; /** * End a session: its shell and every process the command started (see * terminateProcessTree). Killing only the shell left the program running. * Resolves once they are gone: 'terminated', or 'failed' if some could not * be ended; 'not_found' when no session runs as `pid`. Never rejects. */ forceTerminate(pid: number): Promise<TerminateOutcome>; listActiveSessions(): ActiveSession[]; listCompletedSessions(): CompletedSession[]; } export declare const terminalManager: TerminalManager; export {};