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,43 @@
1
+ #!/usr/bin/env bash
2
+ set -euo pipefail
3
+
4
+ SCRIPT_DIR="$(cd -- "$(dirname -- "${BASH_SOURCE[0]}")" && pwd)"
5
+ PROJECT_DIR="$(cd -- "$SCRIPT_DIR/.." && pwd)"
6
+
7
+ SECRETS_FILE="${SMOOTH_SSH_MCP_SECRETS:-$HOME/.config/smooth-ssh-mcp/secrets.env}"
8
+ CONFIG_FILE="${SMOOTH_SSH_MCP_CONFIG:-$HOME/.config/smooth-ssh-mcp/hosts.yaml}"
9
+
10
+ if [[ -f "$SECRETS_FILE" ]]; then
11
+ if [[ ! -O "$SECRETS_FILE" ]]; then
12
+ echo "Refusing to load secrets file not owned by current user: $SECRETS_FILE" >&2
13
+ exit 1
14
+ fi
15
+ mode="$(stat -c '%a' "$SECRETS_FILE")"
16
+ case "$mode" in
17
+ 400|600) ;;
18
+ *)
19
+ echo "Refusing to load secrets file with mode $mode; run: chmod 600 $SECRETS_FILE" >&2
20
+ exit 1
21
+ ;;
22
+ esac
23
+ while IFS= read -r line || [[ -n "$line" ]]; do
24
+ line="${line%$'\r'}"
25
+ if [[ "$line" =~ ^[[:space:]]*$ || "$line" =~ ^[[:space:]]*# ]]; then
26
+ continue
27
+ fi
28
+ if [[ ! "$line" =~ ^[[:space:]]*(export[[:space:]]+)?([A-Za-z_][A-Za-z0-9_]*)=(.*)$ ]]; then
29
+ echo "Refusing to load invalid secrets line from $SECRETS_FILE" >&2
30
+ exit 1
31
+ fi
32
+ key="${BASH_REMATCH[2]}"
33
+ value="${BASH_REMATCH[3]}"
34
+ if [[ "$value" == \"*\" && "$value" == *\" && ${#value} -ge 2 ]]; then
35
+ value="${value:1:${#value}-2}"
36
+ elif [[ "$value" == \'*\' && "$value" == *\' && ${#value} -ge 2 ]]; then
37
+ value="${value:1:${#value}-2}"
38
+ fi
39
+ export "$key=$value"
40
+ done < "$SECRETS_FILE"
41
+ fi
42
+
43
+ exec node "$PROJECT_DIR/dist/server.js" --config "$CONFIG_FILE"
@@ -0,0 +1,23 @@
1
+ export type AuditResultKind = "confirmation_required" | "policy_decision" | "exec_result" | "blocked" | "ok" | "error";
2
+ export type AuditToolCall = {
3
+ tool: string;
4
+ input: unknown;
5
+ result: unknown;
6
+ durationMs: number;
7
+ };
8
+ export type Auditor = {
9
+ recordToolCall(call: AuditToolCall): void;
10
+ };
11
+ export type JsonlAuditorOptions = {
12
+ path?: string;
13
+ enabled?: boolean;
14
+ now?: () => Date;
15
+ };
16
+ export declare class JsonlAuditor implements Auditor {
17
+ private readonly path;
18
+ private readonly enabled;
19
+ private readonly now;
20
+ constructor(options?: JsonlAuditorOptions);
21
+ recordToolCall(call: AuditToolCall): void;
22
+ }
23
+ export declare function summarizeToolCall(call: AuditToolCall, timestamp: Date): Record<string, unknown>;
package/dist/audit.js ADDED
@@ -0,0 +1,140 @@
1
+ import { appendFileSync, mkdirSync } from "node:fs";
2
+ import { dirname, join } from "node:path";
3
+ import { redactAndTruncate } from "./redaction.js";
4
+ const SENSITIVE_KEYS = new Set(["confirmationToken", "env", "stdin", "input", "password", "passwordEnv", "secret", "token"]);
5
+ export class JsonlAuditor {
6
+ path;
7
+ enabled;
8
+ now;
9
+ constructor(options = {}) {
10
+ this.path = expandHome(options.path ?? process.env.SMOOTH_SSH_MCP_AUDIT_LOG ?? "~/.config/smooth-ssh-mcp/audit.jsonl");
11
+ this.enabled = options.enabled ?? process.env.SMOOTH_SSH_MCP_AUDIT !== "0";
12
+ this.now = options.now ?? (() => new Date());
13
+ }
14
+ recordToolCall(call) {
15
+ if (!this.enabled)
16
+ return;
17
+ const entry = summarizeToolCall(call, this.now());
18
+ mkdirSync(dirname(this.path), { recursive: true, mode: 0o700 });
19
+ appendFileSync(this.path, JSON.stringify(entry) + "\n", { encoding: "utf8", mode: 0o600 });
20
+ }
21
+ }
22
+ export function summarizeToolCall(call, timestamp) {
23
+ const result = call.result;
24
+ const redactions = collectRedactions(result);
25
+ return {
26
+ timestamp: timestamp.toISOString(),
27
+ tool: call.tool,
28
+ hostId: findString(call.input, "hostId") ?? findString(result, "hostId"),
29
+ operation: inferOperation(call.tool, result),
30
+ resultKind: resultKind(result),
31
+ exitCode: findNumberOrNull(result, "exitCode"),
32
+ durationMs: call.durationMs,
33
+ remoteDurationMs: findNumber(result, "durationMs"),
34
+ truncated: findBoolean(result, "truncated"),
35
+ redactions,
36
+ input: sanitizeValue(call.input)
37
+ };
38
+ }
39
+ function resultKind(value) {
40
+ if (!value || typeof value !== "object")
41
+ return "ok";
42
+ const input = value;
43
+ if (input.confirmationRequired === true)
44
+ return "confirmation_required";
45
+ if (input.blocked)
46
+ return "blocked";
47
+ if (typeof input.exitCode === "number" || input.exitCode === null)
48
+ return "exec_result";
49
+ if (typeof input.allowed === "boolean" && typeof input.confirmationRequired === "boolean")
50
+ return "policy_decision";
51
+ if (input.error)
52
+ return "error";
53
+ return "ok";
54
+ }
55
+ function inferOperation(tool, result) {
56
+ const resultOperation = findString(result, "operation");
57
+ if (resultOperation)
58
+ return resultOperation;
59
+ if (tool.includes("upload"))
60
+ return "upload";
61
+ if (tool.includes("download"))
62
+ return "download";
63
+ if (tool.includes("forward"))
64
+ return "forward";
65
+ if (tool.includes("session"))
66
+ return "pty";
67
+ if (tool.includes("permission"))
68
+ return "permission";
69
+ if (tool.includes("ssh") || tool.includes("task") || tool.includes("cleanup"))
70
+ return "exec";
71
+ return "local";
72
+ }
73
+ function sanitizeValue(value) {
74
+ if (Array.isArray(value))
75
+ return value.map(sanitizeValue);
76
+ if (!value || typeof value !== "object")
77
+ return sanitizeScalar(value);
78
+ const output = {};
79
+ for (const [key, child] of Object.entries(value)) {
80
+ output[key] = isSensitiveKey(key) ? "[REDACTED]" : sanitizeValue(child);
81
+ }
82
+ return output;
83
+ }
84
+ function sanitizeScalar(value) {
85
+ if (typeof value !== "string")
86
+ return value;
87
+ return redactAndTruncate(value, 1024).text;
88
+ }
89
+ function isSensitiveKey(key) {
90
+ const lower = key.toLowerCase();
91
+ return SENSITIVE_KEYS.has(key) || lower.includes("password") || lower.includes("secret") || lower.includes("token");
92
+ }
93
+ function collectRedactions(value) {
94
+ const redactions = findValue(value, "redactions");
95
+ return Array.isArray(redactions) ? redactions : [];
96
+ }
97
+ function findString(value, key) {
98
+ const found = findValue(value, key);
99
+ return typeof found === "string" ? found : undefined;
100
+ }
101
+ function findNumber(value, key) {
102
+ const found = findValue(value, key);
103
+ return typeof found === "number" ? found : undefined;
104
+ }
105
+ function findNumberOrNull(value, key) {
106
+ const found = findValue(value, key);
107
+ return typeof found === "number" || found === null ? found : undefined;
108
+ }
109
+ function findBoolean(value, key) {
110
+ const found = findValue(value, key);
111
+ return typeof found === "boolean" ? found : undefined;
112
+ }
113
+ function findValue(value, key) {
114
+ if (!value || typeof value !== "object")
115
+ return undefined;
116
+ const direct = value[key];
117
+ if (direct !== undefined)
118
+ return direct;
119
+ const blocked = value.blocked;
120
+ if (blocked && typeof blocked === "object") {
121
+ const blockedValue = findValue(blocked, key);
122
+ if (blockedValue !== undefined)
123
+ return blockedValue;
124
+ }
125
+ const result = value.result;
126
+ if (result && typeof result === "object") {
127
+ const resultValue = findValue(result, key);
128
+ if (resultValue !== undefined)
129
+ return resultValue;
130
+ }
131
+ return undefined;
132
+ }
133
+ function expandHome(path) {
134
+ if (path === "~")
135
+ return process.env.HOME ?? path;
136
+ if (path.startsWith("~/"))
137
+ return join(process.env.HOME ?? "", path.slice(2));
138
+ return path;
139
+ }
140
+ //# sourceMappingURL=audit.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"audit.js","sourceRoot":"","sources":["../src/audit.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,cAAc,EAAE,SAAS,EAAE,MAAM,SAAS,CAAC;AACpD,OAAO,EAAE,OAAO,EAAE,IAAI,EAAE,MAAM,WAAW,CAAC;AAC1C,OAAO,EAAE,iBAAiB,EAAE,MAAM,gBAAgB,CAAC;AAsBnD,MAAM,cAAc,GAAG,IAAI,GAAG,CAAC,CAAC,mBAAmB,EAAE,KAAK,EAAE,OAAO,EAAE,OAAO,EAAE,UAAU,EAAE,aAAa,EAAE,QAAQ,EAAE,OAAO,CAAC,CAAC,CAAC;AAE7H,MAAM,OAAO,YAAY;IACN,IAAI,CAAS;IACb,OAAO,CAAU;IACjB,GAAG,CAAa;IAEjC,YAAY,UAA+B,EAAE;QAC3C,IAAI,CAAC,IAAI,GAAG,UAAU,CAAC,OAAO,CAAC,IAAI,IAAI,OAAO,CAAC,GAAG,CAAC,wBAAwB,IAAI,sCAAsC,CAAC,CAAC;QACvH,IAAI,CAAC,OAAO,GAAG,OAAO,CAAC,OAAO,IAAI,OAAO,CAAC,GAAG,CAAC,oBAAoB,KAAK,GAAG,CAAC;QAC3E,IAAI,CAAC,GAAG,GAAG,OAAO,CAAC,GAAG,IAAI,CAAC,GAAG,EAAE,CAAC,IAAI,IAAI,EAAE,CAAC,CAAC;IAC/C,CAAC;IAED,cAAc,CAAC,IAAmB;QAChC,IAAI,CAAC,IAAI,CAAC,OAAO;YAAE,OAAO;QAC1B,MAAM,KAAK,GAAG,iBAAiB,CAAC,IAAI,EAAE,IAAI,CAAC,GAAG,EAAE,CAAC,CAAC;QAClD,SAAS,CAAC,OAAO,CAAC,IAAI,CAAC,IAAI,CAAC,EAAE,EAAE,SAAS,EAAE,IAAI,EAAE,IAAI,EAAE,KAAK,EAAE,CAAC,CAAC;QAChE,cAAc,CAAC,IAAI,CAAC,IAAI,EAAE,IAAI,CAAC,SAAS,CAAC,KAAK,CAAC,GAAG,IAAI,EAAE,EAAE,QAAQ,EAAE,MAAM,EAAE,IAAI,EAAE,KAAK,EAAE,CAAC,CAAC;IAC7F,CAAC;CACF;AAED,MAAM,UAAU,iBAAiB,CAAC,IAAmB,EAAE,SAAe;IACpE,MAAM,MAAM,GAAG,IAAI,CAAC,MAAM,CAAC;IAC3B,MAAM,UAAU,GAAG,iBAAiB,CAAC,MAAM,CAAC,CAAC;IAC7C,OAAO;QACL,SAAS,EAAE,SAAS,CAAC,WAAW,EAAE;QAClC,IAAI,EAAE,IAAI,CAAC,IAAI;QACf,MAAM,EAAE,UAAU,CAAC,IAAI,CAAC,KAAK,EAAE,QAAQ,CAAC,IAAI,UAAU,CAAC,MAAM,EAAE,QAAQ,CAAC;QACxE,SAAS,EAAE,cAAc,CAAC,IAAI,CAAC,IAAI,EAAE,MAAM,CAAC;QAC5C,UAAU,EAAE,UAAU,CAAC,MAAM,CAAC;QAC9B,QAAQ,EAAE,gBAAgB,CAAC,MAAM,EAAE,UAAU,CAAC;QAC9C,UAAU,EAAE,IAAI,CAAC,UAAU;QAC3B,gBAAgB,EAAE,UAAU,CAAC,MAAM,EAAE,YAAY,CAAC;QAClD,SAAS,EAAE,WAAW,CAAC,MAAM,EAAE,WAAW,CAAC;QAC3C,UAAU;QACV,KAAK,EAAE,aAAa,CAAC,IAAI,CAAC,KAAK,CAAC;KACjC,CAAC;AACJ,CAAC;AAED,SAAS,UAAU,CAAC,KAAc;IAChC,IAAI,CAAC,KAAK,IAAI,OAAO,KAAK,KAAK,QAAQ;QAAE,OAAO,IAAI,CAAC;IACrD,MAAM,KAAK,GAAG,KAAgC,CAAC;IAC/C,IAAI,KAAK,CAAC,oBAAoB,KAAK,IAAI;QAAE,OAAO,uBAAuB,CAAC;IACxE,IAAI,KAAK,CAAC,OAAO;QAAE,OAAO,SAAS,CAAC;IACpC,IAAI,OAAO,KAAK,CAAC,QAAQ,KAAK,QAAQ,IAAI,KAAK,CAAC,QAAQ,KAAK,IAAI;QAAE,OAAO,aAAa,CAAC;IACxF,IAAI,OAAO,KAAK,CAAC,OAAO,KAAK,SAAS,IAAI,OAAO,KAAK,CAAC,oBAAoB,KAAK,SAAS;QAAE,OAAO,iBAAiB,CAAC;IACpH,IAAI,KAAK,CAAC,KAAK;QAAE,OAAO,OAAO,CAAC;IAChC,OAAO,IAAI,CAAC;AACd,CAAC;AAED,SAAS,cAAc,CAAC,IAAY,EAAE,MAAe;IACnD,MAAM,eAAe,GAAG,UAAU,CAAC,MAAM,EAAE,WAAW,CAAC,CAAC;IACxD,IAAI,eAAe;QAAE,OAAO,eAAe,CAAC;IAC5C,IAAI,IAAI,CAAC,QAAQ,CAAC,QAAQ,CAAC;QAAE,OAAO,QAAQ,CAAC;IAC7C,IAAI,IAAI,CAAC,QAAQ,CAAC,UAAU,CAAC;QAAE,OAAO,UAAU,CAAC;IACjD,IAAI,IAAI,CAAC,QAAQ,CAAC,SAAS,CAAC;QAAE,OAAO,SAAS,CAAC;IAC/C,IAAI,IAAI,CAAC,QAAQ,CAAC,SAAS,CAAC;QAAE,OAAO,KAAK,CAAC;IAC3C,IAAI,IAAI,CAAC,QAAQ,CAAC,YAAY,CAAC;QAAE,OAAO,YAAY,CAAC;IACrD,IAAI,IAAI,CAAC,QAAQ,CAAC,KAAK,CAAC,IAAI,IAAI,CAAC,QAAQ,CAAC,MAAM,CAAC,IAAI,IAAI,CAAC,QAAQ,CAAC,SAAS,CAAC;QAAE,OAAO,MAAM,CAAC;IAC7F,OAAO,OAAO,CAAC;AACjB,CAAC;AAED,SAAS,aAAa,CAAC,KAAc;IACnC,IAAI,KAAK,CAAC,OAAO,CAAC,KAAK,CAAC;QAAE,OAAO,KAAK,CAAC,GAAG,CAAC,aAAa,CAAC,CAAC;IAC1D,IAAI,CAAC,KAAK,IAAI,OAAO,KAAK,KAAK,QAAQ;QAAE,OAAO,cAAc,CAAC,KAAK,CAAC,CAAC;IACtE,MAAM,MAAM,GAA4B,EAAE,CAAC;IAC3C,KAAK,MAAM,CAAC,GAAG,EAAE,KAAK,CAAC,IAAI,MAAM,CAAC,OAAO,CAAC,KAAK,CAAC,EAAE,CAAC;QACjD,MAAM,CAAC,GAAG,CAAC,GAAG,cAAc,CAAC,GAAG,CAAC,CAAC,CAAC,CAAC,YAAY,CAAC,CAAC,CAAC,aAAa,CAAC,KAAK,CAAC,CAAC;IAC1E,CAAC;IACD,OAAO,MAAM,CAAC;AAChB,CAAC;AAED,SAAS,cAAc,CAAC,KAAc;IACpC,IAAI,OAAO,KAAK,KAAK,QAAQ;QAAE,OAAO,KAAK,CAAC;IAC5C,OAAO,iBAAiB,CAAC,KAAK,EAAE,IAAI,CAAC,CAAC,IAAI,CAAC;AAC7C,CAAC;AAED,SAAS,cAAc,CAAC,GAAW;IACjC,MAAM,KAAK,GAAG,GAAG,CAAC,WAAW,EAAE,CAAC;IAChC,OAAO,cAAc,CAAC,GAAG,CAAC,GAAG,CAAC,IAAI,KAAK,CAAC,QAAQ,CAAC,UAAU,CAAC,IAAI,KAAK,CAAC,QAAQ,CAAC,QAAQ,CAAC,IAAI,KAAK,CAAC,QAAQ,CAAC,OAAO,CAAC,CAAC;AACtH,CAAC;AAED,SAAS,iBAAiB,CAAC,KAAc;IACvC,MAAM,UAAU,GAAG,SAAS,CAAC,KAAK,EAAE,YAAY,CAAC,CAAC;IAClD,OAAO,KAAK,CAAC,OAAO,CAAC,UAAU,CAAC,CAAC,CAAC,CAAE,UAA0B,CAAC,CAAC,CAAC,EAAE,CAAC;AACtE,CAAC;AAED,SAAS,UAAU,CAAC,KAAc,EAAE,GAAW;IAC7C,MAAM,KAAK,GAAG,SAAS,CAAC,KAAK,EAAE,GAAG,CAAC,CAAC;IACpC,OAAO,OAAO,KAAK,KAAK,QAAQ,CAAC,CAAC,CAAC,KAAK,CAAC,CAAC,CAAC,SAAS,CAAC;AACvD,CAAC;AAED,SAAS,UAAU,CAAC,KAAc,EAAE,GAAW;IAC7C,MAAM,KAAK,GAAG,SAAS,CAAC,KAAK,EAAE,GAAG,CAAC,CAAC;IACpC,OAAO,OAAO,KAAK,KAAK,QAAQ,CAAC,CAAC,CAAC,KAAK,CAAC,CAAC,CAAC,SAAS,CAAC;AACvD,CAAC;AAED,SAAS,gBAAgB,CAAC,KAAc,EAAE,GAAW;IACnD,MAAM,KAAK,GAAG,SAAS,CAAC,KAAK,EAAE,GAAG,CAAC,CAAC;IACpC,OAAO,OAAO,KAAK,KAAK,QAAQ,IAAI,KAAK,KAAK,IAAI,CAAC,CAAC,CAAC,KAAK,CAAC,CAAC,CAAC,SAAS,CAAC;AACzE,CAAC;AAED,SAAS,WAAW,CAAC,KAAc,EAAE,GAAW;IAC9C,MAAM,KAAK,GAAG,SAAS,CAAC,KAAK,EAAE,GAAG,CAAC,CAAC;IACpC,OAAO,OAAO,KAAK,KAAK,SAAS,CAAC,CAAC,CAAC,KAAK,CAAC,CAAC,CAAC,SAAS,CAAC;AACxD,CAAC;AAED,SAAS,SAAS,CAAC,KAAc,EAAE,GAAW;IAC5C,IAAI,CAAC,KAAK,IAAI,OAAO,KAAK,KAAK,QAAQ;QAAE,OAAO,SAAS,CAAC;IAC1D,MAAM,MAAM,GAAI,KAAiC,CAAC,GAAG,CAAC,CAAC;IACvD,IAAI,MAAM,KAAK,SAAS;QAAE,OAAO,MAAM,CAAC;IACxC,MAAM,OAAO,GAAI,KAAiC,CAAC,OAAO,CAAC;IAC3D,IAAI,OAAO,IAAI,OAAO,OAAO,KAAK,QAAQ,EAAE,CAAC;QAC3C,MAAM,YAAY,GAAG,SAAS,CAAC,OAAO,EAAE,GAAG,CAAC,CAAC;QAC7C,IAAI,YAAY,KAAK,SAAS;YAAE,OAAO,YAAY,CAAC;IACtD,CAAC;IACD,MAAM,MAAM,GAAI,KAAiC,CAAC,MAAM,CAAC;IACzD,IAAI,MAAM,IAAI,OAAO,MAAM,KAAK,QAAQ,EAAE,CAAC;QACzC,MAAM,WAAW,GAAG,SAAS,CAAC,MAAM,EAAE,GAAG,CAAC,CAAC;QAC3C,IAAI,WAAW,KAAK,SAAS;YAAE,OAAO,WAAW,CAAC;IACpD,CAAC;IACD,OAAO,SAAS,CAAC;AACnB,CAAC;AAED,SAAS,UAAU,CAAC,IAAY;IAC9B,IAAI,IAAI,KAAK,GAAG;QAAE,OAAO,OAAO,CAAC,GAAG,CAAC,IAAI,IAAI,IAAI,CAAC;IAClD,IAAI,IAAI,CAAC,UAAU,CAAC,IAAI,CAAC;QAAE,OAAO,IAAI,CAAC,OAAO,CAAC,GAAG,CAAC,IAAI,IAAI,EAAE,EAAE,IAAI,CAAC,KAAK,CAAC,CAAC,CAAC,CAAC,CAAC;IAC9E,OAAO,IAAI,CAAC;AACd,CAAC"}
package/dist/auth.d.ts ADDED
@@ -0,0 +1,8 @@
1
+ import type { Host } from "./types.js";
2
+ export type ProcessSpec = {
3
+ file: string;
4
+ args: string[];
5
+ env?: NodeJS.ProcessEnv;
6
+ usesPassword: boolean;
7
+ };
8
+ export declare function wrapWithPasswordAuth(host: Host, file: "ssh" | "scp", args: string[], envSource?: NodeJS.ProcessEnv): ProcessSpec;
package/dist/auth.js ADDED
@@ -0,0 +1,19 @@
1
+ export function wrapWithPasswordAuth(host, file, args, envSource = process.env) {
2
+ if (!host.passwordEnv) {
3
+ return { file, args, usesPassword: false };
4
+ }
5
+ const password = envSource[host.passwordEnv];
6
+ if (!password) {
7
+ throw new Error(`Host ${host.id} requires password environment variable ${host.passwordEnv}, but it is not set`);
8
+ }
9
+ return {
10
+ file: "sshpass",
11
+ args: ["-e", file, ...args],
12
+ env: {
13
+ ...envSource,
14
+ SSHPASS: password
15
+ },
16
+ usesPassword: true
17
+ };
18
+ }
19
+ //# sourceMappingURL=auth.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"auth.js","sourceRoot":"","sources":["../src/auth.ts"],"names":[],"mappings":"AASA,MAAM,UAAU,oBAAoB,CAClC,IAAU,EACV,IAAmB,EACnB,IAAc,EACd,YAA+B,OAAO,CAAC,GAAG;IAE1C,IAAI,CAAC,IAAI,CAAC,WAAW,EAAE,CAAC;QACtB,OAAO,EAAE,IAAI,EAAE,IAAI,EAAE,YAAY,EAAE,KAAK,EAAE,CAAC;IAC7C,CAAC;IAED,MAAM,QAAQ,GAAG,SAAS,CAAC,IAAI,CAAC,WAAW,CAAC,CAAC;IAC7C,IAAI,CAAC,QAAQ,EAAE,CAAC;QACd,MAAM,IAAI,KAAK,CAAC,QAAQ,IAAI,CAAC,EAAE,2CAA2C,IAAI,CAAC,WAAW,qBAAqB,CAAC,CAAC;IACnH,CAAC;IAED,OAAO;QACL,IAAI,EAAE,SAAS;QACf,IAAI,EAAE,CAAC,IAAI,EAAE,IAAI,EAAE,GAAG,IAAI,CAAC;QAC3B,GAAG,EAAE;YACH,GAAG,SAAS;YACZ,OAAO,EAAE,QAAQ;SAClB;QACD,YAAY,EAAE,IAAI;KACnB,CAAC;AACJ,CAAC"}
@@ -0,0 +1,27 @@
1
+ export type DoctorStatus = "ok" | "warning" | "error";
2
+ export type DoctorCheck = {
3
+ id: string;
4
+ label: string;
5
+ status: DoctorStatus;
6
+ message: string;
7
+ fix?: string;
8
+ };
9
+ export type DoctorReport = {
10
+ ok: boolean;
11
+ summary: {
12
+ ok: number;
13
+ warnings: number;
14
+ errors: number;
15
+ };
16
+ checks: DoctorCheck[];
17
+ };
18
+ type DoctorOptions = {
19
+ configPath?: string;
20
+ secretsPath?: string;
21
+ nodeVersion?: string;
22
+ commandExists?: (name: string) => boolean;
23
+ env?: NodeJS.ProcessEnv;
24
+ };
25
+ export declare function runDoctor(options?: DoctorOptions): DoctorReport;
26
+ export declare function formatDoctorReport(report: DoctorReport): string;
27
+ export {};
package/dist/doctor.js ADDED
@@ -0,0 +1,169 @@
1
+ import { existsSync, statSync } from "node:fs";
2
+ import { delimiter } from "node:path";
3
+ import { env as processEnv, version as processVersion } from "node:process";
4
+ import { defaultInventoryPath, loadInventory } from "./inventory.js";
5
+ export function runDoctor(options = {}) {
6
+ const env = options.env ?? processEnv;
7
+ const configPath = expandHome(options.configPath ?? defaultInventoryPath(), env);
8
+ const secretsPath = expandHome(options.secretsPath ?? env.SMOOTH_SSH_MCP_SECRETS ?? "~/.config/smooth-ssh-mcp/secrets.env", env);
9
+ const commandExists = options.commandExists ?? ((name) => commandExistsOnPath(name, env));
10
+ const checks = [
11
+ checkNodeVersion(options.nodeVersion ?? processVersion),
12
+ checkCommand("ssh", commandExists, "Install OpenSSH client so smooth-ssh can connect to remote hosts."),
13
+ checkCommand("scp", commandExists, "Install OpenSSH scp so file transfer tools can work."),
14
+ checkCommand("sshpass", commandExists, "Install sshpass only if you use passwordEnv hosts; key-based hosts do not need it.", true),
15
+ checkInventory(configPath),
16
+ checkFilePermissions("inventory-permissions", "Inventory permissions", configPath, "chmod 600 " + configPath, false),
17
+ checkSecrets(secretsPath),
18
+ checkFilePermissions("secrets-permissions", "Secrets permissions", secretsPath, "chmod 600 " + secretsPath, true)
19
+ ];
20
+ const summary = {
21
+ ok: checks.filter((check) => check.status === "ok").length,
22
+ warnings: checks.filter((check) => check.status === "warning").length,
23
+ errors: checks.filter((check) => check.status === "error").length
24
+ };
25
+ return {
26
+ ok: summary.errors === 0,
27
+ summary,
28
+ checks
29
+ };
30
+ }
31
+ export function formatDoctorReport(report) {
32
+ const icon = (status) => (status === "ok" ? "OK" : status === "warning" ? "WARN" : "ERROR");
33
+ const lines = [
34
+ `smooth-ssh-mcp doctor: ${report.ok ? "ok" : "issues found"}`,
35
+ `summary: ${report.summary.ok} ok, ${report.summary.warnings} warnings, ${report.summary.errors} errors`,
36
+ ...report.checks.map((check) => {
37
+ const fix = check.fix ? ` fix: ${check.fix}` : "";
38
+ return `[${icon(check.status)}] ${check.label}: ${check.message}${fix}`;
39
+ })
40
+ ];
41
+ return lines.join("\n");
42
+ }
43
+ function checkNodeVersion(nodeVersion) {
44
+ const major = Number(nodeVersion.replace(/^v/, "").split(".")[0]);
45
+ if (Number.isFinite(major) && major >= 20) {
46
+ return {
47
+ id: "node",
48
+ label: "Node.js",
49
+ status: "ok",
50
+ message: nodeVersion
51
+ };
52
+ }
53
+ return {
54
+ id: "node",
55
+ label: "Node.js",
56
+ status: "error",
57
+ message: `${nodeVersion} is not supported`,
58
+ fix: "Install Node.js >=20"
59
+ };
60
+ }
61
+ function checkCommand(name, commandExists, fix, optional = false) {
62
+ if (commandExists(name)) {
63
+ return {
64
+ id: name,
65
+ label: name,
66
+ status: "ok",
67
+ message: "found on PATH"
68
+ };
69
+ }
70
+ return {
71
+ id: name,
72
+ label: name,
73
+ status: optional ? "warning" : "error",
74
+ message: "not found on PATH",
75
+ fix
76
+ };
77
+ }
78
+ function checkInventory(path) {
79
+ if (!existsSync(path)) {
80
+ return {
81
+ id: "inventory",
82
+ label: "Inventory",
83
+ status: "error",
84
+ message: `not found at ${path}`,
85
+ fix: `Create ${path} or pass --config /path/to/hosts.yaml`
86
+ };
87
+ }
88
+ try {
89
+ const inventory = loadInventory(path);
90
+ return {
91
+ id: "inventory",
92
+ label: "Inventory",
93
+ status: "ok",
94
+ message: `${inventory.hosts.length} hosts loaded from ${path}`
95
+ };
96
+ }
97
+ catch (error) {
98
+ return {
99
+ id: "inventory",
100
+ label: "Inventory",
101
+ status: "error",
102
+ message: error instanceof Error ? error.message : String(error)
103
+ };
104
+ }
105
+ }
106
+ function checkSecrets(path) {
107
+ if (!existsSync(path)) {
108
+ return {
109
+ id: "secrets",
110
+ label: "Secrets file",
111
+ status: "warning",
112
+ message: `not found at ${path}; this is fine for key-based hosts`
113
+ };
114
+ }
115
+ return {
116
+ id: "secrets",
117
+ label: "Secrets file",
118
+ status: "ok",
119
+ message: `found at ${path}`
120
+ };
121
+ }
122
+ function checkFilePermissions(id, label, path, fix, optional) {
123
+ if (!existsSync(path)) {
124
+ return {
125
+ id,
126
+ label,
127
+ status: optional ? "warning" : "error",
128
+ message: `cannot check permissions; file does not exist`,
129
+ fix: optional ? undefined : fix
130
+ };
131
+ }
132
+ const mode = statSync(path).mode & 0o777;
133
+ if (mode === 0o600 || mode === 0o400) {
134
+ return {
135
+ id,
136
+ label,
137
+ status: "ok",
138
+ message: mode.toString(8)
139
+ };
140
+ }
141
+ return {
142
+ id,
143
+ label,
144
+ status: "error",
145
+ message: `mode ${mode.toString(8)} is too permissive`,
146
+ fix
147
+ };
148
+ }
149
+ function commandExistsOnPath(name, env) {
150
+ const path = env.PATH ?? "";
151
+ const extensions = process.platform === "win32" ? [".exe", ".cmd", ".bat", ""] : [""];
152
+ for (const dir of path.split(delimiter)) {
153
+ if (!dir)
154
+ continue;
155
+ for (const extension of extensions) {
156
+ if (existsSync(`${dir}/${name}${extension}`))
157
+ return true;
158
+ }
159
+ }
160
+ return false;
161
+ }
162
+ function expandHome(path, env) {
163
+ if (path === "~")
164
+ return env.HOME ?? path;
165
+ if (path.startsWith("~/"))
166
+ return `${env.HOME ?? ""}${path.slice(1)}`;
167
+ return path;
168
+ }
169
+ //# sourceMappingURL=doctor.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"doctor.js","sourceRoot":"","sources":["../src/doctor.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,49 @@
1
+ import type { Host } from "./types.js";
2
+ export type SpawnedForwardProcess = {
3
+ pid?: number;
4
+ kill: (signal?: NodeJS.Signals) => unknown;
5
+ on: (event: string, listener: (...args: unknown[]) => void) => unknown;
6
+ };
7
+ type ForwardManagerOptions = {
8
+ controlDir: string;
9
+ maxForwards?: number;
10
+ ttlSeconds?: number;
11
+ env?: NodeJS.ProcessEnv;
12
+ spawnProcess?: (file: string, args: string[], env?: NodeJS.ProcessEnv) => SpawnedForwardProcess;
13
+ };
14
+ type ForwardRecord = {
15
+ forwardId: string;
16
+ hostId: string;
17
+ pid?: number;
18
+ localHost: string;
19
+ localPort: number;
20
+ remoteHost: string;
21
+ remotePort: number;
22
+ state: "starting" | "running" | "exited" | "error";
23
+ startedAt: string;
24
+ expiresAt: string;
25
+ argv: string[];
26
+ process: SpawnedForwardProcess;
27
+ timer?: NodeJS.Timeout;
28
+ };
29
+ export type ForwardInfo = Omit<ForwardRecord, "process">;
30
+ export declare class ForwardManager {
31
+ private readonly options;
32
+ private readonly forwards;
33
+ private readonly maxForwards;
34
+ private readonly env;
35
+ private readonly spawnProcess;
36
+ constructor(options: ForwardManagerOptions);
37
+ start(input: {
38
+ host: Host;
39
+ localHost?: string;
40
+ localPort: number;
41
+ remoteHost: string;
42
+ remotePort: number;
43
+ }): ForwardInfo;
44
+ stop(forwardId: string): ForwardInfo;
45
+ list(): ForwardInfo[];
46
+ private toInfo;
47
+ private cleanupExpired;
48
+ }
49
+ export {};
@@ -0,0 +1,141 @@
1
+ import { spawn } from "node:child_process";
2
+ import { randomUUID } from "node:crypto";
3
+ import { wrapWithPasswordAuth } from "./auth.js";
4
+ import { buildSshArgs } from "./sshArgs.js";
5
+ export class ForwardManager {
6
+ options;
7
+ forwards = new Map();
8
+ maxForwards;
9
+ env;
10
+ spawnProcess;
11
+ constructor(options) {
12
+ this.options = options;
13
+ this.maxForwards = options.maxForwards ?? 8;
14
+ this.env = options.env ?? process.env;
15
+ this.spawnProcess =
16
+ options.spawnProcess ??
17
+ ((file, args, env) => spawn(file, args, {
18
+ env,
19
+ shell: false,
20
+ stdio: ["ignore", "ignore", "pipe"]
21
+ }));
22
+ }
23
+ start(input) {
24
+ this.cleanupExpired();
25
+ if (this.forwards.size >= this.maxForwards) {
26
+ throw new Error(`Maximum active forwards reached: ${this.maxForwards}`);
27
+ }
28
+ validatePort(input.localPort, "localPort");
29
+ validatePort(input.remotePort, "remotePort");
30
+ validateForwardHost(input.localHost ?? "127.0.0.1", "localHost");
31
+ validateForwardHost(input.remoteHost, "remoteHost");
32
+ const base = buildSshArgs(input.host, {
33
+ controlDir: this.options.controlDir,
34
+ batchMode: true
35
+ });
36
+ const target = base.pop();
37
+ const separator = base.pop();
38
+ if (!target || separator !== "--")
39
+ throw new Error("Unable to build ssh target for forward");
40
+ const localHost = input.localHost ?? "127.0.0.1";
41
+ const spec = `${localHost}:${input.localPort}:${input.remoteHost}:${input.remotePort}`;
42
+ const argv = [...base, "-o", "ExitOnForwardFailure=yes", "-N", "-L", spec, "--", target];
43
+ const commandSpec = wrapWithPasswordAuth(input.host, "ssh", argv, this.env);
44
+ const child = this.spawnProcess(commandSpec.file, commandSpec.args, commandSpec.env);
45
+ const ttlSeconds = this.options.ttlSeconds ?? 60 * 60;
46
+ const expiresAt = new Date(Date.now() + ttlSeconds * 1000).toISOString();
47
+ const record = {
48
+ forwardId: randomUUID(),
49
+ hostId: input.host.id,
50
+ pid: child.pid,
51
+ localHost,
52
+ localPort: input.localPort,
53
+ remoteHost: input.remoteHost,
54
+ remotePort: input.remotePort,
55
+ state: "starting",
56
+ startedAt: new Date().toISOString(),
57
+ expiresAt,
58
+ argv: sanitizeForwardArgv(commandSpec.args),
59
+ process: child
60
+ };
61
+ record.timer = setTimeout(() => {
62
+ this.stop(record.forwardId);
63
+ }, ttlSeconds * 1000);
64
+ record.timer.unref();
65
+ setTimeout(() => {
66
+ if (record.state === "starting")
67
+ record.state = "running";
68
+ }, 500).unref();
69
+ child.on("close", () => {
70
+ record.state = "exited";
71
+ });
72
+ child.on("error", () => {
73
+ record.state = "error";
74
+ });
75
+ this.forwards.set(record.forwardId, record);
76
+ return this.toInfo(record);
77
+ }
78
+ stop(forwardId) {
79
+ const record = this.forwards.get(forwardId);
80
+ if (!record)
81
+ throw new Error(`Forward not found: ${forwardId}`);
82
+ if (record.timer)
83
+ clearTimeout(record.timer);
84
+ record.process.kill("SIGTERM");
85
+ forceKillLater(record.process);
86
+ record.state = "exited";
87
+ this.forwards.delete(forwardId);
88
+ return this.toInfo(record);
89
+ }
90
+ list() {
91
+ this.cleanupExpired();
92
+ return [...this.forwards.values()].map((record) => this.toInfo(record));
93
+ }
94
+ toInfo(record) {
95
+ const { process: _process, timer: _timer, ...info } = record;
96
+ return info;
97
+ }
98
+ cleanupExpired() {
99
+ const now = Date.now();
100
+ for (const [forwardId, record] of this.forwards) {
101
+ if (Date.parse(record.expiresAt) <= now || record.state === "exited" || record.state === "error") {
102
+ if (record.timer)
103
+ clearTimeout(record.timer);
104
+ record.process.kill("SIGTERM");
105
+ forceKillLater(record.process);
106
+ this.forwards.delete(forwardId);
107
+ }
108
+ }
109
+ }
110
+ }
111
+ function forceKillLater(process) {
112
+ setTimeout(() => {
113
+ process.kill("SIGKILL");
114
+ }, 1000).unref();
115
+ }
116
+ function validatePort(value, label) {
117
+ if (!Number.isInteger(value) || value < 1 || value > 65535) {
118
+ throw new Error(`Invalid ${label}: ${value}`);
119
+ }
120
+ }
121
+ function validateForwardHost(value, label) {
122
+ if (value.startsWith("-")) {
123
+ throw new Error(`Invalid ${label}: leading dash is not allowed`);
124
+ }
125
+ if (!/^[A-Za-z0-9._:-]+$/.test(value)) {
126
+ throw new Error(`Invalid ${label}: ${value}`);
127
+ }
128
+ }
129
+ function sanitizeForwardArgv(argv) {
130
+ return argv.map((arg, index) => {
131
+ const previous = argv[index - 1];
132
+ if (previous === "-i")
133
+ return "[REDACTED_IDENTITY_FILE]";
134
+ if (arg.startsWith("ControlPath="))
135
+ return "ControlPath=[REDACTED]";
136
+ if (arg.includes("ControlPath="))
137
+ return arg.replace(/ControlPath=[^\s]+/g, "ControlPath=[REDACTED]");
138
+ return arg;
139
+ });
140
+ }
141
+ //# sourceMappingURL=forwardManager.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"forwardManager.js","sourceRoot":"","sources":["../src/forwardManager.ts"],"names":[],"mappings":"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"}