@kikkimo/claude-launcher 3.0.0 → 3.1.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,143 @@ 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
+ // runtimeEnvVars — fill "" not provider values
172
+ if (!api.runtimeEnvVars) {
173
+ api.runtimeEnvVars = {};
174
+ }
175
+ for (const k of PREDEFINED_RUNTIME_KEYS) {
176
+ if (!(k in api.runtimeEnvVars) || typeof api.runtimeEnvVars[k] !== 'string') {
177
+ api.runtimeEnvVars[k] = '';
178
+ }
179
+ }
180
+
181
+ // _runtimeEnvSources — missing → "auto"
182
+ if (!api._runtimeEnvSources) {
183
+ api._runtimeEnvSources = {};
184
+ }
185
+ for (const k of PREDEFINED_RUNTIME_KEYS) {
186
+ if (!(k in api._runtimeEnvSources)) {
187
+ api._runtimeEnvSources[k] = 'auto';
188
+ }
189
+ if (api._runtimeEnvSources[k] !== 'auto' && api._runtimeEnvSources[k] !== 'manual') {
190
+ api._runtimeEnvSources[k] = 'auto';
191
+ }
192
+ }
193
+
194
+ // runtime/source conflict resolution
195
+ for (const k of PREDEFINED_RUNTIME_KEYS) {
196
+ if (api.runtimeEnvVars[k] !== '' && api._runtimeEnvSources[k] === 'auto') {
197
+ api.runtimeEnvVars[k] = '';
198
+ }
199
+ }
200
+
201
+ // customEnvVars
202
+ if (!api.customEnvVars) {
203
+ api.customEnvVars = {};
204
+ }
205
+
206
+ if (api._autoFilledModel) {
207
+ delete api._autoFilledModel;
208
+ }
209
+
210
+ return api;
211
+ }
212
+
53
213
  /**
54
214
  * Save configuration to encrypted file
55
215
  */
@@ -120,7 +280,7 @@ class ApiManager {
120
280
  // Check for duplicates
121
281
  const duplicate = this.checkDuplicate(baseUrl, authToken, model);
122
282
  if (duplicate.isDuplicate) {
123
- throw new Error(`${duplicate.type} already exists for API: ${duplicate.existing.name}`);
283
+ throw new DuplicateApiError(duplicate.existing);
124
284
  }
125
285
 
126
286
  // Encrypt the auth token before storing
@@ -129,6 +289,32 @@ class ApiManager {
129
289
  throw new Error(`Failed to encrypt auth token: ${encryptedToken.error}`);
130
290
  }
131
291
 
292
+ // Compute model env template values
293
+ const { getProvider } = require('./presets/providers');
294
+ const { PREDEFINED_MODEL_ENV_KEYS, PREDEFINED_RUNTIME_KEYS } = require('./validators');
295
+ const providerConfig = getProvider(provider);
296
+ let templateValues;
297
+ if (providerConfig && providerConfig.modelEnvTemplate) {
298
+ templateValues = providerConfig.modelEnvTemplate.getValues(modelValidation.value);
299
+ } else {
300
+ templateValues = {};
301
+ for (const k of PREDEFINED_MODEL_ENV_KEYS) templateValues[k] = modelValidation.value;
302
+ templateValues.smallFastModel = modelValidation.value;
303
+ }
304
+
305
+ const modelEnvVars = {};
306
+ for (const k of PREDEFINED_MODEL_ENV_KEYS) {
307
+ modelEnvVars[k] = templateValues[k] || '';
308
+ }
309
+ const _autoModelEnvVars = { ...templateValues };
310
+
311
+ const runtimeEnvVars = {};
312
+ const _runtimeEnvSources = {};
313
+ for (const k of PREDEFINED_RUNTIME_KEYS) {
314
+ runtimeEnvVars[k] = '';
315
+ _runtimeEnvSources[k] = 'auto';
316
+ }
317
+
132
318
  const newApi = {
133
319
  id: Date.now().toString(),
134
320
  name: name || `API-${this.config.apis.length + 1}`,
@@ -136,15 +322,18 @@ class ApiManager {
136
322
  baseUrl: urlValidation.value,
137
323
  authToken: encryptedToken.value,
138
324
  model: modelValidation.value,
139
- smallFastModel: modelValidation.value, // Same as model as requested
325
+ smallFastModel: templateValues.smallFastModel,
140
326
  createdAt: new Date().toISOString(),
141
- // Existing statistics fields
142
327
  lastUsed: null,
143
328
  usageCount: 0,
144
- // New statistics fields
145
329
  successCount: 0,
146
330
  failCount: 0,
147
- lastError: null
331
+ lastError: null,
332
+ modelEnvVars,
333
+ _autoModelEnvVars,
334
+ runtimeEnvVars,
335
+ _runtimeEnvSources,
336
+ customEnvVars: {},
148
337
  };
149
338
 
150
339
  this.config.apis.push(newApi);
@@ -287,7 +476,7 @@ class ApiManager {
287
476
  if (!validIds.includes(value)) {
288
477
  throw new Error(`Unknown provider: ${value}. Valid: ${validIds.join(', ')}`);
289
478
  }
290
- break;
479
+ return this.updateApiProvider(apiId, value).api;
291
480
  }
292
481
  case 'baseUrl': {
293
482
  const urlValidation = validateBaseUrl(value);
@@ -330,13 +519,143 @@ class ApiManager {
330
519
  // Apply update
331
520
  api[field] = value.trim();
332
521
  if (field === 'model') {
333
- api.smallFastModel = value.trim();
522
+ const { getProvider } = require('./presets/providers');
523
+ const { PREDEFINED_MODEL_ENV_KEYS } = require('./validators');
524
+ const providerConfig = getProvider(api.provider);
525
+ let templateVals;
526
+ if (providerConfig && providerConfig.modelEnvTemplate) {
527
+ templateVals = providerConfig.modelEnvTemplate.getValues(value.trim());
528
+ } else {
529
+ templateVals = {};
530
+ for (const k of PREDEFINED_MODEL_ENV_KEYS) templateVals[k] = value.trim();
531
+ templateVals.smallFastModel = value.trim();
532
+ }
533
+ if (api._autoModelEnvVars) {
534
+ if (!api.modelEnvVars) api.modelEnvVars = {};
535
+ for (const k of PREDEFINED_MODEL_ENV_KEYS) {
536
+ if (api.modelEnvVars[k] === api._autoModelEnvVars[k]) {
537
+ api.modelEnvVars[k] = templateVals[k] || '';
538
+ }
539
+ }
540
+ if (api.smallFastModel === api._autoModelEnvVars.smallFastModel) {
541
+ api.smallFastModel = templateVals.smallFastModel;
542
+ }
543
+ } else {
544
+ // No snapshot means all fields are auto — overwrite all
545
+ if (!api.modelEnvVars) api.modelEnvVars = {};
546
+ for (const k of PREDEFINED_MODEL_ENV_KEYS) {
547
+ api.modelEnvVars[k] = templateVals[k] || '';
548
+ }
549
+ api.smallFastModel = templateVals.smallFastModel;
550
+ }
551
+ api._autoModelEnvVars = { ...templateVals };
334
552
  }
335
553
 
336
554
  this.saveConfig();
337
555
  return api;
338
556
  }
339
557
 
558
+ updateModelEnvVar(apiId, key, value) {
559
+ const { PREDEFINED_MODEL_ENV_KEYS } = require('./validators');
560
+ if (!PREDEFINED_MODEL_ENV_KEYS.includes(key)) throw new Error(`"${key}" is not a predefined model env key`);
561
+ if (typeof value !== 'string') throw new Error('model env value must be a string');
562
+ const index = this.config.apis.findIndex(a => a.id === apiId);
563
+ if (index === -1) throw new Error(`API not found: ${apiId}`);
564
+ const api = this.config.apis[index];
565
+ const autoValue = api._autoModelEnvVars && typeof api._autoModelEnvVars[key] === 'string'
566
+ ? api._autoModelEnvVars[key]
567
+ : '';
568
+ api.modelEnvVars[key] = value === '' ? autoValue : value;
569
+ this.saveConfig();
570
+ return api;
571
+ }
572
+
573
+ updateRuntimeEnvVar(apiId, key, value, options = {}) {
574
+ const { PREDEFINED_RUNTIME_KEYS, validateRuntimeEnvValue } = require('./validators');
575
+ if (!PREDEFINED_RUNTIME_KEYS.includes(key)) throw new Error(`"${key}" is not a predefined runtime env key`);
576
+ const validation = validateRuntimeEnvValue(key, value);
577
+ if (!validation.valid) throw new Error(`Invalid value for ${key}: ${validation.error}`);
578
+ const index = this.config.apis.findIndex(a => a.id === apiId);
579
+ if (index === -1) throw new Error(`API not found: ${apiId}`);
580
+ if (options.source === 'auto') {
581
+ this.config.apis[index].runtimeEnvVars[key] = '';
582
+ this.config.apis[index]._runtimeEnvSources[key] = 'auto';
583
+ } else if (options.source === 'manual') {
584
+ this.config.apis[index].runtimeEnvVars[key] = value;
585
+ this.config.apis[index]._runtimeEnvSources[key] = 'manual';
586
+ } else {
587
+ this.config.apis[index].runtimeEnvVars[key] = value;
588
+ this.config.apis[index]._runtimeEnvSources[key] = (value === '') ? 'auto' : 'manual';
589
+ }
590
+ this.saveConfig();
591
+ return this.config.apis[index];
592
+ }
593
+
594
+ setCustomEnvVar(apiId, key, value) {
595
+ const { validateEnvKey } = require('./validators');
596
+ const kv = validateEnvKey(key);
597
+ if (!kv.valid) throw new Error(`Custom env key "${key}" is reserved or invalid`);
598
+ if (typeof value !== 'string') throw new Error('Custom env value must be a string');
599
+ const index = this.config.apis.findIndex(a => a.id === apiId);
600
+ if (index === -1) throw new Error(`API not found: ${apiId}`);
601
+ this.config.apis[index].customEnvVars[key] = value;
602
+ this.saveConfig();
603
+ return this.config.apis[index];
604
+ }
605
+
606
+ deleteCustomEnvVar(apiId, key) {
607
+ const index = this.config.apis.findIndex(a => a.id === apiId);
608
+ if (index === -1) throw new Error(`API not found: ${apiId}`);
609
+ delete this.config.apis[index].customEnvVars[key];
610
+ this.saveConfig();
611
+ return this.config.apis[index];
612
+ }
613
+
614
+ updateApiProvider(apiId, newProviderId) {
615
+ const { getProvider, getAllProviders, detectProvider } = require('./presets/providers');
616
+ const { PREDEFINED_MODEL_ENV_KEYS, PREDEFINED_RUNTIME_KEYS } = require('./validators');
617
+ const validIds = getAllProviders().map(p => p.id);
618
+ if (!validIds.includes(newProviderId)) throw new Error(`Unknown provider: ${newProviderId}`);
619
+ const index = this.config.apis.findIndex(a => a.id === apiId);
620
+ if (index === -1) throw new Error(`API not found: ${apiId}`);
621
+ const api = this.config.apis[index];
622
+ const newProvider = getProvider(newProviderId);
623
+ const warnings = [];
624
+ if (!newProvider.models.includes(api.model)) {
625
+ warnings.push({ code: 'MODEL_NOT_IN_PROVIDER', messageArgs: { model: api.model, providerName: newProvider.name } });
626
+ }
627
+ if (detectProvider(api.baseUrl) !== newProviderId) {
628
+ warnings.push({ code: 'BASE_URL_NOT_UPDATED', messageArgs: { baseUrl: api.baseUrl } });
629
+ }
630
+ if (api._runtimeEnvSources) {
631
+ for (const [key, source] of Object.entries(api._runtimeEnvSources)) {
632
+ if (source === 'auto' && PREDEFINED_RUNTIME_KEYS.includes(key)) api.runtimeEnvVars[key] = '';
633
+ }
634
+ }
635
+ let templateValues;
636
+ if (newProvider.modelEnvTemplate) {
637
+ templateValues = newProvider.modelEnvTemplate.getValues(api.model);
638
+ } else {
639
+ templateValues = {};
640
+ for (const k of PREDEFINED_MODEL_ENV_KEYS) templateValues[k] = api.model;
641
+ templateValues.smallFastModel = api.model;
642
+ }
643
+ if (api._autoModelEnvVars) {
644
+ if (!api.modelEnvVars) api.modelEnvVars = {};
645
+ for (const k of PREDEFINED_MODEL_ENV_KEYS) {
646
+ if (api.modelEnvVars[k] === api._autoModelEnvVars[k]) api.modelEnvVars[k] = templateValues[k] || '';
647
+ }
648
+ if (api.smallFastModel === api._autoModelEnvVars.smallFastModel) api.smallFastModel = templateValues.smallFastModel;
649
+ }
650
+ api._autoModelEnvVars = { ...templateValues };
651
+ if (detectProvider(api.baseUrl) !== newProviderId && warnings.some(w => w.code === 'MODEL_NOT_IN_PROVIDER')) {
652
+ warnings.push({ code: 'MIXED_PROVIDER_CONFIG', messageArgs: { providerId: newProviderId, baseUrl: api.baseUrl, model: api.model } });
653
+ }
654
+ api.provider = newProviderId;
655
+ this.saveConfig();
656
+ return { api, warnings };
657
+ }
658
+
340
659
  /**
341
660
  * Record a successful API launch
342
661
  * @returns {Object|null} The updated API object or null
@@ -575,14 +894,16 @@ class ApiManager {
575
894
  * Export configuration as plaintext JSON (already authenticated)
576
895
  */
577
896
  exportConfigAuthenticated() {
578
- // Create export data with plaintext API keys
579
897
  const exportData = {
898
+ configVersion: 2,
580
899
  version: this.config.version,
900
+ warning: 'This file contains plaintext API keys and custom environment variables. Handle with care.',
581
901
  exportedAt: new Date().toISOString(),
582
902
  apis: this.config.apis.map(api => {
583
903
  const decrypted = decrypt(api.authToken);
904
+ const { _autoFilledModel, ...safe } = api;
584
905
  return {
585
- ...api,
906
+ ...safe,
586
907
  authToken: decrypted.success ? decrypted.value : '***DECRYPTION_FAILED***'
587
908
  };
588
909
  }),
@@ -619,90 +940,203 @@ class ApiManager {
619
940
  processImportData(configData) {
620
941
  let imported = 0;
621
942
  let skipped = 0;
943
+ const warnings = [];
944
+ const skippedItems = [];
622
945
 
623
946
  if (!configData.apis || !Array.isArray(configData.apis)) {
624
947
  throw new Error('Invalid configuration format - no APIs found');
625
948
  }
626
949
 
950
+ const { PREDEFINED_MODEL_ENV_KEYS, PREDEFINED_RUNTIME_KEYS, validateRuntimeEnvValue, RESERVED_ENV_KEYS } = require('./validators');
951
+
627
952
  configData.apis.forEach(importApi => {
628
953
  if (this.config.apis.length >= 99) {
629
- screen.debug('Import skipped: maximum 99 APIs reached');
630
954
  skipped++;
955
+ skippedItems.push({ apiName: importApi.name || 'Unknown', reason: 'Maximum 99 APIs reached' });
631
956
  return;
632
957
  }
633
958
 
634
- // Validate the API configuration before importing
635
959
  try {
636
- // Validate Base URL
637
960
  const urlValidation = validateBaseUrl(importApi.baseUrl);
638
961
  if (!urlValidation.valid) {
639
- screen.debug(`Skipping API "${importApi.name || 'Unknown'}" - Invalid Base URL: ${urlValidation.error}`);
640
- skipped++;
641
- return;
962
+ skipped++; skippedItems.push({ apiName: importApi.name || 'Unknown', reason: urlValidation.error }); return;
642
963
  }
643
964
 
644
- // Validate Auth Token (skip validation for placeholder tokens)
645
965
  if (importApi.authToken !== '***REQUIRES_MANUAL_INPUT***') {
646
966
  const tokenValidation = validateAuthToken(importApi.authToken);
647
967
  if (!tokenValidation.valid) {
648
- screen.debug(`Skipping API "${importApi.name || 'Unknown'}" - Invalid Auth Token: ${tokenValidation.error}`);
649
- skipped++;
650
- return;
968
+ skipped++; skippedItems.push({ apiName: importApi.name || 'Unknown', reason: tokenValidation.error }); return;
651
969
  }
652
970
  }
653
971
 
654
- // Validate Model
655
972
  const modelValidation = validateModel(importApi.model);
656
973
  if (!modelValidation.valid) {
657
- screen.debug(`Skipping API "${importApi.name || 'Unknown'}" - Invalid Model: ${modelValidation.error}`);
658
- skipped++;
659
- return;
974
+ skipped++; skippedItems.push({ apiName: importApi.name || 'Unknown', reason: modelValidation.error }); return;
660
975
  }
661
976
 
662
- // Check for duplicates using the same logic as addApi
663
977
  const importToken = importApi.authToken === '***REQUIRES_MANUAL_INPUT***' ? '' : importApi.authToken;
664
978
  const duplicate = this.checkDuplicate(importApi.baseUrl, importToken, importApi.model);
665
979
 
666
980
  if (duplicate.isDuplicate) {
667
981
  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;
982
+ skippedItems.push({ apiName: importApi.name || 'Unknown', reason: 'Duplicate configuration' });
983
+ return;
984
+ }
985
+
986
+ // Clean modelEnvVars: whitelist only
987
+ const cleanedModelEnvVars = {};
988
+ if (importApi.modelEnvVars) {
989
+ for (const k of PREDEFINED_MODEL_ENV_KEYS) {
990
+ const v = importApi.modelEnvVars[k];
991
+ cleanedModelEnvVars[k] = (typeof v === 'string') ? v : '';
992
+ }
993
+ for (const k of Object.keys(importApi.modelEnvVars)) {
994
+ if (!PREDEFINED_MODEL_ENV_KEYS.includes(k)) {
995
+ warnings.push({ code: 'UNKNOWN_MODEL_ENV_KEY', apiName: importApi.name || 'Unknown', key: k });
996
+ }
997
+ }
998
+ }
999
+
1000
+ // Clean runtimeEnvVars: whitelist + validate
1001
+ const cleanedRuntimeEnvVars = {};
1002
+ const cleanedRuntimeEnvSources = {};
1003
+ if (importApi.runtimeEnvVars) {
1004
+ for (const k of PREDEFINED_RUNTIME_KEYS) {
1005
+ let v = importApi.runtimeEnvVars[k];
1006
+ if (typeof v !== 'string') v = '';
1007
+ if (v !== '' && !validateRuntimeEnvValue(k, v).valid) {
1008
+ warnings.push({ code: 'INVALID_RUNTIME_ENV_VALUE', apiName: importApi.name || 'Unknown', key: k });
1009
+ v = '';
1010
+ }
1011
+ cleanedRuntimeEnvVars[k] = v;
1012
+ const src = (importApi._runtimeEnvSources || {})[k];
1013
+ cleanedRuntimeEnvSources[k] = (src === 'manual' && v !== '') ? 'manual' : 'auto';
675
1014
  }
1015
+ for (const k of Object.keys(importApi.runtimeEnvVars)) {
1016
+ if (!PREDEFINED_RUNTIME_KEYS.includes(k)) {
1017
+ warnings.push({ code: 'UNKNOWN_RUNTIME_ENV_KEY', apiName: importApi.name || 'Unknown', key: k });
1018
+ }
1019
+ }
1020
+ }
676
1021
 
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;
1022
+ // Clean customEnvVars: skip reserved/predefined
1023
+ const cleanedCustomEnvVars = {};
1024
+ if (importApi.customEnvVars) {
1025
+ const allP = new Set([...RESERVED_ENV_KEYS, ...PREDEFINED_RUNTIME_KEYS, ...PREDEFINED_MODEL_ENV_KEYS]);
1026
+ for (const [k, v] of Object.entries(importApi.customEnvVars)) {
1027
+ if (allP.has(k)) {
1028
+ warnings.push({ code: 'CUSTOM_ENV_KEY_RESERVED', apiName: importApi.name || 'Unknown', key: k });
1029
+ continue;
1030
+ }
1031
+ if (typeof v === 'string') cleanedCustomEnvVars[k] = v;
695
1032
  }
696
1033
  }
1034
+
1035
+ const tokenToEncrypt =
1036
+ importApi.authToken === '***REQUIRES_MANUAL_INPUT***' ? '' : importApi.authToken;
1037
+ const encryptedResult = encrypt(tokenToEncrypt);
1038
+ if (!encryptedResult.success) {
1039
+ skipped++;
1040
+ skippedItems.push({
1041
+ apiName: importApi.name || 'Unknown',
1042
+ reason: `Failed to encrypt auth token: ${encryptedResult.error}`
1043
+ });
1044
+ return;
1045
+ }
1046
+ const encryptedToken = encryptedResult.value;
1047
+
1048
+ const newApi = {
1049
+ id: Date.now() + Math.random(),
1050
+ name: importApi.name || `Imported API ${this.config.apis.length + 1}`,
1051
+ baseUrl: urlValidation.value,
1052
+ authToken: encryptedToken,
1053
+ model: modelValidation.value,
1054
+ provider: importApi.provider || 'custom',
1055
+ smallFastModel: importApi.smallFastModel || importApi.model,
1056
+ createdAt: new Date().toISOString(),
1057
+ lastUsed: null,
1058
+ usageCount: 0,
1059
+ modelEnvVars: cleanedModelEnvVars,
1060
+ runtimeEnvVars: cleanedRuntimeEnvVars,
1061
+ _runtimeEnvSources: cleanedRuntimeEnvSources,
1062
+ customEnvVars: cleanedCustomEnvVars,
1063
+ _autoFilledModel: importApi._autoFilledModel,
1064
+ };
1065
+ if (importApi._autoModelEnvVars) {
1066
+ newApi._autoModelEnvVars = importApi._autoModelEnvVars;
1067
+ }
1068
+ if (importApi.successCount !== undefined) newApi.successCount = importApi.successCount;
1069
+ if (importApi.failCount !== undefined) newApi.failCount = importApi.failCount;
1070
+ if (importApi.lastError !== undefined) newApi.lastError = importApi.lastError;
1071
+
1072
+ this._normalizeApiFields(newApi);
1073
+ this.config.apis.push(newApi);
1074
+ imported++;
1075
+ if (this.config.apis.length === 1) this.config.activeIndex = 0;
697
1076
  } catch (error) {
698
- screen.debug(`Skipping API "${importApi.name || 'Unknown'}" - Validation error: ${error.message}`);
699
1077
  skipped++;
1078
+ skippedItems.push({ apiName: importApi.name || 'Unknown', reason: error.message });
700
1079
  }
701
1080
  });
702
1081
 
703
1082
  this.saveConfig();
704
- return { imported, skipped };
1083
+ return { imported, skipped, warnings, skippedItems };
1084
+ }
1085
+
1086
+ // --- Draft methods (no persistence, for pre-create config editing) ---
1087
+
1088
+ static buildApiDraft(provider, baseUrl, authToken, model, name) {
1089
+ const { getProvider } = require('./presets/providers');
1090
+ const { PREDEFINED_MODEL_ENV_KEYS, PREDEFINED_RUNTIME_KEYS } = require('./validators');
1091
+ const providerConfig = getProvider(provider);
1092
+ let templateValues;
1093
+ if (providerConfig && providerConfig.modelEnvTemplate) {
1094
+ templateValues = providerConfig.modelEnvTemplate.getValues(model);
1095
+ } else {
1096
+ templateValues = {};
1097
+ for (const k of PREDEFINED_MODEL_ENV_KEYS) templateValues[k] = model;
1098
+ templateValues.smallFastModel = model;
1099
+ }
1100
+ const modelEnvVars = {};
1101
+ for (const k of PREDEFINED_MODEL_ENV_KEYS) modelEnvVars[k] = templateValues[k] || '';
1102
+ const _autoModelEnvVars = { ...templateValues };
1103
+ const runtimeEnvVars = {};
1104
+ const _runtimeEnvSources = {};
1105
+ for (const k of PREDEFINED_RUNTIME_KEYS) { runtimeEnvVars[k] = ''; _runtimeEnvSources[k] = 'auto'; }
1106
+ return { provider, baseUrl, authToken, model, name,
1107
+ smallFastModel: templateValues.smallFastModel,
1108
+ modelEnvVars, _autoModelEnvVars,
1109
+ runtimeEnvVars, _runtimeEnvSources, customEnvVars: {} };
1110
+ }
1111
+
1112
+ static applyDraftEnvChange(draft, section, key, value) {
1113
+ const { PREDEFINED_MODEL_ENV_KEYS, PREDEFINED_RUNTIME_KEYS, validateRuntimeEnvValue, validateEnvKey } = require('./validators');
1114
+ if (section === 'model') {
1115
+ if (!PREDEFINED_MODEL_ENV_KEYS.includes(key)) throw new Error(`Unknown model env key: ${key}`);
1116
+ if (typeof value !== 'string') throw new Error('model env value must be string');
1117
+ draft.modelEnvVars[key] = (value === '') ? (draft._autoModelEnvVars[key] || '') : value;
1118
+ } else if (section === 'runtime') {
1119
+ if (!PREDEFINED_RUNTIME_KEYS.includes(key)) throw new Error(`Unknown runtime env key: ${key}`);
1120
+ const v = validateRuntimeEnvValue(key, value);
1121
+ if (!v.valid) throw new Error(`Invalid: ${v.error}`);
1122
+ draft.runtimeEnvVars[key] = value;
1123
+ draft._runtimeEnvSources[key] = (value === '') ? 'auto' : 'manual';
1124
+ } else if (section === 'custom') {
1125
+ const kv = validateEnvKey(key);
1126
+ if (!kv.valid) throw new Error(`Invalid custom key: ${kv.error}`);
1127
+ if (typeof value !== 'string') throw new Error('custom env value must be string');
1128
+ draft.customEnvVars[key] = value;
1129
+ }
1130
+ return draft;
1131
+ }
1132
+
1133
+ static deleteDraftCustomEnvVar(draft, key) {
1134
+ delete draft.customEnvVars[key];
1135
+ return draft;
705
1136
  }
706
1137
  }
707
1138
 
708
- module.exports = ApiManager;
1139
+ module.exports = ApiManager;
1140
+ module.exports.DuplicateApiError = DuplicateApiError;
1141
+ module.exports.MODEL_CONFIG_LABELS = MODEL_CONFIG_LABELS;
1142
+ module.exports.RUNTIME_CONFIG_LABELS = RUNTIME_CONFIG_LABELS;