@pyrokine/mcp-ssh 1.1.0 → 1.1.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.
package/src/ssh-config.ts CHANGED
@@ -8,30 +8,30 @@
8
8
  * - ProxyJump 解析(支持 user@host:port 格式)
9
9
  */
10
10
 
11
- import * as fs from 'fs';
12
- import * as path from 'path';
13
- import * as os from 'os';
11
+ import * as fs from 'fs'
12
+ import * as os from 'os'
13
+ import * as path from 'path'
14
14
 
15
15
  export interface SSHConfigHost {
16
- host: string; // Host 别名
17
- hostName?: string; // 实际地址
18
- user?: string; // 用户名
19
- port?: number; // 端口
20
- identityFile?: string; // 私钥路径
21
- proxyJump?: string; // 跳板机(原始字符串,可能是 user@host:port 格式)
16
+ host: string; // Host 别名
17
+ hostName?: string; // 实际地址
18
+ user?: string; // 用户名
19
+ port?: number; // 端口
20
+ identityFile?: string; // 私钥路径
21
+ proxyJump?: string; // 跳板机(原始字符串,可能是 user@host:port 格式)
22
22
  }
23
23
 
24
24
  /** ProxyJump 解析结果 */
25
25
  export interface ParsedProxyJump {
26
- user?: string;
27
- host: string;
28
- port?: number;
26
+ user?: string;
27
+ host: string;
28
+ port?: number;
29
29
  }
30
30
 
31
31
  /** 内部使用的配置块 */
32
32
  interface ConfigBlock {
33
- patterns: string[]; // Host 行的所有模式/别名
34
- config: Omit<SSHConfigHost, 'host'>;
33
+ patterns: string[]; // Host 行的所有模式/别名
34
+ config: Omit<SSHConfigHost, 'host'>;
35
35
  }
36
36
 
37
37
  /**
@@ -39,39 +39,39 @@ interface ConfigBlock {
39
39
  * 返回 { host, port },host 不含方括号
40
40
  */
41
41
  function parseHostPort(s: string): { host: string; port?: number } {
42
- // IPv6 方括号格式: [addr]:port 或 [addr]
43
- if (s.startsWith('[')) {
44
- const closeBracket = s.indexOf(']');
45
- if (closeBracket !== -1) {
46
- const host = s.slice(1, closeBracket); // 去掉方括号
47
- const rest = s.slice(closeBracket + 1);
48
- if (rest.startsWith(':')) {
49
- const parsedPort = parseInt(rest.slice(1), 10);
50
- if (!isNaN(parsedPort)) {
51
- return { host, port: parsedPort };
42
+ // IPv6 方括号格式: [addr]:port 或 [addr]
43
+ if (s.startsWith('[')) {
44
+ const closeBracket = s.indexOf(']')
45
+ if (closeBracket !== -1) {
46
+ const host = s.slice(1, closeBracket) // 去掉方括号
47
+ const rest = s.slice(closeBracket + 1)
48
+ if (rest.startsWith(':')) {
49
+ const parsedPort = parseInt(rest.slice(1), 10)
50
+ if (!isNaN(parsedPort)) {
51
+ return {host, port: parsedPort}
52
+ }
53
+ }
54
+ return {host}
52
55
  }
53
- }
54
- return { host };
55
56
  }
56
- }
57
-
58
- // 检测裸 IPv6(多个冒号但无方括号):安全失败,当作 host-only
59
- const colonCount = (s.match(/:/g) || []).length;
60
- if (colonCount >= 2) {
61
- return { host: s };
62
- }
63
-
64
- // 普通格式: host:port 或 host
65
- const colonIndex = s.lastIndexOf(':');
66
- if (colonIndex !== -1) {
67
- const host = s.slice(0, colonIndex);
68
- const portStr = s.slice(colonIndex + 1);
69
- const parsedPort = parseInt(portStr, 10);
70
- if (!isNaN(parsedPort)) {
71
- return { host, port: parsedPort };
57
+
58
+ // 检测裸 IPv6(多个冒号但无方括号):安全失败,当作 host-only
59
+ const colonCount = (s.match(/:/g) || []).length
60
+ if (colonCount >= 2) {
61
+ return {host: s}
72
62
  }
73
- }
74
- return { host: s };
63
+
64
+ // 普通格式: host:port host
65
+ const colonIndex = s.lastIndexOf(':')
66
+ if (colonIndex !== -1) {
67
+ const host = s.slice(0, colonIndex)
68
+ const portStr = s.slice(colonIndex + 1)
69
+ const parsedPort = parseInt(portStr, 10)
70
+ if (!isNaN(parsedPort)) {
71
+ return {host, port: parsedPort}
72
+ }
73
+ }
74
+ return {host: s}
75
75
  }
76
76
 
77
77
  /**
@@ -80,39 +80,39 @@ function parseHostPort(s: string): { host: string; port?: number } {
80
80
  * 注意:只解析第一跳,不支持逗号分隔的多跳链路
81
81
  */
82
82
  export function parseProxyJump(proxyJump: string): ParsedProxyJump | null {
83
- if (!proxyJump) {
84
- return null;
85
- }
86
-
87
- // 取第一跳(如果有逗号分隔)
88
- const firstJump = proxyJump.split(',')[0].trim();
89
- if (!firstJump) {
90
- return null;
91
- }
92
-
93
- let user: string | undefined;
94
-
95
- // 解析 user@... 格式
96
- const atIndex = firstJump.indexOf('@');
97
- if (atIndex !== -1) {
98
- user = firstJump.slice(0, atIndex);
99
- const rest = firstJump.slice(atIndex + 1);
100
- const { host, port } = parseHostPort(rest);
101
- return { user, host, port };
102
- }
103
-
104
- const { host, port } = parseHostPort(firstJump);
105
- return { user, host, port };
83
+ if (!proxyJump) {
84
+ return null
85
+ }
86
+
87
+ // 取第一跳(如果有逗号分隔)
88
+ const firstJump = proxyJump.split(',')[0].trim()
89
+ if (!firstJump) {
90
+ return null
91
+ }
92
+
93
+ let user: string | undefined
94
+
95
+ // 解析 user@... 格式
96
+ const atIndex = firstJump.indexOf('@')
97
+ if (atIndex !== -1) {
98
+ user = firstJump.slice(0, atIndex)
99
+ const rest = firstJump.slice(atIndex + 1)
100
+ const {host, port} = parseHostPort(rest)
101
+ return {user, host, port}
102
+ }
103
+
104
+ const {host, port} = parseHostPort(firstJump)
105
+ return {user, host, port}
106
106
  }
107
107
 
108
108
  /**
109
109
  * 展开 ~ 路径
110
110
  */
111
111
  function expandTilde(filePath: string): string {
112
- if (filePath.startsWith('~')) {
113
- return path.join(os.homedir(), filePath.slice(1));
114
- }
115
- return filePath;
112
+ if (filePath.startsWith('~')) {
113
+ return path.join(os.homedir(), filePath.slice(1))
114
+ }
115
+ return filePath
116
116
  }
117
117
 
118
118
  /**
@@ -120,21 +120,21 @@ function expandTilde(filePath: string): string {
120
120
  * "value # comment" -> "value"
121
121
  */
122
122
  function stripInlineComment(value: string): string {
123
- // 查找不在引号内的 #
124
- let inQuote = false;
125
- let quoteChar = '';
126
- for (let i = 0; i < value.length; i++) {
127
- const ch = value[i];
128
- if (!inQuote && (ch === '"' || ch === "'")) {
129
- inQuote = true;
130
- quoteChar = ch;
131
- } else if (inQuote && ch === quoteChar) {
132
- inQuote = false;
133
- } else if (!inQuote && ch === '#') {
134
- return value.slice(0, i).trim();
123
+ // 查找不在引号内的 #
124
+ let inQuote = false
125
+ let quoteChar = ''
126
+ for (let i = 0; i < value.length; i++) {
127
+ const ch = value[i]
128
+ if (!inQuote && (ch === '"' || ch === '\'')) {
129
+ inQuote = true
130
+ quoteChar = ch
131
+ } else if (inQuote && ch === quoteChar) {
132
+ inQuote = false
133
+ } else if (!inQuote && ch === '#') {
134
+ return value.slice(0, i).trim()
135
+ }
135
136
  }
136
- }
137
- return value.trim();
137
+ return value.trim()
138
138
  }
139
139
 
140
140
  /**
@@ -143,122 +143,122 @@ function stripInlineComment(value: string): string {
143
143
  * 跳过 Match 块(避免条件配置被误应用)
144
144
  */
145
145
  export function parseSSHConfig(configPath?: string): SSHConfigHost[] {
146
- const filePath = configPath || path.join(os.homedir(), '.ssh', 'config');
146
+ const filePath = configPath || path.join(os.homedir(), '.ssh', 'config')
147
147
 
148
- if (!fs.existsSync(filePath)) {
149
- return [];
150
- }
148
+ if (!fs.existsSync(filePath)) {
149
+ return []
150
+ }
151
151
 
152
- const content = fs.readFileSync(filePath, 'utf-8');
153
- const lines = content.split('\n');
152
+ const content = fs.readFileSync(filePath, 'utf-8')
153
+ const lines = content.split('\n')
154
154
 
155
- // 第一遍:收集所有配置块
156
- const blocks: ConfigBlock[] = [];
157
- let currentBlock: ConfigBlock | null = null;
158
- let globalDefaults: Omit<SSHConfigHost, 'host'> = {};
159
- let inMatchBlock = false; // 跳过 Match 块
155
+ // 第一遍:收集所有配置块
156
+ const blocks: ConfigBlock[] = []
157
+ let currentBlock: ConfigBlock | null = null
158
+ let globalDefaults: Omit<SSHConfigHost, 'host'> = {}
159
+ let inMatchBlock = false // 跳过 Match 块
160
160
 
161
- for (const line of lines) {
162
- const trimmed = line.trim();
161
+ for (const line of lines) {
162
+ const trimmed = line.trim()
163
163
 
164
- // 跳过空行和注释
165
- if (!trimmed || trimmed.startsWith('#')) {
166
- continue;
167
- }
164
+ // 跳过空行和注释
165
+ if (!trimmed || trimmed.startsWith('#')) {
166
+ continue
167
+ }
168
168
 
169
- // 解析 key value(支持 = 和空格分隔)
170
- const match = trimmed.match(/^(\S+)\s*[=\s]\s*(.+)$/);
171
- if (!match) {
172
- continue;
169
+ // 解析 key value(支持 = 和空格分隔)
170
+ const match = trimmed.match(/^(\S+)\s*[=\s]\s*(.+)$/)
171
+ if (!match) {
172
+ continue
173
+ }
174
+
175
+ const [, key, rawValue] = match
176
+ const keyLower = key.toLowerCase()
177
+ const value = stripInlineComment(rawValue)
178
+
179
+ if (keyLower === 'host') {
180
+ // Host 块开始,结束 Match 块
181
+ inMatchBlock = false
182
+
183
+ // 保存上一个 block
184
+ if (currentBlock) {
185
+ blocks.push(currentBlock)
186
+ }
187
+
188
+ // Host 行可能有多个别名/模式,用空格分隔
189
+ const patterns = value.split(/\s+/).filter(p => p.length > 0)
190
+ currentBlock = {patterns, config: {}}
191
+ } else if (keyLower === 'match') {
192
+ // Match 块开始,跳过直到下一个 Host
193
+ inMatchBlock = true
194
+ // 保存当前 block(如果有)
195
+ if (currentBlock) {
196
+ blocks.push(currentBlock)
197
+ currentBlock = null
198
+ }
199
+ } else if (!inMatchBlock && currentBlock) {
200
+ // 解析配置项(不在 Match 块内)
201
+ switch (keyLower) {
202
+ case 'hostname':
203
+ currentBlock.config.hostName = value
204
+ break
205
+ case 'user':
206
+ currentBlock.config.user = value
207
+ break
208
+ case 'port':
209
+ currentBlock.config.port = parseInt(value, 10)
210
+ break
211
+ case 'identityfile':
212
+ currentBlock.config.identityFile = expandTilde(value)
213
+ break
214
+ case 'proxyjump':
215
+ currentBlock.config.proxyJump = value
216
+ break
217
+ }
218
+ }
173
219
  }
174
220
 
175
- const [, key, rawValue] = match;
176
- const keyLower = key.toLowerCase();
177
- const value = stripInlineComment(rawValue);
178
-
179
- if (keyLower === 'host') {
180
- // Host 块开始,结束 Match 块
181
- inMatchBlock = false;
182
-
183
- // 保存上一个 block
184
- if (currentBlock) {
185
- blocks.push(currentBlock);
186
- }
187
-
188
- // Host 行可能有多个别名/模式,用空格分隔
189
- const patterns = value.split(/\s+/).filter(p => p.length > 0);
190
- currentBlock = { patterns, config: {} };
191
- } else if (keyLower === 'match') {
192
- // Match 块开始,跳过直到下一个 Host
193
- inMatchBlock = true;
194
- // 保存当前 block(如果有)
195
- if (currentBlock) {
196
- blocks.push(currentBlock);
197
- currentBlock = null;
198
- }
199
- } else if (!inMatchBlock && currentBlock) {
200
- // 解析配置项(不在 Match 块内)
201
- switch (keyLower) {
202
- case 'hostname':
203
- currentBlock.config.hostName = value;
204
- break;
205
- case 'user':
206
- currentBlock.config.user = value;
207
- break;
208
- case 'port':
209
- currentBlock.config.port = parseInt(value, 10);
210
- break;
211
- case 'identityfile':
212
- currentBlock.config.identityFile = expandTilde(value);
213
- break;
214
- case 'proxyjump':
215
- currentBlock.config.proxyJump = value;
216
- break;
217
- }
221
+ // 保存最后一个 block
222
+ if (currentBlock && !inMatchBlock) {
223
+ blocks.push(currentBlock)
218
224
  }
219
- }
220
-
221
- // 保存最后一个 block
222
- if (currentBlock && !inMatchBlock) {
223
- blocks.push(currentBlock);
224
- }
225
-
226
- // 第二遍:提取 Host * 的全局默认配置
227
- for (const block of blocks) {
228
- if (block.patterns.length === 1 && block.patterns[0] === '*') {
229
- globalDefaults = { ...block.config };
230
- break;
225
+
226
+ // 第二遍:提取 Host * 的全局默认配置
227
+ for (const block of blocks) {
228
+ if (block.patterns.length === 1 && block.patterns[0] === '*') {
229
+ globalDefaults = {...block.config}
230
+ break
231
+ }
231
232
  }
232
- }
233
-
234
- // 第三遍:展开所有 Host,应用继承
235
- const hosts: SSHConfigHost[] = [];
236
-
237
- for (const block of blocks) {
238
- for (const pattern of block.patterns) {
239
- // 跳过通配符模式(*, *.example.com 等)
240
- if (pattern.includes('*') || pattern.includes('?')) {
241
- continue;
242
- }
243
-
244
- // 合并配置:全局默认 + 当前块配置
245
- const merged: SSHConfigHost = {
246
- host: pattern,
247
- ...globalDefaults,
248
- ...block.config,
249
- };
250
-
251
- hosts.push(merged);
233
+
234
+ // 第三遍:展开所有 Host,应用继承
235
+ const hosts: SSHConfigHost[] = []
236
+
237
+ for (const block of blocks) {
238
+ for (const pattern of block.patterns) {
239
+ // 跳过通配符模式(*, *.example.com 等)
240
+ if (pattern.includes('*') || pattern.includes('?')) {
241
+ continue
242
+ }
243
+
244
+ // 合并配置:全局默认 + 当前块配置
245
+ const merged: SSHConfigHost = {
246
+ host: pattern,
247
+ ...globalDefaults,
248
+ ...block.config,
249
+ }
250
+
251
+ hosts.push(merged)
252
+ }
252
253
  }
253
- }
254
254
 
255
- return hosts;
255
+ return hosts
256
256
  }
257
257
 
258
258
  /**
259
259
  * 根据 Host 名称获取配置
260
260
  */
261
261
  export function getHostConfig(hostName: string, configPath?: string): SSHConfigHost | null {
262
- const hosts = parseSSHConfig(configPath);
263
- return hosts.find(h => h.host === hostName) || null;
262
+ const hosts = parseSSHConfig(configPath)
263
+ return hosts.find(h => h.host === hostName) || null
264
264
  }
package/src/types.ts CHANGED
@@ -3,108 +3,108 @@
3
3
  */
4
4
 
5
5
  export interface SSHConnectionConfig {
6
- host: string;
7
- port: number;
8
- username: string;
9
- password?: string;
10
- privateKeyPath?: string;
11
- privateKey?: string;
12
- passphrase?: string;
13
- alias?: string;
14
- // 高级配置
15
- keepaliveInterval?: number; // 心跳间隔(毫秒)
16
- keepaliveCountMax?: number; // 最大心跳失败次数
17
- readyTimeout?: number; // 连接超时(毫秒)
18
- // 环境配置
19
- env?: Record<string, string>; // 环境变量
20
- lang?: string; // LANG 设置
21
- shell?: string; // Shell 类型
22
- // 跳板机
23
- jumpHost?: SSHConnectionConfig;
6
+ host: string;
7
+ port: number;
8
+ username: string;
9
+ password?: string;
10
+ privateKeyPath?: string;
11
+ privateKey?: string;
12
+ passphrase?: string;
13
+ alias?: string;
14
+ // 高级配置
15
+ keepaliveInterval?: number; // 心跳间隔(毫秒)
16
+ keepaliveCountMax?: number; // 最大心跳失败次数
17
+ readyTimeout?: number; // 连接超时(毫秒)
18
+ // 环境配置
19
+ env?: Record<string, string>; // 环境变量
20
+ lang?: string; // LANG 设置
21
+ shell?: string; // Shell 类型
22
+ // 跳板机
23
+ jumpHost?: SSHConnectionConfig;
24
24
  }
25
25
 
26
26
  export interface SSHSessionInfo {
27
- alias: string;
28
- host: string;
29
- port: number;
30
- username: string;
31
- connected: boolean;
32
- connectedAt: number;
33
- lastUsedAt: number;
34
- env?: Record<string, string>;
27
+ alias: string;
28
+ host: string;
29
+ port: number;
30
+ username: string;
31
+ connected: boolean;
32
+ connectedAt: number;
33
+ lastUsedAt: number;
34
+ env?: Record<string, string>;
35
35
  }
36
36
 
37
37
  export interface ExecOptions {
38
- timeout?: number; // 命令超时(毫秒)
39
- env?: Record<string, string>; // 额外环境变量
40
- cwd?: string; // 工作目录
41
- pty?: boolean; // 是否使用 PTY
42
- maxOutputSize?: number; // 最大输出大小(字节),默认 10MB
43
- // PTY 配置
44
- rows?: number;
45
- cols?: number;
46
- term?: string;
38
+ timeout?: number; // 命令超时(毫秒)
39
+ env?: Record<string, string>; // 额外环境变量
40
+ cwd?: string; // 工作目录
41
+ pty?: boolean; // 是否使用 PTY
42
+ maxOutputSize?: number; // 最大输出大小(字节),默认 10MB
43
+ // PTY 配置
44
+ rows?: number;
45
+ cols?: number;
46
+ term?: string;
47
47
  }
48
48
 
49
49
  export interface ExecResult {
50
- success: boolean;
51
- stdout: string;
52
- stderr: string;
53
- exitCode: number;
54
- duration: number; // 执行时间(毫秒)
50
+ success: boolean;
51
+ stdout: string;
52
+ stderr: string;
53
+ exitCode: number;
54
+ duration: number; // 执行时间(毫秒)
55
55
  }
56
56
 
57
57
  export interface FileInfo {
58
- name: string;
59
- path: string;
60
- size: number;
61
- isDirectory: boolean;
62
- isFile: boolean;
63
- isSymlink: boolean;
64
- permissions: string;
65
- owner: number;
66
- group: number;
67
- mtime: Date;
68
- atime: Date;
58
+ name: string;
59
+ path: string;
60
+ size: number;
61
+ isDirectory: boolean;
62
+ isFile: boolean;
63
+ isSymlink: boolean;
64
+ permissions: string;
65
+ owner: number;
66
+ group: number;
67
+ mtime: Date;
68
+ atime: Date;
69
69
  }
70
70
 
71
71
  export interface TransferProgress {
72
- transferred: number;
73
- total: number;
74
- percent: number;
72
+ transferred: number;
73
+ total: number;
74
+ percent: number;
75
75
  }
76
76
 
77
77
  // 持久化存储的会话配置(不含敏感信息)
78
78
  export interface PersistedSession {
79
- alias: string;
80
- host: string;
81
- port: number;
82
- username: string;
83
- connectedAt: number;
84
- env?: Record<string, string>;
79
+ alias: string;
80
+ host: string;
81
+ port: number;
82
+ username: string;
83
+ connectedAt: number;
84
+ env?: Record<string, string>;
85
85
  }
86
86
 
87
87
  // PTY 会话配置
88
88
  export interface PtyOptions {
89
- rows?: number;
90
- cols?: number;
91
- term?: string;
92
- env?: Record<string, string>;
93
- cwd?: string;
94
- bufferSize?: number; // 输出缓冲区大小,默认 1MB
89
+ rows?: number;
90
+ cols?: number;
91
+ term?: string;
92
+ env?: Record<string, string>;
93
+ cwd?: string;
94
+ bufferSize?: number; // 输出缓冲区大小,默认 1MB
95
95
  }
96
96
 
97
97
  // PTY 会话信息
98
98
  export interface PtySessionInfo {
99
- id: string;
100
- alias: string; // SSH 连接别名
101
- command: string; // 启动命令
102
- rows: number;
103
- cols: number;
104
- createdAt: number;
105
- lastReadAt: number;
106
- bufferSize: number; // 当前缓冲区大小
107
- active: boolean;
99
+ id: string;
100
+ alias: string; // SSH 连接别名
101
+ command: string; // 启动命令
102
+ rows: number;
103
+ cols: number;
104
+ createdAt: number;
105
+ lastReadAt: number;
106
+ bufferSize: number; // 当前缓冲区大小
107
+ active: boolean;
108
108
  }
109
109
 
110
110
  // 端口转发类型
@@ -112,22 +112,22 @@ export type ForwardType = 'local' | 'remote';
112
112
 
113
113
  // 端口转发配置
114
114
  export interface PortForwardConfig {
115
- type: ForwardType;
116
- localHost: string;
117
- localPort: number;
118
- remoteHost: string;
119
- remotePort: number;
115
+ type: ForwardType;
116
+ localHost: string;
117
+ localPort: number;
118
+ remoteHost: string;
119
+ remotePort: number;
120
120
  }
121
121
 
122
122
  // 端口转发信息
123
123
  export interface PortForwardInfo {
124
- id: string;
125
- alias: string; // SSH 连接别名
126
- type: ForwardType;
127
- localHost: string;
128
- localPort: number;
129
- remoteHost: string;
130
- remotePort: number;
131
- createdAt: number;
132
- active: boolean;
124
+ id: string;
125
+ alias: string; // SSH 连接别名
126
+ type: ForwardType;
127
+ localHost: string;
128
+ localPort: number;
129
+ remoteHost: string;
130
+ remotePort: number;
131
+ createdAt: number;
132
+ active: boolean;
133
133
  }
package/tsconfig.json CHANGED
@@ -11,6 +11,11 @@
11
11
  "forceConsistentCasingInFileNames": true,
12
12
  "declaration": true
13
13
  },
14
- "include": ["src/**/*"],
15
- "exclude": ["node_modules", "dist"]
14
+ "include": [
15
+ "src/**/*"
16
+ ],
17
+ "exclude": [
18
+ "node_modules",
19
+ "dist"
20
+ ]
16
21
  }