tlc-claude-code 1.4.1 → 1.4.4

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.
Files changed (91) hide show
  1. package/dashboard/dist/App.js +229 -35
  2. package/dashboard/dist/components/AgentRegistryPane.d.ts +35 -0
  3. package/dashboard/dist/components/AgentRegistryPane.js +89 -0
  4. package/dashboard/dist/components/AgentRegistryPane.test.d.ts +1 -0
  5. package/dashboard/dist/components/AgentRegistryPane.test.js +200 -0
  6. package/dashboard/dist/components/RouterPane.d.ts +5 -0
  7. package/dashboard/dist/components/RouterPane.js +65 -0
  8. package/dashboard/dist/components/RouterPane.test.d.ts +1 -0
  9. package/dashboard/dist/components/RouterPane.test.js +176 -0
  10. package/dashboard/dist/components/accessibility.test.d.ts +1 -0
  11. package/dashboard/dist/components/accessibility.test.js +116 -0
  12. package/dashboard/dist/components/layout/MobileNav.d.ts +16 -0
  13. package/dashboard/dist/components/layout/MobileNav.js +31 -0
  14. package/dashboard/dist/components/layout/MobileNav.test.d.ts +1 -0
  15. package/dashboard/dist/components/layout/MobileNav.test.js +111 -0
  16. package/dashboard/dist/components/performance.test.d.ts +1 -0
  17. package/dashboard/dist/components/performance.test.js +114 -0
  18. package/dashboard/dist/components/responsive.test.d.ts +1 -0
  19. package/dashboard/dist/components/responsive.test.js +114 -0
  20. package/dashboard/dist/components/ui/Dropdown.d.ts +22 -0
  21. package/dashboard/dist/components/ui/Dropdown.js +109 -0
  22. package/dashboard/dist/components/ui/Dropdown.test.d.ts +1 -0
  23. package/dashboard/dist/components/ui/Dropdown.test.js +105 -0
  24. package/dashboard/dist/components/ui/Modal.d.ts +13 -0
  25. package/dashboard/dist/components/ui/Modal.js +25 -0
  26. package/dashboard/dist/components/ui/Modal.test.d.ts +1 -0
  27. package/dashboard/dist/components/ui/Modal.test.js +91 -0
  28. package/dashboard/dist/components/ui/Skeleton.d.ts +32 -0
  29. package/dashboard/dist/components/ui/Skeleton.js +48 -0
  30. package/dashboard/dist/components/ui/Skeleton.test.d.ts +1 -0
  31. package/dashboard/dist/components/ui/Skeleton.test.js +125 -0
  32. package/dashboard/dist/components/ui/Toast.d.ts +32 -0
  33. package/dashboard/dist/components/ui/Toast.js +21 -0
  34. package/dashboard/dist/components/ui/Toast.test.d.ts +1 -0
  35. package/dashboard/dist/components/ui/Toast.test.js +118 -0
  36. package/dashboard/dist/hooks/useTheme.d.ts +37 -0
  37. package/dashboard/dist/hooks/useTheme.js +96 -0
  38. package/dashboard/dist/hooks/useTheme.test.d.ts +1 -0
  39. package/dashboard/dist/hooks/useTheme.test.js +94 -0
  40. package/dashboard/dist/hooks/useWebSocket.d.ts +17 -0
  41. package/dashboard/dist/hooks/useWebSocket.js +100 -0
  42. package/dashboard/dist/hooks/useWebSocket.test.d.ts +1 -0
  43. package/dashboard/dist/hooks/useWebSocket.test.js +115 -0
  44. package/dashboard/dist/stores/projectStore.d.ts +44 -0
  45. package/dashboard/dist/stores/projectStore.js +76 -0
  46. package/dashboard/dist/stores/projectStore.test.d.ts +1 -0
  47. package/dashboard/dist/stores/projectStore.test.js +114 -0
  48. package/dashboard/dist/stores/uiStore.d.ts +29 -0
  49. package/dashboard/dist/stores/uiStore.js +72 -0
  50. package/dashboard/dist/stores/uiStore.test.d.ts +1 -0
  51. package/dashboard/dist/stores/uiStore.test.js +93 -0
  52. package/dashboard/package.json +3 -3
  53. package/docker-compose.dev.yml +6 -1
  54. package/package.json +5 -2
  55. package/server/dashboard/index.html +1336 -779
  56. package/server/index.js +178 -0
  57. package/server/lib/agent-cleanup.js +177 -0
  58. package/server/lib/agent-cleanup.test.js +359 -0
  59. package/server/lib/agent-hooks.js +126 -0
  60. package/server/lib/agent-hooks.test.js +303 -0
  61. package/server/lib/agent-metadata.js +179 -0
  62. package/server/lib/agent-metadata.test.js +383 -0
  63. package/server/lib/agent-persistence.js +191 -0
  64. package/server/lib/agent-persistence.test.js +475 -0
  65. package/server/lib/agent-registry-command.js +340 -0
  66. package/server/lib/agent-registry-command.test.js +334 -0
  67. package/server/lib/agent-registry.js +155 -0
  68. package/server/lib/agent-registry.test.js +239 -0
  69. package/server/lib/agent-state.js +236 -0
  70. package/server/lib/agent-state.test.js +375 -0
  71. package/server/lib/api-provider.js +186 -0
  72. package/server/lib/api-provider.test.js +336 -0
  73. package/server/lib/cli-detector.js +166 -0
  74. package/server/lib/cli-detector.test.js +269 -0
  75. package/server/lib/cli-provider.js +212 -0
  76. package/server/lib/cli-provider.test.js +349 -0
  77. package/server/lib/debug.test.js +62 -0
  78. package/server/lib/devserver-router-api.js +249 -0
  79. package/server/lib/devserver-router-api.test.js +426 -0
  80. package/server/lib/model-router.js +245 -0
  81. package/server/lib/model-router.test.js +313 -0
  82. package/server/lib/output-schemas.js +269 -0
  83. package/server/lib/output-schemas.test.js +307 -0
  84. package/server/lib/provider-interface.js +153 -0
  85. package/server/lib/provider-interface.test.js +394 -0
  86. package/server/lib/provider-queue.js +158 -0
  87. package/server/lib/provider-queue.test.js +315 -0
  88. package/server/lib/router-config.js +221 -0
  89. package/server/lib/router-config.test.js +237 -0
  90. package/server/lib/router-setup-command.js +419 -0
  91. package/server/lib/router-setup-command.test.js +375 -0
@@ -0,0 +1,237 @@
1
+ import { describe, it, expect, vi, beforeEach } from 'vitest';
2
+ import {
3
+ loadRouterConfig,
4
+ validateCapabilities,
5
+ validateProviders,
6
+ getProviderConfig,
7
+ getCapabilityConfig,
8
+ migrateConfig,
9
+ saveRouterConfig,
10
+ defaultConfig,
11
+ } from './router-config.js';
12
+
13
+ vi.mock('fs/promises');
14
+ import fs from 'fs/promises';
15
+
16
+ describe('router-config', () => {
17
+ beforeEach(() => {
18
+ vi.clearAllMocks();
19
+ });
20
+
21
+ describe('loadRouterConfig', () => {
22
+ it('reads from .tlc.json', async () => {
23
+ const config = {
24
+ router: {
25
+ providers: { claude: { type: 'cli', command: 'claude' } },
26
+ capabilities: { review: { providers: ['claude'] } },
27
+ },
28
+ };
29
+
30
+ fs.readFile.mockResolvedValue(JSON.stringify(config));
31
+
32
+ const loaded = await loadRouterConfig('/project');
33
+
34
+ expect(fs.readFile).toHaveBeenCalledWith('/project/.tlc.json', 'utf8');
35
+ });
36
+
37
+ it('validates schema', async () => {
38
+ const invalidConfig = {
39
+ router: {
40
+ providers: { test: { /* missing type */ } },
41
+ },
42
+ };
43
+
44
+ fs.readFile.mockResolvedValue(JSON.stringify(invalidConfig));
45
+
46
+ await expect(loadRouterConfig('/project')).rejects.toThrow(/type/i);
47
+ });
48
+
49
+ it('merges with defaults', async () => {
50
+ const partialConfig = {
51
+ router: {
52
+ providers: { custom: { type: 'api', baseUrl: 'https://example.com' } },
53
+ },
54
+ };
55
+
56
+ fs.readFile.mockResolvedValue(JSON.stringify(partialConfig));
57
+
58
+ const loaded = await loadRouterConfig('/project');
59
+
60
+ expect(loaded.providers.custom).toBeDefined();
61
+ expect(loaded.providers.claude).toBeDefined(); // From defaults
62
+ });
63
+ });
64
+
65
+ describe('validateCapabilities', () => {
66
+ it('checks provider refs', () => {
67
+ const config = {
68
+ providers: { claude: { type: 'cli', command: 'claude' } },
69
+ capabilities: { review: { providers: ['claude'] } },
70
+ };
71
+
72
+ expect(() => validateCapabilities(config)).not.toThrow();
73
+ });
74
+
75
+ it('throws on invalid provider ref', () => {
76
+ const config = {
77
+ providers: { claude: { type: 'cli', command: 'claude' } },
78
+ capabilities: { review: { providers: ['nonexistent'] } },
79
+ };
80
+
81
+ expect(() => validateCapabilities(config)).toThrow(/nonexistent/);
82
+ });
83
+ });
84
+
85
+ describe('validateProviders', () => {
86
+ it('checks required fields for CLI', () => {
87
+ const providers = {
88
+ claude: { type: 'cli', command: 'claude' },
89
+ };
90
+
91
+ expect(() => validateProviders(providers)).not.toThrow();
92
+ });
93
+
94
+ it('throws on missing command for CLI', () => {
95
+ const providers = {
96
+ claude: { type: 'cli' },
97
+ };
98
+
99
+ expect(() => validateProviders(providers)).toThrow(/command/i);
100
+ });
101
+
102
+ it('checks required fields for API', () => {
103
+ const providers = {
104
+ deepseek: { type: 'api', baseUrl: 'https://api.deepseek.com' },
105
+ };
106
+
107
+ expect(() => validateProviders(providers)).not.toThrow();
108
+ });
109
+
110
+ it('throws on missing baseUrl for API', () => {
111
+ const providers = {
112
+ deepseek: { type: 'api' },
113
+ };
114
+
115
+ expect(() => validateProviders(providers)).toThrow(/baseUrl/i);
116
+ });
117
+ });
118
+
119
+ describe('getProviderConfig', () => {
120
+ it('returns provider config', () => {
121
+ const config = {
122
+ providers: {
123
+ claude: { type: 'cli', command: 'claude', capabilities: ['review'] },
124
+ },
125
+ };
126
+
127
+ const provider = getProviderConfig(config, 'claude');
128
+
129
+ expect(provider.type).toBe('cli');
130
+ expect(provider.command).toBe('claude');
131
+ });
132
+
133
+ it('returns null for unknown provider', () => {
134
+ const config = { providers: {} };
135
+
136
+ const provider = getProviderConfig(config, 'unknown');
137
+
138
+ expect(provider).toBeNull();
139
+ });
140
+ });
141
+
142
+ describe('getCapabilityConfig', () => {
143
+ it('returns providers array', () => {
144
+ const config = {
145
+ capabilities: {
146
+ review: { providers: ['claude', 'codex'] },
147
+ },
148
+ };
149
+
150
+ const cap = getCapabilityConfig(config, 'review');
151
+
152
+ expect(cap.providers).toEqual(['claude', 'codex']);
153
+ });
154
+
155
+ it('returns null for unknown capability', () => {
156
+ const config = { capabilities: {} };
157
+
158
+ const cap = getCapabilityConfig(config, 'unknown');
159
+
160
+ expect(cap).toBeNull();
161
+ });
162
+ });
163
+
164
+ describe('migrateConfig', () => {
165
+ it('handles old format', () => {
166
+ const oldConfig = {
167
+ model: 'claude', // Old format
168
+ adapters: { claude: {} },
169
+ };
170
+
171
+ const migrated = migrateConfig(oldConfig);
172
+
173
+ expect(migrated.providers).toBeDefined();
174
+ });
175
+
176
+ it('preserves new format', () => {
177
+ const newConfig = {
178
+ router: {
179
+ providers: { claude: { type: 'cli', command: 'claude' } },
180
+ },
181
+ };
182
+
183
+ const migrated = migrateConfig(newConfig);
184
+
185
+ expect(migrated.providers.claude.type).toBe('cli');
186
+ });
187
+ });
188
+
189
+ describe('defaultConfig', () => {
190
+ it('has sensible defaults', () => {
191
+ expect(defaultConfig.providers).toBeDefined();
192
+ expect(defaultConfig.capabilities).toBeDefined();
193
+ expect(defaultConfig.devserver).toBeDefined();
194
+ });
195
+
196
+ it('includes all standard providers', () => {
197
+ expect(defaultConfig.providers.claude).toBeDefined();
198
+ expect(defaultConfig.providers.codex).toBeDefined();
199
+ expect(defaultConfig.providers.gemini).toBeDefined();
200
+ expect(defaultConfig.providers.deepseek).toBeDefined();
201
+ });
202
+ });
203
+
204
+ describe('saveRouterConfig', () => {
205
+ it('writes to file', async () => {
206
+ fs.readFile.mockResolvedValue('{}');
207
+ fs.writeFile.mockResolvedValue();
208
+
209
+ const config = {
210
+ providers: { claude: { type: 'cli', command: 'claude' } },
211
+ };
212
+
213
+ await saveRouterConfig('/project', config);
214
+
215
+ expect(fs.writeFile).toHaveBeenCalled();
216
+ });
217
+
218
+ it('merges with existing config', async () => {
219
+ fs.readFile.mockResolvedValue(JSON.stringify({
220
+ testFrameworks: { primary: 'vitest' },
221
+ }));
222
+ fs.writeFile.mockResolvedValue();
223
+
224
+ const routerConfig = {
225
+ providers: { claude: { type: 'cli', command: 'claude' } },
226
+ };
227
+
228
+ await saveRouterConfig('/project', routerConfig);
229
+
230
+ const writeCall = fs.writeFile.mock.calls[0];
231
+ const written = JSON.parse(writeCall[1]);
232
+
233
+ expect(written.testFrameworks.primary).toBe('vitest');
234
+ expect(written.router.providers.claude).toBeDefined();
235
+ });
236
+ });
237
+ });
@@ -0,0 +1,419 @@
1
+ /**
2
+ * Router Setup Command - Interactive setup for multi-model routing
3
+ */
4
+
5
+ import { detectAllCLIs } from './cli-detector.js';
6
+ import fs from 'fs/promises';
7
+ import path from 'path';
8
+
9
+ /**
10
+ * Default cost estimates per 1K tokens (in USD)
11
+ */
12
+ const API_COSTS = {
13
+ deepseek: { input: 0.0001, output: 0.0002 },
14
+ mistral: { input: 0.0002, output: 0.0006 },
15
+ default: { input: 0.001, output: 0.002 },
16
+ };
17
+
18
+ /**
19
+ * Average tokens per request type
20
+ */
21
+ const AVG_TOKENS = {
22
+ review: { input: 2000, output: 500 },
23
+ design: { input: 1000, output: 2000 },
24
+ 'code-gen': { input: 500, output: 1500 },
25
+ };
26
+
27
+ /**
28
+ * Execute the router setup command
29
+ * @param {Object} options - Command options
30
+ * @param {string} options.projectDir - Project directory
31
+ * @param {boolean} [options.dryRun] - Don't write config
32
+ * @returns {Promise<Object>} Setup result
33
+ */
34
+ export async function execute(options = {}) {
35
+ const { projectDir = process.cwd(), dryRun = false } = options;
36
+
37
+ // Step 1: Detect local CLIs
38
+ const detected = await detectLocalCLIs();
39
+
40
+ // Step 2: Load existing config
41
+ let existingConfig = {};
42
+ try {
43
+ const configPath = path.join(projectDir, '.tlc.json');
44
+ const content = await fs.readFile(configPath, 'utf8');
45
+ existingConfig = JSON.parse(content);
46
+ } catch (err) {
47
+ // No existing config
48
+ }
49
+
50
+ // Step 3: Test devserver connection
51
+ const devserverUrl = existingConfig.router?.devserver?.url;
52
+ const devserver = await testDevserverConnection(devserverUrl);
53
+
54
+ // Step 4: Build routing table
55
+ const routingTable = buildRoutingTable(detected, devserver);
56
+
57
+ // Step 5: Estimate costs
58
+ const costEstimate = estimateCosts(
59
+ {
60
+ providers: buildProviderConfig(detected),
61
+ capabilities: buildCapabilityConfig(detected),
62
+ },
63
+ { reviewsPerDay: 10, designsPerDay: 2, codeGensPerDay: 5 }
64
+ );
65
+
66
+ // Step 6: Build final config
67
+ const routerConfig = {
68
+ providers: buildProviderConfig(detected),
69
+ capabilities: buildCapabilityConfig(detected),
70
+ devserver: {
71
+ url: devserverUrl || null,
72
+ queue: {
73
+ maxConcurrent: 3,
74
+ timeout: 120000,
75
+ },
76
+ },
77
+ };
78
+
79
+ // Step 7: Save config (unless dry run)
80
+ if (!dryRun) {
81
+ await saveConfig(projectDir, routerConfig);
82
+ }
83
+
84
+ return {
85
+ detected,
86
+ devserver,
87
+ routingTable,
88
+ costEstimate,
89
+ config: routerConfig,
90
+ };
91
+ }
92
+
93
+ /**
94
+ * Detect locally installed CLIs
95
+ * @returns {Promise<Object>} Detected CLI info
96
+ */
97
+ export async function detectLocalCLIs() {
98
+ const detected = await detectAllCLIs();
99
+ const result = {};
100
+
101
+ for (const [name, info] of detected) {
102
+ result[name] = {
103
+ detected: true,
104
+ version: info.version,
105
+ capabilities: info.capabilities || [],
106
+ };
107
+ }
108
+
109
+ return result;
110
+ }
111
+
112
+ /**
113
+ * Test devserver connection
114
+ * @param {string|null} url - Devserver URL
115
+ * @returns {Promise<Object>} Connection result
116
+ */
117
+ export async function testDevserverConnection(url) {
118
+ if (!url) {
119
+ return { connected: false, configured: false };
120
+ }
121
+
122
+ try {
123
+ const response = await fetch(`${url}/api/health`);
124
+ if (response.ok) {
125
+ const data = await response.json();
126
+ return {
127
+ connected: true,
128
+ configured: true,
129
+ healthy: data.healthy,
130
+ providers: data.providers,
131
+ };
132
+ }
133
+ return { connected: false, configured: true, error: 'Unhealthy response' };
134
+ } catch (err) {
135
+ return { connected: false, configured: true, error: err.message };
136
+ }
137
+ }
138
+
139
+ /**
140
+ * Configure a provider
141
+ * @param {Object} config - Current config
142
+ * @param {string} name - Provider name
143
+ * @param {Object} providerConfig - Provider configuration
144
+ * @returns {Object} Updated config
145
+ */
146
+ export function configureProvider(config, name, providerConfig) {
147
+ return {
148
+ ...config,
149
+ providers: {
150
+ ...config.providers,
151
+ [name]: providerConfig,
152
+ },
153
+ };
154
+ }
155
+
156
+ /**
157
+ * Configure a capability
158
+ * @param {Object} config - Current config
159
+ * @param {string} name - Capability name
160
+ * @param {string[]} providers - Provider names
161
+ * @returns {Object} Updated config
162
+ */
163
+ export function configureCapability(config, name, providers) {
164
+ return {
165
+ ...config,
166
+ capabilities: {
167
+ ...config.capabilities,
168
+ [name]: { providers },
169
+ },
170
+ };
171
+ }
172
+
173
+ /**
174
+ * Test provider connectivity
175
+ * @param {Object} provider - Provider config
176
+ * @returns {Promise<Object>} Test result
177
+ */
178
+ export async function testProvider(provider) {
179
+ if (provider.type === 'cli') {
180
+ // CLI providers are available if detected
181
+ return {
182
+ available: provider.detected === true,
183
+ via: provider.detected ? 'local' : 'devserver',
184
+ };
185
+ }
186
+
187
+ if (provider.type === 'api') {
188
+ // Test API endpoint
189
+ try {
190
+ const response = await fetch(`${provider.baseUrl}/v1/models`);
191
+ return { available: response.ok, via: 'api' };
192
+ } catch (err) {
193
+ return { available: true, via: 'api', note: 'Endpoint not tested' };
194
+ }
195
+ }
196
+
197
+ return { available: false, error: 'Unknown provider type' };
198
+ }
199
+
200
+ /**
201
+ * Format routing summary
202
+ * @param {Object} config - Router config
203
+ * @returns {string} Formatted summary
204
+ */
205
+ export function formatRoutingSummary(config) {
206
+ const lines = ['Routing Summary:', ''];
207
+
208
+ for (const [capName, capConfig] of Object.entries(
209
+ config.capabilities || {}
210
+ )) {
211
+ lines.push(` ${capName}:`);
212
+
213
+ for (const providerName of capConfig.providers || []) {
214
+ const provider = config.providers?.[providerName];
215
+ if (!provider) continue;
216
+
217
+ let routing = 'unknown';
218
+ if (provider.type === 'cli') {
219
+ routing = provider.detected ? 'local' : 'devserver';
220
+ } else if (provider.type === 'api') {
221
+ routing = 'devserver';
222
+ }
223
+
224
+ lines.push(` - ${providerName} (${routing})`);
225
+ }
226
+
227
+ lines.push('');
228
+ }
229
+
230
+ return lines.join('\n');
231
+ }
232
+
233
+ /**
234
+ * Estimate costs
235
+ * @param {Object} config - Router config
236
+ * @param {Object} usage - Usage estimates
237
+ * @returns {Object} Cost estimates
238
+ */
239
+ export function estimateCosts(config, usage = {}) {
240
+ const estimate = {};
241
+
242
+ for (const [capName, capConfig] of Object.entries(
243
+ config.capabilities || {}
244
+ )) {
245
+ const perDay = usage[`${capName}sPerDay`] || usage.reviewsPerDay || 10;
246
+ const tokens = AVG_TOKENS[capName] || AVG_TOKENS.review;
247
+
248
+ let localCost = 0;
249
+ let devserverCost = 0;
250
+
251
+ for (const providerName of capConfig.providers || []) {
252
+ const provider = config.providers?.[providerName];
253
+ if (!provider) continue;
254
+
255
+ if (provider.type === 'cli' && provider.detected) {
256
+ // Local CLI is free
257
+ localCost += 0;
258
+ } else {
259
+ // API or devserver costs money
260
+ const pricing = API_COSTS[providerName] || API_COSTS.default;
261
+ const inputCost = (tokens.input / 1000) * pricing.input * perDay * 30;
262
+ const outputCost = (tokens.output / 1000) * pricing.output * perDay * 30;
263
+ devserverCost += inputCost + outputCost;
264
+ }
265
+ }
266
+
267
+ estimate[capName] = {
268
+ local: localCost,
269
+ devserver: Math.round(devserverCost * 100) / 100,
270
+ };
271
+ }
272
+
273
+ return estimate;
274
+ }
275
+
276
+ /**
277
+ * Save router config
278
+ * @param {string} projectDir - Project directory
279
+ * @param {Object} routerConfig - Router configuration
280
+ */
281
+ export async function saveConfig(projectDir, routerConfig) {
282
+ const configPath = path.join(projectDir, '.tlc.json');
283
+
284
+ // Read existing config
285
+ let existingConfig = {};
286
+ try {
287
+ const content = await fs.readFile(configPath, 'utf8');
288
+ existingConfig = JSON.parse(content);
289
+ } catch (err) {
290
+ // No existing config
291
+ }
292
+
293
+ // Merge router config
294
+ const newConfig = {
295
+ ...existingConfig,
296
+ router: routerConfig,
297
+ };
298
+
299
+ await fs.writeFile(configPath, JSON.stringify(newConfig, null, 2));
300
+ }
301
+
302
+ /**
303
+ * Build provider config from detected CLIs
304
+ * @param {Object} detected - Detected CLI info
305
+ * @returns {Object} Provider config
306
+ */
307
+ function buildProviderConfig(detected) {
308
+ const providers = {};
309
+
310
+ // Add detected CLIs
311
+ if (detected.claude) {
312
+ providers.claude = {
313
+ type: 'cli',
314
+ command: 'claude',
315
+ detected: true,
316
+ headlessArgs: ['-p', '--output-format', 'json'],
317
+ capabilities: ['review', 'code-gen', 'refactor', 'explain'],
318
+ };
319
+ }
320
+
321
+ if (detected.codex) {
322
+ providers.codex = {
323
+ type: 'cli',
324
+ command: 'codex',
325
+ detected: true,
326
+ headlessArgs: ['exec', '--json', '--sandbox', 'read-only'],
327
+ capabilities: ['review', 'code-gen', 'refactor'],
328
+ };
329
+ }
330
+
331
+ if (detected.gemini) {
332
+ providers.gemini = {
333
+ type: 'cli',
334
+ command: 'gemini',
335
+ detected: true,
336
+ headlessArgs: ['-p', '--output-format', 'json'],
337
+ capabilities: ['design', 'vision', 'review', 'image-gen'],
338
+ };
339
+ }
340
+
341
+ // Always include API providers (devserver-only)
342
+ providers.deepseek = {
343
+ type: 'api',
344
+ baseUrl: 'https://api.deepseek.com',
345
+ model: 'deepseek-coder',
346
+ capabilities: ['review'],
347
+ devserverOnly: true,
348
+ };
349
+
350
+ return providers;
351
+ }
352
+
353
+ /**
354
+ * Build capability config from detected CLIs
355
+ * @param {Object} detected - Detected CLI info
356
+ * @returns {Object} Capability config
357
+ */
358
+ function buildCapabilityConfig(detected) {
359
+ const capabilities = {};
360
+
361
+ // Review capability - use all available
362
+ const reviewProviders = [];
363
+ if (detected.claude) reviewProviders.push('claude');
364
+ if (detected.codex) reviewProviders.push('codex');
365
+ reviewProviders.push('deepseek'); // Always available via devserver
366
+
367
+ capabilities.review = {
368
+ providers: reviewProviders,
369
+ consensus: 'majority',
370
+ };
371
+
372
+ // Design capability - gemini only
373
+ if (detected.gemini) {
374
+ capabilities.design = {
375
+ providers: ['gemini'],
376
+ };
377
+ }
378
+
379
+ // Code generation - claude preferred
380
+ const codeGenProviders = [];
381
+ if (detected.claude) codeGenProviders.push('claude');
382
+ if (detected.codex) codeGenProviders.push('codex');
383
+
384
+ if (codeGenProviders.length > 0) {
385
+ capabilities['code-gen'] = {
386
+ providers: codeGenProviders,
387
+ };
388
+ }
389
+
390
+ return capabilities;
391
+ }
392
+
393
+ /**
394
+ * Build routing table from detected CLIs and devserver
395
+ * @param {Object} detected - Detected CLI info
396
+ * @param {Object} devserver - Devserver status
397
+ * @returns {Object} Routing table
398
+ */
399
+ function buildRoutingTable(detected, devserver) {
400
+ const table = {};
401
+
402
+ // CLI providers
403
+ for (const name of ['claude', 'codex', 'gemini']) {
404
+ table[name] = {
405
+ local: detected[name]?.detected || false,
406
+ devserver: devserver.connected,
407
+ preferred: detected[name]?.detected ? 'local' : 'devserver',
408
+ };
409
+ }
410
+
411
+ // API providers
412
+ table.deepseek = {
413
+ local: false,
414
+ devserver: true,
415
+ preferred: 'devserver',
416
+ };
417
+
418
+ return table;
419
+ }