@kikkimo/claude-launcher 3.0.0 → 3.2.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.
@@ -9,31 +9,63 @@ const { encrypt, decrypt } = require('./crypto');
9
9
  const { validateBaseUrl, validateAuthToken, validateModel, validateApiName } = require('./validators');
10
10
  const screen = require('./ui/screen');
11
11
 
12
+ class DuplicateApiError extends Error {
13
+ constructor(existingApi) {
14
+ super(`Duplicate API: ${existingApi.name}`);
15
+ this.name = 'DuplicateApiError';
16
+ this.code = 'DUPLICATE_API';
17
+ this.existingApiId = existingApi.id;
18
+ this.existingApiName = existingApi.name;
19
+ }
20
+ }
21
+
22
+ const MODEL_CONFIG_LABELS = {
23
+ ANTHROPIC_DEFAULT_SONNET_MODEL: 'Regular Model (Sonnet)',
24
+ ANTHROPIC_DEFAULT_OPUS_MODEL: 'Heavy Model (Opus)',
25
+ ANTHROPIC_DEFAULT_HAIKU_MODEL: 'Fast Model (Haiku)',
26
+ CLAUDE_CODE_SUBAGENT_MODEL: 'Subagent Model',
27
+ ANTHROPIC_CUSTOM_MODEL_OPTION: 'Custom Model',
28
+ ANTHROPIC_CUSTOM_MODEL_OPTION_NAME: 'Custom Model Name',
29
+ };
30
+
31
+ const RUNTIME_CONFIG_LABELS = {
32
+ API_TIMEOUT_MS: 'Request Timeout',
33
+ CLAUDE_CODE_ATTRIBUTION_HEADER: 'Output Attribution',
34
+ CLAUDE_CODE_DISABLE_NONESSENTIAL_TRAFFIC: 'Reduce Non-Essential Traffic',
35
+ CLAUDE_CODE_EFFORT_LEVEL: 'Reasoning Effort',
36
+ CLAUDE_CODE_DISABLE_EXPERIMENTAL_BETAS: 'Experimental Features',
37
+ CLAUDE_CODE_DISABLE_NONSTREAMING_FALLBACK: 'Disable Non-Streaming Fallback',
38
+ };
39
+
12
40
  class ApiManager {
13
41
  constructor() {
14
42
  this.configFile = path.join(os.homedir(), '.claude-launcher-apis.json');
15
- this.config = this.loadConfig();
43
+ const { config, migrated } = this.loadConfig();
44
+ this.config = config;
45
+ if (migrated) {
46
+ this.saveConfig();
47
+ }
16
48
  }
17
49
 
18
50
  /**
19
51
  * Load configuration from encrypted file
20
52
  */
21
53
  loadConfig() {
54
+ let migrated = false;
22
55
  try {
23
56
  if (fs.existsSync(this.configFile)) {
24
57
  const encryptedData = fs.readFileSync(this.configFile, 'utf8');
25
58
  const decrypted = decrypt(encryptedData);
26
59
  if (decrypted.success) {
27
60
  const config = JSON.parse(decrypted.value);
28
- // Ensure required fields exist
29
- if (!config.hasOwnProperty('exportPassword')) {
30
- config.exportPassword = null;
31
- }
32
- if (!config.hasOwnProperty('passwordSkipped')) {
33
- config.passwordSkipped = false;
34
- }
61
+ if (!config.hasOwnProperty('exportPassword')) config.exportPassword = null;
62
+ if (!config.hasOwnProperty('passwordSkipped')) config.passwordSkipped = false;
35
63
 
36
- return config;
64
+ const { PREDEFINED_MODEL_ENV_KEYS, PREDEFINED_RUNTIME_KEYS } = require('./validators');
65
+ for (const api of config.apis || []) {
66
+ migrated = this._migrateApiEntry(api, PREDEFINED_MODEL_ENV_KEYS, PREDEFINED_RUNTIME_KEYS) || migrated;
67
+ }
68
+ return { config, migrated };
37
69
  }
38
70
  }
39
71
  } catch (error) {
@@ -41,15 +73,169 @@ class ApiManager {
41
73
  }
42
74
 
43
75
  return {
44
- apis: [],
45
- activeIndex: -1,
46
- version: '2.0.0',
47
- createdAt: new Date().toISOString(),
48
- exportPassword: null, // Hashed export password for validation
49
- passwordSkipped: false // Whether user permanently skipped password setup
76
+ config: {
77
+ apis: [],
78
+ activeIndex: -1,
79
+ version: '2.0.0',
80
+ createdAt: new Date().toISOString(),
81
+ exportPassword: null,
82
+ passwordSkipped: false,
83
+ },
84
+ migrated: false,
50
85
  };
51
86
  }
52
87
 
88
+ _migrateApiEntry(api, MODEL_KEYS, RUNTIME_KEYS) {
89
+ const before = JSON.stringify({
90
+ modelEnvVars: api.modelEnvVars,
91
+ _autoModelEnvVars: api._autoModelEnvVars,
92
+ runtimeEnvVars: api.runtimeEnvVars,
93
+ _runtimeEnvSources: api._runtimeEnvSources,
94
+ customEnvVars: api.customEnvVars,
95
+ smallFastModel: api.smallFastModel,
96
+ _autoFilledModel: api._autoFilledModel,
97
+ });
98
+ this._normalizeApiFields(api);
99
+ const after = JSON.stringify({
100
+ modelEnvVars: api.modelEnvVars,
101
+ _autoModelEnvVars: api._autoModelEnvVars,
102
+ runtimeEnvVars: api.runtimeEnvVars,
103
+ _runtimeEnvSources: api._runtimeEnvSources,
104
+ customEnvVars: api.customEnvVars,
105
+ smallFastModel: api.smallFastModel,
106
+ _autoFilledModel: api._autoFilledModel,
107
+ });
108
+ return before !== after;
109
+ }
110
+
111
+ _normalizeApiFields(api) {
112
+ const { PREDEFINED_MODEL_ENV_KEYS, PREDEFINED_RUNTIME_KEYS } = require('./validators');
113
+ const { getProvider } = require('./presets/providers');
114
+
115
+ const effectiveModel = api._autoFilledModel || api.model;
116
+ const hadAutoModelEnvVars = !!api._autoModelEnvVars;
117
+
118
+ const providerConfig = getProvider(api.provider);
119
+ let template;
120
+ if (providerConfig && providerConfig.modelEnvTemplate) {
121
+ template = providerConfig.modelEnvTemplate.getValues(effectiveModel);
122
+ } else {
123
+ template = {};
124
+ for (const k of PREDEFINED_MODEL_ENV_KEYS) template[k] = effectiveModel;
125
+ template.smallFastModel = effectiveModel;
126
+ }
127
+
128
+ let smallFastWasFixed = false;
129
+
130
+ // _autoModelEnvVars — full rebuild via template
131
+ if (!api._autoModelEnvVars) {
132
+ api._autoModelEnvVars = { ...template };
133
+ } else {
134
+ for (const k of PREDEFINED_MODEL_ENV_KEYS) {
135
+ if (!(k in api._autoModelEnvVars) || typeof api._autoModelEnvVars[k] !== 'string') {
136
+ api._autoModelEnvVars[k] = template[k] || '';
137
+ }
138
+ }
139
+ for (const k of Object.keys(api._autoModelEnvVars)) {
140
+ if (!PREDEFINED_MODEL_ENV_KEYS.includes(k) && k !== 'smallFastModel') {
141
+ delete api._autoModelEnvVars[k];
142
+ }
143
+ }
144
+ if (!('smallFastModel' in api._autoModelEnvVars) || typeof api._autoModelEnvVars.smallFastModel !== 'string') {
145
+ api._autoModelEnvVars.smallFastModel = template.smallFastModel;
146
+ smallFastWasFixed = true;
147
+ }
148
+ }
149
+
150
+ // modelEnvVars — fill with template actual values (NOT "")
151
+ if (!api.modelEnvVars) {
152
+ api.modelEnvVars = {};
153
+ }
154
+ for (const k of PREDEFINED_MODEL_ENV_KEYS) {
155
+ if (!(k in api.modelEnvVars) || typeof api.modelEnvVars[k] !== 'string') {
156
+ api.modelEnvVars[k] = template[k] || '';
157
+ }
158
+ }
159
+ for (const k of Object.keys(api.modelEnvVars)) {
160
+ if (!PREDEFINED_MODEL_ENV_KEYS.includes(k)) {
161
+ delete api.modelEnvVars[k];
162
+ }
163
+ }
164
+
165
+ // smallFastModel — sync with template
166
+ if (!api.smallFastModel || typeof api.smallFastModel !== 'string'
167
+ || !hadAutoModelEnvVars || smallFastWasFixed) {
168
+ api.smallFastModel = template.smallFastModel;
169
+ }
170
+
171
+ // template 漂移检测:provider template 升级后首次加载旧配置时,_autoModelEnvVars
172
+ // 仍是旧快照。检测到漂移则刷新仍等于旧快照的 tier 字段(保留用户手动覆盖),
173
+ // 复用 updateApiField() 的保留覆盖模式。_migrateApiEntry 的 before/after 比较
174
+ // 已覆盖 modelEnvVars/_autoModelEnvVars/smallFastModel,会据此返回 migrated=true
175
+ // 并由构造函数统一 saveConfig()。
176
+ if (hadAutoModelEnvVars) {
177
+ let drifted = false;
178
+ for (const k of PREDEFINED_MODEL_ENV_KEYS) {
179
+ if (api._autoModelEnvVars[k] !== template[k]) { drifted = true; break; }
180
+ }
181
+ if (!drifted && api._autoModelEnvVars.smallFastModel !== template.smallFastModel) {
182
+ drifted = true;
183
+ }
184
+ if (drifted) {
185
+ for (const k of PREDEFINED_MODEL_ENV_KEYS) {
186
+ if (api.modelEnvVars[k] === api._autoModelEnvVars[k]) {
187
+ api.modelEnvVars[k] = template[k] || '';
188
+ }
189
+ }
190
+ if (api.smallFastModel === api._autoModelEnvVars.smallFastModel) {
191
+ api.smallFastModel = template.smallFastModel;
192
+ }
193
+ api._autoModelEnvVars = { ...template };
194
+ }
195
+ }
196
+
197
+ // runtimeEnvVars — fill "" not provider values
198
+ if (!api.runtimeEnvVars) {
199
+ api.runtimeEnvVars = {};
200
+ }
201
+ for (const k of PREDEFINED_RUNTIME_KEYS) {
202
+ if (!(k in api.runtimeEnvVars) || typeof api.runtimeEnvVars[k] !== 'string') {
203
+ api.runtimeEnvVars[k] = '';
204
+ }
205
+ }
206
+
207
+ // _runtimeEnvSources — missing → "auto"
208
+ if (!api._runtimeEnvSources) {
209
+ api._runtimeEnvSources = {};
210
+ }
211
+ for (const k of PREDEFINED_RUNTIME_KEYS) {
212
+ if (!(k in api._runtimeEnvSources)) {
213
+ api._runtimeEnvSources[k] = 'auto';
214
+ }
215
+ if (api._runtimeEnvSources[k] !== 'auto' && api._runtimeEnvSources[k] !== 'manual') {
216
+ api._runtimeEnvSources[k] = 'auto';
217
+ }
218
+ }
219
+
220
+ // runtime/source conflict resolution
221
+ for (const k of PREDEFINED_RUNTIME_KEYS) {
222
+ if (api.runtimeEnvVars[k] !== '' && api._runtimeEnvSources[k] === 'auto') {
223
+ api.runtimeEnvVars[k] = '';
224
+ }
225
+ }
226
+
227
+ // customEnvVars
228
+ if (!api.customEnvVars) {
229
+ api.customEnvVars = {};
230
+ }
231
+
232
+ if (api._autoFilledModel) {
233
+ delete api._autoFilledModel;
234
+ }
235
+
236
+ return api;
237
+ }
238
+
53
239
  /**
54
240
  * Save configuration to encrypted file
55
241
  */
@@ -120,7 +306,7 @@ class ApiManager {
120
306
  // Check for duplicates
121
307
  const duplicate = this.checkDuplicate(baseUrl, authToken, model);
122
308
  if (duplicate.isDuplicate) {
123
- throw new Error(`${duplicate.type} already exists for API: ${duplicate.existing.name}`);
309
+ throw new DuplicateApiError(duplicate.existing);
124
310
  }
125
311
 
126
312
  // Encrypt the auth token before storing
@@ -129,6 +315,32 @@ class ApiManager {
129
315
  throw new Error(`Failed to encrypt auth token: ${encryptedToken.error}`);
130
316
  }
131
317
 
318
+ // Compute model env template values
319
+ const { getProvider } = require('./presets/providers');
320
+ const { PREDEFINED_MODEL_ENV_KEYS, PREDEFINED_RUNTIME_KEYS } = require('./validators');
321
+ const providerConfig = getProvider(provider);
322
+ let templateValues;
323
+ if (providerConfig && providerConfig.modelEnvTemplate) {
324
+ templateValues = providerConfig.modelEnvTemplate.getValues(modelValidation.value);
325
+ } else {
326
+ templateValues = {};
327
+ for (const k of PREDEFINED_MODEL_ENV_KEYS) templateValues[k] = modelValidation.value;
328
+ templateValues.smallFastModel = modelValidation.value;
329
+ }
330
+
331
+ const modelEnvVars = {};
332
+ for (const k of PREDEFINED_MODEL_ENV_KEYS) {
333
+ modelEnvVars[k] = templateValues[k] || '';
334
+ }
335
+ const _autoModelEnvVars = { ...templateValues };
336
+
337
+ const runtimeEnvVars = {};
338
+ const _runtimeEnvSources = {};
339
+ for (const k of PREDEFINED_RUNTIME_KEYS) {
340
+ runtimeEnvVars[k] = '';
341
+ _runtimeEnvSources[k] = 'auto';
342
+ }
343
+
132
344
  const newApi = {
133
345
  id: Date.now().toString(),
134
346
  name: name || `API-${this.config.apis.length + 1}`,
@@ -136,15 +348,18 @@ class ApiManager {
136
348
  baseUrl: urlValidation.value,
137
349
  authToken: encryptedToken.value,
138
350
  model: modelValidation.value,
139
- smallFastModel: modelValidation.value, // Same as model as requested
351
+ smallFastModel: templateValues.smallFastModel,
140
352
  createdAt: new Date().toISOString(),
141
- // Existing statistics fields
142
353
  lastUsed: null,
143
354
  usageCount: 0,
144
- // New statistics fields
145
355
  successCount: 0,
146
356
  failCount: 0,
147
- lastError: null
357
+ lastError: null,
358
+ modelEnvVars,
359
+ _autoModelEnvVars,
360
+ runtimeEnvVars,
361
+ _runtimeEnvSources,
362
+ customEnvVars: {},
148
363
  };
149
364
 
150
365
  this.config.apis.push(newApi);
@@ -287,7 +502,7 @@ class ApiManager {
287
502
  if (!validIds.includes(value)) {
288
503
  throw new Error(`Unknown provider: ${value}. Valid: ${validIds.join(', ')}`);
289
504
  }
290
- break;
505
+ return this.updateApiProvider(apiId, value).api;
291
506
  }
292
507
  case 'baseUrl': {
293
508
  const urlValidation = validateBaseUrl(value);
@@ -330,13 +545,143 @@ class ApiManager {
330
545
  // Apply update
331
546
  api[field] = value.trim();
332
547
  if (field === 'model') {
333
- api.smallFastModel = value.trim();
548
+ const { getProvider } = require('./presets/providers');
549
+ const { PREDEFINED_MODEL_ENV_KEYS } = require('./validators');
550
+ const providerConfig = getProvider(api.provider);
551
+ let templateVals;
552
+ if (providerConfig && providerConfig.modelEnvTemplate) {
553
+ templateVals = providerConfig.modelEnvTemplate.getValues(value.trim());
554
+ } else {
555
+ templateVals = {};
556
+ for (const k of PREDEFINED_MODEL_ENV_KEYS) templateVals[k] = value.trim();
557
+ templateVals.smallFastModel = value.trim();
558
+ }
559
+ if (api._autoModelEnvVars) {
560
+ if (!api.modelEnvVars) api.modelEnvVars = {};
561
+ for (const k of PREDEFINED_MODEL_ENV_KEYS) {
562
+ if (api.modelEnvVars[k] === api._autoModelEnvVars[k]) {
563
+ api.modelEnvVars[k] = templateVals[k] || '';
564
+ }
565
+ }
566
+ if (api.smallFastModel === api._autoModelEnvVars.smallFastModel) {
567
+ api.smallFastModel = templateVals.smallFastModel;
568
+ }
569
+ } else {
570
+ // No snapshot means all fields are auto — overwrite all
571
+ if (!api.modelEnvVars) api.modelEnvVars = {};
572
+ for (const k of PREDEFINED_MODEL_ENV_KEYS) {
573
+ api.modelEnvVars[k] = templateVals[k] || '';
574
+ }
575
+ api.smallFastModel = templateVals.smallFastModel;
576
+ }
577
+ api._autoModelEnvVars = { ...templateVals };
334
578
  }
335
579
 
336
580
  this.saveConfig();
337
581
  return api;
338
582
  }
339
583
 
584
+ updateModelEnvVar(apiId, key, value) {
585
+ const { PREDEFINED_MODEL_ENV_KEYS } = require('./validators');
586
+ if (!PREDEFINED_MODEL_ENV_KEYS.includes(key)) throw new Error(`"${key}" is not a predefined model env key`);
587
+ if (typeof value !== 'string') throw new Error('model env value must be a string');
588
+ const index = this.config.apis.findIndex(a => a.id === apiId);
589
+ if (index === -1) throw new Error(`API not found: ${apiId}`);
590
+ const api = this.config.apis[index];
591
+ const autoValue = api._autoModelEnvVars && typeof api._autoModelEnvVars[key] === 'string'
592
+ ? api._autoModelEnvVars[key]
593
+ : '';
594
+ api.modelEnvVars[key] = value === '' ? autoValue : value;
595
+ this.saveConfig();
596
+ return api;
597
+ }
598
+
599
+ updateRuntimeEnvVar(apiId, key, value, options = {}) {
600
+ const { PREDEFINED_RUNTIME_KEYS, validateRuntimeEnvValue } = require('./validators');
601
+ if (!PREDEFINED_RUNTIME_KEYS.includes(key)) throw new Error(`"${key}" is not a predefined runtime env key`);
602
+ const validation = validateRuntimeEnvValue(key, value);
603
+ if (!validation.valid) throw new Error(`Invalid value for ${key}: ${validation.error}`);
604
+ const index = this.config.apis.findIndex(a => a.id === apiId);
605
+ if (index === -1) throw new Error(`API not found: ${apiId}`);
606
+ if (options.source === 'auto') {
607
+ this.config.apis[index].runtimeEnvVars[key] = '';
608
+ this.config.apis[index]._runtimeEnvSources[key] = 'auto';
609
+ } else if (options.source === 'manual') {
610
+ this.config.apis[index].runtimeEnvVars[key] = value;
611
+ this.config.apis[index]._runtimeEnvSources[key] = 'manual';
612
+ } else {
613
+ this.config.apis[index].runtimeEnvVars[key] = value;
614
+ this.config.apis[index]._runtimeEnvSources[key] = (value === '') ? 'auto' : 'manual';
615
+ }
616
+ this.saveConfig();
617
+ return this.config.apis[index];
618
+ }
619
+
620
+ setCustomEnvVar(apiId, key, value) {
621
+ const { validateEnvKey } = require('./validators');
622
+ const kv = validateEnvKey(key);
623
+ if (!kv.valid) throw new Error(`Custom env key "${key}" is reserved or invalid`);
624
+ if (typeof value !== 'string') throw new Error('Custom env value must be a string');
625
+ const index = this.config.apis.findIndex(a => a.id === apiId);
626
+ if (index === -1) throw new Error(`API not found: ${apiId}`);
627
+ this.config.apis[index].customEnvVars[key] = value;
628
+ this.saveConfig();
629
+ return this.config.apis[index];
630
+ }
631
+
632
+ deleteCustomEnvVar(apiId, key) {
633
+ const index = this.config.apis.findIndex(a => a.id === apiId);
634
+ if (index === -1) throw new Error(`API not found: ${apiId}`);
635
+ delete this.config.apis[index].customEnvVars[key];
636
+ this.saveConfig();
637
+ return this.config.apis[index];
638
+ }
639
+
640
+ updateApiProvider(apiId, newProviderId) {
641
+ const { getProvider, getAllProviders, detectProvider } = require('./presets/providers');
642
+ const { PREDEFINED_MODEL_ENV_KEYS, PREDEFINED_RUNTIME_KEYS } = require('./validators');
643
+ const validIds = getAllProviders().map(p => p.id);
644
+ if (!validIds.includes(newProviderId)) throw new Error(`Unknown provider: ${newProviderId}`);
645
+ const index = this.config.apis.findIndex(a => a.id === apiId);
646
+ if (index === -1) throw new Error(`API not found: ${apiId}`);
647
+ const api = this.config.apis[index];
648
+ const newProvider = getProvider(newProviderId);
649
+ const warnings = [];
650
+ if (!newProvider.models.includes(api.model)) {
651
+ warnings.push({ code: 'MODEL_NOT_IN_PROVIDER', messageArgs: { model: api.model, providerName: newProvider.name } });
652
+ }
653
+ if (detectProvider(api.baseUrl) !== newProviderId) {
654
+ warnings.push({ code: 'BASE_URL_NOT_UPDATED', messageArgs: { baseUrl: api.baseUrl } });
655
+ }
656
+ if (api._runtimeEnvSources) {
657
+ for (const [key, source] of Object.entries(api._runtimeEnvSources)) {
658
+ if (source === 'auto' && PREDEFINED_RUNTIME_KEYS.includes(key)) api.runtimeEnvVars[key] = '';
659
+ }
660
+ }
661
+ let templateValues;
662
+ if (newProvider.modelEnvTemplate) {
663
+ templateValues = newProvider.modelEnvTemplate.getValues(api.model);
664
+ } else {
665
+ templateValues = {};
666
+ for (const k of PREDEFINED_MODEL_ENV_KEYS) templateValues[k] = api.model;
667
+ templateValues.smallFastModel = api.model;
668
+ }
669
+ if (api._autoModelEnvVars) {
670
+ if (!api.modelEnvVars) api.modelEnvVars = {};
671
+ for (const k of PREDEFINED_MODEL_ENV_KEYS) {
672
+ if (api.modelEnvVars[k] === api._autoModelEnvVars[k]) api.modelEnvVars[k] = templateValues[k] || '';
673
+ }
674
+ if (api.smallFastModel === api._autoModelEnvVars.smallFastModel) api.smallFastModel = templateValues.smallFastModel;
675
+ }
676
+ api._autoModelEnvVars = { ...templateValues };
677
+ if (detectProvider(api.baseUrl) !== newProviderId && warnings.some(w => w.code === 'MODEL_NOT_IN_PROVIDER')) {
678
+ warnings.push({ code: 'MIXED_PROVIDER_CONFIG', messageArgs: { providerId: newProviderId, baseUrl: api.baseUrl, model: api.model } });
679
+ }
680
+ api.provider = newProviderId;
681
+ this.saveConfig();
682
+ return { api, warnings };
683
+ }
684
+
340
685
  /**
341
686
  * Record a successful API launch
342
687
  * @returns {Object|null} The updated API object or null
@@ -575,14 +920,16 @@ class ApiManager {
575
920
  * Export configuration as plaintext JSON (already authenticated)
576
921
  */
577
922
  exportConfigAuthenticated() {
578
- // Create export data with plaintext API keys
579
923
  const exportData = {
924
+ configVersion: 2,
580
925
  version: this.config.version,
926
+ warning: 'This file contains plaintext API keys and custom environment variables. Handle with care.',
581
927
  exportedAt: new Date().toISOString(),
582
928
  apis: this.config.apis.map(api => {
583
929
  const decrypted = decrypt(api.authToken);
930
+ const { _autoFilledModel, ...safe } = api;
584
931
  return {
585
- ...api,
932
+ ...safe,
586
933
  authToken: decrypted.success ? decrypted.value : '***DECRYPTION_FAILED***'
587
934
  };
588
935
  }),
@@ -619,90 +966,203 @@ class ApiManager {
619
966
  processImportData(configData) {
620
967
  let imported = 0;
621
968
  let skipped = 0;
969
+ const warnings = [];
970
+ const skippedItems = [];
622
971
 
623
972
  if (!configData.apis || !Array.isArray(configData.apis)) {
624
973
  throw new Error('Invalid configuration format - no APIs found');
625
974
  }
626
975
 
976
+ const { PREDEFINED_MODEL_ENV_KEYS, PREDEFINED_RUNTIME_KEYS, validateRuntimeEnvValue, RESERVED_ENV_KEYS } = require('./validators');
977
+
627
978
  configData.apis.forEach(importApi => {
628
979
  if (this.config.apis.length >= 99) {
629
- screen.debug('Import skipped: maximum 99 APIs reached');
630
980
  skipped++;
981
+ skippedItems.push({ apiName: importApi.name || 'Unknown', reason: 'Maximum 99 APIs reached' });
631
982
  return;
632
983
  }
633
984
 
634
- // Validate the API configuration before importing
635
985
  try {
636
- // Validate Base URL
637
986
  const urlValidation = validateBaseUrl(importApi.baseUrl);
638
987
  if (!urlValidation.valid) {
639
- screen.debug(`Skipping API "${importApi.name || 'Unknown'}" - Invalid Base URL: ${urlValidation.error}`);
640
- skipped++;
641
- return;
988
+ skipped++; skippedItems.push({ apiName: importApi.name || 'Unknown', reason: urlValidation.error }); return;
642
989
  }
643
990
 
644
- // Validate Auth Token (skip validation for placeholder tokens)
645
991
  if (importApi.authToken !== '***REQUIRES_MANUAL_INPUT***') {
646
992
  const tokenValidation = validateAuthToken(importApi.authToken);
647
993
  if (!tokenValidation.valid) {
648
- screen.debug(`Skipping API "${importApi.name || 'Unknown'}" - Invalid Auth Token: ${tokenValidation.error}`);
649
- skipped++;
650
- return;
994
+ skipped++; skippedItems.push({ apiName: importApi.name || 'Unknown', reason: tokenValidation.error }); return;
651
995
  }
652
996
  }
653
997
 
654
- // Validate Model
655
998
  const modelValidation = validateModel(importApi.model);
656
999
  if (!modelValidation.valid) {
657
- screen.debug(`Skipping API "${importApi.name || 'Unknown'}" - Invalid Model: ${modelValidation.error}`);
658
- skipped++;
659
- return;
1000
+ skipped++; skippedItems.push({ apiName: importApi.name || 'Unknown', reason: modelValidation.error }); return;
660
1001
  }
661
1002
 
662
- // Check for duplicates using the same logic as addApi
663
1003
  const importToken = importApi.authToken === '***REQUIRES_MANUAL_INPUT***' ? '' : importApi.authToken;
664
1004
  const duplicate = this.checkDuplicate(importApi.baseUrl, importToken, importApi.model);
665
1005
 
666
1006
  if (duplicate.isDuplicate) {
667
1007
  skipped++;
668
- } else {
669
- // Encrypt the auth token if it's not already encrypted or masked
670
- let encryptedToken;
671
- if (importApi.authToken === '***REQUIRES_MANUAL_INPUT***') {
672
- encryptedToken = encrypt('').value; // Empty encrypted token
673
- } else {
674
- encryptedToken = encrypt(importApi.authToken).value;
1008
+ skippedItems.push({ apiName: importApi.name || 'Unknown', reason: 'Duplicate configuration' });
1009
+ return;
1010
+ }
1011
+
1012
+ // Clean modelEnvVars: whitelist only
1013
+ const cleanedModelEnvVars = {};
1014
+ if (importApi.modelEnvVars) {
1015
+ for (const k of PREDEFINED_MODEL_ENV_KEYS) {
1016
+ const v = importApi.modelEnvVars[k];
1017
+ cleanedModelEnvVars[k] = (typeof v === 'string') ? v : '';
1018
+ }
1019
+ for (const k of Object.keys(importApi.modelEnvVars)) {
1020
+ if (!PREDEFINED_MODEL_ENV_KEYS.includes(k)) {
1021
+ warnings.push({ code: 'UNKNOWN_MODEL_ENV_KEY', apiName: importApi.name || 'Unknown', key: k });
1022
+ }
675
1023
  }
1024
+ }
676
1025
 
677
- const newApi = {
678
- id: Date.now() + Math.random(),
679
- name: importApi.name || `Imported API ${this.config.apis.length + 1}`,
680
- baseUrl: urlValidation.value,
681
- authToken: encryptedToken,
682
- model: modelValidation.value,
683
- provider: importApi.provider || 'custom',
684
- createdAt: new Date().toISOString(),
685
- lastUsed: null,
686
- usageCount: 0
687
- };
688
-
689
- this.config.apis.push(newApi);
690
- imported++;
691
-
692
- // Set as active if this is the first API
693
- if (this.config.apis.length === 1) {
694
- this.config.activeIndex = 0;
1026
+ // Clean runtimeEnvVars: whitelist + validate
1027
+ const cleanedRuntimeEnvVars = {};
1028
+ const cleanedRuntimeEnvSources = {};
1029
+ if (importApi.runtimeEnvVars) {
1030
+ for (const k of PREDEFINED_RUNTIME_KEYS) {
1031
+ let v = importApi.runtimeEnvVars[k];
1032
+ if (typeof v !== 'string') v = '';
1033
+ if (v !== '' && !validateRuntimeEnvValue(k, v).valid) {
1034
+ warnings.push({ code: 'INVALID_RUNTIME_ENV_VALUE', apiName: importApi.name || 'Unknown', key: k });
1035
+ v = '';
1036
+ }
1037
+ cleanedRuntimeEnvVars[k] = v;
1038
+ const src = (importApi._runtimeEnvSources || {})[k];
1039
+ cleanedRuntimeEnvSources[k] = (src === 'manual' && v !== '') ? 'manual' : 'auto';
1040
+ }
1041
+ for (const k of Object.keys(importApi.runtimeEnvVars)) {
1042
+ if (!PREDEFINED_RUNTIME_KEYS.includes(k)) {
1043
+ warnings.push({ code: 'UNKNOWN_RUNTIME_ENV_KEY', apiName: importApi.name || 'Unknown', key: k });
1044
+ }
695
1045
  }
696
1046
  }
1047
+
1048
+ // Clean customEnvVars: skip reserved/predefined
1049
+ const cleanedCustomEnvVars = {};
1050
+ if (importApi.customEnvVars) {
1051
+ const allP = new Set([...RESERVED_ENV_KEYS, ...PREDEFINED_RUNTIME_KEYS, ...PREDEFINED_MODEL_ENV_KEYS]);
1052
+ for (const [k, v] of Object.entries(importApi.customEnvVars)) {
1053
+ if (allP.has(k)) {
1054
+ warnings.push({ code: 'CUSTOM_ENV_KEY_RESERVED', apiName: importApi.name || 'Unknown', key: k });
1055
+ continue;
1056
+ }
1057
+ if (typeof v === 'string') cleanedCustomEnvVars[k] = v;
1058
+ }
1059
+ }
1060
+
1061
+ const tokenToEncrypt =
1062
+ importApi.authToken === '***REQUIRES_MANUAL_INPUT***' ? '' : importApi.authToken;
1063
+ const encryptedResult = encrypt(tokenToEncrypt);
1064
+ if (!encryptedResult.success) {
1065
+ skipped++;
1066
+ skippedItems.push({
1067
+ apiName: importApi.name || 'Unknown',
1068
+ reason: `Failed to encrypt auth token: ${encryptedResult.error}`
1069
+ });
1070
+ return;
1071
+ }
1072
+ const encryptedToken = encryptedResult.value;
1073
+
1074
+ const newApi = {
1075
+ id: Date.now() + Math.random(),
1076
+ name: importApi.name || `Imported API ${this.config.apis.length + 1}`,
1077
+ baseUrl: urlValidation.value,
1078
+ authToken: encryptedToken,
1079
+ model: modelValidation.value,
1080
+ provider: importApi.provider || 'custom',
1081
+ smallFastModel: importApi.smallFastModel || importApi.model,
1082
+ createdAt: new Date().toISOString(),
1083
+ lastUsed: null,
1084
+ usageCount: 0,
1085
+ modelEnvVars: cleanedModelEnvVars,
1086
+ runtimeEnvVars: cleanedRuntimeEnvVars,
1087
+ _runtimeEnvSources: cleanedRuntimeEnvSources,
1088
+ customEnvVars: cleanedCustomEnvVars,
1089
+ _autoFilledModel: importApi._autoFilledModel,
1090
+ };
1091
+ if (importApi._autoModelEnvVars) {
1092
+ newApi._autoModelEnvVars = importApi._autoModelEnvVars;
1093
+ }
1094
+ if (importApi.successCount !== undefined) newApi.successCount = importApi.successCount;
1095
+ if (importApi.failCount !== undefined) newApi.failCount = importApi.failCount;
1096
+ if (importApi.lastError !== undefined) newApi.lastError = importApi.lastError;
1097
+
1098
+ this._normalizeApiFields(newApi);
1099
+ this.config.apis.push(newApi);
1100
+ imported++;
1101
+ if (this.config.apis.length === 1) this.config.activeIndex = 0;
697
1102
  } catch (error) {
698
- screen.debug(`Skipping API "${importApi.name || 'Unknown'}" - Validation error: ${error.message}`);
699
1103
  skipped++;
1104
+ skippedItems.push({ apiName: importApi.name || 'Unknown', reason: error.message });
700
1105
  }
701
1106
  });
702
1107
 
703
1108
  this.saveConfig();
704
- return { imported, skipped };
1109
+ return { imported, skipped, warnings, skippedItems };
1110
+ }
1111
+
1112
+ // --- Draft methods (no persistence, for pre-create config editing) ---
1113
+
1114
+ static buildApiDraft(provider, baseUrl, authToken, model, name) {
1115
+ const { getProvider } = require('./presets/providers');
1116
+ const { PREDEFINED_MODEL_ENV_KEYS, PREDEFINED_RUNTIME_KEYS } = require('./validators');
1117
+ const providerConfig = getProvider(provider);
1118
+ let templateValues;
1119
+ if (providerConfig && providerConfig.modelEnvTemplate) {
1120
+ templateValues = providerConfig.modelEnvTemplate.getValues(model);
1121
+ } else {
1122
+ templateValues = {};
1123
+ for (const k of PREDEFINED_MODEL_ENV_KEYS) templateValues[k] = model;
1124
+ templateValues.smallFastModel = model;
1125
+ }
1126
+ const modelEnvVars = {};
1127
+ for (const k of PREDEFINED_MODEL_ENV_KEYS) modelEnvVars[k] = templateValues[k] || '';
1128
+ const _autoModelEnvVars = { ...templateValues };
1129
+ const runtimeEnvVars = {};
1130
+ const _runtimeEnvSources = {};
1131
+ for (const k of PREDEFINED_RUNTIME_KEYS) { runtimeEnvVars[k] = ''; _runtimeEnvSources[k] = 'auto'; }
1132
+ return { provider, baseUrl, authToken, model, name,
1133
+ smallFastModel: templateValues.smallFastModel,
1134
+ modelEnvVars, _autoModelEnvVars,
1135
+ runtimeEnvVars, _runtimeEnvSources, customEnvVars: {} };
1136
+ }
1137
+
1138
+ static applyDraftEnvChange(draft, section, key, value) {
1139
+ const { PREDEFINED_MODEL_ENV_KEYS, PREDEFINED_RUNTIME_KEYS, validateRuntimeEnvValue, validateEnvKey } = require('./validators');
1140
+ if (section === 'model') {
1141
+ if (!PREDEFINED_MODEL_ENV_KEYS.includes(key)) throw new Error(`Unknown model env key: ${key}`);
1142
+ if (typeof value !== 'string') throw new Error('model env value must be string');
1143
+ draft.modelEnvVars[key] = (value === '') ? (draft._autoModelEnvVars[key] || '') : value;
1144
+ } else if (section === 'runtime') {
1145
+ if (!PREDEFINED_RUNTIME_KEYS.includes(key)) throw new Error(`Unknown runtime env key: ${key}`);
1146
+ const v = validateRuntimeEnvValue(key, value);
1147
+ if (!v.valid) throw new Error(`Invalid: ${v.error}`);
1148
+ draft.runtimeEnvVars[key] = value;
1149
+ draft._runtimeEnvSources[key] = (value === '') ? 'auto' : 'manual';
1150
+ } else if (section === 'custom') {
1151
+ const kv = validateEnvKey(key);
1152
+ if (!kv.valid) throw new Error(`Invalid custom key: ${kv.error}`);
1153
+ if (typeof value !== 'string') throw new Error('custom env value must be string');
1154
+ draft.customEnvVars[key] = value;
1155
+ }
1156
+ return draft;
1157
+ }
1158
+
1159
+ static deleteDraftCustomEnvVar(draft, key) {
1160
+ delete draft.customEnvVars[key];
1161
+ return draft;
705
1162
  }
706
1163
  }
707
1164
 
708
- module.exports = ApiManager;
1165
+ module.exports = ApiManager;
1166
+ module.exports.DuplicateApiError = DuplicateApiError;
1167
+ module.exports.MODEL_CONFIG_LABELS = MODEL_CONFIG_LABELS;
1168
+ module.exports.RUNTIME_CONFIG_LABELS = RUNTIME_CONFIG_LABELS;