mcp-ssh-pty 1.2.0 → 1.2.2

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.
@@ -3,6 +3,7 @@ import { ShellResult, ShellConfig } from "./types.js";
3
3
  export declare class ShellManager {
4
4
  private shell;
5
5
  private localProcess;
6
+ private ptyProcess;
6
7
  private outputBuffer;
7
8
  private outputLines;
8
9
  private lastOutputTime;
@@ -14,7 +15,7 @@ export declare class ShellManager {
14
15
  */
15
16
  open(client: Client): Promise<void>;
16
17
  /**
17
- * 打开本地 Shell
18
+ * 打开本地 Shell(使用 node-pty 创建真正的 PTY)
18
19
  */
19
20
  openLocal(): Promise<void>;
20
21
  /**
@@ -1,4 +1,4 @@
1
- import { spawn } from "child_process";
1
+ import * as pty from "node-pty";
2
2
  const DEFAULT_CONFIG = {
3
3
  quickTimeout: 2000,
4
4
  maxTimeout: 5000,
@@ -8,6 +8,7 @@ const DEFAULT_CONFIG = {
8
8
  export class ShellManager {
9
9
  shell = null;
10
10
  localProcess = null;
11
+ ptyProcess = null;
11
12
  outputBuffer = "";
12
13
  outputLines = [];
13
14
  lastOutputTime = 0;
@@ -37,42 +38,39 @@ export class ShellManager {
37
38
  });
38
39
  }
39
40
  /**
40
- * 打开本地 Shell
41
+ * 打开本地 Shell(使用 node-pty 创建真正的 PTY)
41
42
  */
42
43
  async openLocal() {
43
44
  return new Promise((resolve, reject) => {
44
45
  try {
45
46
  // 检测系统默认 shell
46
- const shellPath = process.env.SHELL || (process.platform === "win32" ? "cmd.exe" : "/bin/sh");
47
- const proc = spawn(shellPath, [], {
48
- stdio: ["pipe", "pipe", "pipe"],
49
- env: { ...process.env, TERM: "xterm-256color" },
47
+ const shellPath = process.env.SHELL || (process.platform === "win32" ? "powershell.exe" : "/bin/sh");
48
+ // 使用 node-pty 创建真正的 PTY
49
+ const ptyProc = pty.spawn(shellPath, [], {
50
+ name: "xterm-256color",
51
+ cols: 120,
52
+ rows: 40,
53
+ cwd: process.cwd(),
54
+ env: process.env,
50
55
  });
51
- this.localProcess = proc;
56
+ this.ptyProcess = ptyProc;
52
57
  this.isLocal = true;
53
58
  // 创建统一的 stream 接口
54
59
  const dataListeners = [];
55
60
  const closeListeners = [];
56
61
  const errorListeners = [];
57
- proc.stdout?.on("data", (data) => {
58
- dataListeners.forEach((l) => l(data));
62
+ ptyProc.onData((data) => {
63
+ dataListeners.forEach((l) => l(Buffer.from(data)));
59
64
  });
60
- proc.stderr?.on("data", (data) => {
61
- dataListeners.forEach((l) => l(data));
62
- });
63
- proc.on("close", () => {
65
+ ptyProc.onExit(() => {
64
66
  closeListeners.forEach((l) => l());
65
67
  });
66
- proc.on("error", (err) => {
67
- errorListeners.forEach((l) => l(err));
68
- });
69
68
  const stream = {
70
69
  write: (data) => {
71
- proc.stdin?.write(data);
70
+ ptyProc.write(data);
72
71
  },
73
72
  end: () => {
74
- proc.stdin?.end();
75
- proc.kill();
73
+ ptyProc.kill();
76
74
  },
77
75
  on: ((event, listener) => {
78
76
  if (event === "data") {
@@ -88,10 +86,10 @@ export class ShellManager {
88
86
  };
89
87
  this.shell = stream;
90
88
  this.setupStream(stream);
91
- // 等待初始化
89
+ // 等待初始提示符
92
90
  setTimeout(() => {
93
91
  resolve();
94
- }, 300);
92
+ }, 500);
95
93
  }
96
94
  catch (error) {
97
95
  reject(new Error(`无法打开本地 shell: ${error instanceof Error ? error.message : String(error)}`));
@@ -124,6 +122,7 @@ export class ShellManager {
124
122
  stream.on("close", () => {
125
123
  this.shell = null;
126
124
  this.localProcess = null;
125
+ this.ptyProcess = null;
127
126
  });
128
127
  stream.on("error", (err) => {
129
128
  console.error("Shell error:", err.message);
@@ -223,6 +222,10 @@ export class ShellManager {
223
222
  this.localProcess.kill();
224
223
  this.localProcess = null;
225
224
  }
225
+ if (this.ptyProcess) {
226
+ this.ptyProcess.kill();
227
+ this.ptyProcess = null;
228
+ }
226
229
  this.outputLines = [];
227
230
  this.outputBuffer = "";
228
231
  this.isLocal = false;
@@ -248,8 +251,14 @@ export class ShellManager {
248
251
  const newLines = this.outputLines.slice(startLineCount);
249
252
  const currentOutput = newLines.join("\n") +
250
253
  (this.outputBuffer ? "\n" + this.outputBuffer : "");
251
- const lastLine = this.outputBuffer ||
254
+ // 获取最后一行用于提示符检测
255
+ // 处理 \r(回车):取最后一个 \r 后面的内容,因为那才是当前可见的行
256
+ let lastLine = this.outputBuffer ||
252
257
  (newLines.length > 0 ? newLines[newLines.length - 1] : "");
258
+ const lastCR = lastLine.lastIndexOf("\r");
259
+ if (lastCR !== -1) {
260
+ lastLine = lastLine.slice(lastCR + 1);
261
+ }
253
262
  const hasPrompt = this.detectPrompt(lastLine);
254
263
  // 检测输出是否稳定(连续 3 次检查没有新输出)
255
264
  if (this.lastOutputTime <= lastCheckTime) {
@@ -296,8 +305,13 @@ export class ShellManager {
296
305
  * 私有方法:检测提示符
297
306
  */
298
307
  detectPrompt(line) {
299
- // 移除 ANSI 转义序列
300
- const cleanLine = line.replace(/\x1b\[[0-9;]*m/g, "").trim();
308
+ // 移除 ANSI 转义序列(更完整的正则)
309
+ const cleanLine = line
310
+ .replace(/\x1b\[[0-9;]*[a-zA-Z]/g, "") // 标准 ANSI 序列
311
+ .replace(/\x1b\][^\x07]*\x07/g, "") // OSC 序列 (如 \e]2;...\a)
312
+ .replace(/\x1b\][^\x1b]*\x1b\\/g, "") // OSC 序列 (如 \e]7;...\e\)
313
+ .replace(/[\x00-\x1f]/g, "") // 其他控制字符
314
+ .trim();
301
315
  if (!cleanLine)
302
316
  return false;
303
317
  // 常见提示符模式
@@ -310,6 +324,10 @@ export class ShellManager {
310
324
  /\)\s*[$#>]\s*$/, // )$ 或 )# 结尾(一些自定义 PS1)
311
325
  /~\s*[$#>]\s*$/, // ~$ 结尾
312
326
  /@.*:\s*[$#>]\s*$/, // user@host: $ 格式
327
+ /^➜\s+/, // oh-my-zsh robbyrussell 主题 (➜ 开头)
328
+ /❯\s*$/, // pure/starship 主题
329
+ /λ\s*$/, // lambda 主题
330
+ /^\s*%\s*$/, // zsh 默认提示符
313
331
  ];
314
332
  return patterns.some((p) => p.test(cleanLine));
315
333
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "mcp-ssh-pty",
3
- "version": "1.2.0",
3
+ "version": "1.2.2",
4
4
  "description": "MCP Server for SSH remote command execution with PTY shell support",
5
5
  "type": "module",
6
6
  "main": "dist/index.js",
@@ -33,6 +33,7 @@
33
33
  "@inquirer/prompts": "^8.1.0",
34
34
  "@modelcontextprotocol/sdk": "^1.0.0",
35
35
  "commander": "^14.0.2",
36
+ "node-pty": "^1.1.0",
36
37
  "ssh2": "^1.16.0",
37
38
  "zod": "^3.24.0"
38
39
  },