@halilertekin/claude-code-router-config 1.1.1

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.
package/install.js ADDED
@@ -0,0 +1,160 @@
1
+ #!/usr/bin/env node
2
+
3
+ /**
4
+ * Claude Code Router Config - Interactive Installer
5
+ * For use with @musistudio/claude-code-router
6
+ *
7
+ * Original project: https://github.com/musistudio/claude-code-router
8
+ * Configuration by Halil Ertekin
9
+ */
10
+
11
+ const fs = require('fs-extra');
12
+ const path = require('path');
13
+ const chalk = require('chalk');
14
+ const inquirer = require('inquirer');
15
+ const dotenv = require('dotenv');
16
+ const { execSync } = require('child_process');
17
+
18
+ const configDir = path.join(process.env.HOME || process.env.USERPROFILE, '.claude-code-router');
19
+ const packageDir = __dirname;
20
+
21
+ async function checkRequirements() {
22
+ console.log(chalk.blue('📋 Checking requirements...'));
23
+
24
+ // Check Node version
25
+ const nodeVersion = process.version;
26
+ const majorVersion = parseInt(nodeVersion.slice(1).split('.')[0]);
27
+
28
+ if (majorVersion < 16) {
29
+ console.error(chalk.red(`❌ Node.js ${majorVersion} detected. Node.js 16+ required.`));
30
+ process.exit(1);
31
+ }
32
+ console.log(chalk.green(`✅ Node.js ${nodeVersion}`));
33
+
34
+ // Check for pnpm
35
+ try {
36
+ execSync('pnpm --version', { stdio: 'ignore' });
37
+ console.log(chalk.green('✅ pnpm found'));
38
+ return 'pnpm';
39
+ } catch {
40
+ try {
41
+ execSync('npm --version', { stdio: 'ignore' });
42
+ console.log(chalk.yellow('⚠️ pnpm not found, using npm'));
43
+ return 'npm';
44
+ } catch {
45
+ console.error(chalk.red('❌ Neither pnpm nor npm found'));
46
+ process.exit(1);
47
+ }
48
+ }
49
+ }
50
+
51
+ async function installRouter(packageManager) {
52
+ console.log(chalk.blue('📦 Installing claude-code-router...'));
53
+
54
+ try {
55
+ const command = `${packageManager} add -g @musistudio/claude-code-router`;
56
+ console.log(chalk.gray(`Running: ${command}`));
57
+ execSync(command, { stdio: 'inherit' });
58
+ console.log(chalk.green('✅ claude-code-router installed'));
59
+ } catch (error) {
60
+ console.error(chalk.red('❌ Failed to install claude-code-router'));
61
+ console.error(error.message);
62
+ process.exit(1);
63
+ }
64
+ }
65
+
66
+ async function setupConfig() {
67
+ console.log(chalk.blue('⚙️ Setting up configuration...'));
68
+
69
+ // Ensure config directory exists
70
+ await fs.ensureDir(configDir);
71
+
72
+ // Copy config files
73
+ const configFiles = ['config.json', 'intent-router.js'];
74
+ for (const file of configFiles) {
75
+ const src = path.join(packageDir, 'config', file);
76
+ const dest = path.join(configDir, file);
77
+
78
+ if (await fs.pathExists(dest)) {
79
+ const { overwrite } = await inquirer.prompt([
80
+ {
81
+ type: 'confirm',
82
+ name: 'overwrite',
83
+ message: `File ${file} exists. Overwrite?`,
84
+ default: false
85
+ }
86
+ ]);
87
+
88
+ if (!overwrite) {
89
+ console.log(chalk.yellow(`⚠️ Skipping ${file}`));
90
+ continue;
91
+ }
92
+ }
93
+
94
+ await fs.copy(src, dest);
95
+ console.log(chalk.green(`✅ ${file} copied`));
96
+ }
97
+
98
+ // Copy .env.example if .env doesn't exist
99
+ const envFile = path.join(process.env.HOME || process.env.USERPROFILE, '.env');
100
+ const envExample = path.join(packageDir, '.env.example');
101
+
102
+ if (!(await fs.pathExists(envFile))) {
103
+ await fs.copy(envExample, envFile);
104
+ console.log(chalk.green('✅ .env file created from example'));
105
+ } else {
106
+ console.log(chalk.yellow('⚠️ .env file already exists'));
107
+ }
108
+ }
109
+
110
+ async function showNextSteps() {
111
+ console.log(chalk.green('\n🎉 Installation complete!'));
112
+ console.log(chalk.blue('\n📝 Next steps:'));
113
+ console.log('\n1. Edit your API keys in ~/.env file:');
114
+ console.log(chalk.gray(' nano ~/.env'));
115
+
116
+ console.log('\n2. Add environment variables to your shell (~/.zshrc or ~/.bashrc):');
117
+ console.log(chalk.cyan(`
118
+ # Claude Code Router
119
+ export $(cat ~/.env | xargs)
120
+ export ANTHROPIC_BASE_URL="http://127.0.0.1:3456"
121
+ export NO_PROXY="127.0.0.1"
122
+ `));
123
+
124
+ console.log('\n3. Reload your shell:');
125
+ console.log(chalk.gray(' source ~/.zshrc'));
126
+
127
+ console.log('\n4. Start the router:');
128
+ console.log(chalk.gray(' ccr code'));
129
+
130
+ console.log(chalk.blue('\n📚 Documentation:'));
131
+ console.log(chalk.gray(' https://github.com/halilertekin/claude-code-router-config'));
132
+
133
+ console.log(chalk.blue('\n🔑 Get API keys:'));
134
+ console.log(chalk.gray(' OpenAI: https://platform.openai.com/api-keys'));
135
+ console.log(chalk.gray(' Anthropic: https://console.anthropic.com/settings/keys'));
136
+ console.log(chalk.gray(' Gemini: https://aistudio.google.com/apikey'));
137
+ console.log(chalk.gray(' Qwen: https://dashscope.console.aliyun.com/apiKey'));
138
+ console.log(chalk.gray(' GLM: https://open.bigmodel.cn/usercenter/apikeys'));
139
+ console.log(chalk.gray(' OpenRouter: https://openrouter.ai/keys'));
140
+ console.log(chalk.gray(' Copilot: https://github.com/settings/tokens'));
141
+
142
+ console.log(chalk.yellow('\n⭐ Attribution:'));
143
+ console.log(chalk.gray(' This config is for @musistudio/claude-code-router'));
144
+ console.log(chalk.gray(' Original: https://github.com/musistudio/claude-code-router'));
145
+ }
146
+
147
+ async function main() {
148
+ console.log(chalk.cyan.bold('\n🚀 Claude Code Router Config Installer\n'));
149
+
150
+ const packageManager = await checkRequirements();
151
+ await installRouter(packageManager);
152
+ await setupConfig();
153
+ await showNextSteps();
154
+ }
155
+
156
+ if (require.main === module) {
157
+ main().catch(console.error);
158
+ }
159
+
160
+ module.exports = { checkRequirements, installRouter, setupConfig };
package/install.sh ADDED
@@ -0,0 +1,73 @@
1
+ #!/bin/bash
2
+
3
+ # Claude Code Router - Install Script
4
+ # Usage: ./install.sh
5
+
6
+ set -e
7
+
8
+ echo "=========================================="
9
+ echo " Claude Code Router - Kurulum"
10
+ echo "=========================================="
11
+ echo ""
12
+
13
+ # Colors
14
+ RED='\033[0;31m'
15
+ GREEN='\033[0;32m'
16
+ YELLOW='\033[1;33m'
17
+ NC='\033[0m' # No Color
18
+
19
+ # Check for pnpm
20
+ if ! command -v pnpm &> /dev/null; then
21
+ echo -e "${YELLOW}pnpm bulunamadı. Kuruluyor...${NC}"
22
+ npm install -g pnpm
23
+ fi
24
+
25
+ # Install claude-code-router
26
+ echo -e "${GREEN}[1/3] Claude Code Router kuruluyor...${NC}"
27
+ pnpm add -g @musistudio/claude-code-router
28
+
29
+ # Create config directory
30
+ echo -e "${GREEN}[2/3] Config dizini oluşturuluyor...${NC}"
31
+ mkdir -p ~/.claude-code-router
32
+
33
+ # Copy config files
34
+ echo -e "${GREEN}[3/3] Config dosyaları kopyalanıyor...${NC}"
35
+ SCRIPT_DIR="$( cd "$( dirname "${BASH_SOURCE[0]}" )" && pwd )"
36
+ cp "$SCRIPT_DIR/config/config.json" ~/.claude-code-router/
37
+ cp "$SCRIPT_DIR/config/intent-router.js" ~/.claude-code-router/
38
+
39
+ echo ""
40
+ echo -e "${GREEN}=========================================="
41
+ echo " Kurulum Tamamlandı!"
42
+ echo "==========================================${NC}"
43
+ echo ""
44
+ echo -e "${YELLOW}SON ADIM: ~/.zshrc dosyanıza aşağıdaki satırları ekleyin:${NC}"
45
+ echo ""
46
+ cat << 'EOF'
47
+ # ═══════════════════════════════════════════════════
48
+ # Claude Code Router - API Keys
49
+ # ═══════════════════════════════════════════════════
50
+ export OPENAI_API_KEY="sk-..."
51
+ export ANTHROPIC_API_KEY="sk-ant-..."
52
+ export GEMINI_API_KEY="AIza..."
53
+ export QWEN_API_KEY="sk-..."
54
+ export GLM_API_KEY="..."
55
+ export OPENROUTER_API_KEY="sk-or-..."
56
+
57
+ # Router Connection
58
+ export ANTHROPIC_BASE_URL="http://127.0.0.1:3456"
59
+ export NO_PROXY="127.0.0.1"
60
+ EOF
61
+
62
+ echo ""
63
+ echo -e "${GREEN}Sonra:${NC}"
64
+ echo " source ~/.zshrc"
65
+ echo " ccr code"
66
+ echo ""
67
+ echo -e "${YELLOW}API Key Alma Linkleri:${NC}"
68
+ echo " OpenAI: https://platform.openai.com/api-keys"
69
+ echo " Anthropic: https://console.anthropic.com/settings/keys"
70
+ echo " Gemini: https://aistudio.google.com/apikey"
71
+ echo " Qwen: https://dashscope.console.aliyun.com/apiKey"
72
+ echo " GLM: https://open.bigmodel.cn/usercenter/apikeys"
73
+ echo " OpenRouter: https://openrouter.ai/keys"
@@ -0,0 +1,410 @@
1
+ const fs = require('fs');
2
+ const path = require('path');
3
+ const os = require('os');
4
+ const chalk = require('chalk');
5
+ const { spawn } = require('child_process');
6
+
7
+ class EnhancedLogger {
8
+ constructor(options = {}) {
9
+ this.logDir = options.logDir || path.join(os.homedir(), '.claude-code-router', 'logs');
10
+ this.level = options.level || 'info';
11
+ this.enableConsole = options.enableConsole !== false;
12
+ this.enableFile = options.enableFile !== false;
13
+ this.enableMetrics = options.enableMetrics !== false;
14
+ this.enableAnalytics = options.enableAnalytics !== false;
15
+ this.maxFileSize = options.maxFileSize || 10 * 1024 * 1024; // 10MB
16
+ this.maxFiles = options.maxFiles || 5;
17
+
18
+ // Initialize log directory
19
+ this.initLogDirectory();
20
+
21
+ // Current date for log rotation
22
+ this.currentDate = new Date().toISOString().split('T')[0];
23
+ this.logFile = path.join(this.logDir, `claude-router-${this.currentDate}.log`);
24
+ this.metricsFile = path.join(this.logDir, 'metrics.json');
25
+ this.errorFile = path.join(this.logDir, 'errors.log');
26
+ }
27
+
28
+ initLogDirectory() {
29
+ if (!fs.existsSync(this.logDir)) {
30
+ fs.mkdirSync(this.logDir, { recursive: true });
31
+ }
32
+ }
33
+
34
+ // Log levels with numeric values for filtering
35
+ static levels = {
36
+ fatal: 0,
37
+ error: 1,
38
+ warn: 2,
39
+ info: 3,
40
+ debug: 4,
41
+ trace: 5
42
+ };
43
+
44
+ // Check if we should log at this level
45
+ shouldLog(level) {
46
+ return EnhancedLogger.levels[level] <= EnhancedLogger.levels[this.level];
47
+ }
48
+
49
+ // Format log entry
50
+ formatEntry(level, message, meta = {}) {
51
+ const timestamp = new Date().toISOString();
52
+ const pid = process.pid;
53
+ const entry = {
54
+ timestamp,
55
+ level: level.toUpperCase(),
56
+ pid,
57
+ message,
58
+ ...meta
59
+ };
60
+
61
+ // Format for console
62
+ const consoleMessage = `[${timestamp}] ${level.toUpperCase()} ${message}`;
63
+
64
+ return { entry, consoleMessage };
65
+ }
66
+
67
+ // Write to file with rotation
68
+ writeToFile(entry) {
69
+ if (!this.enableFile) return;
70
+
71
+ try {
72
+ // Check if we need to rotate log file
73
+ if (fs.existsSync(this.logFile)) {
74
+ const stats = fs.statSync(this.logFile);
75
+ if (stats.size > this.maxFileSize) {
76
+ this.rotateLog();
77
+ }
78
+ }
79
+
80
+ const logLine = JSON.stringify(entry) + '\n';
81
+ fs.appendFileSync(this.logFile, logLine);
82
+ } catch (error) {
83
+ console.error('Failed to write to log file:', error);
84
+ }
85
+ }
86
+
87
+ // Log rotation
88
+ rotateLog() {
89
+ const baseName = path.join(this.logDir, `claude-router-${this.currentDate}`);
90
+
91
+ // Rotate existing files
92
+ for (let i = this.maxFiles - 1; i >= 1; i--) {
93
+ const oldFile = `${baseName}.${i}.log`;
94
+ const newFile = `${baseName}.${i + 1}.log`;
95
+
96
+ if (fs.existsSync(oldFile)) {
97
+ if (i === this.maxFiles - 1) {
98
+ fs.unlinkSync(oldFile); // Delete oldest
99
+ } else {
100
+ fs.renameSync(oldFile, newFile);
101
+ }
102
+ }
103
+ }
104
+
105
+ // Move current log to .1
106
+ if (fs.existsSync(this.logFile)) {
107
+ fs.renameSync(this.logFile, `${baseName}.1.log`);
108
+ }
109
+ }
110
+
111
+ // Log to console with colors
112
+ writeToConsole(level, consoleMessage) {
113
+ if (!this.enableConsole) return;
114
+
115
+ let coloredMessage;
116
+ switch (level) {
117
+ case 'fatal':
118
+ coloredMessage = chalk.red.bold(consoleMessage);
119
+ break;
120
+ case 'error':
121
+ coloredMessage = chalk.red(consoleMessage);
122
+ break;
123
+ case 'warn':
124
+ coloredMessage = chalk.yellow(consoleMessage);
125
+ break;
126
+ case 'info':
127
+ coloredMessage = chalk.blue(consoleMessage);
128
+ break;
129
+ case 'debug':
130
+ coloredMessage = chalk.magenta(consoleMessage);
131
+ break;
132
+ case 'trace':
133
+ coloredMessage = chalk.gray(consoleMessage);
134
+ break;
135
+ default:
136
+ coloredMessage = consoleMessage;
137
+ }
138
+
139
+ console.log(coloredMessage);
140
+ }
141
+
142
+ // Log entry method
143
+ log(level, message, meta = {}) {
144
+ if (!this.shouldLog(level)) return;
145
+
146
+ const { entry, consoleMessage } = this.formatEntry(level, message, meta);
147
+
148
+ // Write to console
149
+ this.writeToConsole(level, consoleMessage);
150
+
151
+ // Write to file
152
+ this.writeToFile(entry);
153
+
154
+ // Handle errors specially
155
+ if (level === 'error' || level === 'fatal') {
156
+ this.logError(entry);
157
+ }
158
+
159
+ // Update metrics
160
+ if (this.enableMetrics) {
161
+ this.updateMetrics(level, meta);
162
+ }
163
+
164
+ // Send to analytics if enabled
165
+ if (this.enableAnalytics && meta.provider && meta.model) {
166
+ this.sendToAnalytics(entry);
167
+ }
168
+ }
169
+
170
+ // Log errors to separate file
171
+ logError(entry) {
172
+ try {
173
+ const errorLine = JSON.stringify(entry) + '\n';
174
+ fs.appendFileSync(this.errorFile, errorLine);
175
+ } catch (error) {
176
+ console.error('Failed to write to error log:', error);
177
+ }
178
+ }
179
+
180
+ // Update metrics
181
+ updateMetrics(level, meta) {
182
+ try {
183
+ let metrics = {};
184
+
185
+ if (fs.existsSync(this.metricsFile)) {
186
+ metrics = JSON.parse(fs.readFileSync(this.metricsFile, 'utf8'));
187
+ }
188
+
189
+ const now = new Date().toISOString();
190
+
191
+ // Initialize metrics structure
192
+ if (!metrics.requests) metrics.requests = { total: 0, byLevel: {}, byProvider: {} };
193
+ if (!metrics.latency) metrics.latency = { avg: 0, min: Infinity, max: 0, samples: [] };
194
+ if (!metrics.errors) metrics.errors = { total: 0, byProvider: {} };
195
+ if (!metrics.costs) metrics.costs = { total: 0, byProvider: {} };
196
+ if (!metrics.uptime) metrics.uptime = { start: metrics.uptime?.start || now, lastActivity: now };
197
+
198
+ // Update request counts
199
+ metrics.requests.total++;
200
+ metrics.requests.byLevel[level] = (metrics.requests.byLevel[level] || 0) + 1;
201
+
202
+ if (meta.provider) {
203
+ metrics.requests.byProvider[meta.provider] = (metrics.requests.byProvider[meta.provider] || 0) + 1;
204
+ }
205
+
206
+ // Update latency
207
+ if (meta.latency) {
208
+ metrics.latency.samples.push(meta.latency);
209
+ metrics.latency.min = Math.min(metrics.latency.min, meta.latency);
210
+ metrics.latency.max = Math.max(metrics.latency.max, meta.latency);
211
+
212
+ // Keep only last 1000 samples for performance
213
+ if (metrics.latency.samples.length > 1000) {
214
+ metrics.latency.samples = metrics.latency.samples.slice(-1000);
215
+ }
216
+
217
+ metrics.latency.avg = Math.round(
218
+ metrics.latency.samples.reduce((a, b) => a + b, 0) / metrics.latency.samples.length
219
+ );
220
+ }
221
+
222
+ // Update errors
223
+ if (level === 'error' || level === 'fatal') {
224
+ metrics.errors.total++;
225
+ if (meta.provider) {
226
+ metrics.errors.byProvider[meta.provider] = (metrics.errors.byProvider[meta.provider] || 0) + 1;
227
+ }
228
+ }
229
+
230
+ // Update costs
231
+ if (meta.cost) {
232
+ metrics.costs.total += meta.cost;
233
+ if (meta.provider) {
234
+ metrics.costs.byProvider[meta.provider] = (metrics.costs.byProvider[meta.provider] || 0) + meta.cost;
235
+ }
236
+ }
237
+
238
+ // Update uptime
239
+ metrics.uptime.lastActivity = now;
240
+
241
+ fs.writeFileSync(this.metricsFile, JSON.stringify(metrics, null, 2));
242
+ } catch (error) {
243
+ console.error('Failed to update metrics:', error);
244
+ }
245
+ }
246
+
247
+ // Send to analytics module
248
+ sendToAnalytics(entry) {
249
+ if (!entry.meta || !entry.meta.provider || !entry.meta.model) return;
250
+
251
+ try {
252
+ const analyticsPath = path.join(__dirname, '..', 'cli', 'analytics.js');
253
+
254
+ // Call analytics module if available
255
+ const child = spawn('node', [analyticsPath, 'record',
256
+ entry.meta.provider,
257
+ entry.meta.model,
258
+ entry.meta.inputTokens || 0,
259
+ entry.meta.outputTokens || 0,
260
+ entry.meta.latency || 0,
261
+ entry.success !== false
262
+ ], {
263
+ stdio: 'ignore',
264
+ detached: true
265
+ });
266
+
267
+ child.unref();
268
+ } catch (error) {
269
+ // Silently fail analytics - don't break logging
270
+ }
271
+ }
272
+
273
+ // Convenience methods
274
+ fatal(message, meta = {}) {
275
+ this.log('fatal', message, { ...meta, stack: new Error().stack });
276
+ }
277
+
278
+ error(message, meta = {}) {
279
+ this.log('error', message, meta);
280
+ }
281
+
282
+ warn(message, meta = {}) {
283
+ this.log('warn', message, meta);
284
+ }
285
+
286
+ info(message, meta = {}) {
287
+ this.log('info', message, meta);
288
+ }
289
+
290
+ debug(message, meta = {}) {
291
+ this.log('debug', message, meta);
292
+ }
293
+
294
+ trace(message, meta = {}) {
295
+ this.log('trace', message, meta);
296
+ }
297
+
298
+ // Request logging with standard format
299
+ logRequest(provider, model, inputTokens, outputTokens, latency, success, cost = null) {
300
+ this.info('API Request', {
301
+ event: 'api_request',
302
+ provider,
303
+ model,
304
+ inputTokens,
305
+ outputTokens,
306
+ totalTokens: inputTokens + outputTokens,
307
+ latency,
308
+ success,
309
+ cost,
310
+ timestamp: new Date().toISOString()
311
+ });
312
+ }
313
+
314
+ // Route decision logging
315
+ logRoute(request, selectedProvider, selectedModel, reason, alternatives = []) {
316
+ this.debug('Route Decision', {
317
+ event: 'route_decision',
318
+ requestId: request.id || 'unknown',
319
+ requestType: request.type || 'unknown',
320
+ selectedProvider,
321
+ selectedModel,
322
+ reason,
323
+ alternatives,
324
+ timestamp: new Date().toISOString()
325
+ });
326
+ }
327
+
328
+ // Health check logging
329
+ logHealthCheck(provider, status, latency = null, error = null) {
330
+ const level = status === 'healthy' ? 'info' : 'warn';
331
+ this.log(level, `Health Check - ${provider}`, {
332
+ event: 'health_check',
333
+ provider,
334
+ status,
335
+ latency,
336
+ error,
337
+ timestamp: new Date().toISOString()
338
+ });
339
+ }
340
+
341
+ // Get metrics summary
342
+ getMetrics() {
343
+ try {
344
+ if (fs.existsSync(this.metricsFile)) {
345
+ return JSON.parse(fs.readFileSync(this.metricsFile, 'utf8'));
346
+ }
347
+ } catch (error) {
348
+ console.error('Failed to read metrics:', error);
349
+ }
350
+ return null;
351
+ }
352
+
353
+ // Get recent logs
354
+ getRecentLogs(count = 100, level = null) {
355
+ try {
356
+ if (!fs.existsSync(this.logFile)) return [];
357
+
358
+ const content = fs.readFileSync(this.logFile, 'utf8');
359
+ const lines = content.trim().split('\n').filter(line => line);
360
+ const logs = lines.map(line => {
361
+ try {
362
+ return JSON.parse(line);
363
+ } catch {
364
+ return null;
365
+ }
366
+ }).filter(Boolean);
367
+
368
+ // Filter by level if specified
369
+ if (level) {
370
+ return logs.filter(log => log.level === level.toUpperCase()).slice(-count);
371
+ }
372
+
373
+ return logs.slice(-count);
374
+ } catch (error) {
375
+ console.error('Failed to read recent logs:', error);
376
+ return [];
377
+ }
378
+ }
379
+
380
+ // Clean old logs
381
+ cleanup() {
382
+ try {
383
+ const files = fs.readdirSync(this.logDir);
384
+ const now = Date.now();
385
+ const maxAge = 30 * 24 * 60 * 60 * 1000; // 30 days
386
+
387
+ files.forEach(file => {
388
+ if (file.endsWith('.log') || file.endsWith('.json')) {
389
+ const filePath = path.join(this.logDir, file);
390
+ const stats = fs.statSync(filePath);
391
+
392
+ if (now - stats.mtime.getTime() > maxAge) {
393
+ fs.unlinkSync(filePath);
394
+ console.log(`Cleaned up old log file: ${file}`);
395
+ }
396
+ }
397
+ });
398
+ } catch (error) {
399
+ console.error('Failed to cleanup logs:', error);
400
+ }
401
+ }
402
+ }
403
+
404
+ // Export singleton instance
405
+ const logger = new EnhancedLogger();
406
+
407
+ module.exports = {
408
+ EnhancedLogger,
409
+ logger
410
+ };