@kikkimo/claude-launcher 2.4.0 → 3.0.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/docs/README-zh.md CHANGED
@@ -13,24 +13,29 @@
13
13
 
14
14
  ### 🎨 **精美界面**
15
15
  - Claude 风格界面,采用正宗的橙色/琥珀色配色方案
16
+ - ANSI 备用屏幕缓冲区,无漂移渲染(类似 vim/htop)
16
17
  - 方向键导航,流畅的菜单切换
18
+ - 分页 API 表格,支持 ←→ 翻页浏览大量 API
17
19
  - API 选择和管理的交互式表格
18
20
  - 多语言支持(简体中文、繁体中文、英文、德文、法文、西班牙文、意大利文、葡萄牙文、日文、韩文、俄文)
19
21
 
20
22
  ### 🔐 **高级安全**
21
23
  - 所有敏感数据使用 AES-256-CBC 加密
22
24
  - 机器特定的加密密钥增强安全性
25
+ - 统一密码守卫保护高风险操作(编辑、删除、导入、导出)
23
26
  - 密码保护的配置导入/导出
24
27
  - 安全的 API 令牌存储,带掩码显示
25
28
  - 强密码要求和验证
26
29
 
27
30
  ### 🚀 **第三方 API 管理**
28
- - 全面支持多个第三方 API 提供商(Anthropic、OpenAI、DeepSeek、Moonshot/Kimi、MiniMax、GLM/智谱AI 和自定义 API)
31
+ - 全面支持多个第三方 API 提供商(Anthropic、DeepSeek、Kimi K2.5、MiniMax M2.7、GLM-5.1/智谱AI 和自定义 API)
29
32
  - 带验证的交互式 API 配置
33
+ - **编辑 API**:修改已有 API 的名称、供应商、Base URL 和模型
30
34
  - API 使用统计,支持成功/失败率追踪
31
35
  - 模型升级通知和自动升级支持
32
36
  - 安全的配置备份和恢复
33
37
  - 简单的 API 切换、删除和批量清空
38
+ - 每个配置最多支持 99 个 API
34
39
 
35
40
  ### 🌍 **企业级功能**
36
41
  - 全局安装 - 在任何地方都可以使用 `claude-launcher`
@@ -84,23 +89,25 @@ node claude-launcher
84
89
 
85
90
  1. **启动 Claude Code** - 标准 Claude Code 启动
86
91
  2. **启动 Claude Code(跳过权限)** - 使用 `--dangerously-skip-permissions` 启动
87
- 3. **使用第三方 API 启动 Claude Code** - 使用配置的第三方 API
88
- 4. **使用第三方 API 启动 Claude Code(跳过权限)** - 结合第三方 API 和跳过权限
89
- 5. **第三方 API 管理** - 完整的 API 生命周期管理:
90
- - 添加、切换和删除 API
92
+ 3. **启动 Claude Code(启用自动模式)** - 使用 `--enable-auto-mode` 启动,支持分类器自动审批(Team 计划;启动后按 Shift+Tab 切换)
93
+ 4. **使用第三方 API 启动 Claude Code** - 使用配置的第三方 API
94
+ 5. **使用第三方 API 启动 Claude Code(跳过权限)** - 结合第三方 API 和跳过权限
95
+ 6. **第三方 API 管理** - 完整的 API 生命周期管理:
96
+ - 添加、编辑、切换和删除 API
91
97
  - 查看使用统计(含成功/失败率)
92
98
  - 模型升级设置(自动/手动升级)
93
- - 导入/导出配置
94
- 6. **语言设置** - 在11种支持的语言之间切换
95
- 7. **版本更新检查** - 检查启动器更新
96
- 8. **退出** - 关闭启动器
99
+ - 导入/导出配置(密码保护)
100
+ 7. **配置管理** - 语言、遥测、启动模式、模型升级设置
101
+ 8. **版本更新检查** - 检查启动器更新
102
+ 9. **退出** - 关闭启动器
97
103
 
98
104
  ### 交互式导航
99
105
 
100
- - **方向键**:使用 ↑↓ 导航,Enter 选择
101
- - **Escape 键**:按 Esc 返回或退出
106
+ - **方向键**:使用 ↑↓ 导航,←→ 翻页(分页表格中),Enter 选择
107
+ - **Escape 键**:按 Esc 返回或取消
108
+ - **Ctrl+C**:第一次按下显示警告,第二次按下干净退出
102
109
  - **多语言**:所有界面文本适应您选择的语言
103
- - **智能表格**:API 管理的交互式表格,具有清晰的视觉反馈
110
+ - **智能表格**:分页交互式表格,支持每页选择记忆
104
111
 
105
112
  ### 示例会话
106
113
 
@@ -115,10 +122,11 @@ $ claude-launcher
115
122
 
116
123
  → 启动 Claude Code
117
124
  启动 Claude Code(跳过权限)
125
+ 启动 Claude Code(启用自动模式)
118
126
  使用第三方 API 启动 Claude Code
119
127
  使用第三方 API 启动 Claude Code(跳过权限)
120
128
  第三方 API 管理
121
- 语言设置
129
+ 配置管理
122
130
  版本更新检查
123
131
  退出
124
132
  ```
@@ -131,12 +139,13 @@ $ claude-launcher
131
139
  📋 第三方 API 管理
132
140
 
133
141
  → 添加新 API
142
+ 编辑 API → 选择 API → 编辑名称/供应商/URL/模型
134
143
  删除 API → 删除单个 API / 清空所有 API
135
144
  切换活动 API
136
145
  查看统计信息 → 查看详情 / 重置统计
137
146
  模型升级设置 → 自动升级 [开/关] / 手动升级
138
- 导出配置
139
- 导入配置
147
+ 导出配置 🔒(需要密码)
148
+ 导入配置 🔒(需要密码)
140
149
  更改密码
141
150
  返回主菜单
142
151
  ```
@@ -152,7 +161,7 @@ $ claude-launcher
152
161
 
153
162
  ### 现代配置系统
154
163
 
155
- Claude Launcher 2.0 使用先进的配置系统:
164
+ Claude Launcher 使用先进的配置系统:
156
165
 
157
166
  1. **加密 JSON 存储**:配置存储在 `~/.claude-launcher-apis.json`
158
167
  2. **交互式设置**:首次设置向导引导您完成所有选项
@@ -171,7 +180,7 @@ Claude Launcher 2.0 使用先进的配置系统:
171
180
 
172
181
  通过交互界面配置任何第三方 API 提供商:
173
182
 
174
- - **支持的提供商**:Anthropic、OpenAI、DeepSeek、Moonshot/KimiMiniMax(国内版/国际版)、GLM/智谱AI(GLM-4、GLM-5)和自定义 Anthropic 兼容 API
183
+ - **支持的提供商**:Anthropic、DeepSeek、Moonshot/Kimi(K2.5)、MiniMax(国内版/国际版,M2.7)、GLM/智谱AI(GLM-5.1)和自定义 Anthropic 兼容 API
175
184
  - **安全存储**:所有 API 令牌在存储前加密
176
185
  - **验证**:URL、令牌和模型的实时验证
177
186
  - **使用跟踪**:监控 API 使用统计,支持成功/失败率追踪
@@ -212,6 +221,12 @@ cd claude-launcher
212
221
  npm install
213
222
  ```
214
223
 
224
+ ### 运行测试
225
+
226
+ ```bash
227
+ npm test
228
+ ```
229
+
215
230
  ### 本地测试
216
231
 
217
232
  ```bash
@@ -6,7 +6,8 @@ const fs = require('fs');
6
6
  const path = require('path');
7
7
  const os = require('os');
8
8
  const { encrypt, decrypt } = require('./crypto');
9
- const { validateBaseUrl, validateAuthToken, validateModel } = require('./validators');
9
+ const { validateBaseUrl, validateAuthToken, validateModel, validateApiName } = require('./validators');
10
+ const screen = require('./ui/screen');
10
11
 
11
12
  class ApiManager {
12
13
  constructor() {
@@ -36,7 +37,7 @@ class ApiManager {
36
37
  }
37
38
  }
38
39
  } catch (error) {
39
- console.error(`[!] Could not load API config: ${error.message}`);
40
+ screen.debug(`[!] Could not load API config: ${error.message}`);
40
41
  }
41
42
 
42
43
  return {
@@ -60,11 +61,11 @@ class ApiManager {
60
61
  fs.writeFileSync(this.configFile, encrypted.value);
61
62
  return true;
62
63
  } else {
63
- console.error(`[!] Failed to save API config: ${encrypted.error}`);
64
+ screen.debug(`[!] Failed to save API config: ${encrypted.error}`);
64
65
  return false;
65
66
  }
66
67
  } catch (error) {
67
- console.error(`[!] Error saving API config: ${error.message}`);
68
+ screen.debug(`[!] Error saving API config: ${error.message}`);
68
69
  return false;
69
70
  }
70
71
  }
@@ -96,6 +97,10 @@ class ApiManager {
96
97
  * Add a new API configuration
97
98
  */
98
99
  addApi(baseUrl, authToken, model, name, provider = 'custom') {
100
+ if (this.config.apis.length >= 99) {
101
+ throw new Error('Maximum 99 APIs supported. Remove unused APIs before adding new ones.');
102
+ }
103
+
99
104
  // Validate inputs
100
105
  const urlValidation = validateBaseUrl(baseUrl);
101
106
  if (!urlValidation.valid) {
@@ -241,15 +246,95 @@ class ApiManager {
241
246
  * @returns {Object} The updated API object
242
247
  */
243
248
  updateApiModel(apiId, newModel) {
249
+ return this.updateApiField(apiId, 'model', newModel);
250
+ }
251
+
252
+ /**
253
+ * Update a single field of an API configuration with validation
254
+ * @param {string} apiId - The API id
255
+ * @param {string} field - Field name: 'name', 'provider', 'baseUrl', 'model'
256
+ * @param {string} value - New value
257
+ * @returns {Object} The updated API object
258
+ */
259
+ updateApiField(apiId, field, value) {
260
+ const allowedFields = ['name', 'provider', 'baseUrl', 'model'];
261
+ if (!allowedFields.includes(field)) {
262
+ throw new Error(`Field '${field}' is not allowed. Allowed: ${allowedFields.join(', ')}`);
263
+ }
264
+
244
265
  const index = this.config.apis.findIndex(api => api.id === apiId);
245
266
  if (index === -1) {
246
267
  throw new Error(`API not found: ${apiId}`);
247
268
  }
248
269
 
249
- this.config.apis[index].model = newModel;
250
- this.config.apis[index].smallFastModel = newModel;
270
+ const api = this.config.apis[index];
271
+
272
+ // Manager-level validation
273
+ switch (field) {
274
+ case 'name': {
275
+ if (!value || value.trim() === '') {
276
+ throw new Error('Name cannot be empty when editing');
277
+ }
278
+ const nameValidation = validateApiName(value);
279
+ if (!nameValidation.valid) {
280
+ throw new Error(`Invalid name: ${nameValidation.error}`);
281
+ }
282
+ break;
283
+ }
284
+ case 'provider': {
285
+ const { getAllProviders } = require('./presets/providers');
286
+ const validIds = getAllProviders().map(p => p.id);
287
+ if (!validIds.includes(value)) {
288
+ throw new Error(`Unknown provider: ${value}. Valid: ${validIds.join(', ')}`);
289
+ }
290
+ break;
291
+ }
292
+ case 'baseUrl': {
293
+ const urlValidation = validateBaseUrl(value);
294
+ if (!urlValidation.valid) {
295
+ throw new Error(`Invalid URL: ${urlValidation.error}`);
296
+ }
297
+ break;
298
+ }
299
+ case 'model': {
300
+ const modelValidation = validateModel(value);
301
+ if (!modelValidation.valid) {
302
+ throw new Error(`Invalid model: ${modelValidation.error}`);
303
+ }
304
+ break;
305
+ }
306
+ }
307
+
308
+ // Duplicate check for uniqueness-affecting fields
309
+ if (field === 'baseUrl' || field === 'model') {
310
+ const checkUrl = field === 'baseUrl' ? value : api.baseUrl;
311
+ const checkModel = field === 'model' ? value : api.model;
312
+ const decryptedToken = decrypt(api.authToken);
313
+ const tokenValue = decryptedToken.success ? decryptedToken.value : '';
314
+
315
+ // Check against all OTHER apis (exclude self)
316
+ const duplicate = this.config.apis.find((other, idx) => {
317
+ if (idx === index) return false;
318
+ const otherToken = decrypt(other.authToken);
319
+ const otherTokenValue = otherToken.success ? otherToken.value : '';
320
+ return other.baseUrl === checkUrl &&
321
+ otherTokenValue === tokenValue &&
322
+ other.model === checkModel;
323
+ });
324
+
325
+ if (duplicate) {
326
+ throw new Error(`Duplicate configuration: URL + Token + Model already exists for API '${duplicate.name}'`);
327
+ }
328
+ }
329
+
330
+ // Apply update
331
+ api[field] = value.trim();
332
+ if (field === 'model') {
333
+ api.smallFastModel = value.trim();
334
+ }
335
+
251
336
  this.saveConfig();
252
- return this.config.apis[index];
337
+ return api;
253
338
  }
254
339
 
255
340
  /**
@@ -289,6 +374,40 @@ class ApiManager {
289
374
  return null;
290
375
  }
291
376
 
377
+ /**
378
+ * Record a launch attempt (optimistic success)
379
+ * Call rollbackLaunchAttempt() if a pre-launch sync error occurs
380
+ * @returns {Object|null} The updated API object or null
381
+ */
382
+ recordLaunchAttempt() {
383
+ const activeApi = this.getActiveApi();
384
+ if (activeApi) {
385
+ const index = this.config.activeIndex;
386
+ this.config.apis[index].lastUsed = new Date().toISOString();
387
+ this.config.apis[index].usageCount = (this.config.apis[index].usageCount || 0) + 1;
388
+ this.config.apis[index].successCount = (this.config.apis[index].successCount || 0) + 1;
389
+ this.config.apis[index].lastError = null;
390
+ this.saveConfig();
391
+ return this.config.apis[index];
392
+ }
393
+ return null;
394
+ }
395
+
396
+ /**
397
+ * Rollback an optimistic launch attempt on pre-launch sync error
398
+ * @param {string} errorMessage - The error message
399
+ */
400
+ rollbackLaunchAttempt(errorMessage) {
401
+ const activeApi = this.getActiveApi();
402
+ if (activeApi) {
403
+ const index = this.config.activeIndex;
404
+ this.config.apis[index].successCount = Math.max(0, (this.config.apis[index].successCount || 0) - 1);
405
+ this.config.apis[index].failCount = (this.config.apis[index].failCount || 0) + 1;
406
+ this.config.apis[index].lastError = errorMessage;
407
+ this.saveConfig();
408
+ }
409
+ }
410
+
292
411
  /**
293
412
  * Get statistics about API usage
294
413
  */
@@ -506,12 +625,18 @@ class ApiManager {
506
625
  }
507
626
 
508
627
  configData.apis.forEach(importApi => {
628
+ if (this.config.apis.length >= 99) {
629
+ screen.debug('Import skipped: maximum 99 APIs reached');
630
+ skipped++;
631
+ return;
632
+ }
633
+
509
634
  // Validate the API configuration before importing
510
635
  try {
511
636
  // Validate Base URL
512
637
  const urlValidation = validateBaseUrl(importApi.baseUrl);
513
638
  if (!urlValidation.valid) {
514
- console.warn(`Skipping API "${importApi.name || 'Unknown'}" - Invalid Base URL: ${urlValidation.error}`);
639
+ screen.debug(`Skipping API "${importApi.name || 'Unknown'}" - Invalid Base URL: ${urlValidation.error}`);
515
640
  skipped++;
516
641
  return;
517
642
  }
@@ -520,7 +645,7 @@ class ApiManager {
520
645
  if (importApi.authToken !== '***REQUIRES_MANUAL_INPUT***') {
521
646
  const tokenValidation = validateAuthToken(importApi.authToken);
522
647
  if (!tokenValidation.valid) {
523
- console.warn(`Skipping API "${importApi.name || 'Unknown'}" - Invalid Auth Token: ${tokenValidation.error}`);
648
+ screen.debug(`Skipping API "${importApi.name || 'Unknown'}" - Invalid Auth Token: ${tokenValidation.error}`);
524
649
  skipped++;
525
650
  return;
526
651
  }
@@ -529,7 +654,7 @@ class ApiManager {
529
654
  // Validate Model
530
655
  const modelValidation = validateModel(importApi.model);
531
656
  if (!modelValidation.valid) {
532
- console.warn(`Skipping API "${importApi.name || 'Unknown'}" - Invalid Model: ${modelValidation.error}`);
657
+ screen.debug(`Skipping API "${importApi.name || 'Unknown'}" - Invalid Model: ${modelValidation.error}`);
533
658
  skipped++;
534
659
  return;
535
660
  }
@@ -570,7 +695,7 @@ class ApiManager {
570
695
  }
571
696
  }
572
697
  } catch (error) {
573
- console.warn(`Skipping API "${importApi.name || 'Unknown'}" - Validation error: ${error.message}`);
698
+ screen.debug(`Skipping API "${importApi.name || 'Unknown'}" - Validation error: ${error.message}`);
574
699
  skipped++;
575
700
  }
576
701
  });
@@ -5,6 +5,7 @@
5
5
 
6
6
  const readline = require('readline');
7
7
  const stdinManager = require('../utils/stdin-manager');
8
+ const screen = require('../ui/screen');
8
9
 
9
10
  /**
10
11
  * Get password input with proper masking (no plaintext display)
@@ -18,7 +19,7 @@ function getPasswordInput(prompt) {
18
19
  let cleanedUp = false;
19
20
 
20
21
  // Display prompt - this is necessary for password input
21
- process.stdout.write(prompt);
22
+ screen.write(prompt);
22
23
 
23
24
  if (!process.stdin.isTTY) {
24
25
  // Non-TTY environment fallback - use scope's createReadline
@@ -52,11 +53,14 @@ function getPasswordInput(prompt) {
52
53
  return;
53
54
  }
54
55
 
56
+ screen.showCursor();
57
+
55
58
  const cleanup = () => {
56
59
  if (cleanedUp) return;
57
60
  cleanedUp = true;
58
61
 
59
62
  try {
63
+ screen.hideCursor();
60
64
  scope.removeAllListeners('data');
61
65
  scope.release();
62
66
  } catch (error) {
@@ -85,7 +89,7 @@ function getPasswordInput(prompt) {
85
89
  case '\r': // Enter (CR)
86
90
  case '\n': // Line Feed (LF)
87
91
  case '\r\n': // CRLF
88
- process.stdout.write('\n');
92
+ screen.write('\n');
89
93
  cleanup();
90
94
  resolve(password);
91
95
  return;
@@ -95,7 +99,7 @@ function getPasswordInput(prompt) {
95
99
  if (password.length > 0) {
96
100
  password = password.slice(0, -1);
97
101
  // Clear the last asterisk
98
- process.stdout.write('\b \b');
102
+ screen.write('\b \b');
99
103
  }
100
104
  return;
101
105
 
@@ -114,7 +118,7 @@ function getPasswordInput(prompt) {
114
118
  // Filter out control characters (only accept ASCII printable)
115
119
  if (charCode >= 32 && charCode < 127) {
116
120
  password += char;
117
- process.stdout.write('*');
121
+ screen.write('*');
118
122
  }
119
123
  }
120
124
  return;