@kikkimo/claude-launcher 1.0.0 → 2.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.
@@ -0,0 +1,359 @@
1
+ /**
2
+ * Launcher Module - Handles Claude Code launching with various configurations
3
+ */
4
+
5
+ const { spawn } = require('child_process');
6
+ const colors = require('./ui/colors');
7
+ const i18n = require('./i18n');
8
+ const { getProvider } = require('./presets/providers');
9
+ const stdinManager = require('./utils/stdin-manager');
10
+
11
+ /**
12
+ * Launch Claude Code with specified environment variables
13
+ */
14
+ function launchClaude(command, envVars = {}, disableAuthTokens = false) {
15
+ // Disable Ctrl+C monitoring before launching Claude Code
16
+ // This allows Ctrl+C to be handled exclusively by Claude Code process
17
+ stdinManager.disableCtrlC();
18
+
19
+ console.log('');
20
+ console.log(colors.yellow + '[*] ' + i18n.tSync('launch.starting') + colors.reset);
21
+ console.log(colors.gray + i18n.tSync('launch.command', command) + colors.reset);
22
+
23
+ if (Object.keys(envVars).length > 0) {
24
+ console.log(colors.gray + i18n.tSync('launch.environment_variables') + colors.reset);
25
+ // Mask sensitive environment variables based on key name patterns
26
+ const secretKeyRe = /(token|key|secret|pass|auth|credential)/i;
27
+ for (const [key, value] of Object.entries(envVars)) {
28
+ const masked = secretKeyRe.test(key) ? '***' : String(value);
29
+ console.log(colors.gray + ' ' + key + '=' + masked + colors.reset);
30
+ }
31
+ }
32
+
33
+ console.log('');
34
+ console.log(colors.green + '[>] ' + i18n.tSync('launch.run_in_terminal') + colors.reset);
35
+ console.log(colors.gray + ' ' + i18n.tSync('launch.launcher_exit') + colors.reset);
36
+ console.log('');
37
+
38
+ // Prepare clean environment
39
+ const env = { ...process.env, ...envVars };
40
+
41
+ // Disable conflicting auth tokens when using third-party API
42
+ if (disableAuthTokens) {
43
+ // Only delete CLAUDE_CODE_OAUTH_TOKEN - keep ANTHROPIC_AUTH_TOKEN that we just set
44
+ delete env.CLAUDE_CODE_OAUTH_TOKEN;
45
+ console.log(colors.gray + ' Disabled: CLAUDE_CODE_OAUTH_TOKEN' + colors.reset);
46
+ }
47
+
48
+ // Parse command and arguments
49
+ const args = command.split(' ');
50
+ const cmd = args.shift();
51
+
52
+ let consoleRelinquished = false;
53
+ const relinquishConsoleToChild = () => {
54
+ if (consoleRelinquished) return;
55
+ consoleRelinquished = true;
56
+ // Do the minimal changes: switch to cooked mode and detach current scope, but do not pause stdin nor swallow signals
57
+ try {
58
+ if (process.stdin.isTTY) {
59
+ process.stdin.setRawMode(false);
60
+ }
61
+ } catch (_) {}
62
+
63
+ // Detach only current scope listeners to avoid affecting other modules
64
+ if (stdinManager.activeScope && typeof stdinManager.activeScope.detach === 'function') {
65
+ stdinManager.activeScope.detach();
66
+ }
67
+
68
+ // Suspend stdin manager so no new listeners are attached while Claude is running
69
+ if (typeof stdinManager.suspend === 'function') {
70
+ stdinManager.suspend();
71
+ }
72
+ };
73
+
74
+ const restoreConsoleForLauncher = () => {
75
+ if (!consoleRelinquished) return;
76
+ consoleRelinquished = false;
77
+ if (typeof stdinManager.resume === 'function') {
78
+ stdinManager.resume();
79
+ }
80
+ stdinManager.enableCtrlC();
81
+ };
82
+
83
+ const handleLaunchFailure = (message, opts = {}) => {
84
+ if (opts.afterHandover) {
85
+ restoreConsoleForLauncher();
86
+ } else {
87
+ stdinManager.enableCtrlC();
88
+ }
89
+
90
+ console.log(colors.red + '[x] ' + message + colors.reset);
91
+ console.log(colors.gray + i18n.tSync('ui.general.press_key_return_menu') + colors.reset);
92
+
93
+ if (process.stdin.isTTY) {
94
+ try {
95
+ process.stdin.setRawMode(true);
96
+ process.stdin.resume();
97
+ } catch (_) {
98
+ // Ignore setup failures
99
+ }
100
+
101
+ // Set timeout to prevent infinite hanging
102
+ const timeoutId = setTimeout(() => {
103
+ try {
104
+ process.stdin.setRawMode(false);
105
+ } catch (_) {
106
+ // Ignore cleanup failures
107
+ }
108
+ process.exit(1);
109
+ }, 60000); // 60 second timeout
110
+
111
+ process.stdin.once('data', () => {
112
+ clearTimeout(timeoutId);
113
+ try {
114
+ process.stdin.setRawMode(false);
115
+ } catch (_) {
116
+ // Ignore cleanup failures
117
+ }
118
+ // Exit after user acknowledges the error
119
+ process.exit(1);
120
+ });
121
+ } else {
122
+ // For non-TTY environments, exit immediately
123
+ process.exit(1);
124
+ }
125
+ };
126
+
127
+ try {
128
+ // Clean up terminal state before launching Claude
129
+ if (process.stdin.isTTY) {
130
+ try {
131
+ process.stdin.setRawMode(false);
132
+ process.stdin.pause();
133
+ } catch (_) {
134
+ // Ignore cleanup failures
135
+ }
136
+ }
137
+
138
+ // Note: stdin listener management is handled by relinquishConsoleToChild()
139
+ // using stdinManager.activeScope.detach() and stdinManager.suspend().
140
+ // This ensures only the current scope's listeners are detached while
141
+ // preserving any listeners from other modules.
142
+ //
143
+ // Note: Do NOT remove global SIGINT/SIGTERM handlers here.
144
+ // The existing handlers already check stdinManager.isSuspended() and
145
+ // will properly ignore signals during child process execution.
146
+ // Removing all handlers would break other modules and degrade reliability.
147
+
148
+ // Launch Claude in current terminal, inherit stdio
149
+ const child = spawn(cmd, args, {
150
+ stdio: 'inherit',
151
+ env: env,
152
+ cwd: process.cwd(),
153
+ shell: true
154
+ });
155
+
156
+ relinquishConsoleToChild();
157
+
158
+ child.on('close', (code) => {
159
+ restoreConsoleForLauncher();
160
+ process.exit(code || 0);
161
+ });
162
+
163
+ child.on('error', (error) => {
164
+ handleLaunchFailure('Error running Claude: ' + error.message, { afterHandover: true });
165
+ });
166
+
167
+ } catch (error) {
168
+ handleLaunchFailure('Error launching Claude Code: ' + error.message);
169
+ }
170
+ }
171
+
172
+ /**
173
+ * Launch Claude with default settings
174
+ */
175
+ function launchClaudeDefault() {
176
+ launchClaude('claude');
177
+ }
178
+
179
+ /**
180
+ * Launch Claude with skip permissions flag
181
+ */
182
+ function launchClaudeSkipPermissions() {
183
+ launchClaude('claude --dangerously-skip-permissions');
184
+ }
185
+
186
+ /**
187
+ * Get environment variables based on provider type
188
+ */
189
+ function getProviderEnvVars(api) {
190
+ // Decrypt the auth token (all tokens are stored encrypted)
191
+ const { decrypt } = require('./crypto');
192
+ const decrypted = decrypt(api.authToken);
193
+
194
+ if (!decrypted.success) {
195
+ console.error('Failed to decrypt auth token:', decrypted.error);
196
+ throw new Error('Failed to decrypt API auth token. Please check your configuration.');
197
+ }
198
+
199
+ const authToken = decrypted.value;
200
+
201
+ const baseEnvVars = {
202
+ ANTHROPIC_BASE_URL: api.baseUrl,
203
+ ANTHROPIC_AUTH_TOKEN: authToken,
204
+ ANTHROPIC_MODEL: api.model,
205
+ ANTHROPIC_SMALL_FAST_MODEL: api.smallFastModel || api.model
206
+ };
207
+
208
+ // Get provider configuration and merge provider-specific environment variables
209
+ const providerConfig = getProvider(api.provider);
210
+
211
+ if (providerConfig && providerConfig.envVars) {
212
+ // Merge base env vars with provider-specific env vars
213
+ return {
214
+ ...baseEnvVars,
215
+ ...providerConfig.envVars
216
+ };
217
+ }
218
+
219
+ return baseEnvVars;
220
+ }
221
+
222
+ /**
223
+ * Launch Claude with third-party API configuration
224
+ */
225
+ function launchClaudeWithApi(api, skipPermissions = false) {
226
+ const command = skipPermissions
227
+ ? 'claude --dangerously-skip-permissions'
228
+ : 'claude';
229
+
230
+ const envVars = getProviderEnvVars(api);
231
+
232
+ console.log('');
233
+ console.log(colors.bright + colors.orange + '🔗 ' + i18n.tSync('launch.using_third_party_api') + colors.reset);
234
+
235
+ // Get provider configuration for display
236
+ const providerConfig = getProvider(api.provider);
237
+ const providerName = providerConfig ? providerConfig.name : (api.provider || 'Custom');
238
+
239
+ console.log(colors.gray + ` Provider: ${providerName}` + colors.reset);
240
+ console.log(colors.gray + ` API: ${api.name}` + colors.reset);
241
+ console.log(colors.gray + ` Base URL: ${api.baseUrl}` + colors.reset);
242
+ console.log(colors.gray + ` Model: ${api.model}` + colors.reset);
243
+
244
+ // Show provider-specific optimizations if envVars are defined
245
+ if (providerConfig && providerConfig.envVars && Object.keys(providerConfig.envVars).length > 0) {
246
+ console.log(colors.yellow + ' ⚡ ' + i18n.tSync('launch.provider_optimizations_applied') + colors.reset);
247
+
248
+ // Display specific optimizations based on envVars
249
+ const msRaw = providerConfig.envVars.API_TIMEOUT_MS;
250
+ const ms = Number(msRaw);
251
+ if (Number.isFinite(ms) && ms > 0) {
252
+ const timeoutSec = Math.floor(ms / 1000);
253
+ const timeoutMin = Math.floor(timeoutSec / 60);
254
+ // Use singular or plural form based on timeoutMin value
255
+ const key = timeoutMin === 1 ? 'launch.extended_timeout_format_singular' : 'launch.extended_timeout_format';
256
+ console.log(colors.gray + ' • ' + i18n.tSync(key, timeoutSec, timeoutMin) + colors.reset);
257
+ }
258
+
259
+ if (providerConfig.envVars.CLAUDE_CODE_DISABLE_NONESSENTIAL_TRAFFIC) {
260
+ console.log(colors.gray + ' • ' + i18n.tSync('launch.non_essential_traffic_disabled') + colors.reset);
261
+ }
262
+
263
+ // Display any other custom env vars (excluding the ones already shown)
264
+ // Apply same masking logic as in launchClaude to protect sensitive values
265
+ const secretKeyRe = /(token|key|secret|pass|auth|credential)/i;
266
+ for (const [key, value] of Object.entries(providerConfig.envVars)) {
267
+ if (key === 'API_TIMEOUT_MS' || key === 'CLAUDE_CODE_DISABLE_NONESSENTIAL_TRAFFIC') continue;
268
+ const masked = secretKeyRe.test(key) ? '***' : String(value);
269
+ console.log(colors.gray + ' • ' + i18n.tSync('launch.custom_env_var', key, masked) + colors.reset);
270
+ }
271
+ }
272
+
273
+ console.log('');
274
+
275
+ launchClaude(command, envVars, true);
276
+ }
277
+
278
+ /**
279
+ * Test API connection
280
+ */
281
+ async function testApiConnection(api) {
282
+ console.log(colors.yellow + '🔍 Testing API connection...' + colors.reset);
283
+
284
+ try {
285
+ // Handle both plaintext (during testing) and encrypted tokens (from stored APIs)
286
+ let authToken = api.authToken;
287
+
288
+ // Try to decrypt if it looks like an encrypted token
289
+ if (typeof authToken === 'string' && authToken.includes(':')) {
290
+ const { decrypt } = require('./crypto');
291
+ const decrypted = decrypt(authToken);
292
+
293
+ if (decrypted.success) {
294
+ authToken = decrypted.value;
295
+ }
296
+ // If decryption fails but token contains ':', it might be encrypted but corrupted
297
+ else if (authToken.split(':').length === 3) {
298
+ console.error('Failed to decrypt auth token for testing:', decrypted.error);
299
+ return { success: false, error: 'Failed to decrypt auth token' };
300
+ }
301
+ // Otherwise, treat as plaintext token
302
+ }
303
+
304
+ // Try to make a simple request to test the connection
305
+ const https = require('https');
306
+ const url = new URL(api.baseUrl);
307
+
308
+ return new Promise((resolve) => {
309
+ const options = {
310
+ hostname: url.hostname,
311
+ port: url.port || 443,
312
+ path: url.pathname,
313
+ method: 'GET',
314
+ timeout: 5000,
315
+ headers: {
316
+ 'Authorization': `Bearer ${authToken}`
317
+ }
318
+ };
319
+
320
+ const req = https.request(options, (res) => {
321
+ if (res.statusCode === 401) {
322
+ console.log(colors.yellow + '⚠️ API returned 401 - Check your auth token' + colors.reset);
323
+ resolve({ success: false, error: 'Authentication failed' });
324
+ } else if (res.statusCode >= 200 && res.statusCode < 500) {
325
+ console.log(colors.green + '✓ API is reachable' + colors.reset);
326
+ resolve({ success: true });
327
+ } else {
328
+ console.log(colors.red + `❌ API returned status ${res.statusCode}` + colors.reset);
329
+ resolve({ success: false, error: `HTTP ${res.statusCode}` });
330
+ }
331
+ });
332
+
333
+ req.on('error', (error) => {
334
+ console.log(colors.red + `❌ Connection failed: ${error.message}` + colors.reset);
335
+ resolve({ success: false, error: error.message });
336
+ });
337
+
338
+ req.on('timeout', () => {
339
+ console.log(colors.red + '❌ Connection timeout' + colors.reset);
340
+ req.destroy();
341
+ resolve({ success: false, error: 'Timeout' });
342
+ });
343
+
344
+ req.end();
345
+ });
346
+ } catch (error) {
347
+ console.log(colors.red + `❌ Test failed: ${error.message}` + colors.reset);
348
+ return { success: false, error: error.message };
349
+ }
350
+ }
351
+
352
+ module.exports = {
353
+ launchClaude,
354
+ launchClaudeDefault,
355
+ launchClaudeSkipPermissions,
356
+ launchClaudeWithApi,
357
+ getProviderEnvVars,
358
+ testApiConnection
359
+ };
@@ -0,0 +1,148 @@
1
+ /**
2
+ * API Providers Presets - Claude Code compatible API providers
3
+ *
4
+ * Note: Only includes APIs that are compatible with Claude Code's Anthropic API format
5
+ */
6
+
7
+ const providers = {
8
+ anthropic: {
9
+ name: 'Anthropic (Official)',
10
+ baseUrl: 'https://api.anthropic.com',
11
+ models: [
12
+ 'claude-3-5-haiku-20241022',
13
+ 'claude-3.7-sonnet',
14
+ 'claude-sonnet-4',
15
+ 'claude-opus-4',
16
+ 'claude-opus-4.1'
17
+ ],
18
+ authTokenFormat: 'sk-ant-api03-...',
19
+ description: 'Official Anthropic API - Fully compatible',
20
+ requiresToken: true,
21
+ compatibility: 'native'
22
+ },
23
+ moonshot: {
24
+ name: 'Moonshot AI (Kimi-K2)',
25
+ baseUrl: 'https://api.moonshot.cn/anthropic',
26
+ models: [
27
+ 'kimi-k2-0711-preview',
28
+ 'kimi-k2-0905-preview',
29
+ 'kimi-k2-turbo-preview'
30
+ ],
31
+ authTokenFormat: 'sk-...',
32
+ description: 'Moonshot AI - Provides Anthropic-compatible API',
33
+ requiresToken: true,
34
+ compatibility: 'anthropic-compatible',
35
+ envVars: {
36
+ API_TIMEOUT_MS: '3000000',
37
+ CLAUDE_CODE_DISABLE_NONESSENTIAL_TRAFFIC: '1'
38
+ },
39
+ note: 'Requires extended timeout for large responses'
40
+ },
41
+ deepseek: {
42
+ name: 'DeepSeek (DeepSeek V3/V3.1)',
43
+ baseUrl: 'https://api.deepseek.com/anthropic',
44
+ models: [
45
+ 'deepseek-chat'
46
+ ],
47
+ authTokenFormat: 'sk-...',
48
+ description: 'DeepSeek AI - Anthropic-compatible endpoint',
49
+ requiresToken: true,
50
+ compatibility: 'anthropic-compatible',
51
+ envVars: {
52
+ API_TIMEOUT_MS: '600000',
53
+ CLAUDE_CODE_DISABLE_NONESSENTIAL_TRAFFIC: '1'
54
+ },
55
+ note: 'Requires extended timeout for complex reasoning tasks'
56
+ },
57
+ zhipu: {
58
+ name: 'ZhiPu AI (GLM-4.5/4.6) - 智谱清言',
59
+ baseUrl: 'https://open.bigmodel.cn/api/anthropic',
60
+ models: [
61
+ 'glm-4.5',
62
+ 'glm-4.6'
63
+ ],
64
+ authTokenFormat: 'sk-...',
65
+ description: 'ZhiPu AI (智谱清言) - Anthropic-compatible API for mainland China',
66
+ requiresToken: true,
67
+ compatibility: 'anthropic-compatible',
68
+ envVars: {
69
+ API_TIMEOUT_MS: '3000000',
70
+ CLAUDE_CODE_DISABLE_NONESSENTIAL_TRAFFIC: '1'
71
+ },
72
+ note: 'Requires extended timeout for large responses'
73
+ },
74
+ zai: {
75
+ name: 'Z.ai (GLM-4.5/4.6) - ZhiPu Global',
76
+ baseUrl: 'https://api.z.ai/api/anthropic',
77
+ models: [
78
+ 'glm-4.5',
79
+ 'glm-4.6'
80
+ ],
81
+ authTokenFormat: 'sk-...',
82
+ description: 'Z.ai (ZhiPu AI Global) - Anthropic-compatible API for international users',
83
+ requiresToken: true,
84
+ compatibility: 'anthropic-compatible',
85
+ envVars: {
86
+ API_TIMEOUT_MS: '3000000',
87
+ CLAUDE_CODE_DISABLE_NONESSENTIAL_TRAFFIC: '1'
88
+ },
89
+ note: 'Requires extended timeout for large responses'
90
+ },
91
+ custom: {
92
+ name: 'Custom Anthropic-Compatible API',
93
+ baseUrl: 'https://your-api-server.com/v1/anthropic',
94
+ models: [
95
+ 'your-model-name'
96
+ ],
97
+ authTokenFormat: 'Bearer token or API key',
98
+ description: 'Custom server with Anthropic-compatible API',
99
+ requiresToken: true,
100
+ compatibility: 'anthropic-compatible',
101
+ note: 'Replace URL and model with your actual server details'
102
+ }
103
+ };
104
+
105
+ /**
106
+ * Get all available providers
107
+ */
108
+ function getAllProviders() {
109
+ return Object.keys(providers).map(key => ({
110
+ id: key,
111
+ ...providers[key]
112
+ }));
113
+ }
114
+
115
+ /**
116
+ * Get a specific provider by ID
117
+ */
118
+ function getProvider(providerId) {
119
+ return providers[providerId] || null;
120
+ }
121
+
122
+ /**
123
+ * Get suggested models for a provider
124
+ */
125
+ function getSuggestedModels(providerId) {
126
+ const provider = providers[providerId];
127
+ return provider ? provider.models : [];
128
+ }
129
+
130
+ /**
131
+ * Validate if a URL matches a known provider
132
+ */
133
+ function detectProvider(baseUrl) {
134
+ for (const [key, provider] of Object.entries(providers)) {
135
+ if (baseUrl.includes(provider.baseUrl.replace('https://', '').replace('http://', '').split('/')[0])) {
136
+ return key;
137
+ }
138
+ }
139
+ return 'custom';
140
+ }
141
+
142
+ module.exports = {
143
+ providers,
144
+ getAllProviders,
145
+ getProvider,
146
+ getSuggestedModels,
147
+ detectProvider
148
+ };
@@ -0,0 +1,32 @@
1
+ /**
2
+ * Colors Module - ANSI color codes for Claude-style theming
3
+ */
4
+
5
+ const colors = {
6
+ reset: '\x1b[0m',
7
+ bright: '\x1b[1m',
8
+ dim: '\x1b[2m',
9
+
10
+ // Claude theme colors
11
+ orange: '\x1b[38;5;208m', // Claude brand orange
12
+ amber: '\x1b[38;5;214m', // Amber/yellow-orange
13
+
14
+ // Standard colors
15
+ white: '\x1b[37m',
16
+ gray: '\x1b[90m',
17
+ green: '\x1b[32m',
18
+ red: '\x1b[31m',
19
+ yellow: '\x1b[33m',
20
+ blue: '\x1b[34m',
21
+ cyan: '\x1b[36m',
22
+ black: '\x1b[30m',
23
+
24
+ // Background colors
25
+ bgOrange: '\x1b[48;5;208m', // Background orange
26
+ bgAmber: '\x1b[48;5;214m', // Background amber
27
+ bgRed: '\x1b[41m',
28
+ bgGreen: '\x1b[42m',
29
+ bgBlue: '\x1b[44m'
30
+ };
31
+
32
+ module.exports = colors;