@kikkimo/claude-launcher 1.0.0 → 2.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,540 @@
1
+ /**
2
+ * Prompts Module - User input prompts and interactions
3
+ */
4
+
5
+ const readline = require('readline');
6
+ const colors = require('./colors');
7
+ const { getAllProviders } = require('../presets/providers');
8
+ const { validateBaseUrl, validateAuthToken, validateModel } = require('../validators');
9
+ const i18n = require('../i18n');
10
+
11
+ /**
12
+ * Simple input using readline
13
+ */
14
+ async function simpleInput(prompt) {
15
+ return new Promise((resolve) => {
16
+ const rl = readline.createInterface({
17
+ input: process.stdin,
18
+ output: process.stdout
19
+ });
20
+
21
+ rl.question(prompt, (answer) => {
22
+ rl.close();
23
+ resolve(answer.trim());
24
+ });
25
+ });
26
+ }
27
+
28
+ /**
29
+ * Get provider choice with ESC key support
30
+ */
31
+ async function getProviderChoice(prompt) {
32
+ return new Promise((resolve) => {
33
+ if (process.stdin.isTTY) {
34
+ // Use raw mode to capture ESC key - this is necessary for interactive input
35
+ process.stdout.write(colors.green + prompt + colors.reset);
36
+
37
+ let input = '';
38
+
39
+ // Save original state
40
+ const originalRawMode = process.stdin.isRaw;
41
+ const originalPaused = process.stdin.isPaused();
42
+
43
+ process.stdin.setRawMode(true);
44
+ process.stdin.resume();
45
+ process.stdin.setEncoding('utf8');
46
+
47
+ const cleanup = () => {
48
+ try {
49
+ process.stdin.setRawMode(false);
50
+ if (originalPaused) {
51
+ process.stdin.pause();
52
+ }
53
+ process.stdin.removeAllListeners('data');
54
+ } catch (error) {
55
+ // Ignore cleanup errors
56
+ }
57
+ };
58
+
59
+ const handleKeyPress = (key) => {
60
+ const keyCode = key.charCodeAt(0);
61
+
62
+ switch (keyCode) {
63
+ case 27: // ESC key
64
+ cleanup();
65
+ process.stdout.write('\n');
66
+ resolve(null);
67
+ return;
68
+
69
+ case 13: // Enter key
70
+ cleanup();
71
+ process.stdout.write('\n');
72
+ resolve(input);
73
+ return;
74
+
75
+ case 127: // Backspace
76
+ case 8: // Backspace (some terminals)
77
+ if (input.length > 0) {
78
+ input = input.slice(0, -1);
79
+ process.stdout.write('\b \b');
80
+ }
81
+ return;
82
+
83
+ case 3: // Ctrl+C
84
+ cleanup();
85
+ process.stdout.write('\n');
86
+ resolve(null);
87
+ return;
88
+
89
+ default:
90
+ // Only accept printable characters
91
+ if (keyCode >= 32 && keyCode < 127) {
92
+ input += key;
93
+ process.stdout.write(key);
94
+ }
95
+ return;
96
+ }
97
+ };
98
+
99
+ process.stdin.on('data', handleKeyPress);
100
+ } else {
101
+ // Fallback for non-TTY environments
102
+ const rl = readline.createInterface({
103
+ input: process.stdin,
104
+ output: process.stdout
105
+ });
106
+
107
+ rl.question(colors.green + prompt + colors.reset, (answer) => {
108
+ rl.close();
109
+ resolve(answer.trim());
110
+ });
111
+ }
112
+ });
113
+ }
114
+
115
+
116
+ /**
117
+ * Wait for any key press
118
+ */
119
+ async function waitForKey(message = 'Press any key to continue...') {
120
+ console.log(colors.gray + message + colors.reset);
121
+
122
+ return new Promise((resolve) => {
123
+ if (process.stdin.isTTY) {
124
+ // Force complete cleanup before setting up new listeners
125
+ try {
126
+ process.stdin.removeAllListeners('data');
127
+ process.stdin.removeAllListeners('keypress');
128
+ } catch (error) {
129
+ // Ignore cleanup errors
130
+ }
131
+
132
+ process.stdin.setRawMode(true);
133
+ process.stdin.resume();
134
+ process.stdin.once('data', () => {
135
+ // Complete cleanup after key press
136
+ try {
137
+ process.stdin.setRawMode(false);
138
+ process.stdin.removeAllListeners('data');
139
+ process.stdin.removeAllListeners('keypress');
140
+ process.stdin.pause();
141
+ } catch (error) {
142
+ // Ignore cleanup errors
143
+ }
144
+ resolve();
145
+ });
146
+ } else {
147
+ const rl = readline.createInterface({
148
+ input: process.stdin,
149
+ output: process.stdout
150
+ });
151
+ rl.question('', () => {
152
+ rl.close();
153
+ resolve();
154
+ });
155
+ }
156
+ });
157
+ }
158
+
159
+ /**
160
+ * Prompt for third-party API configuration with enhanced guidance
161
+ */
162
+ async function promptForThirdPartyApi() {
163
+ try {
164
+ // Step 1: Show information and wait for acknowledgment
165
+ console.clear();
166
+ console.log('');
167
+ console.log(colors.bright + colors.orange + i18n.tSync('ui.general.add_new_api_title') + colors.reset);
168
+ console.log('');
169
+
170
+ // Security and privacy information
171
+ console.log(colors.yellow + i18n.tSync('ui.general.security_privacy_info') + colors.reset);
172
+ const securityItems = i18n.tSync('ui.general.security_items');
173
+ securityItems.forEach(item => {
174
+ console.log(colors.bright + colors.green + ' • ' + item + colors.reset);
175
+ });
176
+ console.log('');
177
+
178
+ console.log(colors.yellow + i18n.tSync('ui.general.configuration_tips') + colors.reset);
179
+ const configTips = i18n.tSync('ui.general.config_tip_items');
180
+ configTips.forEach(tip => {
181
+ console.log(colors.gray + ' • ' + tip + colors.reset);
182
+ });
183
+ console.log(colors.gray + ' • ' + i18n.tSync('ui.general.type_exit_cancel') + colors.reset);
184
+ console.log('');
185
+
186
+ console.log(colors.yellow + i18n.tSync('ui.general.all_providers_compatible') + colors.reset);
187
+ console.log('');
188
+
189
+ await waitForKey(i18n.tSync('ui.general.press_continue_provider_selection'));
190
+
191
+ // Step 2: Show provider selection menu
192
+ console.clear();
193
+ console.log('');
194
+ console.log(colors.bright + colors.orange + i18n.tSync('ui.general.add_new_api_title') + colors.reset);
195
+ console.log('');
196
+
197
+ // Show available providers
198
+ console.log(colors.cyan + i18n.tSync('ui.general.compatible_providers_title') + colors.reset);
199
+ console.log('');
200
+ const providers = getAllProviders();
201
+ providers.forEach((provider, index) => {
202
+ const compatIcon = provider.compatibility === 'native' ? '🎯' : '✅';
203
+ console.log(colors.gray + ` ${index + 1}. ${compatIcon} ${provider.name}` + colors.reset);
204
+ console.log(colors.dim + ` ${provider.description}` + colors.reset);
205
+ });
206
+ console.log('');
207
+
208
+ // Select provider or custom with validation
209
+ let selectedProvider = null;
210
+ let baseUrl = '';
211
+ let suggestedModels = [];
212
+
213
+ while (true) {
214
+ const selectPrompt = i18n.tSync('ui.general.select_provider_prompt').replace('{0}', providers.length);
215
+ const providerChoice = await getProviderChoice(selectPrompt);
216
+
217
+ if (providerChoice === null) {
218
+ // User cancelled with ESC
219
+ throw new Error(i18n.tSync('errors.general.cancelled_by_user'));
220
+ }
221
+
222
+ if (providerChoice.toLowerCase() === 'exit' || providerChoice.toLowerCase() === 'quit') {
223
+ throw new Error(i18n.tSync('errors.general.cancelled_by_user'));
224
+ }
225
+
226
+ // Require non-empty input
227
+ if (!providerChoice || providerChoice.trim() === '') {
228
+ console.log(colors.red + i18n.tSync('ui.general.provider_selection_required', providers.length) + colors.reset);
229
+ continue;
230
+ }
231
+
232
+ // Validate numeric input
233
+ if (isNaN(providerChoice)) {
234
+ console.log(colors.red + i18n.tSync('ui.general.invalid_provider_selection').replace('{0}', providers.length) + colors.reset);
235
+ continue;
236
+ }
237
+
238
+ const index = parseInt(providerChoice) - 1;
239
+ if (index < 0 || index >= providers.length) {
240
+ console.log(colors.red + i18n.tSync('ui.general.invalid_provider_number').replace('{0}', providers.length) + colors.reset);
241
+ continue;
242
+ }
243
+
244
+ // Valid selection
245
+ selectedProvider = providers[index];
246
+ baseUrl = selectedProvider.baseUrl;
247
+ suggestedModels = selectedProvider.models || [];
248
+
249
+ console.log('');
250
+ console.log(colors.green + i18n.tSync('ui.general.selected_provider', selectedProvider.name) + colors.reset);
251
+ if (selectedProvider.note) {
252
+ if (selectedProvider.id === 'custom') {
253
+ console.log(colors.yellow + ' ' + i18n.tSync('ui.general.replace_url_model_note') + colors.reset);
254
+ } else {
255
+ console.log(colors.yellow + ` Note: ${selectedProvider.note}` + colors.reset);
256
+ }
257
+ }
258
+ console.log('');
259
+ break;
260
+ }
261
+
262
+ // Input base URL - different handling for custom vs specific providers
263
+ if (selectedProvider && selectedProvider.id === 'custom') {
264
+ // Custom provider - show reference URL and require manual input
265
+ console.log(colors.gray + ` ` + i18n.tSync('ui.general.reference_base_url', baseUrl) + colors.reset);
266
+ console.log('');
267
+
268
+ while (true) {
269
+ const inputUrl = await simpleInput(colors.green + i18n.tSync('ui.general.api_base_url_prompt') + colors.reset);
270
+
271
+ if (inputUrl.toLowerCase() === 'exit' || inputUrl.toLowerCase() === 'quit') {
272
+ throw new Error(i18n.tSync('errors.general.cancelled_by_user'));
273
+ }
274
+
275
+ if (!inputUrl || inputUrl.trim() === '') {
276
+ console.log(colors.red + i18n.tSync('ui.general.base_url_required') + colors.reset);
277
+ continue;
278
+ }
279
+
280
+ const validation = validateBaseUrl(inputUrl);
281
+ if (!validation.valid) {
282
+ console.log(colors.red + `❌ ${validation.error}` + colors.reset);
283
+ continue;
284
+ }
285
+ baseUrl = validation.value;
286
+ break;
287
+ }
288
+ } else if (selectedProvider && !baseUrl.includes('{')) {
289
+ // Specific providers - show recommended URL with option to use default
290
+ console.log(colors.gray + ` ` + i18n.tSync('ui.general.recommended_base_url', baseUrl) + colors.reset);
291
+
292
+ // For all known providers, show the recommended URL in the prompt
293
+ let prompt;
294
+ if (selectedProvider.id === 'anthropic' || selectedProvider.id === 'deepseek' || selectedProvider.id === 'moonshot') {
295
+ prompt = colors.green + i18n.tSync('ui.general.press_enter_default_url') + `${colors.yellow}${baseUrl}${colors.green}` + colors.reset;
296
+ console.log(colors.gray + ' ' + i18n.tSync('ui.general.edit_url_hint') + colors.reset);
297
+ } else {
298
+ prompt = colors.green + i18n.tSync('ui.general.press_enter_default_url') + colors.reset;
299
+ }
300
+
301
+ const customUrl = await simpleInput(prompt);
302
+ if (customUrl) {
303
+ if (customUrl.toLowerCase() === 'exit' || customUrl.toLowerCase() === 'quit') {
304
+ throw new Error(i18n.tSync('errors.general.cancelled_by_user'));
305
+ }
306
+ baseUrl = customUrl;
307
+ }
308
+ } else {
309
+ // Fallback case
310
+ while (true) {
311
+ const inputUrl = await simpleInput(colors.green + i18n.tSync('ui.general.api_base_url_prompt') + colors.reset);
312
+
313
+ if (inputUrl.toLowerCase() === 'exit' || inputUrl.toLowerCase() === 'quit') {
314
+ throw new Error(i18n.tSync('errors.general.cancelled_by_user'));
315
+ }
316
+
317
+ const validation = validateBaseUrl(inputUrl);
318
+ if (!validation.valid) {
319
+ console.log(colors.red + `❌ ${validation.error}` + colors.reset);
320
+ continue;
321
+ }
322
+ baseUrl = validation.value;
323
+ break;
324
+ }
325
+ }
326
+
327
+ // Input auth token
328
+ let authToken;
329
+ console.log('');
330
+
331
+ // Simplified API token input
332
+ if (selectedProvider) {
333
+ console.log(colors.gray + ` ` + i18n.tSync('ui.general.expected_format', selectedProvider.authTokenFormat) + colors.reset);
334
+ }
335
+ console.log(colors.gray + ' ' + i18n.tSync('ui.general.type_exit_cancel_setup') + colors.reset);
336
+ console.log('');
337
+
338
+ while (true) {
339
+ const token = await simpleInput(colors.green + i18n.tSync('ui.general.auth_token_prompt') + colors.reset);
340
+
341
+ if (token.toLowerCase() === 'exit' || token.toLowerCase() === 'quit') {
342
+ throw new Error(i18n.tSync('errors.general.cancelled_by_user'));
343
+ }
344
+
345
+ const validation = validateAuthToken(token);
346
+ if (!validation.valid) {
347
+ console.log(colors.red + `❌ ${validation.error}` + colors.reset);
348
+ continue;
349
+ }
350
+ authToken = validation.value;
351
+ break;
352
+ }
353
+
354
+ // Input model - different handling for custom vs specific providers
355
+ let model;
356
+ console.log('');
357
+
358
+ if (selectedProvider && selectedProvider.id === 'custom') {
359
+ // Custom provider - always require manual input, no suggested models
360
+ while (true) {
361
+ const inputModel = await simpleInput(colors.green + i18n.tSync('ui.general.model_name_prompt') + colors.reset);
362
+
363
+ if (inputModel.toLowerCase() === 'exit' || inputModel.toLowerCase() === 'quit') {
364
+ throw new Error(i18n.tSync('errors.general.cancelled_by_user'));
365
+ }
366
+
367
+ const validation = validateModel(inputModel);
368
+ if (!validation.valid) {
369
+ console.log(colors.red + `❌ ${validation.error}` + colors.reset);
370
+ continue;
371
+ }
372
+ model = validation.value;
373
+ break;
374
+ }
375
+ } else if (suggestedModels.length > 0) {
376
+ // Specific providers - show suggested models
377
+ console.log(colors.cyan + ' ' + i18n.tSync('ui.general.suggested_models') + colors.reset);
378
+ suggestedModels.forEach((m, i) => {
379
+ console.log(colors.gray + ` ${i + 1}. ${m}` + colors.reset);
380
+ });
381
+ console.log('');
382
+
383
+ while (true) {
384
+ const modelPrompt = i18n.tSync('ui.general.select_model_prompt').replace('{0}', suggestedModels.length);
385
+ const modelChoice = await simpleInput(colors.green + modelPrompt + colors.reset);
386
+
387
+ if (modelChoice.toLowerCase() === 'exit' || modelChoice.toLowerCase() === 'quit') {
388
+ throw new Error(i18n.tSync('errors.general.cancelled_by_user'));
389
+ }
390
+
391
+ // Check if it's a number selection
392
+ if (!isNaN(modelChoice) && modelChoice.trim() !== '') {
393
+ const index = parseInt(modelChoice) - 1;
394
+ if (index >= 0 && index < suggestedModels.length) {
395
+ model = suggestedModels[index];
396
+ break;
397
+ } else {
398
+ console.log(colors.red + i18n.tSync('ui.general.invalid_model_selection').replace('{0}', suggestedModels.length) + colors.reset);
399
+ continue;
400
+ }
401
+ }
402
+
403
+ // If not a number, validate as custom model name
404
+ const validation = validateModel(modelChoice);
405
+ if (!validation.valid) {
406
+ console.log(colors.red + `❌ ${validation.error}` + colors.reset);
407
+ continue;
408
+ }
409
+ model = validation.value;
410
+ break;
411
+ }
412
+ } else {
413
+ // Fallback - manual input
414
+ while (true) {
415
+ const inputModel = await simpleInput(colors.green + i18n.tSync('ui.general.model_name_prompt') + colors.reset);
416
+
417
+ if (inputModel.toLowerCase() === 'exit' || inputModel.toLowerCase() === 'quit') {
418
+ throw new Error(i18n.tSync('errors.general.cancelled_by_user'));
419
+ }
420
+
421
+ const validation = validateModel(inputModel);
422
+ if (!validation.valid) {
423
+ console.log(colors.red + `❌ ${validation.error}` + colors.reset);
424
+ continue;
425
+ }
426
+ model = validation.value;
427
+ break;
428
+ }
429
+ }
430
+
431
+ // Input name
432
+ const name = await simpleInput(colors.green + i18n.tSync('ui.general.api_name_prompt') + colors.reset);
433
+ if (name.toLowerCase() === 'exit' || name.toLowerCase() === 'quit') {
434
+ throw new Error(i18n.tSync('errors.general.cancelled_by_user'));
435
+ }
436
+
437
+ return {
438
+ baseUrl,
439
+ authToken,
440
+ model,
441
+ name: name || undefined,
442
+ provider: selectedProvider?.id || 'custom'
443
+ };
444
+
445
+ } catch (error) {
446
+ // Let the upper layer handle error display to avoid duplicate messages
447
+ throw error;
448
+ }
449
+ }
450
+
451
+ /**
452
+ * Confirm action prompt
453
+ */
454
+ async function confirmAction(message) {
455
+ console.log(colors.yellow + message + colors.reset);
456
+ console.log(colors.gray + i18n.tSync('ui.general.press_y_confirm') + colors.reset);
457
+
458
+ return new Promise((resolve) => {
459
+ if (process.stdin.isTTY) {
460
+ // Force complete cleanup before setting up new listeners
461
+ try {
462
+ process.stdin.removeAllListeners('data');
463
+ process.stdin.removeAllListeners('keypress');
464
+ } catch (error) {
465
+ // Ignore cleanup errors
466
+ }
467
+
468
+ process.stdin.setRawMode(true);
469
+ process.stdin.resume();
470
+ process.stdin.once('data', (key) => {
471
+ // Complete cleanup after key press
472
+ try {
473
+ process.stdin.setRawMode(false);
474
+ process.stdin.removeAllListeners('data');
475
+ process.stdin.removeAllListeners('keypress');
476
+ process.stdin.pause();
477
+ } catch (error) {
478
+ // Ignore cleanup errors
479
+ }
480
+ resolve(key.toString().toLowerCase() === 'y');
481
+ });
482
+ } else {
483
+ const rl = readline.createInterface({
484
+ input: process.stdin,
485
+ output: process.stdout
486
+ });
487
+ rl.question('', (answer) => {
488
+ rl.close();
489
+ resolve(answer.toLowerCase() === 'y');
490
+ });
491
+ }
492
+ });
493
+ }
494
+
495
+ /**
496
+ * Display success message
497
+ */
498
+ function showSuccess(title, details = []) {
499
+ console.log('');
500
+ console.log(colors.bright + colors.green + `✓ ${title}` + colors.reset);
501
+ details.forEach(detail => {
502
+ console.log(colors.gray + ` ${detail}` + colors.reset);
503
+ });
504
+ console.log('');
505
+ }
506
+
507
+ /**
508
+ * Display error message
509
+ */
510
+ function showError(title, details = []) {
511
+ console.log('');
512
+ console.log(colors.bright + colors.red + `❌ ${title}` + colors.reset);
513
+ details.forEach(detail => {
514
+ console.log(colors.gray + ` ${detail}` + colors.reset);
515
+ });
516
+ console.log('');
517
+ }
518
+
519
+ /**
520
+ * Display info message
521
+ */
522
+ function showInfo(title, details = []) {
523
+ console.log('');
524
+ console.log(colors.bright + colors.cyan + `ℹ️ ${title}` + colors.reset);
525
+ details.forEach(detail => {
526
+ console.log(colors.gray + ` ${detail}` + colors.reset);
527
+ });
528
+ console.log('');
529
+ }
530
+
531
+
532
+ module.exports = {
533
+ simpleInput,
534
+ waitForKey,
535
+ promptForThirdPartyApi,
536
+ confirmAction,
537
+ showSuccess,
538
+ showError,
539
+ showInfo
540
+ };