@pikecode/api-key-manager 1.0.16 → 1.0.18

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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@pikecode/api-key-manager",
3
- "version": "1.0.16",
3
+ "version": "1.0.18",
4
4
  "description": "A CLI tool for managing and switching multiple API provider configurations",
5
5
  "main": "src/index.js",
6
6
  "bin": {
@@ -22,7 +22,7 @@ class CommandRegistry {
22
22
  return command;
23
23
  }
24
24
 
25
- throw new Error(`命令 '${name}' 未注册`);
25
+ throw new Error(`未知命令 '${name}'\n运行 'akm --help' 查看所有可用命令`);
26
26
  }
27
27
 
28
28
  // 执行命令
@@ -313,12 +313,8 @@ class ProviderAdder extends BaseCommand {
313
313
  ? await this.promptModelConfiguration()
314
314
  : { primaryModel: null, smallFastModel: null };
315
315
 
316
- // 如果是 Codex 快捷方式,确保 ideName 被设置为 'codex'
317
- const finalIdeName = forceCodex ? 'codex' : answers.ideName;
318
-
319
316
  await this.configManager.addProvider(answers.name, {
320
317
  displayName: answers.displayName || answers.name,
321
- ideName: finalIdeName, // 'claude' 或 'codex'
322
318
  baseUrl: answers.baseUrl,
323
319
  authToken: answers.authToken,
324
320
  authMode: answers.authMode,
@@ -25,7 +25,7 @@ class EnvSwitcher extends BaseCommand {
25
25
  await this.configManager.load();
26
26
  const provider = this.configManager.getProvider(providerName);
27
27
  if (!provider) {
28
- throw new Error(`供应商 '${providerName}' 不存在`);
28
+ throw new Error(`供应商 '${providerName}' 不存在\n使用 'akm list' 查看所有已配置的供应商`);
29
29
  }
30
30
  return provider;
31
31
  }
package/src/config.js CHANGED
@@ -50,7 +50,23 @@ class ConfigManager {
50
50
  async _performLoad() {
51
51
  try {
52
52
  if (await fs.pathExists(this.configPath)) {
53
- const data = await fs.readJSON(this.configPath);
53
+ // 检查并修复文件权限(仅 Unix 系统)
54
+ await this._checkAndFixPermissions();
55
+
56
+ let data;
57
+ try {
58
+ data = await fs.readJSON(this.configPath);
59
+ } catch (jsonError) {
60
+ // 配置文件损坏,创建备份并重置
61
+ const backupPath = `${this.configPath}.corrupted.${Date.now()}`;
62
+ await fs.copy(this.configPath, backupPath);
63
+ console.log(chalk.yellow('⚠️ 配置文件损坏,已备份到:'), backupPath);
64
+
65
+ this.config = this.getDefaultConfig();
66
+ await this._performSave();
67
+ this.isLoaded = true;
68
+ return this.config;
69
+ }
54
70
 
55
71
  if (!data || typeof data !== 'object' || Array.isArray(data)) {
56
72
  // 配置文件被写成非对象内容时,重置为默认配置
@@ -84,9 +100,35 @@ class ConfigManager {
84
100
  }
85
101
  }
86
102
 
103
+ async _checkAndFixPermissions() {
104
+ // Windows 不支持 Unix 文件权限
105
+ if (process.platform === 'win32') {
106
+ return;
107
+ }
108
+
109
+ try {
110
+ const stat = await fs.stat(this.configPath);
111
+ const mode = stat.mode & 0o777;
112
+
113
+ // 检查权限是否为 600 (仅所有者可读写) 或 400 (仅所有者可读)
114
+ if (mode !== 0o600 && mode !== 0o400) {
115
+ console.log(chalk.yellow('⚠️ 配置文件权限不安全:'), mode.toString(8), chalk.gray('建议: 0600'));
116
+
117
+ try {
118
+ await fs.chmod(this.configPath, 0o600);
119
+ console.log(chalk.green('✅ 已自动修复文件权限为: 0600'));
120
+ } catch (chmodError) {
121
+ console.log(chalk.red('❌ 无法自动修复权限,请手动执行:'));
122
+ console.log(chalk.gray(` chmod 600 ${this.configPath}`));
123
+ }
124
+ }
125
+ } catch (error) {
126
+ // 忽略权限检查错误,不影响主流程
127
+ }
128
+ }
129
+
87
130
  _migrateAuthModes() {
88
- // 迁移旧的 api_token 模式到新的 auth_token 模式
89
- // 同时为旧配置添加 ideName 字段(默认为 'claude')
131
+ // 迁移旧配置以保持向后兼容
90
132
  if (this.config.providers) {
91
133
  Object.keys(this.config.providers).forEach(key => {
92
134
  const provider = this.config.providers[key];
@@ -96,7 +138,7 @@ class ConfigManager {
96
138
  provider.authMode = 'auth_token';
97
139
  }
98
140
 
99
- // 为缺少 ideName 的旧配置添加默认值
141
+ // 为旧配置添加 ideName 字段(历史兼容性字段,默认为 'claude')
100
142
  if (!provider.ideName) {
101
143
  provider.ideName = 'claude';
102
144
  }
@@ -130,6 +172,12 @@ class ConfigManager {
130
172
  // 保存前确保迁移已应用
131
173
  this._migrateAuthModes();
132
174
  await fs.writeJSON(this.configPath, this.config, { spaces: 2 });
175
+
176
+ // 设置文件权限为 600 (仅所有者可读写)
177
+ if (process.platform !== 'win32') {
178
+ await fs.chmod(this.configPath, 0o600);
179
+ }
180
+
133
181
  // 更新最后修改时间
134
182
  const stat = await fs.stat(this.configPath);
135
183
  this.lastModified = stat.mtime;
@@ -153,11 +201,11 @@ class ConfigManager {
153
201
  this.config.providers[name] = {
154
202
  name,
155
203
  displayName: providerConfig.displayName || name,
156
- ideName: providerConfig.ideName || 'claude', // 'claude' 或 'codex'
204
+ ideName: providerConfig.ideName || 'claude', // 历史兼容性字段
157
205
  baseUrl: providerConfig.baseUrl,
158
206
  authToken: providerConfig.authToken,
159
207
  authMode: providerConfig.authMode || 'api_key',
160
- tokenType: providerConfig.tokenType || 'api_key', // 'api_key' 或 'auth_token' - 仅在 authMode 为 'api_key' 时使用
208
+ tokenType: providerConfig.tokenType || 'api_key', // 仅在 authMode 为 'api_key' 时使用
161
209
  launchArgs: providerConfig.launchArgs || [],
162
210
  models: {
163
211
  primary: providerConfig.primaryModel || null,
@@ -186,9 +234,9 @@ class ConfigManager {
186
234
 
187
235
  async removeProvider(name) {
188
236
  await this.ensureLoaded();
189
-
237
+
190
238
  if (!this.config.providers[name]) {
191
- throw new Error(`供应商 '${name}' 不存在`);
239
+ throw new Error(`供应商 '${name}' 不存在\n使用 'akm list' 查看所有已配置的供应商`);
192
240
  }
193
241
 
194
242
  delete this.config.providers[name];
@@ -203,9 +251,9 @@ class ConfigManager {
203
251
 
204
252
  async setCurrentProvider(name) {
205
253
  await this.ensureLoaded();
206
-
254
+
207
255
  if (!this.config.providers[name]) {
208
- throw new Error(`供应商 '${name}' 不存在`);
256
+ throw new Error(`供应商 '${name}' 不存在\n使用 'akm list' 查看所有已配置的供应商`);
209
257
  }
210
258
 
211
259
  // 重置所有供应商的current状态
@@ -34,9 +34,14 @@ function openFileWithDefaultApp(filePath) {
34
34
 
35
35
  async function openAKMConfigFile() {
36
36
  if (!(await ensureConfigExists())) {
37
- throw new Error('未找到 ~/.akm-config.json,请先运行 akm add 创建配置');
37
+ throw new Error('未找到配置文件 ~/.akm-config.json\n请先运行 \'akm add\' 创建第一个供应商配置');
38
+ }
39
+
40
+ try {
41
+ await openFileWithDefaultApp(AKM_CONFIG_FILE);
42
+ } catch (error) {
43
+ throw new Error(`无法打开配置文件: ${error.message}\n配置文件位置: ${AKM_CONFIG_FILE}`);
38
44
  }
39
- await openFileWithDefaultApp(AKM_CONFIG_FILE);
40
45
  }
41
46
 
42
47
  module.exports = {
@@ -1,5 +1,26 @@
1
1
  const spawn = require('cross-spawn');
2
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
+
3
24
  function clearTerminal() {
4
25
  if (!process.stdout || typeof process.stdout.write !== 'function') {
5
26
  return;
@@ -24,33 +45,37 @@ function clearTerminal() {
24
45
  function buildEnvVariables(config) {
25
46
  const env = { ...process.env };
26
47
 
27
- // Claude Code 配置
28
- if (config.authMode === 'oauth_token') {
29
- env.CLAUDE_CODE_OAUTH_TOKEN = config.authToken;
30
- } else if (config.authMode === 'api_key') {
31
- env.ANTHROPIC_BASE_URL = config.baseUrl;
32
- // 根据 tokenType 选择设置哪种 token
33
- if (config.tokenType === 'auth_token') {
34
- env.ANTHROPIC_AUTH_TOKEN = config.authToken;
48
+ try {
49
+ // Claude Code 配置
50
+ if (config.authMode === 'oauth_token') {
51
+ env.CLAUDE_CODE_OAUTH_TOKEN = sanitizeEnvValue(config.authToken);
52
+ } else if (config.authMode === 'api_key') {
53
+ env.ANTHROPIC_BASE_URL = sanitizeEnvValue(config.baseUrl);
54
+ // 根据 tokenType 选择设置哪种 token
55
+ if (config.tokenType === 'auth_token') {
56
+ env.ANTHROPIC_AUTH_TOKEN = sanitizeEnvValue(config.authToken);
57
+ } else {
58
+ // 默认使用 ANTHROPIC_API_KEY
59
+ env.ANTHROPIC_API_KEY = sanitizeEnvValue(config.authToken);
60
+ }
35
61
  } else {
36
- // 默认使用 ANTHROPIC_API_KEY
37
- env.ANTHROPIC_API_KEY = config.authToken;
62
+ // auth_token 模式
63
+ env.ANTHROPIC_BASE_URL = sanitizeEnvValue(config.baseUrl);
64
+ env.ANTHROPIC_AUTH_TOKEN = sanitizeEnvValue(config.authToken);
38
65
  }
39
- } else {
40
- // auth_token 模式
41
- env.ANTHROPIC_BASE_URL = config.baseUrl;
42
- env.ANTHROPIC_AUTH_TOKEN = config.authToken;
43
- }
44
66
 
45
- if (config.models && config.models.primary) {
46
- env.ANTHROPIC_MODEL = config.models.primary;
47
- }
67
+ if (config.models && config.models.primary) {
68
+ env.ANTHROPIC_MODEL = sanitizeEnvValue(config.models.primary);
69
+ }
48
70
 
49
- if (config.models && config.models.smallFast) {
50
- env.ANTHROPIC_SMALL_FAST_MODEL = config.models.smallFast;
51
- }
71
+ if (config.models && config.models.smallFast) {
72
+ env.ANTHROPIC_SMALL_FAST_MODEL = sanitizeEnvValue(config.models.smallFast);
73
+ }
52
74
 
53
- return env;
75
+ return env;
76
+ } catch (error) {
77
+ throw new Error(`配置验证失败: ${error.message}\n请使用 'akm edit ${config.name}' 修复配置`);
78
+ }
54
79
  }
55
80
 
56
81
  async function executeWithEnv(config, launchArgs = []) {
@@ -72,11 +97,17 @@ async function executeWithEnv(config, launchArgs = []) {
72
97
  if (code === 0) {
73
98
  resolve();
74
99
  } else {
75
- reject(new Error(`Claude Code 退出,代码: ${code}`));
100
+ reject(new Error(`Claude Code 退出,退出代码: ${code}\n提示: 请检查 API 配置是否正确`));
76
101
  }
77
102
  });
78
103
 
79
- child.on('error', reject);
104
+ child.on('error', (error) => {
105
+ if (error.code === 'ENOENT') {
106
+ reject(new Error('找不到 claude 命令\n请确认已安装 Claude Code (https://claude.com/code)'));
107
+ } else {
108
+ reject(new Error(`启动 Claude Code 失败: ${error.message}`));
109
+ }
110
+ });
80
111
  });
81
112
  }
82
113
 
@@ -3,15 +3,33 @@ const validator = {
3
3
  if (!name || typeof name !== 'string') {
4
4
  return '供应商名称不能为空';
5
5
  }
6
-
6
+
7
7
  if (name.trim().length === 0) {
8
8
  return '供应商名称不能为空或只包含空格';
9
9
  }
10
-
10
+
11
+ // 禁止文件系统特殊字符
12
+ if (/[<>:"/\\|?*\x00-\x1F]/.test(name)) {
13
+ return '供应商名称包含非法字符 (不能包含: < > : " / \\ | ? *)';
14
+ }
15
+
16
+ // 禁止使用保留名称 (Windows)
17
+ const reserved = ['CON', 'PRN', 'AUX', 'NUL', 'COM1', 'COM2', 'COM3', 'COM4',
18
+ 'COM5', 'COM6', 'COM7', 'COM8', 'COM9', 'LPT1', 'LPT2',
19
+ 'LPT3', 'LPT4', 'LPT5', 'LPT6', 'LPT7', 'LPT8', 'LPT9'];
20
+ if (reserved.includes(name.toUpperCase())) {
21
+ return '供应商名称不能使用系统保留名称';
22
+ }
23
+
24
+ // 禁止以点或空格开头/结尾 (Windows 限制)
25
+ if (/^[. ]|[. ]$/.test(name)) {
26
+ return '供应商名称不能以点或空格开头/结尾';
27
+ }
28
+
11
29
  if (name.length > 100) {
12
30
  return '供应商名称不能超过100个字符';
13
31
  }
14
-
32
+
15
33
  return null;
16
34
  },
17
35
 
@@ -54,11 +72,27 @@ const validator = {
54
72
  if (!token || typeof token !== 'string') {
55
73
  return 'Token不能为空';
56
74
  }
57
-
58
- if (token.length < 10) {
75
+
76
+ if (token.trim().length === 0) {
77
+ return 'Token不能只包含空格';
78
+ }
79
+
80
+ if (token.trim().length < 10) {
59
81
  return 'Token长度不能少于10个字符';
60
82
  }
61
-
83
+
84
+ // 检测常见的占位符文本
85
+ const placeholders = [
86
+ 'your-key-here', 'your-token', 'your_key', 'your_token',
87
+ 'example', 'test-key', 'demo', 'placeholder', 'replace-me',
88
+ 'insert-key', 'api-key-here', 'token-here', 'xxx', 'yyy',
89
+ 'zzz', 'abc123', '123456'
90
+ ];
91
+ const lowerToken = token.toLowerCase();
92
+ if (placeholders.some(p => lowerToken.includes(p))) {
93
+ return 'Token 似乎是占位符,请输入真实的 API Token';
94
+ }
95
+
62
96
  return null;
63
97
  },
64
98
 
package/bin/cc.js DELETED
@@ -1,101 +0,0 @@
1
- #!/usr/bin/env node
2
-
3
- const { program } = require('commander');
4
- const chalk = require('chalk');
5
- const { main } = require('../src/index');
6
- const { registry } = require('../src/CommandRegistry');
7
- const pkg = require('../package.json');
8
- const { checkForUpdates } = require('../src/utils/update-checker');
9
-
10
- // Set up CLI
11
- program
12
- .name('akm')
13
- .description('API密钥管理工具 - Manage and switch multiple API provider configurations')
14
- .version(pkg.version, '-v, -V, --version', '显示版本号');
15
-
16
- // Check for updates before any command runs
17
- program.hook('preAction', async () => {
18
- await checkForUpdates({ packageName: pkg.name, currentVersion: pkg.version });
19
- });
20
-
21
- // Default command - show provider selection
22
- program
23
- .argument('[provider]', '直接切换到指定供应商')
24
- .action(async (provider) => {
25
- try {
26
- await main(provider);
27
- } catch (error) {
28
- console.error(chalk.red('❌ 执行失败:'), error.message);
29
- process.exit(1);
30
- }
31
- });
32
-
33
- // Add command
34
- program
35
- .command('add')
36
- .description('添加新供应商配置')
37
- .action(async () => {
38
- try {
39
- await registry.executeCommand('add');
40
- } catch (error) {
41
- console.error(chalk.red('❌ 添加失败:'), error.message);
42
- process.exit(1);
43
- }
44
- });
45
-
46
- // Remove command
47
- program
48
- .command('remove')
49
- .argument('[provider]', '要删除的供应商名称')
50
- .description('删除供应商配置')
51
- .action(async (provider) => {
52
- try {
53
- await registry.executeCommand('remove', provider);
54
- } catch (error) {
55
- console.error(chalk.red('❌ 删除失败:'), error.message);
56
- process.exit(1);
57
- }
58
- });
59
-
60
- // List command
61
- program
62
- .command('list')
63
- .description('列出所有供应商')
64
- .action(async () => {
65
- try {
66
- await registry.executeCommand('list');
67
- } catch (error) {
68
- console.error(chalk.red('❌ 列表失败:'), error.message);
69
- process.exit(1);
70
- }
71
- });
72
-
73
- // Current command
74
- program
75
- .command('current')
76
- .description('显示当前配置')
77
- .action(async () => {
78
- try {
79
- await registry.executeCommand('current');
80
- } catch (error) {
81
- console.error(chalk.red('❌ 获取当前配置失败:'), error.message);
82
- process.exit(1);
83
- }
84
- });
85
-
86
- // Edit command
87
- program
88
- .command('edit')
89
- .argument('[provider]', '要编辑的供应商名称')
90
- .description('编辑供应商配置')
91
- .action(async (provider) => {
92
- try {
93
- await registry.executeCommand('edit', provider);
94
- } catch (error) {
95
- console.error(chalk.red('❌ 编辑失败:'), error.message);
96
- process.exit(1);
97
- }
98
- });
99
-
100
- // Parse arguments
101
- program.parse();