@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.
@@ -1,210 +1,668 @@
1
- /**
2
- * API Editor Module — Edit API field-by-field with validation
3
- * Flow: Select API → Field Menu → Edit Single Field → Save
4
- */
5
-
6
- const colors = require('./colors');
7
- const Menu = require('./menu');
8
- const screen = require('./screen');
9
- const { showApiSelectionTable } = require('./interactive-table');
10
- const { simpleInput, waitForKey, selectProvider } = require('./prompts');
11
- const { getProvider, detectProvider, getSuggestedModels } = require('../presets/providers');
12
- const { truncateStringToWidth, getStringWidth, padStringToWidth } = require('../utils/string-width');
13
- const i18n = require('../i18n');
14
-
15
- const FIELD_VALUE_MAX_WIDTH = 30;
16
- const HINT_PROVIDER_NAME_MAX_WIDTH = 20;
17
-
18
- /**
19
- * Resolve provider id to display name
20
- * @param {string} providerId
21
- * @returns {string} Display name or raw id as fallback
22
- */
23
- function resolveProviderName(providerId) {
24
- const provider = getProvider(providerId);
25
- return provider ? provider.name : providerId;
26
- }
27
-
28
- /**
29
- * Build field menu options with current values
30
- */
31
- function buildFieldMenuOptions(api) {
32
- const fields = [
33
- { key: 'name', label: i18n.tSync('api.edit.field_name'), value: api.name || '' },
34
- { key: 'provider', label: i18n.tSync('api.edit.field_provider'), value: resolveProviderName(api.provider) },
35
- { key: 'baseUrl', label: i18n.tSync('api.edit.field_base_url'), value: api.baseUrl || '' },
36
- { key: 'model', label: i18n.tSync('api.edit.field_model'), value: api.model || '' }
37
- ];
38
-
39
- const maxLabelWidth = Math.max(...fields.map(f => getStringWidth(f.label)));
40
- const options = fields.map(f => {
41
- const paddedLabel = padStringToWidth(f.label, maxLabelWidth);
42
- const truncatedValue = truncateStringToWidth(f.value, FIELD_VALUE_MAX_WIDTH);
43
- return `${paddedLabel} ${truncatedValue}`;
44
- });
45
-
46
- options.push(i18n.tSync('api.edit.back'));
47
- return options;
48
- }
49
-
50
- /**
51
- * Build hintCallback for field menu — shows mismatch warning or blank
52
- */
53
- function buildFieldMenuHintCallback(api) {
54
- return function (_selectedIndex) {
55
- const detected = detectProvider(api.baseUrl);
56
- if (detected !== api.provider) {
57
- const currentName = truncateStringToWidth(resolveProviderName(api.provider), HINT_PROVIDER_NAME_MAX_WIDTH);
58
- const detectedName = truncateStringToWidth(resolveProviderName(detected), HINT_PROVIDER_NAME_MAX_WIDTH);
59
- const line1 = i18n.tSync('api.edit.provider_url_mismatch');
60
- const line2 = i18n.tSync('api.edit.provider_url_mismatch_detail', currentName, detectedName);
61
- return `${line1}\n${line2}\n\n`;
62
- }
63
- return null;
64
- };
65
- }
66
-
67
- /**
68
- * Edit a single text field (name, baseUrl, model)
69
- */
70
- async function editTextField(apiManager, api, fieldKey, fieldLabel) {
71
- const currentValue = api[fieldKey] || '';
72
-
73
- const headerLines = [
74
- '',
75
- colors.cyan + i18n.tSync('api.edit.current_value', currentValue) + colors.reset,
76
- '',
77
- ];
78
-
79
- // Show suggested models for model field
80
- if (fieldKey === 'model') {
81
- const suggested = getSuggestedModels(api.provider);
82
- if (suggested.length > 0) {
83
- headerLines.push(colors.gray + ' Suggested models: ' + suggested.join(', ') + colors.reset);
84
- headerLines.push('');
85
- }
86
- }
87
-
88
- screen.render(headerLines);
89
-
90
- const input = await simpleInput(colors.green + i18n.tSync('api.edit.new_value') + colors.reset);
91
-
92
- // Cancel check
93
- if (input.toLowerCase() === 'exit' || input.toLowerCase() === 'quit') {
94
- screen.write(colors.yellow + i18n.tSync('api.edit.cancelled') + colors.reset + '\n');
95
- await waitForKey(i18n.tSync('ui.general.press_any_key_continue'));
96
- return null;
97
- }
98
-
99
- // Attempt save
100
- try {
101
- const updated = apiManager.updateApiField(api.id, fieldKey, input);
102
-
103
- // Success message
104
- screen.write(colors.green + i18n.tSync('api.edit.success', fieldLabel) + colors.reset + '\n');
105
-
106
- // baseUrl mismatch warning on same screen
107
- if (fieldKey === 'baseUrl') {
108
- const detected = detectProvider(input);
109
- if (detected !== updated.provider) {
110
- const detectedName = resolveProviderName(detected);
111
- const currentName = resolveProviderName(updated.provider);
112
- screen.write(colors.yellow + i18n.tSync('api.edit.url_provider_hint', detectedName, currentName) + colors.reset + '\n');
113
- }
114
- }
115
-
116
- await waitForKey(i18n.tSync('ui.general.press_any_key_continue'));
117
- return updated;
118
- } catch (error) {
119
- screen.write(colors.red + '❌ ' + error.message + colors.reset + '\n');
120
- // Re-prompt stay in field edit
121
- return await editTextField(apiManager, api, fieldKey, fieldLabel);
122
- }
123
- }
124
-
125
- /**
126
- * Edit provider field via selectProvider() helper
127
- */
128
- async function editProviderField(apiManager, api) {
129
- screen.render([
130
- '',
131
- colors.cyan + i18n.tSync('api.edit.current_value', resolveProviderName(api.provider)) + colors.reset,
132
- '',
133
- ]);
134
-
135
- const result = await selectProvider({ title: null, showNote: false });
136
-
137
- if (!result) {
138
- screen.write(colors.yellow + i18n.tSync('api.edit.cancelled') + colors.reset + '\n');
139
- await waitForKey(i18n.tSync('ui.general.press_any_key_continue'));
140
- return null;
141
- }
142
-
143
- try {
144
- const updated = apiManager.updateApiField(api.id, 'provider', result.id);
145
- screen.write(colors.green + i18n.tSync('api.edit.success', i18n.tSync('api.edit.field_provider')) + colors.reset + '\n');
146
- await waitForKey(i18n.tSync('ui.general.press_any_key_continue'));
147
- return updated;
148
- } catch (error) {
149
- screen.write(colors.red + '❌ ' + error.message + colors.reset + '\n');
150
- await waitForKey(i18n.tSync('ui.general.press_any_key_continue'));
151
- return null;
152
- }
153
- }
154
-
155
- /**
156
- * Main edit API flow
157
- * @param {Object} apiManager - ApiManager instance
158
- */
159
- async function editApi(apiManager) {
160
- // Step 1: Select API
161
- const apis = apiManager.getApis();
162
- const selectedApi = await showApiSelectionTable(
163
- apis,
164
- i18n.tSync('api.edit.select_api'),
165
- 'edit'
166
- );
167
-
168
- if (!selectedApi) return;
169
-
170
- // Step 2: Field menu loop
171
- let currentApi = selectedApi;
172
- const fieldMenu = new Menu();
173
-
174
- while (true) {
175
- fieldMenu.setOptions(buildFieldMenuOptions(currentApi));
176
- const hintCallback = buildFieldMenuHintCallback(currentApi);
177
- const choice = await fieldMenu.navigate(null, hintCallback);
178
-
179
- // Esc or Back
180
- if (choice === -1 || choice === 4) {
181
- return;
182
- }
183
-
184
- // Step 3: Edit selected field
185
- const fieldKeys = ['name', 'provider', 'baseUrl', 'model'];
186
- const fieldLabels = [
187
- i18n.tSync('api.edit.field_name'),
188
- i18n.tSync('api.edit.field_provider'),
189
- i18n.tSync('api.edit.field_base_url'),
190
- i18n.tSync('api.edit.field_model')
191
- ];
192
-
193
- let updated = null;
194
- if (choice === 1) {
195
- // Provider uses selectProvider() not simpleInput()
196
- updated = await editProviderField(apiManager, currentApi);
197
- } else if (choice >= 0 && choice < 4) {
198
- updated = await editTextField(apiManager, currentApi, fieldKeys[choice], fieldLabels[choice]);
199
- }
200
-
201
- if (updated) {
202
- currentApi = updated;
203
- }
204
- }
205
- }
206
-
207
- module.exports = {
208
- editApi,
209
- resolveProviderName
210
- };
1
+ /**
2
+ * API Editor Module — Edit API field-by-field with validation
3
+ * Flow: Select API → Field Menu → Edit Single Field → Save
4
+ */
5
+
6
+ const colors = require('./colors');
7
+ const Menu = require('./menu');
8
+ const screen = require('./screen');
9
+ const { showApiSelectionTable } = require('./interactive-table');
10
+ const { simpleInput, waitForKey, selectProvider } = require('./prompts');
11
+ const { getProvider, detectProvider, getSuggestedModels } = require('../presets/providers');
12
+ const { truncateStringToWidth, getStringWidth, padStringToWidth } = require('../utils/string-width');
13
+ const i18n = require('../i18n');
14
+ const { i18nLabel } = require('./i18n-labels');
15
+
16
+ const FIELD_VALUE_MAX_WIDTH = 30;
17
+ const HINT_PROVIDER_NAME_MAX_WIDTH = 20;
18
+
19
+ /**
20
+ * Resolve provider id to display name
21
+ * @param {string} providerId
22
+ * @returns {string} Display name or raw id as fallback
23
+ */
24
+ function resolveProviderName(providerId) {
25
+ const provider = getProvider(providerId);
26
+ return provider ? provider.name : providerId;
27
+ }
28
+
29
+ /**
30
+ * Build field menu options with current values
31
+ */
32
+ function buildFieldMenuOptions(api) {
33
+ const { PREDEFINED_MODEL_ENV_KEYS, PREDEFINED_RUNTIME_KEYS } = require('../validators');
34
+ const modelCustomCount = PREDEFINED_MODEL_ENV_KEYS.filter((key) => {
35
+ const currentVal = (api.modelEnvVars || {})[key] || '';
36
+ const autoVal = api._autoModelEnvVars ? api._autoModelEnvVars[key] : undefined;
37
+ return autoVal !== undefined ? currentVal !== autoVal : currentVal !== '';
38
+ }).length;
39
+ const runtimeManualCount = PREDEFINED_RUNTIME_KEYS.filter((key) => {
40
+ return ((api._runtimeEnvSources || {})[key] || 'auto') === 'manual';
41
+ }).length;
42
+ const customCount = Object.keys(api.customEnvVars || {}).length;
43
+ const envSummary = i18n.tSync('status.overridden') + ' ' + modelCustomCount + '/' + PREDEFINED_MODEL_ENV_KEYS.length
44
+ + ' | ' + i18n.tSync('status.overridden') + ' ' + runtimeManualCount + '/' + PREDEFINED_RUNTIME_KEYS.length
45
+ + ' | ' + i18n.tSync('summary.x_items', String(customCount));
46
+ const fields = [
47
+ { key: 'name', label: i18n.tSync('api.edit.field_name'), value: api.name || '' },
48
+ { key: 'provider', label: i18n.tSync('api.edit.field_provider'), value: resolveProviderName(api.provider) },
49
+ { key: 'baseUrl', label: i18n.tSync('api.edit.field_base_url'), value: api.baseUrl || '' },
50
+ { key: 'model', label: i18n.tSync('api.edit.field_model'), value: api.model || '' },
51
+ { key: 'model_runtime', label: i18n.tSync('page.model_runtime_config'), value: envSummary },
52
+ ];
53
+
54
+ const maxLabelWidth = Math.max(...fields.map(f => getStringWidth(f.label)));
55
+ const options = fields.map(f => {
56
+ const paddedLabel = padStringToWidth(f.label, maxLabelWidth);
57
+ const truncatedValue = truncateStringToWidth(f.value, FIELD_VALUE_MAX_WIDTH);
58
+ return `${paddedLabel} ${truncatedValue}`;
59
+ });
60
+
61
+ options.push(i18n.tSync('api.edit.back'));
62
+ return options;
63
+ }
64
+
65
+ function getApiById(apiManager, apiId) {
66
+ return apiManager.config.apis.find((api) => api.id === apiId) || null;
67
+ }
68
+
69
+ async function editApiEnvVarsById(apiManager, { apiId, initialSection } = {}) {
70
+ let api = getApiById(apiManager, apiId);
71
+ if (!api) {
72
+ screen.write(colors.red + i18n.tSync('errors.api.not_found', apiId) + colors.reset + '\n');
73
+ return;
74
+ }
75
+
76
+ const menu = new Menu();
77
+ const { PREDEFINED_MODEL_ENV_KEYS, PREDEFINED_RUNTIME_KEYS } = require('../validators');
78
+
79
+ while (true) {
80
+ if (initialSection === 'modelEnvVars') { await editModelEnvVarsMenu(apiManager, api); initialSection = null; api = getApiById(apiManager, apiId); continue; }
81
+ if (initialSection === 'runtimeEnvVars') { await editRuntimeEnvVarsMenu(apiManager, api); initialSection = null; api = getApiById(apiManager, apiId); continue; }
82
+ if (initialSection === 'customEnvVars') { await editCustomEnvVarsMenu(apiManager, api); initialSection = null; api = getApiById(apiManager, apiId); continue; }
83
+
84
+ const providerConfig = getProvider(api.provider);
85
+ const providerDefaults = providerConfig ? providerConfig.envVars || {} : {};
86
+
87
+ const modelOverridden = PREDEFINED_MODEL_ENV_KEYS.filter(k => {
88
+ const v = (api.modelEnvVars || {})[k] || '';
89
+ const a = api._autoModelEnvVars ? api._autoModelEnvVars[k] : undefined;
90
+ return a !== undefined && v !== a;
91
+ }).length;
92
+ const runtimeOverridden = PREDEFINED_RUNTIME_KEYS.filter(k => {
93
+ const val = (api.runtimeEnvVars || {})[k] || '';
94
+ const providerVal = (providerDefaults || {})[k];
95
+ if (val === '') return false;
96
+ const effective = val;
97
+ const recommended = providerVal !== undefined ? providerVal : '';
98
+ return effective !== recommended;
99
+ }).length;
100
+ const customCount = Object.keys(api.customEnvVars || {}).length;
101
+
102
+ const opts = [
103
+ i18n.tSync('page.model_config') + ' ' + modelOverridden + '/' + PREDEFINED_MODEL_ENV_KEYS.length + ' ' + i18n.tSync('status.overridden'),
104
+ i18n.tSync('page.runtime_config') + ' ' + runtimeOverridden + '/' + PREDEFINED_RUNTIME_KEYS.length + ' ' + i18n.tSync('status.overridden'),
105
+ i18n.tSync('page.custom_vars') + ' ' + i18n.tSync('summary.x_items', String(customCount)),
106
+ i18n.tSync('api.edit.back'),
107
+ ];
108
+ menu.setOptions(opts);
109
+ const hintFn = (idx) => {
110
+ if (idx === 0) return i18n.tSync('page.model_config') + ': ' + i18n.tSync('hints.model.desc');
111
+ if (idx === 1) return i18n.tSync('page.runtime_config') + ': ' + i18n.tSync('hints.runtime.desc');
112
+ if (idx === 2) return i18n.tSync('page.custom_vars') + ': ' + i18n.tSync('hints.custom.desc');
113
+ return null;
114
+ };
115
+ const choice = await menu.navigate(null, hintFn, 'navigation.enter_to_select');
116
+
117
+ if (choice === -1 || choice === 3) return;
118
+ if (choice === 0) await editModelEnvVarsMenu(apiManager, api);
119
+ else if (choice === 1) await editRuntimeEnvVarsMenu(apiManager, api);
120
+ else if (choice === 2) await editCustomEnvVarsMenu(apiManager, api);
121
+ api = getApiById(apiManager, apiId);
122
+ if (!api) return;
123
+ }
124
+ }
125
+
126
+ /**
127
+ * Build hintCallback for field menu — shows mismatch warning or blank
128
+ */
129
+ function buildFieldMenuHintCallback(api) {
130
+ return function (_selectedIndex) {
131
+ const detected = detectProvider(api.baseUrl);
132
+ if (detected !== api.provider) {
133
+ const currentName = truncateStringToWidth(resolveProviderName(api.provider), HINT_PROVIDER_NAME_MAX_WIDTH);
134
+ const detectedName = truncateStringToWidth(resolveProviderName(detected), HINT_PROVIDER_NAME_MAX_WIDTH);
135
+ const line1 = i18n.tSync('api.edit.provider_url_mismatch');
136
+ const line2 = i18n.tSync('api.edit.provider_url_mismatch_detail', currentName, detectedName);
137
+ return `${line1}\n${line2}\n\n`;
138
+ }
139
+ return null;
140
+ };
141
+ }
142
+
143
+ /**
144
+ * Edit a single text field (name, baseUrl, model)
145
+ */
146
+ async function editTextField(apiManager, api, fieldKey, fieldLabel) {
147
+ const currentValue = api[fieldKey] || '';
148
+
149
+ const headerLines = [
150
+ '',
151
+ colors.cyan + i18n.tSync('api.edit.current_value', currentValue) + colors.reset,
152
+ '',
153
+ ];
154
+
155
+ // Show suggested models for model field
156
+ if (fieldKey === 'model') {
157
+ const suggested = getSuggestedModels(api.provider);
158
+ if (suggested.length > 0) {
159
+ headerLines.push(colors.gray + ' Suggested models: ' + suggested.join(', ') + colors.reset);
160
+ headerLines.push('');
161
+ }
162
+ }
163
+
164
+ screen.render(headerLines);
165
+
166
+ const input = await simpleInput(colors.green + i18n.tSync('api.edit.new_value') + colors.reset);
167
+
168
+ // Cancel check
169
+ if (input.toLowerCase() === 'exit' || input.toLowerCase() === 'quit') {
170
+ screen.write(colors.yellow + i18n.tSync('api.edit.cancelled') + colors.reset + '\n');
171
+ await waitForKey(i18n.tSync('ui.general.press_any_key_continue'));
172
+ return null;
173
+ }
174
+
175
+ // Attempt save
176
+ try {
177
+ const updated = apiManager.updateApiField(api.id, fieldKey, input);
178
+
179
+ // Success message
180
+ screen.write(colors.green + i18n.tSync('api.edit.success', fieldLabel) + colors.reset + '\n');
181
+
182
+ // baseUrl mismatch warning on same screen
183
+ if (fieldKey === 'baseUrl') {
184
+ const detected = detectProvider(input);
185
+ if (detected !== updated.provider) {
186
+ const detectedName = resolveProviderName(detected);
187
+ const currentName = resolveProviderName(updated.provider);
188
+ screen.write(colors.yellow + i18n.tSync('api.edit.url_provider_hint', detectedName, currentName) + colors.reset + '\n');
189
+ }
190
+ }
191
+
192
+ await waitForKey(i18n.tSync('ui.general.press_any_key_continue'));
193
+ return updated;
194
+ } catch (error) {
195
+ screen.write(colors.red + '❌ ' + error.message + colors.reset + '\n');
196
+ // Re-prompt stay in field edit
197
+ return await editTextField(apiManager, api, fieldKey, fieldLabel);
198
+ }
199
+ }
200
+
201
+ /**
202
+ * Edit provider field via selectProvider() helper
203
+ */
204
+ async function editProviderField(apiManager, api) {
205
+ screen.render([
206
+ '',
207
+ colors.cyan + i18n.tSync('api.edit.current_value', resolveProviderName(api.provider)) + colors.reset,
208
+ '',
209
+ ]);
210
+
211
+ const result = await selectProvider({ title: null, showNote: false });
212
+
213
+ if (!result) {
214
+ screen.write(colors.yellow + i18n.tSync('api.edit.cancelled') + colors.reset + '\n');
215
+ await waitForKey(i18n.tSync('ui.general.press_any_key_continue'));
216
+ return null;
217
+ }
218
+
219
+ try {
220
+ const { api: updated, warnings } = apiManager.updateApiProvider(api.id, result.id);
221
+ screen.write(colors.green + i18n.tSync('api.edit.success', i18n.tSync('api.edit.field_provider')) + colors.reset + '\n');
222
+ for (const w of warnings) {
223
+ if (w.code === 'MODEL_NOT_IN_PROVIDER') {
224
+ screen.write(colors.yellow + i18n.tSync('api.edit.warn_model_not_in_provider', w.messageArgs.model, w.messageArgs.providerName) + colors.reset + '\n');
225
+ } else if (w.code === 'BASE_URL_NOT_UPDATED') {
226
+ screen.write(colors.yellow + i18n.tSync('api.edit.warn_base_url_not_updated', w.messageArgs.baseUrl) + colors.reset + '\n');
227
+ } else if (w.code === 'MIXED_PROVIDER_CONFIG') {
228
+ screen.write(colors.yellow + i18n.tSync('api.edit.warn_mixed_provider') + colors.reset + '\n');
229
+ }
230
+ }
231
+ await waitForKey(i18n.tSync('ui.general.press_any_key_continue'));
232
+ return updated;
233
+ } catch (error) {
234
+ screen.write(colors.red + '❌ ' + error.message + colors.reset + '\n');
235
+ await waitForKey(i18n.tSync('ui.general.press_any_key_continue'));
236
+ return null;
237
+ }
238
+ }
239
+
240
+ /**
241
+ * Shared field-menu loop for editing an API.
242
+ * Handles choices 0-5 and refreshes currentApi after sub-page edits.
243
+ */
244
+ async function _runFieldEditLoop(apiManager, currentApi) {
245
+ const fieldMenu = new Menu();
246
+
247
+ while (true) {
248
+ fieldMenu.setOptions(buildFieldMenuOptions(currentApi));
249
+ const hintCallback = buildFieldMenuHintCallback(currentApi);
250
+ const choice = await fieldMenu.navigate(null, hintCallback);
251
+
252
+ if (choice === -1 || choice === 5) return;
253
+
254
+ const fieldKeys = ['name', 'provider', 'baseUrl', 'model'];
255
+ const fieldLabels = [
256
+ i18n.tSync('api.edit.field_name'), i18n.tSync('api.edit.field_provider'),
257
+ i18n.tSync('api.edit.field_base_url'), i18n.tSync('api.edit.field_model'),
258
+ ];
259
+
260
+ let updated = null;
261
+ if (choice === 1) {
262
+ updated = await editProviderField(apiManager, currentApi);
263
+ } else if (choice >= 0 && choice < 4) {
264
+ updated = await editTextField(apiManager, currentApi, fieldKeys[choice], fieldLabels[choice]);
265
+ } else if (choice === 4) {
266
+ await editApiEnvVarsById(apiManager, { apiId: currentApi.id });
267
+ currentApi = getApiById(apiManager, currentApi.id);
268
+ }
269
+
270
+ if (updated) currentApi = updated;
271
+ }
272
+ }
273
+
274
+ /**
275
+ * Programmatic entry: edit a specific API by ID with optional sub-menu jump.
276
+ */
277
+ async function editApiById(apiManager, { apiId, initialSection }) {
278
+ const apis = apiManager.getApis();
279
+ const index = apis.findIndex(a => a.id === apiId);
280
+ if (index === -1) {
281
+ screen.write(colors.red + i18n.tSync('errors.api.not_found', apiId) + colors.reset + '\n');
282
+ return;
283
+ }
284
+ let currentApi = apis[index];
285
+
286
+ while (initialSection) {
287
+ if (initialSection === 'runtimeEnvVars') {
288
+ await editRuntimeEnvVarsMenu(apiManager, currentApi);
289
+ initialSection = null;
290
+ currentApi = apiManager.config.apis.find(a => a.id === apiId);
291
+ continue;
292
+ } else if (initialSection === 'modelEnvVars') {
293
+ await editModelEnvVarsMenu(apiManager, currentApi);
294
+ initialSection = null;
295
+ currentApi = getApiById(apiManager, apiId);
296
+ continue;
297
+ } else if (initialSection === 'customEnvVars') {
298
+ await editCustomEnvVarsMenu(apiManager, currentApi);
299
+ initialSection = null;
300
+ currentApi = getApiById(apiManager, apiId);
301
+ continue;
302
+ }
303
+ break;
304
+ }
305
+
306
+ return _runFieldEditLoop(apiManager, currentApi);
307
+ }
308
+
309
+ /**
310
+ * Main edit API flow
311
+ * @param {Object} apiManager - ApiManager instance
312
+ */
313
+ async function editApi(apiManager) {
314
+ const apis = apiManager.getApis();
315
+ const selectedApi = await showApiSelectionTable(
316
+ apis,
317
+ i18n.tSync('api.edit.select_api'),
318
+ 'edit'
319
+ );
320
+
321
+ if (!selectedApi) return;
322
+
323
+ return _runFieldEditLoop(apiManager, selectedApi);
324
+ }
325
+
326
+ // === Sub-menus for env config editing ===
327
+
328
+ async function editModelEnvVarsMenu(apiManager, api) {
329
+ const menu = new Menu();
330
+ const { PREDEFINED_MODEL_ENV_KEYS } = require('../validators');
331
+ const { MODEL_CONFIG_LABELS } = require('../api-manager');
332
+ const getLabel = (key) => i18nLabel("model", key, MODEL_CONFIG_LABELS);
333
+
334
+ while (true) {
335
+ const rows = PREDEFINED_MODEL_ENV_KEYS.map((key) => {
336
+ const currentVal = (api.modelEnvVars || {})[key] || '';
337
+ const autoVal = api._autoModelEnvVars ? (api._autoModelEnvVars[key] || '') : '';
338
+ const effectiveVal = currentVal || autoVal;
339
+ const displayVal = effectiveVal || i18n.tSync('status.not_set');
340
+ const isOverridden = autoVal !== '' && currentVal !== autoVal;
341
+ const mark = isOverridden ? ' ' + colors.cyan + i18n.tSync('status.overridden') + colors.reset : '';
342
+ return { label: ' ' + getLabel(key), displayVal, mark };
343
+ });
344
+ const maxLabelW = Math.max(...rows.map(r => getStringWidth(r.label)));
345
+ const maxValueW = Math.max(...rows.map(r => getStringWidth(r.displayVal)));
346
+ const options = rows.map(r => {
347
+ return colors.reset + padStringToWidth(r.label, maxLabelW + 2) + ' ' + padStringToWidth(r.displayVal, maxValueW) + r.mark;
348
+ });
349
+ options.push(i18n.tSync('api.edit.back'));
350
+ menu.setOptions(options);
351
+ const hintFn = (idx) => {
352
+ if (idx >= PREDEFINED_MODEL_ENV_KEYS.length) return null;
353
+ const key = PREDEFINED_MODEL_ENV_KEYS[idx];
354
+ const detailKeys = {
355
+ ANTHROPIC_DEFAULT_SONNET_MODEL: 'hints.model.sonnet_detail',
356
+ ANTHROPIC_DEFAULT_OPUS_MODEL: 'hints.model.opus_detail',
357
+ ANTHROPIC_DEFAULT_HAIKU_MODEL: 'hints.model.haiku_detail',
358
+ CLAUDE_CODE_SUBAGENT_MODEL: 'hints.model.subagent_detail',
359
+ ANTHROPIC_CUSTOM_MODEL_OPTION: 'hints.model.custom_option_detail',
360
+ ANTHROPIC_CUSTOM_MODEL_OPTION_NAME: 'hints.model.custom_name_detail',
361
+ };
362
+ return detailKeys[key] ? i18n.tSync(detailKeys[key]) : null;
363
+ };
364
+ const choice = await menu.navigate(null, hintFn, 'navigation.enter_to_edit');
365
+
366
+ if (choice === -1 || choice === PREDEFINED_MODEL_ENV_KEYS.length) return;
367
+
368
+ const key = PREDEFINED_MODEL_ENV_KEYS[choice];
369
+ await editModelEnvSingleField(apiManager, api, key);
370
+ api = getApiById(apiManager, api.id);
371
+ }
372
+ }
373
+
374
+ async function editModelEnvSingleField(apiManager, api, key) {
375
+ const { MODEL_CONFIG_LABELS } = require('../api-manager');
376
+ const label = i18nLabel("model", key, MODEL_CONFIG_LABELS);
377
+ const currentVal = api.modelEnvVars[key] || '';
378
+ const autoVal = (api._autoModelEnvVars || {})[key] || '';
379
+ const menu = new Menu();
380
+ const shortKeyMap = {
381
+ ANTHROPIC_DEFAULT_SONNET_MODEL: 'sonnet', ANTHROPIC_DEFAULT_OPUS_MODEL: 'opus',
382
+ ANTHROPIC_DEFAULT_HAIKU_MODEL: 'haiku', CLAUDE_CODE_SUBAGENT_MODEL: 'subagent',
383
+ ANTHROPIC_CUSTOM_MODEL_OPTION: 'custom_option', ANTHROPIC_CUSTOM_MODEL_OPTION_NAME: 'custom_name',
384
+ };
385
+ const descKey = 'hints.model.' + (shortKeyMap[key] || key) + '_detail';
386
+
387
+ while (true) {
388
+ screen.render([
389
+ '', colors.cyan + label + colors.reset, '',
390
+ i18n.tSync('status.current_value') + ': ' + (currentVal || i18n.tSync('status.not_set')),
391
+ i18n.tSync('status.recommended_value') + ': ' + (autoVal || i18n.tSync('status.not_set')),
392
+ '', colors.gray + i18n.tSync(descKey) + colors.reset,
393
+ '', i18n.tSync('action.please_choose'), '',
394
+ ]);
395
+ menu.setOptions([
396
+ i18n.tSync('action.follow_recommended'),
397
+ i18n.tSync('action.custom_input'),
398
+ i18n.tSync('api.edit.back'),
399
+ ]);
400
+ const choice = await menu.navigate(null, null);
401
+ if (choice === -1 || choice === 2) return;
402
+
403
+ if (choice === 0) {
404
+ try {
405
+ api = apiManager.updateModelEnvVar(api.id, key, '');
406
+ screen.write(colors.green + i18n.tSync('api.edit.success', label) + colors.reset + '\n');
407
+ await waitForKey(i18n.tSync('ui.general.press_any_key_continue'));
408
+ return;
409
+ } catch (e) {
410
+ screen.write(colors.red + e.message + colors.reset + '\n');
411
+ await waitForKey(i18n.tSync('ui.general.press_any_key_continue'));
412
+ }
413
+ } else if (choice === 1) {
414
+ screen.render(['', colors.cyan + i18n.tSync('api.edit.current_value', currentVal) + colors.reset,
415
+ colors.gray + i18n.tSync('prompt.exit_to_cancel') + colors.reset, '']);
416
+ const input = await simpleInput(colors.green + i18n.tSync('api.edit.new_value') + colors.reset);
417
+ if (input.toLowerCase() === 'exit' || input.toLowerCase() === 'quit') continue;
418
+ try {
419
+ api = apiManager.updateModelEnvVar(api.id, key, input);
420
+ screen.write(colors.green + i18n.tSync('api.edit.success', label) + colors.reset + '\n');
421
+ await waitForKey(i18n.tSync('ui.general.press_any_key_continue'));
422
+ return;
423
+ } catch (e) {
424
+ screen.write(colors.red + e.message + colors.reset + '\n');
425
+ await waitForKey(i18n.tSync('ui.general.press_any_key_continue'));
426
+ }
427
+ }
428
+ }
429
+ }
430
+
431
+ async function editRuntimeEnvVarsMenu(apiManager, api) {
432
+ const menu = new Menu();
433
+ const { PREDEFINED_RUNTIME_KEYS, TYPE_A_FIELDS, TYPE_B_FIELDS } = require('../validators');
434
+ const { RUNTIME_CONFIG_LABELS } = require('../api-manager');
435
+ const providerConfig = getProvider(api.provider);
436
+ const providerDefaults = providerConfig ? providerConfig.envVars || {} : {};
437
+ const getLabel = (key) => i18nLabel("runtime", key, RUNTIME_CONFIG_LABELS);
438
+
439
+ while (true) {
440
+ const runtimeKeys = [...PREDEFINED_RUNTIME_KEYS];
441
+ const rows = runtimeKeys.map(key => {
442
+ const val = api.runtimeEnvVars[key] || '';
443
+ const providerVal = providerDefaults[key];
444
+ let display;
445
+ if (val === '') {
446
+ if (providerVal !== undefined) {
447
+ if (TYPE_A_FIELDS.includes(key) && providerVal === '1') {
448
+ display = i18n.tSync('status.enabled');
449
+ } else {
450
+ display = providerVal;
451
+ }
452
+ } else {
453
+ display = i18n.tSync('status.auto');
454
+ }
455
+ } else if (TYPE_A_FIELDS.includes(key) && val === 'off') {
456
+ display = i18n.tSync('status.disabled');
457
+ } else if (TYPE_A_FIELDS.includes(key) && val === '1') {
458
+ display = i18n.tSync('status.enabled');
459
+ } else {
460
+ display = val;
461
+ }
462
+ // result-based override check
463
+ let effectiveForCompare = val;
464
+ if (val === '') { effectiveForCompare = providerVal !== undefined ? providerVal : ''; }
465
+ const recommendedVal = providerVal !== undefined ? providerVal : '';
466
+ const isOverridden = val !== '' && effectiveForCompare !== recommendedVal;
467
+ const mark = isOverridden ? ' ' + colors.cyan + i18n.tSync('status.overridden') + colors.reset : '';
468
+ return { label: ' ' + getLabel(key), display, mark };
469
+ });
470
+ const maxLabelW = Math.max(...rows.map(r => getStringWidth(r.label)));
471
+ const maxValueW = Math.max(...rows.map(r => getStringWidth(r.display)));
472
+ const options = rows.map(r => {
473
+ return colors.reset + padStringToWidth(r.label, maxLabelW + 2) + ' ' + padStringToWidth(r.display, maxValueW) + r.mark;
474
+ });
475
+ options.push(i18n.tSync('api.edit.back'));
476
+ menu.setOptions(options);
477
+ const hintFn = (idx) => {
478
+ if (idx >= runtimeKeys.length) return null;
479
+ const key = runtimeKeys[idx];
480
+ const detailKeys = {
481
+ API_TIMEOUT_MS: 'hints.runtime.timeout_detail',
482
+ CLAUDE_CODE_ATTRIBUTION_HEADER: 'hints.runtime.attribution_detail',
483
+ CLAUDE_CODE_DISABLE_NONESSENTIAL_TRAFFIC: 'hints.runtime.nonessential_detail',
484
+ CLAUDE_CODE_EFFORT_LEVEL: 'hints.runtime.effort_detail',
485
+ CLAUDE_CODE_DISABLE_EXPERIMENTAL_BETAS: 'hints.runtime.experimental_detail',
486
+ CLAUDE_CODE_DISABLE_NONSTREAMING_FALLBACK: 'hints.runtime.nonstreaming_detail',
487
+ };
488
+ if (!detailKeys[key]) return null;
489
+ // Dynamic source indicator
490
+ const val = api.runtimeEnvVars[key] || '';
491
+ const isManual = (api._runtimeEnvSources || {})[key] === 'manual';
492
+ let sourceText;
493
+ if (isManual && val !== '') {
494
+ sourceText = i18n.tSync('hints.runtime.source_manual');
495
+ } else if (providerDefaults[key] !== undefined) {
496
+ sourceText = i18n.tSync('hints.runtime.source_provider');
497
+ } else {
498
+ sourceText = i18n.tSync('hints.runtime.source_default');
499
+ }
500
+ return i18n.tSync(detailKeys[key]) + '\n' + sourceText;
501
+ };
502
+ const choice = await menu.navigate(null, hintFn, 'navigation.enter_to_edit');
503
+
504
+ if (choice === -1 || choice === runtimeKeys.length) return;
505
+
506
+ const key = runtimeKeys[choice];
507
+ if (TYPE_A_FIELDS.includes(key) || TYPE_B_FIELDS.includes(key)) {
508
+ await editRuntimeSwitchField(apiManager, api, key);
509
+ } else {
510
+ await editRuntimeTextField(apiManager, api, key);
511
+ }
512
+ api = getApiById(apiManager, api.id);
513
+ }
514
+ }
515
+
516
+ async function editRuntimeSwitchField(apiManager, api, key) {
517
+ const { RUNTIME_CONFIG_LABELS } = require('../api-manager');
518
+ const { TYPE_A_FIELDS, TYPE_B_FIELDS } = require('../validators');
519
+ const label = i18nLabel("runtime", key, RUNTIME_CONFIG_LABELS);
520
+ const currentVal = api.runtimeEnvVars[key] || '';
521
+ const isManual = (api._runtimeEnvSources || {})[key] === 'manual';
522
+ const providerConfig = getProvider(api.provider);
523
+ const providerVal = (providerConfig && providerConfig.envVars) ? providerConfig.envVars[key] : undefined;
524
+ const effectiveVal = isManual ? currentVal : (providerVal !== undefined ? providerVal : '');
525
+ let currentDisplay;
526
+ if (effectiveVal === '') {
527
+ currentDisplay = i18n.tSync('status.auto');
528
+ } else if (TYPE_A_FIELDS.includes(key) && effectiveVal === '1') {
529
+ currentDisplay = i18n.tSync('status.enabled');
530
+ } else if (TYPE_A_FIELDS.includes(key) && effectiveVal === 'off') {
531
+ currentDisplay = i18n.tSync('status.disabled');
532
+ } else {
533
+ currentDisplay = effectiveVal;
534
+ }
535
+ const runtimeShortKeys = {
536
+ API_TIMEOUT_MS: 'timeout', CLAUDE_CODE_ATTRIBUTION_HEADER: 'attribution',
537
+ CLAUDE_CODE_DISABLE_NONESSENTIAL_TRAFFIC: 'nonessential', CLAUDE_CODE_EFFORT_LEVEL: 'effort',
538
+ CLAUDE_CODE_DISABLE_EXPERIMENTAL_BETAS: 'experimental', CLAUDE_CODE_DISABLE_NONSTREAMING_FALLBACK: 'nonstreaming',
539
+ };
540
+ const descKey = 'hints.runtime.' + (runtimeShortKeys[key] || key) + '_detail';
541
+ const menu = new Menu();
542
+ while (true) {
543
+ screen.render([
544
+ '', colors.cyan + label + colors.reset, '',
545
+ i18n.tSync('status.current_value') + ': ' + currentDisplay,
546
+ '', colors.gray + i18n.tSync(descKey) + colors.reset,
547
+ '', i18n.tSync('action.please_choose'), '',
548
+ ]);
549
+ menu.setOptions([
550
+ i18n.tSync('action.follow_recommended'),
551
+ i18n.tSync('action.force_enable'),
552
+ i18n.tSync('action.force_disable'),
553
+ i18n.tSync('api.edit.back'),
554
+ ]);
555
+ const choice = await menu.navigate(null, null);
556
+ if (choice === -1 || choice === 3) return;
557
+ const values = TYPE_A_FIELDS.includes(key) ? ['', '1', 'off'] : ['', '1', '0'];
558
+ try {
559
+ api = apiManager.updateRuntimeEnvVar(api.id, key, values[choice]);
560
+ return;
561
+ } catch (e) {
562
+ screen.write(colors.red + e.message + colors.reset + '\n');
563
+ await waitForKey(i18n.tSync('ui.general.press_any_key_continue'));
564
+ }
565
+ }
566
+ }
567
+
568
+ async function editRuntimeTextField(apiManager, api, key) {
569
+ const { RUNTIME_CONFIG_LABELS } = require('../api-manager');
570
+ const label = i18nLabel("runtime", key, RUNTIME_CONFIG_LABELS);
571
+ const currentVal = api.runtimeEnvVars[key] || '';
572
+ const providerConfig = getProvider(api.provider);
573
+ const providerVal = (providerConfig && providerConfig.envVars) ? providerConfig.envVars[key] : undefined;
574
+ const displayVal = currentVal || providerVal || i18n.tSync('status.auto');
575
+ const menu = new Menu();
576
+ const runtimeShortKeys = {
577
+ API_TIMEOUT_MS: 'timeout', CLAUDE_CODE_ATTRIBUTION_HEADER: 'attribution',
578
+ CLAUDE_CODE_DISABLE_NONESSENTIAL_TRAFFIC: 'nonessential', CLAUDE_CODE_EFFORT_LEVEL: 'effort',
579
+ CLAUDE_CODE_DISABLE_EXPERIMENTAL_BETAS: 'experimental', CLAUDE_CODE_DISABLE_NONSTREAMING_FALLBACK: 'nonstreaming',
580
+ };
581
+ const descKey = 'hints.runtime.' + (runtimeShortKeys[key] || key) + '_detail';
582
+ const isEffortLevel = (key === 'CLAUDE_CODE_EFFORT_LEVEL');
583
+ while (true) {
584
+ const renderLines = [
585
+ '', colors.cyan + label + colors.reset, '',
586
+ i18n.tSync('status.current_value') + ': ' + displayVal,
587
+ '', colors.gray + i18n.tSync(descKey) + colors.reset,
588
+ ];
589
+ if (isEffortLevel) {
590
+ renderLines.push(colors.gray + i18n.tSync('hints.runtime.effort_values') + colors.reset);
591
+ }
592
+ renderLines.push('', i18n.tSync('action.please_choose'), '');
593
+ screen.render(renderLines);
594
+ menu.setOptions([
595
+ i18n.tSync('action.follow_recommended'),
596
+ i18n.tSync('action.custom_input'),
597
+ i18n.tSync('api.edit.back'),
598
+ ]);
599
+ const choice = await menu.navigate(null, null);
600
+ if (choice === -1 || choice === 2) return;
601
+ if (choice === 0) {
602
+ try {
603
+ api = apiManager.updateRuntimeEnvVar(api.id, key, '');
604
+ return;
605
+ } catch (e) {
606
+ screen.write(colors.red + e.message + colors.reset + '\n');
607
+ await waitForKey(i18n.tSync('ui.general.press_any_key_continue'));
608
+ }
609
+ } else if (choice === 1) {
610
+ screen.render(['', colors.cyan + i18n.tSync('api.edit.current_value', currentVal) + colors.reset,
611
+ colors.gray + i18n.tSync('prompt.exit_to_cancel') + colors.reset, '']);
612
+ const input = await simpleInput(colors.green + i18n.tSync('api.edit.new_value') + colors.reset);
613
+ if (input.toLowerCase() === 'exit' || input.toLowerCase() === 'quit') continue;
614
+ try {
615
+ api = apiManager.updateRuntimeEnvVar(api.id, key, input);
616
+ return;
617
+ } catch (e) {
618
+ screen.write(colors.red + e.message + colors.reset + '\n');
619
+ await waitForKey(i18n.tSync('ui.general.press_any_key_continue'));
620
+ }
621
+ }
622
+ }
623
+ }
624
+
625
+ async function editCustomEnvVarsMenu(apiManager, api) {
626
+ const menu = new Menu();
627
+ while (true) {
628
+ const keys = Object.keys(api.customEnvVars || {});
629
+ const options = keys.length > 0
630
+ ? keys.map(k => ' ' + k + ' = ' + api.customEnvVars[k])
631
+ : [colors.gray + i18n.tSync('api.edit.no_custom_vars') + colors.reset];
632
+ options.push(i18n.tSync('action.add_variable'));
633
+ options.push(i18n.tSync('api.edit.back'));
634
+ menu.setOptions(options);
635
+ const choice = await menu.navigate(null, null, 'navigation.enter_to_edit');
636
+
637
+ if (choice === -1 || choice === options.length - 1) return;
638
+ if (choice === options.length - 2) {
639
+ screen.render(['', colors.green + i18n.tSync('api.edit.enter_custom_key') + colors.reset]);
640
+ const key = await simpleInput('> ');
641
+ if (!key || key.toLowerCase() === 'exit') continue;
642
+ screen.render(['', colors.green + i18n.tSync('api.edit.enter_custom_value') + colors.reset]);
643
+ const val = await simpleInput('> ');
644
+ try {
645
+ api = apiManager.setCustomEnvVar(api.id, key, val || '');
646
+ screen.write(colors.green + i18n.tSync('api.edit.success', key) + colors.reset + '\n');
647
+ await waitForKey(i18n.tSync('ui.general.press_any_key_continue'));
648
+ } catch (e) {
649
+ screen.write(colors.red + e.message + colors.reset + '\n');
650
+ await waitForKey(i18n.tSync('ui.general.press_any_key_continue'));
651
+ }
652
+ } else if (choice < keys.length) {
653
+ const key = keys[choice];
654
+ screen.render(['', colors.yellow + i18n.tSync('confirm.delete_variable') + colors.reset]);
655
+ const confirm = await simpleInput('> ');
656
+ if (confirm.toLowerCase() === 'y') {
657
+ api = apiManager.deleteCustomEnvVar(api.id, key);
658
+ }
659
+ }
660
+ }
661
+ }
662
+
663
+ module.exports = {
664
+ editApi,
665
+ editApiById,
666
+ editApiEnvVarsById,
667
+ resolveProviderName,
668
+ };