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,336 @@
1
+ import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest';
2
+ import {
3
+ createAPIProvider,
4
+ callAPI,
5
+ parseResponse,
6
+ calculateCost,
7
+ API_PRICING,
8
+ } from './api-provider.js';
9
+
10
+ // Mock fetch
11
+ global.fetch = vi.fn();
12
+
13
+ describe('api-provider', () => {
14
+ beforeEach(() => {
15
+ vi.clearAllMocks();
16
+ });
17
+
18
+ describe('createAPIProvider', () => {
19
+ it('creates provider with API type', () => {
20
+ const provider = createAPIProvider({
21
+ name: 'deepseek',
22
+ baseUrl: 'https://api.deepseek.com',
23
+ model: 'deepseek-coder',
24
+ capabilities: ['review'],
25
+ });
26
+
27
+ expect(provider.type).toBe('api');
28
+ expect(provider.name).toBe('deepseek');
29
+ });
30
+
31
+ it('sets devserverOnly to true by default', () => {
32
+ const provider = createAPIProvider({
33
+ name: 'deepseek',
34
+ baseUrl: 'https://api.deepseek.com',
35
+ });
36
+
37
+ expect(provider.devserverOnly).toBe(true);
38
+ });
39
+
40
+ it('stores baseUrl and model', () => {
41
+ const provider = createAPIProvider({
42
+ name: 'deepseek',
43
+ baseUrl: 'https://api.deepseek.com',
44
+ model: 'deepseek-coder',
45
+ });
46
+
47
+ expect(provider.baseUrl).toBe('https://api.deepseek.com');
48
+ expect(provider.model).toBe('deepseek-coder');
49
+ });
50
+ });
51
+
52
+ describe('callAPI', () => {
53
+ it('calls baseUrl/v1/chat/completions', async () => {
54
+ global.fetch.mockResolvedValue({
55
+ ok: true,
56
+ json: () => Promise.resolve({
57
+ choices: [{ message: { content: '{"result": "ok"}' } }],
58
+ usage: { prompt_tokens: 100, completion_tokens: 50 },
59
+ }),
60
+ });
61
+
62
+ await callAPI({
63
+ baseUrl: 'https://api.deepseek.com',
64
+ model: 'deepseek-coder',
65
+ prompt: 'test',
66
+ apiKey: 'sk-test',
67
+ });
68
+
69
+ expect(global.fetch).toHaveBeenCalledWith(
70
+ 'https://api.deepseek.com/v1/chat/completions',
71
+ expect.any(Object)
72
+ );
73
+ });
74
+
75
+ it('sets Authorization header', async () => {
76
+ global.fetch.mockResolvedValue({
77
+ ok: true,
78
+ json: () => Promise.resolve({
79
+ choices: [{ message: { content: '{}' } }],
80
+ usage: { prompt_tokens: 100, completion_tokens: 50 },
81
+ }),
82
+ });
83
+
84
+ await callAPI({
85
+ baseUrl: 'https://api.deepseek.com',
86
+ model: 'deepseek-coder',
87
+ prompt: 'test',
88
+ apiKey: 'sk-test-key',
89
+ });
90
+
91
+ expect(global.fetch).toHaveBeenCalledWith(
92
+ expect.any(String),
93
+ expect.objectContaining({
94
+ headers: expect.objectContaining({
95
+ 'Authorization': 'Bearer sk-test-key',
96
+ }),
97
+ })
98
+ );
99
+ });
100
+
101
+ it('sends model in body', async () => {
102
+ global.fetch.mockResolvedValue({
103
+ ok: true,
104
+ json: () => Promise.resolve({
105
+ choices: [{ message: { content: '{}' } }],
106
+ usage: { prompt_tokens: 100, completion_tokens: 50 },
107
+ }),
108
+ });
109
+
110
+ await callAPI({
111
+ baseUrl: 'https://api.deepseek.com',
112
+ model: 'deepseek-coder',
113
+ prompt: 'test',
114
+ apiKey: 'sk-test',
115
+ });
116
+
117
+ const callArgs = global.fetch.mock.calls[0];
118
+ const body = JSON.parse(callArgs[1].body);
119
+
120
+ expect(body.model).toBe('deepseek-coder');
121
+ });
122
+
123
+ it('includes response_format when schema provided', async () => {
124
+ const schema = {
125
+ type: 'object',
126
+ properties: {
127
+ result: { type: 'string' },
128
+ },
129
+ };
130
+
131
+ global.fetch.mockResolvedValue({
132
+ ok: true,
133
+ json: () => Promise.resolve({
134
+ choices: [{ message: { content: '{"result": "ok"}' } }],
135
+ usage: { prompt_tokens: 100, completion_tokens: 50 },
136
+ }),
137
+ });
138
+
139
+ await callAPI({
140
+ baseUrl: 'https://api.deepseek.com',
141
+ model: 'deepseek-coder',
142
+ prompt: 'test',
143
+ apiKey: 'sk-test',
144
+ outputSchema: schema,
145
+ });
146
+
147
+ const callArgs = global.fetch.mock.calls[0];
148
+ const body = JSON.parse(callArgs[1].body);
149
+
150
+ expect(body.response_format).toBeDefined();
151
+ expect(body.response_format.type).toBe('json_schema');
152
+ });
153
+ });
154
+
155
+ describe('parseResponse', () => {
156
+ it('extracts content from response', () => {
157
+ const response = {
158
+ choices: [{ message: { content: '{"result": "ok"}' } }],
159
+ usage: { prompt_tokens: 100, completion_tokens: 50 },
160
+ };
161
+
162
+ const result = parseResponse(response);
163
+
164
+ expect(result.raw).toBe('{"result": "ok"}');
165
+ });
166
+
167
+ it('extracts token usage', () => {
168
+ const response = {
169
+ choices: [{ message: { content: '{}' } }],
170
+ usage: { prompt_tokens: 100, completion_tokens: 50 },
171
+ };
172
+
173
+ const result = parseResponse(response);
174
+
175
+ expect(result.tokenUsage).toEqual({ input: 100, output: 50 });
176
+ });
177
+
178
+ it('parses JSON content', () => {
179
+ const response = {
180
+ choices: [{ message: { content: '{"score": 85}' } }],
181
+ usage: { prompt_tokens: 100, completion_tokens: 50 },
182
+ };
183
+
184
+ const result = parseResponse(response);
185
+
186
+ expect(result.parsed).toEqual({ score: 85 });
187
+ });
188
+
189
+ it('handles non-JSON content', () => {
190
+ const response = {
191
+ choices: [{ message: { content: 'Plain text response' } }],
192
+ usage: { prompt_tokens: 100, completion_tokens: 50 },
193
+ };
194
+
195
+ const result = parseResponse(response);
196
+
197
+ expect(result.raw).toBe('Plain text response');
198
+ expect(result.parsed).toBeNull();
199
+ });
200
+ });
201
+
202
+ describe('calculateCost', () => {
203
+ it('uses provider pricing', () => {
204
+ const cost = calculateCost(
205
+ { input: 1000, output: 500 },
206
+ { input: 0.001, output: 0.002 }
207
+ );
208
+
209
+ // (1000 * 0.001 + 500 * 0.002) / 1000 = 0.002
210
+ expect(cost).toBeCloseTo(0.002);
211
+ });
212
+
213
+ it('handles zero tokens', () => {
214
+ const cost = calculateCost(
215
+ { input: 0, output: 0 },
216
+ { input: 0.001, output: 0.002 }
217
+ );
218
+
219
+ expect(cost).toBe(0);
220
+ });
221
+
222
+ it('returns null when no pricing', () => {
223
+ const cost = calculateCost({ input: 1000, output: 500 }, null);
224
+
225
+ expect(cost).toBeNull();
226
+ });
227
+ });
228
+
229
+ describe('rate limit handling', () => {
230
+ it('retries on rate limit', async () => {
231
+ let attempts = 0;
232
+ global.fetch.mockImplementation(() => {
233
+ attempts++;
234
+ if (attempts === 1) {
235
+ return Promise.resolve({
236
+ ok: false,
237
+ status: 429,
238
+ headers: { get: () => '1' }, // Retry-After
239
+ });
240
+ }
241
+ return Promise.resolve({
242
+ ok: true,
243
+ json: () => Promise.resolve({
244
+ choices: [{ message: { content: '{}' } }],
245
+ usage: { prompt_tokens: 10, completion_tokens: 5 },
246
+ }),
247
+ });
248
+ });
249
+
250
+ const result = await callAPI({
251
+ baseUrl: 'https://api.example.com',
252
+ model: 'test',
253
+ prompt: 'test',
254
+ apiKey: 'sk-test',
255
+ retryDelay: 10,
256
+ });
257
+
258
+ expect(attempts).toBe(2);
259
+ expect(result.exitCode).toBe(0);
260
+ });
261
+ });
262
+
263
+ describe('error handling', () => {
264
+ it('handles network errors gracefully', async () => {
265
+ global.fetch.mockRejectedValue(new Error('Network error'));
266
+
267
+ const result = await callAPI({
268
+ baseUrl: 'https://api.example.com',
269
+ model: 'test',
270
+ prompt: 'test',
271
+ apiKey: 'sk-test',
272
+ maxRetries: 1,
273
+ });
274
+
275
+ expect(result.exitCode).not.toBe(0);
276
+ expect(result.error).toBeDefined();
277
+ });
278
+
279
+ it('handles API errors', async () => {
280
+ global.fetch.mockResolvedValue({
281
+ ok: false,
282
+ status: 500,
283
+ statusText: 'Internal Server Error',
284
+ json: () => Promise.resolve({ error: { message: 'Server error' } }),
285
+ });
286
+
287
+ const result = await callAPI({
288
+ baseUrl: 'https://api.example.com',
289
+ model: 'test',
290
+ prompt: 'test',
291
+ apiKey: 'sk-test',
292
+ maxRetries: 1,
293
+ });
294
+
295
+ expect(result.exitCode).not.toBe(0);
296
+ });
297
+ });
298
+
299
+ describe('DeepSeek support', () => {
300
+ it('supports DeepSeek endpoint', async () => {
301
+ global.fetch.mockResolvedValue({
302
+ ok: true,
303
+ json: () => Promise.resolve({
304
+ choices: [{ message: { content: '{"review": "LGTM"}' } }],
305
+ usage: { prompt_tokens: 500, completion_tokens: 100 },
306
+ }),
307
+ });
308
+
309
+ const result = await callAPI({
310
+ baseUrl: 'https://api.deepseek.com',
311
+ model: 'deepseek-coder',
312
+ prompt: 'Review this code',
313
+ apiKey: 'sk-deepseek-key',
314
+ });
315
+
316
+ expect(result.parsed).toEqual({ review: 'LGTM' });
317
+ expect(result.tokenUsage).toEqual({ input: 500, output: 100 });
318
+ });
319
+ });
320
+
321
+ describe('API_PRICING', () => {
322
+ it('has pricing for deepseek', () => {
323
+ expect(API_PRICING.deepseek).toBeDefined();
324
+ expect(API_PRICING.deepseek.input).toBeDefined();
325
+ expect(API_PRICING.deepseek.output).toBeDefined();
326
+ });
327
+
328
+ it('has pricing for mistral', () => {
329
+ expect(API_PRICING.mistral).toBeDefined();
330
+ });
331
+
332
+ it('has default pricing', () => {
333
+ expect(API_PRICING.default).toBeDefined();
334
+ });
335
+ });
336
+ });
@@ -0,0 +1,166 @@
1
+ /**
2
+ * CLI Detector - Detects locally installed AI CLI tools
3
+ *
4
+ * Supports detection of:
5
+ * - claude (Claude Code)
6
+ * - codex (Codex CLI)
7
+ * - gemini (Gemini CLI)
8
+ */
9
+
10
+ import { execSync } from 'child_process';
11
+
12
+ /**
13
+ * CLI tool configurations
14
+ */
15
+ export const CLI_TOOLS = {
16
+ claude: {
17
+ command: 'claude',
18
+ headlessArgs: ['-p', '--output-format', 'json'],
19
+ capabilities: ['review', 'code-gen', 'refactor', 'explain', 'test-gen'],
20
+ versionFlag: '--version',
21
+ versionParser: (output) => {
22
+ const match = output.match(/v?(\d+\.\d+\.\d+)/);
23
+ return match ? match[0] : output.trim();
24
+ },
25
+ },
26
+ codex: {
27
+ command: 'codex',
28
+ headlessArgs: ['exec', '--json', '--sandbox', 'read-only'],
29
+ capabilities: ['review', 'code-gen', 'refactor', 'explain'],
30
+ versionFlag: '--version',
31
+ versionParser: (output) => {
32
+ const match = output.match(/(\d+\.\d+\.\d+)/);
33
+ return match ? match[0] : output.trim();
34
+ },
35
+ },
36
+ gemini: {
37
+ command: 'gemini',
38
+ headlessArgs: ['-p', '--output-format', 'json'],
39
+ capabilities: ['design', 'image-gen', 'vision', 'review', 'explain'],
40
+ versionFlag: '--version',
41
+ versionParser: (output) => {
42
+ const match = output.match(/(\d+\.\d+\.\d+)/);
43
+ return match ? match[0] : output.trim();
44
+ },
45
+ },
46
+ };
47
+
48
+ // Detection cache
49
+ let detectionCache = null;
50
+ let cacheTimestamp = 0;
51
+ const CACHE_TTL = 60000; // 1 minute
52
+
53
+ /**
54
+ * Get the 'which' command based on platform
55
+ * @returns {string} Command to find executables
56
+ */
57
+ function getWhichCommand() {
58
+ return process.platform === 'win32' ? 'where' : 'which';
59
+ }
60
+
61
+ /**
62
+ * Execute a command with timeout
63
+ * @param {string} command - Command to execute
64
+ * @param {number} timeout - Timeout in ms
65
+ * @returns {string|null} Output or null on failure
66
+ */
67
+ function execWithTimeout(command, timeout = 5000) {
68
+ try {
69
+ const result = execSync(command, {
70
+ timeout,
71
+ encoding: 'utf8',
72
+ stdio: ['pipe', 'pipe', 'pipe'],
73
+ });
74
+ return result.toString().trim();
75
+ } catch (err) {
76
+ return null;
77
+ }
78
+ }
79
+
80
+ /**
81
+ * Detect a single CLI tool
82
+ * @param {string} name - CLI tool name (claude, codex, gemini)
83
+ * @returns {Promise<Object|null>} Detection result or null if not found
84
+ */
85
+ export async function detectCLI(name) {
86
+ const tool = CLI_TOOLS[name];
87
+ if (!tool) {
88
+ return null;
89
+ }
90
+
91
+ const whichCmd = getWhichCommand();
92
+
93
+ // Check if command exists
94
+ const path = execWithTimeout(`${whichCmd} ${tool.command}`);
95
+ if (!path) {
96
+ return null;
97
+ }
98
+
99
+ // Get version
100
+ let version = 'unknown';
101
+ try {
102
+ const versionOutput = execWithTimeout(`${tool.command} ${tool.versionFlag}`);
103
+ if (versionOutput) {
104
+ version = tool.versionParser(versionOutput);
105
+ }
106
+ } catch (err) {
107
+ // Version detection failed, but CLI exists
108
+ }
109
+
110
+ return {
111
+ name,
112
+ command: tool.command,
113
+ path: path.split('\n')[0].trim(),
114
+ version,
115
+ capabilities: tool.capabilities,
116
+ headlessArgs: tool.headlessArgs,
117
+ };
118
+ }
119
+
120
+ /**
121
+ * Detect all CLI tools
122
+ * @param {boolean} [useCache=true] - Whether to use cached results
123
+ * @returns {Promise<Map<string, Object>>} Map of detected CLIs
124
+ */
125
+ export async function detectAllCLIs(useCache = true) {
126
+ const now = Date.now();
127
+
128
+ // Return cached results if valid
129
+ if (useCache && detectionCache && (now - cacheTimestamp) < CACHE_TTL) {
130
+ return detectionCache;
131
+ }
132
+
133
+ const detected = new Map();
134
+
135
+ // Detect each CLI tool
136
+ for (const name of Object.keys(CLI_TOOLS)) {
137
+ const result = await detectCLI(name);
138
+ if (result) {
139
+ detected.set(name, result);
140
+ }
141
+ }
142
+
143
+ // Update cache
144
+ detectionCache = detected;
145
+ cacheTimestamp = now;
146
+
147
+ return detected;
148
+ }
149
+
150
+ /**
151
+ * Clear the detection cache
152
+ */
153
+ export function clearCache() {
154
+ detectionCache = null;
155
+ cacheTimestamp = 0;
156
+ }
157
+
158
+ /**
159
+ * Get capabilities for a CLI tool
160
+ * @param {string} name - CLI tool name
161
+ * @returns {string[]} List of capabilities
162
+ */
163
+ export function getCapabilities(name) {
164
+ const tool = CLI_TOOLS[name];
165
+ return tool ? [...tool.capabilities] : [];
166
+ }