ai-account-switch 1.9.0 → 1.12.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 +45 -45
- package/src/accounts/base-account.js +39 -0
- package/src/accounts/ccr-account.js +118 -0
- package/src/accounts/claude-account.js +62 -0
- package/src/accounts/codex-account.js +192 -0
- package/src/accounts/droids-account.js +80 -0
- package/src/accounts/index.js +29 -0
- 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/global-config.js +266 -0
- package/src/config/project-config.js +255 -0
- package/src/config.js +129 -1300
- package/src/config.js.bak +1593 -0
- package/src/constants.js +86 -0
- package/src/generators/base-generator.js +124 -0
- package/src/generators/ccr-generator.js +113 -0
- package/src/generators/claude-generator.js +124 -0
- package/src/generators/codex-generator.js +207 -0
- package/src/generators/droids-generator.js +49 -0
- package/src/generators/index.js +29 -0
- package/src/index.js +63 -1
- package/src/mcp/mcp-manager.js +309 -0
- package/src/ui-server.js +1093 -9
package/src/constants.js
ADDED
|
@@ -0,0 +1,86 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Constants for AI Account Switch (AIS)
|
|
3
|
+
* Centralized constants definition for the entire application
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
// Wire API modes for Codex accounts
|
|
7
|
+
const WIRE_API_MODES = {
|
|
8
|
+
CHAT: 'chat',
|
|
9
|
+
RESPONSES: 'responses',
|
|
10
|
+
ENV: 'env'
|
|
11
|
+
};
|
|
12
|
+
|
|
13
|
+
const DEFAULT_WIRE_API = WIRE_API_MODES.CHAT;
|
|
14
|
+
|
|
15
|
+
// Account types
|
|
16
|
+
const ACCOUNT_TYPES = {
|
|
17
|
+
CLAUDE: 'Claude',
|
|
18
|
+
CODEX: 'Codex',
|
|
19
|
+
CCR: 'CCR',
|
|
20
|
+
DROIDS: 'Droids'
|
|
21
|
+
};
|
|
22
|
+
|
|
23
|
+
// Account type values as array (for iteration)
|
|
24
|
+
const ACCOUNT_TYPE_VALUES = ['Claude', 'Codex', 'CCR', 'Droids', 'Other'];
|
|
25
|
+
|
|
26
|
+
// MCP server scopes
|
|
27
|
+
const MCP_SCOPES = {
|
|
28
|
+
LOCAL: 'local', // Only available in current project
|
|
29
|
+
PROJECT: 'project', // Shared with project members via .mcp.json
|
|
30
|
+
USER: 'user' // Available to all projects for current user (global)
|
|
31
|
+
};
|
|
32
|
+
|
|
33
|
+
const DEFAULT_MCP_SCOPE = MCP_SCOPES.LOCAL;
|
|
34
|
+
|
|
35
|
+
// Model-related environment variable keys
|
|
36
|
+
const MODEL_KEYS = [
|
|
37
|
+
'DEFAULT_MODEL',
|
|
38
|
+
'ANTHROPIC_DEFAULT_OPUS_MODEL',
|
|
39
|
+
'ANTHROPIC_DEFAULT_SONNET_MODEL',
|
|
40
|
+
'ANTHROPIC_DEFAULT_HAIKU_MODEL',
|
|
41
|
+
'CLAUDE_CODE_SUBAGENT_MODEL',
|
|
42
|
+
'ANTHROPIC_MODEL'
|
|
43
|
+
];
|
|
44
|
+
|
|
45
|
+
// Configuration file names
|
|
46
|
+
const CONFIG_FILES = {
|
|
47
|
+
GLOBAL_DIR: '.ai-account-switch',
|
|
48
|
+
GLOBAL_CONFIG: 'config.json',
|
|
49
|
+
PROJECT_CONFIG: '.ais-project-config',
|
|
50
|
+
CLAUDE_DIR: '.claude',
|
|
51
|
+
CLAUDE_LOCAL_CONFIG: 'settings.local.json',
|
|
52
|
+
CLAUDE_USER_CONFIG: 'settings.json',
|
|
53
|
+
CODEX_DIR: '.codex',
|
|
54
|
+
CODEX_CONFIG: 'config.toml',
|
|
55
|
+
CODEX_AUTH: 'auth.json',
|
|
56
|
+
CODEX_PROFILE: '.codex-profile',
|
|
57
|
+
CCR_DIR: '.claude-code-router',
|
|
58
|
+
CCR_CONFIG: 'config.json',
|
|
59
|
+
DROIDS_DIR: '.droids',
|
|
60
|
+
DROIDS_CONFIG: 'config.json',
|
|
61
|
+
MCP_CONFIG: '.mcp.json'
|
|
62
|
+
};
|
|
63
|
+
|
|
64
|
+
// Default CCR port
|
|
65
|
+
const DEFAULT_CCR_PORT = 3456;
|
|
66
|
+
|
|
67
|
+
// Gitignore entries for AIS
|
|
68
|
+
const GITIGNORE_ENTRIES = [
|
|
69
|
+
CONFIG_FILES.PROJECT_CONFIG,
|
|
70
|
+
`${CONFIG_FILES.CLAUDE_DIR}/${CONFIG_FILES.CLAUDE_LOCAL_CONFIG}`,
|
|
71
|
+
CONFIG_FILES.CODEX_PROFILE,
|
|
72
|
+
`${CONFIG_FILES.DROIDS_DIR}/${CONFIG_FILES.DROIDS_CONFIG}`
|
|
73
|
+
];
|
|
74
|
+
|
|
75
|
+
module.exports = {
|
|
76
|
+
WIRE_API_MODES,
|
|
77
|
+
DEFAULT_WIRE_API,
|
|
78
|
+
ACCOUNT_TYPES,
|
|
79
|
+
ACCOUNT_TYPE_VALUES,
|
|
80
|
+
MCP_SCOPES,
|
|
81
|
+
DEFAULT_MCP_SCOPE,
|
|
82
|
+
MODEL_KEYS,
|
|
83
|
+
CONFIG_FILES,
|
|
84
|
+
DEFAULT_CCR_PORT,
|
|
85
|
+
GITIGNORE_ENTRIES
|
|
86
|
+
};
|
|
@@ -0,0 +1,124 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Base Generator Class
|
|
3
|
+
* Provides common functionality for all configuration generators
|
|
4
|
+
*/
|
|
5
|
+
const fs = require('fs');
|
|
6
|
+
const path = require('path');
|
|
7
|
+
const { CONFIG_FILES, DEFAULT_CCR_PORT } = require('../constants');
|
|
8
|
+
|
|
9
|
+
class BaseGenerator {
|
|
10
|
+
constructor(projectRoot) {
|
|
11
|
+
this.projectRoot = projectRoot;
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
/**
|
|
15
|
+
* Ensure a directory exists, create if not
|
|
16
|
+
*/
|
|
17
|
+
ensureDir(dir) {
|
|
18
|
+
if (!fs.existsSync(dir)) {
|
|
19
|
+
fs.mkdirSync(dir, { recursive: true });
|
|
20
|
+
}
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
/**
|
|
24
|
+
* Read and parse a JSON file
|
|
25
|
+
*/
|
|
26
|
+
readJsonFile(filePath, defaultValue = {}) {
|
|
27
|
+
if (fs.existsSync(filePath)) {
|
|
28
|
+
try {
|
|
29
|
+
const data = fs.readFileSync(filePath, 'utf8');
|
|
30
|
+
return JSON.parse(data);
|
|
31
|
+
} catch (error) {
|
|
32
|
+
return defaultValue;
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
return defaultValue;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
/**
|
|
39
|
+
* Write data to a JSON file
|
|
40
|
+
*/
|
|
41
|
+
writeJsonFile(filePath, data) {
|
|
42
|
+
fs.writeFileSync(filePath, JSON.stringify(data, null, 2), 'utf8');
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
/**
|
|
46
|
+
* Read auth.json file
|
|
47
|
+
*/
|
|
48
|
+
readAuthJson(authJsonFile) {
|
|
49
|
+
if (!fs.existsSync(authJsonFile)) {
|
|
50
|
+
return {};
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
try {
|
|
54
|
+
const content = fs.readFileSync(authJsonFile, 'utf8');
|
|
55
|
+
return JSON.parse(content);
|
|
56
|
+
} catch (parseError) {
|
|
57
|
+
const chalk = require('chalk');
|
|
58
|
+
console.warn(
|
|
59
|
+
chalk.yellow(
|
|
60
|
+
`⚠ Warning: Could not parse existing auth.json, will create new file (警告: 无法解析现有 auth.json,将创建新文件)`
|
|
61
|
+
)
|
|
62
|
+
);
|
|
63
|
+
return {};
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
/**
|
|
68
|
+
* Write auth.json file atomically with proper permissions
|
|
69
|
+
*/
|
|
70
|
+
writeAuthJson(authJsonFile, authData) {
|
|
71
|
+
const tempFile = `${authJsonFile}.tmp.${process.pid}`;
|
|
72
|
+
|
|
73
|
+
try {
|
|
74
|
+
// Write to temporary file first (atomic operation)
|
|
75
|
+
fs.writeFileSync(tempFile, JSON.stringify(authData, null, 2), 'utf8');
|
|
76
|
+
|
|
77
|
+
// Set file permissions to 600 (owner read/write only) for security
|
|
78
|
+
if (process.platform !== 'win32') {
|
|
79
|
+
fs.chmodSync(tempFile, 0o600);
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
// Atomically rename temp file to actual file
|
|
83
|
+
fs.renameSync(tempFile, authJsonFile);
|
|
84
|
+
} catch (error) {
|
|
85
|
+
// Clean up temp file if it exists
|
|
86
|
+
if (fs.existsSync(tempFile)) {
|
|
87
|
+
try {
|
|
88
|
+
fs.unlinkSync(tempFile);
|
|
89
|
+
} catch (cleanupError) {
|
|
90
|
+
// Ignore cleanup errors
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
throw error;
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
/**
|
|
98
|
+
* Get CCR port from config file
|
|
99
|
+
*/
|
|
100
|
+
getCcrPort() {
|
|
101
|
+
const ccrConfigFile = path.join(require('os').homedir(), CONFIG_FILES.CCR_DIR, CONFIG_FILES.CCR_CONFIG);
|
|
102
|
+
let port = DEFAULT_CCR_PORT;
|
|
103
|
+
if (fs.existsSync(ccrConfigFile)) {
|
|
104
|
+
try {
|
|
105
|
+
const ccrConfig = JSON.parse(fs.readFileSync(ccrConfigFile, 'utf8'));
|
|
106
|
+
if (ccrConfig.PORT) {
|
|
107
|
+
port = ccrConfig.PORT;
|
|
108
|
+
}
|
|
109
|
+
} catch (e) {
|
|
110
|
+
// Use default port if reading fails
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
return port;
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
/**
|
|
117
|
+
* Generate configuration - to be implemented by subclasses
|
|
118
|
+
*/
|
|
119
|
+
generate(account, options = {}) {
|
|
120
|
+
throw new Error('generate() must be implemented by subclass');
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
module.exports = BaseGenerator;
|
|
@@ -0,0 +1,113 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* CCR Configuration Generator
|
|
3
|
+
* Generates ~/.claude-code-router/config.json and Claude config for CCR
|
|
4
|
+
*/
|
|
5
|
+
const fs = require('fs');
|
|
6
|
+
const path = require('path');
|
|
7
|
+
const os = require('os');
|
|
8
|
+
const BaseGenerator = require('./base-generator');
|
|
9
|
+
const ClaudeGenerator = require('./claude-generator');
|
|
10
|
+
const { CONFIG_FILES } = require('../constants');
|
|
11
|
+
|
|
12
|
+
class CCRGenerator extends BaseGenerator {
|
|
13
|
+
constructor(projectRoot) {
|
|
14
|
+
super(projectRoot);
|
|
15
|
+
this.ccrConfigDir = path.join(os.homedir(), CONFIG_FILES.CCR_DIR);
|
|
16
|
+
this.ccrConfigFile = path.join(this.ccrConfigDir, CONFIG_FILES.CCR_CONFIG);
|
|
17
|
+
this.claudeGenerator = new ClaudeGenerator(projectRoot);
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
/**
|
|
21
|
+
* Generate CCR configuration in ~/.claude-code-router/config.json
|
|
22
|
+
*/
|
|
23
|
+
generateCCRConfig(account) {
|
|
24
|
+
// Read existing config
|
|
25
|
+
let config = {};
|
|
26
|
+
if (fs.existsSync(this.ccrConfigFile)) {
|
|
27
|
+
const data = fs.readFileSync(this.ccrConfigFile, 'utf8');
|
|
28
|
+
config = JSON.parse(data);
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
if (!account.ccrConfig) return;
|
|
32
|
+
|
|
33
|
+
const { providerName, models, defaultModel, backgroundModel, thinkModel } = account.ccrConfig;
|
|
34
|
+
|
|
35
|
+
// Check if provider exists
|
|
36
|
+
const providerIndex = config.Providers?.findIndex(p => p.name === providerName);
|
|
37
|
+
|
|
38
|
+
const provider = {
|
|
39
|
+
api_base_url: account.apiUrl || '',
|
|
40
|
+
api_key: account.apiKey,
|
|
41
|
+
models: models,
|
|
42
|
+
name: providerName
|
|
43
|
+
};
|
|
44
|
+
|
|
45
|
+
if (providerIndex >= 0) {
|
|
46
|
+
config.Providers[providerIndex] = provider;
|
|
47
|
+
} else {
|
|
48
|
+
if (!config.Providers) config.Providers = [];
|
|
49
|
+
config.Providers.push(provider);
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
// Update Router configuration
|
|
53
|
+
if (!config.Router) config.Router = {};
|
|
54
|
+
config.Router.default = `${providerName},${defaultModel}`;
|
|
55
|
+
config.Router.background = `${providerName},${backgroundModel}`;
|
|
56
|
+
config.Router.think = `${providerName},${thinkModel}`;
|
|
57
|
+
|
|
58
|
+
fs.writeFileSync(this.ccrConfigFile, JSON.stringify(config, null, 2), 'utf8');
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
/**
|
|
62
|
+
* Generate Claude configuration for CCR type accounts
|
|
63
|
+
*/
|
|
64
|
+
generateClaudeConfigForCCR(account) {
|
|
65
|
+
const port = this.getCcrPort();
|
|
66
|
+
|
|
67
|
+
// Create .claude directory if it doesn't exist
|
|
68
|
+
const claudeDir = path.join(this.projectRoot, CONFIG_FILES.CLAUDE_DIR);
|
|
69
|
+
const claudeConfigFile = path.join(claudeDir, CONFIG_FILES.CLAUDE_LOCAL_CONFIG);
|
|
70
|
+
this.ensureDir(claudeDir);
|
|
71
|
+
|
|
72
|
+
// Read existing config if it exists
|
|
73
|
+
const existingConfig = this.readJsonFile(claudeConfigFile, {});
|
|
74
|
+
|
|
75
|
+
const claudeConfig = {
|
|
76
|
+
...existingConfig,
|
|
77
|
+
env: {
|
|
78
|
+
...(existingConfig.env || {}),
|
|
79
|
+
ANTHROPIC_AUTH_TOKEN: account.apiKey,
|
|
80
|
+
ANTHROPIC_BASE_URL: `http://127.0.0.1:${port}`
|
|
81
|
+
}
|
|
82
|
+
};
|
|
83
|
+
|
|
84
|
+
// Add custom environment variables if specified
|
|
85
|
+
if (account.customEnv && typeof account.customEnv === 'object') {
|
|
86
|
+
Object.keys(account.customEnv).forEach(key => {
|
|
87
|
+
claudeConfig.env[key] = account.customEnv[key];
|
|
88
|
+
});
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
// Preserve existing permissions if any
|
|
92
|
+
if (!claudeConfig.permissions) {
|
|
93
|
+
claudeConfig.permissions = existingConfig.permissions || {
|
|
94
|
+
allow: [],
|
|
95
|
+
deny: [],
|
|
96
|
+
ask: []
|
|
97
|
+
};
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
// Write Claude configuration
|
|
101
|
+
this.writeJsonFile(claudeConfigFile, claudeConfig);
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
/**
|
|
105
|
+
* Generate both CCR and Claude configurations
|
|
106
|
+
*/
|
|
107
|
+
generate(account, options = {}) {
|
|
108
|
+
this.generateCCRConfig(account);
|
|
109
|
+
this.generateClaudeConfigForCCR(account);
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
module.exports = CCRGenerator;
|
|
@@ -0,0 +1,124 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Claude Configuration Generator
|
|
3
|
+
* Generates .claude/settings.local.json for Claude Code
|
|
4
|
+
*/
|
|
5
|
+
const fs = require('fs');
|
|
6
|
+
const path = require('path');
|
|
7
|
+
const BaseGenerator = require('./base-generator');
|
|
8
|
+
const { MODEL_KEYS, CONFIG_FILES } = require('../constants');
|
|
9
|
+
|
|
10
|
+
class ClaudeGenerator extends BaseGenerator {
|
|
11
|
+
constructor(projectRoot) {
|
|
12
|
+
super(projectRoot);
|
|
13
|
+
this.claudeDir = path.join(this.projectRoot, CONFIG_FILES.CLAUDE_DIR);
|
|
14
|
+
this.claudeConfigFile = path.join(this.claudeDir, CONFIG_FILES.CLAUDE_LOCAL_CONFIG);
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
/**
|
|
18
|
+
* Generate Claude Code .claude/settings.local.json configuration
|
|
19
|
+
*/
|
|
20
|
+
generate(account, options = {}) {
|
|
21
|
+
this.ensureDir(this.claudeDir);
|
|
22
|
+
|
|
23
|
+
// Read existing config if it exists
|
|
24
|
+
const existingConfig = this.readJsonFile(this.claudeConfigFile, {});
|
|
25
|
+
|
|
26
|
+
// Build Claude configuration - preserve existing env but clear model configs
|
|
27
|
+
const existingEnv = existingConfig.env || {};
|
|
28
|
+
const cleanedEnv = {};
|
|
29
|
+
|
|
30
|
+
// Copy all existing env vars except model-related ones
|
|
31
|
+
Object.keys(existingEnv).forEach(key => {
|
|
32
|
+
if (!MODEL_KEYS.includes(key)) {
|
|
33
|
+
cleanedEnv[key] = existingEnv[key];
|
|
34
|
+
}
|
|
35
|
+
});
|
|
36
|
+
|
|
37
|
+
const claudeConfig = {
|
|
38
|
+
...existingConfig,
|
|
39
|
+
env: {
|
|
40
|
+
...cleanedEnv,
|
|
41
|
+
ANTHROPIC_AUTH_TOKEN: account.apiKey
|
|
42
|
+
}
|
|
43
|
+
};
|
|
44
|
+
|
|
45
|
+
// Add API URL if specified
|
|
46
|
+
if (account.apiUrl) {
|
|
47
|
+
claudeConfig.env.ANTHROPIC_BASE_URL = account.apiUrl;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
// Add custom environment variables if specified
|
|
51
|
+
if (account.customEnv && typeof account.customEnv === 'object') {
|
|
52
|
+
Object.keys(account.customEnv).forEach(key => {
|
|
53
|
+
claudeConfig.env[key] = account.customEnv[key];
|
|
54
|
+
});
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
// Add model configuration from active model group
|
|
58
|
+
this._applyModelConfig(claudeConfig, account);
|
|
59
|
+
|
|
60
|
+
// Preserve existing permissions if any
|
|
61
|
+
if (!claudeConfig.permissions) {
|
|
62
|
+
claudeConfig.permissions = existingConfig.permissions || {
|
|
63
|
+
allow: [],
|
|
64
|
+
deny: [],
|
|
65
|
+
ask: []
|
|
66
|
+
};
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
// Add MCP servers if provided
|
|
70
|
+
if (options.mcpServers) {
|
|
71
|
+
claudeConfig.mcpServers = options.mcpServers;
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
// Write Claude configuration
|
|
75
|
+
this.writeJsonFile(this.claudeConfigFile, claudeConfig);
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
/**
|
|
79
|
+
* Apply model configuration from account
|
|
80
|
+
* @private
|
|
81
|
+
*/
|
|
82
|
+
_applyModelConfig(claudeConfig, account) {
|
|
83
|
+
// Add model configuration from active model group
|
|
84
|
+
if (account.modelGroups && account.activeModelGroup) {
|
|
85
|
+
const activeGroup = account.modelGroups[account.activeModelGroup];
|
|
86
|
+
|
|
87
|
+
if (activeGroup && typeof activeGroup === 'object') {
|
|
88
|
+
const defaultModel = activeGroup.DEFAULT_MODEL;
|
|
89
|
+
|
|
90
|
+
// Set DEFAULT_MODEL if specified
|
|
91
|
+
if (defaultModel) {
|
|
92
|
+
claudeConfig.env.DEFAULT_MODEL = defaultModel;
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
// Set other model configs, using DEFAULT_MODEL as fallback if they're not specified
|
|
96
|
+
MODEL_KEYS.slice(1).forEach(key => {
|
|
97
|
+
if (activeGroup[key]) {
|
|
98
|
+
claudeConfig.env[key] = activeGroup[key];
|
|
99
|
+
} else if (defaultModel) {
|
|
100
|
+
claudeConfig.env[key] = defaultModel;
|
|
101
|
+
}
|
|
102
|
+
});
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
// Backward compatibility: support old modelConfig structure
|
|
106
|
+
else if (account.modelConfig && typeof account.modelConfig === 'object') {
|
|
107
|
+
const defaultModel = account.modelConfig.DEFAULT_MODEL;
|
|
108
|
+
|
|
109
|
+
if (defaultModel) {
|
|
110
|
+
claudeConfig.env.DEFAULT_MODEL = defaultModel;
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
MODEL_KEYS.slice(1).forEach(key => {
|
|
114
|
+
if (account.modelConfig[key]) {
|
|
115
|
+
claudeConfig.env[key] = account.modelConfig[key];
|
|
116
|
+
} else if (defaultModel) {
|
|
117
|
+
claudeConfig.env[key] = defaultModel;
|
|
118
|
+
}
|
|
119
|
+
});
|
|
120
|
+
}
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
module.exports = ClaudeGenerator;
|
|
@@ -0,0 +1,207 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Codex Configuration Generator
|
|
3
|
+
* Generates ~/.codex/config.toml and handles auth.json
|
|
4
|
+
*/
|
|
5
|
+
const fs = require('fs');
|
|
6
|
+
const path = require('path');
|
|
7
|
+
const os = require('os');
|
|
8
|
+
const chalk = require('chalk');
|
|
9
|
+
const BaseGenerator = require('./base-generator');
|
|
10
|
+
const { CONFIG_FILES } = require('../constants');
|
|
11
|
+
|
|
12
|
+
class CodexGenerator extends BaseGenerator {
|
|
13
|
+
constructor(projectRoot) {
|
|
14
|
+
super(projectRoot);
|
|
15
|
+
this.codexConfigDir = path.join(os.homedir(), CONFIG_FILES.CODEX_DIR);
|
|
16
|
+
this.codexConfigFile = path.join(this.codexConfigDir, CONFIG_FILES.CODEX_CONFIG);
|
|
17
|
+
this.authJsonFile = path.join(this.codexConfigDir, CONFIG_FILES.CODEX_AUTH);
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
/**
|
|
21
|
+
* Generate Codex profile in global ~/.codex/config.toml
|
|
22
|
+
*/
|
|
23
|
+
generate(account, options = {}) {
|
|
24
|
+
const { WIRE_API_MODES, DEFAULT_WIRE_API } = require('../constants');
|
|
25
|
+
|
|
26
|
+
// Create .codex directory if it doesn't exist
|
|
27
|
+
this.ensureDir(this.codexConfigDir);
|
|
28
|
+
|
|
29
|
+
// Read existing config if it exists
|
|
30
|
+
let existingConfig = '';
|
|
31
|
+
if (fs.existsSync(this.codexConfigFile)) {
|
|
32
|
+
existingConfig = fs.readFileSync(this.codexConfigFile, 'utf8');
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
// Generate profile name based on project path
|
|
36
|
+
const projectName = path.basename(this.projectRoot);
|
|
37
|
+
const profileName = `ais_${projectName}`;
|
|
38
|
+
|
|
39
|
+
// Build profile configuration
|
|
40
|
+
let profileConfig = `\n# AIS Profile for project: ${this.projectRoot}\n`;
|
|
41
|
+
profileConfig += `[profiles.${profileName}]\n`;
|
|
42
|
+
|
|
43
|
+
// For Codex type accounts, use custom provider configuration
|
|
44
|
+
const providerName = `ais_${account.name || 'provider'}`;
|
|
45
|
+
const escapedProviderName = providerName.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
|
|
46
|
+
|
|
47
|
+
profileConfig += `model_provider = "${providerName}"\n`;
|
|
48
|
+
|
|
49
|
+
// Add model configuration
|
|
50
|
+
if (account.model) {
|
|
51
|
+
profileConfig += `model = "${account.model}"\n`;
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
// Smart /v1 path handling
|
|
55
|
+
let baseUrl = account.apiUrl || '';
|
|
56
|
+
if (baseUrl) {
|
|
57
|
+
baseUrl = baseUrl.replace(/\/+$/, '');
|
|
58
|
+
const isDomainOnly = baseUrl.match(/^https?:\/\/[^\/]+$/);
|
|
59
|
+
if (isDomainOnly) {
|
|
60
|
+
baseUrl += '/v1';
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
// Remove existing provider if it exists
|
|
65
|
+
const providerPattern = new RegExp(`\\[model_providers\\.${escapedProviderName}\\][\\s\\S]*?(?=\\n\\[|$)`, 'g');
|
|
66
|
+
existingConfig = existingConfig.replace(providerPattern, '');
|
|
67
|
+
|
|
68
|
+
// Add new provider details
|
|
69
|
+
profileConfig += `\n[model_providers.${providerName}]\n`;
|
|
70
|
+
profileConfig += `name = "${providerName}"\n`;
|
|
71
|
+
|
|
72
|
+
if (baseUrl) {
|
|
73
|
+
profileConfig += `base_url = "${baseUrl}"\n`;
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
// Determine wire_api based on account configuration
|
|
77
|
+
const wireApi = account.wireApi || DEFAULT_WIRE_API;
|
|
78
|
+
|
|
79
|
+
if (wireApi === WIRE_API_MODES.CHAT) {
|
|
80
|
+
profileConfig += `wire_api = "${WIRE_API_MODES.CHAT}"\n`;
|
|
81
|
+
profileConfig += `http_headers = { "Authorization" = "Bearer ${account.apiKey}" }\n`;
|
|
82
|
+
} else if (wireApi === WIRE_API_MODES.RESPONSES) {
|
|
83
|
+
profileConfig += `wire_api = "${WIRE_API_MODES.RESPONSES}"\n`;
|
|
84
|
+
profileConfig += `requires_openai_auth = true\n`;
|
|
85
|
+
this.updateCodexAuthJson(account.apiKey);
|
|
86
|
+
} else if (wireApi === WIRE_API_MODES.ENV) {
|
|
87
|
+
profileConfig += `wire_api = "${WIRE_API_MODES.CHAT}"\n`;
|
|
88
|
+
const envKey = account.envKey || 'AIS_USER_API_KEY';
|
|
89
|
+
profileConfig += `env_key = "${envKey}"\n`;
|
|
90
|
+
this.clearCodexAuthJson();
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
// Remove all old profiles with the same name
|
|
94
|
+
existingConfig = this._removeProfileFromConfig(existingConfig, profileName);
|
|
95
|
+
|
|
96
|
+
// Append new profile
|
|
97
|
+
const newConfig = existingConfig.trimEnd() + '\n' + profileConfig;
|
|
98
|
+
|
|
99
|
+
// Write Codex configuration
|
|
100
|
+
fs.writeFileSync(this.codexConfigFile, newConfig, 'utf8');
|
|
101
|
+
|
|
102
|
+
// Create a helper script in project directory
|
|
103
|
+
const helperScript = path.join(this.projectRoot, '.codex-profile');
|
|
104
|
+
fs.writeFileSync(helperScript, profileName, 'utf8');
|
|
105
|
+
|
|
106
|
+
return { profileName };
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
/**
|
|
110
|
+
* Remove a profile from TOML config string
|
|
111
|
+
* @private
|
|
112
|
+
*/
|
|
113
|
+
_removeProfileFromConfig(configContent, profileName) {
|
|
114
|
+
const lines = configContent.split('\n');
|
|
115
|
+
const cleanedLines = [];
|
|
116
|
+
let skipUntilNextSection = false;
|
|
117
|
+
const profileSectionHeader = `[profiles.${profileName}]`;
|
|
118
|
+
|
|
119
|
+
for (let i = 0; i < lines.length; i++) {
|
|
120
|
+
const line = lines[i];
|
|
121
|
+
const trimmedLine = line.trim();
|
|
122
|
+
|
|
123
|
+
if (trimmedLine === profileSectionHeader) {
|
|
124
|
+
skipUntilNextSection = true;
|
|
125
|
+
|
|
126
|
+
if (cleanedLines.length > 0) {
|
|
127
|
+
const lastLine = cleanedLines[cleanedLines.length - 1].trim();
|
|
128
|
+
if (lastLine.startsWith('# AIS Profile for project:')) {
|
|
129
|
+
cleanedLines.pop();
|
|
130
|
+
}
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
while (cleanedLines.length > 0 && cleanedLines[cleanedLines.length - 1].trim() === '') {
|
|
134
|
+
cleanedLines.pop();
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
continue;
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
if (skipUntilNextSection) {
|
|
141
|
+
if (trimmedLine.startsWith('[')) {
|
|
142
|
+
skipUntilNextSection = false;
|
|
143
|
+
} else {
|
|
144
|
+
continue;
|
|
145
|
+
}
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
cleanedLines.push(line);
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
let result = cleanedLines.join('\n');
|
|
152
|
+
result = result.replace(/\n{3,}/g, '\n\n');
|
|
153
|
+
|
|
154
|
+
return result;
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
/**
|
|
158
|
+
* Clear OPENAI_API_KEY in ~/.codex/auth.json for chat mode
|
|
159
|
+
*/
|
|
160
|
+
clearCodexAuthJson() {
|
|
161
|
+
try {
|
|
162
|
+
this.ensureDir(this.codexConfigDir);
|
|
163
|
+
const authData = this.readAuthJson(this.authJsonFile);
|
|
164
|
+
authData.OPENAI_API_KEY = "";
|
|
165
|
+
this.writeAuthJson(this.authJsonFile, authData);
|
|
166
|
+
|
|
167
|
+
console.log(
|
|
168
|
+
chalk.cyan(
|
|
169
|
+
`✓ Cleared OPENAI_API_KEY in auth.json (chat mode) (已清空 auth.json 中的 OPENAI_API_KEY)`
|
|
170
|
+
)
|
|
171
|
+
);
|
|
172
|
+
} catch (error) {
|
|
173
|
+
console.error(
|
|
174
|
+
chalk.yellow(
|
|
175
|
+
`⚠ Warning: Failed to clear auth.json: ${error.message} (警告: 清空 auth.json 失败)`
|
|
176
|
+
)
|
|
177
|
+
);
|
|
178
|
+
}
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
/**
|
|
182
|
+
* Update ~/.codex/auth.json with API key for responses mode
|
|
183
|
+
*/
|
|
184
|
+
updateCodexAuthJson(apiKey) {
|
|
185
|
+
try {
|
|
186
|
+
this.ensureDir(this.codexConfigDir);
|
|
187
|
+
const authData = this.readAuthJson(this.authJsonFile);
|
|
188
|
+
authData.OPENAI_API_KEY = apiKey;
|
|
189
|
+
this.writeAuthJson(this.authJsonFile, authData);
|
|
190
|
+
|
|
191
|
+
console.log(
|
|
192
|
+
chalk.green(
|
|
193
|
+
`✓ Updated auth.json at: ${this.authJsonFile} (已更新 auth.json)`
|
|
194
|
+
)
|
|
195
|
+
);
|
|
196
|
+
} catch (error) {
|
|
197
|
+
console.error(
|
|
198
|
+
chalk.red(
|
|
199
|
+
`✗ Failed to update auth.json: ${error.message} (更新 auth.json 失败)`
|
|
200
|
+
)
|
|
201
|
+
);
|
|
202
|
+
throw error;
|
|
203
|
+
}
|
|
204
|
+
}
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
module.exports = CodexGenerator;
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Droids Configuration Generator
|
|
3
|
+
* Generates .droids/config.json
|
|
4
|
+
*/
|
|
5
|
+
const fs = require('fs');
|
|
6
|
+
const path = require('path');
|
|
7
|
+
const BaseGenerator = require('./base-generator');
|
|
8
|
+
const { CONFIG_FILES } = require('../constants');
|
|
9
|
+
|
|
10
|
+
class DroidsGenerator extends BaseGenerator {
|
|
11
|
+
constructor(projectRoot) {
|
|
12
|
+
super(projectRoot);
|
|
13
|
+
this.droidsDir = path.join(this.projectRoot, CONFIG_FILES.DROIDS_DIR);
|
|
14
|
+
this.droidsConfigFile = path.join(this.droidsDir, CONFIG_FILES.DROIDS_CONFIG);
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
/**
|
|
18
|
+
* Generate Droids configuration in .droids/config.json
|
|
19
|
+
*/
|
|
20
|
+
generate(account, options = {}) {
|
|
21
|
+
// Create .droids directory if it doesn't exist
|
|
22
|
+
this.ensureDir(this.droidsDir);
|
|
23
|
+
|
|
24
|
+
// Build Droids configuration
|
|
25
|
+
const droidsConfig = {
|
|
26
|
+
apiKey: account.apiKey
|
|
27
|
+
};
|
|
28
|
+
|
|
29
|
+
// Add API URL if specified
|
|
30
|
+
if (account.apiUrl) {
|
|
31
|
+
droidsConfig.baseUrl = account.apiUrl;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
// Add model configuration
|
|
35
|
+
if (account.model) {
|
|
36
|
+
droidsConfig.model = account.model;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
// Add custom environment variables as customSettings
|
|
40
|
+
if (account.customEnv && typeof account.customEnv === 'object') {
|
|
41
|
+
droidsConfig.customSettings = account.customEnv;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
// Write Droids configuration
|
|
45
|
+
this.writeJsonFile(this.droidsConfigFile, droidsConfig);
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
module.exports = DroidsGenerator;
|