coding-tool-x 3.3.6 → 3.3.7

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.
@@ -107,6 +107,65 @@ function resolveWindowsSpawnCommand(command, env, cwd) {
107
107
  return normalizedCommand;
108
108
  }
109
109
 
110
+ function getCommandInstallHint(command) {
111
+ const normalized = path.basename(stripWrappingQuotes(command)).toLowerCase()
112
+
113
+ if (['node', 'node.exe', 'npm', 'npm.cmd', 'npx', 'npx.cmd'].includes(normalized)) {
114
+ return '请先安装 Node.js,并确认 `node` / `npm` / `npx` 已加入 PATH。'
115
+ }
116
+
117
+ if (['uv', 'uv.exe', 'uvx', 'uvx.exe'].includes(normalized)) {
118
+ return '请先安装 `uv`,并确认 `uv` / `uvx` 可以在终端直接执行。'
119
+ }
120
+
121
+ if (['python', 'python.exe', 'python3', 'py'].includes(normalized)) {
122
+ return '请先安装 Python,并确认对应命令已加入 PATH。'
123
+ }
124
+
125
+ if (process.platform === 'win32' && ['netstat', 'taskkill'].includes(normalized)) {
126
+ return '请确认 Windows 自带命令可用,并检查 `C:\\Windows\\System32` 是否在 PATH 中。'
127
+ }
128
+
129
+ return `请确认已安装 "${command}",或改用可执行文件的绝对路径。`
130
+ }
131
+
132
+ function createMissingCommandHint(command, resolvedCommand, env = {}) {
133
+ const pathKey = getPathEnvKey(env)
134
+ const pathValue = typeof env[pathKey] === 'string' ? env[pathKey] : ''
135
+ const pathPreview = pathValue
136
+ ? pathValue.split(path.delimiter).slice(0, 5).join(path.delimiter)
137
+ : ''
138
+ const commandHint = resolvedCommand === command
139
+ ? command
140
+ : `${command} (resolved: ${resolvedCommand})`
141
+
142
+ const details = [
143
+ getCommandInstallHint(command),
144
+ process.platform === 'win32'
145
+ ? 'Windows 可优先尝试 `.cmd` / `.exe` 文件,必要时直接填写绝对路径。'
146
+ : `可尝试填写绝对路径(例如 \`/usr/bin/node\` 或 \`$(which ${command})\`)。`
147
+ ]
148
+
149
+ if (pathPreview) {
150
+ details.push(`当前 PATH 前 5 项: ${pathPreview}`)
151
+ } else {
152
+ details.push('当前 PATH 为空,请检查环境变量配置。')
153
+ }
154
+
155
+ return {
156
+ type: 'missing-command',
157
+ command,
158
+ resolvedCommand,
159
+ title: `命令 "${commandHint}" 未找到`,
160
+ details
161
+ }
162
+ }
163
+
164
+ function buildMissingCommandMessage(command, resolvedCommand, env = {}) {
165
+ const hint = createMissingCommandHint(command, resolvedCommand, env)
166
+ return [hint.title, ...hint.details].join('\n')
167
+ }
168
+
110
169
  // ============================================================================
111
170
  // McpClient
112
171
  // ============================================================================
@@ -356,19 +415,11 @@ class McpClient extends EventEmitter {
356
415
 
357
416
  this._child.on('error', (err) => {
358
417
  if (err.code === 'ENOENT') {
359
- const pathKey = getPathEnvKey(mergedEnv);
360
- const pathValue = typeof mergedEnv[pathKey] === 'string' ? mergedEnv[pathKey] : '';
361
- const pathHint = pathValue
362
- ? `\n Current PATH: ${pathValue.split(path.delimiter).slice(0, 5).join(path.delimiter)}\n (showing first 5 entries)`
363
- : '\n PATH is not set!';
364
- const commandHint = resolvedCommand === command
365
- ? command
366
- : `${command} (resolved: ${resolvedCommand})`;
418
+ const hint = createMissingCommandHint(command, resolvedCommand, mergedEnv)
367
419
  settle(new McpClientError(
368
- `Command "${commandHint}" not found. Please check:\n` +
369
- ` 1. Is "${command}" installed?\n` +
370
- ` 2. Try using absolute path${process.platform === 'win32' ? ' (e.g., C:\\\\Program Files\\\\nodejs\\\\npx.cmd)' : ` (e.g., /usr/bin/node or $(which ${command}))`}\n` +
371
- ` 3. Check your PATH environment variable${pathHint}`
420
+ buildMissingCommandMessage(command, resolvedCommand, mergedEnv),
421
+ undefined,
422
+ { hint }
372
423
  ));
373
424
  } else {
374
425
  settle(new McpClientError(`Failed to start process: ${err.message}`));
@@ -873,5 +924,11 @@ async function createClient(serverSpec, options = {}) {
873
924
  module.exports = {
874
925
  McpClient,
875
926
  McpClientError,
876
- createClient
927
+ createClient,
928
+ buildMissingCommandMessage,
929
+ createMissingCommandHint,
930
+ _test: {
931
+ createMissingCommandHint,
932
+ buildMissingCommandMessage
933
+ }
877
934
  };
@@ -11,7 +11,7 @@ const toml = require('@iarna/toml');
11
11
  const { spawn } = require('child_process');
12
12
  const http = require('http');
13
13
  const https = require('https');
14
- const { McpClient } = require('./mcp-client');
14
+ const { McpClient, buildMissingCommandMessage, createMissingCommandHint } = require('./mcp-client');
15
15
  const { NATIVE_PATHS } = require('../../config/paths');
16
16
  const { resolvePreferredHomeDir } = require('../../utils/home-dir');
17
17
 
@@ -501,6 +501,19 @@ function resolveWindowsSpawnCommand(command, env, cwd) {
501
501
  return normalizedCommand;
502
502
  }
503
503
 
504
+ function extractMcpHint(error) {
505
+ return error?.data?.hint || error?.hint || null;
506
+ }
507
+
508
+ function buildMcpFailureResult(error, fallbackMessage, duration) {
509
+ const hint = extractMcpHint(error);
510
+ return {
511
+ message: hint?.title || fallbackMessage || error?.message || '操作失败',
512
+ hint,
513
+ duration
514
+ };
515
+ }
516
+
504
517
  // ============================================================================
505
518
  // MCP 数据管理
506
519
  // ============================================================================
@@ -1301,10 +1314,12 @@ async function testServer(serverId) {
1301
1314
  return { success: false, message: `不支持的服务器类型: ${type}` };
1302
1315
  }
1303
1316
  } catch (err) {
1317
+ const failure = buildMcpFailureResult(err, err.message, Date.now() - startTime);
1304
1318
  return {
1305
1319
  success: false,
1306
- message: err.message,
1307
- duration: Date.now() - startTime
1320
+ message: failure.message,
1321
+ hint: failure.hint,
1322
+ duration: failure.duration
1308
1323
  };
1309
1324
  }
1310
1325
  }
@@ -1370,10 +1385,11 @@ async function testStdioServer(spec) {
1370
1385
 
1371
1386
  child.on('error', (err) => {
1372
1387
  if (err.code === 'ENOENT') {
1373
- const commandLabel = resolvedCommand === command ? command : `${command} (resolved: ${resolvedCommand})`;
1388
+ const hint = createMissingCommandHint(command, resolvedCommand, mergedEnv);
1374
1389
  done({
1375
1390
  success: false,
1376
- message: `命令 "${commandLabel}" 未找到,请确保已安装(Windows 可尝试 npx.cmd 或绝对路径)`,
1391
+ message: buildMissingCommandMessage(command, resolvedCommand, mergedEnv),
1392
+ hint,
1377
1393
  duration: Date.now() - startTime
1378
1394
  });
1379
1395
  } else {
@@ -1423,10 +1439,12 @@ async function testStdioServer(spec) {
1423
1439
  }, timeout);
1424
1440
 
1425
1441
  } catch (err) {
1442
+ const failure = buildMcpFailureResult(err, `测试失败: ${err.message}`, Date.now() - startTime);
1426
1443
  done({
1427
1444
  success: false,
1428
- message: `测试失败: ${err.message}`,
1429
- duration: Date.now() - startTime
1445
+ message: failure.message,
1446
+ hint: failure.hint,
1447
+ duration: failure.duration
1430
1448
  });
1431
1449
  }
1432
1450
  });
@@ -1571,11 +1589,14 @@ async function getServerTools(serverId) {
1571
1589
  mcpClientPool.delete(serverId);
1572
1590
  }
1573
1591
 
1592
+ const failure = buildMcpFailureResult(err, err.message, Date.now() - startTime);
1574
1593
  return {
1575
1594
  tools: [],
1576
- duration: Date.now() - startTime,
1595
+ duration: failure.duration,
1577
1596
  status: 'error',
1578
- error: err.message
1597
+ error: failure.message,
1598
+ message: failure.message,
1599
+ hint: failure.hint
1579
1600
  };
1580
1601
  }
1581
1602
  }
@@ -1677,14 +1698,17 @@ async function callServerTool(serverId, toolName, arguments = {}) {
1677
1698
  mcpClientPool.delete(serverId);
1678
1699
  }
1679
1700
 
1701
+ const failure = buildMcpFailureResult(err, err.message, Date.now() - startTime);
1680
1702
  return {
1681
1703
  result: {
1682
- error: err.message,
1704
+ error: failure.message,
1683
1705
  code: err.code,
1684
1706
  data: err.data
1685
1707
  },
1686
- duration: Date.now() - startTime,
1687
- isError: true
1708
+ duration: failure.duration,
1709
+ isError: true,
1710
+ message: failure.message,
1711
+ hint: failure.hint
1688
1712
  };
1689
1713
  }
1690
1714
  }
@@ -1864,5 +1888,9 @@ module.exports = {
1864
1888
  callServerTool,
1865
1889
  updateServerStatus,
1866
1890
  updateServerOrder,
1867
- exportServers
1891
+ exportServers,
1892
+ _test: {
1893
+ extractMcpHint,
1894
+ buildMcpFailureResult
1895
+ }
1868
1896
  };
@@ -23,6 +23,7 @@ const MODEL_PRIORITY = {
23
23
  'claude-haiku-4-5-20251001'
24
24
  ],
25
25
  codex: [
26
+ 'gpt-5.4',
26
27
  'gpt-5.2-codex',
27
28
  'gpt-5.1-codex-max',
28
29
  'gpt-5.1-codex',
@@ -2,10 +2,74 @@ const { execSync } = require('child_process');
2
2
  const net = require('net');
3
3
  const { isWindowsLikePlatform } = require('./home-dir');
4
4
 
5
+ let lastPortToolIssue = null;
6
+
5
7
  function isWindowsLikeRuntime(platform = process.platform, env = process.env) {
6
8
  return isWindowsLikePlatform(platform, env);
7
9
  }
8
10
 
11
+ function clearPortToolIssue() {
12
+ lastPortToolIssue = null;
13
+ }
14
+
15
+ function rememberPortToolIssue(issue) {
16
+ if (!lastPortToolIssue) {
17
+ lastPortToolIssue = issue;
18
+ }
19
+ return lastPortToolIssue;
20
+ }
21
+
22
+ function getPortToolIssue() {
23
+ return lastPortToolIssue;
24
+ }
25
+
26
+ function isMissingCommandError(error) {
27
+ const message = [
28
+ error && typeof error.message === 'string' ? error.message : '',
29
+ error && typeof error.stderr === 'string'
30
+ ? error.stderr
31
+ : Buffer.isBuffer(error?.stderr)
32
+ ? error.stderr.toString('utf-8')
33
+ : ''
34
+ ].join('\n');
35
+
36
+ return error?.code === 'ENOENT' || /not found|not recognized as an internal or external command/i.test(message);
37
+ }
38
+
39
+ function createPortToolIssue(command, phase, isWindows) {
40
+ if (isWindows) {
41
+ return {
42
+ command,
43
+ phase,
44
+ platform: 'windows',
45
+ summary: `未找到系统命令 ${command},无法${phase === 'kill' ? '关闭' : '检测'}占用端口的进程。`,
46
+ hints: [
47
+ '请确认已安装或启用 Windows 自带网络工具。',
48
+ '请将 `C:\\Windows\\System32` 加入 PATH 后重试。'
49
+ ]
50
+ };
51
+ }
52
+
53
+ return {
54
+ command,
55
+ phase,
56
+ platform: 'unix',
57
+ summary: `未找到 ${command},无法${phase === 'kill' ? '关闭' : '检测'}占用端口的进程。`,
58
+ hints: [
59
+ '请安装 `lsof`,或安装提供 `fuser` 的系统工具后重试。',
60
+ '如果暂时不想安装,也可以改用其他端口。'
61
+ ]
62
+ };
63
+ }
64
+
65
+ function formatPortToolIssue(issue = lastPortToolIssue) {
66
+ if (!issue) {
67
+ return [];
68
+ }
69
+
70
+ return [issue.summary, ...issue.hints];
71
+ }
72
+
9
73
  function parsePidsFromNetstatOutput(output, port) {
10
74
  const target = `:${port}`;
11
75
  const pids = new Set();
@@ -69,6 +133,7 @@ function isPortInUse(port, host = '127.0.0.1') {
69
133
  * 查找占用端口的进程PID(跨平台)
70
134
  */
71
135
  function findProcessByPort(port) {
136
+ clearPortToolIssue();
72
137
  const isWindows = isWindowsLikeRuntime();
73
138
  if (isWindows) {
74
139
  try {
@@ -76,20 +141,28 @@ function findProcessByPort(port) {
76
141
  const result = execSync('netstat -ano', { encoding: 'utf-8' });
77
142
  return parsePidsFromNetstatOutput(result, port);
78
143
  } catch (e) {
144
+ if (isMissingCommandError(e)) {
145
+ rememberPortToolIssue(createPortToolIssue('netstat', 'lookup', true));
146
+ }
79
147
  return [];
80
148
  }
81
149
  }
82
150
 
151
+ let lsofMissing = false;
83
152
  try {
84
153
  // macOS/Linux 使用 lsof
85
154
  const result = execSync(`lsof -ti :${port}`, { encoding: 'utf-8' }).trim();
86
155
  return result.split('\n').filter(pid => pid);
87
156
  } catch (err) {
157
+ lsofMissing = isMissingCommandError(err);
88
158
  // 如果 lsof 失败,尝试使用 fuser(某些 Linux 系统)
89
159
  try {
90
160
  const result = execSync(`fuser ${port}/tcp 2>/dev/null`, { encoding: 'utf-8' }).trim();
91
161
  return result.split(/\s+/).filter(pid => pid);
92
162
  } catch (e) {
163
+ if (lsofMissing && isMissingCommandError(e)) {
164
+ rememberPortToolIssue(createPortToolIssue('lsof / fuser', 'lookup', false));
165
+ }
93
166
  return [];
94
167
  }
95
168
  }
@@ -106,6 +179,7 @@ function killProcessByPort(port) {
106
179
  }
107
180
 
108
181
  const isWindows = isWindowsLikeRuntime();
182
+ let killedAny = false;
109
183
  pids.forEach(pid => {
110
184
  try {
111
185
  if (isWindows) {
@@ -113,12 +187,16 @@ function killProcessByPort(port) {
113
187
  } else {
114
188
  execSync(`kill -9 ${pid}`, { stdio: 'ignore' });
115
189
  }
190
+ killedAny = true;
116
191
  } catch (err) {
192
+ if (isWindows && isMissingCommandError(err)) {
193
+ rememberPortToolIssue(createPortToolIssue('taskkill', 'kill', true));
194
+ }
117
195
  // 忽略单个进程杀掉失败的错误
118
196
  }
119
197
  });
120
198
 
121
- return true;
199
+ return killedAny;
122
200
  } catch (err) {
123
201
  return false;
124
202
  }
@@ -146,6 +224,13 @@ module.exports = {
146
224
  findProcessByPort,
147
225
  killProcessByPort,
148
226
  waitForPortRelease,
227
+ getPortToolIssue,
228
+ formatPortToolIssue,
149
229
  isWindowsLikeRuntime,
150
- parsePidsFromNetstatOutput
230
+ parsePidsFromNetstatOutput,
231
+ _test: {
232
+ isMissingCommandError,
233
+ createPortToolIssue,
234
+ formatPortToolIssue
235
+ }
151
236
  };