mcp-ssh-terminal 0.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,119 @@
1
+ import { Session } from "./session.js";
2
+ import { expandHome } from "./sshConfig.js";
3
+ /** Reject values that would be parsed as ssh options (argument injection). */
4
+ function assertNotOptionLike(value, name) {
5
+ if (value !== undefined && value.startsWith("-")) {
6
+ throw new Error(`\`${name}\` must not start with "-" (would be parsed as an ssh option): ${value}`);
7
+ }
8
+ }
9
+ /**
10
+ * Build the argv for the OpenSSH client from a connect spec. Everything the
11
+ * ssh client natively understands — ~/.ssh/config, known_hosts, ssh-agent,
12
+ * ProxyJump — is left to ssh; we only translate the explicit overrides.
13
+ */
14
+ export function buildSshCommand(spec) {
15
+ assertNotOptionLike(spec.host, "host");
16
+ assertNotOptionLike(spec.user, "user");
17
+ assertNotOptionLike(spec.jump, "jump");
18
+ if (spec.user && spec.host.includes("@")) {
19
+ throw new Error(`\`user\` was given but \`host\` already contains "@": ${spec.host}`);
20
+ }
21
+ const args = [];
22
+ // Disable the ssh escape character. We drive stdin programmatically, so the
23
+ // interactive "~." disconnect sequence would otherwise be a footgun and
24
+ // would stop arbitrary control bytes from passing through verbatim.
25
+ args.push("-e", "none");
26
+ if (spec.forceTty !== false) {
27
+ args.push("-tt"); // force remote PTY even though stdin is already a tty
28
+ }
29
+ if (spec.port !== undefined)
30
+ args.push("-p", String(spec.port));
31
+ if (spec.identityFile)
32
+ args.push("-i", expandHome(spec.identityFile));
33
+ if (spec.jump)
34
+ args.push("-J", spec.jump);
35
+ if (spec.extraArgs)
36
+ args.push(...spec.extraArgs);
37
+ // Sessions are long-lived by design; keepalives stop NAT/conntrack from
38
+ // silently dropping idle connections, and ConnectTimeout bounds a dead
39
+ // host. OpenSSH uses the first occurrence of an -o option, so these come
40
+ // after extraArgs and act as overridable defaults.
41
+ // ConnectTimeout also caps the destination's banner exchange, and on a
42
+ // ProxyJump chain that clock keeps running while interactive prompts at
43
+ // the jump hop await answers — 30s gives an agent room to answer them.
44
+ args.push("-o", "ServerAliveInterval=30", "-o", "ServerAliveCountMax=3", "-o", "ConnectTimeout=30");
45
+ const destination = spec.user ? `${spec.user}@${spec.host}` : spec.host;
46
+ // "--" ends option parsing so a hostile destination can't inject options
47
+ // (e.g. host: "-oProxyCommand=...").
48
+ args.push("--", destination);
49
+ if (spec.remoteCommand)
50
+ args.push(spec.remoteCommand);
51
+ const label = spec.remoteCommand ? `${destination} :: ${spec.remoteCommand}` : destination;
52
+ return { file: "ssh", args, label };
53
+ }
54
+ /** Cap on concurrent sessions; oldest exited ones are evicted to make room. */
55
+ const MAX_SESSIONS = 32;
56
+ /** In-memory registry of live sessions. */
57
+ export class SessionManager {
58
+ sessions = new Map();
59
+ counter = 0;
60
+ create(cmd, cols, rows) {
61
+ if (this.sessions.size >= MAX_SESSIONS) {
62
+ const oldestExited = [...this.sessions.values()]
63
+ .filter((s) => s.exited)
64
+ .sort((a, b) => a.startedAt - b.startedAt)[0];
65
+ if (!oldestExited) {
66
+ throw new Error(`Session limit (${MAX_SESSIONS}) reached and all are live; ssh_disconnect one first.`);
67
+ }
68
+ this.remove(oldestExited.id);
69
+ }
70
+ const id = `s${++this.counter}`;
71
+ const session = new Session(id, {
72
+ file: cmd.file,
73
+ args: cmd.args,
74
+ cols,
75
+ rows,
76
+ label: cmd.label,
77
+ });
78
+ this.sessions.set(id, session);
79
+ return session;
80
+ }
81
+ get(id) {
82
+ return this.sessions.get(id);
83
+ }
84
+ require(id) {
85
+ const s = this.sessions.get(id);
86
+ if (!s) {
87
+ const known = [...this.sessions.keys()].join(", ") || "none";
88
+ throw new Error(`Unknown session "${id}". Active sessions: ${known}.`);
89
+ }
90
+ return s;
91
+ }
92
+ list() {
93
+ return [...this.sessions.values()];
94
+ }
95
+ remove(id) {
96
+ const s = this.sessions.get(id);
97
+ if (!s)
98
+ return false;
99
+ s.close();
100
+ // Escalate like closeAll: a child that ignores SIGHUP (e.g. a wedged
101
+ // ProxyCommand) would otherwise leak past the registry.
102
+ setTimeout(() => s.kill(), 250).unref?.();
103
+ this.sessions.delete(id);
104
+ return true;
105
+ }
106
+ /**
107
+ * SIGHUP everything, escalating to SIGKILL after `escalateMs` for children
108
+ * that ignore it. Callers that exit afterwards must wait past `escalateMs`.
109
+ */
110
+ closeAll(escalateMs = 250) {
111
+ for (const s of this.sessions.values())
112
+ s.close();
113
+ setTimeout(() => {
114
+ for (const s of this.sessions.values())
115
+ s.kill();
116
+ }, escalateMs).unref?.();
117
+ }
118
+ }
119
+ //# sourceMappingURL=manager.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"manager.js","sourceRoot":"","sources":["../src/manager.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,3 @@
1
+ import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
2
+ import { SessionManager } from "./manager.js";
3
+ export declare function createServer(manager: SessionManager): McpServer;
package/dist/server.js ADDED
@@ -0,0 +1,348 @@
1
+ import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
2
+ import { z } from "zod";
3
+ import { buildSshCommand } from "./manager.js";
4
+ import { resolveKeys } from "./keys.js";
5
+ import { listConfigHosts } from "./sshConfig.js";
6
+ const DEFAULT_COLS = 120;
7
+ const DEFAULT_ROWS = 40;
8
+ // Settle/timeout defaults (ms). Connecting allows more time for banners,
9
+ // host-key prompts, and multi-hop ProxyJump handshakes.
10
+ const CONNECT_SETTLE = 700;
11
+ const CONNECT_TIMEOUT = 20000;
12
+ const SEND_SETTLE = 500;
13
+ const SEND_TIMEOUT = 12000;
14
+ const DEFAULT_MAX_LINES = 200;
15
+ function text(s) {
16
+ return { content: [{ type: "text", text: s }] };
17
+ }
18
+ function errorText(s) {
19
+ return { content: [{ type: "text", text: s }], isError: true };
20
+ }
21
+ /** Compose the standard status footer shown after a screen render. */
22
+ function statusLine(session, wait, render) {
23
+ const parts = [];
24
+ parts.push(`session=${session.id}`);
25
+ if (session.exited) {
26
+ const code = session.exitCode;
27
+ const sig = session.exitSignal;
28
+ parts.push(`state=exited${code !== null ? ` code=${code}` : ""}${sig ? ` signal=${sig}` : ""}`);
29
+ }
30
+ else {
31
+ parts.push("state=live");
32
+ }
33
+ parts.push(`wait=${wait}`);
34
+ parts.push(`cursor=${render.cursorRow + 1},${render.cursorCol + 1}`);
35
+ parts.push(`screen=${render.rows}x${render.cols}`);
36
+ parts.push(`shown=${render.shownLines}/${render.totalLines}`);
37
+ return `[${parts.join(" | ")}]`;
38
+ }
39
+ function screenBlock(render, session, wait) {
40
+ const body = render.text.length > 0 ? render.text : "(no output yet)";
41
+ const hint = wait === "timeout" && !session.exited
42
+ ? "\n(output still arriving — call ssh_read again to see more, or wait longer)"
43
+ : wait === "quiet"
44
+ ? "\n(no new output arrived within waitMs — the program may be running quietly or awaiting input)"
45
+ : "";
46
+ return `${body}\n${statusLine(session, wait, render)}${hint}`;
47
+ }
48
+ // Zod bounds: generous enough for any real terminal, tight enough that a
49
+ // bogus value can't allocate a gigantic emulator buffer or hang a call.
50
+ const colsSchema = z.number().int().min(10).max(1000);
51
+ const rowsSchema = z.number().int().min(4).max(500);
52
+ const settleSchema = z.number().int().nonnegative().max(60_000);
53
+ const timeoutSchema = z.number().int().positive().max(600_000);
54
+ const maxLinesSchema = z.number().int().positive().max(5000);
55
+ export function createServer(manager) {
56
+ const server = new McpServer({
57
+ name: "mcp-ssh-terminal",
58
+ version: "0.2.0",
59
+ }, {
60
+ instructions: [
61
+ "Persistent, interactive SSH sessions over the real OpenSSH client.",
62
+ "",
63
+ "How it works: each session is a live `ssh` process attached to a PTY, with",
64
+ "output rendered through a terminal emulator so you read the screen a human",
65
+ "would see. Sessions stay open across tool calls — connect once, then send",
66
+ "input and read output repeatedly.",
67
+ "",
68
+ "Config, keys, known_hosts, and ProxyJump are all handled by ssh itself, so",
69
+ "~/.ssh/config Host aliases, IdentityFile, and multi-hop jumps 'just work'.",
70
+ "",
71
+ "Typical flow:",
72
+ " 1. ssh_connect { host } -> returns a session id and the initial screen.",
73
+ " 2. If a password / passphrase / host-key prompt appears on the screen,",
74
+ " answer it with ssh_send (e.g. type the password, or send 'yes').",
75
+ " 3. ssh_send { session, text: 'command', appendEnter: true } to run a command.",
76
+ " 4. ssh_read to poll long-running output; ssh_interrupt for Ctrl-C.",
77
+ "",
78
+ "Control characters & special keys: use the `keys` array on ssh_send, e.g.",
79
+ "['C-c'] for Ctrl-C, ['C-x'] for Ctrl-X, ['Up'], ['Tab'], ['Escape'], ['F5'].",
80
+ "",
81
+ "Works with non-Unix shells (e.g. Mikrotik RouterOS): it is a faithful",
82
+ "terminal, so interactive CLIs, tab-completion, and paging behave normally.",
83
+ "",
84
+ "Mikrotik RouterOS tips:",
85
+ " - '?' and Tab are live hotkeys in the RouterOS console (inline help /",
86
+ " completion) — they act immediately, so avoid them inside `text` unless",
87
+ " intended. Prefer single-line commands; multi-line pastes get mangled",
88
+ " by the console's auto-indent.",
89
+ " - Long `print` output pauses on a pager line ('-- [Q quit|D dump|down]'):",
90
+ " answer via ssh_send ('D' dumps all, 'Q' quits, Space pages), or run",
91
+ " `print without-paging` in the first place.",
92
+ " - Ctrl-X (keys: ['C-x']) toggles RouterOS Safe Mode: enter it before",
93
+ " config changes so they auto-revert if the session drops; press it",
94
+ " again to commit. Don't send it casually.",
95
+ ].join("\n"),
96
+ });
97
+ server.registerTool("ssh_connect", {
98
+ title: "Open SSH session",
99
+ description: "Open a persistent interactive SSH session to a host and return the initial screen. " +
100
+ "`host` may be an ~/.ssh/config Host alias, a hostname, or user@hostname. Config, " +
101
+ "key auth, known_hosts, and ProxyJump are delegated to the ssh client. If a password, " +
102
+ "passphrase, 2FA, or host-key prompt appears in the returned screen, respond with ssh_send.",
103
+ inputSchema: {
104
+ host: z.string().min(1).describe("Destination: ssh_config Host alias, hostname, or user@hostname"),
105
+ user: z.string().optional().describe("Username (prepended as user@host if host has no '@')"),
106
+ port: z.number().int().min(1).max(65535).optional().describe("Port (ssh -p); usually unnecessary if set in config"),
107
+ identityFile: z.string().optional().describe("Private key path (ssh -i); ~ is expanded"),
108
+ jump: z
109
+ .string()
110
+ .optional()
111
+ .describe("ProxyJump chain (ssh -J), e.g. 'bastion' or 'userA@a,userB@b' for A->B->C"),
112
+ remoteCommand: z
113
+ .string()
114
+ .optional()
115
+ .describe("Run this command instead of an interactive login shell (still on a PTY)"),
116
+ extraArgs: z.array(z.string()).optional().describe("Extra raw ssh arguments appended verbatim"),
117
+ forceTty: z.boolean().optional().describe("Force remote PTY allocation (ssh -tt). Default true"),
118
+ cols: colsSchema.optional().describe(`Terminal width (default ${DEFAULT_COLS})`),
119
+ rows: rowsSchema.optional().describe(`Terminal height (default ${DEFAULT_ROWS})`),
120
+ settleMs: settleSchema.optional().describe(`Idle-quiet threshold before reading (default ${CONNECT_SETTLE})`),
121
+ timeoutMs: timeoutSchema.optional().describe(`Max wait for initial screen (default ${CONNECT_TIMEOUT})`),
122
+ maxLines: maxLinesSchema.optional().describe(`Max screen lines to return (default ${DEFAULT_MAX_LINES})`),
123
+ },
124
+ }, async (a) => {
125
+ try {
126
+ const cols = a.cols ?? DEFAULT_COLS;
127
+ const rows = a.rows ?? DEFAULT_ROWS;
128
+ const cmd = buildSshCommand({
129
+ host: a.host,
130
+ user: a.user,
131
+ port: a.port,
132
+ identityFile: a.identityFile,
133
+ jump: a.jump,
134
+ remoteCommand: a.remoteCommand,
135
+ extraArgs: a.extraArgs,
136
+ forceTty: a.forceTty,
137
+ });
138
+ const session = manager.create(cmd, cols, rows);
139
+ // requireData: the settle clock starts at the first byte of output, so
140
+ // a slow DNS/TCP/kex phase isn't misread as an already-settled screen.
141
+ const wait = await session.waitForIdle(a.settleMs ?? CONNECT_SETTLE, a.timeoutMs ?? CONNECT_TIMEOUT, true);
142
+ const render = await session.render(a.maxLines ?? DEFAULT_MAX_LINES);
143
+ const header = `Connecting: ssh ${cmd.args.join(" ")}\n`;
144
+ const noOutputHint = session.exited && render.text.length === 0
145
+ ? "\n(ssh exited immediately with no output — is the OpenSSH client installed and on PATH?)"
146
+ : "";
147
+ return text(header + screenBlock(render, session, wait.reason) + noOutputHint);
148
+ }
149
+ catch (err) {
150
+ return errorText(`ssh_connect failed: ${err.message}`);
151
+ }
152
+ });
153
+ server.registerTool("ssh_send", {
154
+ title: "Send input to SSH session",
155
+ description: "Send text and/or special keys to a session, wait for the output to settle, and return the " +
156
+ "updated screen. Use `text` for literal typing, `keys` for control chars and special keys " +
157
+ "(e.g. ['C-c'], ['Up'], ['Tab'], ['Escape']), and `appendEnter` to press Enter after the text. " +
158
+ "Order sent: text, then keys, then (if set) Enter.",
159
+ inputSchema: {
160
+ session: z.string().min(1).describe("Session id from ssh_connect"),
161
+ text: z.string().optional().describe("Literal text to type (control chars are not interpreted here)"),
162
+ keys: z
163
+ .array(z.string())
164
+ .optional()
165
+ .describe("Ordered key tokens: 'Enter','Tab','Escape','Up','Down','F5','C-c'(Ctrl-C),'C-x','M-b'(Alt-b),'hex:1b', " +
166
+ "or a single literal character. Unknown multi-character tokens are rejected — literal text goes in `text`."),
167
+ appendEnter: z.boolean().optional().describe("Press Enter after text/keys (convenience for running a command)"),
168
+ settleMs: settleSchema.optional().describe(`Idle-quiet threshold (default ${SEND_SETTLE})`),
169
+ timeoutMs: timeoutSchema.optional().describe(`Max wait for output (default ${SEND_TIMEOUT})`),
170
+ maxLines: maxLinesSchema.optional().describe(`Max screen lines to return (default ${DEFAULT_MAX_LINES})`),
171
+ },
172
+ }, async (a) => {
173
+ try {
174
+ const session = manager.require(a.session);
175
+ if (session.exited)
176
+ return errorText(`Session ${session.id} has exited and cannot receive input.`);
177
+ let payload = "";
178
+ if (a.text !== undefined)
179
+ payload += a.text;
180
+ if (a.keys && a.keys.length > 0) {
181
+ payload += resolveKeys(a.keys, { applicationCursorKeys: session.applicationCursorKeys });
182
+ }
183
+ if (a.appendEnter)
184
+ payload += "\r";
185
+ if (payload.length === 0) {
186
+ return errorText("Nothing to send: provide `text`, `keys`, and/or `appendEnter`.");
187
+ }
188
+ session.write(payload);
189
+ const wait = await session.waitForIdle(a.settleMs ?? SEND_SETTLE, a.timeoutMs ?? SEND_TIMEOUT);
190
+ const render = await session.render(a.maxLines ?? DEFAULT_MAX_LINES);
191
+ return text(screenBlock(render, session, wait.reason));
192
+ }
193
+ catch (err) {
194
+ return errorText(`ssh_send failed: ${err.message}`);
195
+ }
196
+ });
197
+ server.registerTool("ssh_read", {
198
+ title: "Read SSH screen",
199
+ description: "Read the current session screen without sending input. Use this to poll output from a " +
200
+ "long-running command. Set `waitMs` to block until new output actually arrives (or the " +
201
+ "session exits, or `waitMs` elapses) before reading.",
202
+ inputSchema: {
203
+ session: z.string().min(1).describe("Session id"),
204
+ mode: z.enum(["screen", "raw"]).optional().describe("'screen' = rendered text (default); 'raw' = raw byte stream with escape codes"),
205
+ waitMs: z
206
+ .number()
207
+ .int()
208
+ .nonnegative()
209
+ .max(600_000)
210
+ .optional()
211
+ .describe("Block up to this long for NEW output before reading; footer shows wait=quiet if nothing arrived (default 0 = read immediately)"),
212
+ maxLines: maxLinesSchema.optional().describe(`Max screen lines (screen mode; default ${DEFAULT_MAX_LINES})`),
213
+ maxBytes: z.number().int().positive().max(524_288).optional().describe("Max bytes (raw mode; default 8192)"),
214
+ },
215
+ }, async (a) => {
216
+ try {
217
+ const session = manager.require(a.session);
218
+ let wait = "idle";
219
+ if (a.waitMs && a.waitMs > 0) {
220
+ // Block for genuinely new output, then let it settle briefly so the
221
+ // render isn't a half-written line. Reasons surfaced to the caller:
222
+ // idle — new output arrived and settled
223
+ // timeout — new output arrived and is still streaming
224
+ // quiet — nothing new arrived within waitMs
225
+ // exited — the session ended while waiting
226
+ const first = await session.waitForData(a.waitMs);
227
+ if (first.reason === "data") {
228
+ wait = (await session.waitForIdle(Math.min(300, a.waitMs), 1000)).reason;
229
+ }
230
+ else if (first.reason === "timeout") {
231
+ wait = "quiet";
232
+ }
233
+ else {
234
+ wait = first.reason;
235
+ }
236
+ }
237
+ if (a.mode === "raw") {
238
+ const raw = session.rawTail(a.maxBytes ?? 8192);
239
+ return text(`${JSON.stringify(raw)}\n[session=${session.id} | state=${session.exited ? "exited" : "live"} | rawBytes=${raw.length}]`);
240
+ }
241
+ const render = await session.render(a.maxLines ?? DEFAULT_MAX_LINES);
242
+ return text(screenBlock(render, session, wait));
243
+ }
244
+ catch (err) {
245
+ return errorText(`ssh_read failed: ${err.message}`);
246
+ }
247
+ });
248
+ server.registerTool("ssh_interrupt", {
249
+ title: "Send Ctrl-C",
250
+ description: "Send Ctrl-C (SIGINT) to the foreground program in a session, then return the updated screen.",
251
+ inputSchema: {
252
+ session: z.string().min(1).describe("Session id"),
253
+ settleMs: settleSchema.optional(),
254
+ timeoutMs: timeoutSchema.optional(),
255
+ maxLines: maxLinesSchema.optional(),
256
+ },
257
+ }, async (a) => {
258
+ try {
259
+ const session = manager.require(a.session);
260
+ if (session.exited)
261
+ return errorText(`Session ${session.id} has already exited.`);
262
+ session.write("\x03");
263
+ const wait = await session.waitForIdle(a.settleMs ?? SEND_SETTLE, a.timeoutMs ?? SEND_TIMEOUT);
264
+ const render = await session.render(a.maxLines ?? DEFAULT_MAX_LINES);
265
+ return text(screenBlock(render, session, wait.reason));
266
+ }
267
+ catch (err) {
268
+ return errorText(`ssh_interrupt failed: ${err.message}`);
269
+ }
270
+ });
271
+ server.registerTool("ssh_resize", {
272
+ title: "Resize terminal",
273
+ description: "Change the terminal dimensions of a session (affects line wrapping and full-screen apps).",
274
+ inputSchema: {
275
+ session: z.string().min(1).describe("Session id"),
276
+ cols: colsSchema.describe("New column count"),
277
+ rows: rowsSchema.describe("New row count"),
278
+ },
279
+ }, async (a) => {
280
+ try {
281
+ const session = manager.require(a.session);
282
+ session.resize(a.cols, a.rows);
283
+ return text(`Resized ${session.id} to ${a.cols}x${a.rows}.`);
284
+ }
285
+ catch (err) {
286
+ return errorText(`ssh_resize failed: ${err.message}`);
287
+ }
288
+ });
289
+ server.registerTool("ssh_list", {
290
+ title: "List SSH sessions",
291
+ description: "List all active SSH sessions with their destination, state, and age.",
292
+ inputSchema: {},
293
+ }, async () => {
294
+ const sessions = manager.list();
295
+ if (sessions.length === 0)
296
+ return text("No active sessions.");
297
+ const lines = sessions.map((s) => {
298
+ const ageSec = Math.round((Date.now() - s.startedAt) / 1000);
299
+ const state = s.exited ? `exited(code=${s.exitCode ?? "?"})` : "live";
300
+ const { cols, rows } = s.dimensions;
301
+ return `${s.id}\t${state}\t${cols}x${rows}\tpid=${s.pid}\tage=${ageSec}s\t${s.label}`;
302
+ });
303
+ return text(`Active sessions (${sessions.length}):\n${lines.join("\n")}`);
304
+ });
305
+ server.registerTool("ssh_disconnect", {
306
+ title: "Close SSH session",
307
+ description: "Close a session and terminate its ssh process.",
308
+ inputSchema: {
309
+ session: z.string().min(1).describe("Session id to close"),
310
+ },
311
+ }, async (a) => {
312
+ const ok = manager.remove(a.session);
313
+ return ok ? text(`Closed session ${a.session}.`) : errorText(`Unknown session "${a.session}".`);
314
+ });
315
+ server.registerTool("ssh_config_hosts", {
316
+ title: "List ssh_config hosts",
317
+ description: "List Host aliases from ~/.ssh/config (best-effort, follows Include) for discovery. " +
318
+ "Connection semantics are always resolved by the ssh client, not this listing.",
319
+ inputSchema: {
320
+ configPath: z.string().optional().describe("Alternate config path (default ~/.ssh/config)"),
321
+ },
322
+ }, async (a) => {
323
+ try {
324
+ const hosts = listConfigHosts(a.configPath);
325
+ if (hosts.length === 0)
326
+ return text("No Host entries found in ssh config.");
327
+ const lines = hosts.map((h) => {
328
+ const bits = [];
329
+ if (h.hostName)
330
+ bits.push(`hostname=${h.hostName}`);
331
+ if (h.user)
332
+ bits.push(`user=${h.user}`);
333
+ if (h.port)
334
+ bits.push(`port=${h.port}`);
335
+ if (h.proxyJump)
336
+ bits.push(`jump=${h.proxyJump}`);
337
+ const detail = bits.length ? ` (${bits.join(", ")})` : "";
338
+ return `${h.patterns.join(" ")}${detail}`;
339
+ });
340
+ return text(`Host aliases (${hosts.length}):\n${lines.join("\n")}`);
341
+ }
342
+ catch (err) {
343
+ return errorText(`ssh_config_hosts failed: ${err.message}`);
344
+ }
345
+ });
346
+ return server;
347
+ }
348
+ //# sourceMappingURL=server.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"server.js","sourceRoot":"","sources":["../src/server.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,89 @@
1
+ export interface SessionOptions {
2
+ /** The command to run (usually "ssh"). */
3
+ file: string;
4
+ /** Arguments for the command. */
5
+ args: string[];
6
+ cols: number;
7
+ rows: number;
8
+ /** Human-readable label describing the destination, for listings. */
9
+ label: string;
10
+ /** Lines of scrollback the emulator retains. */
11
+ scrollback?: number;
12
+ }
13
+ export interface RenderResult {
14
+ text: string;
15
+ cursorRow: number;
16
+ cursorCol: number;
17
+ rows: number;
18
+ cols: number;
19
+ totalLines: number;
20
+ shownLines: number;
21
+ }
22
+ export type WaitReason = "idle" | "timeout" | "exited" | "data" | "quiet";
23
+ export interface WaitResult {
24
+ reason: WaitReason;
25
+ }
26
+ /**
27
+ * One persistent child process (typically the ssh client) attached to a PTY,
28
+ * with its output fed into a headless xterm emulator so callers can read a
29
+ * rendered screen instead of a raw escape-code stream.
30
+ */
31
+ export declare class Session {
32
+ readonly id: string;
33
+ readonly label: string;
34
+ readonly startedAt: number;
35
+ private pty;
36
+ private term;
37
+ private raw;
38
+ private lastDataAt;
39
+ /** Bumped only by child output, never by our own writes. */
40
+ private dataEvents;
41
+ private _exited;
42
+ private _exitCode;
43
+ private _exitSignal;
44
+ constructor(id: string, opts: SessionOptions);
45
+ get exited(): boolean;
46
+ get exitCode(): number | null;
47
+ get exitSignal(): number | null;
48
+ get pid(): number;
49
+ get dimensions(): {
50
+ cols: number;
51
+ rows: number;
52
+ };
53
+ /** True while the remote app has DECCKM (application cursor keys) enabled. */
54
+ get applicationCursorKeys(): boolean;
55
+ /** Write raw bytes to the PTY. Marks activity so idle-waits behave. */
56
+ write(data: string): void;
57
+ resize(cols: number, rows: number): void;
58
+ /**
59
+ * Wait until the output has been quiet for `settleMs`, or `timeoutMs`
60
+ * elapses, or the process exits — whichever comes first. This is how we
61
+ * decide a command's output has "settled" enough to read.
62
+ *
63
+ * With `requireData`, the settle clock only counts once the child has
64
+ * produced at least one byte of output — so a slow connection (DNS, TCP,
65
+ * key exchange) isn't mistaken for an already-settled screen.
66
+ */
67
+ waitForIdle(settleMs: number, timeoutMs: number, requireData?: boolean): Promise<WaitResult>;
68
+ /**
69
+ * Wait for *new* child output (any byte after this call), the process
70
+ * exiting, or `timeoutMs` — whichever comes first. Unlike waitForIdle, an
71
+ * already-quiet session blocks here until something actually arrives.
72
+ */
73
+ waitForData(timeoutMs: number): Promise<WaitResult>;
74
+ /** Flush the xterm write queue so a subsequent render sees all output. */
75
+ private flush;
76
+ /**
77
+ * Render the emulated screen as text. Returns the tail of the buffer down
78
+ * to the bottom of the viewport (not just the cursor line — full-screen
79
+ * apps often leave the cursor mid-screen with content below it), capped at
80
+ * `maxLines`. Trailing blank lines are trimmed. This collapses
81
+ * redraws/colors/cursor moves into the plain text a human would see.
82
+ */
83
+ render(maxLines: number): Promise<RenderResult>;
84
+ /** Return the tail of the raw PTY byte stream (escape codes included). */
85
+ rawTail(maxBytes: number): string;
86
+ /** Terminate the child. SIGHUP first; caller may escalate. */
87
+ close(signal?: string): void;
88
+ kill(): void;
89
+ }