#
tokens: 10201/50000 8/8 files
lines: on (toggle) GitHub
raw markdown copy reset
# Directory Structure

```
├── .gitignore
├── Dockerfile
├── package.json
├── PROJ_INFO_4AI.md
├── README_CN.md
├── README.md
├── smithery.yaml
├── src
│   ├── executor.ts
│   ├── index.ts
│   └── ssh.ts
└── tsconfig.json
```

# Files

--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------

```markdown
  1 | # Terminal MCP Server
  2 | [![smithery badge](https://smithery.ai/badge/@weidwonder/terminal-mcp-server)](https://smithery.ai/server/@weidwonder/terminal-mcp-server)
  3 | 
  4 | ## Notice 注意事项
  5 | Current Project not in maintance anymore. I recommend you guys to use more advanced command tool —— [Desktop Commander](https://desktopcommander.app/)  
  6 | 当前项目已经不在维护。我建议大家用更先进的终端MCP工具 [Desktop Commander](https://desktopcommander.app/)
  7 | 
  8 | 
  9 | *[中文文档](README_CN.md)*
 10 | 
 11 | Terminal MCP Server is a Model Context Protocol (MCP) server that allows executing commands on local or remote hosts. It provides a simple yet powerful interface for AI models and other applications to execute system commands, either on the local machine or on remote hosts via SSH.
 12 | 
 13 | ## Features
 14 | 
 15 | - **Local Command Execution**: Execute commands directly on the local machine
 16 | - **Remote Command Execution**: Execute commands on remote hosts via SSH
 17 | - **Session Persistence**: Support for persistent sessions that reuse the same terminal environment for a specified time (default 20 minutes)
 18 | - **Environment Variables**: Set custom environment variables for commands
 19 | - **Multiple Connection Methods**: Connect via stdio or SSE (Server-Sent Events)
 20 | 
 21 | ## Installation
 22 | 
 23 | ### Installing via Smithery
 24 | 
 25 | To install terminal-mcp-server for Claude Desktop automatically via [Smithery](https://smithery.ai/server/@weidwonder/terminal-mcp-server):
 26 | 
 27 | ```bash
 28 | npx -y @smithery/cli install @weidwonder/terminal-mcp-server --client claude
 29 | ```
 30 | 
 31 | ### Manual Installation
 32 | ```bash
 33 | # Clone the repository
 34 | git clone https://github.com/weidwonder/terminal-mcp-server.git
 35 | cd terminal-mcp-server
 36 | 
 37 | # Install dependencies
 38 | npm install
 39 | 
 40 | # Build the project
 41 | npm run build
 42 | ```
 43 | 
 44 | ## Usage
 45 | 
 46 | ### Starting the Server
 47 | 
 48 | ```bash
 49 | # Start the server using stdio (default mode)
 50 | npm start
 51 | 
 52 | # Or run the built file directly
 53 | node build/index.js
 54 | ```
 55 | 
 56 | ### Starting the Server in SSE Mode
 57 | 
 58 | The SSE (Server-Sent Events) mode allows you to connect to the server remotely via HTTP.
 59 | 
 60 | ```bash
 61 | # Start the server in SSE mode
 62 | npm run start:sse
 63 | 
 64 | # Or run the built file directly with SSE flag
 65 | node build/index.js --sse
 66 | ```
 67 | 
 68 | You can customize the SSE server with the following command-line options:
 69 | 
 70 | | Option | Description | Default |
 71 | |--------|-------------|---------|
 72 | | `--port` or `-p` | The port to listen on | 8080 |
 73 | | `--endpoint` or `-e` | The endpoint path | /sse |
 74 | | `--host` or `-h` | The host to bind to | localhost |
 75 | 
 76 | Example with custom options:
 77 | 
 78 | ```bash
 79 | # Start SSE server on port 3000, endpoint /mcp, and bind to all interfaces
 80 | node build/index.js --sse --port 3000 --endpoint /mcp --host 0.0.0.0
 81 | ```
 82 | 
 83 | This will start the server and listen for SSE connections at `http://0.0.0.0:3000/mcp`.
 84 | 
 85 | ### Testing with MCP Inspector
 86 | 
 87 | ```bash
 88 | # Start the MCP Inspector tool
 89 | npm run inspector
 90 | ```
 91 | 
 92 | ## The execute_command Tool
 93 | 
 94 | The execute_command tool is the core functionality provided by Terminal MCP Server, used to execute commands on local or remote hosts.
 95 | 
 96 | ### Parameters
 97 | 
 98 | | Parameter | Type | Required | Description |
 99 | |-----------|------|----------|-------------|
100 | | command | string | Yes | The command to execute |
101 | | host | string | No | The remote host to connect to. If not provided, the command will be executed locally |
102 | | username | string | Required when host is specified | The username for SSH connection |
103 | | session | string | No | Session name, defaults to "default". The same session name will reuse the same terminal environment for 20 minutes |
104 | | env | object | No | Environment variables, defaults to an empty object |
105 | 
106 | ### Examples
107 | 
108 | #### Executing a Command Locally
109 | 
110 | ```json
111 | {
112 |   "command": "ls -la",
113 |   "session": "my-local-session",
114 |   "env": {
115 |     "NODE_ENV": "development"
116 |   }
117 | }
118 | ```
119 | 
120 | #### Executing a Command on a Remote Host
121 | 
122 | ```json
123 | {
124 |   "host": "example.com",
125 |   "username": "user",
126 |   "command": "ls -la",
127 |   "session": "my-remote-session",
128 |   "env": {
129 |     "NODE_ENV": "production"
130 |   }
131 | }
132 | ```
133 | 
134 | ## Configuring with AI Assistants
135 | 
136 | ### Configuring with Roo Code
137 | 
138 | 1. Open VSCode and install the Roo Code extension
139 | 2. Open the Roo Code settings file: `~/Library/Application Support/Code/User/globalStorage/rooveterinaryinc.roo-cline/settings/cline_mcp_settings.json`
140 | 3. Add the following configuration:
141 | 
142 | #### For stdio mode (local connection)
143 | 
144 | ```json
145 | {
146 |   "mcpServers": {
147 |     "terminal-mcp": {
148 |       "command": "node",
149 |       "args": ["/path/to/terminal-mcp-server/build/index.js"],
150 |       "env": {}
151 |     }
152 |   }
153 | }
154 | ```
155 | 
156 | #### For SSE mode (remote connection)
157 | 
158 | ```json
159 | {
160 |   "mcpServers": {
161 |     "terminal-mcp-sse": {
162 |       "url": "http://localhost:8080/sse",
163 |       "headers": {}
164 |     }
165 |   }
166 | }
167 | ```
168 | 
169 | Replace `localhost:8080/sse` with your actual server address, port, and endpoint if you've customized them.
170 | 
171 | ### Configuring with Cline
172 | 
173 | 1. Open the Cline settings file: `~/.cline/config.json`
174 | 2. Add the following configuration:
175 | 
176 | #### For stdio mode (local connection)
177 | 
178 | ```json
179 | {
180 |   "mcpServers": {
181 |     "terminal-mcp": {
182 |       "command": "node",
183 |       "args": ["/path/to/terminal-mcp-server/build/index.js"],
184 |       "env": {}
185 |     }
186 |   }
187 | }
188 | ```
189 | 
190 | #### For SSE mode (remote connection)
191 | 
192 | ```json
193 | {
194 |   "mcpServers": {
195 |     "terminal-mcp-sse": {
196 |       "url": "http://localhost:8080/sse",
197 |       "headers": {}
198 |     }
199 |   }
200 | }
201 | ```
202 | 
203 | ### Configuring with Claude Desktop
204 | 
205 | 1. Open the Claude Desktop settings file: `~/Library/Application Support/Claude/claude_desktop_config.json`
206 | 2. Add the following configuration:
207 | 
208 | #### For stdio mode (local connection)
209 | 
210 | ```json
211 | {
212 |   "mcpServers": {
213 |     "terminal-mcp": {
214 |       "command": "node",
215 |       "args": ["/path/to/terminal-mcp-server/build/index.js"],
216 |       "env": {}
217 |     }
218 |   }
219 | }
220 | ```
221 | 
222 | #### For SSE mode (remote connection)
223 | 
224 | ```json
225 | {
226 |   "mcpServers": {
227 |     "terminal-mcp-sse": {
228 |       "url": "http://localhost:8080/sse",
229 |       "headers": {}
230 |     }
231 |   }
232 | }
233 | ```
234 | 
235 | ## Best Practices
236 | 
237 | ### Command Execution
238 | 
239 | - Before running commands, it's best to determine the system type (Mac, Linux, etc.)
240 | - Use full paths to avoid path-related issues
241 | - For command sequences that need to maintain environment, use `&&` to connect multiple commands
242 | - For long-running commands, consider using `nohup` or `screen`/`tmux`
243 | 
244 | ### SSH Connection
245 | 
246 | - Ensure SSH key-based authentication is set up
247 | - If connection fails, check if the key file exists (default path: `~/.ssh/id_rsa`)
248 | - Make sure the SSH service is running on the remote host
249 | 
250 | ### Session Management
251 | 
252 | - Use the session parameter to maintain environment between related commands
253 | - For operations requiring specific environments, use the same session name
254 | - Note that sessions will automatically close after 20 minutes of inactivity
255 | 
256 | ### Error Handling
257 | 
258 | - Command execution results include both stdout and stderr
259 | - Check stderr to determine if the command executed successfully
260 | - For complex operations, add verification steps to ensure success
261 | 
262 | ## Important Notes
263 | 
264 | - For remote command execution, SSH key-based authentication must be set up in advance
265 | - For local command execution, commands will run in the context of the user who started the server
266 | - Session timeout is 20 minutes, after which the connection will be automatically closed
267 | 
```

--------------------------------------------------------------------------------
/tsconfig.json:
--------------------------------------------------------------------------------

```json
 1 | {
 2 |   "compilerOptions": {
 3 |     "target": "ES2022",
 4 |     "module": "Node16",
 5 |     "moduleResolution": "Node16",
 6 |     "outDir": "./build",
 7 |     "rootDir": "./src",
 8 |     "strict": true,
 9 |     "esModuleInterop": true,
10 |     "skipLibCheck": true,
11 |     "forceConsistentCasingInFileNames": true
12 |   },
13 |   "include": ["src/**/*"],
14 |   "exclude": ["node_modules"]
15 | }
16 | 
```

--------------------------------------------------------------------------------
/Dockerfile:
--------------------------------------------------------------------------------

```dockerfile
 1 | # Generated by https://smithery.ai. See: https://smithery.ai/docs/config#dockerfile
 2 | FROM node:lts-alpine
 3 | 
 4 | # Create app directory
 5 | WORKDIR /app
 6 | 
 7 | # Install app dependencies
 8 | # A wildcard is used to ensure both package.json and package-lock.json are copied
 9 | COPY package*.json ./
10 | 
11 | # Install dependencies (skip prepare script to avoid build issues, we'll explicitly run build step)
12 | RUN npm install --ignore-scripts
13 | 
14 | # Copy app source code
15 | COPY . .
16 | 
17 | # Build the application
18 | RUN npm run build
19 | 
20 | # Expose port 8080 for SSE mode (if used)
21 | EXPOSE 8080
22 | 
23 | # Command to run the application
24 | CMD ["node", "build/index.js"]
25 | 
```

--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------

```json
 1 | {
 2 |   "name": "terminal-mcp-server",
 3 |   "version": "0.1.0",
 4 |   "description": "操作X主机",
 5 |   "private": true,
 6 |   "type": "module",
 7 |   "bin": {
 8 |     "terminal-mcp-server": "./build/index.js"
 9 |   },
10 |   "files": [
11 |     "build"
12 |   ],
13 |   "scripts": {
14 |     "build": "tsc && node -e \"require('fs').chmodSync('build/index.js', '755')\"",
15 |     "prepare": "npm run build",
16 |     "watch": "tsc --watch",
17 |     "inspector": "npx @modelcontextprotocol/inspector build/index.js",
18 |     "start": "node build/index.js"
19 |   },
20 |   "dependencies": {
21 |     "@modelcontextprotocol/sdk": "^1.6.0",
22 |     "ssh2": "^1.16.0"
23 |   },
24 |   "devDependencies": {
25 |     "@types/node": "^20.11.24",
26 |     "@types/ssh2": "^1.15.4",
27 |     "typescript": "^5.3.3"
28 |   }
29 | }
30 | 
```

--------------------------------------------------------------------------------
/smithery.yaml:
--------------------------------------------------------------------------------

```yaml
 1 | # Smithery configuration file: https://smithery.ai/docs/config#smitheryyaml
 2 | 
 3 | startCommand:
 4 |   type: stdio
 5 |   configSchema:
 6 |     # JSON Schema defining the configuration options for the MCP.
 7 |     type: object
 8 |     properties:
 9 |       sse:
10 |         type: boolean
11 |         default: false
12 |         description: If true, run the server in SSE mode instead of default stdio mode.
13 |       port:
14 |         type: number
15 |         default: 8080
16 |         description: Port to listen on when running in SSE mode.
17 |       endpoint:
18 |         type: string
19 |         default: /sse
20 |         description: Endpoint path for SSE connections.
21 |       host:
22 |         type: string
23 |         default: localhost
24 |         description: Host to bind to when running in SSE mode.
25 |   commandFunction:
26 |     # A JS function that produces the CLI command based on the given config to start the MCP on stdio.
27 |     |-
28 |     (config) => { const args = []; if(config.sse){ args.push('--sse'); if(config.port) { args.push('--port', config.port.toString()); } if(config.endpoint){ args.push('--endpoint', config.endpoint); } if(config.host){ args.push('--host', config.host); } } return { command: 'node', args: ['build/index.js', ...args] }; }
29 |   exampleConfig:
30 |     sse: true
31 |     port: 3000
32 |     endpoint: /mcp
33 |     host: 0.0.0.0
34 | 
```

--------------------------------------------------------------------------------
/src/ssh.ts:
--------------------------------------------------------------------------------

```typescript
  1 | import { Client } from 'ssh2';
  2 | import * as fs from 'fs';
  3 | import * as path from 'path';
  4 | import * as os from 'os';
  5 | 
  6 | export class SSHManager {
  7 |   private client: Client | null = null;
  8 |   private connection: Promise<void> | null = null;
  9 |   private timeout: NodeJS.Timeout | null = null;
 10 |   private sessionTimeout: number = 20 * 60 * 1000; // 20 minutes
 11 | 
 12 |   constructor() {
 13 |     this.client = new Client();
 14 |   }
 15 | 
 16 |   async connect(host: string): Promise<void> {
 17 |     if (this.connection) {
 18 |       return this.connection;
 19 |     }
 20 | 
 21 |     const privateKey = fs.readFileSync(path.join(os.homedir(), '.ssh', 'id_rsa'));
 22 | 
 23 |     this.connection = new Promise((resolve, reject) => {
 24 |       this.client = new Client();
 25 |       this.client
 26 |         .on('ready', () => {
 27 |           this.resetTimeout();
 28 |           resolve();
 29 |         })
 30 |         .on('error', (err) => {
 31 |           reject(err);
 32 |         })
 33 |         .connect({
 34 |           host: host,
 35 |           username: 'weidwonder',
 36 |           privateKey: privateKey,
 37 |         });
 38 |     });
 39 | 
 40 |     return this.connection;
 41 |   }
 42 | 
 43 |   private resetTimeout(): void {
 44 |     if (this.timeout) {
 45 |       clearTimeout(this.timeout);
 46 |     }
 47 | 
 48 |     this.timeout = setTimeout(async () => {
 49 |       console.log('SSH session timeout, disconnecting');
 50 |       await this.disconnect();
 51 |     }, this.sessionTimeout);
 52 |   }
 53 | 
 54 |   async executeCommand(host: string, command: string, env: Record<string, string> | {} = {}): Promise<{stdout: string; stderr: string}> {
 55 |     if (!this.client) {
 56 |       await this.connect(host);
 57 |     }
 58 |     this.resetTimeout();
 59 | 
 60 |     return new Promise((resolve, reject) => {
 61 |       // 使用完整的shell路径,以交互式登录模式执行命令
 62 |       // 这更接近于用户直接SSH登录的体验
 63 |       
 64 |       // 构建环境变量设置命令
 65 |       const envSetup = Object.entries(env as Record<string, string>)
 66 |         .map(([key, value]) => `export ${key}="${String(value).replace(/"/g, '\\"')}"`)
 67 |         .join(' && ');
 68 |       
 69 |       // 如果有环境变量,先设置环境变量,再执行命令
 70 |       const fullCommand = envSetup ? `${envSetup} && ${command}` : command;
 71 |       
 72 |       this.client?.exec(`/bin/bash --login -c "${fullCommand}"`, (err, stream) => {
 73 |         if (err) {
 74 |           reject(err);
 75 |           return
 76 |         }
 77 | 
 78 |         let stdout = ""
 79 |         let stderr = '';
 80 | 
 81 |         stream
 82 |           .on("data", (data: Buffer) => {
 83 |             this.resetTimeout();
 84 |             stdout += data.toString();
 85 |           })
 86 |           .stderr.on('data', (data: Buffer) => {
 87 |             stderr += data.toString();
 88 |           })
 89 |           .on('close', () => {
 90 |             resolve({ stdout, stderr });
 91 |           })
 92 |           .on('error', (err) => {
 93 |             reject(err);
 94 |           });
 95 |       });
 96 |     });
 97 |   }
 98 | 
 99 |   async disconnect(): Promise<void> {
100 |     if (this.client) {
101 |       this.client.end();
102 |       this.client = null;
103 |       this.connection = null;
104 |     }
105 |   }
106 | }
```

--------------------------------------------------------------------------------
/src/index.ts:
--------------------------------------------------------------------------------

```typescript
  1 | #!/usr/bin/env node
  2 | 
  3 | import { Server } from "@modelcontextprotocol/sdk/server/index.js";
  4 | import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
  5 | import {
  6 |   CallToolRequestSchema,
  7 |   ListToolsRequestSchema,
  8 |   ErrorCode,
  9 |   McpError,
 10 | } from "@modelcontextprotocol/sdk/types.js";
 11 | import { CommandExecutor } from "./executor.js";
 12 | 
 13 | // 全局日志函数,确保所有日志都通过stderr输出
 14 | export const log = {
 15 |   debug: (message: string, ...args: any[]) => {
 16 |     if (process.env.DEBUG === 'true') {
 17 |       console.error(`[DEBUG] ${message}`, ...args);
 18 |     }
 19 |   },
 20 |   info: (message: string, ...args: any[]) => {
 21 |     console.error(`[INFO] ${message}`, ...args);
 22 |   },
 23 |   warn: (message: string, ...args: any[]) => {
 24 |     console.error(`[WARN] ${message}`, ...args);
 25 |   },
 26 |   error: (message: string, ...args: any[]) => {
 27 |     console.error(`[ERROR] ${message}`, ...args);
 28 |   }
 29 | };
 30 | 
 31 | const commandExecutor = new CommandExecutor();
 32 | 
 33 | // 创建服务器
 34 | function createServer() {
 35 |   const server = new Server(
 36 |     {
 37 |       name: "remote-ops-server",
 38 |       version: "0.1.0",
 39 |     },
 40 |     {
 41 |       capabilities: {
 42 |         tools: {},
 43 |       },
 44 |     }
 45 |   );
 46 | 
 47 |   server.setRequestHandler(ListToolsRequestSchema, async () => {
 48 |     return {
 49 |       tools: [
 50 |         {
 51 |           name: "execute_command",
 52 |           description: "Execute commands on remote hosts or locally (This tool can be used for both remote hosts and the current machine)",
 53 |           inputSchema: {
 54 |             type: "object",
 55 |             properties: {
 56 |               host: {
 57 |                 type: "string",
 58 |                 description: "Host to connect to (optional, if not provided the command will be executed locally)"
 59 |               },
 60 |               username: {
 61 |                 type: "string",
 62 |                 description: "Username for SSH connection (required when host is specified)"
 63 |               },
 64 |               session: {
 65 |                 type: "string",
 66 |                 description: "Session name, defaults to 'default'. The same session name will reuse the same terminal environment for 20 minutes, which is useful for operations requiring specific environments like conda.",
 67 |                 default: "default"
 68 |               },
 69 |               command: {
 70 |                 type: "string",
 71 |                 description: "Command to execute. Before running commands, it's best to determine the system type (Mac, Linux, etc.)"
 72 |               },
 73 |               env: {
 74 |                 type: "object",
 75 |                 description: "Environment variables",
 76 |                 default: {}
 77 |               }
 78 |             },
 79 |             required: ["command"]
 80 |           }
 81 |         }
 82 |       ]
 83 |     };
 84 |   });
 85 | 
 86 |   server.setRequestHandler(CallToolRequestSchema, async (request) => {
 87 |     try {
 88 |       if (request.params.name !== "execute_command") {
 89 |         throw new McpError(ErrorCode.MethodNotFound, "Unknown tool");
 90 |       }
 91 |       
 92 |       const host = request.params.arguments?.host ? String(request.params.arguments.host) : undefined;
 93 |       const username = request.params.arguments?.username ? String(request.params.arguments.username) : undefined;
 94 |       const session = String(request.params.arguments?.session || "default");
 95 |       const command = String(request.params.arguments?.command);
 96 |       if (!command) {
 97 |         throw new McpError(ErrorCode.InvalidParams, "Command is required");
 98 |       }
 99 |       const env = request.params.arguments?.env || {};
100 | 
101 |       // 如果指定了host但没有指定username
102 |       if (host && !username) {
103 |         throw new McpError(ErrorCode.InvalidParams, "Username is required when host is specified");
104 |       }
105 | 
106 |       try {
107 |         const result = await commandExecutor.executeCommand(command, {
108 |           host,
109 |           username,
110 |           session,
111 |           env: env as Record<string, string>
112 |         });
113 |         
114 |         return {
115 |           content: [{
116 |             type: "text",
117 |             text: `Command Output:\nstdout: ${result.stdout}\nstderr: ${result.stderr}`
118 |           }]
119 |         };
120 |       } catch (error) {
121 |         if (error instanceof Error && error.message.includes('SSH')) {
122 |           throw new McpError(
123 |             ErrorCode.InternalError,
124 |             `SSH connection error: ${error.message}. Please ensure SSH key-based authentication is set up.`
125 |           );
126 |         }
127 |         throw error;
128 |       }
129 |     } catch (error) {
130 |       if (error instanceof McpError) {
131 |         throw error;
132 |       }
133 |       throw new McpError(
134 |         ErrorCode.InternalError,
135 |         error instanceof Error ? error.message : String(error)
136 |       );
137 |     }
138 |   });
139 | 
140 |   return server;
141 | }
142 | 
143 | async function main() {
144 |   try {
145 |     // 使用标准输入输出
146 |     const server = createServer();
147 |     
148 |     // 设置MCP错误处理程序
149 |     server.onerror = (error) => {
150 |       log.error(`MCP Error: ${error.message}`);
151 |     };
152 |     
153 |     const transport = new StdioServerTransport();
154 |     await server.connect(transport);
155 |     log.info("Remote Ops MCP server running on stdio");
156 | 
157 |     // 处理进程退出
158 |     process.on('SIGINT', async () => {
159 |       log.info("Shutting down server...");
160 |       await commandExecutor.disconnect();
161 |       process.exit(0);
162 |     });
163 |   } catch (error) {
164 |     log.error("Server error:", error);
165 |     process.exit(1);
166 |   }
167 | }
168 | 
169 | main().catch((error) => {
170 |   log.error("Server error:", error);
171 |   process.exit(1);
172 | });
173 | 
```

--------------------------------------------------------------------------------
/src/executor.ts:
--------------------------------------------------------------------------------

```typescript
  1 | import { Client } from 'ssh2';
  2 | import * as fs from 'fs';
  3 | import * as path from 'path';
  4 | import * as os from 'os';
  5 | import { exec } from 'child_process';
  6 | import { log } from './index.js';
  7 | 
  8 | export class CommandExecutor {
  9 |   private sessions: Map<string, {
 10 |     client: Client | null;
 11 |     connection: Promise<void> | null;
 12 |     timeout: NodeJS.Timeout | null;
 13 |     host?: string;
 14 |     env?: Record<string, string>; // 添加环境变量存储
 15 |     shell?: any; // 添加shell会话
 16 |     shellReady?: boolean; // shell是否准备好
 17 |   }> = new Map();
 18 |   
 19 |   private sessionTimeout: number = 20 * 60 * 1000; // 20 minutes
 20 | 
 21 |   constructor() {}
 22 | 
 23 |   private getSessionKey(host: string | undefined, sessionName: string): string {
 24 |     return `${host || 'local'}-${sessionName}`;
 25 |   }
 26 | 
 27 |   async connect(host: string, username: string, sessionName: string = 'default'): Promise<void> {
 28 |     const sessionKey = this.getSessionKey(host, sessionName);
 29 |     const session = this.sessions.get(sessionKey);
 30 |     
 31 |     // 如果会话存在且连接有效,直接返回现有连接
 32 |     if (session?.connection && session?.client) {
 33 |       // 检查客户端是否仍然连接
 34 |       if (session.client.listenerCount('ready') > 0 || session.client.listenerCount('data') > 0) {
 35 |         log.info(`Reusing existing session: ${sessionKey}`);
 36 |         return session.connection;
 37 |       }
 38 |       // 如果客户端已断开连接,清理旧会话
 39 |       log.info(`Session ${sessionKey} disconnected, creating new session`);
 40 |       this.sessions.delete(sessionKey);
 41 |     }
 42 | 
 43 |     try {
 44 |       const privateKey = fs.readFileSync(path.join(os.homedir(), '.ssh', 'id_rsa'));
 45 | 
 46 |       const client = new Client();
 47 |       const connection = new Promise<void>((resolve, reject) => {
 48 |         client
 49 |           .on('ready', () => {
 50 |             log.info(`Session ${sessionKey} connected`);
 51 |             this.resetTimeout(sessionKey);
 52 |             
 53 |             // 创建一个交互式shell
 54 |             client.shell((err, stream) => {
 55 |               if (err) {
 56 |                 log.error(`Failed to create interactive shell: ${err.message}`);
 57 |                 reject(err);
 58 |                 return;
 59 |               }
 60 |               
 61 |               log.info(`Creating interactive shell for session ${sessionKey}`);
 62 |               
 63 |               // 获取会话对象
 64 |               const sessionData = this.sessions.get(sessionKey);
 65 |               if (sessionData) {
 66 |                 // 设置shell和shellReady标志
 67 |                 sessionData.shell = stream;
 68 |                 sessionData.shellReady = true;
 69 |                 
 70 |                 // 更新会话
 71 |                 this.sessions.set(sessionKey, sessionData);
 72 |               }
 73 |               
 74 |               // 处理shell关闭事件
 75 |               stream.on('close', () => {
 76 |                 log.info(`Interactive shell for session ${sessionKey} closed`);
 77 |                 const sessionData = this.sessions.get(sessionKey);
 78 |                 if (sessionData) {
 79 |                   sessionData.shellReady = false;
 80 |                   this.sessions.set(sessionKey, sessionData);
 81 |                 }
 82 |               });
 83 |               
 84 |               // 等待shell准备好
 85 |               stream.write('echo "Shell ready"\n');
 86 |               
 87 |               // 解析promise
 88 |               resolve();
 89 |             });
 90 |           })
 91 |           .on('error', (err) => {
 92 |             log.error(`会话 ${sessionKey} 连接错误:`, err.message);
 93 |             reject(err);
 94 |           })
 95 |           .connect({
 96 |             host: host,
 97 |             username: username,
 98 |             privateKey: privateKey,
 99 |             keepaliveInterval: 60000, // 每分钟发送一次keepalive包
100 |           });
101 |       });
102 | 
103 |       log.info(`Creating new session: ${sessionKey}`);
104 |       this.sessions.set(sessionKey, {
105 |         client,
106 |         connection,
107 |         timeout: null,
108 |         host,
109 |         shell: null,
110 |         shellReady: false
111 |       });
112 | 
113 |       return connection;
114 |     } catch (error) {
115 |       if (error instanceof Error && error.message.includes('ENOENT')) {
116 |         throw new Error('SSH key file does not exist, please ensure SSH key-based authentication is set up');
117 |       }
118 |       throw error;
119 |     }
120 |   }
121 | 
122 |   private resetTimeout(sessionKey: string): void {
123 |     const session = this.sessions.get(sessionKey);
124 |     if (!session) return;
125 | 
126 |     if (session.timeout) {
127 |       clearTimeout(session.timeout);
128 |     }
129 | 
130 |     session.timeout = setTimeout(async () => {
131 |       log.info(`Session ${sessionKey} timeout, disconnecting`);
132 |       await this.disconnectSession(sessionKey);
133 |     }, this.sessionTimeout);
134 | 
135 |     this.sessions.set(sessionKey, session);
136 |   }
137 | 
138 |   async executeCommand(
139 |     command: string,
140 |     options: {
141 |       host?: string;
142 |       username?: string;
143 |       session?: string;
144 |       env?: Record<string, string>;
145 |     } = {}
146 |   ): Promise<{stdout: string; stderr: string}> {
147 |     const { host, username, session = 'default', env = {} } = options;
148 |     const sessionKey = this.getSessionKey(host, session);
149 | 
150 |     // 如果指定了host,则使用SSH执行命令
151 |     if (host) {
152 |       if (!username) {
153 |         throw new Error('Username is required when using SSH');
154 |       }
155 |       
156 |       let sessionData = this.sessions.get(sessionKey);
157 |       
158 |       // 检查会话是否存在且有效
159 |       let needNewConnection = false;
160 |       if (!sessionData || sessionData.host !== host) {
161 |         needNewConnection = true;
162 |       } else if (sessionData.client) {
163 |         // 检查客户端是否仍然连接
164 |         if (sessionData.client.listenerCount('ready') === 0 && sessionData.client.listenerCount('data') === 0) {
165 |           log.info(`Session ${sessionKey} disconnected, reconnecting`);
166 |           needNewConnection = true;
167 |         }
168 |       } else {
169 |         needNewConnection = true;
170 |       }
171 |       
172 |       // 如果需要新连接,则创建
173 |       if (needNewConnection) {
174 |         log.info(`Creating new connection for command execution: ${sessionKey}`);
175 |         await this.connect(host, username, session);
176 |         sessionData = this.sessions.get(sessionKey);
177 |       } else {
178 |         log.info(`Reusing existing session for command execution: ${sessionKey}`);
179 |       }
180 |       
181 |       if (!sessionData || !sessionData.client) {
182 |         throw new Error(`无法创建到 ${host} 的SSH会话`);
183 |       }
184 |       
185 |       this.resetTimeout(sessionKey);
186 | 
187 |       // 检查是否有交互式shell可用
188 |       if (sessionData.shellReady && sessionData.shell) {
189 |         log.info(`Executing command using interactive shell: ${command}`);
190 |         return new Promise((resolve, reject) => {
191 |           let stdout = "";
192 |           let stderr = "";
193 |           let commandFinished = false;
194 |           const uniqueMarker = `CMD_END_${Date.now()}_${Math.random().toString(36).substring(2, 15)}`;
195 |           
196 |           // 构建环境变量设置命令
197 |           const envSetup = Object.entries(env)
198 |             .map(([key, value]) => `export ${key}="${String(value).replace(/"/g, '\\"')}"`)
199 |             .join(' && ');
200 |           
201 |           // 如果有环境变量,先设置环境变量,再执行命令
202 |           const fullCommand = envSetup ? `${envSetup} && ${command}` : command;
203 |           
204 |           // 添加数据处理器
205 |           const dataHandler = (data: Buffer) => {
206 |             const str = data.toString();
207 |             log.debug(`Shell数据: ${str}`);
208 |             
209 |             if (str.includes(uniqueMarker)) {
210 |               // 命令执行完成
211 |               commandFinished = true;
212 |               
213 |               // 提取命令输出(从命令开始到标记之前的内容)
214 |               const lines = stdout.split('\n');
215 |               let commandOutput = '';
216 |               let foundCommand = false;
217 |               
218 |               for (const line of lines) {
219 |                 if (foundCommand) {
220 |                   if (line.includes(uniqueMarker)) {
221 |                     break;
222 |                   }
223 |                   commandOutput += line + '\n';
224 |                 } else if (line.includes(fullCommand)) {
225 |                   foundCommand = true;
226 |                 }
227 |               }
228 |               
229 |               // 解析输出
230 |               resolve({ stdout: commandOutput.trim(), stderr });
231 |               
232 |               // 移除处理器
233 |               sessionData.shell.removeListener('data', dataHandler);
234 |               clearTimeout(timeout);
235 |             } else if (!commandFinished) {
236 |               stdout += str;
237 |             }
238 |           };
239 |           
240 |           // 添加错误处理器
241 |           const errorHandler = (err: Error) => {
242 |             stderr += err.message;
243 |             reject(err);
244 |             sessionData.shell.removeListener('data', dataHandler);
245 |             sessionData.shell.removeListener('error', errorHandler);
246 |           };
247 |           
248 |           // 监听数据和错误
249 |           sessionData.shell.on('data', dataHandler);
250 |           sessionData.shell.on('error', errorHandler);
251 |           
252 |           // 执行命令并添加唯一标记
253 |           // 使用一个更明确的方式来执行命令和捕获输出
254 |           sessionData.shell.write(`echo "Starting command execution: ${fullCommand}"\n`);
255 |           sessionData.shell.write(`${fullCommand}\n`);
256 |           sessionData.shell.write(`echo "${uniqueMarker}"\n`);
257 |           
258 |           // 设置超时
259 |           const timeout = setTimeout(() => {
260 |             if (!commandFinished) {
261 |               stderr += "Command execution timed out";
262 |               resolve({ stdout, stderr });
263 |               sessionData.shell.removeListener('data', dataHandler);
264 |               sessionData.shell.removeListener('error', errorHandler);
265 |             }
266 |           }, 30000); // 30秒超时
267 |         });
268 |       } else {
269 |         log.info(`Executing command using exec: ${command}`);
270 |         return new Promise((resolve, reject) => {
271 |           // 构建环境变量设置命令
272 |           const envSetup = Object.entries(env)
273 |             .map(([key, value]) => `export ${key}="${String(value).replace(/"/g, '\\"')}"`)
274 |             .join(' && ');
275 |           
276 |           // 如果有环境变量,先设置环境变量,再执行命令
277 |           const fullCommand = envSetup ? `${envSetup} && ${command}` : command;
278 |           
279 |           sessionData?.client?.exec(`/bin/bash --login -c "${fullCommand.replace(/"/g, '\\"')}"`, (err, stream) => {
280 |             if (err) {
281 |               reject(err);
282 |               return;
283 |             }
284 | 
285 |             let stdout = "";
286 |             let stderr = '';
287 | 
288 |             stream
289 |               .on("data", (data: Buffer) => {
290 |                 this.resetTimeout(sessionKey);
291 |                 stdout += data.toString();
292 |               })
293 |               .stderr.on('data', (data: Buffer) => {
294 |                 stderr += data.toString();
295 |               })
296 |               .on('close', () => {
297 |                 resolve({ stdout, stderr });
298 |               })
299 |               .on('error', (err) => {
300 |                 reject(err);
301 |               });
302 |           });
303 |         });
304 |       }
305 |     } 
306 |     // 否则在本地执行命令
307 |     else {
308 |       // 在本地执行命令时,也使用会话机制来保持环境变量
309 |       log.info(`Executing command using local session: ${sessionKey}`);
310 |       
311 |       // 检查是否已有本地会话
312 |       let sessionData = this.sessions.get(sessionKey);
313 |       let sessionEnv = {};
314 |       
315 |       if (!sessionData) {
316 |         // 为本地会话创建一个空条目,以便跟踪超时
317 |         sessionData = {
318 |           client: null,
319 |           connection: null,
320 |           timeout: null,
321 |           host: undefined,
322 |           env: { ...env } // 保存初始环境变量
323 |         };
324 |         this.sessions.set(sessionKey, sessionData);
325 |         log.info(`Creating new local session: ${sessionKey}`);
326 |         sessionEnv = env;
327 |       } else {
328 |         log.info(`Reusing existing local session: ${sessionKey}`);
329 |         // 合并现有会话环境变量和新的环境变量
330 |         if (!sessionData.env) {
331 |           sessionData.env = {};
332 |         }
333 |         sessionData.env = { ...sessionData.env, ...env };
334 |         sessionEnv = sessionData.env;
335 |         // 更新会话
336 |         this.sessions.set(sessionKey, sessionData);
337 |       }
338 |       
339 |       this.resetTimeout(sessionKey);
340 |       
341 |       return new Promise((resolve, reject) => {
342 |         // 构建环境变量,优先级:系统环境变量 < 会话环境变量 < 当前命令环境变量
343 |         const envVars = { ...process.env, ...sessionEnv };
344 |         
345 |         // 执行命令
346 |         log.info(`Executing local command: ${command}`);
347 |         exec(command, { env: envVars }, (error, stdout, stderr) => {
348 |           if (error && error.code !== 0) {
349 |             // 我们不直接拒绝,而是返回错误信息作为stderr
350 |             resolve({ stdout, stderr: stderr || error.message });
351 |           } else {
352 |             resolve({ stdout, stderr });
353 |           }
354 |         });
355 |       });
356 |     }
357 |   }
358 | 
359 |   private async disconnectSession(sessionKey: string): Promise<void> {
360 |     const session = this.sessions.get(sessionKey);
361 |     if (session) {
362 |       if (session.shell) {
363 |         log.info(`Closing interactive shell for session ${sessionKey}`);
364 |         session.shell.end();
365 |         session.shellReady = false;
366 |       }
367 |       if (session.client) {
368 |         log.info(`Disconnecting SSH connection for session ${sessionKey}`);
369 |         session.client.end();
370 |       }
371 |       if (session.timeout) {
372 |         clearTimeout(session.timeout);
373 |       }
374 |       log.info(`Disconnecting session: ${sessionKey}`);
375 |       this.sessions.delete(sessionKey);
376 |     }
377 |   }
378 | 
379 |   async disconnect(): Promise<void> {
380 |     const disconnectPromises = Array.from(this.sessions.keys()).map(
381 |       sessionKey => this.disconnectSession(sessionKey)
382 |     );
383 |     
384 |     await Promise.all(disconnectPromises);
385 |     this.sessions.clear();
386 |   }
387 | }
```