/** * Background job protocol: commands sent to the server and parsing their replies. * * A job's state lives entirely on the remote disk, so the MCP server * remembers nothing between calls and survives its own restart. * * The command forms follow real platform differences rather than general * rules: BusyBox's `kill` rejects `--`, `$!` from a background launch names * the wrapper rather than the job itself, and `ps -o` isn't available * everywhere. Building and parsing live side by side because they change together. */ /** How long a finished job's directory lives, in seconds */ export declare const JOB_TTL_SEC: number; /** * The three outcomes are never conflated: `lost` means "nothing to check", * not a failure. A job killed by a signal leaves no exit code, so reporting * it as succeeded or failed would be dishonest. */ export type JobState = 'running' | 'finished' | 'lost' | 'missing'; export interface JobStatus { state: JobState; pid?: number; exitCode?: number; /** Start time, seconds since the epoch */ startedAt?: number; /** Size of the accumulated output in bytes — also the cursor for reading */ outputSize?: number; /** The last lines the job wrote; empty while it has written nothing */ outputTail?: string[]; /** The command as it was given */ command?: string; } export interface JobSummary { id: string; state: JobState; exitCode?: number; startedAt?: number; outputSize?: number; } export interface JobListing { jobs: JobSummary[]; /** Jobs whose directories were removed once their TTL expired */ removed: string[]; } export interface JobOutput { /** Total output size on the server: the next read starts from here */ size: number; text: string; /** The job's directory doesn't exist: this is not "output is empty" but "nothing to ask" */ missing: boolean; } /** * Job id: start time plus a random tail. * * Time up front keeps the list readable in order, and the random tail rules * out a collision between two jobs launched in the same millisecond. * * A job started under sudo carries that in its own id. The caller hands the * id back on every question about the job, so nothing else has to be * remembered — not by the caller, and not by this server between calls. */ export declare function createJobId(elevated?: boolean): string; /** Whether this job's process belongs to root, and its protocol needs sudo */ export declare function isElevatedJobId(id: string): boolean; /** * The id ends up in a path on the server, so a foreign one is checked before it's sent. * * Only letters, digits and dashes are allowed: that's enough for our own ids, * and `..`, a path separator, whitespace and a substitution are all rejected * by one rule. */ export declare function assertJobId(id: unknown): string; /** The jobs root on the server. An empty home is a refusal: the write path cannot be guessed. */ export declare function jobsRoot(home: string): string; /** Paths for one job: its directory sits inside the shared root */ export declare function jobPaths(home: string, id: string): { root: string; dir: string; }; /** * The background part is wrapped in braces, otherwise `&` would apply to the * whole preparation pipeline: dash leaves a subshell on it, which holds the * ssh channel open until the job finishes, so the launch stops being * instant. BusyBox does not behave that way — the defect only shows up on * some servers. * * The directory and the command travel as the `$0` and `$1` parameters * instead of being substituted into the text: that way quoting is applied * once, and a command containing a space, an apostrophe or `$(…)` arrives whole. * * The job writes its own pid: `$!` from a background launch names the * wrapper, not the process that actually runs the command. The job also * becomes the leader of its own process group and session — that's what * makes it possible to kill it as a whole. */ export declare function buildStartCommand(dir: string, command: string, useSetsid: boolean): string; /** Status command: fields on one line, the job's command trailing after the marker */ export declare function buildStatusCommand(dir: string): string; /** * Command to read output from a position. * * `tail -c +N` counts from one, so the position is shifted by one: zero reads * the whole file. The size is printed first — it's also the cursor for the next read. */ export declare function buildOutputCommand(dir: string, offset: number): string; /** * A minus sign before the number targets the whole process group — it kills * the job together with its children. `--` is never used: BusyBox responds * to it with `invalid number` and does nothing. Killing the single process * remains a fallback for a job launched without its own session. */ export declare function buildKillCommand(dir: string, signal?: 'TERM' | 'KILL'): string; /** * Listing command with cleanup along the way. * * Age is computed from the start time we recorded, not `find -mtime`: `find` * dialects differ, but a number of seconds is the same everywhere. Only * directories inside our own root are removed, and only for jobs that are no * longer running. */ export declare function buildListCommand(root: string, ttlSec?: number): string; /** Parse the answer of the status command */ export declare function parseJobStatus(stdout: string): JobStatus; /** Parse the answer of the list command */ export declare function parseJobList(stdout: string): JobListing; /** * Parse the answer of the output-read command. * * The size arrives as the first line and is cut out of the text: it is * bookkeeping, and passing it off as part of the job's output would misreport it. */ export declare function parseJobOutput(stdout: string): JobOutput; /** Parse the answer of the kill command: whether the job was killed and why not */ export declare function parseJobKill(stdout: string): { killed: boolean; reason?: string; }; /** The pid announced by the launched job */ export declare function parseJobStart(stdout: string): number | undefined;