zcf 3.2.3 → 3.3.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.
Files changed (41) hide show
  1. package/README.md +102 -14
  2. package/dist/chunks/api-providers.mjs +76 -0
  3. package/dist/chunks/claude-code-config-manager.mjs +21 -7
  4. package/dist/chunks/claude-code-incremental-manager.mjs +210 -14
  5. package/dist/chunks/codex-config-switch.mjs +197 -12
  6. package/dist/chunks/codex-provider-manager.mjs +20 -9
  7. package/dist/chunks/codex-uninstaller.mjs +19 -24
  8. package/dist/chunks/commands.mjs +1 -1
  9. package/dist/chunks/features.mjs +637 -0
  10. package/dist/chunks/simple-config.mjs +182 -29
  11. package/dist/cli.mjs +13 -613
  12. package/dist/i18n/locales/en/api.json +6 -1
  13. package/dist/i18n/locales/en/codex.json +7 -0
  14. package/dist/i18n/locales/en/errors.json +1 -0
  15. package/dist/i18n/locales/en/multi-config.json +8 -1
  16. package/dist/i18n/locales/zh-CN/api.json +6 -1
  17. package/dist/i18n/locales/zh-CN/codex.json +7 -0
  18. package/dist/i18n/locales/zh-CN/errors.json +1 -0
  19. package/dist/i18n/locales/zh-CN/multi-config.json +8 -1
  20. package/dist/index.d.mts +13 -1
  21. package/dist/index.d.ts +13 -1
  22. package/dist/index.mjs +1 -1
  23. package/package.json +1 -1
  24. package/templates/CLAUDE.md +191 -0
  25. package/templates/claude-code/CLAUDE.md +1 -0
  26. package/templates/claude-code/en/output-styles/engineer-professional.md +2 -1
  27. package/templates/claude-code/en/output-styles/laowang-engineer.md +1 -0
  28. package/templates/claude-code/en/output-styles/nekomata-engineer.md +1 -0
  29. package/templates/claude-code/en/output-styles/ojousama-engineer.md +1 -0
  30. package/templates/claude-code/zh-CN/output-styles/engineer-professional.md +2 -1
  31. package/templates/claude-code/zh-CN/output-styles/laowang-engineer.md +1 -0
  32. package/templates/claude-code/zh-CN/output-styles/nekomata-engineer.md +1 -0
  33. package/templates/claude-code/zh-CN/output-styles/ojousama-engineer.md +1 -0
  34. package/templates/codex/en/system-prompt/engineer-professional.md +2 -1
  35. package/templates/codex/en/system-prompt/laowang-engineer.md +1 -0
  36. package/templates/codex/en/system-prompt/nekomata-engineer.md +1 -0
  37. package/templates/codex/en/system-prompt/ojousama-engineer.md +1 -0
  38. package/templates/codex/zh-CN/system-prompt/engineer-professional.md +2 -1
  39. package/templates/codex/zh-CN/system-prompt/laowang-engineer.md +1 -0
  40. package/templates/codex/zh-CN/system-prompt/nekomata-engineer.md +1 -0
  41. package/templates/codex/zh-CN/system-prompt/ojousama-engineer.md +1 -0
package/README.md CHANGED
@@ -111,14 +111,17 @@ npx zcf ccr --all-lang zh-CN # Configure CCR in Chinese
111
111
  For CI/CD and automated setups, use `--skip-prompt` with parameters:
112
112
 
113
113
  ```bash
114
- # Shorthand version
114
+ # Using API provider preset (v3.3.0+ New - Simplified)
115
+ npx zcf i -s -p 302ai -k "sk-xxx"
116
+
117
+ # Shorthand version (traditional)
115
118
  npx zcf i -s -g zh-CN -t api_key -k "sk-xxx" -u "https://xxx.xxx"
116
119
 
117
- # Complete version
120
+ # Complete version (traditional)
118
121
  npx zcf i --skip-prompt --all-lang zh-CN --api-type api_key --api-key "sk-xxx" --api-url "https://xxx.xxx"
119
122
 
120
- # Shorthand version (with custom models)
121
- npx zcf i -s -t api_key -k "sk-xxx" -M "claude-sonnet-4-5" -F "claude-haiku-4-5"
123
+ # Using provider preset with custom models
124
+ npx zcf i -s -p 302ai -k "sk-xxx" -M "claude-sonnet-4-5" -F "claude-haiku-4-5"
122
125
 
123
126
  # Complete version (with custom models)
124
127
  npx zcf i --skip-prompt \
@@ -127,8 +130,66 @@ npx zcf i --skip-prompt \
127
130
  --api-url "https://xxx.xxx" \
128
131
  --api-model "claude-sonnet-4-5" \
129
132
  --api-fast-model "claude-haiku-4-5"
133
+
134
+ # Multiple API configurations (JSON string)
135
+ npx zcf i -s --api-configs '[
136
+ {"provider":"302ai","key":"sk-xxx"},
137
+ {"provider":"glm","key":"sk-yyy"},
138
+ {"name":"custom","type":"api_key","key":"sk-zzz","url":"https://custom.api.com","primaryModel":"claude-sonnet-4-5","fastModel":"claude-haiku-4-5","default":true}
139
+ ]'
140
+
141
+ # Multiple API configurations (JSON file)
142
+ npx zcf i -s --api-configs-file ./api-configs.json
143
+
144
+ # For Codex with multiple providers
145
+ npx zcf i -s -T cx --api-configs '[
146
+ {"provider":"302ai","key":"sk-xxx"},
147
+ {"name":"custom","type":"api_key","key":"sk-yyy","url":"https://custom.api.com","primaryModel":"gpt-5","default":true}
148
+ ]'
130
149
  ```
131
150
 
151
+ #### 🎯 API Provider Presets (v3.3.0+ New)
152
+
153
+ ZCF now supports API provider presets that automatically configure baseUrl and models, simplifying configuration from 5+ parameters to just 2:
154
+
155
+ **Supported Providers:**
156
+ - `302ai` - [302.AI](https://share.302.ai/gAT9VG) API Service
157
+ - `glm` - GLM (z.ai)
158
+ - `minimax` - MiniMax API Service
159
+ - `kimi` - Kimi (Moonshot AI)
160
+ - `custom` - Custom API endpoint (requires manual URL configuration)
161
+
162
+ **Usage Examples:**
163
+
164
+ ```bash
165
+ # Using 302.AI provider
166
+ npx zcf i --skip-prompt --provider 302ai --api-key "sk-xxx"
167
+ # or shorthand
168
+ npx zcf i -s -p 302ai -k "sk-xxx"
169
+
170
+ # Using GLM provider
171
+ npx zcf i -s -p glm -k "sk-xxx"
172
+
173
+ # Using MiniMax provider
174
+ npx zcf i -s -p minimax -k "sk-xxx"
175
+
176
+ # Using Kimi provider
177
+ npx zcf i -s -p kimi -k "sk-xxx"
178
+
179
+ # Using custom provider (requires URL)
180
+ npx zcf i -s -p custom -k "sk-xxx" -u "https://api.example.com"
181
+
182
+ # For Codex
183
+ npx zcf i -s -T cx -p 302ai -k "sk-xxx"
184
+ ```
185
+
186
+ **Benefits:**
187
+ - ✅ Automatic baseUrl configuration
188
+ - ✅ Automatic authType selection
189
+ - ✅ Automatic model configuration (if available)
190
+ - ✅ Reduces configuration from 5+ parameters to 2
191
+ - ✅ Supports both Claude Code and Codex
192
+
132
193
  #### Non-interactive Mode Parameters
133
194
 
134
195
  When using `--skip-prompt`, the following parameters are available:
@@ -136,21 +197,25 @@ When using `--skip-prompt`, the following parameters are available:
136
197
  | Parameter | Description | Values | Required | Default |
137
198
  | ---------------------------- | -------------------------------------------------------- | -------------------------------------------------------------------------------------------------- | -------------------------------------- | -------------------------------------------------------------------------------------------------------------------------------- |
138
199
  | `--skip-prompt, -s` | Skip all interactive prompts | - | Yes (for non-interactive mode) | - |
200
+ | `--provider, -p` | API provider preset (v3.3.0+ New) | `302ai`, `glm`, `minimax`, `kimi`, `custom` | No | - (Simplifies configuration by auto-filling baseUrl and models) |
139
201
  | `--lang, -l` | ZCF display language (applies to all commands) | `zh-CN`, `en` | No | `en` or user's saved preference |
140
202
  | `--config-lang, -c` | Configuration language (template files language) | `zh-CN`, `en` | No | `en` |
141
203
  | `--ai-output-lang, -a` | AI output language | `zh-CN`, `en`, custom string | No | `en` |
142
204
  | `--all-lang, -g` | Set all language parameters (applies to all commands) | `zh-CN`, `en`, custom string | No | - (Priority: `--all-lang` > `--lang` > saved user preference > interactive prompt. Custom string sets AI output language to custom while interaction and config languages remain 'en') |
143
205
  | `--config-action, -r` | Config handling | `new`, `backup`, `merge`, `docs-only`, `skip` | No | `backup` |
144
- | `--api-type, -t` | API configuration type | `auth_token`, `api_key`, `ccr_proxy`, `skip` | No | `skip` |
206
+ | `--api-type, -t` | API configuration type | `auth_token`, `api_key`, `ccr_proxy`, `skip` | No | `skip` (auto-set to `api_key` when `--provider` is specified) |
145
207
  | `--api-key, -k` | API key (for both API key and auth token types) | string | Required when `api-type` is not `skip` | - |
146
- | `--api-url, -u` | Custom API URL | URL string | No | official API |
147
- | `--api-model, -M` | Primary API model | string (e.g., `claude-sonnet-4-5`) | No | - |
148
- | `--api-fast-model, -F` | Fast API model (Claude Code only) | string (e.g., `claude-haiku-4-5`) | No | - |
208
+ | `--api-url, -u` | Custom API URL | URL string | No | official API (auto-filled when using `--provider`) |
209
+ | `--api-model, -M` | Primary API model | string (e.g., `claude-sonnet-4-5`) | No | - (auto-filled when using `--provider` if available) |
210
+ | `--api-fast-model, -F` | Fast API model (Claude Code only) | string (e.g., `claude-haiku-4-5`) | No | - (auto-filled when using `--provider` if available) |
149
211
  | `--mcp-services, -m` | MCP services to install (multi-select, comma-separated) | `context7`, `open-websearch`, `spec-workflow`, `mcp-deepwiki`, `Playwright`, `exa`, `serena`, or `skip` for none | No | `all` |
150
212
  | `--workflows, -w` | Workflows to install (multi-select, comma-separated) | `commonTools`, `sixStepsWorkflow`, `featPlanUx`, `gitWorkflow`, `bmadWorkflow`, or `skip` for none | No | `all` |
151
213
  | `--output-styles, -o` | Output styles to install (multi-select, comma-separated) | `engineer-professional`, `nekomata-engineer`, `laowang-engineer`, `ojousama-engineer`, or `skip` for none | No | `all` |
152
214
  | `--default-output-style, -d` | Default output style | Same as output styles plus built-in: `default`, `explanatory`, `learning` | No | `engineer-professional` |
153
215
  | `--install-cometix-line, -x` | Install CCometixLine statusline tool | `true`, `false` | No | `true` |
216
+ | `--code-type, -T` | Target code tool type | `claude-code`, `codex`, `cc`, `cx` | No | Current active tool type from ZCF config |
217
+ | `--api-configs` | Multiple API configurations (JSON string) | JSON array string of API configuration objects | No | - (Mutually exclusive with `--api-configs-file`) |
218
+ | `--api-configs-file` | Multiple API configurations (JSON file path) | Path to JSON file containing API configuration array | No | - (Mutually exclusive with `--api-configs`) |
154
219
 
155
220
  #### 🤖 Codex Support (v3.0.0+ New)
156
221
 
@@ -402,12 +467,35 @@ After configuration:
402
467
 
403
468
  ### 🔐 API Configuration
404
469
 
405
- - Supports two authentication methods:
406
- - **Auth Token**: For tokens obtained via OAuth or browser login
407
- - **API Key**: For API keys from Anthropic Console
408
- - Custom API URL support
409
- - Support for manual configuration later
410
- - Partial modification: Update only needed configuration items (v2.0 new)
470
+ ZCF provides flexible API configuration options for both Claude Code and Codex:
471
+
472
+ **Quick Setup with API Provider Presets (v3.3.0+ New):**
473
+
474
+ Choose from popular API providers with pre-configured settings:
475
+ - **302.AI** - Pay-as-you-go AI service with comprehensive model support
476
+ - **GLM (智谱AI)** - Zhipu AI's GLM models
477
+ - **MiniMax** - MiniMax AI service
478
+ - **Kimi (Moonshot AI)** - Moonshot's Kimi models
479
+ - **Custom Configuration** - Full manual configuration for any provider
480
+
481
+ When using a preset provider, you only need to:
482
+ 1. Select the provider from the list
483
+ 2. Enter your API key
484
+
485
+ The system automatically configures:
486
+ - API base URL
487
+ - Authentication type (API Key or Auth Token)
488
+ - Default models (if applicable)
489
+ - Wire API protocol (for Codex)
490
+
491
+ **Traditional Configuration Methods:**
492
+
493
+ - **Official Login**: Use official authentication system (no API configuration needed)
494
+ - **Auth Token**: For tokens obtained via OAuth or browser login
495
+ - **API Key**: For API keys from Anthropic Console or custom providers
496
+ - **CCR Proxy**: Configure Claude Code Router proxy
497
+ - **Custom API URL**: Support for any compatible API endpoint
498
+ - **Partial Modification**: Update only needed configuration items (v2.0+)
411
499
 
412
500
  ### 💾 Configuration Management
413
501
 
@@ -0,0 +1,76 @@
1
+ const API_PROVIDER_PRESETS = [
2
+ {
3
+ id: "302ai",
4
+ name: "302.AI",
5
+ supportedCodeTools: ["claude-code", "codex"],
6
+ claudeCode: {
7
+ baseUrl: "https://api.302.ai/cc",
8
+ authType: "api_key"
9
+ },
10
+ codex: {
11
+ baseUrl: "https://api.302.ai/v1",
12
+ wireApi: "responses"
13
+ },
14
+ description: "302.AI API Service"
15
+ },
16
+ {
17
+ id: "glm",
18
+ name: "GLM",
19
+ supportedCodeTools: ["claude-code", "codex"],
20
+ claudeCode: {
21
+ baseUrl: "https://open.bigmodel.cn/api/anthropic",
22
+ authType: "auth_token"
23
+ },
24
+ codex: {
25
+ baseUrl: "https://open.bigmodel.cn/api/coding/paas/v4",
26
+ wireApi: "chat",
27
+ defaultModel: "GLM-4.6"
28
+ },
29
+ description: "GLM (\u667A\u8C31AI)"
30
+ },
31
+ {
32
+ id: "minimax",
33
+ name: "MiniMax",
34
+ supportedCodeTools: ["claude-code", "codex"],
35
+ claudeCode: {
36
+ baseUrl: "https://api.minimaxi.com/anthropic",
37
+ authType: "auth_token",
38
+ defaultModels: ["MiniMax-M2", "MiniMax-M2"]
39
+ },
40
+ codex: {
41
+ baseUrl: "https://api.minimaxi.com/v1",
42
+ wireApi: "chat",
43
+ defaultModel: "MiniMax-M2"
44
+ },
45
+ description: "MiniMax API Service"
46
+ },
47
+ {
48
+ id: "kimi",
49
+ name: "Kimi",
50
+ supportedCodeTools: ["claude-code", "codex"],
51
+ claudeCode: {
52
+ baseUrl: "https://api.moonshot.cn/anthropic",
53
+ authType: "auth_token",
54
+ defaultModels: ["kimi-k2-0905-preview", "kimi-k2-turbo-preview"]
55
+ },
56
+ codex: {
57
+ baseUrl: "https://api.moonshot.cn/v1",
58
+ wireApi: "chat",
59
+ defaultModel: "kimi-k2-0905-preview"
60
+ },
61
+ description: "Kimi (Moonshot AI)"
62
+ }
63
+ ];
64
+ function getApiProviders(codeToolType) {
65
+ return API_PROVIDER_PRESETS.filter(
66
+ (provider) => provider.supportedCodeTools.includes(codeToolType)
67
+ );
68
+ }
69
+ function getProviderPreset(providerId) {
70
+ return API_PROVIDER_PRESETS.find((provider) => provider.id === providerId);
71
+ }
72
+ function getValidProviderIds() {
73
+ return API_PROVIDER_PRESETS.map((provider) => provider.id);
74
+ }
75
+
76
+ export { API_PROVIDER_PRESETS, getApiProviders, getProviderPreset, getValidProviderIds };
@@ -1,6 +1,6 @@
1
1
  import dayjs from 'dayjs';
2
2
  import { join } from 'pathe';
3
- import { j as ZCF_CONFIG_FILE, Z as ZCF_CONFIG_DIR, a8 as ensureDir, a9 as readDefaultTomlConfig, aa as createDefaultTomlConfig, ab as exists, ac as readJsonConfig, ad as writeTomlConfig, S as SETTINGS_FILE, ae as copyFile } from './simple-config.mjs';
3
+ import { q as ZCF_CONFIG_FILE, Z as ZCF_CONFIG_DIR, ae as ensureDir, af as readDefaultTomlConfig, ag as createDefaultTomlConfig, ah as exists, ai as readJsonConfig, aj as writeTomlConfig, S as SETTINGS_FILE, ak as copyFile } from './simple-config.mjs';
4
4
  import 'node:fs';
5
5
  import 'node:process';
6
6
  import 'ansis';
@@ -182,15 +182,15 @@ class ClaudeCodeConfigManager {
182
182
  * Apply profile settings to Claude Code runtime
183
183
  */
184
184
  static async applyProfileSettings(profile) {
185
- const { ensureI18nInitialized, i18n } = await import('./simple-config.mjs').then(function (n) { return n.a_; });
185
+ const { ensureI18nInitialized, i18n } = await import('./simple-config.mjs').then(function (n) { return n.b4; });
186
186
  ensureI18nInitialized();
187
187
  try {
188
188
  if (!profile) {
189
- const { switchToOfficialLogin } = await import('./simple-config.mjs').then(function (n) { return n.b2; });
189
+ const { switchToOfficialLogin } = await import('./simple-config.mjs').then(function (n) { return n.b8; });
190
190
  switchToOfficialLogin();
191
191
  return;
192
192
  }
193
- const { readJsonConfig: readJsonConfig2, writeJsonConfig } = await import('./simple-config.mjs').then(function (n) { return n.b0; });
193
+ const { readJsonConfig: readJsonConfig2, writeJsonConfig } = await import('./simple-config.mjs').then(function (n) { return n.b6; });
194
194
  const settings = readJsonConfig2(SETTINGS_FILE) || {};
195
195
  if (!settings.env)
196
196
  settings.env = {};
@@ -202,7 +202,7 @@ class ClaudeCodeConfigManager {
202
202
  settings.env.ANTHROPIC_AUTH_TOKEN = profile.apiKey;
203
203
  delete settings.env.ANTHROPIC_API_KEY;
204
204
  } else if (profile.authType === "ccr_proxy") {
205
- const { readCcrConfig } = await import('./simple-config.mjs').then(function (n) { return n.b3; });
205
+ const { readCcrConfig } = await import('./simple-config.mjs').then(function (n) { return n.b9; });
206
206
  const ccrConfig = readCcrConfig();
207
207
  if (!ccrConfig) {
208
208
  throw new Error(i18n.t("ccr:ccrNotConfigured") || "CCR proxy configuration not found");
@@ -221,8 +221,18 @@ class ClaudeCodeConfigManager {
221
221
  else
222
222
  delete settings.env.ANTHROPIC_BASE_URL;
223
223
  }
224
+ if (profile.primaryModel) {
225
+ settings.env.ANTHROPIC_MODEL = profile.primaryModel;
226
+ } else {
227
+ delete settings.env.ANTHROPIC_MODEL;
228
+ }
229
+ if (profile.fastModel) {
230
+ settings.env.ANTHROPIC_SMALL_FAST_MODEL = profile.fastModel;
231
+ } else {
232
+ delete settings.env.ANTHROPIC_SMALL_FAST_MODEL;
233
+ }
224
234
  writeJsonConfig(SETTINGS_FILE, settings);
225
- const { setPrimaryApiKey, addCompletedOnboarding } = await import('./simple-config.mjs').then(function (n) { return n.b1; });
235
+ const { setPrimaryApiKey, addCompletedOnboarding } = await import('./simple-config.mjs').then(function (n) { return n.b7; });
226
236
  setPrimaryApiKey();
227
237
  addCompletedOnboarding();
228
238
  if (shouldRestartCcr) {
@@ -250,6 +260,10 @@ class ClaudeCodeConfigManager {
250
260
  sanitized.apiKey = profile.apiKey;
251
261
  if (profile.baseUrl)
252
262
  sanitized.baseUrl = profile.baseUrl;
263
+ if (profile.primaryModel)
264
+ sanitized.primaryModel = profile.primaryModel;
265
+ if (profile.fastModel)
266
+ sanitized.fastModel = profile.fastModel;
253
267
  return sanitized;
254
268
  }
255
269
  /**
@@ -575,7 +589,7 @@ class ClaudeCodeConfigManager {
575
589
  */
576
590
  static async syncCcrProfile() {
577
591
  try {
578
- const { readCcrConfig } = await import('./simple-config.mjs').then(function (n) { return n.b3; });
592
+ const { readCcrConfig } = await import('./simple-config.mjs').then(function (n) { return n.b9; });
579
593
  const ccrConfig = readCcrConfig();
580
594
  if (!ccrConfig) {
581
595
  await this.ensureCcrProfileExists(ccrConfig);
@@ -1,6 +1,6 @@
1
1
  import ansis from 'ansis';
2
2
  import inquirer from 'inquirer';
3
- import { a4 as ensureI18nInitialized, a5 as i18n, a6 as addNumbersToChoices, a7 as validateApiKey } from './simple-config.mjs';
3
+ import { aa as ensureI18nInitialized, ab as i18n, ac as addNumbersToChoices, ad as validateApiKey } from './simple-config.mjs';
4
4
  import { ClaudeCodeConfigManager } from './claude-code-config-manager.mjs';
5
5
  import 'node:fs';
6
6
  import 'node:process';
@@ -48,6 +48,7 @@ async function configureIncrementalManagement() {
48
48
  const choices = [
49
49
  { name: i18n.t("multi-config:addProfile"), value: "add" },
50
50
  { name: i18n.t("multi-config:editProfile"), value: "edit" },
51
+ { name: i18n.t("multi-config:copyProfile"), value: "copy" },
51
52
  { name: i18n.t("multi-config:deleteProfile"), value: "delete" },
52
53
  { name: i18n.t("common:skip"), value: "skip" }
53
54
  ];
@@ -68,6 +69,9 @@ async function configureIncrementalManagement() {
68
69
  case "edit":
69
70
  await handleEditProfile(profiles);
70
71
  break;
72
+ case "copy":
73
+ await handleCopyProfile(profiles);
74
+ break;
71
75
  case "delete":
72
76
  await handleDeleteProfile(profiles);
73
77
  break;
@@ -85,17 +89,40 @@ async function promptContinueAdding() {
85
89
  async function handleAddProfile() {
86
90
  console.log(ansis.cyan(`
87
91
  ${i18n.t("multi-config:addingNewProfile")}`));
92
+ const { getApiProviders } = await import('./api-providers.mjs');
93
+ const providers = getApiProviders("claude-code");
94
+ const providerChoices = [
95
+ { name: i18n.t("api:customProvider"), value: "custom" },
96
+ ...providers.map((p) => ({ name: p.name, value: p.id }))
97
+ ];
98
+ const { selectedProvider } = await inquirer.prompt([{
99
+ type: "list",
100
+ name: "selectedProvider",
101
+ message: i18n.t("api:selectApiProvider"),
102
+ choices: addNumbersToChoices(providerChoices)
103
+ }]);
104
+ let prefilledBaseUrl;
105
+ let prefilledAuthType;
106
+ if (selectedProvider !== "custom") {
107
+ const provider = providers.find((p) => p.id === selectedProvider);
108
+ if (provider?.claudeCode) {
109
+ prefilledBaseUrl = provider.claudeCode.baseUrl;
110
+ prefilledAuthType = provider.claudeCode.authType;
111
+ console.log(ansis.gray(i18n.t("api:providerSelected", { name: provider.name })));
112
+ }
113
+ }
88
114
  const answers = await inquirer.prompt([
89
115
  {
90
116
  type: "input",
91
117
  name: "profileName",
92
118
  message: i18n.t("multi-config:profileNamePrompt"),
119
+ default: selectedProvider !== "custom" ? providers.find((p) => p.id === selectedProvider)?.name : void 0,
93
120
  validate: (input) => {
94
121
  const trimmed = input.trim();
95
122
  if (!trimmed) {
96
123
  return i18n.t("multi-config:profileNameRequired");
97
124
  }
98
- if (!/^[\w\-\s\u4E00-\u9FA5]+$/.test(trimmed)) {
125
+ if (!/^[\w\-\s.\u4E00-\u9FA5]+$/.test(trimmed)) {
99
126
  return i18n.t("multi-config:profileNameInvalid");
100
127
  }
101
128
  return true;
@@ -109,14 +136,16 @@ ${i18n.t("multi-config:addingNewProfile")}`));
109
136
  { name: i18n.t("multi-config:authType.api_key"), value: "api_key" },
110
137
  { name: i18n.t("multi-config:authType.auth_token"), value: "auth_token" }
111
138
  ],
112
- default: "api_key"
139
+ default: prefilledAuthType || "api_key",
140
+ when: () => selectedProvider === "custom"
141
+ // Only ask if custom
113
142
  },
114
143
  {
115
144
  type: "input",
116
145
  name: "baseUrl",
117
146
  message: i18n.t("multi-config:baseUrlPrompt"),
118
- default: "https://api.anthropic.com",
119
- when: (answers2) => answers2.authType !== "ccr_proxy",
147
+ default: prefilledBaseUrl || "https://api.anthropic.com",
148
+ when: (answers2) => selectedProvider === "custom" && answers2.authType !== "ccr_proxy",
120
149
  validate: (input) => {
121
150
  const trimmed = input.trim();
122
151
  if (!trimmed) {
@@ -133,8 +162,8 @@ ${i18n.t("multi-config:addingNewProfile")}`));
133
162
  {
134
163
  type: "input",
135
164
  name: "apiKey",
136
- message: i18n.t("multi-config:apiKeyPrompt"),
137
- when: (answers2) => answers2.authType !== "ccr_proxy",
165
+ message: selectedProvider !== "custom" ? i18n.t("api:enterProviderApiKey", { provider: providers.find((p) => p.id === selectedProvider)?.name || selectedProvider }) : i18n.t("multi-config:apiKeyPrompt"),
166
+ when: (answers2) => selectedProvider === "custom" ? answers2.authType !== "ccr_proxy" : true,
138
167
  validate: (input) => {
139
168
  const trimmed = input.trim();
140
169
  if (!trimmed) {
@@ -146,7 +175,14 @@ ${i18n.t("multi-config:addingNewProfile")}`));
146
175
  }
147
176
  return true;
148
177
  }
149
- },
178
+ }
179
+ ]);
180
+ let modelConfig = null;
181
+ if (selectedProvider === "custom") {
182
+ const { promptCustomModels } = await import('./features.mjs');
183
+ modelConfig = await promptCustomModels();
184
+ }
185
+ const { setAsDefault } = await inquirer.prompt([
150
186
  {
151
187
  type: "confirm",
152
188
  name: "setAsDefault",
@@ -159,11 +195,19 @@ ${i18n.t("multi-config:addingNewProfile")}`));
159
195
  const profile = {
160
196
  id: profileId,
161
197
  name: profileName,
162
- authType: answers.authType
198
+ authType: selectedProvider === "custom" ? answers.authType : prefilledAuthType
163
199
  };
164
- if (answers.authType !== "ccr_proxy") {
200
+ if (profile.authType !== "ccr_proxy") {
165
201
  profile.apiKey = answers.apiKey.trim();
166
- profile.baseUrl = answers.baseUrl.trim();
202
+ profile.baseUrl = selectedProvider === "custom" ? answers.baseUrl.trim() : prefilledBaseUrl;
203
+ }
204
+ if (modelConfig) {
205
+ if (modelConfig.primaryModel.trim()) {
206
+ profile.primaryModel = modelConfig.primaryModel.trim();
207
+ }
208
+ if (modelConfig.fastModel.trim()) {
209
+ profile.fastModel = modelConfig.fastModel.trim();
210
+ }
167
211
  }
168
212
  const existingProfile = ClaudeCodeConfigManager.getProfileByName(profile.name);
169
213
  if (existingProfile) {
@@ -188,14 +232,16 @@ ${i18n.t("multi-config:addingNewProfile")}`));
188
232
  name: profile.name,
189
233
  authType: profile.authType,
190
234
  apiKey: profile.apiKey,
191
- baseUrl: profile.baseUrl
235
+ baseUrl: profile.baseUrl,
236
+ primaryModel: profile.primaryModel,
237
+ fastModel: profile.fastModel
192
238
  });
193
239
  if (updateResult.success) {
194
240
  console.log(ansis.green(i18n.t("multi-config:profileUpdated", { name: profile.name })));
195
241
  if (updateResult.backupPath) {
196
242
  console.log(ansis.gray(i18n.t("common:backupCreated", { path: updateResult.backupPath })));
197
243
  }
198
- if (answers.setAsDefault) {
244
+ if (setAsDefault) {
199
245
  const switchResult = await ClaudeCodeConfigManager.switchProfile(existingProfile.id);
200
246
  if (switchResult.success) {
201
247
  console.log(ansis.green(i18n.t("multi-config:profileSetAsDefault", { name: profile.name })));
@@ -213,7 +259,7 @@ ${i18n.t("multi-config:addingNewProfile")}`));
213
259
  if (result.backupPath) {
214
260
  console.log(ansis.gray(i18n.t("common:backupCreated", { path: result.backupPath })));
215
261
  }
216
- if (answers.setAsDefault) {
262
+ if (setAsDefault) {
217
263
  const switchResult = await ClaudeCodeConfigManager.switchProfile(runtimeProfile.id);
218
264
  if (switchResult.success) {
219
265
  console.log(ansis.green(i18n.t("multi-config:profileSetAsDefault", { name: runtimeProfile.name })));
@@ -306,12 +352,28 @@ ${i18n.t("multi-config:editingProfile", { name: selectedProfile.name })}`));
306
352
  }
307
353
  }
308
354
  ]);
355
+ let modelConfig = null;
356
+ if (selectedProfile.authType !== "ccr_proxy") {
357
+ const { promptCustomModels } = await import('./features.mjs');
358
+ modelConfig = await promptCustomModels(
359
+ selectedProfile.primaryModel,
360
+ selectedProfile.fastModel
361
+ );
362
+ }
309
363
  const updateData = {
310
364
  name: answers.profileName.trim()
311
365
  };
312
366
  if (selectedProfile.authType !== "ccr_proxy") {
313
367
  updateData.apiKey = answers.apiKey.trim();
314
368
  updateData.baseUrl = answers.baseUrl.trim();
369
+ if (modelConfig) {
370
+ if (modelConfig.primaryModel.trim()) {
371
+ updateData.primaryModel = modelConfig.primaryModel.trim();
372
+ }
373
+ if (modelConfig.fastModel.trim()) {
374
+ updateData.fastModel = modelConfig.fastModel.trim();
375
+ }
376
+ }
315
377
  }
316
378
  const result = await ClaudeCodeConfigManager.updateProfile(selectedProfile.id, updateData);
317
379
  if (result.success) {
@@ -331,6 +393,140 @@ ${i18n.t("multi-config:editingProfile", { name: selectedProfile.name })}`));
331
393
  console.log(ansis.red(i18n.t("multi-config:profileUpdateFailed", { error: result.error })));
332
394
  }
333
395
  }
396
+ async function handleCopyProfile(profiles) {
397
+ const choices = profiles.map((profile) => ({
398
+ name: `${profile.name} (${getAuthTypeLabel(profile.authType)})`,
399
+ value: profile.id
400
+ }));
401
+ const { selectedProfileId } = await inquirer.prompt([{
402
+ type: "list",
403
+ name: "selectedProfileId",
404
+ message: i18n.t("multi-config:selectProfileToCopy"),
405
+ choices: addNumbersToChoices(choices)
406
+ }]);
407
+ if (!selectedProfileId) {
408
+ console.log(ansis.yellow(i18n.t("common:cancelled")));
409
+ return;
410
+ }
411
+ const selectedProfile = profiles.find((p) => p.id === selectedProfileId);
412
+ if (!selectedProfile) {
413
+ console.log(ansis.red(i18n.t("multi-config:profileNotFound")));
414
+ return;
415
+ }
416
+ console.log(ansis.cyan(`
417
+ ${i18n.t("multi-config:copyingProfile", { name: selectedProfile.name })}`));
418
+ const copiedName = `${selectedProfile.name}-copy`;
419
+ const questions = [
420
+ {
421
+ type: "input",
422
+ name: "profileName",
423
+ message: i18n.t("multi-config:profileNamePrompt"),
424
+ default: copiedName,
425
+ validate: (input) => {
426
+ const trimmed = input.trim();
427
+ if (!trimmed) {
428
+ return i18n.t("multi-config:profileNameRequired");
429
+ }
430
+ if (!/^[\w\-\s.\u4E00-\u9FA5]+$/.test(trimmed)) {
431
+ return i18n.t("multi-config:profileNameInvalid");
432
+ }
433
+ return true;
434
+ }
435
+ }
436
+ ];
437
+ if (selectedProfile.authType !== "ccr_proxy") {
438
+ questions.push(
439
+ {
440
+ type: "input",
441
+ name: "baseUrl",
442
+ message: i18n.t("multi-config:baseUrlPrompt"),
443
+ default: selectedProfile.baseUrl || "https://api.anthropic.com",
444
+ validate: (input) => {
445
+ const trimmed = input.trim();
446
+ if (!trimmed) {
447
+ return i18n.t("multi-config:baseUrlRequired");
448
+ }
449
+ try {
450
+ new URL(trimmed);
451
+ return true;
452
+ } catch {
453
+ return i18n.t("multi-config:baseUrlInvalid");
454
+ }
455
+ }
456
+ },
457
+ {
458
+ type: "input",
459
+ name: "apiKey",
460
+ message: i18n.t("multi-config:apiKeyPrompt"),
461
+ default: selectedProfile.apiKey,
462
+ validate: (input) => {
463
+ const trimmed = input.trim();
464
+ if (!trimmed) {
465
+ return i18n.t("multi-config:apiKeyRequired");
466
+ }
467
+ const validation = validateApiKey(trimmed);
468
+ if (!validation.isValid) {
469
+ return validation.error || "Invalid API key format";
470
+ }
471
+ return true;
472
+ }
473
+ }
474
+ );
475
+ }
476
+ const answers = await inquirer.prompt(questions);
477
+ let modelConfig = null;
478
+ if (selectedProfile.authType !== "ccr_proxy") {
479
+ const { promptCustomModels } = await import('./features.mjs');
480
+ modelConfig = await promptCustomModels(
481
+ selectedProfile.primaryModel,
482
+ selectedProfile.fastModel
483
+ );
484
+ }
485
+ const { setAsDefault } = await inquirer.prompt([
486
+ {
487
+ type: "confirm",
488
+ name: "setAsDefault",
489
+ message: i18n.t("multi-config:setAsDefaultPrompt"),
490
+ default: false
491
+ }
492
+ ]);
493
+ const profileName = answers.profileName.trim();
494
+ const profileId = ClaudeCodeConfigManager.generateProfileId(profileName);
495
+ const copiedProfile = {
496
+ id: profileId,
497
+ name: profileName,
498
+ authType: selectedProfile.authType
499
+ };
500
+ if (selectedProfile.authType !== "ccr_proxy") {
501
+ copiedProfile.apiKey = answers.apiKey.trim();
502
+ copiedProfile.baseUrl = answers.baseUrl.trim();
503
+ if (modelConfig) {
504
+ if (modelConfig.primaryModel.trim()) {
505
+ copiedProfile.primaryModel = modelConfig.primaryModel.trim();
506
+ }
507
+ if (modelConfig.fastModel.trim()) {
508
+ copiedProfile.fastModel = modelConfig.fastModel.trim();
509
+ }
510
+ }
511
+ }
512
+ const result = await ClaudeCodeConfigManager.addProfile(copiedProfile);
513
+ if (result.success) {
514
+ const runtimeProfile = result.addedProfile || { ...copiedProfile, id: profileId };
515
+ console.log(ansis.green(i18n.t("multi-config:profileCopied", { name: runtimeProfile.name })));
516
+ if (result.backupPath) {
517
+ console.log(ansis.gray(i18n.t("common:backupCreated", { path: result.backupPath })));
518
+ }
519
+ if (setAsDefault) {
520
+ const switchResult = await ClaudeCodeConfigManager.switchProfile(runtimeProfile.id);
521
+ if (switchResult.success) {
522
+ console.log(ansis.green(i18n.t("multi-config:profileSetAsDefault", { name: runtimeProfile.name })));
523
+ await ClaudeCodeConfigManager.applyProfileSettings(runtimeProfile);
524
+ }
525
+ }
526
+ } else {
527
+ console.log(ansis.red(i18n.t("multi-config:profileCopyFailed", { error: result.error })));
528
+ }
529
+ }
334
530
  async function handleDeleteProfile(profiles) {
335
531
  if (profiles.length <= 1) {
336
532
  console.log(ansis.yellow(i18n.t("multi-config:cannotDeleteLast")));