@pikecode/api-key-manager 1.0.20 → 1.0.21

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/bin/akm.js CHANGED
@@ -61,9 +61,12 @@ program
61
61
  program
62
62
  .command('list')
63
63
  .description('列出所有API密钥配置')
64
- .action(async () => {
64
+ .option('--codex', '仅显示 Codex CLI 供应商')
65
+ .option('--claude', '仅显示 Claude Code 供应商')
66
+ .action(async (options) => {
65
67
  try {
66
- await registry.executeCommand('list');
68
+ const filter = options.codex ? 'codex' : (options.claude ? 'claude' : null);
69
+ await registry.executeCommand('list', filter);
67
70
  } catch (error) {
68
71
  console.error(chalk.red('❌ 列表失败:'), error.message);
69
72
  process.exit(1);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@pikecode/api-key-manager",
3
- "version": "1.0.20",
3
+ "version": "1.0.21",
4
4
  "description": "A CLI tool for managing and switching multiple API provider configurations",
5
5
  "main": "src/index.js",
6
6
  "bin": {
@@ -194,6 +194,16 @@ class ProviderAdder extends BaseCommand {
194
194
  return true;
195
195
  }
196
196
  },
197
+ {
198
+ type: 'list',
199
+ name: 'ideName',
200
+ message: '选择要管理的 IDE:',
201
+ choices: [
202
+ { name: 'Claude Code (Anthropic)', value: 'claude' },
203
+ { name: 'Codex CLI (OpenAI)', value: 'codex' }
204
+ ],
205
+ default: 'claude'
206
+ },
197
207
  {
198
208
  type: 'list',
199
209
  name: 'authMode',
@@ -203,7 +213,8 @@ class ProviderAdder extends BaseCommand {
203
213
  { name: '🔐 认证令牌模式 (仅 ANTHROPIC_AUTH_TOKEN) - 适用于某些服务商', value: 'auth_token' },
204
214
  { name: '🌐 OAuth令牌模式 (CLAUDE_CODE_OAUTH_TOKEN) - 适用于官方Claude Code', value: 'oauth_token' }
205
215
  ],
206
- default: 'api_key'
216
+ default: 'api_key',
217
+ when: (answers) => answers.ideName !== 'codex'
207
218
  },
208
219
  {
209
220
  type: 'list',
@@ -214,7 +225,7 @@ class ProviderAdder extends BaseCommand {
214
225
  { name: '🔐 ANTHROPIC_AUTH_TOKEN - 认证令牌', value: 'auth_token' }
215
226
  ],
216
227
  default: 'api_key',
217
- when: (answers) => answers.authMode === 'api_key'
228
+ when: (answers) => answers.ideName !== 'codex' && answers.authMode === 'api_key'
218
229
  },
219
230
  {
220
231
  type: 'input',
@@ -238,7 +249,7 @@ class ProviderAdder extends BaseCommand {
238
249
  if (error) return error;
239
250
  return true;
240
251
  },
241
- when: (answers) => answers.authMode === 'api_key' || answers.authMode === 'auth_token'
252
+ when: (answers) => answers.ideName !== 'codex' && (answers.authMode === 'api_key' || answers.authMode === 'auth_token')
242
253
  },
243
254
  {
244
255
  type: 'input',
@@ -261,6 +272,33 @@ class ProviderAdder extends BaseCommand {
261
272
  if (error) return error;
262
273
  return true;
263
274
  }
275
+ ,
276
+ when: (answers) => answers.ideName !== 'codex'
277
+ },
278
+ {
279
+ type: 'input',
280
+ name: 'baseUrl',
281
+ message: '请输入 OpenAI API 基础URL (如使用官方API可留空):',
282
+ default: '',
283
+ validate: (input) => {
284
+ if (!input) return true;
285
+ const error = validator.validateUrl(input);
286
+ if (error) return error;
287
+ return true;
288
+ },
289
+ when: (answers) => answers.ideName === 'codex'
290
+ },
291
+ {
292
+ type: 'input',
293
+ name: 'authToken',
294
+ message: '请输入 OpenAI API Key (OPENAI_API_KEY):',
295
+ validate: (input) => {
296
+ if (!input) return 'API Key 不能为空';
297
+ const error = validator.validateToken(input);
298
+ if (error) return error;
299
+ return true;
300
+ },
301
+ when: (answers) => answers.ideName === 'codex'
264
302
  },
265
303
  {
266
304
  type: 'confirm',
@@ -272,19 +310,27 @@ class ProviderAdder extends BaseCommand {
272
310
  type: 'confirm',
273
311
  name: 'configureLaunchArgs',
274
312
  message: '是否配置启动参数?',
275
- default: false
313
+ default: false,
314
+ when: (answers) => answers.ideName !== 'codex'
276
315
  },
277
316
  {
278
317
  type: 'confirm',
279
318
  name: 'configureModels',
280
319
  message: '是否配置模型参数?',
281
- default: false
320
+ default: false,
321
+ when: (answers) => answers.ideName !== 'codex'
282
322
  }
283
323
  ]);
284
324
 
285
325
  // 移除 ESC 键监听
286
326
  this.removeESCListener(escListener);
287
327
 
328
+ if (answers.ideName === 'codex') {
329
+ answers.authMode = 'openai_api_key';
330
+ answers.tokenType = null;
331
+ answers.codexFiles = null;
332
+ }
333
+
288
334
  await this.saveProvider(answers);
289
335
  } catch (error) {
290
336
  // 移除 ESC 键监听
@@ -315,10 +361,12 @@ class ProviderAdder extends BaseCommand {
315
361
 
316
362
  await this.configManager.addProvider(answers.name, {
317
363
  displayName: answers.displayName || answers.name,
364
+ ideName: answers.ideName || 'claude',
318
365
  baseUrl: answers.baseUrl,
319
366
  authToken: answers.authToken,
320
367
  authMode: answers.authMode,
321
368
  tokenType: answers.tokenType, // 仅在 authMode 为 'api_key' 时使用
369
+ codexFiles: answers.codexFiles || null,
322
370
  launchArgs,
323
371
  primaryModel: modelConfig.primaryModel,
324
372
  smallFastModel: modelConfig.smallFastModel,
@@ -472,6 +520,18 @@ class ProviderAdder extends BaseCommand {
472
520
  console.log(chalk.gray(` 名称: ${answers.name}`));
473
521
  console.log(chalk.gray(` 显示名称: ${finalDisplayName}`));
474
522
 
523
+ if (answers.ideName === 'codex') {
524
+ console.log(chalk.gray(' IDE: Codex CLI'));
525
+ if (answers.baseUrl) {
526
+ console.log(chalk.gray(` OPENAI_BASE_URL: ${answers.baseUrl}`));
527
+ }
528
+ if (answers.authToken) {
529
+ console.log(chalk.gray(` OPENAI_API_KEY: ${answers.authToken}`));
530
+ }
531
+ console.log(chalk.green('\n🎉 供应商添加完成!正在返回主界面...'));
532
+ return;
533
+ }
534
+
475
535
  const authModeDisplay = {
476
536
  api_key: '通用API密钥模式',
477
537
  auth_token: '认证令牌模式 (仅 ANTHROPIC_AUTH_TOKEN)',
@@ -14,7 +14,7 @@ class CurrentConfig {
14
14
 
15
15
  if (!currentProvider) {
16
16
  Logger.warning('未设置当前供应商');
17
- Logger.info('请使用 "cc <供应商名>" 切换供应商');
17
+ Logger.info('请使用 "akm <供应商名>" 切换供应商');
18
18
  return;
19
19
  }
20
20
 
@@ -24,6 +24,29 @@ class CurrentConfig {
24
24
  console.log(chalk.green(`供应商: ${currentProvider.displayName}`));
25
25
  console.log(chalk.gray(`内部名称: ${currentProvider.name}`));
26
26
 
27
+ if (currentProvider.ideName === 'codex') {
28
+ console.log(chalk.gray('IDE: Codex CLI'));
29
+ if (currentProvider.baseUrl) {
30
+ console.log(chalk.gray(`OPENAI_BASE_URL: ${currentProvider.baseUrl}`));
31
+ }
32
+ if (currentProvider.authToken) {
33
+ console.log(chalk.gray(`OPENAI_API_KEY: ${currentProvider.authToken}`));
34
+ }
35
+ console.log(chalk.gray(`创建时间: ${new Date(currentProvider.createdAt).toLocaleString()}`));
36
+ console.log(chalk.gray(`最后使用: ${new Date(currentProvider.lastUsed).toLocaleString()}`));
37
+ console.log(chalk.gray('═'.repeat(60)));
38
+
39
+ console.log(chalk.blue('\n🔧 环境变量设置:'));
40
+ if (currentProvider.baseUrl) {
41
+ console.log(chalk.gray(`set OPENAI_BASE_URL=${currentProvider.baseUrl}`));
42
+ }
43
+ if (currentProvider.authToken) {
44
+ console.log(chalk.gray(`set OPENAI_API_KEY=${currentProvider.authToken}`));
45
+ }
46
+ console.log(chalk.gray('codex'));
47
+ return;
48
+ }
49
+
27
50
  // 显示认证模式
28
51
  const authModeDisplay = {
29
52
  api_key: '通用API密钥模式',
@@ -91,4 +114,4 @@ async function currentCommand() {
91
114
  await current.show();
92
115
  }
93
116
 
94
- module.exports = { currentCommand, CurrentConfig };
117
+ module.exports = { currentCommand, CurrentConfig };
@@ -64,6 +64,8 @@ class ProviderEditor extends BaseCommand {
64
64
  console.log(UIHelper.createTooltip('请更新供应商配置信息。按 Enter 键接受默认值。'));
65
65
  console.log();
66
66
 
67
+ const isCodex = providerToEdit.ideName === 'codex';
68
+
67
69
  const escListener = this.createESCListener(() => {
68
70
  Logger.info('取消编辑供应商。');
69
71
  const { registry } = require('../CommandRegistry');
@@ -73,61 +75,88 @@ class ProviderEditor extends BaseCommand {
73
75
  try {
74
76
  let answers;
75
77
  try {
76
- answers = await this.prompt([
78
+ const questions = [
77
79
  {
78
80
  type: 'input',
79
81
  name: 'displayName',
80
82
  message: '供应商显示名称:',
81
83
  default: providerToEdit.displayName,
82
84
  validate: (input) => validator.validateDisplayName(input) || true,
83
- },
84
- {
85
- type: 'list',
86
- name: 'authMode',
87
- message: '认证模式:',
88
- choices: [
89
- { name: '🔑 通用API密钥模式 - 支持 ANTHROPIC_API_KEY 和 ANTHROPIC_AUTH_TOKEN', value: 'api_key' },
90
- { name: '🔐 认证令牌模式 (仅 ANTHROPIC_AUTH_TOKEN) - 适用于某些服务商', value: 'auth_token' },
91
- { name: '🌐 OAuth令牌模式 (CLAUDE_CODE_OAUTH_TOKEN) - 适用于官方Claude Code', value: 'oauth_token' },
92
- ],
93
- default: providerToEdit.authMode,
94
- },
95
- {
96
- type: 'list',
97
- name: 'tokenType',
98
- message: 'Token类型:',
99
- choices: [
100
- { name: '🔑 ANTHROPIC_API_KEY - 通用API密钥', value: 'api_key' },
101
- { name: '🔐 ANTHROPIC_AUTH_TOKEN - 认证令牌', value: 'auth_token' }
102
- ],
103
- default: providerToEdit.tokenType || 'api_key',
104
- when: (answers) => answers.authMode === 'api_key'
105
- },
106
- {
107
- type: 'input',
108
- name: 'baseUrl',
109
- message: 'API基础URL:',
110
- default: providerToEdit.baseUrl,
111
- validate: (input) => validator.validateUrl(input) || true,
112
- when: (answers) => answers.authMode === 'api_key' || answers.authMode === 'auth_token',
113
- },
114
- {
115
- type: 'input',
116
- name: 'authToken',
117
- message: (answers) => {
118
- switch (answers.authMode) {
119
- case 'api_key':
120
- const tokenTypeLabel = answers.tokenType === 'auth_token' ? 'ANTHROPIC_AUTH_TOKEN' : 'ANTHROPIC_API_KEY';
121
- return `Token (${tokenTypeLabel}):`;
122
- case 'auth_token': return '认证令牌 (ANTHROPIC_AUTH_TOKEN):';
123
- case 'oauth_token': return 'OAuth令牌 (CLAUDE_CODE_OAUTH_TOKEN):';
124
- default: return '认证令牌:';
85
+ }
86
+ ];
87
+
88
+ if (isCodex) {
89
+ questions.push(
90
+ {
91
+ type: 'input',
92
+ name: 'baseUrl',
93
+ message: 'OpenAI API 基础URL (留空使用官方API):',
94
+ default: providerToEdit.baseUrl || '',
95
+ validate: (input) => {
96
+ if (!input) return true;
97
+ return validator.validateUrl(input) || true;
125
98
  }
126
99
  },
127
- default: providerToEdit.authToken,
128
- validate: (input) => validator.validateToken(input) || true,
129
- },
130
- {
100
+ {
101
+ type: 'input',
102
+ name: 'authToken',
103
+ message: 'OpenAI API Key (OPENAI_API_KEY):',
104
+ default: providerToEdit.authToken,
105
+ validate: (input) => {
106
+ if (!input) return 'API Key 不能为空';
107
+ return validator.validateToken(input) || true;
108
+ }
109
+ }
110
+ );
111
+ } else {
112
+ questions.push(
113
+ {
114
+ type: 'list',
115
+ name: 'authMode',
116
+ message: '认证模式:',
117
+ choices: [
118
+ { name: '🔑 通用API密钥模式 - 支持 ANTHROPIC_API_KEY 和 ANTHROPIC_AUTH_TOKEN', value: 'api_key' },
119
+ { name: '🔐 认证令牌模式 (仅 ANTHROPIC_AUTH_TOKEN) - 适用于某些服务商', value: 'auth_token' },
120
+ { name: '🌐 OAuth令牌模式 (CLAUDE_CODE_OAUTH_TOKEN) - 适用于官方Claude Code', value: 'oauth_token' },
121
+ ],
122
+ default: providerToEdit.authMode,
123
+ },
124
+ {
125
+ type: 'list',
126
+ name: 'tokenType',
127
+ message: 'Token类型:',
128
+ choices: [
129
+ { name: '🔑 ANTHROPIC_API_KEY - 通用API密钥', value: 'api_key' },
130
+ { name: '🔐 ANTHROPIC_AUTH_TOKEN - 认证令牌', value: 'auth_token' }
131
+ ],
132
+ default: providerToEdit.tokenType || 'api_key',
133
+ when: (answers) => answers.authMode === 'api_key'
134
+ },
135
+ {
136
+ type: 'input',
137
+ name: 'baseUrl',
138
+ message: 'API基础URL:',
139
+ default: providerToEdit.baseUrl,
140
+ validate: (input) => validator.validateUrl(input) || true,
141
+ when: (answers) => answers.authMode === 'api_key' || answers.authMode === 'auth_token',
142
+ },
143
+ {
144
+ type: 'input',
145
+ name: 'authToken',
146
+ message: (answers) => {
147
+ switch (answers.authMode) {
148
+ case 'api_key':
149
+ const tokenTypeLabel = answers.tokenType === 'auth_token' ? 'ANTHROPIC_AUTH_TOKEN' : 'ANTHROPIC_API_KEY';
150
+ return `Token (${tokenTypeLabel}):`;
151
+ case 'auth_token': return '认证令牌 (ANTHROPIC_AUTH_TOKEN):';
152
+ case 'oauth_token': return 'OAuth令牌 (CLAUDE_CODE_OAUTH_TOKEN):';
153
+ default: return '认证令牌:';
154
+ }
155
+ },
156
+ default: providerToEdit.authToken,
157
+ validate: (input) => validator.validateToken(input) || true,
158
+ },
159
+ {
131
160
  type: 'checkbox',
132
161
  name: 'launchArgs',
133
162
  message: '启动参数:',
@@ -136,8 +165,11 @@ class ProviderEditor extends BaseCommand {
136
165
  value: arg.name,
137
166
  checked: providerToEdit.launchArgs && providerToEdit.launchArgs.includes(arg.name),
138
167
  })),
139
- },
140
- ]);
168
+ }
169
+ );
170
+ }
171
+
172
+ answers = await this.prompt(questions);
141
173
  } catch (error) {
142
174
  this.removeESCListener(escListener);
143
175
  if (this.isEscCancelled(error)) {
@@ -147,6 +179,7 @@ class ProviderEditor extends BaseCommand {
147
179
  }
148
180
 
149
181
  this.removeESCListener(escListener);
182
+
150
183
  await this.saveProvider(providerToEdit.name, answers);
151
184
 
152
185
  } catch (error) {
@@ -160,19 +193,39 @@ class ProviderEditor extends BaseCommand {
160
193
 
161
194
  async saveProvider(name, answers) {
162
195
  try {
163
- // Re-use addProvider which can overwrite existing providers
164
- await this.configManager.addProvider(name, {
165
- displayName: answers.displayName,
166
- baseUrl: answers.baseUrl,
167
- authToken: answers.authToken,
168
- authMode: answers.authMode,
169
- tokenType: answers.tokenType, // 仅在 authMode 为 'api_key' 时使用
170
- launchArgs: answers.launchArgs,
171
- // Retain original model settings unless we add editing for them
172
- primaryModel: this.configManager.getProvider(name).models.primary,
173
- smallFastModel: this.configManager.getProvider(name).models.smallFast,
174
- setAsDefault: false, // Don't change default status on edit
175
- });
196
+ const existingProvider = this.configManager.getProvider(name);
197
+ const ideName = existingProvider?.ideName || 'claude';
198
+
199
+ if (ideName === 'codex') {
200
+ await this.configManager.addProvider(name, {
201
+ displayName: answers.displayName,
202
+ ideName: 'codex',
203
+ authMode: 'openai_api_key',
204
+ baseUrl: answers.baseUrl || null,
205
+ authToken: answers.authToken,
206
+ tokenType: null,
207
+ codexFiles: null,
208
+ launchArgs: existingProvider.launchArgs || [],
209
+ primaryModel: existingProvider.models?.primary || null,
210
+ smallFastModel: existingProvider.models?.smallFast || null,
211
+ setAsDefault: false
212
+ });
213
+ } else {
214
+ // Re-use addProvider which can overwrite existing providers
215
+ await this.configManager.addProvider(name, {
216
+ displayName: answers.displayName,
217
+ ideName,
218
+ baseUrl: answers.baseUrl,
219
+ authToken: answers.authToken,
220
+ authMode: answers.authMode,
221
+ tokenType: answers.tokenType, // 仅在 authMode 为 'api_key' 时使用
222
+ launchArgs: answers.launchArgs,
223
+ // Retain original model settings unless we add editing for them
224
+ primaryModel: existingProvider.models.primary,
225
+ smallFastModel: existingProvider.models.smallFast,
226
+ setAsDefault: false, // Don't change default status on edit
227
+ });
228
+ }
176
229
 
177
230
  Logger.success(`供应商 '${answers.displayName}' 更新成功!`);
178
231
 
@@ -9,20 +9,34 @@ class ProviderLister {
9
9
  this.statusChecker = new ProviderStatusChecker();
10
10
  }
11
11
 
12
- async list() {
12
+ async list(filter = null) {
13
13
  try {
14
14
  await this.configManager.ensureLoaded();
15
- const providers = this.configManager.listProviders();
15
+ let providers = this.configManager.listProviders();
16
16
  const currentProvider = this.configManager.getCurrentProvider();
17
+
18
+ // 应用过滤器
19
+ if (filter === 'codex') {
20
+ providers = providers.filter(p => p.ideName === 'codex');
21
+ } else if (filter === 'claude') {
22
+ providers = providers.filter(p => p.ideName !== 'codex');
23
+ }
24
+
17
25
  const statusMap = await this.statusChecker.checkAll(providers);
18
26
 
19
27
  if (providers.length === 0) {
20
- Logger.warning('暂无配置的供应商');
28
+ if (filter) {
29
+ const filterName = filter === 'codex' ? 'Codex CLI' : 'Claude Code';
30
+ Logger.warning(`暂无 ${filterName} 供应商配置`);
31
+ } else {
32
+ Logger.warning('暂无配置的供应商');
33
+ }
21
34
  Logger.info('请使用 "akm add" 添加供应商配置');
22
35
  return;
23
36
  }
24
37
 
25
- console.log(chalk.blue('\n📋 供应商列表:'));
38
+ const titleSuffix = filter === 'codex' ? ' (Codex CLI)' : (filter === 'claude' ? ' (Claude Code)' : '');
39
+ console.log(chalk.blue(`\n📋 供应商列表${titleSuffix}:`));
26
40
  console.log(chalk.gray('═'.repeat(60)));
27
41
 
28
42
  providers.forEach((provider, index) => {
@@ -35,39 +49,49 @@ class ProviderLister {
35
49
 
36
50
  console.log(`${status} ${availabilityIcon} ${nameColor(provider.name)} (${provider.displayName}) - ${availabilityText}`);
37
51
 
38
- // 显示认证模式
39
- const authModeDisplay = {
40
- api_key: '通用API密钥模式',
41
- auth_token: '认证令牌模式',
42
- oauth_token: 'OAuth令牌模式'
43
- };
44
- console.log(chalk.gray(` 认证模式: ${authModeDisplay[provider.authMode] || provider.authMode}`));
45
-
46
- // 根据不同模式显示对应的环境变量名称
47
- if (provider.authMode === 'oauth_token') {
48
- // OAuth 模式
49
- if (provider.authToken) {
50
- console.log(chalk.gray(` CLAUDE_CODE_OAUTH_TOKEN: ${provider.authToken}`));
51
- }
52
- if (provider.baseUrl) {
53
- console.log(chalk.gray(` ANTHROPIC_BASE_URL: ${provider.baseUrl}`));
54
- }
55
- } else if (provider.authMode === 'api_key') {
56
- // API Key 模式
52
+ if (provider.ideName === 'codex') {
53
+ console.log(chalk.gray(' IDE: Codex CLI'));
57
54
  if (provider.baseUrl) {
58
- console.log(chalk.gray(` ANTHROPIC_BASE_URL: ${provider.baseUrl}`));
55
+ console.log(chalk.gray(` OPENAI_BASE_URL: ${provider.baseUrl}`));
59
56
  }
60
57
  if (provider.authToken) {
61
- const tokenEnvName = provider.tokenType === 'auth_token' ? 'ANTHROPIC_AUTH_TOKEN' : 'ANTHROPIC_API_KEY';
62
- console.log(chalk.gray(` ${tokenEnvName}: ${provider.authToken}`));
58
+ console.log(chalk.gray(` OPENAI_API_KEY: ${provider.authToken}`));
63
59
  }
64
60
  } else {
65
- // auth_token 模式
66
- if (provider.baseUrl) {
67
- console.log(chalk.gray(` ANTHROPIC_BASE_URL: ${provider.baseUrl}`));
68
- }
69
- if (provider.authToken) {
70
- console.log(chalk.gray(` ANTHROPIC_AUTH_TOKEN: ${provider.authToken}`));
61
+ // 显示认证模式
62
+ const authModeDisplay = {
63
+ api_key: '通用API密钥模式',
64
+ auth_token: '认证令牌模式',
65
+ oauth_token: 'OAuth令牌模式'
66
+ };
67
+ console.log(chalk.gray(` 认证模式: ${authModeDisplay[provider.authMode] || provider.authMode}`));
68
+
69
+ // 根据不同模式显示对应的环境变量名称
70
+ if (provider.authMode === 'oauth_token') {
71
+ // OAuth 模式
72
+ if (provider.authToken) {
73
+ console.log(chalk.gray(` CLAUDE_CODE_OAUTH_TOKEN: ${provider.authToken}`));
74
+ }
75
+ if (provider.baseUrl) {
76
+ console.log(chalk.gray(` ANTHROPIC_BASE_URL: ${provider.baseUrl}`));
77
+ }
78
+ } else if (provider.authMode === 'api_key') {
79
+ // API Key 模式
80
+ if (provider.baseUrl) {
81
+ console.log(chalk.gray(` ANTHROPIC_BASE_URL: ${provider.baseUrl}`));
82
+ }
83
+ if (provider.authToken) {
84
+ const tokenEnvName = provider.tokenType === 'auth_token' ? 'ANTHROPIC_AUTH_TOKEN' : 'ANTHROPIC_API_KEY';
85
+ console.log(chalk.gray(` ${tokenEnvName}: ${provider.authToken}`));
86
+ }
87
+ } else {
88
+ // auth_token 模式
89
+ if (provider.baseUrl) {
90
+ console.log(chalk.gray(` ANTHROPIC_BASE_URL: ${provider.baseUrl}`));
91
+ }
92
+ if (provider.authToken) {
93
+ console.log(chalk.gray(` ANTHROPIC_AUTH_TOKEN: ${provider.authToken}`));
94
+ }
71
95
  }
72
96
  }
73
97
 
@@ -132,9 +156,9 @@ class ProviderLister {
132
156
  }
133
157
  }
134
158
 
135
- async function listCommand() {
159
+ async function listCommand(filter = null) {
136
160
  const lister = new ProviderLister();
137
- await lister.list();
161
+ await lister.list(filter);
138
162
  }
139
163
 
140
164
  module.exports = { listCommand, ProviderLister };
@@ -4,6 +4,7 @@ const chalk = require('chalk');
4
4
  const Choices = require('inquirer/lib/objects/choices');
5
5
  const { ConfigManager } = require('../config');
6
6
  const { executeWithEnv } = require('../utils/env-launcher');
7
+ const { executeCodexWithEnv } = require('../utils/codex-launcher');
7
8
  const { Logger } = require('../utils/logger');
8
9
  const { UIHelper } = require('../utils/ui-helper');
9
10
  const { findSettingsConflict, backupSettingsFile, clearConflictKeys, saveSettingsFile } = require('../utils/claude-settings');
@@ -34,6 +35,10 @@ class EnvSwitcher extends BaseCommand {
34
35
  try {
35
36
  this.clearScreen();
36
37
  const provider = await this.validateProvider(providerName);
38
+ if (provider.ideName === 'codex') {
39
+ // Codex CLI 使用环境变量注入方式启动,直接跳过参数选择
40
+ return await this.launchProvider(provider, provider.launchArgs || []);
41
+ }
37
42
  const availableArgs = this.getAvailableLaunchArgs();
38
43
 
39
44
  console.log(UIHelper.createTitle('启动配置', UIHelper.icons.launch));
@@ -198,15 +203,20 @@ class EnvSwitcher extends BaseCommand {
198
203
 
199
204
  async launchProvider(provider, selectedLaunchArgs) {
200
205
  try {
201
- // 执行 Claude Code 设置兼容性检查
202
- const shouldContinue = await this.ensureClaudeSettingsCompatibility(provider);
203
- if (!shouldContinue) {
204
- return;
206
+ const isCodex = provider.ideName === 'codex';
207
+
208
+ // Claude Code 才需要检测设置冲突
209
+ if (!isCodex) {
210
+ const shouldContinue = await this.ensureClaudeSettingsCompatibility(provider);
211
+ if (!shouldContinue) {
212
+ return;
213
+ }
205
214
  }
206
215
 
207
216
  this.clearScreen();
208
217
  console.log(UIHelper.createTitle('正在启动', UIHelper.icons.loading));
209
218
  console.log();
219
+ const ideDisplayName = isCodex ? 'Codex CLI' : 'Claude Code';
210
220
  console.log(UIHelper.createCard('目标供应商', UIHelper.formatProvider(provider), UIHelper.icons.launch));
211
221
 
212
222
  if (selectedLaunchArgs.length > 0) {
@@ -228,11 +238,15 @@ class EnvSwitcher extends BaseCommand {
228
238
 
229
239
  UIHelper.clearLoadingAnimation(loadingInterval);
230
240
 
231
- console.log(UIHelper.createCard('准备就绪', '环境配置完成,正在启动 🚀 Claude Code...', UIHelper.icons.success));
241
+ console.log(UIHelper.createCard('准备就绪', `环境配置完成,正在启动 🚀 ${ideDisplayName}...`, UIHelper.icons.success));
232
242
  console.log();
233
243
 
234
- // 设置环境变量并启动 Claude Code
235
- await executeWithEnv(provider, selectedLaunchArgs);
244
+ if (isCodex) {
245
+ await executeCodexWithEnv(provider, selectedLaunchArgs);
246
+ } else {
247
+ // 设置环境变量并启动 Claude Code
248
+ await executeWithEnv(provider, selectedLaunchArgs);
249
+ }
236
250
 
237
251
  } catch (error) {
238
252
  UIHelper.clearLoadingAnimation(loadingInterval);
@@ -0,0 +1,120 @@
1
+ const fs = require('fs-extra');
2
+ const path = require('path');
3
+ const os = require('os');
4
+
5
+ const CODEX_DIR_NAME = '.codex';
6
+ const CONFIG_TOML_FILE = 'config.toml';
7
+ const AUTH_JSON_FILE = 'auth.json';
8
+ const BACKUP_DIR_NAME = 'akm-backups';
9
+
10
+ function resolveCodexHome() {
11
+ return process.env.CODEX_HOME || path.join(os.homedir(), CODEX_DIR_NAME);
12
+ }
13
+
14
+ function buildCodexPaths(codexHome = resolveCodexHome()) {
15
+ return {
16
+ codexHome,
17
+ configTomlPath: path.join(codexHome, CONFIG_TOML_FILE),
18
+ authJsonPath: path.join(codexHome, AUTH_JSON_FILE)
19
+ };
20
+ }
21
+
22
+ function timestampSuffix() {
23
+ const now = new Date();
24
+ const pad = (num) => String(num).padStart(2, '0');
25
+ return [
26
+ now.getFullYear(),
27
+ pad(now.getMonth() + 1),
28
+ pad(now.getDate()),
29
+ '_',
30
+ pad(now.getHours()),
31
+ pad(now.getMinutes()),
32
+ pad(now.getSeconds())
33
+ ].join('');
34
+ }
35
+
36
+ async function ensureCodexHome(codexHome = resolveCodexHome()) {
37
+ await fs.ensureDir(codexHome);
38
+ return codexHome;
39
+ }
40
+
41
+ async function setSecurePermissions(filePath) {
42
+ if (process.platform === 'win32') {
43
+ return;
44
+ }
45
+ try {
46
+ await fs.chmod(filePath, 0o600);
47
+ } catch {
48
+ // 忽略权限设置失败
49
+ }
50
+ }
51
+
52
+ async function readCodexFiles(codexHome = resolveCodexHome()) {
53
+ const { configTomlPath, authJsonPath } = buildCodexPaths(codexHome);
54
+ const configToml = await fs.pathExists(configTomlPath)
55
+ ? await fs.readFile(configTomlPath, 'utf8')
56
+ : null;
57
+ const authJson = await fs.pathExists(authJsonPath)
58
+ ? await fs.readFile(authJsonPath, 'utf8')
59
+ : null;
60
+
61
+ return {
62
+ codexHome,
63
+ configToml,
64
+ authJson
65
+ };
66
+ }
67
+
68
+ async function backupCodexFiles(codexHome = resolveCodexHome()) {
69
+ const { configTomlPath, authJsonPath } = buildCodexPaths(codexHome);
70
+ const hasConfig = await fs.pathExists(configTomlPath);
71
+ const hasAuth = await fs.pathExists(authJsonPath);
72
+ if (!hasConfig && !hasAuth) {
73
+ return null;
74
+ }
75
+
76
+ const backupRoot = path.join(codexHome, BACKUP_DIR_NAME);
77
+ const backupDir = path.join(backupRoot, `backup-${timestampSuffix()}`);
78
+ await fs.ensureDir(backupDir);
79
+
80
+ if (hasConfig) {
81
+ await fs.copy(configTomlPath, path.join(backupDir, CONFIG_TOML_FILE));
82
+ }
83
+ if (hasAuth) {
84
+ await fs.copy(authJsonPath, path.join(backupDir, AUTH_JSON_FILE));
85
+ }
86
+
87
+ return backupDir;
88
+ }
89
+
90
+ async function applyCodexProfile(profile, options = {}) {
91
+ if (!profile || (profile.configToml == null && profile.authJson == null)) {
92
+ throw new Error('Codex 配置为空,无法切换');
93
+ }
94
+
95
+ const codexHome = await ensureCodexHome(profile.codexHome || options.codexHome);
96
+ const { configTomlPath, authJsonPath } = buildCodexPaths(codexHome);
97
+
98
+ const backupDir = await backupCodexFiles(codexHome);
99
+
100
+ if (profile.configToml != null) {
101
+ await fs.writeFile(configTomlPath, profile.configToml, 'utf8');
102
+ await setSecurePermissions(configTomlPath);
103
+ }
104
+
105
+ if (profile.authJson != null) {
106
+ await fs.writeFile(authJsonPath, profile.authJson, 'utf8');
107
+ await setSecurePermissions(authJsonPath);
108
+ }
109
+
110
+ return { codexHome, backupDir };
111
+ }
112
+
113
+ module.exports = {
114
+ resolveCodexHome,
115
+ buildCodexPaths,
116
+ readCodexFiles,
117
+ applyCodexProfile,
118
+ backupCodexFiles
119
+ };
120
+
@@ -0,0 +1,121 @@
1
+ const spawn = require('cross-spawn');
2
+
3
+ /**
4
+ * 清理环境变量值,移除危险字符
5
+ * @param {string} value - 要清理的值
6
+ * @returns {string} 清理后的值
7
+ */
8
+ function sanitizeEnvValue(value) {
9
+ if (typeof value !== 'string') {
10
+ throw new Error('环境变量值必须是字符串');
11
+ }
12
+
13
+ // 移除控制字符
14
+ let cleaned = value.replace(/[\x00-\x08\x0B\x0C\x0E-\x1F]/g, '');
15
+
16
+ // 检测可能的 shell 命令注入
17
+ if (/[;&|`$()]/.test(cleaned)) {
18
+ throw new Error('环境变量值包含潜在不安全的字符');
19
+ }
20
+
21
+ return cleaned;
22
+ }
23
+
24
+ function clearTerminal() {
25
+ if (!process.stdout || typeof process.stdout.write !== 'function') {
26
+ return;
27
+ }
28
+
29
+ try {
30
+ process.stdout.write('\x1bc');
31
+ } catch (error) {
32
+ // 某些终端可能不支持 RIS 序列,忽略即可
33
+ }
34
+
35
+ const sequence = process.platform === 'win32'
36
+ ? '\x1b[3J\x1b[2J\x1b[0f'
37
+ : '\x1b[3J\x1b[2J\x1b[H';
38
+ try {
39
+ process.stdout.write(sequence);
40
+ } catch (error) {
41
+ // 忽略清屏失败
42
+ }
43
+ }
44
+
45
+ /**
46
+ * 构建 Codex CLI 环境变量
47
+ * @param {object} config - 供应商配置
48
+ * @returns {object} 环境变量对象
49
+ */
50
+ function buildCodexEnvVariables(config) {
51
+ const env = { ...process.env };
52
+
53
+ try {
54
+ // Codex CLI 使用 OpenAI 环境变量
55
+ if (config.authToken) {
56
+ env.OPENAI_API_KEY = sanitizeEnvValue(config.authToken);
57
+ }
58
+
59
+ if (config.baseUrl) {
60
+ env.OPENAI_BASE_URL = sanitizeEnvValue(config.baseUrl);
61
+ }
62
+
63
+ // 支持自定义模型
64
+ if (config.models && config.models.primary) {
65
+ env.OPENAI_MODEL = sanitizeEnvValue(config.models.primary);
66
+ }
67
+
68
+ return env;
69
+ } catch (error) {
70
+ throw new Error(`配置验证失败: ${error.message}\n请使用 'akm edit ${config.name}' 修复配置`);
71
+ }
72
+ }
73
+
74
+ /**
75
+ * 使用环境变量注入方式执行 Codex CLI
76
+ * @param {object} config - 供应商配置
77
+ * @param {string[]} launchArgs - 启动参数
78
+ * @returns {Promise<void>}
79
+ */
80
+ async function executeCodexWithEnv(config, launchArgs = []) {
81
+ if (!config || config.ideName !== 'codex') {
82
+ throw new Error('无效的 Codex 供应商配置');
83
+ }
84
+
85
+ if (!config.authToken) {
86
+ throw new Error(`供应商 '${config.name}' 未配置 API Key,请使用 'akm edit ${config.name}' 添加`);
87
+ }
88
+
89
+ const env = buildCodexEnvVariables(config);
90
+ const args = Array.isArray(launchArgs) ? [...launchArgs] : [];
91
+
92
+ clearTerminal();
93
+
94
+ console.log('\n启动 Codex CLI...\n');
95
+
96
+ return new Promise((resolve, reject) => {
97
+ const child = spawn('codex', args, {
98
+ stdio: 'inherit',
99
+ env,
100
+ shell: true
101
+ });
102
+
103
+ child.on('close', (code) => {
104
+ if (code === 0) {
105
+ resolve();
106
+ } else {
107
+ reject(new Error(`Codex CLI 退出,退出代码: ${code}\n提示: 请检查 API 配置是否正确`));
108
+ }
109
+ });
110
+
111
+ child.on('error', (error) => {
112
+ if (error.code === 'ENOENT') {
113
+ reject(new Error('找不到 codex 命令\n请先安装 Codex CLI: npm i -g @openai/codex 或 brew install --cask codex'));
114
+ } else {
115
+ reject(new Error(`启动 Codex CLI 失败: ${error.message}`));
116
+ }
117
+ });
118
+ });
119
+ }
120
+
121
+ module.exports = { executeCodexWithEnv, buildCodexEnvVariables };
@@ -13,6 +13,10 @@ class ProviderStatusChecker {
13
13
  return this._result('unknown', '未找到配置', null);
14
14
  }
15
15
 
16
+ if (provider.ideName === 'codex') {
17
+ return this._checkCodex(provider);
18
+ }
19
+
16
20
  if (provider.authMode === 'oauth_token') {
17
21
  return this._result('unknown', '暂不支持 OAuth 令牌检测', null);
18
22
  }
@@ -246,6 +250,52 @@ class ProviderStatusChecker {
246
250
  _result(state, label, latency) {
247
251
  return { state, label, latency };
248
252
  }
253
+
254
+ async _checkCodex(provider) {
255
+ if (!provider.authToken) {
256
+ return this._result('unknown', '未配置 API Key', null);
257
+ }
258
+
259
+ const baseUrl = provider.baseUrl || 'https://api.openai.com/v1';
260
+ const modelsUrl = `${baseUrl.replace(/\/$/, '')}/models`;
261
+
262
+ try {
263
+ const controller = new AbortController();
264
+ const timeoutId = setTimeout(() => controller.abort(), this.timeout);
265
+
266
+ const start = process.hrtime.bigint();
267
+ const response = await fetch(modelsUrl, {
268
+ method: 'GET',
269
+ headers: {
270
+ 'Authorization': `Bearer ${provider.authToken}`,
271
+ 'Content-Type': 'application/json'
272
+ },
273
+ signal: controller.signal
274
+ });
275
+ const latency = Number(process.hrtime.bigint() - start) / 1e6;
276
+
277
+ clearTimeout(timeoutId);
278
+
279
+ if (response.ok) {
280
+ return this._result('online', `可用 ${latency.toFixed(0)}ms`, latency);
281
+ }
282
+
283
+ if (response.status === 401 || response.status === 403) {
284
+ return this._result('offline', `认证失败 (${response.status})`, null);
285
+ }
286
+
287
+ if (response.status >= 500) {
288
+ return this._result('degraded', `服务异常 (${response.status})`, null);
289
+ }
290
+
291
+ return this._result('offline', `请求失败 (${response.status})`, null);
292
+ } catch (error) {
293
+ if (error.name === 'AbortError') {
294
+ return this._result('offline', '请求超时', null);
295
+ }
296
+ return this._result('offline', `检测失败: ${error.message}`, null);
297
+ }
298
+ }
249
299
  }
250
300
 
251
301
  module.exports = { ProviderStatusChecker };