@kikkimo/claude-launcher 2.5.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.
@@ -11,6 +11,7 @@ const EventEmitter = require('events');
11
11
  const crypto = require('crypto');
12
12
  const colors = require('../ui/colors');
13
13
  const i18n = require('../i18n');
14
+ const screen = require('../ui/screen');
14
15
 
15
16
  /**
16
17
  * Generate a cryptographically unique ID
@@ -111,7 +112,7 @@ class StdinManager extends EventEmitter {
111
112
  */
112
113
  _debug(...args) {
113
114
  if (this.debugMode) {
114
- console.error('[StdinManager]', ...args);
115
+ screen.debug('[StdinManager] ' + args.join(' '));
115
116
  }
116
117
  }
117
118
 
@@ -389,8 +390,8 @@ class StdinManager extends EventEmitter {
389
390
  // Clear the warning text only in TTY mode to avoid spamming ANSI codes
390
391
  if (process.stdout && process.stdout.isTTY) {
391
392
  // Clear the warning text (2 lines: empty line + warning line)
392
- process.stdout.write('\x1b[1A\r\x1b[K'); // Up 1 line, clear warning
393
- process.stdout.write('\x1b[1A\r\x1b[K'); // Up 1 line, clear empty line
393
+ screen.write('\x1b[1A\r\x1b[K'); // Up 1 line, clear warning
394
+ screen.write('\x1b[1A\r\x1b[K'); // Up 1 line, clear empty line
394
395
  }
395
396
  this._debug('Ctrl+C cancelled by user input');
396
397
  }
@@ -432,8 +433,8 @@ class StdinManager extends EventEmitter {
432
433
  // Clear the warning text only in TTY mode to avoid spamming ANSI codes
433
434
  if (process.stdout && process.stdout.isTTY) {
434
435
  // Clear the warning text (2 lines: empty line + warning line)
435
- process.stdout.write('\x1b[1A\r\x1b[K'); // Up 1 line, clear warning
436
- process.stdout.write('\x1b[1A\r\x1b[K'); // Up 1 line, clear empty line
436
+ screen.write('\x1b[1A\r\x1b[K'); // Up 1 line, clear warning
437
+ screen.write('\x1b[1A\r\x1b[K'); // Up 1 line, clear empty line
437
438
  }
438
439
  this._debug('Ctrl+C timer expired - warning cleared, returning to normal operation');
439
440
  }
@@ -464,8 +465,8 @@ class StdinManager extends EventEmitter {
464
465
 
465
466
  if (this.ctrlCCount === 1) {
466
467
  // First Ctrl+C - show warning
467
- console.log('');
468
- console.log(colors.yellow + '⚠️ ' + i18n.tSync('messages.prompts.ctrl_c_again') + colors.reset);
468
+ screen.write('\n');
469
+ screen.write(colors.yellow + '⚠️ ' + i18n.tSync('messages.prompts.ctrl_c_again') + colors.reset + '\n');
469
470
 
470
471
  // Clear any existing timer
471
472
  if (this.ctrlCTimer) {
@@ -486,8 +487,8 @@ class StdinManager extends EventEmitter {
486
487
  this.ctrlCTimer = null;
487
488
  }
488
489
 
489
- console.log('');
490
- console.log(colors.green + i18n.tSync('ui.general.goodbye') + colors.reset);
490
+ screen.write('\n');
491
+ screen.write(colors.green + i18n.tSync('ui.general.goodbye') + colors.reset + '\n');
491
492
 
492
493
  // Clean up stdin before exit
493
494
  // Use clearAll=true since we're exiting anyway
@@ -497,6 +498,7 @@ class StdinManager extends EventEmitter {
497
498
  // Ignore cleanup errors during exit
498
499
  }
499
500
 
501
+ screen.exit();
500
502
  process.exit(0);
501
503
  }
502
504
  }
@@ -27,17 +27,34 @@ async function loadConfig() {
27
27
  // Add new fields for backward compatibility
28
28
  if (config.autoModelUpgrade === undefined) config.autoModelUpgrade = false;
29
29
  if (config.lastModelUpgradeCheck === undefined) config.lastModelUpgradeCheck = 0;
30
+ if (config.disableTelemetry === undefined) config.disableTelemetry = true;
31
+ if (config.showModelUpgradeNotification === undefined) config.showModelUpgradeNotification = true;
32
+ if (config.apiLaunchMode === undefined) config.apiLaunchMode = 'direct';
33
+ if (config.noFlicker === undefined) config.noFlicker = true;
30
34
 
31
35
  return config;
32
36
  } catch (error) {
33
- // Return default config if file doesn't exist
34
- return {
35
- language: 'zh',
37
+ const defaultConfig = {
38
+ language: 'en',
36
39
  lastVersionCheck: 0,
37
40
  cachedLatestVersion: null,
38
41
  autoModelUpgrade: false,
39
- lastModelUpgradeCheck: 0
42
+ lastModelUpgradeCheck: 0,
43
+ disableTelemetry: true,
44
+ showModelUpgradeNotification: true,
45
+ apiLaunchMode: 'direct',
46
+ noFlicker: false,
40
47
  };
48
+
49
+ // Write default config on first run so subsequent reads are consistent
50
+ try {
51
+ const configPath = getConfigPath();
52
+ await fs.writeFile(configPath, JSON.stringify(defaultConfig, null, 2), 'utf8');
53
+ } catch (_) {
54
+ // Silently fail
55
+ }
56
+
57
+ return defaultConfig;
41
58
  }
42
59
  }
43
60
 
@@ -249,11 +266,55 @@ async function setAutoModelUpgrade(enabled) {
249
266
  await saveConfig(config);
250
267
  }
251
268
 
269
+ /**
270
+ * Load configuration from file synchronously
271
+ * Used by launcher.js where async is not available
272
+ */
273
+ function loadConfigSync() {
274
+ try {
275
+ const fsSync = require('fs');
276
+ const data = fsSync.readFileSync(getConfigPath(), 'utf8');
277
+ const config = JSON.parse(data);
278
+
279
+ if (config.autoModelUpgrade === undefined) config.autoModelUpgrade = false;
280
+ if (config.lastModelUpgradeCheck === undefined) config.lastModelUpgradeCheck = 0;
281
+ if (config.disableTelemetry === undefined) config.disableTelemetry = true;
282
+ if (config.showModelUpgradeNotification === undefined) config.showModelUpgradeNotification = true;
283
+ if (config.apiLaunchMode === undefined) config.apiLaunchMode = 'direct';
284
+ if (config.noFlicker === undefined) config.noFlicker = true;
285
+
286
+ return config;
287
+ } catch (_) {
288
+ const defaultConfig = {
289
+ language: 'en',
290
+ lastVersionCheck: 0,
291
+ cachedLatestVersion: null,
292
+ autoModelUpgrade: false,
293
+ lastModelUpgradeCheck: 0,
294
+ disableTelemetry: true,
295
+ showModelUpgradeNotification: true,
296
+ apiLaunchMode: 'direct',
297
+ noFlicker: false,
298
+ };
299
+
300
+ // Write default config on first run so subsequent reads are consistent
301
+ try {
302
+ const fsSync = require('fs');
303
+ fsSync.writeFileSync(getConfigPath(), JSON.stringify(defaultConfig, null, 2), 'utf8');
304
+ } catch (__) {
305
+ // Silently fail
306
+ }
307
+
308
+ return defaultConfig;
309
+ }
310
+ }
311
+
252
312
  module.exports = {
253
313
  checkForUpdates,
254
314
  forceCheckForUpdates,
255
315
  clearCache,
256
316
  loadConfig,
317
+ loadConfigSync,
257
318
  saveConfig,
258
319
  setAutoModelUpgrade
259
320
  };
package/lib/validators.js CHANGED
@@ -120,11 +120,112 @@ function maskApiToken(token) {
120
120
  }
121
121
  }
122
122
 
123
+ // ============================================================
124
+ // Environment variable constants & validators
125
+ // ============================================================
126
+
127
+ const RESERVED_ENV_KEYS = [
128
+ 'ANTHROPIC_BASE_URL',
129
+ 'ANTHROPIC_AUTH_TOKEN',
130
+ 'ANTHROPIC_API_KEY',
131
+ 'ANTHROPIC_MODEL',
132
+ 'ANTHROPIC_SMALL_FAST_MODEL',
133
+ 'CLAUDE_CODE_OAUTH_TOKEN',
134
+ 'DISABLE_TELEMETRY',
135
+ 'CLAUDE_CODE_NO_FLICKER',
136
+ ];
137
+
138
+ const PREDEFINED_RUNTIME_KEYS = [
139
+ 'API_TIMEOUT_MS',
140
+ 'CLAUDE_CODE_ATTRIBUTION_HEADER',
141
+ 'CLAUDE_CODE_DISABLE_NONESSENTIAL_TRAFFIC',
142
+ 'CLAUDE_CODE_EFFORT_LEVEL',
143
+ 'CLAUDE_CODE_DISABLE_EXPERIMENTAL_BETAS',
144
+ 'CLAUDE_CODE_DISABLE_NONSTREAMING_FALLBACK',
145
+ ];
146
+
147
+ const PREDEFINED_MODEL_ENV_KEYS = [
148
+ 'ANTHROPIC_CUSTOM_MODEL_OPTION',
149
+ 'ANTHROPIC_CUSTOM_MODEL_OPTION_NAME',
150
+ 'ANTHROPIC_DEFAULT_SONNET_MODEL',
151
+ 'ANTHROPIC_DEFAULT_OPUS_MODEL',
152
+ 'ANTHROPIC_DEFAULT_HAIKU_MODEL',
153
+ 'CLAUDE_CODE_SUBAGENT_MODEL',
154
+ ];
155
+
156
+ const TYPE_A_FIELDS = [
157
+ 'CLAUDE_CODE_DISABLE_NONESSENTIAL_TRAFFIC',
158
+ 'CLAUDE_CODE_DISABLE_EXPERIMENTAL_BETAS',
159
+ 'CLAUDE_CODE_DISABLE_NONSTREAMING_FALLBACK',
160
+ ];
161
+
162
+ const TYPE_B_FIELDS = [
163
+ 'CLAUDE_CODE_ATTRIBUTION_HEADER',
164
+ ];
165
+
166
+ const ALL_PREDEFINED_KEYS = new Set([
167
+ ...RESERVED_ENV_KEYS,
168
+ ...PREDEFINED_RUNTIME_KEYS,
169
+ ...PREDEFINED_MODEL_ENV_KEYS,
170
+ ]);
171
+
172
+ function validateEnvKey(key) {
173
+ if (typeof key !== 'string' || key.trim() === '') {
174
+ return { valid: false, error: 'custom_env_key_empty' };
175
+ }
176
+ if (ALL_PREDEFINED_KEYS.has(key.trim())) {
177
+ return { valid: false, error: 'custom_env_key_reserved' };
178
+ }
179
+ return { valid: true, value: key.trim() };
180
+ }
181
+
182
+ function validateTypeATriState(value) {
183
+ if (value === '' || value === '1' || value === 'off') {
184
+ return { valid: true, value };
185
+ }
186
+ return { valid: false, error: 'tri_state_type_a_invalid' };
187
+ }
188
+
189
+ function validateTypeBTriState(value) {
190
+ if (value === '' || value === '1' || value === '0') {
191
+ return { valid: true, value };
192
+ }
193
+ return { valid: false, error: 'tri_state_type_b_invalid' };
194
+ }
195
+
196
+ function validateRuntimeEnvValue(key, value) {
197
+ if (typeof value !== 'string') {
198
+ return { valid: false, error: 'env_value_not_string' };
199
+ }
200
+ if (TYPE_A_FIELDS.includes(key)) return validateTypeATriState(value);
201
+ if (TYPE_B_FIELDS.includes(key)) return validateTypeBTriState(value);
202
+ if (key === 'API_TIMEOUT_MS') {
203
+ if (value === '') return { valid: true, value };
204
+ if (/^\d+$/.test(value) && parseInt(value, 10) > 0) return { valid: true, value };
205
+ return { valid: false, error: 'env_value_timeout_invalid' };
206
+ }
207
+ if (key === 'CLAUDE_CODE_EFFORT_LEVEL') {
208
+ if (value === '') return { valid: true, value };
209
+ if (['low','medium','high','xhigh','max','auto'].includes(value)) return { valid: true, value };
210
+ return { valid: false, error: 'env_value_effort_invalid' };
211
+ }
212
+ return { valid: true, value };
213
+ }
214
+
123
215
  module.exports = {
124
216
  validateBaseUrl,
125
217
  validateAuthToken,
126
218
  validateModel,
127
219
  validateApiName,
128
220
  maskSensitiveData,
129
- maskApiToken
221
+ maskApiToken,
222
+ RESERVED_ENV_KEYS,
223
+ PREDEFINED_RUNTIME_KEYS,
224
+ PREDEFINED_MODEL_ENV_KEYS,
225
+ TYPE_A_FIELDS,
226
+ TYPE_B_FIELDS,
227
+ validateEnvKey,
228
+ validateTypeATriState,
229
+ validateTypeBTriState,
230
+ validateRuntimeEnvValue,
130
231
  };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@kikkimo/claude-launcher",
3
- "version": "2.5.0",
3
+ "version": "3.1.0",
4
4
  "description": "Interactive launcher for Claude Code with beautiful Claude-style interface",
5
5
  "main": "claude-launcher",
6
6
  "bin": {
@@ -8,7 +8,7 @@
8
8
  },
9
9
  "scripts": {
10
10
  "start": "node claude-launcher",
11
- "test": "node test/providers.test.js && node test/menu-hints.test.js",
11
+ "test": "node test/providers.test.js && node test/menu-hints.test.js && node test/version-checker.test.js && node test/api-manager.test.js && node test/launcher.test.js && node test/config-management.test.js && node test/api-select.test.js && node test/env-vars-validators.test.js && node test/env-vars-providers.test.js && node test/env-vars-config.test.js && node test/env-vars-migration.test.js && node test/env-vars-add-api.test.js && node test/env-vars-write-interfaces.test.js && node test/env-vars-import-export.test.js && node test/env-vars-ui.test.js",
12
12
  "prepublishOnly": "echo \"Publishing claude-launcher...\"",
13
13
  "postinstall": "echo \"Claude Launcher installed successfully! Run 'claude-launcher' to start.\"",
14
14
  "publish:public": "npm publish --access public"