ai-account-switch 1.9.0 → 1.11.0
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/.playwright-mcp/grid-view-before.png +0 -0
- package/.playwright-mcp/list-view.png +0 -0
- package/CLAUDE.md +338 -0
- package/README.md +3 -1
- package/package.json +4 -4
- package/src/commands/account.js +68 -0
- package/src/commands/env.js +728 -0
- package/src/commands/helpers.js +32 -0
- package/src/commands/index.js +22 -1
- package/src/commands/mcp.js +71 -13
- package/src/config.js +211 -30
- package/src/index.js +63 -1
- package/src/ui-server.js +1093 -9
package/src/commands/helpers.js
CHANGED
|
@@ -9,6 +9,37 @@ function maskApiKey(apiKey) {
|
|
|
9
9
|
return `${apiKey.substring(0, 4)}****${apiKey.substring(apiKey.length - 4)}`;
|
|
10
10
|
}
|
|
11
11
|
|
|
12
|
+
/**
|
|
13
|
+
* Mask sensitive value for display
|
|
14
|
+
* Shows first 2 chars + fixed 6 stars + last 2 chars for sensitive variables
|
|
15
|
+
* 用于显示敏感环境变量值,前2字符+固定6星号+后2字符
|
|
16
|
+
* @param {string} key - Variable name to check if sensitive
|
|
17
|
+
* @param {string} value - Value to mask
|
|
18
|
+
* @returns {string} Masked value or original value if not sensitive
|
|
19
|
+
*/
|
|
20
|
+
function maskEnvValue(key, value) {
|
|
21
|
+
if (!key || !value) return value;
|
|
22
|
+
|
|
23
|
+
// Check if variable name contains sensitive keywords
|
|
24
|
+
const isSensitive = key.includes('KEY') || key.includes('TOKEN') || key.includes('SECRET') || key.includes('PASSWORD');
|
|
25
|
+
|
|
26
|
+
if (!isSensitive) {
|
|
27
|
+
return value;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
// For sensitive values, show first 2 + fixed 6 stars + last 2
|
|
31
|
+
const strValue = String(value);
|
|
32
|
+
if (strValue.length <= 4) {
|
|
33
|
+
// If value is too short, show all stars
|
|
34
|
+
return '*'.repeat(strValue.length);
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
const firstTwo = strValue.substring(0, 2);
|
|
38
|
+
const lastTwo = strValue.substring(strValue.length - 2);
|
|
39
|
+
|
|
40
|
+
return firstTwo + '******' + lastTwo;
|
|
41
|
+
}
|
|
42
|
+
|
|
12
43
|
/**
|
|
13
44
|
* Validate account by testing API key
|
|
14
45
|
* 验证账号的 API key 是否有效
|
|
@@ -31,5 +62,6 @@ async function validateAccount(apiKey, apiUrl) {
|
|
|
31
62
|
|
|
32
63
|
module.exports = {
|
|
33
64
|
maskApiKey,
|
|
65
|
+
maskEnvValue,
|
|
34
66
|
validateAccount
|
|
35
67
|
};
|
package/src/commands/index.js
CHANGED
|
@@ -42,6 +42,17 @@ const {
|
|
|
42
42
|
testMcpServer
|
|
43
43
|
} = require('./mcp');
|
|
44
44
|
|
|
45
|
+
const {
|
|
46
|
+
listEnv,
|
|
47
|
+
addEnv,
|
|
48
|
+
setEnv,
|
|
49
|
+
removeEnv,
|
|
50
|
+
unsetEnv,
|
|
51
|
+
showEnv,
|
|
52
|
+
clearEnv,
|
|
53
|
+
editEnv
|
|
54
|
+
} = require('./env');
|
|
55
|
+
|
|
45
56
|
module.exports = {
|
|
46
57
|
// Account management commands
|
|
47
58
|
addAccount,
|
|
@@ -74,5 +85,15 @@ module.exports = {
|
|
|
74
85
|
disableMcpServer,
|
|
75
86
|
showEnabledMcpServers,
|
|
76
87
|
syncMcpConfig,
|
|
77
|
-
testMcpServer
|
|
88
|
+
testMcpServer,
|
|
89
|
+
|
|
90
|
+
// Environment variable management commands
|
|
91
|
+
listEnv,
|
|
92
|
+
addEnv,
|
|
93
|
+
setEnv,
|
|
94
|
+
removeEnv,
|
|
95
|
+
unsetEnv,
|
|
96
|
+
showEnv,
|
|
97
|
+
clearEnv,
|
|
98
|
+
editEnv
|
|
78
99
|
};
|
package/src/commands/mcp.js
CHANGED
|
@@ -10,6 +10,8 @@ const config = new ConfigManager();
|
|
|
10
10
|
*/
|
|
11
11
|
async function addMcpServer(name) {
|
|
12
12
|
try {
|
|
13
|
+
const { MCP_SCOPES, DEFAULT_MCP_SCOPE } = require('../config');
|
|
14
|
+
|
|
13
15
|
if (!name) {
|
|
14
16
|
const { serverName } = await inquirer.prompt([{
|
|
15
17
|
type: 'input',
|
|
@@ -79,8 +81,22 @@ async function addMcpServer(name) {
|
|
|
79
81
|
const { description } = await inquirer.prompt([{ type: 'input', name: 'description', message: 'Enter description:', default: '' }]);
|
|
80
82
|
serverData.description = description;
|
|
81
83
|
|
|
84
|
+
// Ask for default scope
|
|
85
|
+
const { scope } = await inquirer.prompt([{
|
|
86
|
+
type: 'list',
|
|
87
|
+
name: 'scope',
|
|
88
|
+
message: 'Select default scope (默认作用范围):',
|
|
89
|
+
choices: [
|
|
90
|
+
{ name: 'local - Only current project (仅当前项目)', value: MCP_SCOPES.LOCAL },
|
|
91
|
+
{ name: 'project - Share with project members (与项目成员共享)', value: MCP_SCOPES.PROJECT },
|
|
92
|
+
{ name: 'user - All projects for current user (当前用户所有项目)', value: MCP_SCOPES.USER }
|
|
93
|
+
],
|
|
94
|
+
default: DEFAULT_MCP_SCOPE
|
|
95
|
+
}]);
|
|
96
|
+
serverData.scope = scope;
|
|
97
|
+
|
|
82
98
|
config.addMcpServer(name, serverData);
|
|
83
|
-
console.log(chalk.green(`✓ MCP server '${name}' added successfully!`));
|
|
99
|
+
console.log(chalk.green(`✓ MCP server '${name}' added successfully with scope: ${scope}!`));
|
|
84
100
|
|
|
85
101
|
// Auto-test the server
|
|
86
102
|
console.log(chalk.cyan('\nTesting server availability...'));
|
|
@@ -101,7 +117,7 @@ async function addMcpServer(name) {
|
|
|
101
117
|
*/
|
|
102
118
|
async function listMcpServers() {
|
|
103
119
|
try {
|
|
104
|
-
const servers = config.
|
|
120
|
+
const servers = config.getAllAvailableMcpServers();
|
|
105
121
|
const projectServers = config.getEnabledMcpServers();
|
|
106
122
|
|
|
107
123
|
if (Object.keys(servers).length === 0) {
|
|
@@ -110,12 +126,16 @@ async function listMcpServers() {
|
|
|
110
126
|
}
|
|
111
127
|
|
|
112
128
|
console.log(chalk.bold.cyan('\n📋 Available MCP servers:\n'));
|
|
113
|
-
console.log(' Name Type Active Description');
|
|
114
|
-
console.log('
|
|
115
|
-
|
|
116
|
-
Object.
|
|
117
|
-
const
|
|
118
|
-
|
|
129
|
+
console.log(' Name Type Active Scope Description');
|
|
130
|
+
console.log(' ───────────────────────────────────────────────────────────────────────');
|
|
131
|
+
|
|
132
|
+
Object.entries(servers).forEach(([key, server]) => {
|
|
133
|
+
const name = server.name || key;
|
|
134
|
+
const type = server.type || 'unknown';
|
|
135
|
+
const isActive = projectServers.includes(name) ? chalk.green('✓') : ' ';
|
|
136
|
+
const scope = server.scope || 'local';
|
|
137
|
+
const scopeDisplay = scope.padEnd(10);
|
|
138
|
+
console.log(` ${name.padEnd(12)} ${type.padEnd(7)} ${isActive} ${scopeDisplay} ${server.description || ''}`);
|
|
119
139
|
});
|
|
120
140
|
|
|
121
141
|
const activeCount = projectServers.length;
|
|
@@ -133,7 +153,7 @@ async function listMcpServers() {
|
|
|
133
153
|
async function showMcpServer(name) {
|
|
134
154
|
try {
|
|
135
155
|
if (!name) {
|
|
136
|
-
const servers = config.
|
|
156
|
+
const servers = config.getAllAvailableMcpServers();
|
|
137
157
|
if (Object.keys(servers).length === 0) {
|
|
138
158
|
console.log(chalk.yellow('No MCP servers configured'));
|
|
139
159
|
return;
|
|
@@ -155,6 +175,7 @@ async function showMcpServer(name) {
|
|
|
155
175
|
|
|
156
176
|
console.log(chalk.bold.cyan(`\n📋 MCP Server: ${name}\n`));
|
|
157
177
|
console.log(chalk.bold('Type:'), server.type);
|
|
178
|
+
console.log(chalk.bold('Scope:'), server.scope || 'local');
|
|
158
179
|
console.log(chalk.bold('Description:'), server.description || 'N/A');
|
|
159
180
|
|
|
160
181
|
if (server.command) console.log(chalk.bold('Command:'), server.command);
|
|
@@ -322,8 +343,17 @@ async function removeMcpServer(name) {
|
|
|
322
343
|
/**
|
|
323
344
|
* Enable MCP server for current project
|
|
324
345
|
*/
|
|
325
|
-
async function enableMcpServer(name) {
|
|
346
|
+
async function enableMcpServer(name, options = {}) {
|
|
326
347
|
try {
|
|
348
|
+
const { MCP_SCOPES, DEFAULT_MCP_SCOPE } = require('../config');
|
|
349
|
+
let scope = options.scope || DEFAULT_MCP_SCOPE;
|
|
350
|
+
|
|
351
|
+
// Validate scope
|
|
352
|
+
if (!Object.values(MCP_SCOPES).includes(scope)) {
|
|
353
|
+
console.log(chalk.red(`✗ Invalid scope '${scope}'. Valid scopes: local, project, user`));
|
|
354
|
+
return;
|
|
355
|
+
}
|
|
356
|
+
|
|
327
357
|
if (!name) {
|
|
328
358
|
const servers = config.getAllMcpServers();
|
|
329
359
|
const enabled = config.getEnabledMcpServers();
|
|
@@ -341,12 +371,38 @@ async function enableMcpServer(name) {
|
|
|
341
371
|
choices: available
|
|
342
372
|
}]);
|
|
343
373
|
name = serverName;
|
|
374
|
+
|
|
375
|
+
// Ask for scope if not provided
|
|
376
|
+
if (!options.scope) {
|
|
377
|
+
const { selectedScope } = await inquirer.prompt([{
|
|
378
|
+
type: 'list',
|
|
379
|
+
name: 'selectedScope',
|
|
380
|
+
message: 'Select scope (作用范围):',
|
|
381
|
+
choices: [
|
|
382
|
+
{ name: 'local - Only current project (仅当前项目)', value: MCP_SCOPES.LOCAL },
|
|
383
|
+
{ name: 'project - Share with project members via .mcp.json (通过 .mcp.json 与项目成员共享)', value: MCP_SCOPES.PROJECT },
|
|
384
|
+
{ name: 'user - All projects for current user (当前用户所有项目)', value: MCP_SCOPES.USER }
|
|
385
|
+
],
|
|
386
|
+
default: DEFAULT_MCP_SCOPE
|
|
387
|
+
}]);
|
|
388
|
+
scope = selectedScope;
|
|
389
|
+
}
|
|
344
390
|
}
|
|
345
391
|
|
|
346
|
-
if (config.enableProjectMcpServer(name)) {
|
|
392
|
+
if (config.enableProjectMcpServer(name, scope)) {
|
|
347
393
|
config.syncMcpConfig();
|
|
348
|
-
console.log(chalk.green(`✓ MCP server '${name}' activated for current project`));
|
|
394
|
+
console.log(chalk.green(`✓ MCP server '${name}' activated for current project with scope: ${scope}`));
|
|
349
395
|
console.log(chalk.green('✓ Claude configuration updated'));
|
|
396
|
+
|
|
397
|
+
// Show scope-specific information
|
|
398
|
+
if (scope === MCP_SCOPES.LOCAL) {
|
|
399
|
+
console.log(chalk.cyan(' Scope: local - Only available in this project'));
|
|
400
|
+
} else if (scope === MCP_SCOPES.PROJECT) {
|
|
401
|
+
console.log(chalk.cyan(' Scope: project - Configuration stored in project, shared with team members'));
|
|
402
|
+
console.log(chalk.gray(' Note: Make sure to commit .ais-project-config to share with your team'));
|
|
403
|
+
} else if (scope === MCP_SCOPES.USER) {
|
|
404
|
+
console.log(chalk.cyan(' Scope: user - Available to all your projects'));
|
|
405
|
+
}
|
|
350
406
|
} else {
|
|
351
407
|
console.log(chalk.red(`✗ MCP server '${name}' not found`));
|
|
352
408
|
}
|
|
@@ -408,7 +464,9 @@ async function showEnabledMcpServers() {
|
|
|
408
464
|
enabled.forEach(name => {
|
|
409
465
|
const server = servers[name];
|
|
410
466
|
if (server) {
|
|
411
|
-
|
|
467
|
+
const serverName = server.name || name;
|
|
468
|
+
const type = server.type || 'unknown';
|
|
469
|
+
console.log(` ${String(serverName).padEnd(12)} ${String(type).padEnd(7)} ${server.description || ''}`);
|
|
412
470
|
}
|
|
413
471
|
});
|
|
414
472
|
|
package/src/config.js
CHANGED
|
@@ -5,7 +5,8 @@ const os = require('os');
|
|
|
5
5
|
// Constants for wire API modes
|
|
6
6
|
const WIRE_API_MODES = {
|
|
7
7
|
CHAT: 'chat',
|
|
8
|
-
RESPONSES: 'responses'
|
|
8
|
+
RESPONSES: 'responses',
|
|
9
|
+
ENV: 'env'
|
|
9
10
|
};
|
|
10
11
|
|
|
11
12
|
const DEFAULT_WIRE_API = WIRE_API_MODES.CHAT;
|
|
@@ -18,6 +19,15 @@ const ACCOUNT_TYPES = {
|
|
|
18
19
|
DROIDS: 'Droids'
|
|
19
20
|
};
|
|
20
21
|
|
|
22
|
+
// Constants for MCP server scopes
|
|
23
|
+
const MCP_SCOPES = {
|
|
24
|
+
LOCAL: 'local', // Only available in current project
|
|
25
|
+
PROJECT: 'project', // Shared with project members via .mcp.json
|
|
26
|
+
USER: 'user' // Available to all projects for current user (global)
|
|
27
|
+
};
|
|
28
|
+
|
|
29
|
+
const DEFAULT_MCP_SCOPE = MCP_SCOPES.LOCAL;
|
|
30
|
+
|
|
21
31
|
/**
|
|
22
32
|
* Cross-platform configuration manager
|
|
23
33
|
* Stores global accounts in user home directory
|
|
@@ -698,23 +708,20 @@ class ConfigManager {
|
|
|
698
708
|
|
|
699
709
|
// Update auth.json with API key
|
|
700
710
|
this.updateCodexAuthJson(account.apiKey);
|
|
711
|
+
} else if (wireApi === WIRE_API_MODES.ENV) {
|
|
712
|
+
// Env mode: use environment variable for authentication
|
|
713
|
+
profileConfig += `wire_api = "${WIRE_API_MODES.CHAT}"\n`;
|
|
714
|
+
const envKey = account.envKey || 'AIS_USER_API_KEY';
|
|
715
|
+
profileConfig += `env_key = "${envKey}"\n`;
|
|
716
|
+
|
|
717
|
+
// Clear auth.json to ensure env mode is used
|
|
718
|
+
this.clearCodexAuthJson();
|
|
701
719
|
}
|
|
702
720
|
}
|
|
703
721
|
|
|
704
722
|
// Remove all old profiles with the same name (including duplicates)
|
|
705
|
-
//
|
|
706
|
-
|
|
707
|
-
`# AIS Profile for project: [^\\n]*${escapedProjectName}[^\\n]*\\n\\[profiles\\.${escapedProfileName}\\]\\n(?:[^\\[].*\\n)*`,
|
|
708
|
-
'g'
|
|
709
|
-
);
|
|
710
|
-
existingConfig = existingConfig.replace(commentedProfilePattern, '');
|
|
711
|
-
|
|
712
|
-
// Pattern 2: Remove standalone profiles without comment
|
|
713
|
-
const standaloneProfilePattern = new RegExp(
|
|
714
|
-
`\\n\\[profiles\\.${escapedProfileName}\\]\\n(?:[^\\[].*\\n)*(?=\\n\\[|$)`,
|
|
715
|
-
'g'
|
|
716
|
-
);
|
|
717
|
-
existingConfig = existingConfig.replace(standaloneProfilePattern, '');
|
|
723
|
+
// Use line-by-line parsing for more reliable cleanup
|
|
724
|
+
existingConfig = this._removeProfileFromConfig(existingConfig, profileName);
|
|
718
725
|
|
|
719
726
|
// Append new profile
|
|
720
727
|
const newConfig = existingConfig.trimEnd() + '\n' + profileConfig;
|
|
@@ -787,6 +794,68 @@ class ConfigManager {
|
|
|
787
794
|
}
|
|
788
795
|
}
|
|
789
796
|
|
|
797
|
+
/**
|
|
798
|
+
* Remove a profile from TOML config string
|
|
799
|
+
* Uses line-by-line parsing for reliable removal of all instances
|
|
800
|
+
* @private
|
|
801
|
+
* @param {string} configContent - The TOML config content
|
|
802
|
+
* @param {string} profileName - The profile name to remove (e.g., "ais_myproject")
|
|
803
|
+
* @returns {string} Cleaned config content
|
|
804
|
+
*/
|
|
805
|
+
_removeProfileFromConfig(configContent, profileName) {
|
|
806
|
+
const lines = configContent.split('\n');
|
|
807
|
+
const cleanedLines = [];
|
|
808
|
+
let skipUntilNextSection = false;
|
|
809
|
+
const profileSectionHeader = `[profiles.${profileName}]`;
|
|
810
|
+
|
|
811
|
+
for (let i = 0; i < lines.length; i++) {
|
|
812
|
+
const line = lines[i];
|
|
813
|
+
const trimmedLine = line.trim();
|
|
814
|
+
|
|
815
|
+
// Check if this is the profile section we want to remove
|
|
816
|
+
if (trimmedLine === profileSectionHeader) {
|
|
817
|
+
skipUntilNextSection = true;
|
|
818
|
+
|
|
819
|
+
// Remove the AIS comment line before it if present
|
|
820
|
+
if (cleanedLines.length > 0) {
|
|
821
|
+
const lastLine = cleanedLines[cleanedLines.length - 1].trim();
|
|
822
|
+
if (lastLine.startsWith('# AIS Profile for project:')) {
|
|
823
|
+
cleanedLines.pop();
|
|
824
|
+
}
|
|
825
|
+
}
|
|
826
|
+
|
|
827
|
+
// Remove trailing empty lines before the profile
|
|
828
|
+
while (cleanedLines.length > 0 && cleanedLines[cleanedLines.length - 1].trim() === '') {
|
|
829
|
+
cleanedLines.pop();
|
|
830
|
+
}
|
|
831
|
+
|
|
832
|
+
continue; // Skip the profile header line
|
|
833
|
+
}
|
|
834
|
+
|
|
835
|
+
// If we're in skip mode, check if we've reached the next section
|
|
836
|
+
if (skipUntilNextSection) {
|
|
837
|
+
// A new section starts with '[' at the beginning (after trimming)
|
|
838
|
+
if (trimmedLine.startsWith('[')) {
|
|
839
|
+
skipUntilNextSection = false;
|
|
840
|
+
// Don't skip this line - it's the start of a new section
|
|
841
|
+
} else {
|
|
842
|
+
// Skip this line as it belongs to the profile we're removing
|
|
843
|
+
continue;
|
|
844
|
+
}
|
|
845
|
+
}
|
|
846
|
+
|
|
847
|
+
cleanedLines.push(line);
|
|
848
|
+
}
|
|
849
|
+
|
|
850
|
+
// Join lines and clean up excessive empty lines
|
|
851
|
+
let result = cleanedLines.join('\n');
|
|
852
|
+
|
|
853
|
+
// Replace 3+ consecutive newlines with just 2 (one blank line)
|
|
854
|
+
result = result.replace(/\n{3,}/g, '\n\n');
|
|
855
|
+
|
|
856
|
+
return result;
|
|
857
|
+
}
|
|
858
|
+
|
|
790
859
|
/**
|
|
791
860
|
* Clear OPENAI_API_KEY in ~/.codex/auth.json for chat mode
|
|
792
861
|
* @deprecated This method is no longer called automatically.
|
|
@@ -923,6 +992,12 @@ class ConfigManager {
|
|
|
923
992
|
addMcpServer(name, serverData) {
|
|
924
993
|
const config = this.readGlobalConfig();
|
|
925
994
|
if (!config.mcpServers) config.mcpServers = {};
|
|
995
|
+
|
|
996
|
+
// Set default scope if not specified
|
|
997
|
+
if (!serverData.scope) {
|
|
998
|
+
serverData.scope = DEFAULT_MCP_SCOPE;
|
|
999
|
+
}
|
|
1000
|
+
|
|
926
1001
|
config.mcpServers[name] = {
|
|
927
1002
|
...serverData,
|
|
928
1003
|
createdAt: config.mcpServers[name]?.createdAt || new Date().toISOString(),
|
|
@@ -1043,9 +1118,11 @@ class ConfigManager {
|
|
|
1043
1118
|
}
|
|
1044
1119
|
|
|
1045
1120
|
/**
|
|
1046
|
-
* Enable MCP server for current project
|
|
1121
|
+
* Enable MCP server for current project with scope
|
|
1122
|
+
* @param {string} serverName - Name of the MCP server
|
|
1123
|
+
* @param {string} scope - Scope: 'local', 'project', or 'user'
|
|
1047
1124
|
*/
|
|
1048
|
-
enableProjectMcpServer(serverName) {
|
|
1125
|
+
enableProjectMcpServer(serverName, scope = DEFAULT_MCP_SCOPE) {
|
|
1049
1126
|
const server = this.getMcpServer(serverName);
|
|
1050
1127
|
if (!server) return false;
|
|
1051
1128
|
|
|
@@ -1063,11 +1140,42 @@ class ConfigManager {
|
|
|
1063
1140
|
const data = fs.readFileSync(projectConfigFile, 'utf8');
|
|
1064
1141
|
const projectConfig = JSON.parse(data);
|
|
1065
1142
|
|
|
1066
|
-
|
|
1067
|
-
|
|
1068
|
-
|
|
1069
|
-
|
|
1143
|
+
// Update server scope in global config
|
|
1144
|
+
const globalConfig = this.readGlobalConfig();
|
|
1145
|
+
if (globalConfig.mcpServers[serverName]) {
|
|
1146
|
+
globalConfig.mcpServers[serverName].scope = scope;
|
|
1147
|
+
this.saveGlobalConfig(globalConfig);
|
|
1070
1148
|
}
|
|
1149
|
+
|
|
1150
|
+
// Handle different scopes
|
|
1151
|
+
if (scope === MCP_SCOPES.LOCAL) {
|
|
1152
|
+
// Local scope: only enable for current project
|
|
1153
|
+
if (!projectConfig.enabledMcpServers) projectConfig.enabledMcpServers = [];
|
|
1154
|
+
if (!projectConfig.enabledMcpServers.includes(serverName)) {
|
|
1155
|
+
projectConfig.enabledMcpServers.push(serverName);
|
|
1156
|
+
}
|
|
1157
|
+
} else if (scope === MCP_SCOPES.PROJECT) {
|
|
1158
|
+
// Project scope: store in project config for sharing
|
|
1159
|
+
if (!projectConfig.projectMcpServers) projectConfig.projectMcpServers = {};
|
|
1160
|
+
projectConfig.projectMcpServers[serverName] = {
|
|
1161
|
+
...server,
|
|
1162
|
+
scope: MCP_SCOPES.PROJECT
|
|
1163
|
+
};
|
|
1164
|
+
|
|
1165
|
+
// Also add to enabled list
|
|
1166
|
+
if (!projectConfig.enabledMcpServers) projectConfig.enabledMcpServers = [];
|
|
1167
|
+
if (!projectConfig.enabledMcpServers.includes(serverName)) {
|
|
1168
|
+
projectConfig.enabledMcpServers.push(serverName);
|
|
1169
|
+
}
|
|
1170
|
+
} else if (scope === MCP_SCOPES.USER) {
|
|
1171
|
+
// User scope: mark as globally enabled
|
|
1172
|
+
if (!projectConfig.enabledMcpServers) projectConfig.enabledMcpServers = [];
|
|
1173
|
+
if (!projectConfig.enabledMcpServers.includes(serverName)) {
|
|
1174
|
+
projectConfig.enabledMcpServers.push(serverName);
|
|
1175
|
+
}
|
|
1176
|
+
}
|
|
1177
|
+
|
|
1178
|
+
fs.writeFileSync(projectConfigFile, JSON.stringify(projectConfig, null, 2), 'utf8');
|
|
1071
1179
|
return true;
|
|
1072
1180
|
} catch (error) {
|
|
1073
1181
|
throw new Error(`Failed to enable MCP server: ${error.message}`);
|
|
@@ -1108,13 +1216,58 @@ class ConfigManager {
|
|
|
1108
1216
|
|
|
1109
1217
|
/**
|
|
1110
1218
|
* Get enabled MCP servers for current project
|
|
1219
|
+
* Includes local, project, and user-scoped servers
|
|
1111
1220
|
*/
|
|
1112
1221
|
getEnabledMcpServers() {
|
|
1113
|
-
|
|
1222
|
+
const projectServers = this.getProjectMcpServers();
|
|
1223
|
+
const globalServers = this.getAllMcpServers();
|
|
1224
|
+
|
|
1225
|
+
// Add user-scoped servers that are globally enabled
|
|
1226
|
+
const userScopedServers = Object.keys(globalServers).filter(name =>
|
|
1227
|
+
globalServers[name].scope === MCP_SCOPES.USER && !projectServers.includes(name)
|
|
1228
|
+
);
|
|
1229
|
+
|
|
1230
|
+
return [...projectServers, ...userScopedServers];
|
|
1231
|
+
}
|
|
1232
|
+
|
|
1233
|
+
/**
|
|
1234
|
+
* Get all available MCP servers including project-scoped ones
|
|
1235
|
+
*/
|
|
1236
|
+
getAllAvailableMcpServers() {
|
|
1237
|
+
const globalServers = this.getAllMcpServers();
|
|
1238
|
+
const projectRoot = this.findProjectRoot();
|
|
1239
|
+
|
|
1240
|
+
if (!projectRoot) {
|
|
1241
|
+
return globalServers;
|
|
1242
|
+
}
|
|
1243
|
+
|
|
1244
|
+
try {
|
|
1245
|
+
const projectConfigFile = path.join(projectRoot, this.projectConfigFilename);
|
|
1246
|
+
if (!fs.existsSync(projectConfigFile)) {
|
|
1247
|
+
return globalServers;
|
|
1248
|
+
}
|
|
1249
|
+
|
|
1250
|
+
const data = fs.readFileSync(projectConfigFile, 'utf8');
|
|
1251
|
+
const projectConfig = JSON.parse(data);
|
|
1252
|
+
|
|
1253
|
+
// Merge global and project servers
|
|
1254
|
+
const allServers = { ...globalServers };
|
|
1255
|
+
|
|
1256
|
+
if (projectConfig.projectMcpServers) {
|
|
1257
|
+
Object.entries(projectConfig.projectMcpServers).forEach(([name, server]) => {
|
|
1258
|
+
allServers[name] = server;
|
|
1259
|
+
});
|
|
1260
|
+
}
|
|
1261
|
+
|
|
1262
|
+
return allServers;
|
|
1263
|
+
} catch (error) {
|
|
1264
|
+
return globalServers;
|
|
1265
|
+
}
|
|
1114
1266
|
}
|
|
1115
1267
|
|
|
1116
1268
|
/**
|
|
1117
1269
|
* Get Claude Code user config path (cross-platform)
|
|
1270
|
+
* Priority: ~/.claude/settings.json > platform-specific paths > legacy paths
|
|
1118
1271
|
*/
|
|
1119
1272
|
getClaudeUserConfigPath() {
|
|
1120
1273
|
const platform = process.platform;
|
|
@@ -1122,12 +1275,30 @@ class ConfigManager {
|
|
|
1122
1275
|
|
|
1123
1276
|
if (!home) return null;
|
|
1124
1277
|
|
|
1125
|
-
//
|
|
1126
|
-
const locations = [
|
|
1127
|
-
|
|
1128
|
-
|
|
1129
|
-
|
|
1278
|
+
// Priority order for Claude user config
|
|
1279
|
+
const locations = [];
|
|
1280
|
+
|
|
1281
|
+
// Primary location: ~/.claude/settings.json (modern Claude Code)
|
|
1282
|
+
locations.push(path.join(home, '.claude', 'settings.json'));
|
|
1130
1283
|
|
|
1284
|
+
// Platform-specific locations
|
|
1285
|
+
if (platform === 'win32') {
|
|
1286
|
+
// Windows: %APPDATA%\claude\settings.json
|
|
1287
|
+
const appData = process.env.APPDATA;
|
|
1288
|
+
if (appData) {
|
|
1289
|
+
locations.push(path.join(appData, 'claude', 'settings.json'));
|
|
1290
|
+
locations.push(path.join(appData, 'claude', 'config.json'));
|
|
1291
|
+
}
|
|
1292
|
+
} else {
|
|
1293
|
+
// macOS/Linux: ~/.config/claude/settings.json
|
|
1294
|
+
locations.push(path.join(home, '.config', 'claude', 'settings.json'));
|
|
1295
|
+
locations.push(path.join(home, '.config', 'claude', 'config.json'));
|
|
1296
|
+
}
|
|
1297
|
+
|
|
1298
|
+
// Legacy fallback: ~/.claude.json
|
|
1299
|
+
locations.push(path.join(home, '.claude.json'));
|
|
1300
|
+
|
|
1301
|
+
// Return first existing location
|
|
1131
1302
|
for (const loc of locations) {
|
|
1132
1303
|
if (fs.existsSync(loc)) {
|
|
1133
1304
|
return loc;
|
|
@@ -1353,14 +1524,22 @@ class ConfigManager {
|
|
|
1353
1524
|
|
|
1354
1525
|
// Get enabled MCP servers
|
|
1355
1526
|
const enabledServers = this.getEnabledMcpServers();
|
|
1356
|
-
const allServers = this.
|
|
1527
|
+
const allServers = this.getAllAvailableMcpServers();
|
|
1528
|
+
|
|
1529
|
+
// Filter servers by scope:
|
|
1530
|
+
// - Only 'project' scoped servers should be in .mcp.json (shared with team)
|
|
1531
|
+
// - 'local' and 'user' scoped servers should NOT be in .mcp.json
|
|
1532
|
+
const projectScopedServers = enabledServers.filter(serverName => {
|
|
1533
|
+
const server = allServers[serverName];
|
|
1534
|
+
return server && server.scope === MCP_SCOPES.PROJECT;
|
|
1535
|
+
});
|
|
1357
1536
|
|
|
1358
|
-
if (
|
|
1537
|
+
if (projectScopedServers.length > 0) {
|
|
1359
1538
|
const mcpConfig = {
|
|
1360
1539
|
mcpServers: {}
|
|
1361
1540
|
};
|
|
1362
1541
|
|
|
1363
|
-
|
|
1542
|
+
projectScopedServers.forEach(serverName => {
|
|
1364
1543
|
const server = allServers[serverName];
|
|
1365
1544
|
if (server) {
|
|
1366
1545
|
const serverConfig = {};
|
|
@@ -1395,7 +1574,7 @@ class ConfigManager {
|
|
|
1395
1574
|
|
|
1396
1575
|
fs.writeFileSync(mcpConfigFile, JSON.stringify(mcpConfig, null, 2), 'utf8');
|
|
1397
1576
|
} else {
|
|
1398
|
-
// Remove .mcp.json if no servers are enabled
|
|
1577
|
+
// Remove .mcp.json if no project-scoped servers are enabled
|
|
1399
1578
|
if (fs.existsSync(mcpConfigFile)) {
|
|
1400
1579
|
fs.unlinkSync(mcpConfigFile);
|
|
1401
1580
|
}
|
|
@@ -1410,3 +1589,5 @@ module.exports = ConfigManager;
|
|
|
1410
1589
|
module.exports.WIRE_API_MODES = WIRE_API_MODES;
|
|
1411
1590
|
module.exports.DEFAULT_WIRE_API = DEFAULT_WIRE_API;
|
|
1412
1591
|
module.exports.ACCOUNT_TYPES = ACCOUNT_TYPES;
|
|
1592
|
+
module.exports.MCP_SCOPES = MCP_SCOPES;
|
|
1593
|
+
module.exports.DEFAULT_MCP_SCOPE = DEFAULT_MCP_SCOPE;
|
package/src/index.js
CHANGED
|
@@ -27,7 +27,15 @@ const {
|
|
|
27
27
|
disableMcpServer,
|
|
28
28
|
showEnabledMcpServers,
|
|
29
29
|
syncMcpConfig,
|
|
30
|
-
testMcpServer
|
|
30
|
+
testMcpServer,
|
|
31
|
+
listEnv,
|
|
32
|
+
addEnv,
|
|
33
|
+
setEnv,
|
|
34
|
+
removeEnv,
|
|
35
|
+
unsetEnv,
|
|
36
|
+
showEnv,
|
|
37
|
+
clearEnv,
|
|
38
|
+
editEnv
|
|
31
39
|
} = require('./commands');
|
|
32
40
|
|
|
33
41
|
// Package info
|
|
@@ -167,6 +175,7 @@ mcpCommand
|
|
|
167
175
|
mcpCommand
|
|
168
176
|
.command('enable [name]')
|
|
169
177
|
.description('Activate MCP server for current project (为当前项目激活 MCP 服务器)')
|
|
178
|
+
.option('-s, --scope <scope>', 'Scope: local (default), project, or user (作用范围: local(默认), project, user)')
|
|
170
179
|
.action(enableMcpServer);
|
|
171
180
|
|
|
172
181
|
mcpCommand
|
|
@@ -189,6 +198,58 @@ mcpCommand
|
|
|
189
198
|
.description('Test MCP server availability (测试 MCP 服务器可用性)')
|
|
190
199
|
.action(testMcpServer);
|
|
191
200
|
|
|
201
|
+
// Environment variable management commands
|
|
202
|
+
const envCommand = program
|
|
203
|
+
.command('env')
|
|
204
|
+
.description('Manage Claude environment variables (管理 Claude 环境变量)');
|
|
205
|
+
|
|
206
|
+
envCommand
|
|
207
|
+
.command('list')
|
|
208
|
+
.alias('ls')
|
|
209
|
+
.description('List all environment variables from project and user configs (列出项目和用户配置中的所有环境变量)')
|
|
210
|
+
.action(listEnv);
|
|
211
|
+
|
|
212
|
+
envCommand
|
|
213
|
+
.command('add')
|
|
214
|
+
.description('Add or update an environment variable interactively (交互式添加或更新环境变量)')
|
|
215
|
+
.action(addEnv);
|
|
216
|
+
|
|
217
|
+
envCommand
|
|
218
|
+
.command('set <key> <value>')
|
|
219
|
+
.description('Set an environment variable (non-interactive) (设置环境变量,非交互式)')
|
|
220
|
+
.option('-l, --level <level>', 'Configuration level: project or user (default: user) (配置级别: project 或 user,默认: user)')
|
|
221
|
+
.action(setEnv);
|
|
222
|
+
|
|
223
|
+
envCommand
|
|
224
|
+
.command('remove')
|
|
225
|
+
.alias('rm')
|
|
226
|
+
.description('Remove an environment variable interactively (交互式删除环境变量)')
|
|
227
|
+
.action(removeEnv);
|
|
228
|
+
|
|
229
|
+
envCommand
|
|
230
|
+
.command('unset <key>')
|
|
231
|
+
.description('Remove an environment variable by key (non-interactive) (通过键名删除环境变量,非交互式)')
|
|
232
|
+
.option('-l, --level <level>', 'Configuration level: project or user (default: user) (配置级别: project 或 user,默认: user)')
|
|
233
|
+
.action(unsetEnv);
|
|
234
|
+
|
|
235
|
+
envCommand
|
|
236
|
+
.command('show <key>')
|
|
237
|
+
.description('Show an environment variable value (显示环境变量值)')
|
|
238
|
+
.option('-l, --level <level>', 'Configuration level: project or user (if not specified, searches both) (配置级别: project 或 user,未指定则搜索两者)')
|
|
239
|
+
.action(showEnv);
|
|
240
|
+
|
|
241
|
+
envCommand
|
|
242
|
+
.command('clear')
|
|
243
|
+
.description('Clear all environment variables at a level (清空某个级别的所有环境变量)')
|
|
244
|
+
.option('-l, --level <level>', 'Configuration level: project or user (default: user) (配置级别: project 或 user,默认: user)')
|
|
245
|
+
.action(clearEnv);
|
|
246
|
+
|
|
247
|
+
envCommand
|
|
248
|
+
.command('edit')
|
|
249
|
+
.description('Edit environment variables interactively (交互式编辑环境变量)')
|
|
250
|
+
.option('-l, --level <level>', 'Configuration level: project or user (default: user) (配置级别: project 或 user,默认: user)')
|
|
251
|
+
.action(editEnv);
|
|
252
|
+
|
|
192
253
|
// Help command
|
|
193
254
|
program
|
|
194
255
|
.command('help')
|
|
@@ -211,6 +272,7 @@ program
|
|
|
211
272
|
console.log(' ui Start web-based account manager UI (启动基于 Web 的账号管理界面)');
|
|
212
273
|
console.log(' model Manage model groups (管理模型组)');
|
|
213
274
|
console.log(' mcp Manage MCP servers (管理 MCP 服务器)');
|
|
275
|
+
console.log(' env Manage Claude environment variables (管理 Claude 环境变量)');
|
|
214
276
|
console.log(' help Display this help message (显示此帮助信息)');
|
|
215
277
|
console.log(' version Show version number (显示版本号)\n');
|
|
216
278
|
|