coder-config 0.53.4-beta → 0.53.6-beta

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/config-loader.js CHANGED
@@ -35,6 +35,7 @@ const { heartbeat: runHeartbeat, formatReport, getExitCode, saveLastHeartbeat, s
35
35
  const { getSessionStatus, showSessionStatus, flushContext, clearContext, installHooks: sessionInstallHooks, getFlushedContext, installFlushCommand, installAll: sessionInstallAll, SESSION_DIR, FLUSHED_CONTEXT_FILE } = require('./lib/sessions');
36
36
  const { runCli } = require('./lib/cli');
37
37
  const { shellStatus, shellInstall, shellUninstall, printShellStatus } = require('./lib/shell');
38
+ const { getConfig: routerGetConfig, saveConfig: routerSaveConfig, listProviders: routerListProviders, addProvider: routerAddProvider, removeProvider: routerRemoveProvider, getRouterRules, setRouterRule, getActivationEnv: routerGetActivationEnv, getStatus: routerGetStatus, listPresets: routerListPresets, savePreset: routerSavePreset, loadPreset: routerLoadPreset } = require('./lib/router');
38
39
 
39
40
  class ClaudeConfigManager {
40
41
  constructor() {
@@ -332,6 +333,20 @@ class ClaudeConfigManager {
332
333
  shellUninstall() { return shellUninstall(); }
333
334
  getShellStatus() { return shellStatus(); }
334
335
 
336
+ // Router (CCR)
337
+ routerGetConfig() { return routerGetConfig(); }
338
+ routerSaveConfig(config) { return routerSaveConfig(config); }
339
+ routerListProviders() { return routerListProviders(); }
340
+ routerAddProvider(name, options) { return routerAddProvider(name, options); }
341
+ routerRemoveProvider(name) { return routerRemoveProvider(name); }
342
+ routerGetRules() { return getRouterRules(); }
343
+ routerSetRule(task, providerModel) { return setRouterRule(task, providerModel); }
344
+ routerGetActivationEnv() { return routerGetActivationEnv(); }
345
+ routerGetStatus() { return routerGetStatus(); }
346
+ routerListPresets() { return routerListPresets(); }
347
+ routerSavePreset(name) { return routerSavePreset(name); }
348
+ routerLoadPreset(name) { return routerLoadPreset(name); }
349
+
335
350
  // Update - check npm for updates or update from local source
336
351
  async update(args = []) {
337
352
  const https = require('https');
package/lib/cli.js CHANGED
@@ -352,6 +352,147 @@ function runCli(manager) {
352
352
  }
353
353
  break;
354
354
 
355
+ // Router (CCR)
356
+ case 'router': {
357
+ const sub = args[1];
358
+ if (sub === 'status') {
359
+ const status = manager.routerGetStatus();
360
+ console.log(`CCR installed: ${status.installed ? 'yes' : 'no'}`);
361
+ console.log(`Proxy running: ${status.proxyRunning ? 'yes' : 'no'}`);
362
+ if (status.proxyUrl) console.log(`Proxy URL: ${status.proxyUrl}`);
363
+ if (status.providers) console.log(`Providers: ${status.providers.length}`);
364
+ } else if (sub === 'list') {
365
+ const providers = manager.routerListProviders();
366
+ if (providers.length === 0) {
367
+ console.log('No providers configured.');
368
+ } else {
369
+ console.log('Providers:');
370
+ for (const p of providers) {
371
+ const models = p.models && p.models.length > 0 ? ` (${p.models.join(', ')})` : '';
372
+ console.log(` ${p.name} — ${p.url}${models}`);
373
+ }
374
+ }
375
+ console.log('');
376
+ const rules = manager.routerGetRules();
377
+ const ruleKeys = Object.keys(rules);
378
+ if (ruleKeys.length === 0) {
379
+ console.log('No routing rules configured.');
380
+ } else {
381
+ console.log('Routing rules:');
382
+ for (const key of ruleKeys) {
383
+ console.log(` ${key} → ${rules[key]}`);
384
+ }
385
+ }
386
+ } else if (sub === 'add-provider') {
387
+ const name = args[2];
388
+ if (!name) {
389
+ console.error('Usage: coder-config router add-provider <name> --url <url> [--key <key>] [--models <m1,m2>]');
390
+ process.exitCode = 1;
391
+ break;
392
+ }
393
+ const urlIdx = args.indexOf('--url');
394
+ const keyIdx = args.indexOf('--key');
395
+ const modelsIdx = args.indexOf('--models');
396
+ const url = urlIdx !== -1 ? args[urlIdx + 1] : undefined;
397
+ const apiKey = keyIdx !== -1 ? args[keyIdx + 1] : undefined;
398
+ const models = modelsIdx !== -1 ? args[modelsIdx + 1].split(',') : undefined;
399
+ if (!url) {
400
+ console.error('--url is required');
401
+ process.exitCode = 1;
402
+ break;
403
+ }
404
+ manager.routerAddProvider(name, { url, apiKey, models });
405
+ console.log(`Provider '${name}' added.`);
406
+ } else if (sub === 'remove-provider') {
407
+ const name = args[2];
408
+ if (!name) {
409
+ console.error('Usage: coder-config router remove-provider <name>');
410
+ process.exitCode = 1;
411
+ break;
412
+ }
413
+ manager.routerRemoveProvider(name);
414
+ console.log(`Provider '${name}' removed.`);
415
+ } else if (sub === 'set-rule') {
416
+ const task = args[2];
417
+ const providerModel = args[3];
418
+ if (!task || !providerModel) {
419
+ console.error('Usage: coder-config router set-rule <task> <provider,model>');
420
+ process.exitCode = 1;
421
+ break;
422
+ }
423
+ manager.routerSetRule(task, providerModel);
424
+ console.log(`Rule set: ${task} → ${providerModel}`);
425
+ } else if (sub === 'preset') {
426
+ const presetCmd = args[2];
427
+ if (presetCmd === 'list') {
428
+ const presets = manager.routerListPresets();
429
+ if (presets.length === 0) {
430
+ console.log('No presets saved.');
431
+ } else {
432
+ console.log('Presets:');
433
+ for (const p of presets) {
434
+ console.log(` ${p}`);
435
+ }
436
+ }
437
+ } else if (presetCmd === 'save') {
438
+ const name = args[3];
439
+ if (!name) {
440
+ console.error('Usage: coder-config router preset save <name>');
441
+ process.exitCode = 1;
442
+ break;
443
+ }
444
+ manager.routerSavePreset(name);
445
+ console.log(`Preset '${name}' saved.`);
446
+ } else if (presetCmd === 'load') {
447
+ const name = args[3];
448
+ if (!name) {
449
+ console.error('Usage: coder-config router preset load <name>');
450
+ process.exitCode = 1;
451
+ break;
452
+ }
453
+ manager.routerLoadPreset(name);
454
+ console.log(`Preset '${name}' loaded.`);
455
+ } else {
456
+ console.log('Usage: coder-config router preset <list|save|load> [name]');
457
+ }
458
+ } else if (sub === 'start') {
459
+ const { execSync } = require('child_process');
460
+ try {
461
+ execSync('ccr code', { stdio: 'inherit' });
462
+ } catch (e) {
463
+ console.error('Failed to start CCR proxy. Is ccr installed?');
464
+ process.exitCode = 1;
465
+ }
466
+ } else if (sub === 'stop') {
467
+ const { execSync } = require('child_process');
468
+ try {
469
+ execSync('ccr stop', { stdio: 'inherit' });
470
+ } catch (e) {
471
+ console.error('Failed to stop CCR proxy. Is ccr installed?');
472
+ process.exitCode = 1;
473
+ }
474
+ } else if (sub === 'activate') {
475
+ const env = manager.routerGetActivationEnv();
476
+ for (const [key, value] of Object.entries(env)) {
477
+ console.log(`export ${key}="${value}"`);
478
+ }
479
+ } else {
480
+ console.log('Usage: coder-config router <command>');
481
+ console.log('');
482
+ console.log('Commands:');
483
+ console.log(' status Show CCR install/proxy status');
484
+ console.log(' list Show providers and routing rules');
485
+ console.log(' add-provider <name> --url <url> Add a provider');
486
+ console.log(' remove-provider <name> Remove a provider');
487
+ console.log(' set-rule <task> <provider,model> Set routing rule');
488
+ console.log(' preset list|save|load [name] Manage presets');
489
+ console.log(' start Start CCR proxy');
490
+ console.log(' stop Stop CCR proxy');
491
+ console.log(' activate Print env vars for shell eval');
492
+ }
493
+ break;
494
+ }
495
+
355
496
  case 'version':
356
497
  case '-v':
357
498
  case '--version':
@@ -495,6 +636,20 @@ ${chalk.dim('Configuration manager for AI coding tools (Claude Code, Gemini CLI,
495
636
  ]));
496
637
  console.log();
497
638
 
639
+ // Router (CCR)
640
+ console.log(box('Router (CCR)', [
641
+ cmd('router status', 'Show CCR install/proxy status'),
642
+ cmd('router list', 'Show providers and routing rules'),
643
+ cmd('router add-provider <name>', 'Add a provider'),
644
+ cmd('router remove-provider <name>', 'Remove a provider'),
645
+ cmd('router set-rule <task> <p,m>', 'Set routing rule'),
646
+ cmd('router preset list|save|load', 'Manage presets'),
647
+ cmd('router start', 'Start CCR proxy'),
648
+ cmd('router stop', 'Stop CCR proxy'),
649
+ cmd('router activate', 'Print env vars for shell eval'),
650
+ ]));
651
+ console.log();
652
+
498
653
  // UI & Maintenance
499
654
  console.log(box('UI & Maintenance', [
500
655
  cmd('ui', 'Start web UI (daemon mode)'),
package/lib/constants.js CHANGED
@@ -2,7 +2,7 @@
2
2
  * Constants and tool path configurations
3
3
  */
4
4
 
5
- const VERSION = '0.53.4-beta';
5
+ const VERSION = '0.53.6-beta';
6
6
 
7
7
  // Tool-specific path configurations
8
8
  const TOOL_PATHS = {
package/lib/router.js ADDED
@@ -0,0 +1,191 @@
1
+ /**
2
+ * Claude Code Router (CCR) configuration management
3
+ */
4
+
5
+ const fs = require('fs');
6
+ const path = require('path');
7
+ const { execSync } = require('child_process');
8
+
9
+ const CCR_DIR_NAME = '.claude-code-router';
10
+ const CONFIG_FILE = 'config.json';
11
+ const PRESET_DIR_NAME = 'coder-config-presets';
12
+ const CCR_PORT = 3456;
13
+
14
+ /**
15
+ * Get path to CCR config file
16
+ */
17
+ function getConfigPath() {
18
+ const homeDir = process.env.HOME || '';
19
+ return path.join(homeDir, CCR_DIR_NAME, CONFIG_FILE);
20
+ }
21
+
22
+ /**
23
+ * Read and parse CCR config, return {} if missing or invalid
24
+ */
25
+ function getConfig() {
26
+ const configPath = getConfigPath();
27
+ try {
28
+ if (!fs.existsSync(configPath)) return {};
29
+ return JSON.parse(fs.readFileSync(configPath, 'utf8'));
30
+ } catch {
31
+ return {};
32
+ }
33
+ }
34
+
35
+ /**
36
+ * Write config JSON, creating directory if needed
37
+ */
38
+ function saveConfig(config) {
39
+ const configPath = getConfigPath();
40
+ const dir = path.dirname(configPath);
41
+ fs.mkdirSync(dir, { recursive: true });
42
+ fs.writeFileSync(configPath, JSON.stringify(config, null, 2) + '\n');
43
+ }
44
+
45
+ /**
46
+ * Return Providers array from config, or []
47
+ */
48
+ function listProviders() {
49
+ const config = getConfig();
50
+ return Array.isArray(config.Providers) ? config.Providers : [];
51
+ }
52
+
53
+ /**
54
+ * Add or replace a provider by name
55
+ */
56
+ function addProvider(name, providerConfig) {
57
+ const config = getConfig();
58
+ if (!Array.isArray(config.Providers)) {
59
+ config.Providers = [];
60
+ }
61
+ // Remove existing provider with same name
62
+ config.Providers = config.Providers.filter(p => p.name !== name);
63
+ config.Providers.push({ name, ...providerConfig });
64
+ saveConfig(config);
65
+ }
66
+
67
+ /**
68
+ * Remove a provider by name
69
+ */
70
+ function removeProvider(name) {
71
+ const config = getConfig();
72
+ if (!Array.isArray(config.Providers)) return;
73
+ config.Providers = config.Providers.filter(p => p.name !== name);
74
+ saveConfig(config);
75
+ }
76
+
77
+ /**
78
+ * Return Router rules object or {}
79
+ */
80
+ function getRouterRules() {
81
+ const config = getConfig();
82
+ return config.Router || {};
83
+ }
84
+
85
+ /**
86
+ * Set a single router rule (task -> providerModel string)
87
+ */
88
+ function setRouterRule(task, providerModel) {
89
+ const config = getConfig();
90
+ if (!config.Router) {
91
+ config.Router = {};
92
+ }
93
+ config.Router[task] = providerModel;
94
+ saveConfig(config);
95
+ }
96
+
97
+ /**
98
+ * Return environment variables needed to activate CCR proxy
99
+ */
100
+ function getActivationEnv() {
101
+ const config = getConfig();
102
+ return {
103
+ ANTHROPIC_BASE_URL: `http://127.0.0.1:${CCR_PORT}`,
104
+ ANTHROPIC_AUTH_TOKEN: config.APIKEY || '',
105
+ NO_PROXY: '127.0.0.1',
106
+ DISABLE_TELEMETRY: '1',
107
+ DISABLE_COST_WARNINGS: '1',
108
+ };
109
+ }
110
+
111
+ /**
112
+ * Check CCR installation and running status
113
+ */
114
+ function getStatus() {
115
+ let installed = false;
116
+ try {
117
+ execSync('which ccr', { stdio: 'pipe' });
118
+ installed = true;
119
+ } catch {
120
+ // not installed
121
+ }
122
+
123
+ let running = false;
124
+ try {
125
+ execSync(`lsof -i :${CCR_PORT}`, { stdio: 'pipe' });
126
+ running = true;
127
+ } catch {
128
+ // not running
129
+ }
130
+
131
+ const configExists = fs.existsSync(getConfigPath());
132
+
133
+ return { installed, running, configExists };
134
+ }
135
+
136
+ /**
137
+ * Get presets directory path
138
+ */
139
+ function getPresetsDir() {
140
+ const homeDir = process.env.HOME || '';
141
+ return path.join(homeDir, CCR_DIR_NAME, PRESET_DIR_NAME);
142
+ }
143
+
144
+ /**
145
+ * List saved presets (names without .json extension)
146
+ */
147
+ function listPresets() {
148
+ const presetsDir = getPresetsDir();
149
+ if (!fs.existsSync(presetsDir)) return [];
150
+ return fs.readdirSync(presetsDir)
151
+ .filter(f => f.endsWith('.json'))
152
+ .map(f => f.replace(/\.json$/, ''));
153
+ }
154
+
155
+ /**
156
+ * Snapshot current config to a named preset
157
+ */
158
+ function savePreset(name) {
159
+ const config = getConfig();
160
+ const presetsDir = getPresetsDir();
161
+ fs.mkdirSync(presetsDir, { recursive: true });
162
+ fs.writeFileSync(path.join(presetsDir, `${name}.json`), JSON.stringify(config, null, 2) + '\n');
163
+ }
164
+
165
+ /**
166
+ * Restore config from a named preset
167
+ */
168
+ function loadPreset(name) {
169
+ const presetPath = path.join(getPresetsDir(), `${name}.json`);
170
+ if (!fs.existsSync(presetPath)) {
171
+ throw new Error(`Preset "${name}" not found`);
172
+ }
173
+ const config = JSON.parse(fs.readFileSync(presetPath, 'utf8'));
174
+ saveConfig(config);
175
+ }
176
+
177
+ module.exports = {
178
+ getConfigPath,
179
+ getConfig,
180
+ saveConfig,
181
+ listProviders,
182
+ addProvider,
183
+ removeProvider,
184
+ getRouterRules,
185
+ setRouterRule,
186
+ getActivationEnv,
187
+ getStatus,
188
+ listPresets,
189
+ savePreset,
190
+ loadPreset,
191
+ };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "coder-config",
3
- "version": "0.53.4-beta",
3
+ "version": "0.53.6-beta",
4
4
  "description": "Configuration manager for AI coding tools - Claude Code, Gemini CLI, Codex CLI, Antigravity. Manage MCPs, rules, permissions, memory, and workstreams.",
5
5
  "author": "regression.io",
6
6
  "main": "config-loader.js",