coding-tool-x 3.3.6 → 3.3.8
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/CHANGELOG.md +14 -0
- package/dist/web/assets/{Analytics-TtaduRqL.js → Analytics-DLpoDZ2M.js} +1 -1
- package/dist/web/assets/{ConfigTemplates-BP2lLBMN.js → ConfigTemplates-D_hRb55W.js} +1 -1
- package/dist/web/assets/Home-BMoFdAwy.css +1 -0
- package/dist/web/assets/Home-DNwp-0J-.js +1 -0
- package/dist/web/assets/{PluginManager-HmISlyMK.js → PluginManager-JXsyym1s.js} +1 -1
- package/dist/web/assets/{ProjectList-DoN8Hjbu.js → ProjectList-DZWSeb-q.js} +1 -1
- package/dist/web/assets/{SessionList-Da8BYzNi.js → SessionList-Cs624DR3.js} +1 -1
- package/dist/web/assets/{SkillManager-DqLAXh9o.js → SkillManager-bEliz7qz.js} +1 -1
- package/dist/web/assets/{WorkspaceManager-B_TxOgPW.js → WorkspaceManager-J3RecFGn.js} +1 -1
- package/dist/web/assets/{icons-B29onFfZ.js → icons-Cuc23WS7.js} +1 -1
- package/dist/web/assets/index-BXeSvAwU.js +2 -0
- package/dist/web/assets/index-DWAC3Tdv.css +1 -0
- package/dist/web/index.html +3 -3
- package/package.json +3 -2
- package/src/commands/daemon.js +44 -6
- package/src/commands/toggle-proxy.js +100 -5
- package/src/config/default.js +1 -1
- package/src/config/model-metadata.js +2 -2
- package/src/config/model-metadata.json +7 -2
- package/src/config/paths.js +102 -19
- package/src/server/api/channels.js +9 -0
- package/src/server/api/codex-channels.js +9 -0
- package/src/server/api/codex-proxy.js +22 -11
- package/src/server/api/gemini-proxy.js +22 -11
- package/src/server/api/mcp.js +26 -4
- package/src/server/api/oauth-credentials.js +163 -0
- package/src/server/api/opencode-proxy.js +22 -10
- package/src/server/api/plugins.js +3 -1
- package/src/server/api/proxy.js +39 -44
- package/src/server/api/skills.js +91 -13
- package/src/server/codex-proxy-server.js +1 -11
- package/src/server/index.js +26 -2
- package/src/server/services/channels.js +18 -22
- package/src/server/services/codex-channels.js +124 -175
- package/src/server/services/codex-config.js +2 -5
- package/src/server/services/codex-settings-manager.js +12 -348
- package/src/server/services/config-export-service.js +572 -117
- package/src/server/services/gemini-channels.js +11 -9
- package/src/server/services/mcp-client.js +70 -13
- package/src/server/services/mcp-service.js +74 -29
- package/src/server/services/model-detector.js +1 -0
- package/src/server/services/native-keychain.js +243 -0
- package/src/server/services/native-oauth-adapters.js +890 -0
- package/src/server/services/oauth-credentials-service.js +786 -0
- package/src/server/services/oauth-utils.js +49 -0
- package/src/server/services/opencode-channels.js +13 -9
- package/src/server/services/opencode-settings-manager.js +169 -16
- package/src/server/services/plugins-service.js +22 -1
- package/src/server/services/settings-manager.js +13 -0
- package/src/server/services/skill-service.js +712 -332
- package/src/utils/port-helper.js +87 -2
- package/dist/web/assets/Home-BsSioaaB.css +0 -1
- package/dist/web/assets/Home-CbbyopS-.js +0 -1
- package/dist/web/assets/index-By3mDEvx.js +0 -2
- package/dist/web/assets/index-CsWInMQV.css +0 -1
|
@@ -2,6 +2,7 @@ const fs = require('fs');
|
|
|
2
2
|
const path = require('path');
|
|
3
3
|
const crypto = require('crypto');
|
|
4
4
|
const { PATHS, NATIVE_PATHS } = require('../../config/paths');
|
|
5
|
+
const { clearNativeOAuth } = require('./native-oauth-adapters');
|
|
5
6
|
|
|
6
7
|
/**
|
|
7
8
|
* Gemini 渠道管理服务(多渠道架构)
|
|
@@ -252,14 +253,6 @@ function updateChannel(channelId, updates) {
|
|
|
252
253
|
console.log(`[Gemini Single-channel mode] Enabled "${nextChannel.name}", disabled all others`);
|
|
253
254
|
}
|
|
254
255
|
|
|
255
|
-
// Prevent disabling last enabled channel when proxy is OFF
|
|
256
|
-
if (!isProxyRunning && !nextChannel.enabled && oldChannel.enabled) {
|
|
257
|
-
const enabledCount = data.channels.filter(ch => ch.enabled).length;
|
|
258
|
-
if (enabledCount === 0) {
|
|
259
|
-
throw new Error('无法禁用最后一个启用的渠道。请先启用其他渠道或启动动态切换。');
|
|
260
|
-
}
|
|
261
|
-
}
|
|
262
|
-
|
|
263
256
|
saveChannels(data);
|
|
264
257
|
|
|
265
258
|
// Only sync .env when proxy is OFF.
|
|
@@ -299,6 +292,8 @@ function applyChannelToSettings(channelId, channels = null) {
|
|
|
299
292
|
saveChannels(data);
|
|
300
293
|
}
|
|
301
294
|
|
|
295
|
+
clearNativeOAuth('gemini');
|
|
296
|
+
|
|
302
297
|
const geminiDir = getGeminiDir();
|
|
303
298
|
|
|
304
299
|
if (!fs.existsSync(geminiDir)) {
|
|
@@ -455,6 +450,12 @@ function saveChannelOrder(order) {
|
|
|
455
450
|
saveChannels(data);
|
|
456
451
|
}
|
|
457
452
|
|
|
453
|
+
function disableAllChannels() {
|
|
454
|
+
const data = loadChannels();
|
|
455
|
+
data.channels.forEach(ch => { ch.enabled = false; });
|
|
456
|
+
saveChannels(data);
|
|
457
|
+
}
|
|
458
|
+
|
|
458
459
|
module.exports = {
|
|
459
460
|
getChannels,
|
|
460
461
|
createChannel,
|
|
@@ -465,5 +466,6 @@ module.exports = {
|
|
|
465
466
|
saveChannelOrder,
|
|
466
467
|
isProxyConfig,
|
|
467
468
|
getGeminiDir,
|
|
468
|
-
applyChannelToSettings
|
|
469
|
+
applyChannelToSettings,
|
|
470
|
+
disableAllChannels
|
|
469
471
|
};
|
|
@@ -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
|
|
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
|
-
|
|
369
|
-
|
|
370
|
-
|
|
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
|
|
|
@@ -290,15 +290,16 @@ function writeJsonFile(filePath, data) {
|
|
|
290
290
|
* 安全读取 TOML 文件
|
|
291
291
|
*/
|
|
292
292
|
function readTomlFile(filePath, defaultValue = {}) {
|
|
293
|
+
if (!fs.existsSync(filePath)) {
|
|
294
|
+
return defaultValue;
|
|
295
|
+
}
|
|
296
|
+
|
|
293
297
|
try {
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
return toml.parse(content);
|
|
297
|
-
}
|
|
298
|
+
const content = fs.readFileSync(filePath, 'utf-8');
|
|
299
|
+
return toml.parse(content);
|
|
298
300
|
} catch (err) {
|
|
299
|
-
|
|
301
|
+
throw new Error(`Failed to parse ${filePath}: ${err.message}`);
|
|
300
302
|
}
|
|
301
|
-
return defaultValue;
|
|
302
303
|
}
|
|
303
304
|
|
|
304
305
|
/**
|
|
@@ -501,6 +502,19 @@ function resolveWindowsSpawnCommand(command, env, cwd) {
|
|
|
501
502
|
return normalizedCommand;
|
|
502
503
|
}
|
|
503
504
|
|
|
505
|
+
function extractMcpHint(error) {
|
|
506
|
+
return error?.data?.hint || error?.hint || null;
|
|
507
|
+
}
|
|
508
|
+
|
|
509
|
+
function buildMcpFailureResult(error, fallbackMessage, duration) {
|
|
510
|
+
const hint = extractMcpHint(error);
|
|
511
|
+
return {
|
|
512
|
+
message: hint?.title || fallbackMessage || error?.message || '操作失败',
|
|
513
|
+
hint,
|
|
514
|
+
duration
|
|
515
|
+
};
|
|
516
|
+
}
|
|
517
|
+
|
|
504
518
|
// ============================================================================
|
|
505
519
|
// MCP 数据管理
|
|
506
520
|
// ============================================================================
|
|
@@ -580,14 +594,14 @@ async function saveServer(server, options = {}) {
|
|
|
580
594
|
server.apps = normalizeServerApps(server.apps, previousApps || DEFAULT_SERVER_APPS);
|
|
581
595
|
}
|
|
582
596
|
|
|
583
|
-
servers[server.id] = server;
|
|
584
|
-
writeJsonFile(MCP_SERVERS_FILE, servers);
|
|
585
|
-
|
|
586
597
|
// 同步到各平台配置
|
|
587
598
|
if (syncPlatforms) {
|
|
588
599
|
await syncServerToAllPlatforms(server, previousApps);
|
|
589
600
|
}
|
|
590
601
|
|
|
602
|
+
servers[server.id] = server;
|
|
603
|
+
writeJsonFile(MCP_SERVERS_FILE, servers);
|
|
604
|
+
|
|
591
605
|
return server;
|
|
592
606
|
}
|
|
593
607
|
|
|
@@ -602,12 +616,12 @@ async function deleteServer(id) {
|
|
|
602
616
|
return false;
|
|
603
617
|
}
|
|
604
618
|
|
|
605
|
-
delete servers[id];
|
|
606
|
-
writeJsonFile(MCP_SERVERS_FILE, servers);
|
|
607
|
-
|
|
608
619
|
// 从所有平台配置中移除
|
|
609
620
|
await removeServerFromAllPlatforms(id);
|
|
610
621
|
|
|
622
|
+
delete servers[id];
|
|
623
|
+
writeJsonFile(MCP_SERVERS_FILE, servers);
|
|
624
|
+
|
|
611
625
|
return true;
|
|
612
626
|
}
|
|
613
627
|
|
|
@@ -629,8 +643,6 @@ async function toggleServerApp(serverId, app, enabled) {
|
|
|
629
643
|
server.apps[app] = enabled;
|
|
630
644
|
server.updatedAt = Date.now();
|
|
631
645
|
|
|
632
|
-
writeJsonFile(MCP_SERVERS_FILE, servers);
|
|
633
|
-
|
|
634
646
|
// 同步到对应平台
|
|
635
647
|
if (enabled) {
|
|
636
648
|
await syncServerToPlatform(server, app);
|
|
@@ -638,6 +650,8 @@ async function toggleServerApp(serverId, app, enabled) {
|
|
|
638
650
|
await removeServerFromPlatform(serverId, app);
|
|
639
651
|
}
|
|
640
652
|
|
|
653
|
+
writeJsonFile(MCP_SERVERS_FILE, servers);
|
|
654
|
+
|
|
641
655
|
return server;
|
|
642
656
|
}
|
|
643
657
|
|
|
@@ -777,6 +791,7 @@ async function removeServerFromPlatform(serverId, platform) {
|
|
|
777
791
|
console.log(`[MCP] Removed "${serverId}" from ${platform}`);
|
|
778
792
|
} catch (err) {
|
|
779
793
|
console.error(`[MCP] Failed to remove "${serverId}" from ${platform}:`, err.message);
|
|
794
|
+
throw err;
|
|
780
795
|
}
|
|
781
796
|
}
|
|
782
797
|
|
|
@@ -820,14 +835,22 @@ function removeFromClaudeConfig(serverId) {
|
|
|
820
835
|
* 同步到 Codex 配置
|
|
821
836
|
*/
|
|
822
837
|
function syncToCodexConfig(server) {
|
|
838
|
+
if (!fs.existsSync(CODEX_CONFIG_PATH)) {
|
|
839
|
+
throw new Error('Codex config.toml not found. Please run Codex CLI at least once before syncing MCP servers.');
|
|
840
|
+
}
|
|
841
|
+
|
|
823
842
|
const config = readTomlFile(CODEX_CONFIG_PATH, {});
|
|
843
|
+
const nextSpec = convertToCodexFormat(server.server);
|
|
824
844
|
|
|
825
845
|
if (!config.mcp_servers) {
|
|
826
846
|
config.mcp_servers = {};
|
|
827
847
|
}
|
|
828
848
|
|
|
829
|
-
|
|
830
|
-
|
|
849
|
+
if (JSON.stringify(config.mcp_servers[server.id] || null) === JSON.stringify(nextSpec)) {
|
|
850
|
+
return;
|
|
851
|
+
}
|
|
852
|
+
|
|
853
|
+
config.mcp_servers[server.id] = nextSpec;
|
|
831
854
|
|
|
832
855
|
writeTomlFile(CODEX_CONFIG_PATH, config);
|
|
833
856
|
}
|
|
@@ -836,10 +859,17 @@ function syncToCodexConfig(server) {
|
|
|
836
859
|
* 从 Codex 配置移除
|
|
837
860
|
*/
|
|
838
861
|
function removeFromCodexConfig(serverId) {
|
|
862
|
+
if (!fs.existsSync(CODEX_CONFIG_PATH)) {
|
|
863
|
+
return;
|
|
864
|
+
}
|
|
865
|
+
|
|
839
866
|
const config = readTomlFile(CODEX_CONFIG_PATH, {});
|
|
840
867
|
|
|
841
868
|
if (config.mcp_servers && config.mcp_servers[serverId]) {
|
|
842
869
|
delete config.mcp_servers[serverId];
|
|
870
|
+
if (Object.keys(config.mcp_servers).length === 0) {
|
|
871
|
+
delete config.mcp_servers;
|
|
872
|
+
}
|
|
843
873
|
writeTomlFile(CODEX_CONFIG_PATH, config);
|
|
844
874
|
}
|
|
845
875
|
}
|
|
@@ -1301,10 +1331,12 @@ async function testServer(serverId) {
|
|
|
1301
1331
|
return { success: false, message: `不支持的服务器类型: ${type}` };
|
|
1302
1332
|
}
|
|
1303
1333
|
} catch (err) {
|
|
1334
|
+
const failure = buildMcpFailureResult(err, err.message, Date.now() - startTime);
|
|
1304
1335
|
return {
|
|
1305
1336
|
success: false,
|
|
1306
|
-
message:
|
|
1307
|
-
|
|
1337
|
+
message: failure.message,
|
|
1338
|
+
hint: failure.hint,
|
|
1339
|
+
duration: failure.duration
|
|
1308
1340
|
};
|
|
1309
1341
|
}
|
|
1310
1342
|
}
|
|
@@ -1370,10 +1402,11 @@ async function testStdioServer(spec) {
|
|
|
1370
1402
|
|
|
1371
1403
|
child.on('error', (err) => {
|
|
1372
1404
|
if (err.code === 'ENOENT') {
|
|
1373
|
-
const
|
|
1405
|
+
const hint = createMissingCommandHint(command, resolvedCommand, mergedEnv);
|
|
1374
1406
|
done({
|
|
1375
1407
|
success: false,
|
|
1376
|
-
message:
|
|
1408
|
+
message: buildMissingCommandMessage(command, resolvedCommand, mergedEnv),
|
|
1409
|
+
hint,
|
|
1377
1410
|
duration: Date.now() - startTime
|
|
1378
1411
|
});
|
|
1379
1412
|
} else {
|
|
@@ -1423,10 +1456,12 @@ async function testStdioServer(spec) {
|
|
|
1423
1456
|
}, timeout);
|
|
1424
1457
|
|
|
1425
1458
|
} catch (err) {
|
|
1459
|
+
const failure = buildMcpFailureResult(err, `测试失败: ${err.message}`, Date.now() - startTime);
|
|
1426
1460
|
done({
|
|
1427
1461
|
success: false,
|
|
1428
|
-
message:
|
|
1429
|
-
|
|
1462
|
+
message: failure.message,
|
|
1463
|
+
hint: failure.hint,
|
|
1464
|
+
duration: failure.duration
|
|
1430
1465
|
});
|
|
1431
1466
|
}
|
|
1432
1467
|
});
|
|
@@ -1571,11 +1606,14 @@ async function getServerTools(serverId) {
|
|
|
1571
1606
|
mcpClientPool.delete(serverId);
|
|
1572
1607
|
}
|
|
1573
1608
|
|
|
1609
|
+
const failure = buildMcpFailureResult(err, err.message, Date.now() - startTime);
|
|
1574
1610
|
return {
|
|
1575
1611
|
tools: [],
|
|
1576
|
-
duration:
|
|
1612
|
+
duration: failure.duration,
|
|
1577
1613
|
status: 'error',
|
|
1578
|
-
error:
|
|
1614
|
+
error: failure.message,
|
|
1615
|
+
message: failure.message,
|
|
1616
|
+
hint: failure.hint
|
|
1579
1617
|
};
|
|
1580
1618
|
}
|
|
1581
1619
|
}
|
|
@@ -1677,14 +1715,17 @@ async function callServerTool(serverId, toolName, arguments = {}) {
|
|
|
1677
1715
|
mcpClientPool.delete(serverId);
|
|
1678
1716
|
}
|
|
1679
1717
|
|
|
1718
|
+
const failure = buildMcpFailureResult(err, err.message, Date.now() - startTime);
|
|
1680
1719
|
return {
|
|
1681
1720
|
result: {
|
|
1682
|
-
error:
|
|
1721
|
+
error: failure.message,
|
|
1683
1722
|
code: err.code,
|
|
1684
1723
|
data: err.data
|
|
1685
1724
|
},
|
|
1686
|
-
duration:
|
|
1687
|
-
isError: true
|
|
1725
|
+
duration: failure.duration,
|
|
1726
|
+
isError: true,
|
|
1727
|
+
message: failure.message,
|
|
1728
|
+
hint: failure.hint
|
|
1688
1729
|
};
|
|
1689
1730
|
}
|
|
1690
1731
|
}
|
|
@@ -1864,5 +1905,9 @@ module.exports = {
|
|
|
1864
1905
|
callServerTool,
|
|
1865
1906
|
updateServerStatus,
|
|
1866
1907
|
updateServerOrder,
|
|
1867
|
-
exportServers
|
|
1908
|
+
exportServers,
|
|
1909
|
+
_test: {
|
|
1910
|
+
extractMcpHint,
|
|
1911
|
+
buildMcpFailureResult
|
|
1912
|
+
}
|
|
1868
1913
|
};
|
|
@@ -0,0 +1,243 @@
|
|
|
1
|
+
const { spawnSync } = require('child_process');
|
|
2
|
+
|
|
3
|
+
function runCommand(command, args, options = {}) {
|
|
4
|
+
const result = spawnSync(command, args, {
|
|
5
|
+
encoding: 'utf8',
|
|
6
|
+
maxBuffer: 10 * 1024 * 1024,
|
|
7
|
+
...options
|
|
8
|
+
});
|
|
9
|
+
|
|
10
|
+
if (result.error) {
|
|
11
|
+
throw result.error;
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
return result;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
function getWindowsPowerShellCommand() {
|
|
18
|
+
return process.env.ComSpec && process.env.ComSpec.toLowerCase().includes('powershell')
|
|
19
|
+
? process.env.ComSpec
|
|
20
|
+
: 'powershell.exe';
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
function runPowerShell(script, env = {}) {
|
|
24
|
+
return runCommand(getWindowsPowerShellCommand(), [
|
|
25
|
+
'-NoProfile',
|
|
26
|
+
'-NonInteractive',
|
|
27
|
+
'-Command',
|
|
28
|
+
script
|
|
29
|
+
], {
|
|
30
|
+
env: {
|
|
31
|
+
...process.env,
|
|
32
|
+
...env
|
|
33
|
+
}
|
|
34
|
+
});
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
function isSupported() {
|
|
38
|
+
if (process.platform === 'darwin') return true;
|
|
39
|
+
if (process.platform === 'win32') return true;
|
|
40
|
+
|
|
41
|
+
if (process.platform === 'linux') {
|
|
42
|
+
try {
|
|
43
|
+
const result = runCommand('secret-tool', ['--help']);
|
|
44
|
+
return result.status === 0 || result.status === 1;
|
|
45
|
+
} catch {
|
|
46
|
+
return false;
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
return false;
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
function getPassword(service, account) {
|
|
54
|
+
if (!service || !account) {
|
|
55
|
+
return null;
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
try {
|
|
59
|
+
if (process.platform === 'darwin') {
|
|
60
|
+
const result = runCommand('security', [
|
|
61
|
+
'find-generic-password',
|
|
62
|
+
'-a',
|
|
63
|
+
String(account),
|
|
64
|
+
'-w',
|
|
65
|
+
'-s',
|
|
66
|
+
String(service)
|
|
67
|
+
]);
|
|
68
|
+
|
|
69
|
+
if (result.status !== 0) {
|
|
70
|
+
return null;
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
return String(result.stdout || '').trim() || null;
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
if (process.platform === 'linux') {
|
|
77
|
+
const result = runCommand('secret-tool', [
|
|
78
|
+
'lookup',
|
|
79
|
+
'service',
|
|
80
|
+
String(service),
|
|
81
|
+
'account',
|
|
82
|
+
String(account)
|
|
83
|
+
]);
|
|
84
|
+
|
|
85
|
+
if (result.status !== 0) {
|
|
86
|
+
return null;
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
return String(result.stdout || '').trim() || null;
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
if (process.platform === 'win32') {
|
|
93
|
+
const script = `
|
|
94
|
+
Add-Type -AssemblyName System.Runtime.WindowsRuntime
|
|
95
|
+
$vault = New-Object Windows.Security.Credentials.PasswordVault
|
|
96
|
+
try {
|
|
97
|
+
$cred = $vault.Retrieve($env:CC_TOOL_SERVICE, $env:CC_TOOL_ACCOUNT)
|
|
98
|
+
$cred.RetrievePassword()
|
|
99
|
+
[Console]::Out.Write($cred.Password)
|
|
100
|
+
exit 0
|
|
101
|
+
} catch {
|
|
102
|
+
exit 2
|
|
103
|
+
}
|
|
104
|
+
`;
|
|
105
|
+
const result = runPowerShell(script, {
|
|
106
|
+
CC_TOOL_SERVICE: String(service),
|
|
107
|
+
CC_TOOL_ACCOUNT: String(account)
|
|
108
|
+
});
|
|
109
|
+
|
|
110
|
+
if (result.status !== 0) {
|
|
111
|
+
return null;
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
return String(result.stdout || '').trim() || null;
|
|
115
|
+
}
|
|
116
|
+
} catch {
|
|
117
|
+
return null;
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
return null;
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
function setPassword(service, account, password) {
|
|
124
|
+
if (!service || !account) {
|
|
125
|
+
return false;
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
try {
|
|
129
|
+
if (process.platform === 'darwin') {
|
|
130
|
+
const result = runCommand('security', [
|
|
131
|
+
'add-generic-password',
|
|
132
|
+
'-U',
|
|
133
|
+
'-a',
|
|
134
|
+
String(account),
|
|
135
|
+
'-s',
|
|
136
|
+
String(service),
|
|
137
|
+
'-w',
|
|
138
|
+
String(password ?? '')
|
|
139
|
+
]);
|
|
140
|
+
return result.status === 0;
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
if (process.platform === 'linux') {
|
|
144
|
+
deletePassword(service, account);
|
|
145
|
+
const result = runCommand('secret-tool', [
|
|
146
|
+
'store',
|
|
147
|
+
'--label',
|
|
148
|
+
String(service),
|
|
149
|
+
'service',
|
|
150
|
+
String(service),
|
|
151
|
+
'account',
|
|
152
|
+
String(account)
|
|
153
|
+
], {
|
|
154
|
+
input: String(password ?? '')
|
|
155
|
+
});
|
|
156
|
+
return result.status === 0;
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
if (process.platform === 'win32') {
|
|
160
|
+
const script = `
|
|
161
|
+
Add-Type -AssemblyName System.Runtime.WindowsRuntime
|
|
162
|
+
$vault = New-Object Windows.Security.Credentials.PasswordVault
|
|
163
|
+
try {
|
|
164
|
+
$existing = $vault.Retrieve($env:CC_TOOL_SERVICE, $env:CC_TOOL_ACCOUNT)
|
|
165
|
+
$vault.Remove($existing)
|
|
166
|
+
} catch {}
|
|
167
|
+
$credential = New-Object Windows.Security.Credentials.PasswordCredential($env:CC_TOOL_SERVICE, $env:CC_TOOL_ACCOUNT, $env:CC_TOOL_PASSWORD)
|
|
168
|
+
$vault.Add($credential)
|
|
169
|
+
exit 0
|
|
170
|
+
`;
|
|
171
|
+
const result = runPowerShell(script, {
|
|
172
|
+
CC_TOOL_SERVICE: String(service),
|
|
173
|
+
CC_TOOL_ACCOUNT: String(account),
|
|
174
|
+
CC_TOOL_PASSWORD: String(password ?? '')
|
|
175
|
+
});
|
|
176
|
+
return result.status === 0;
|
|
177
|
+
}
|
|
178
|
+
} catch {
|
|
179
|
+
return false;
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
return false;
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
function deletePassword(service, account) {
|
|
186
|
+
if (!service || !account) {
|
|
187
|
+
return false;
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
try {
|
|
191
|
+
if (process.platform === 'darwin') {
|
|
192
|
+
const result = runCommand('security', [
|
|
193
|
+
'delete-generic-password',
|
|
194
|
+
'-a',
|
|
195
|
+
String(account),
|
|
196
|
+
'-s',
|
|
197
|
+
String(service)
|
|
198
|
+
]);
|
|
199
|
+
return result.status === 0;
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
if (process.platform === 'linux') {
|
|
203
|
+
const result = runCommand('secret-tool', [
|
|
204
|
+
'clear',
|
|
205
|
+
'service',
|
|
206
|
+
String(service),
|
|
207
|
+
'account',
|
|
208
|
+
String(account)
|
|
209
|
+
]);
|
|
210
|
+
return result.status === 0;
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
if (process.platform === 'win32') {
|
|
214
|
+
const script = `
|
|
215
|
+
Add-Type -AssemblyName System.Runtime.WindowsRuntime
|
|
216
|
+
$vault = New-Object Windows.Security.Credentials.PasswordVault
|
|
217
|
+
try {
|
|
218
|
+
$credential = $vault.Retrieve($env:CC_TOOL_SERVICE, $env:CC_TOOL_ACCOUNT)
|
|
219
|
+
$vault.Remove($credential)
|
|
220
|
+
exit 0
|
|
221
|
+
} catch {
|
|
222
|
+
exit 2
|
|
223
|
+
}
|
|
224
|
+
`;
|
|
225
|
+
const result = runPowerShell(script, {
|
|
226
|
+
CC_TOOL_SERVICE: String(service),
|
|
227
|
+
CC_TOOL_ACCOUNT: String(account)
|
|
228
|
+
});
|
|
229
|
+
return result.status === 0;
|
|
230
|
+
}
|
|
231
|
+
} catch {
|
|
232
|
+
return false;
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
return false;
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
module.exports = {
|
|
239
|
+
isSupported,
|
|
240
|
+
getPassword,
|
|
241
|
+
setPassword,
|
|
242
|
+
deletePassword
|
|
243
|
+
};
|