tlc-claude-code 1.4.4 → 1.4.5

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 (70) hide show
  1. package/dashboard/dist/App.js +28 -2
  2. package/dashboard/dist/api/health-diagnostics.d.ts +26 -0
  3. package/dashboard/dist/api/health-diagnostics.js +85 -0
  4. package/dashboard/dist/api/health-diagnostics.test.d.ts +1 -0
  5. package/dashboard/dist/api/health-diagnostics.test.js +126 -0
  6. package/dashboard/dist/api/index.d.ts +5 -0
  7. package/dashboard/dist/api/index.js +5 -0
  8. package/dashboard/dist/api/notes-api.d.ts +18 -0
  9. package/dashboard/dist/api/notes-api.js +68 -0
  10. package/dashboard/dist/api/notes-api.test.d.ts +1 -0
  11. package/dashboard/dist/api/notes-api.test.js +113 -0
  12. package/dashboard/dist/api/safeFetch.d.ts +50 -0
  13. package/dashboard/dist/api/safeFetch.js +135 -0
  14. package/dashboard/dist/api/safeFetch.test.d.ts +1 -0
  15. package/dashboard/dist/api/safeFetch.test.js +215 -0
  16. package/dashboard/dist/api/tasks-api.d.ts +32 -0
  17. package/dashboard/dist/api/tasks-api.js +98 -0
  18. package/dashboard/dist/api/tasks-api.test.d.ts +1 -0
  19. package/dashboard/dist/api/tasks-api.test.js +383 -0
  20. package/dashboard/dist/components/BugsPane.d.ts +20 -0
  21. package/dashboard/dist/components/BugsPane.js +210 -0
  22. package/dashboard/dist/components/BugsPane.test.d.ts +1 -0
  23. package/dashboard/dist/components/BugsPane.test.js +256 -0
  24. package/dashboard/dist/components/HealthPane.d.ts +3 -1
  25. package/dashboard/dist/components/HealthPane.js +44 -6
  26. package/dashboard/dist/components/HealthPane.test.js +105 -2
  27. package/dashboard/dist/components/RouterPane.d.ts +4 -3
  28. package/dashboard/dist/components/RouterPane.js +60 -57
  29. package/dashboard/dist/components/RouterPane.test.js +150 -96
  30. package/dashboard/dist/components/UpdateBanner.d.ts +26 -0
  31. package/dashboard/dist/components/UpdateBanner.js +30 -0
  32. package/dashboard/dist/components/UpdateBanner.test.d.ts +1 -0
  33. package/dashboard/dist/components/UpdateBanner.test.js +96 -0
  34. package/dashboard/dist/components/ui/EmptyState.d.ts +14 -0
  35. package/dashboard/dist/components/ui/EmptyState.js +58 -0
  36. package/dashboard/dist/components/ui/EmptyState.test.d.ts +1 -0
  37. package/dashboard/dist/components/ui/EmptyState.test.js +97 -0
  38. package/dashboard/dist/components/ui/ErrorState.d.ts +17 -0
  39. package/dashboard/dist/components/ui/ErrorState.js +80 -0
  40. package/dashboard/dist/components/ui/ErrorState.test.d.ts +1 -0
  41. package/dashboard/dist/components/ui/ErrorState.test.js +166 -0
  42. package/dashboard/package.json +3 -0
  43. package/package.json +1 -1
  44. package/server/dashboard/index.html +205 -8
  45. package/server/index.js +64 -0
  46. package/server/lib/api-provider.js +104 -186
  47. package/server/lib/api-provider.test.js +238 -336
  48. package/server/lib/cli-detector.js +90 -166
  49. package/server/lib/cli-detector.test.js +114 -269
  50. package/server/lib/cli-provider.js +142 -212
  51. package/server/lib/cli-provider.test.js +196 -349
  52. package/server/lib/debug.test.js +1 -1
  53. package/server/lib/devserver-router-api.js +54 -249
  54. package/server/lib/devserver-router-api.test.js +126 -426
  55. package/server/lib/introspect.js +309 -0
  56. package/server/lib/introspect.test.js +286 -0
  57. package/server/lib/model-router.js +107 -245
  58. package/server/lib/model-router.test.js +122 -313
  59. package/server/lib/output-schemas.js +146 -269
  60. package/server/lib/output-schemas.test.js +106 -307
  61. package/server/lib/provider-interface.js +99 -153
  62. package/server/lib/provider-interface.test.js +228 -394
  63. package/server/lib/provider-queue.js +164 -158
  64. package/server/lib/provider-queue.test.js +186 -315
  65. package/server/lib/router-config.js +99 -221
  66. package/server/lib/router-config.test.js +83 -237
  67. package/server/lib/router-setup-command.js +94 -419
  68. package/server/lib/router-setup-command.test.js +96 -375
  69. package/server/lib/router-status-api.js +93 -0
  70. package/server/lib/router-status-api.test.js +270 -0
@@ -1,313 +1,122 @@
1
- import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest';
2
- import {
3
- createRouter,
4
- resolveProvider,
5
- resolveCapability,
6
- loadConfig,
7
- DEFAULT_CONFIG,
8
- } from './model-router.js';
9
-
10
- // Mock dependencies
11
- vi.mock('./cli-detector.js', () => ({
12
- detectAllCLIs: vi.fn(),
13
- clearCache: vi.fn(),
14
- }));
15
-
16
- vi.mock('fs/promises', () => ({
17
- default: {
18
- readFile: vi.fn(),
19
- },
20
- }));
21
-
22
- import { detectAllCLIs } from './cli-detector.js';
23
- import fs from 'fs/promises';
24
-
25
- describe('model-router', () => {
26
- beforeEach(() => {
27
- vi.clearAllMocks();
28
- });
29
-
30
- describe('createRouter', () => {
31
- it('creates router with default config', async () => {
32
- detectAllCLIs.mockResolvedValue(new Map());
33
-
34
- const router = await createRouter();
35
-
36
- expect(router).toBeDefined();
37
- expect(router.resolveProvider).toBeDefined();
38
- expect(router.resolveCapability).toBeDefined();
39
- });
40
-
41
- it('creates router with custom config', async () => {
42
- detectAllCLIs.mockResolvedValue(new Map());
43
-
44
- const config = {
45
- providers: {
46
- claude: { type: 'cli', command: 'claude' },
47
- },
48
- capabilities: {
49
- review: { providers: ['claude'] },
50
- },
51
- };
52
-
53
- const router = await createRouter(config);
54
-
55
- expect(router).toBeDefined();
56
- });
57
-
58
- it('detects local CLIs on creation', async () => {
59
- detectAllCLIs.mockResolvedValue(new Map([
60
- ['claude', { name: 'claude', version: 'v4.0.0' }],
61
- ]));
62
-
63
- await createRouter();
64
-
65
- expect(detectAllCLIs).toHaveBeenCalled();
66
- });
67
- });
68
-
69
- describe('resolveProvider', () => {
70
- it('returns local when CLI detected', async () => {
71
- detectAllCLIs.mockResolvedValue(new Map([
72
- ['claude', { name: 'claude', version: 'v4.0.0', detected: true }],
73
- ]));
74
-
75
- const router = await createRouter({
76
- providers: {
77
- claude: { type: 'cli', command: 'claude', capabilities: ['review'] },
78
- },
79
- });
80
-
81
- const result = router.resolveProvider('claude');
82
-
83
- expect(result.via).toBe('local');
84
- expect(result.provider.name).toBe('claude');
85
- });
86
-
87
- it('returns devserver when CLI not detected', async () => {
88
- detectAllCLIs.mockResolvedValue(new Map()); // No CLIs detected
89
-
90
- const router = await createRouter({
91
- providers: {
92
- claude: { type: 'cli', command: 'claude', capabilities: ['review'] },
93
- },
94
- devserver: { url: 'https://devserver.example.com' },
95
- });
96
-
97
- const result = router.resolveProvider('claude');
98
-
99
- expect(result.via).toBe('devserver');
100
- });
101
-
102
- it('returns devserver for API type', async () => {
103
- detectAllCLIs.mockResolvedValue(new Map());
104
-
105
- const router = await createRouter({
106
- providers: {
107
- deepseek: {
108
- type: 'api',
109
- baseUrl: 'https://api.deepseek.com',
110
- capabilities: ['review'],
111
- },
112
- },
113
- });
114
-
115
- const result = router.resolveProvider('deepseek');
116
-
117
- expect(result.via).toBe('devserver');
118
- });
119
-
120
- it('returns null for unknown provider', async () => {
121
- detectAllCLIs.mockResolvedValue(new Map());
122
-
123
- const router = await createRouter();
124
-
125
- const result = router.resolveProvider('unknown');
126
-
127
- expect(result).toBeNull();
128
- });
129
- });
130
-
131
- describe('resolveCapability', () => {
132
- it('returns ordered providers for capability', async () => {
133
- detectAllCLIs.mockResolvedValue(new Map([
134
- ['claude', { name: 'claude' }],
135
- ]));
136
-
137
- const router = await createRouter({
138
- providers: {
139
- claude: { type: 'cli', command: 'claude', capabilities: ['review'] },
140
- deepseek: { type: 'api', baseUrl: 'https://api.deepseek.com', capabilities: ['review'] },
141
- },
142
- capabilities: {
143
- review: { providers: ['claude', 'deepseek'] },
144
- },
145
- });
146
-
147
- const providers = router.resolveCapability('review');
148
-
149
- expect(providers.length).toBe(2);
150
- expect(providers[0].name).toBe('claude');
151
- expect(providers[1].name).toBe('deepseek');
152
- });
153
-
154
- it('filters by capability', async () => {
155
- detectAllCLIs.mockResolvedValue(new Map());
156
-
157
- const router = await createRouter({
158
- providers: {
159
- claude: { type: 'cli', command: 'claude', capabilities: ['review', 'code-gen'] },
160
- gemini: { type: 'cli', command: 'gemini', capabilities: ['design'] },
161
- },
162
- capabilities: {
163
- review: { providers: ['claude'] },
164
- design: { providers: ['gemini'] },
165
- },
166
- });
167
-
168
- const reviewProviders = router.resolveCapability('review');
169
- const designProviders = router.resolveCapability('design');
170
-
171
- expect(reviewProviders.length).toBe(1);
172
- expect(reviewProviders[0].name).toBe('claude');
173
-
174
- expect(designProviders.length).toBe(1);
175
- expect(designProviders[0].name).toBe('gemini');
176
- });
177
-
178
- it('returns empty array for unknown capability', async () => {
179
- detectAllCLIs.mockResolvedValue(new Map());
180
-
181
- const router = await createRouter();
182
-
183
- const providers = router.resolveCapability('unknown');
184
-
185
- expect(providers).toEqual([]);
186
- });
187
- });
188
-
189
- describe('cascade behavior', () => {
190
- it('tries local first', async () => {
191
- detectAllCLIs.mockResolvedValue(new Map([
192
- ['claude', { name: 'claude', detected: true }],
193
- ]));
194
-
195
- const router = await createRouter({
196
- providers: {
197
- claude: { type: 'cli', command: 'claude', capabilities: ['review'] },
198
- },
199
- devserver: { url: 'https://devserver.example.com' },
200
- });
201
-
202
- const result = router.resolveProvider('claude');
203
-
204
- expect(result.via).toBe('local');
205
- });
206
-
207
- it('falls back to devserver when local unavailable', async () => {
208
- detectAllCLIs.mockResolvedValue(new Map()); // No CLIs detected
209
-
210
- const router = await createRouter({
211
- providers: {
212
- claude: { type: 'cli', command: 'claude', capabilities: ['review'] },
213
- },
214
- devserver: { url: 'https://devserver.example.com' },
215
- });
216
-
217
- const result = router.resolveProvider('claude');
218
-
219
- expect(result.via).toBe('devserver');
220
- });
221
- });
222
-
223
- describe('loadConfig', () => {
224
- it('reads from .tlc.json', async () => {
225
- const config = {
226
- router: {
227
- providers: { claude: { type: 'cli', command: 'claude' } },
228
- },
229
- };
230
-
231
- fs.readFile.mockResolvedValue(JSON.stringify(config));
232
-
233
- const loaded = await loadConfig('/project');
234
-
235
- expect(fs.readFile).toHaveBeenCalledWith('/project/.tlc.json', 'utf8');
236
- expect(loaded.providers.claude).toBeDefined();
237
- });
238
-
239
- it('uses defaults when file missing', async () => {
240
- fs.readFile.mockRejectedValue(new Error('ENOENT'));
241
-
242
- const loaded = await loadConfig('/project');
243
-
244
- expect(loaded).toEqual(DEFAULT_CONFIG);
245
- });
246
-
247
- it('merges with defaults', async () => {
248
- const config = {
249
- router: {
250
- providers: { custom: { type: 'api', baseUrl: 'https://example.com' } },
251
- },
252
- };
253
-
254
- fs.readFile.mockResolvedValue(JSON.stringify(config));
255
-
256
- const loaded = await loadConfig('/project');
257
-
258
- // Should have custom provider
259
- expect(loaded.providers.custom).toBeDefined();
260
- // Should still have defaults
261
- expect(loaded.providers.claude).toBeDefined();
262
- });
263
- });
264
-
265
- describe('DEFAULT_CONFIG', () => {
266
- it('includes claude provider', () => {
267
- expect(DEFAULT_CONFIG.providers.claude).toBeDefined();
268
- expect(DEFAULT_CONFIG.providers.claude.type).toBe('cli');
269
- });
270
-
271
- it('includes codex provider', () => {
272
- expect(DEFAULT_CONFIG.providers.codex).toBeDefined();
273
- expect(DEFAULT_CONFIG.providers.codex.type).toBe('cli');
274
- });
275
-
276
- it('includes gemini provider', () => {
277
- expect(DEFAULT_CONFIG.providers.gemini).toBeDefined();
278
- expect(DEFAULT_CONFIG.providers.gemini.type).toBe('cli');
279
- });
280
-
281
- it('includes deepseek provider', () => {
282
- expect(DEFAULT_CONFIG.providers.deepseek).toBeDefined();
283
- expect(DEFAULT_CONFIG.providers.deepseek.type).toBe('api');
284
- });
285
-
286
- it('includes review capability', () => {
287
- expect(DEFAULT_CONFIG.capabilities.review).toBeDefined();
288
- expect(DEFAULT_CONFIG.capabilities.review.providers).toContain('claude');
289
- });
290
- });
291
-
292
- describe('handleUnavailable', () => {
293
- it('skips unavailable providers', async () => {
294
- detectAllCLIs.mockResolvedValue(new Map()); // No local CLIs
295
-
296
- const router = await createRouter({
297
- providers: {
298
- claude: { type: 'cli', command: 'claude', capabilities: ['review'] },
299
- deepseek: { type: 'api', baseUrl: 'https://api.deepseek.com', capabilities: ['review'] },
300
- },
301
- capabilities: {
302
- review: { providers: ['claude', 'deepseek'] },
303
- },
304
- devserver: { url: 'https://devserver.example.com' },
305
- });
306
-
307
- const providers = router.resolveCapability('review');
308
-
309
- // Both should be available (claude via devserver, deepseek via api)
310
- expect(providers.length).toBe(2);
311
- });
312
- });
313
- });
1
+ import { describe, it, expect, vi, beforeEach } from 'vitest';
2
+ import {
3
+ ModelRouter,
4
+ resolveProvider,
5
+ resolveCapability,
6
+ } from './model-router.js';
7
+
8
+ describe('Model Router', () => {
9
+ let router;
10
+
11
+ beforeEach(() => {
12
+ router = new ModelRouter({
13
+ providers: {
14
+ claude: { type: 'cli', command: 'claude', capabilities: ['review', 'code-gen'] },
15
+ codex: { type: 'cli', command: 'codex', capabilities: ['review', 'code-gen'] },
16
+ deepseek: { type: 'api', baseUrl: 'https://api.deepseek.com', capabilities: ['review'] },
17
+ },
18
+ capabilities: {
19
+ review: { providers: ['claude', 'codex', 'deepseek'] },
20
+ 'code-gen': { providers: ['claude', 'codex'] },
21
+ },
22
+ });
23
+ });
24
+
25
+ describe('resolveProvider', () => {
26
+ it('returns local when CLI detected', async () => {
27
+ router._detectCLI = vi.fn().mockResolvedValue({ found: true, path: '/usr/bin/claude' });
28
+
29
+ const provider = await router.resolveProvider('claude');
30
+
31
+ expect(provider.location).toBe('local');
32
+ });
33
+
34
+ it('returns devserver when CLI not detected', async () => {
35
+ router._detectCLI = vi.fn().mockResolvedValue({ found: false });
36
+ router.devserverUrl = 'https://dev.example.com';
37
+
38
+ const provider = await router.resolveProvider('claude');
39
+
40
+ expect(provider.location).toBe('devserver');
41
+ });
42
+
43
+ it('returns devserver for API type', async () => {
44
+ router.devserverUrl = 'https://dev.example.com';
45
+
46
+ const provider = await router.resolveProvider('deepseek');
47
+
48
+ expect(provider.location).toBe('devserver');
49
+ });
50
+ });
51
+
52
+ describe('resolveCapability', () => {
53
+ it('returns ordered providers', async () => {
54
+ const providers = await router.resolveCapability('review');
55
+
56
+ expect(providers.length).toBeGreaterThan(0);
57
+ expect(providers[0]).toHaveProperty('name');
58
+ });
59
+
60
+ it('filters by capability', async () => {
61
+ const reviewProviders = await router.resolveCapability('review');
62
+ const codeGenProviders = await router.resolveCapability('code-gen');
63
+
64
+ expect(reviewProviders.length).toBe(3);
65
+ expect(codeGenProviders.length).toBe(2);
66
+ });
67
+ });
68
+
69
+ describe('cascade', () => {
70
+ it('tries local first', async () => {
71
+ router._detectCLI = vi.fn().mockResolvedValue({ found: true, path: '/usr/bin/claude' });
72
+
73
+ const provider = await router.resolveProvider('claude');
74
+
75
+ expect(provider.location).toBe('local');
76
+ });
77
+
78
+ it('falls back to devserver', async () => {
79
+ router._detectCLI = vi.fn().mockResolvedValue({ found: false });
80
+ router.devserverUrl = 'https://dev.example.com';
81
+
82
+ const provider = await router.resolveProvider('claude');
83
+
84
+ expect(provider.location).toBe('devserver');
85
+ });
86
+ });
87
+
88
+ describe('loadConfig', () => {
89
+ it('reads from .tlc.json', async () => {
90
+ router._readConfig = vi.fn().mockResolvedValue({
91
+ router: {
92
+ providers: { test: { type: 'cli', command: 'test' } },
93
+ },
94
+ });
95
+
96
+ await router.loadConfig();
97
+
98
+ expect(router.config.providers).toHaveProperty('test');
99
+ });
100
+
101
+ it('uses defaults when missing', async () => {
102
+ router._readConfig = vi.fn().mockResolvedValue({});
103
+
104
+ await router.loadConfig();
105
+
106
+ expect(router.config).toBeDefined();
107
+ });
108
+ });
109
+
110
+ describe('handleUnavailable', () => {
111
+ it('skips to next provider', async () => {
112
+ router._detectCLI = vi.fn()
113
+ .mockResolvedValueOnce({ found: false })
114
+ .mockResolvedValueOnce({ found: true, path: '/usr/bin/codex' });
115
+
116
+ const providers = await router.resolveCapability('review');
117
+ const available = providers.filter(p => p.available);
118
+
119
+ expect(available.length).toBeGreaterThan(0);
120
+ });
121
+ });
122
+ });