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.
- package/dashboard/dist/App.js +229 -35
- package/dashboard/dist/components/AgentRegistryPane.d.ts +35 -0
- package/dashboard/dist/components/AgentRegistryPane.js +89 -0
- package/dashboard/dist/components/AgentRegistryPane.test.d.ts +1 -0
- package/dashboard/dist/components/AgentRegistryPane.test.js +200 -0
- package/dashboard/dist/components/RouterPane.d.ts +5 -0
- package/dashboard/dist/components/RouterPane.js +65 -0
- package/dashboard/dist/components/RouterPane.test.d.ts +1 -0
- package/dashboard/dist/components/RouterPane.test.js +176 -0
- package/dashboard/dist/components/accessibility.test.d.ts +1 -0
- package/dashboard/dist/components/accessibility.test.js +116 -0
- package/dashboard/dist/components/layout/MobileNav.d.ts +16 -0
- package/dashboard/dist/components/layout/MobileNav.js +31 -0
- package/dashboard/dist/components/layout/MobileNav.test.d.ts +1 -0
- package/dashboard/dist/components/layout/MobileNav.test.js +111 -0
- package/dashboard/dist/components/performance.test.d.ts +1 -0
- package/dashboard/dist/components/performance.test.js +114 -0
- package/dashboard/dist/components/responsive.test.d.ts +1 -0
- package/dashboard/dist/components/responsive.test.js +114 -0
- package/dashboard/dist/components/ui/Dropdown.d.ts +22 -0
- package/dashboard/dist/components/ui/Dropdown.js +109 -0
- package/dashboard/dist/components/ui/Dropdown.test.d.ts +1 -0
- package/dashboard/dist/components/ui/Dropdown.test.js +105 -0
- package/dashboard/dist/components/ui/Modal.d.ts +13 -0
- package/dashboard/dist/components/ui/Modal.js +25 -0
- package/dashboard/dist/components/ui/Modal.test.d.ts +1 -0
- package/dashboard/dist/components/ui/Modal.test.js +91 -0
- package/dashboard/dist/components/ui/Skeleton.d.ts +32 -0
- package/dashboard/dist/components/ui/Skeleton.js +48 -0
- package/dashboard/dist/components/ui/Skeleton.test.d.ts +1 -0
- package/dashboard/dist/components/ui/Skeleton.test.js +125 -0
- package/dashboard/dist/components/ui/Toast.d.ts +32 -0
- package/dashboard/dist/components/ui/Toast.js +21 -0
- package/dashboard/dist/components/ui/Toast.test.d.ts +1 -0
- package/dashboard/dist/components/ui/Toast.test.js +118 -0
- package/dashboard/dist/hooks/useTheme.d.ts +37 -0
- package/dashboard/dist/hooks/useTheme.js +96 -0
- package/dashboard/dist/hooks/useTheme.test.d.ts +1 -0
- package/dashboard/dist/hooks/useTheme.test.js +94 -0
- package/dashboard/dist/hooks/useWebSocket.d.ts +17 -0
- package/dashboard/dist/hooks/useWebSocket.js +100 -0
- package/dashboard/dist/hooks/useWebSocket.test.d.ts +1 -0
- package/dashboard/dist/hooks/useWebSocket.test.js +115 -0
- package/dashboard/dist/stores/projectStore.d.ts +44 -0
- package/dashboard/dist/stores/projectStore.js +76 -0
- package/dashboard/dist/stores/projectStore.test.d.ts +1 -0
- package/dashboard/dist/stores/projectStore.test.js +114 -0
- package/dashboard/dist/stores/uiStore.d.ts +29 -0
- package/dashboard/dist/stores/uiStore.js +72 -0
- package/dashboard/dist/stores/uiStore.test.d.ts +1 -0
- package/dashboard/dist/stores/uiStore.test.js +93 -0
- package/dashboard/package.json +3 -3
- package/docker-compose.dev.yml +6 -1
- package/package.json +5 -2
- package/server/dashboard/index.html +1336 -779
- package/server/index.js +178 -0
- package/server/lib/agent-cleanup.js +177 -0
- package/server/lib/agent-cleanup.test.js +359 -0
- package/server/lib/agent-hooks.js +126 -0
- package/server/lib/agent-hooks.test.js +303 -0
- package/server/lib/agent-metadata.js +179 -0
- package/server/lib/agent-metadata.test.js +383 -0
- package/server/lib/agent-persistence.js +191 -0
- package/server/lib/agent-persistence.test.js +475 -0
- package/server/lib/agent-registry-command.js +340 -0
- package/server/lib/agent-registry-command.test.js +334 -0
- package/server/lib/agent-registry.js +155 -0
- package/server/lib/agent-registry.test.js +239 -0
- package/server/lib/agent-state.js +236 -0
- package/server/lib/agent-state.test.js +375 -0
- package/server/lib/api-provider.js +186 -0
- package/server/lib/api-provider.test.js +336 -0
- package/server/lib/cli-detector.js +166 -0
- package/server/lib/cli-detector.test.js +269 -0
- package/server/lib/cli-provider.js +212 -0
- package/server/lib/cli-provider.test.js +349 -0
- package/server/lib/debug.test.js +62 -0
- package/server/lib/devserver-router-api.js +249 -0
- package/server/lib/devserver-router-api.test.js +426 -0
- package/server/lib/model-router.js +245 -0
- package/server/lib/model-router.test.js +313 -0
- package/server/lib/output-schemas.js +269 -0
- package/server/lib/output-schemas.test.js +307 -0
- package/server/lib/provider-interface.js +153 -0
- package/server/lib/provider-interface.test.js +394 -0
- package/server/lib/provider-queue.js +158 -0
- package/server/lib/provider-queue.test.js +315 -0
- package/server/lib/router-config.js +221 -0
- package/server/lib/router-config.test.js +237 -0
- package/server/lib/router-setup-command.js +419 -0
- 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
|
+
}
|