@pikecode/api-key-manager 1.0.45 → 1.1.1

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
@@ -7,12 +7,88 @@ const { registry } = require('../src/CommandRegistry');
7
7
  const pkg = require('../package.json');
8
8
  const { checkForUpdates } = require('../src/utils/update-checker');
9
9
 
10
+ // 命令分组定义
11
+ const COMMAND_GROUPS = {
12
+ '核心命令': ['add', 'switch', 'remove', 'list', 'current', 'edit'],
13
+ '运维命令': ['export', 'import', 'backup', 'validate', 'clone'],
14
+ '工具命令': ['stats', 'health', 'batch', 'benchmark', 'claude', 'mcp']
15
+ };
16
+
10
17
  // Set up CLI
11
18
  program
12
19
  .name('akm')
13
20
  .description('API密钥管理工具 - Manage and switch multiple API provider configurations')
14
21
  .version(pkg.version, '-V, --version', '显示版本号');
15
22
 
23
+ // 自定义 help 输出,按分组显示命令
24
+ program.configureHelp({
25
+ formatHelp(cmd, helper) {
26
+ const termWidth = helper.padWidth(cmd, helper);
27
+ const helpWidth = helper.helpWidth || 80;
28
+
29
+ let output = '';
30
+
31
+ // 标题
32
+ output += `${chalk.bold('用法:')} ${helper.commandUsage(cmd)}\n\n`;
33
+
34
+ // 描述
35
+ const desc = helper.commandDescription(cmd);
36
+ if (desc) {
37
+ output += `${desc}\n\n`;
38
+ }
39
+
40
+ // 参数
41
+ const argList = helper.visibleArguments(cmd).map(arg => {
42
+ return helper.formatHelp ? ` ${helper.argumentTerm(arg).padEnd(termWidth)} ${helper.argumentDescription(arg)}` : '';
43
+ });
44
+ if (argList.length > 0) {
45
+ output += `${chalk.bold('参数:')}\n${argList.join('\n')}\n\n`;
46
+ }
47
+
48
+ // 选项
49
+ const optList = helper.visibleOptions(cmd).map(opt => {
50
+ return ` ${helper.optionTerm(opt).padEnd(termWidth)} ${helper.optionDescription(opt)}`;
51
+ });
52
+ if (optList.length > 0) {
53
+ output += `${chalk.bold('选项:')}\n${optList.join('\n')}\n\n`;
54
+ }
55
+
56
+ // 分组命令
57
+ const commands = helper.visibleCommands(cmd);
58
+ if (commands.length > 0) {
59
+ const commandMap = {};
60
+ commands.forEach(sub => {
61
+ commandMap[sub.name()] = sub;
62
+ });
63
+
64
+ Object.entries(COMMAND_GROUPS).forEach(([groupName, cmdNames]) => {
65
+ const groupCmds = cmdNames
66
+ .filter(name => commandMap[name])
67
+ .map(name => {
68
+ const sub = commandMap[name];
69
+ return ` ${chalk.cyan(helper.subcommandTerm(sub).padEnd(termWidth))} ${helper.subcommandDescription(sub)}`;
70
+ });
71
+
72
+ if (groupCmds.length > 0) {
73
+ output += `${chalk.bold.yellow(groupName + ':')}\n${groupCmds.join('\n')}\n\n`;
74
+ }
75
+ });
76
+
77
+ // 未分组的命令
78
+ const groupedNames = Object.values(COMMAND_GROUPS).flat();
79
+ const ungrouped = commands
80
+ .filter(sub => !groupedNames.includes(sub.name()))
81
+ .map(sub => ` ${chalk.cyan(helper.subcommandTerm(sub).padEnd(termWidth))} ${helper.subcommandDescription(sub)}`);
82
+
83
+ if (ungrouped.length > 0) {
84
+ output += `${chalk.bold.yellow('其他命令:')}\n${ungrouped.join('\n')}\n\n`;
85
+ }
86
+ }
87
+
88
+ return output;
89
+ }
90
+ });
91
+
16
92
  // Check for updates before any command runs
17
93
  program.hook('preAction', async () => {
18
94
  await checkForUpdates({ packageName: pkg.name, currentVersion: pkg.version });
@@ -91,11 +167,12 @@ program
91
167
  .description('列出所有API密钥配置')
92
168
  .option('--codex', '仅显示 Codex CLI 供应商')
93
169
  .option('--claude', '仅显示 Claude Code 供应商')
170
+ .option('--status', '检测供应商在线状态')
94
171
  .option('--show-token', '显示完整 Token(默认脱敏)')
95
172
  .action(async (options) => {
96
173
  try {
97
174
  const filter = options.codex ? 'codex' : (options.claude ? 'claude' : null);
98
- await registry.executeCommand('list', filter, { showToken: options.showToken });
175
+ await registry.executeCommand('list', filter, { showToken: options.showToken, checkStatus: options.status });
99
176
  } catch (error) {
100
177
  console.error(chalk.red('❌ 列表失败:'), error.message);
101
178
  process.exit(1);
@@ -193,6 +270,20 @@ program
193
270
  }
194
271
  });
195
272
 
273
+ // Clone command
274
+ program
275
+ .command('clone')
276
+ .argument('[source]', '要克隆的源供应商名称')
277
+ .description('克隆现有供应商配置')
278
+ .action(async (source) => {
279
+ try {
280
+ await registry.executeCommand('clone', source);
281
+ } catch (error) {
282
+ console.error(chalk.red('❌ 克隆失败:'), error.message);
283
+ process.exit(1);
284
+ }
285
+ });
286
+
196
287
  // Stats command
197
288
  program
198
289
  .command('stats')
@@ -285,5 +376,38 @@ program
285
376
  }
286
377
  });
287
378
 
379
+ // Claude command
380
+ program
381
+ .command('claude')
382
+ .argument('<subcommand>', '子命令: clean (清理) | analyze (分析)')
383
+ .description('Claude Code 配置管理 (clean/analyze)')
384
+ .action(async (subcommand) => {
385
+ try {
386
+ if (subcommand !== 'clean' && subcommand !== 'analyze') {
387
+ console.error(chalk.red(`❌ 未知子命令: ${subcommand}`));
388
+ console.error(chalk.gray(' 可用子命令: clean, analyze'));
389
+ process.exit(1);
390
+ }
391
+ await registry.executeCommand('claude-clean', subcommand);
392
+ } catch (error) {
393
+ console.error(chalk.red('❌ 操作失败:'), error.message);
394
+ process.exit(1);
395
+ }
396
+ });
397
+
398
+ // MCP command
399
+ program
400
+ .command('mcp')
401
+ .argument('<subcommand>', '子命令: list | add | edit | remove')
402
+ .description('MCP 服务器管理 (list/add/edit/remove)')
403
+ .action(async (subcommand) => {
404
+ try {
405
+ await registry.executeCommand('mcp', subcommand);
406
+ } catch (error) {
407
+ console.error(chalk.red('❌ MCP 操作失败:'), error.message);
408
+ process.exit(1);
409
+ }
410
+ });
411
+
288
412
  // Parse arguments
289
413
  program.parse();
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@pikecode/api-key-manager",
3
- "version": "1.0.45",
3
+ "version": "1.1.1",
4
4
  "description": "A CLI tool for managing and switching multiple API provider configurations",
5
5
  "main": "src/index.js",
6
6
  "bin": {
@@ -111,4 +111,19 @@ registry.registerLazy('benchmark', async () => {
111
111
  return benchmarkCommand;
112
112
  });
113
113
 
114
+ registry.registerLazy('clone', async () => {
115
+ const { cloneCommand } = require('./commands/clone');
116
+ return cloneCommand;
117
+ });
118
+
119
+ registry.registerLazy('claude-clean', async () => {
120
+ const { claudeCleanCommand } = require('./commands/claude-clean');
121
+ return claudeCleanCommand;
122
+ });
123
+
124
+ registry.registerLazy('mcp', async () => {
125
+ const { mcpCommand } = require('./commands/mcp');
126
+ return mcpCommand;
127
+ });
128
+
114
129
  module.exports = { CommandRegistry, registry };
@@ -13,7 +13,6 @@ const { UIHelper } = require('../utils/ui-helper');
13
13
  const { BaseCommand } = require('./BaseCommand');
14
14
  const {
15
15
  AUTH_MODE_DISPLAY_DETAILED,
16
- TOKEN_TYPE_DISPLAY,
17
16
  IDE_NAMES
18
17
  } = require('../constants');
19
18
 
@@ -41,55 +40,9 @@ class ProviderAdder extends BaseCommand {
41
40
  async interactive() {
42
41
  console.log(UIHelper.createTitle('添加新供应商', UIHelper.icons.add));
43
42
  console.log();
44
- console.log(UIHelper.createTooltip('选择供应商类型或手动配置'));
45
- console.log();
46
- console.log(UIHelper.createStepIndicator(1, 3, '选择供应商类型'));
47
- console.log(UIHelper.createHintLine([
48
- ['↑ / ↓', '选择类型'],
49
- ['Enter', '确认'],
50
- ['ESC', '取消添加']
51
- ]));
52
- console.log();
53
-
54
- try {
55
- // 首先选择是否使用预设配置
56
- const typeAnswer = await this.promptWithESC([
57
- {
58
- type: 'list',
59
- name: 'providerType',
60
- message: '选择供应商类型:',
61
- choices: [
62
- { name: '🔒 官方 Claude Code (OAuth)', value: 'official_oauth' },
63
- { name: '⚙️ 自定义配置', value: 'custom' }
64
- ],
65
- default: 'custom'
66
- }
67
- ], '取消添加', () => {
68
- Logger.info('取消添加供应商');
69
- // 使用CommandRegistry避免循环引用
70
- const { registry } = require('../CommandRegistry');
71
- registry.executeCommand('switch');
72
- });
73
-
74
- if (typeAnswer.providerType === 'official_oauth') {
75
- return await this.addOfficialOAuthProvider();
76
- } else {
77
- return await this.addCustomProvider();
78
- }
79
- } catch (error) {
80
- if (this.isEscCancelled(error)) {
81
- return;
82
- }
83
- throw error;
84
- }
85
- }
86
-
87
- async addOfficialOAuthProvider() {
88
- console.log(UIHelper.createTitle('添加官方 OAuth 供应商', UIHelper.icons.add));
89
- console.log();
90
- console.log(UIHelper.createTooltip('配置官方 Claude Code OAuth 认证'));
43
+ console.log(UIHelper.createTooltip('请填写供应商配置信息'));
91
44
  console.log();
92
- console.log(UIHelper.createStepIndicator(2, 3, '填写官方 OAuth 信息'));
45
+ console.log(UIHelper.createStepIndicator(1, 2, '填写供应商信息'));
93
46
  console.log(UIHelper.createHintLine([
94
47
  ['Enter', '确认输入'],
95
48
  ['Tab', '切换字段'],
@@ -98,61 +51,7 @@ class ProviderAdder extends BaseCommand {
98
51
  console.log();
99
52
 
100
53
  try {
101
- const answers = await this.promptWithESC([
102
- {
103
- type: 'input',
104
- name: 'name',
105
- message: '请输入供应商名称 (用于命令行):',
106
- default: 'claude-official',
107
- validate: (input) => {
108
- const error = validator.validateName(input);
109
- if (error) return error;
110
- return true;
111
- }
112
- },
113
- {
114
- type: 'input',
115
- name: 'displayName',
116
- message: '请输入供应商显示名称:',
117
- default: 'Claude Code 官方 (OAuth)',
118
- validate: (input) => {
119
- const error = validator.validateDisplayName(input);
120
- if (error) return error;
121
- return true;
122
- }
123
- },
124
- {
125
- type: 'input',
126
- name: 'authToken',
127
- message: '请输入 OAuth Token (sk-ant-oat01-...):',
128
- validate: (input) => {
129
- if (!input || !input.startsWith('sk-ant-oat01-')) {
130
- return '请输入有效的 OAuth Token (格式: sk-ant-oat01-...)';
131
- }
132
- const error = validator.validateToken(input);
133
- if (error) return error;
134
- return true;
135
- }
136
- },
137
- {
138
- type: 'confirm',
139
- name: 'setAsDefault',
140
- message: '是否设置为当前供应商?',
141
- default: true
142
- }
143
- ], '取消添加', () => {
144
- Logger.info('取消添加供应商');
145
- // 使用CommandRegistry避免循环引用
146
- const { registry } = require('../CommandRegistry');
147
- registry.executeCommand('switch');
148
- });
149
-
150
- // 使用官方 OAuth 配置
151
- await this.saveProvider({
152
- ...answers,
153
- authMode: 'oauth_token',
154
- baseUrl: null // OAuth 模式不需要 baseUrl
155
- });
54
+ return await this.addCustomProvider();
156
55
  } catch (error) {
157
56
  if (this.isEscCancelled(error)) {
158
57
  return;
@@ -162,18 +61,6 @@ class ProviderAdder extends BaseCommand {
162
61
  }
163
62
 
164
63
  async addCustomProvider() {
165
- console.log(UIHelper.createTitle('添加自定义供应商', UIHelper.icons.add));
166
- console.log();
167
- console.log(UIHelper.createTooltip('请填写供应商配置信息'));
168
- console.log();
169
- console.log(UIHelper.createStepIndicator(2, 3, '填写供应商信息'));
170
- console.log(UIHelper.createHintLine([
171
- ['Enter', '确认输入'],
172
- ['Tab', '切换字段'],
173
- ['ESC', '取消添加']
174
- ]));
175
- console.log();
176
-
177
64
  try {
178
65
  const answers = await this.promptWithESC([
179
66
  {
@@ -201,29 +88,8 @@ class ProviderAdder extends BaseCommand {
201
88
  {
202
89
  type: 'input',
203
90
  name: 'name',
204
- message: '请输入供应商名称 (用于命令行):',
205
- validate: (input) => {
206
- const error = validator.validateName(input);
207
- if (error) return error;
208
- return true;
209
- }
210
- },
211
- {
212
- type: 'input',
213
- name: 'displayName',
214
- message: '请输入供应商显示名称 (可选,默认为供应商名称):',
215
- validate: (input) => {
216
- const error = validator.validateDisplayName(input);
217
- if (error) return error;
218
- return true;
219
- }
220
- },
221
- {
222
- type: 'input',
223
- name: 'alias',
224
- message: '请输入供应商别名 (可选,用于快速切换):',
91
+ message: '请输入供应商名称:',
225
92
  validate: (input) => {
226
- if (!input) return true; // 别名是可选的
227
93
  const error = validator.validateName(input);
228
94
  if (error) return error;
229
95
  return true;
@@ -234,63 +100,30 @@ class ProviderAdder extends BaseCommand {
234
100
  name: 'authMode',
235
101
  message: '选择认证模式:',
236
102
  choices: [
237
- { name: '🔑 通用API密钥模式 - 支持 ANTHROPIC_API_KEY 和 ANTHROPIC_AUTH_TOKEN', value: 'api_key' },
238
- { name: '🔐 认证令牌模式 (仅 ANTHROPIC_AUTH_TOKEN) - 适用于某些服务商', value: 'auth_token' },
239
- { name: '🌐 OAuth令牌模式 (CLAUDE_CODE_OAUTH_TOKEN) - 适用于官方Claude Code', value: 'oauth_token' }
103
+ { name: '🔑 ANTHROPIC_API_KEY - 大多数第三方代理使用', value: 'api_key' },
104
+ { name: '🔐 ANTHROPIC_AUTH_TOKEN - 部分服务商使用', value: 'auth_token' }
240
105
  ],
241
106
  default: 'api_key',
242
107
  when: (answers) => (answers.ideName || this.presetIdeName) !== 'codex'
243
108
  },
244
- {
245
- type: 'list',
246
- name: 'tokenType',
247
- message: '选择Token类型:',
248
- choices: [
249
- { name: '🔑 ANTHROPIC_API_KEY - 通用API密钥', value: 'api_key' },
250
- { name: '🔐 ANTHROPIC_AUTH_TOKEN - 认证令牌', value: 'auth_token' }
251
- ],
252
- default: 'api_key',
253
- when: (answers) => (answers.ideName || this.presetIdeName) !== 'codex' && answers.authMode === 'api_key'
254
- },
255
109
  {
256
110
  type: 'input',
257
111
  name: 'baseUrl',
258
- message: (answers) => {
259
- if (answers.authMode === 'auth_token') {
260
- return '请输入API基础URL (如使用官方API可留空):';
261
- }
262
- return '请输入API基础URL:';
263
- },
264
- validate: (input, answers) => {
265
- // auth_token 模式允许空值(使用官方 API)
266
- if (input === '' && answers.authMode === 'auth_token') {
267
- return true;
268
- }
269
- // 其他模式需要有效的 URL
270
- if (!input && answers.authMode === 'api_key') {
271
- return 'API基础URL不能为空';
272
- }
112
+ message: '请输入 API 基础URL (ANTHROPIC_BASE_URL):',
113
+ validate: (input) => {
114
+ if (!input) return 'API 基础URL不能为空';
273
115
  const error = validator.validateUrl(input);
274
116
  if (error) return error;
275
117
  return true;
276
118
  },
277
- when: (answers) => (answers.ideName || this.presetIdeName) !== 'codex' && (answers.authMode === 'api_key' || answers.authMode === 'auth_token')
119
+ when: (answers) => (answers.ideName || this.presetIdeName) !== 'codex'
278
120
  },
279
121
  {
280
122
  type: 'input',
281
123
  name: 'authToken',
282
124
  message: (answers) => {
283
- switch (answers.authMode) {
284
- case 'api_key':
285
- const tokenTypeLabel = answers.tokenType === 'auth_token' ? 'ANTHROPIC_AUTH_TOKEN' : 'ANTHROPIC_API_KEY';
286
- return `请输入Token (${tokenTypeLabel}):`;
287
- case 'auth_token':
288
- return '请输入认证令牌 (ANTHROPIC_AUTH_TOKEN):';
289
- case 'oauth_token':
290
- return '请输入OAuth令牌 (CLAUDE_CODE_OAUTH_TOKEN):';
291
- default:
292
- return '请输入认证令牌:';
293
- }
125
+ const envVar = answers.authMode === 'auth_token' ? 'ANTHROPIC_AUTH_TOKEN' : 'ANTHROPIC_API_KEY';
126
+ return `请输入 Token (${envVar}):`;
294
127
  },
295
128
  validate: (input) => {
296
129
  const error = validator.validateToken(input);
@@ -365,7 +198,6 @@ class ProviderAdder extends BaseCommand {
365
198
 
366
199
  if (answers.ideName === 'codex') {
367
200
  answers.authMode = 'openai_api_key';
368
- answers.tokenType = null;
369
201
  answers.codexFiles = null;
370
202
 
371
203
  // 从现有配置导入
@@ -436,7 +268,6 @@ class ProviderAdder extends BaseCommand {
436
268
  baseUrl: answers.baseUrl,
437
269
  authToken: answers.authToken,
438
270
  authMode: answers.authMode,
439
- tokenType: answers.tokenType, // 仅在 authMode 为 'api_key' 时使用
440
271
  codexFiles: answers.codexFiles || null,
441
272
  launchArgs,
442
273
  primaryModel: modelConfig.primaryModel,
@@ -484,7 +315,7 @@ class ProviderAdder extends BaseCommand {
484
315
  console.log();
485
316
  console.log(UIHelper.createTooltip('选择要使用的启动参数'));
486
317
  console.log();
487
- console.log(UIHelper.createStepIndicator(3, 3, '可选: 配置启动参数'));
318
+ console.log(UIHelper.createStepIndicator(2, 2, '可选: 配置启动参数'));
488
319
  console.log(UIHelper.createHintLine([
489
320
  ['空格', '切换选中'],
490
321
  ['A', '全选'],
@@ -517,7 +348,7 @@ class ProviderAdder extends BaseCommand {
517
348
  console.log();
518
349
  console.log(UIHelper.createTooltip('配置主模型和快速模型(可选)'));
519
350
  console.log();
520
- console.log(UIHelper.createStepIndicator(3, 3, '可选: 配置模型参数'));
351
+ console.log(UIHelper.createStepIndicator(2, 2, '可选: 配置模型参数'));
521
352
  console.log(UIHelper.createHintLine([
522
353
  ['Enter', '确认输入'],
523
354
  ['ESC', '跳过配置']
@@ -559,7 +390,7 @@ class ProviderAdder extends BaseCommand {
559
390
 
560
391
  async importCodexConfig() {
561
392
  try {
562
- const { readCodexFiles } = require('../utils/codex-files');
393
+ const { readCodexFiles, extractBaseUrlFromConfigToml } = require('../utils/codex-files');
563
394
  const codexFiles = await readCodexFiles();
564
395
 
565
396
  if (!codexFiles.authJson) {
@@ -574,19 +405,15 @@ class ProviderAdder extends BaseCommand {
574
405
  return null;
575
406
  }
576
407
 
577
- // 尝试从 config.toml 获取 base URL
578
- // 支持 api_base_url(akm 写入的格式)和 api_base(某些旧配置可能使用)
408
+ // config.toml 中读取当前激活 provider 的 base_url
409
+ // 优先从 [model_providers.<key>] section 读取,兼容旧的顶层 api_base_url 格式
579
410
  let baseUrl = null;
580
411
  if (codexFiles.configToml) {
581
- const baseUrlMatch = codexFiles.configToml.match(/api_base_url\s*=\s*["']([^"']+)["']/);
582
- if (baseUrlMatch) {
583
- baseUrl = baseUrlMatch[1];
584
- } else {
585
- // 兼容旧格式
586
- const legacyMatch = codexFiles.configToml.match(/api_base\s*=\s*["']([^"']+)["']/);
587
- if (legacyMatch) {
588
- baseUrl = legacyMatch[1];
589
- }
412
+ baseUrl = extractBaseUrlFromConfigToml(codexFiles.configToml);
413
+ if (!baseUrl) {
414
+ // 兼容旧格式(akm 之前错误写入的顶层字段)
415
+ const legacyMatch = codexFiles.configToml.match(/^api_base_url\s*=\s*["']([^"']+)["']/m);
416
+ if (legacyMatch) baseUrl = legacyMatch[1];
590
417
  }
591
418
  }
592
419
 
@@ -669,12 +496,6 @@ class ProviderAdder extends BaseCommand {
669
496
 
670
497
  console.log(chalk.gray(` 认证模式: ${AUTH_MODE_DISPLAY_DETAILED[answers.authMode] || answers.authMode}`));
671
498
 
672
- // 如果是 api_key 模式,显示 tokenType
673
- if (answers.authMode === 'api_key' && answers.tokenType) {
674
- const tokenTypeDisplay = TOKEN_TYPE_DISPLAY[answers.tokenType];
675
- console.log(chalk.gray(` Token类型: ${tokenTypeDisplay}`));
676
- }
677
-
678
499
  if (answers.baseUrl) {
679
500
  console.log(chalk.gray(` 基础URL: ${answers.baseUrl}`));
680
501
  }
@@ -98,6 +98,9 @@ class BackupManager {
98
98
 
99
99
  await this.configManager.ensureLoaded();
100
100
 
101
+ // 导入前自动备份当前配置
102
+ await this.backup();
103
+
101
104
  const importedCount = Object.keys(importData.providers).length;
102
105
  let addedCount = 0;
103
106
  let skippedCount = 0;