smooth-ssh-mcp 0.1.1

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.
Files changed (56) hide show
  1. package/LICENSE +21 -0
  2. package/README.en.md +319 -0
  3. package/README.md +32 -0
  4. package/README.zh-CN.md +319 -0
  5. package/bin/smooth-ssh-mcp-codex +43 -0
  6. package/dist/audit.d.ts +23 -0
  7. package/dist/audit.js +140 -0
  8. package/dist/audit.js.map +1 -0
  9. package/dist/auth.d.ts +8 -0
  10. package/dist/auth.js +19 -0
  11. package/dist/auth.js.map +1 -0
  12. package/dist/doctor.d.ts +27 -0
  13. package/dist/doctor.js +169 -0
  14. package/dist/doctor.js.map +1 -0
  15. package/dist/forwardManager.d.ts +49 -0
  16. package/dist/forwardManager.js +141 -0
  17. package/dist/forwardManager.js.map +1 -0
  18. package/dist/init.d.ts +21 -0
  19. package/dist/init.js +80 -0
  20. package/dist/init.js.map +1 -0
  21. package/dist/inventory.d.ts +4 -0
  22. package/dist/inventory.js +262 -0
  23. package/dist/inventory.js.map +1 -0
  24. package/dist/mcpServer.d.ts +8 -0
  25. package/dist/mcpServer.js +403 -0
  26. package/dist/mcpServer.js.map +1 -0
  27. package/dist/operations.d.ts +167 -0
  28. package/dist/operations.js +1240 -0
  29. package/dist/operations.js.map +1 -0
  30. package/dist/policy.d.ts +21 -0
  31. package/dist/policy.js +470 -0
  32. package/dist/policy.js.map +1 -0
  33. package/dist/redaction.d.ts +2 -0
  34. package/dist/redaction.js +64 -0
  35. package/dist/redaction.js.map +1 -0
  36. package/dist/runner.d.ts +24 -0
  37. package/dist/runner.js +90 -0
  38. package/dist/runner.js.map +1 -0
  39. package/dist/server.d.ts +9 -0
  40. package/dist/server.js +130 -0
  41. package/dist/server.js.map +1 -0
  42. package/dist/sessionManager.d.ts +77 -0
  43. package/dist/sessionManager.js +195 -0
  44. package/dist/sessionManager.js.map +1 -0
  45. package/dist/sshArgs.d.ts +24 -0
  46. package/dist/sshArgs.js +135 -0
  47. package/dist/sshArgs.js.map +1 -0
  48. package/dist/stateStore.d.ts +27 -0
  49. package/dist/stateStore.js +99 -0
  50. package/dist/stateStore.js.map +1 -0
  51. package/dist/types.d.ts +95 -0
  52. package/dist/types.js +2 -0
  53. package/dist/types.js.map +1 -0
  54. package/docs/mcp-client.example.json +15 -0
  55. package/examples/hosts.example.yaml +79 -0
  56. package/package.json +58 -0
@@ -0,0 +1,64 @@
1
+ const PATTERNS = [
2
+ {
3
+ name: "authorization-bearer",
4
+ regex: /\bAuthorization:\s*Bearer\s+[A-Za-z0-9._~+/=-]+/gi,
5
+ replace: "Authorization: Bearer [REDACTED]"
6
+ },
7
+ {
8
+ name: "password-assignment",
9
+ regex: /\b(password|passwd|secret|token|api[_-]?key)\b\s*[:=]\s*("[^"]*"|'[^']*'|\S+)/gi,
10
+ replace: (_match, key) => `${key}=[REDACTED]`
11
+ },
12
+ {
13
+ name: "private-key-block",
14
+ regex: /-----BEGIN [A-Z ]*PRIVATE KEY-----[\s\S]*?-----END [A-Z ]*PRIVATE KEY-----/g,
15
+ replace: "[REDACTED PRIVATE KEY]"
16
+ },
17
+ {
18
+ name: "private-key-incomplete",
19
+ regex: /-----BEGIN [A-Z ]*PRIVATE KEY-----[\s\S]*$/g,
20
+ replace: "[REDACTED PRIVATE KEY]"
21
+ }
22
+ ];
23
+ export function redactAndTruncate(text, maxBytes) {
24
+ const redactions = [];
25
+ let output = text;
26
+ for (const pattern of PATTERNS) {
27
+ let count = 0;
28
+ output = output.replace(pattern.regex, (...args) => {
29
+ count += 1;
30
+ if (typeof pattern.replace === "function") {
31
+ return pattern.replace(args[0], ...args.slice(1));
32
+ }
33
+ return pattern.replace;
34
+ });
35
+ if (count > 0) {
36
+ redactions.push({ pattern: pattern.name, count });
37
+ }
38
+ }
39
+ const originalBytes = Buffer.byteLength(output, "utf8");
40
+ if (originalBytes <= maxBytes) {
41
+ return { text: output, redactions, truncated: false, originalBytes };
42
+ }
43
+ return {
44
+ text: truncateUtf8(output, maxBytes),
45
+ redactions,
46
+ truncated: true,
47
+ originalBytes
48
+ };
49
+ }
50
+ function truncateUtf8(text, maxBytes) {
51
+ if (maxBytes <= 0)
52
+ return "";
53
+ let used = 0;
54
+ let result = "";
55
+ for (const char of text) {
56
+ const size = Buffer.byteLength(char, "utf8");
57
+ if (used + size > maxBytes)
58
+ break;
59
+ result += char;
60
+ used += size;
61
+ }
62
+ return result;
63
+ }
64
+ //# sourceMappingURL=redaction.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"redaction.js","sourceRoot":"","sources":["../src/redaction.ts"],"names":[],"mappings":"AAQA,MAAM,QAAQ,GAAc;IAC1B;QACE,IAAI,EAAE,sBAAsB;QAC5B,KAAK,EAAE,mDAAmD;QAC1D,OAAO,EAAE,kCAAkC;KAC5C;IACD;QACE,IAAI,EAAE,qBAAqB;QAC3B,KAAK,EAAE,iFAAiF;QACxF,OAAO,EAAE,CAAC,MAAM,EAAE,GAAW,EAAE,EAAE,CAAC,GAAG,GAAG,aAAa;KACtD;IACD;QACE,IAAI,EAAE,mBAAmB;QACzB,KAAK,EAAE,6EAA6E;QACpF,OAAO,EAAE,wBAAwB;KAClC;IACD;QACE,IAAI,EAAE,wBAAwB;QAC9B,KAAK,EAAE,6CAA6C;QACpD,OAAO,EAAE,wBAAwB;KAClC;CACF,CAAC;AAEF,MAAM,UAAU,iBAAiB,CAAC,IAAY,EAAE,QAAgB;IAC9D,MAAM,UAAU,GAAgB,EAAE,CAAC;IACnC,IAAI,MAAM,GAAG,IAAI,CAAC;IAElB,KAAK,MAAM,OAAO,IAAI,QAAQ,EAAE,CAAC;QAC/B,IAAI,KAAK,GAAG,CAAC,CAAC;QACd,MAAM,GAAG,MAAM,CAAC,OAAO,CAAC,OAAO,CAAC,KAAK,EAAE,CAAC,GAAG,IAAI,EAAE,EAAE;YACjD,KAAK,IAAI,CAAC,CAAC;YACX,IAAI,OAAO,OAAO,CAAC,OAAO,KAAK,UAAU,EAAE,CAAC;gBAC1C,OAAO,OAAO,CAAC,OAAO,CAAC,IAAI,CAAC,CAAC,CAAC,EAAE,GAAG,IAAI,CAAC,KAAK,CAAC,CAAC,CAAC,CAAC,CAAC;YACpD,CAAC;YACD,OAAO,OAAO,CAAC,OAAO,CAAC;QACzB,CAAC,CAAC,CAAC;QACH,IAAI,KAAK,GAAG,CAAC,EAAE,CAAC;YACd,UAAU,CAAC,IAAI,CAAC,EAAE,OAAO,EAAE,OAAO,CAAC,IAAI,EAAE,KAAK,EAAE,CAAC,CAAC;QACpD,CAAC;IACH,CAAC;IAED,MAAM,aAAa,GAAG,MAAM,CAAC,UAAU,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC;IACxD,IAAI,aAAa,IAAI,QAAQ,EAAE,CAAC;QAC9B,OAAO,EAAE,IAAI,EAAE,MAAM,EAAE,UAAU,EAAE,SAAS,EAAE,KAAK,EAAE,aAAa,EAAE,CAAC;IACvE,CAAC;IAED,OAAO;QACL,IAAI,EAAE,YAAY,CAAC,MAAM,EAAE,QAAQ,CAAC;QACpC,UAAU;QACV,SAAS,EAAE,IAAI;QACf,aAAa;KACd,CAAC;AACJ,CAAC;AAED,SAAS,YAAY,CAAC,IAAY,EAAE,QAAgB;IAClD,IAAI,QAAQ,IAAI,CAAC;QAAE,OAAO,EAAE,CAAC;IAC7B,IAAI,IAAI,GAAG,CAAC,CAAC;IACb,IAAI,MAAM,GAAG,EAAE,CAAC;IAChB,KAAK,MAAM,IAAI,IAAI,IAAI,EAAE,CAAC;QACxB,MAAM,IAAI,GAAG,MAAM,CAAC,UAAU,CAAC,IAAI,EAAE,MAAM,CAAC,CAAC;QAC7C,IAAI,IAAI,GAAG,IAAI,GAAG,QAAQ;YAAE,MAAM;QAClC,MAAM,IAAI,IAAI,CAAC;QACf,IAAI,IAAI,IAAI,CAAC;IACf,CAAC;IACD,OAAO,MAAM,CAAC;AAChB,CAAC"}
@@ -0,0 +1,24 @@
1
+ export type RunOptions = {
2
+ timeoutMs?: number;
3
+ input?: string;
4
+ cwd?: string;
5
+ env?: NodeJS.ProcessEnv;
6
+ maxBufferBytes?: number;
7
+ };
8
+ export type RunResult = {
9
+ exitCode: number | null;
10
+ signal: NodeJS.Signals | null;
11
+ stdout: string;
12
+ stderr: string;
13
+ startedAt: Date;
14
+ endedAt: Date;
15
+ durationMs: number;
16
+ timedOut: boolean;
17
+ stdoutTruncated?: boolean;
18
+ stderrTruncated?: boolean;
19
+ };
20
+ export type Runner = {
21
+ run(file: string, args: string[], options?: RunOptions): Promise<RunResult>;
22
+ };
23
+ export declare const nodeRunner: Runner;
24
+ export declare function runProcess(file: string, args: string[], options?: RunOptions): Promise<RunResult>;
package/dist/runner.js ADDED
@@ -0,0 +1,90 @@
1
+ import { spawn } from "node:child_process";
2
+ export const nodeRunner = {
3
+ run(file, args, options = {}) {
4
+ return runProcess(file, args, options);
5
+ }
6
+ };
7
+ export function runProcess(file, args, options = {}) {
8
+ const startedAt = new Date();
9
+ return new Promise((resolve, reject) => {
10
+ const child = spawn(file, args, {
11
+ cwd: options.cwd,
12
+ env: options.env,
13
+ shell: false,
14
+ stdio: ["pipe", "pipe", "pipe"]
15
+ });
16
+ let stdout = "";
17
+ let stderr = "";
18
+ let stdoutTruncated = false;
19
+ let stderrTruncated = false;
20
+ let timedOut = false;
21
+ const maxBufferBytes = options.maxBufferBytes ?? 2 * 1024 * 1024;
22
+ const timeout = options.timeoutMs
23
+ ? setTimeout(() => {
24
+ timedOut = true;
25
+ child.kill("SIGTERM");
26
+ setTimeout(() => {
27
+ if (!child.killed)
28
+ child.kill("SIGKILL");
29
+ }, 1000).unref();
30
+ }, options.timeoutMs)
31
+ : undefined;
32
+ timeout?.unref();
33
+ child.stdout.setEncoding("utf8");
34
+ child.stderr.setEncoding("utf8");
35
+ child.stdout.on("data", (chunk) => {
36
+ const next = appendBounded(stdout, chunk, maxBufferBytes);
37
+ stdout = next.text;
38
+ stdoutTruncated ||= next.truncated;
39
+ });
40
+ child.stderr.on("data", (chunk) => {
41
+ const next = appendBounded(stderr, chunk, maxBufferBytes);
42
+ stderr = next.text;
43
+ stderrTruncated ||= next.truncated;
44
+ });
45
+ child.on("error", (error) => {
46
+ if (timeout)
47
+ clearTimeout(timeout);
48
+ reject(error);
49
+ });
50
+ child.on("close", (exitCode, signal) => {
51
+ if (timeout)
52
+ clearTimeout(timeout);
53
+ const endedAt = new Date();
54
+ resolve({
55
+ exitCode,
56
+ signal,
57
+ stdout,
58
+ stderr,
59
+ startedAt,
60
+ endedAt,
61
+ durationMs: endedAt.getTime() - startedAt.getTime(),
62
+ timedOut,
63
+ stdoutTruncated,
64
+ stderrTruncated
65
+ });
66
+ });
67
+ if (options.input)
68
+ child.stdin.write(options.input);
69
+ child.stdin.end();
70
+ });
71
+ }
72
+ function appendBounded(current, chunk, maxBytes) {
73
+ if (maxBytes <= 0)
74
+ return { text: "", truncated: true };
75
+ const combined = current + chunk;
76
+ if (Buffer.byteLength(combined, "utf8") <= maxBytes) {
77
+ return { text: combined, truncated: false };
78
+ }
79
+ let used = 0;
80
+ let text = "";
81
+ for (const char of combined) {
82
+ const size = Buffer.byteLength(char, "utf8");
83
+ if (used + size > maxBytes)
84
+ break;
85
+ text += char;
86
+ used += size;
87
+ }
88
+ return { text, truncated: true };
89
+ }
90
+ //# sourceMappingURL=runner.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"runner.js","sourceRoot":"","sources":["../src/runner.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,9 @@
1
+ #!/usr/bin/env node
2
+ export declare function parseArgs(argv: string[]): {
3
+ mode: "serve" | "doctor" | "init" | "help" | "version";
4
+ configPath: string;
5
+ secretsPath?: string;
6
+ json: boolean;
7
+ force: boolean;
8
+ };
9
+ export declare function main(): Promise<void>;
package/dist/server.js ADDED
@@ -0,0 +1,130 @@
1
+ #!/usr/bin/env node
2
+ import { existsSync, realpathSync } from "node:fs";
3
+ import { fileURLToPath } from "node:url";
4
+ import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
5
+ import { defaultInventoryPath, loadInventory } from "./inventory.js";
6
+ import { createMcpServer } from "./mcpServer.js";
7
+ import { formatDoctorReport, runDoctor } from "./doctor.js";
8
+ import { formatInitReport, runInit } from "./init.js";
9
+ import { JsonlAuditor } from "./audit.js";
10
+ import { SshOperations } from "./operations.js";
11
+ const VERSION = "0.1.1";
12
+ export function parseArgs(argv) {
13
+ const mode = argv[0] === "--help" || argv[0] === "-h" ? "help" : argv[0] === "--version" || argv[0] === "-v" ? "version" : argv[0] === "doctor" ? "doctor" : argv[0] === "init" ? "init" : "serve";
14
+ const args = mode === "serve" ? argv : argv.slice(1);
15
+ const configIndex = args.findIndex((arg) => arg === "--config" || arg === "-c");
16
+ if (configIndex >= 0) {
17
+ const value = args[configIndex + 1];
18
+ if (!value)
19
+ throw new Error("--config requires a file path");
20
+ return { mode, configPath: value, secretsPath: parseOptionalValue(args, "--secrets"), json: args.includes("--json"), force: args.includes("--force") };
21
+ }
22
+ return { mode, configPath: defaultInventoryPath(), secretsPath: parseOptionalValue(args, "--secrets"), json: args.includes("--json"), force: args.includes("--force") };
23
+ }
24
+ function parseOptionalValue(argv, flag) {
25
+ const index = argv.findIndex((arg) => arg === flag);
26
+ if (index < 0)
27
+ return undefined;
28
+ const value = argv[index + 1];
29
+ if (!value)
30
+ throw new Error(`${flag} requires a file path`);
31
+ return value;
32
+ }
33
+ function formatHelp() {
34
+ return [
35
+ "smooth-ssh-mcp 0.1.1",
36
+ "",
37
+ "Usage:",
38
+ " smooth-ssh-mcp [--config <hosts.yaml>]",
39
+ " smooth-ssh-mcp init [--config <hosts.yaml>] [--secrets <secrets.env>] [--force] [--json]",
40
+ " smooth-ssh-mcp doctor [--config <hosts.yaml>] [--secrets <secrets.env>] [--json]",
41
+ "",
42
+ "Options:",
43
+ " -c, --config <path> Host inventory path",
44
+ " --secrets <path> Secrets env file path for init and doctor",
45
+ " --force Regenerate init files even when they already exist",
46
+ " --json Print JSON for init and doctor",
47
+ " -h, --help Show this help",
48
+ " -v, --version Show version"
49
+ ].join("\n");
50
+ }
51
+ function loadInventoryForServer(configPath) {
52
+ const expanded = configPath.startsWith("~/")
53
+ ? `${process.env.HOME ?? ""}${configPath.slice(1)}`
54
+ : configPath;
55
+ if (!existsSync(expanded)) {
56
+ console.error(`[smooth-ssh-mcp] Inventory not found at ${configPath}. Starting with no hosts. ` +
57
+ "Create hosts.yaml or pass --config /path/to/hosts.yaml.");
58
+ return { hosts: [] };
59
+ }
60
+ return loadInventory(configPath);
61
+ }
62
+ export async function main() {
63
+ const args = parseArgs(process.argv.slice(2));
64
+ if (args.mode === "help") {
65
+ console.log(formatHelp());
66
+ process.exit(0);
67
+ }
68
+ if (args.mode === "version") {
69
+ console.log(VERSION);
70
+ process.exit(0);
71
+ }
72
+ if (args.mode === "doctor") {
73
+ const report = runDoctor({ configPath: args.configPath, secretsPath: args.secretsPath });
74
+ console.log(args.json ? JSON.stringify(report, null, 2) : formatDoctorReport(report));
75
+ process.exit(report.ok ? 0 : 1);
76
+ }
77
+ if (args.mode === "init") {
78
+ const report = runInit({ configPath: args.configPath, secretsPath: args.secretsPath, force: args.force });
79
+ console.log(args.json ? JSON.stringify(report, null, 2) : formatInitReport(report));
80
+ process.exit(report.ok ? 0 : 1);
81
+ }
82
+ const { configPath } = args;
83
+ const inventory = loadInventoryForServer(configPath);
84
+ const operations = new SshOperations({ inventory });
85
+ installShutdownCleanup(operations);
86
+ const server = createMcpServer(operations, { auditor: new JsonlAuditor() });
87
+ process.stdin.resume();
88
+ await server.connect(new StdioServerTransport());
89
+ console.error(`[smooth-ssh-mcp] Running on stdio with ${inventory.hosts.length} configured hosts.`);
90
+ await keepStdioServerAlive();
91
+ }
92
+ function installShutdownCleanup(operations) {
93
+ let cleaned = false;
94
+ const cleanup = () => {
95
+ if (cleaned)
96
+ return;
97
+ cleaned = true;
98
+ operations.dispose();
99
+ };
100
+ process.once("beforeExit", cleanup);
101
+ for (const signal of ["SIGINT", "SIGTERM"]) {
102
+ process.once(signal, () => {
103
+ cleanup();
104
+ process.exit(0);
105
+ });
106
+ }
107
+ }
108
+ function keepStdioServerAlive() {
109
+ setInterval(() => undefined, 60_000);
110
+ return new Promise(() => undefined);
111
+ }
112
+ if (isDirectExecution()) {
113
+ main().catch((error) => {
114
+ console.error("[smooth-ssh-mcp] Fatal error:", error);
115
+ process.exit(1);
116
+ });
117
+ }
118
+ function isDirectExecution() {
119
+ const entryPath = process.argv[1];
120
+ if (!entryPath)
121
+ return false;
122
+ const currentPath = fileURLToPath(import.meta.url);
123
+ try {
124
+ return realpathSync(entryPath) === realpathSync(currentPath);
125
+ }
126
+ catch {
127
+ return entryPath === currentPath;
128
+ }
129
+ }
130
+ //# sourceMappingURL=server.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"server.js","sourceRoot":"","sources":["../src/server.ts"],"names":[],"mappings":";AACA,OAAO,EAAE,UAAU,EAAE,YAAY,EAAE,MAAM,SAAS,CAAC;AACnD,OAAO,EAAE,aAAa,EAAE,MAAM,UAAU,CAAC;AACzC,OAAO,EAAE,oBAAoB,EAAE,MAAM,2CAA2C,CAAC;AACjF,OAAO,EAAE,oBAAoB,EAAE,aAAa,EAAE,MAAM,gBAAgB,CAAC;AACrE,OAAO,EAAE,eAAe,EAAE,MAAM,gBAAgB,CAAC;AACjD,OAAO,EAAE,kBAAkB,EAAE,SAAS,EAAE,MAAM,aAAa,CAAC;AAC5D,OAAO,EAAE,gBAAgB,EAAE,OAAO,EAAE,MAAM,WAAW,CAAC;AACtD,OAAO,EAAE,YAAY,EAAE,MAAM,YAAY,CAAC;AAC1C,OAAO,EAAE,aAAa,EAAE,MAAM,iBAAiB,CAAC;AAGhD,MAAM,OAAO,GAAG,OAAO,CAAC;AAExB,MAAM,UAAU,SAAS,CAAC,IAAc;IACtC,MAAM,IAAI,GAAG,IAAI,CAAC,CAAC,CAAC,KAAK,QAAQ,IAAI,IAAI,CAAC,CAAC,CAAC,KAAK,IAAI,CAAC,CAAC,CAAC,MAAM,CAAC,CAAC,CAAC,IAAI,CAAC,CAAC,CAAC,KAAK,WAAW,IAAI,IAAI,CAAC,CAAC,CAAC,KAAK,IAAI,CAAC,CAAC,CAAC,SAAS,CAAC,CAAC,CAAC,IAAI,CAAC,CAAC,CAAC,KAAK,QAAQ,CAAC,CAAC,CAAC,QAAQ,CAAC,CAAC,CAAC,IAAI,CAAC,CAAC,CAAC,KAAK,MAAM,CAAC,CAAC,CAAC,MAAM,CAAC,CAAC,CAAC,OAAO,CAAC;IACnM,MAAM,IAAI,GAAG,IAAI,KAAK,OAAO,CAAC,CAAC,CAAC,IAAI,CAAC,CAAC,CAAC,IAAI,CAAC,KAAK,CAAC,CAAC,CAAC,CAAC;IACrD,MAAM,WAAW,GAAG,IAAI,CAAC,SAAS,CAAC,CAAC,GAAG,EAAE,EAAE,CAAC,GAAG,KAAK,UAAU,IAAI,GAAG,KAAK,IAAI,CAAC,CAAC;IAChF,IAAI,WAAW,IAAI,CAAC,EAAE,CAAC;QACrB,MAAM,KAAK,GAAG,IAAI,CAAC,WAAW,GAAG,CAAC,CAAC,CAAC;QACpC,IAAI,CAAC,KAAK;YAAE,MAAM,IAAI,KAAK,CAAC,+BAA+B,CAAC,CAAC;QAC7D,OAAO,EAAE,IAAI,EAAE,UAAU,EAAE,KAAK,EAAE,WAAW,EAAE,kBAAkB,CAAC,IAAI,EAAE,WAAW,CAAC,EAAE,IAAI,EAAE,IAAI,CAAC,QAAQ,CAAC,QAAQ,CAAC,EAAE,KAAK,EAAE,IAAI,CAAC,QAAQ,CAAC,SAAS,CAAC,EAAE,CAAC;IACzJ,CAAC;IACD,OAAO,EAAE,IAAI,EAAE,UAAU,EAAE,oBAAoB,EAAE,EAAE,WAAW,EAAE,kBAAkB,CAAC,IAAI,EAAE,WAAW,CAAC,EAAE,IAAI,EAAE,IAAI,CAAC,QAAQ,CAAC,QAAQ,CAAC,EAAE,KAAK,EAAE,IAAI,CAAC,QAAQ,CAAC,SAAS,CAAC,EAAE,CAAC;AAC1K,CAAC;AAED,SAAS,kBAAkB,CAAC,IAAc,EAAE,IAAY;IACtD,MAAM,KAAK,GAAG,IAAI,CAAC,SAAS,CAAC,CAAC,GAAG,EAAE,EAAE,CAAC,GAAG,KAAK,IAAI,CAAC,CAAC;IACpD,IAAI,KAAK,GAAG,CAAC;QAAE,OAAO,SAAS,CAAC;IAChC,MAAM,KAAK,GAAG,IAAI,CAAC,KAAK,GAAG,CAAC,CAAC,CAAC;IAC9B,IAAI,CAAC,KAAK;QAAE,MAAM,IAAI,KAAK,CAAC,GAAG,IAAI,uBAAuB,CAAC,CAAC;IAC5D,OAAO,KAAK,CAAC;AACf,CAAC;AAGD,SAAS,UAAU;IACjB,OAAO;QACL,sBAAsB;QACtB,EAAE;QACF,QAAQ;QACR,0CAA0C;QAC1C,4FAA4F;QAC5F,oFAAoF;QACpF,EAAE;QACF,UAAU;QACV,4CAA4C;QAC5C,kEAAkE;QAClE,2EAA2E;QAC3E,uDAAuD;QACvD,uCAAuC;QACvC,qCAAqC;KACtC,CAAC,IAAI,CAAC,IAAI,CAAC,CAAC;AACf,CAAC;AAED,SAAS,sBAAsB,CAAC,UAAkB;IAChD,MAAM,QAAQ,GAAG,UAAU,CAAC,UAAU,CAAC,IAAI,CAAC;QAC1C,CAAC,CAAC,GAAG,OAAO,CAAC,GAAG,CAAC,IAAI,IAAI,EAAE,GAAG,UAAU,CAAC,KAAK,CAAC,CAAC,CAAC,EAAE;QACnD,CAAC,CAAC,UAAU,CAAC;IACf,IAAI,CAAC,UAAU,CAAC,QAAQ,CAAC,EAAE,CAAC;QAC1B,OAAO,CAAC,KAAK,CACX,2CAA2C,UAAU,4BAA4B;YAC/E,yDAAyD,CAC5D,CAAC;QACF,OAAO,EAAE,KAAK,EAAE,EAAE,EAAE,CAAC;IACvB,CAAC;IACD,OAAO,aAAa,CAAC,UAAU,CAAC,CAAC;AACnC,CAAC;AAED,MAAM,CAAC,KAAK,UAAU,IAAI;IACxB,MAAM,IAAI,GAAG,SAAS,CAAC,OAAO,CAAC,IAAI,CAAC,KAAK,CAAC,CAAC,CAAC,CAAC,CAAC;IAC9C,IAAI,IAAI,CAAC,IAAI,KAAK,MAAM,EAAE,CAAC;QACzB,OAAO,CAAC,GAAG,CAAC,UAAU,EAAE,CAAC,CAAC;QAC1B,OAAO,CAAC,IAAI,CAAC,CAAC,CAAC,CAAC;IAClB,CAAC;IACD,IAAI,IAAI,CAAC,IAAI,KAAK,SAAS,EAAE,CAAC;QAC5B,OAAO,CAAC,GAAG,CAAC,OAAO,CAAC,CAAC;QACrB,OAAO,CAAC,IAAI,CAAC,CAAC,CAAC,CAAC;IAClB,CAAC;IACD,IAAI,IAAI,CAAC,IAAI,KAAK,QAAQ,EAAE,CAAC;QAC3B,MAAM,MAAM,GAAG,SAAS,CAAC,EAAE,UAAU,EAAE,IAAI,CAAC,UAAU,EAAE,WAAW,EAAE,IAAI,CAAC,WAAW,EAAE,CAAC,CAAC;QACzF,OAAO,CAAC,GAAG,CAAC,IAAI,CAAC,IAAI,CAAC,CAAC,CAAC,IAAI,CAAC,SAAS,CAAC,MAAM,EAAE,IAAI,EAAE,CAAC,CAAC,CAAC,CAAC,CAAC,kBAAkB,CAAC,MAAM,CAAC,CAAC,CAAC;QACtF,OAAO,CAAC,IAAI,CAAC,MAAM,CAAC,EAAE,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC;IAClC,CAAC;IACD,IAAI,IAAI,CAAC,IAAI,KAAK,MAAM,EAAE,CAAC;QACzB,MAAM,MAAM,GAAG,OAAO,CAAC,EAAE,UAAU,EAAE,IAAI,CAAC,UAAU,EAAE,WAAW,EAAE,IAAI,CAAC,WAAW,EAAE,KAAK,EAAE,IAAI,CAAC,KAAK,EAAE,CAAC,CAAC;QAC1G,OAAO,CAAC,GAAG,CAAC,IAAI,CAAC,IAAI,CAAC,CAAC,CAAC,IAAI,CAAC,SAAS,CAAC,MAAM,EAAE,IAAI,EAAE,CAAC,CAAC,CAAC,CAAC,CAAC,gBAAgB,CAAC,MAAM,CAAC,CAAC,CAAC;QACpF,OAAO,CAAC,IAAI,CAAC,MAAM,CAAC,EAAE,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC;IAClC,CAAC;IACD,MAAM,EAAE,UAAU,EAAE,GAAG,IAAI,CAAC;IAC5B,MAAM,SAAS,GAAG,sBAAsB,CAAC,UAAU,CAAC,CAAC;IACrD,MAAM,UAAU,GAAG,IAAI,aAAa,CAAC,EAAE,SAAS,EAAE,CAAC,CAAC;IACpD,sBAAsB,CAAC,UAAU,CAAC,CAAC;IACnC,MAAM,MAAM,GAAG,eAAe,CAAC,UAAU,EAAE,EAAE,OAAO,EAAE,IAAI,YAAY,EAAE,EAAE,CAAC,CAAC;IAC5E,OAAO,CAAC,KAAK,CAAC,MAAM,EAAE,CAAC;IACvB,MAAM,MAAM,CAAC,OAAO,CAAC,IAAI,oBAAoB,EAAE,CAAC,CAAC;IACjD,OAAO,CAAC,KAAK,CAAC,0CAA0C,SAAS,CAAC,KAAK,CAAC,MAAM,oBAAoB,CAAC,CAAC;IACpG,MAAM,oBAAoB,EAAE,CAAC;AAC/B,CAAC;AAED,SAAS,sBAAsB,CAAC,UAAyB;IACvD,IAAI,OAAO,GAAG,KAAK,CAAC;IACpB,MAAM,OAAO,GAAG,GAAG,EAAE;QACnB,IAAI,OAAO;YAAE,OAAO;QACpB,OAAO,GAAG,IAAI,CAAC;QACf,UAAU,CAAC,OAAO,EAAE,CAAC;IACvB,CAAC,CAAC;IAEF,OAAO,CAAC,IAAI,CAAC,YAAY,EAAE,OAAO,CAAC,CAAC;IACpC,KAAK,MAAM,MAAM,IAAI,CAAC,QAAQ,EAAE,SAAS,CAAU,EAAE,CAAC;QACpD,OAAO,CAAC,IAAI,CAAC,MAAM,EAAE,GAAG,EAAE;YACxB,OAAO,EAAE,CAAC;YACV,OAAO,CAAC,IAAI,CAAC,CAAC,CAAC,CAAC;QAClB,CAAC,CAAC,CAAC;IACL,CAAC;AACH,CAAC;AAED,SAAS,oBAAoB;IAC3B,WAAW,CAAC,GAAG,EAAE,CAAC,SAAS,EAAE,MAAM,CAAC,CAAC;IACrC,OAAO,IAAI,OAAO,CAAC,GAAG,EAAE,CAAC,SAAS,CAAC,CAAC;AACtC,CAAC;AAED,IAAI,iBAAiB,EAAE,EAAE,CAAC;IACxB,IAAI,EAAE,CAAC,KAAK,CAAC,CAAC,KAAK,EAAE,EAAE;QACrB,OAAO,CAAC,KAAK,CAAC,+BAA+B,EAAE,KAAK,CAAC,CAAC;QACtD,OAAO,CAAC,IAAI,CAAC,CAAC,CAAC,CAAC;IAClB,CAAC,CAAC,CAAC;AACL,CAAC;AAED,SAAS,iBAAiB;IACxB,MAAM,SAAS,GAAG,OAAO,CAAC,IAAI,CAAC,CAAC,CAAC,CAAC;IAClC,IAAI,CAAC,SAAS;QAAE,OAAO,KAAK,CAAC;IAC7B,MAAM,WAAW,GAAG,aAAa,CAAC,MAAM,CAAC,IAAI,CAAC,GAAG,CAAC,CAAC;IACnD,IAAI,CAAC;QACH,OAAO,YAAY,CAAC,SAAS,CAAC,KAAK,YAAY,CAAC,WAAW,CAAC,CAAC;IAC/D,CAAC;IAAC,MAAM,CAAC;QACP,OAAO,SAAS,KAAK,WAAW,CAAC;IACnC,CAAC;AACH,CAAC"}
@@ -0,0 +1,77 @@
1
+ import { EventEmitter } from "node:events";
2
+ import type { Host } from "./types.js";
3
+ type InputStreamLike = {
4
+ write?: (chunk: string) => unknown;
5
+ end?: () => unknown;
6
+ };
7
+ export type SpawnedSessionProcess = {
8
+ pid?: number;
9
+ stdout: EventEmitter;
10
+ stderr: EventEmitter;
11
+ stdin: InputStreamLike;
12
+ kill: (signal?: NodeJS.Signals) => unknown;
13
+ on: (event: string, listener: (...args: unknown[]) => void) => unknown;
14
+ unref?: () => unknown;
15
+ };
16
+ type SessionManagerOptions = {
17
+ controlDir: string;
18
+ maxSessions?: number;
19
+ outputBufferBytes?: number;
20
+ ttlSeconds?: number;
21
+ idleTimeoutSeconds?: number;
22
+ env?: NodeJS.ProcessEnv;
23
+ spawnProcess?: (file: string, args: string[], env?: NodeJS.ProcessEnv) => SpawnedSessionProcess;
24
+ spawnControlProcess?: (file: string, args: string[], env?: NodeJS.ProcessEnv) => SpawnedSessionProcess;
25
+ };
26
+ type SessionRecord = {
27
+ sessionId: string;
28
+ hostId: string;
29
+ host: Host;
30
+ pid?: number;
31
+ startedAt: number;
32
+ lastActiveAt: number;
33
+ state: "running" | "exited" | "error";
34
+ buffer: string;
35
+ truncated: boolean;
36
+ finalized: boolean;
37
+ process: SpawnedSessionProcess;
38
+ ttlSeconds: number;
39
+ idleTimeoutSeconds: number;
40
+ outputBufferBytes: number;
41
+ };
42
+ export type SessionInfo = Omit<SessionRecord, "buffer" | "finalized" | "host" | "process" | "startedAt" | "lastActiveAt"> & {
43
+ startedAt: string;
44
+ lastActiveAt: string;
45
+ };
46
+ export declare class SessionManager {
47
+ private readonly sessions;
48
+ private readonly maxSessions;
49
+ private readonly outputBufferBytes;
50
+ private readonly ttlSeconds;
51
+ private readonly idleTimeoutSeconds;
52
+ private readonly env;
53
+ private readonly spawnProcess;
54
+ private readonly spawnControlProcess;
55
+ constructor(options: SessionManagerOptions);
56
+ private readonly controlDir;
57
+ start(host: Host): SessionInfo;
58
+ send(sessionId: string, input: string): SessionInfo;
59
+ read(sessionId: string, maxBytes?: number): {
60
+ sessionId: string;
61
+ output: string;
62
+ truncated: boolean;
63
+ state: string;
64
+ };
65
+ hostForSession(sessionId: string): Host;
66
+ stop(sessionId: string): SessionInfo;
67
+ list(): SessionInfo[];
68
+ stopAll(): void;
69
+ getProcessForTest(sessionId: string): SpawnedSessionProcess;
70
+ private appendOutput;
71
+ private cleanupExpired;
72
+ private finalizeSession;
73
+ private closeControlMaster;
74
+ private requireSession;
75
+ private toInfo;
76
+ }
77
+ export {};
@@ -0,0 +1,195 @@
1
+ import { spawn } from "node:child_process";
2
+ import { randomUUID } from "node:crypto";
3
+ import { wrapWithPasswordAuth } from "./auth.js";
4
+ import { buildSshArgs, buildSshControlArgs } from "./sshArgs.js";
5
+ import { redactAndTruncate } from "./redaction.js";
6
+ export class SessionManager {
7
+ sessions = new Map();
8
+ maxSessions;
9
+ outputBufferBytes;
10
+ ttlSeconds;
11
+ idleTimeoutSeconds;
12
+ env;
13
+ spawnProcess;
14
+ spawnControlProcess;
15
+ constructor(options) {
16
+ this.maxSessions = options.maxSessions ?? 8;
17
+ this.outputBufferBytes = options.outputBufferBytes ?? 64 * 1024;
18
+ this.ttlSeconds = options.ttlSeconds ?? 30 * 60;
19
+ this.idleTimeoutSeconds = options.idleTimeoutSeconds ?? 5 * 60;
20
+ this.env = options.env ?? process.env;
21
+ this.spawnProcess =
22
+ options.spawnProcess ??
23
+ ((file, args, env) => spawn(file, args, {
24
+ env,
25
+ shell: false,
26
+ stdio: ["pipe", "pipe", "pipe"]
27
+ }));
28
+ this.spawnControlProcess =
29
+ options.spawnControlProcess ??
30
+ ((file, args, env) => spawn(file, args, {
31
+ detached: true,
32
+ env,
33
+ shell: false,
34
+ stdio: "ignore"
35
+ }));
36
+ this.controlDir = options.controlDir;
37
+ }
38
+ controlDir;
39
+ start(host) {
40
+ this.cleanupExpired();
41
+ if (this.sessions.size >= this.maxSessions) {
42
+ throw new Error(`Maximum active sessions reached: ${this.maxSessions}`);
43
+ }
44
+ const args = buildSshArgs(host, {
45
+ controlDir: this.controlDir,
46
+ forceTty: true,
47
+ batchMode: false
48
+ });
49
+ const commandSpec = wrapWithPasswordAuth(host, "ssh", args, this.env);
50
+ const child = this.spawnProcess(commandSpec.file, commandSpec.args, commandSpec.env);
51
+ const now = Date.now();
52
+ const record = {
53
+ sessionId: randomUUID(),
54
+ hostId: host.id,
55
+ host,
56
+ pid: child.pid,
57
+ startedAt: now,
58
+ lastActiveAt: now,
59
+ state: "running",
60
+ buffer: "",
61
+ truncated: false,
62
+ finalized: false,
63
+ process: child,
64
+ ttlSeconds: this.ttlSeconds,
65
+ idleTimeoutSeconds: this.idleTimeoutSeconds,
66
+ outputBufferBytes: this.outputBufferBytes
67
+ };
68
+ this.sessions.set(record.sessionId, record);
69
+ child.stdout.on("data", (chunk) => this.appendOutput(record, chunk));
70
+ child.stderr.on("data", (chunk) => this.appendOutput(record, chunk));
71
+ child.on("close", () => {
72
+ this.finalizeSession(record, { state: "exited", terminateProcess: false });
73
+ });
74
+ child.on("error", () => {
75
+ this.finalizeSession(record, { state: "error", terminateProcess: false });
76
+ });
77
+ return this.toInfo(record);
78
+ }
79
+ send(sessionId, input) {
80
+ const record = this.requireSession(sessionId);
81
+ if (record.state !== "running")
82
+ throw new Error(`Session is not running: ${sessionId}`);
83
+ record.process.stdin.write?.(input);
84
+ record.lastActiveAt = Date.now();
85
+ return this.toInfo(record);
86
+ }
87
+ read(sessionId, maxBytes) {
88
+ const record = this.requireSession(sessionId);
89
+ record.lastActiveAt = Date.now();
90
+ const limit = maxBytes ?? record.outputBufferBytes;
91
+ const redacted = redactAndTruncate(record.buffer, limit);
92
+ const truncated = record.truncated || redacted.truncated;
93
+ record.buffer = "";
94
+ record.truncated = false;
95
+ return {
96
+ sessionId,
97
+ output: redacted.text,
98
+ truncated,
99
+ state: record.state
100
+ };
101
+ }
102
+ hostForSession(sessionId) {
103
+ return this.requireSession(sessionId).host;
104
+ }
105
+ stop(sessionId) {
106
+ const record = this.requireSession(sessionId);
107
+ return this.finalizeSession(record, { state: "exited", terminateProcess: true });
108
+ }
109
+ list() {
110
+ this.cleanupExpired();
111
+ return [...this.sessions.values()].map((record) => this.toInfo(record));
112
+ }
113
+ stopAll() {
114
+ for (const record of [...this.sessions.values()]) {
115
+ this.finalizeSession(record, { state: "exited", terminateProcess: true });
116
+ }
117
+ }
118
+ getProcessForTest(sessionId) {
119
+ return this.requireSession(sessionId).process;
120
+ }
121
+ appendOutput(record, chunk) {
122
+ record.lastActiveAt = Date.now();
123
+ record.buffer += Buffer.isBuffer(chunk) ? chunk.toString("utf8") : String(chunk);
124
+ while (Buffer.byteLength(record.buffer, "utf8") > record.outputBufferBytes) {
125
+ record.buffer = record.buffer.slice(1);
126
+ record.truncated = true;
127
+ }
128
+ }
129
+ cleanupExpired() {
130
+ const now = Date.now();
131
+ for (const [sessionId, record] of this.sessions) {
132
+ const ttlExpired = now - record.startedAt > record.ttlSeconds * 1000;
133
+ const idleExpired = now - record.lastActiveAt > record.idleTimeoutSeconds * 1000;
134
+ if (ttlExpired || idleExpired) {
135
+ this.finalizeSession(record, { state: "exited", terminateProcess: true });
136
+ }
137
+ }
138
+ }
139
+ finalizeSession(record, options) {
140
+ if (!record.finalized) {
141
+ record.finalized = true;
142
+ if (options.terminateProcess) {
143
+ record.process.stdin.end?.();
144
+ record.process.kill("SIGTERM");
145
+ forceKillLater(record.process);
146
+ }
147
+ this.closeControlMaster(record.host);
148
+ }
149
+ record.state = options.state;
150
+ record.lastActiveAt = Date.now();
151
+ this.sessions.delete(record.sessionId);
152
+ return this.toInfo(record);
153
+ }
154
+ closeControlMaster(host) {
155
+ const args = buildSshControlArgs(host, {
156
+ controlDir: this.controlDir,
157
+ controlCommand: "exit"
158
+ });
159
+ try {
160
+ const child = this.spawnControlProcess("ssh", args, this.env);
161
+ child.on("error", () => undefined);
162
+ child.unref?.();
163
+ }
164
+ catch {
165
+ // Best-effort cleanup only; a missing or already-closed control socket is fine.
166
+ }
167
+ }
168
+ requireSession(sessionId) {
169
+ this.cleanupExpired();
170
+ const record = this.sessions.get(sessionId);
171
+ if (!record)
172
+ throw new Error(`Session not found: ${sessionId}`);
173
+ return record;
174
+ }
175
+ toInfo(record) {
176
+ return {
177
+ sessionId: record.sessionId,
178
+ hostId: record.hostId,
179
+ pid: record.pid,
180
+ startedAt: new Date(record.startedAt).toISOString(),
181
+ lastActiveAt: new Date(record.lastActiveAt).toISOString(),
182
+ state: record.state,
183
+ truncated: record.truncated,
184
+ ttlSeconds: record.ttlSeconds,
185
+ idleTimeoutSeconds: record.idleTimeoutSeconds,
186
+ outputBufferBytes: record.outputBufferBytes
187
+ };
188
+ }
189
+ }
190
+ function forceKillLater(process) {
191
+ setTimeout(() => {
192
+ process.kill("SIGKILL");
193
+ }, 1000).unref();
194
+ }
195
+ //# sourceMappingURL=sessionManager.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"sessionManager.js","sourceRoot":"","sources":["../src/sessionManager.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,24 @@
1
+ import type { Host } from "./types.js";
2
+ type SshArgOptions = {
3
+ controlDir: string;
4
+ command?: string;
5
+ timeoutSeconds?: number;
6
+ forceTty?: boolean;
7
+ batchMode?: boolean;
8
+ };
9
+ type ScpArgOptions = {
10
+ controlDir: string;
11
+ direction: "upload" | "download";
12
+ localPath: string;
13
+ remotePath: string;
14
+ };
15
+ type SshControlOptions = {
16
+ controlDir: string;
17
+ controlCommand: "check" | "exit" | "stop";
18
+ };
19
+ export declare function buildSshArgs(host: Host, options: SshArgOptions): string[];
20
+ export declare function buildSshControlArgs(host: Host, options: SshControlOptions): string[];
21
+ export declare function buildScpArgs(host: Host, options: ScpArgOptions): string[];
22
+ export declare function controlPathForHost(host: Host, controlDir: string): string;
23
+ export declare function targetForHost(host: Host): string;
24
+ export {};