tlc-claude-code 1.4.1 → 1.4.2

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 (46) 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/package.json +5 -2
  11. package/server/index.js +178 -0
  12. package/server/lib/agent-cleanup.js +177 -0
  13. package/server/lib/agent-cleanup.test.js +359 -0
  14. package/server/lib/agent-hooks.js +126 -0
  15. package/server/lib/agent-hooks.test.js +303 -0
  16. package/server/lib/agent-metadata.js +179 -0
  17. package/server/lib/agent-metadata.test.js +383 -0
  18. package/server/lib/agent-persistence.js +191 -0
  19. package/server/lib/agent-persistence.test.js +475 -0
  20. package/server/lib/agent-registry-command.js +340 -0
  21. package/server/lib/agent-registry-command.test.js +334 -0
  22. package/server/lib/agent-registry.js +155 -0
  23. package/server/lib/agent-registry.test.js +239 -0
  24. package/server/lib/agent-state.js +236 -0
  25. package/server/lib/agent-state.test.js +375 -0
  26. package/server/lib/api-provider.js +186 -0
  27. package/server/lib/api-provider.test.js +336 -0
  28. package/server/lib/cli-detector.js +166 -0
  29. package/server/lib/cli-detector.test.js +269 -0
  30. package/server/lib/cli-provider.js +212 -0
  31. package/server/lib/cli-provider.test.js +349 -0
  32. package/server/lib/debug.test.js +62 -0
  33. package/server/lib/devserver-router-api.js +249 -0
  34. package/server/lib/devserver-router-api.test.js +426 -0
  35. package/server/lib/model-router.js +245 -0
  36. package/server/lib/model-router.test.js +313 -0
  37. package/server/lib/output-schemas.js +269 -0
  38. package/server/lib/output-schemas.test.js +307 -0
  39. package/server/lib/provider-interface.js +153 -0
  40. package/server/lib/provider-interface.test.js +394 -0
  41. package/server/lib/provider-queue.js +158 -0
  42. package/server/lib/provider-queue.test.js +315 -0
  43. package/server/lib/router-config.js +221 -0
  44. package/server/lib/router-config.test.js +237 -0
  45. package/server/lib/router-setup-command.js +419 -0
  46. package/server/lib/router-setup-command.test.js +375 -0
@@ -0,0 +1,375 @@
1
+ import { describe, it, expect, beforeEach, vi } from 'vitest';
2
+ import {
3
+ AgentState,
4
+ createAgentState,
5
+ STATES,
6
+ VALID_TRANSITIONS,
7
+ } from './agent-state.js';
8
+
9
+ describe('AgentState', () => {
10
+ let state;
11
+
12
+ beforeEach(() => {
13
+ vi.useFakeTimers();
14
+ vi.setSystemTime(new Date('2024-01-15T10:00:00Z'));
15
+ });
16
+
17
+ describe('createAgentState', () => {
18
+ it('starts in pending state', () => {
19
+ state = createAgentState();
20
+
21
+ expect(state.getState()).toBe(STATES.PENDING);
22
+ });
23
+
24
+ it('accepts optional agent ID', () => {
25
+ state = createAgentState({ agentId: 'agent-123' });
26
+
27
+ expect(state.getAgentId()).toBe('agent-123');
28
+ });
29
+
30
+ it('generates ID if not provided', () => {
31
+ state = createAgentState();
32
+
33
+ expect(state.getAgentId()).toBeDefined();
34
+ expect(state.getAgentId()).toMatch(/^state-/);
35
+ });
36
+ });
37
+
38
+ describe('valid transitions', () => {
39
+ beforeEach(() => {
40
+ state = createAgentState();
41
+ });
42
+
43
+ it('transition from pending to running succeeds', () => {
44
+ const result = state.transition(STATES.RUNNING);
45
+
46
+ expect(result.success).toBe(true);
47
+ expect(state.getState()).toBe(STATES.RUNNING);
48
+ });
49
+
50
+ it('transition from running to completed succeeds', () => {
51
+ state.transition(STATES.RUNNING);
52
+
53
+ const result = state.transition(STATES.COMPLETED);
54
+
55
+ expect(result.success).toBe(true);
56
+ expect(state.getState()).toBe(STATES.COMPLETED);
57
+ });
58
+
59
+ it('transition from running to failed succeeds', () => {
60
+ state.transition(STATES.RUNNING);
61
+
62
+ const result = state.transition(STATES.FAILED, { error: 'Task error' });
63
+
64
+ expect(result.success).toBe(true);
65
+ expect(state.getState()).toBe(STATES.FAILED);
66
+ });
67
+
68
+ it('transition from running to cancelled succeeds', () => {
69
+ state.transition(STATES.RUNNING);
70
+
71
+ const result = state.transition(STATES.CANCELLED, { reason: 'User cancelled' });
72
+
73
+ expect(result.success).toBe(true);
74
+ expect(state.getState()).toBe(STATES.CANCELLED);
75
+ });
76
+
77
+ it('transition from pending to cancelled succeeds', () => {
78
+ const result = state.transition(STATES.CANCELLED, { reason: 'Never started' });
79
+
80
+ expect(result.success).toBe(true);
81
+ expect(state.getState()).toBe(STATES.CANCELLED);
82
+ });
83
+ });
84
+
85
+ describe('invalid transitions', () => {
86
+ beforeEach(() => {
87
+ state = createAgentState();
88
+ });
89
+
90
+ it('transition from pending to completed fails', () => {
91
+ const result = state.transition(STATES.COMPLETED);
92
+
93
+ expect(result.success).toBe(false);
94
+ expect(result.error).toContain('Invalid transition');
95
+ expect(state.getState()).toBe(STATES.PENDING);
96
+ });
97
+
98
+ it('transition from completed to running fails', () => {
99
+ state.transition(STATES.RUNNING);
100
+ state.transition(STATES.COMPLETED);
101
+
102
+ const result = state.transition(STATES.RUNNING);
103
+
104
+ expect(result.success).toBe(false);
105
+ expect(result.error).toContain('Invalid transition');
106
+ expect(state.getState()).toBe(STATES.COMPLETED);
107
+ });
108
+
109
+ it('transition from failed to running fails', () => {
110
+ state.transition(STATES.RUNNING);
111
+ state.transition(STATES.FAILED);
112
+
113
+ const result = state.transition(STATES.RUNNING);
114
+
115
+ expect(result.success).toBe(false);
116
+ expect(state.getState()).toBe(STATES.FAILED);
117
+ });
118
+
119
+ it('transition from cancelled to running fails', () => {
120
+ state.transition(STATES.CANCELLED);
121
+
122
+ const result = state.transition(STATES.RUNNING);
123
+
124
+ expect(result.success).toBe(false);
125
+ expect(state.getState()).toBe(STATES.CANCELLED);
126
+ });
127
+
128
+ it('transition to unknown state fails', () => {
129
+ const result = state.transition('unknown-state');
130
+
131
+ expect(result.success).toBe(false);
132
+ expect(result.error).toContain('Unknown state');
133
+ expect(state.getState()).toBe(STATES.PENDING);
134
+ });
135
+ });
136
+
137
+ describe('onTransition callback', () => {
138
+ beforeEach(() => {
139
+ state = createAgentState();
140
+ });
141
+
142
+ it('fires on state change', () => {
143
+ const callback = vi.fn();
144
+ state.onTransition(callback);
145
+
146
+ state.transition(STATES.RUNNING);
147
+
148
+ expect(callback).toHaveBeenCalledTimes(1);
149
+ expect(callback).toHaveBeenCalledWith(
150
+ expect.objectContaining({
151
+ from: STATES.PENDING,
152
+ to: STATES.RUNNING,
153
+ })
154
+ );
155
+ });
156
+
157
+ it('does not fire on failed transition', () => {
158
+ const callback = vi.fn();
159
+ state.onTransition(callback);
160
+
161
+ state.transition(STATES.COMPLETED); // Invalid from pending
162
+
163
+ expect(callback).not.toHaveBeenCalled();
164
+ });
165
+
166
+ it('supports multiple callbacks', () => {
167
+ const callback1 = vi.fn();
168
+ const callback2 = vi.fn();
169
+ state.onTransition(callback1);
170
+ state.onTransition(callback2);
171
+
172
+ state.transition(STATES.RUNNING);
173
+
174
+ expect(callback1).toHaveBeenCalledTimes(1);
175
+ expect(callback2).toHaveBeenCalledTimes(1);
176
+ });
177
+
178
+ it('returns unsubscribe function', () => {
179
+ const callback = vi.fn();
180
+ const unsubscribe = state.onTransition(callback);
181
+
182
+ unsubscribe();
183
+ state.transition(STATES.RUNNING);
184
+
185
+ expect(callback).not.toHaveBeenCalled();
186
+ });
187
+
188
+ it('passes metadata in callback', () => {
189
+ const callback = vi.fn();
190
+ state.onTransition(callback);
191
+
192
+ state.transition(STATES.RUNNING, { taskId: 'task-456' });
193
+
194
+ expect(callback).toHaveBeenCalledWith(
195
+ expect.objectContaining({
196
+ metadata: { taskId: 'task-456' },
197
+ })
198
+ );
199
+ });
200
+ });
201
+
202
+ describe('getHistory', () => {
203
+ beforeEach(() => {
204
+ state = createAgentState();
205
+ });
206
+
207
+ it('returns all transitions', () => {
208
+ state.transition(STATES.RUNNING);
209
+ vi.advanceTimersByTime(1000);
210
+ state.transition(STATES.COMPLETED);
211
+
212
+ const history = state.getHistory();
213
+
214
+ expect(history).toHaveLength(2);
215
+ expect(history[0].from).toBe(STATES.PENDING);
216
+ expect(history[0].to).toBe(STATES.RUNNING);
217
+ expect(history[1].from).toBe(STATES.RUNNING);
218
+ expect(history[1].to).toBe(STATES.COMPLETED);
219
+ });
220
+
221
+ it('includes timestamps for each transition', () => {
222
+ const startTime = Date.now();
223
+ state.transition(STATES.RUNNING);
224
+ vi.advanceTimersByTime(5000);
225
+ state.transition(STATES.COMPLETED);
226
+
227
+ const history = state.getHistory();
228
+
229
+ expect(history[0].timestamp).toBe(startTime);
230
+ expect(history[1].timestamp).toBe(startTime + 5000);
231
+ });
232
+
233
+ it('includes metadata in history', () => {
234
+ state.transition(STATES.RUNNING, { taskId: 'task-789' });
235
+ state.transition(STATES.FAILED, { error: 'Something went wrong' });
236
+
237
+ const history = state.getHistory();
238
+
239
+ expect(history[0].metadata).toEqual({ taskId: 'task-789' });
240
+ expect(history[1].metadata).toEqual({ error: 'Something went wrong' });
241
+ });
242
+
243
+ it('returns empty array when no transitions', () => {
244
+ const history = state.getHistory();
245
+
246
+ expect(history).toEqual([]);
247
+ });
248
+
249
+ it('does not include failed transition attempts', () => {
250
+ state.transition(STATES.COMPLETED); // Invalid - should fail
251
+ state.transition(STATES.RUNNING); // Valid
252
+
253
+ const history = state.getHistory();
254
+
255
+ expect(history).toHaveLength(1);
256
+ expect(history[0].to).toBe(STATES.RUNNING);
257
+ });
258
+ });
259
+
260
+ describe('getElapsedTime', () => {
261
+ beforeEach(() => {
262
+ state = createAgentState();
263
+ });
264
+
265
+ it('calculates duration from creation', () => {
266
+ vi.advanceTimersByTime(3000);
267
+
268
+ const elapsed = state.getElapsedTime();
269
+
270
+ expect(elapsed).toBe(3000);
271
+ });
272
+
273
+ it('calculates duration in specific state', () => {
274
+ state.transition(STATES.RUNNING);
275
+ vi.advanceTimersByTime(5000);
276
+
277
+ const runningTime = state.getElapsedTime(STATES.RUNNING);
278
+
279
+ expect(runningTime).toBe(5000);
280
+ });
281
+
282
+ it('returns 0 for state never entered', () => {
283
+ const failedTime = state.getElapsedTime(STATES.FAILED);
284
+
285
+ expect(failedTime).toBe(0);
286
+ });
287
+
288
+ it('calculates completed state duration', () => {
289
+ state.transition(STATES.RUNNING);
290
+ vi.advanceTimersByTime(2000);
291
+ state.transition(STATES.COMPLETED);
292
+ vi.advanceTimersByTime(1000);
293
+
294
+ const runningTime = state.getElapsedTime(STATES.RUNNING);
295
+
296
+ expect(runningTime).toBe(2000);
297
+ });
298
+
299
+ it('returns total elapsed when no state specified', () => {
300
+ state.transition(STATES.RUNNING);
301
+ vi.advanceTimersByTime(2000);
302
+ state.transition(STATES.COMPLETED);
303
+ vi.advanceTimersByTime(3000);
304
+
305
+ const totalTime = state.getElapsedTime();
306
+
307
+ expect(totalTime).toBe(5000);
308
+ });
309
+ });
310
+
311
+ describe('isTerminal', () => {
312
+ it('returns false for pending', () => {
313
+ state = createAgentState();
314
+
315
+ expect(state.isTerminal()).toBe(false);
316
+ });
317
+
318
+ it('returns false for running', () => {
319
+ state = createAgentState();
320
+ state.transition(STATES.RUNNING);
321
+
322
+ expect(state.isTerminal()).toBe(false);
323
+ });
324
+
325
+ it('returns true for completed', () => {
326
+ state = createAgentState();
327
+ state.transition(STATES.RUNNING);
328
+ state.transition(STATES.COMPLETED);
329
+
330
+ expect(state.isTerminal()).toBe(true);
331
+ });
332
+
333
+ it('returns true for failed', () => {
334
+ state = createAgentState();
335
+ state.transition(STATES.RUNNING);
336
+ state.transition(STATES.FAILED);
337
+
338
+ expect(state.isTerminal()).toBe(true);
339
+ });
340
+
341
+ it('returns true for cancelled', () => {
342
+ state = createAgentState();
343
+ state.transition(STATES.CANCELLED);
344
+
345
+ expect(state.isTerminal()).toBe(true);
346
+ });
347
+ });
348
+
349
+ describe('STATES constants', () => {
350
+ it('exports all state values', () => {
351
+ expect(STATES.PENDING).toBe('pending');
352
+ expect(STATES.RUNNING).toBe('running');
353
+ expect(STATES.COMPLETED).toBe('completed');
354
+ expect(STATES.FAILED).toBe('failed');
355
+ expect(STATES.CANCELLED).toBe('cancelled');
356
+ });
357
+ });
358
+
359
+ describe('VALID_TRANSITIONS', () => {
360
+ it('exports transition map', () => {
361
+ expect(VALID_TRANSITIONS).toBeDefined();
362
+ expect(VALID_TRANSITIONS[STATES.PENDING]).toContain(STATES.RUNNING);
363
+ expect(VALID_TRANSITIONS[STATES.PENDING]).toContain(STATES.CANCELLED);
364
+ expect(VALID_TRANSITIONS[STATES.RUNNING]).toContain(STATES.COMPLETED);
365
+ expect(VALID_TRANSITIONS[STATES.RUNNING]).toContain(STATES.FAILED);
366
+ expect(VALID_TRANSITIONS[STATES.RUNNING]).toContain(STATES.CANCELLED);
367
+ });
368
+
369
+ it('terminal states have no valid transitions', () => {
370
+ expect(VALID_TRANSITIONS[STATES.COMPLETED]).toEqual([]);
371
+ expect(VALID_TRANSITIONS[STATES.FAILED]).toEqual([]);
372
+ expect(VALID_TRANSITIONS[STATES.CANCELLED]).toEqual([]);
373
+ });
374
+ });
375
+ });
@@ -0,0 +1,186 @@
1
+ /**
2
+ * API Provider - Provider implementation for REST API endpoints
3
+ *
4
+ * Supports OpenAI-compatible endpoints:
5
+ * - DeepSeek
6
+ * - Mistral
7
+ * - Any OpenAI-compatible API
8
+ */
9
+
10
+ import { createProvider, PROVIDER_TYPES } from './provider-interface.js';
11
+
12
+ /**
13
+ * API pricing per 1K tokens (USD)
14
+ */
15
+ export const API_PRICING = {
16
+ deepseek: { input: 0.0001, output: 0.0002 },
17
+ 'deepseek-coder': { input: 0.0001, output: 0.0002 },
18
+ mistral: { input: 0.0002, output: 0.0006 },
19
+ 'mistral-large': { input: 0.002, output: 0.006 },
20
+ groq: { input: 0.0001, output: 0.0001 },
21
+ default: { input: 0.001, output: 0.002 },
22
+ };
23
+
24
+ /**
25
+ * Calculate cost from token usage
26
+ * @param {Object} tokenUsage - { input, output }
27
+ * @param {Object} pricing - { input, output } per 1K tokens
28
+ * @returns {number|null} Cost in USD
29
+ */
30
+ export function calculateCost(tokenUsage, pricing) {
31
+ if (!tokenUsage || !pricing) return null;
32
+
33
+ const inputCost = (tokenUsage.input * pricing.input) / 1000;
34
+ const outputCost = (tokenUsage.output * pricing.output) / 1000;
35
+
36
+ return inputCost + outputCost;
37
+ }
38
+
39
+ /**
40
+ * Parse API response
41
+ * @param {Object} response - API response
42
+ * @returns {Object} Parsed result
43
+ */
44
+ export function parseResponse(response) {
45
+ const content = response.choices?.[0]?.message?.content || '';
46
+ const usage = response.usage || {};
47
+
48
+ let parsed = null;
49
+ try {
50
+ parsed = JSON.parse(content);
51
+ } catch (e) {
52
+ // Not JSON, that's ok
53
+ }
54
+
55
+ return {
56
+ raw: content,
57
+ parsed,
58
+ tokenUsage: {
59
+ input: usage.prompt_tokens || 0,
60
+ output: usage.completion_tokens || 0,
61
+ },
62
+ };
63
+ }
64
+
65
+ /**
66
+ * Call an OpenAI-compatible API
67
+ * @param {Object} params - Parameters
68
+ * @param {string} params.baseUrl - API base URL
69
+ * @param {string} params.model - Model name
70
+ * @param {string} params.prompt - The prompt
71
+ * @param {string} params.apiKey - API key
72
+ * @param {Object} [params.outputSchema] - JSON schema for output
73
+ * @param {number} [params.maxRetries=3] - Max retry attempts
74
+ * @param {number} [params.retryDelay=1000] - Retry delay in ms
75
+ * @returns {Promise<Object>} ProviderResult
76
+ */
77
+ export async function callAPI({
78
+ baseUrl,
79
+ model,
80
+ prompt,
81
+ apiKey,
82
+ outputSchema,
83
+ maxRetries = 3,
84
+ retryDelay = 1000,
85
+ }) {
86
+ const url = `${baseUrl}/v1/chat/completions`;
87
+
88
+ const body = {
89
+ model,
90
+ messages: [{ role: 'user', content: prompt }],
91
+ };
92
+
93
+ if (outputSchema) {
94
+ body.response_format = {
95
+ type: 'json_schema',
96
+ json_schema: {
97
+ name: 'response',
98
+ schema: outputSchema,
99
+ },
100
+ };
101
+ }
102
+
103
+ let lastError = null;
104
+
105
+ for (let attempt = 0; attempt < maxRetries; attempt++) {
106
+ try {
107
+ const response = await fetch(url, {
108
+ method: 'POST',
109
+ headers: {
110
+ 'Content-Type': 'application/json',
111
+ 'Authorization': `Bearer ${apiKey}`,
112
+ },
113
+ body: JSON.stringify(body),
114
+ });
115
+
116
+ // Handle rate limiting
117
+ if (response.status === 429) {
118
+ const retryAfter = parseInt(response.headers.get('Retry-After') || '1', 10);
119
+ await new Promise(r => setTimeout(r, retryAfter * 1000 || retryDelay));
120
+ continue;
121
+ }
122
+
123
+ if (!response.ok) {
124
+ const errorBody = await response.json().catch(() => ({}));
125
+ lastError = new Error(errorBody.error?.message || response.statusText);
126
+ continue;
127
+ }
128
+
129
+ const data = await response.json();
130
+ const parsed = parseResponse(data);
131
+
132
+ // Get pricing for cost calculation
133
+ const pricing = API_PRICING[model] || API_PRICING.default;
134
+ const cost = calculateCost(parsed.tokenUsage, pricing);
135
+
136
+ return {
137
+ ...parsed,
138
+ exitCode: 0,
139
+ cost,
140
+ };
141
+ } catch (err) {
142
+ lastError = err;
143
+
144
+ if (attempt < maxRetries - 1) {
145
+ await new Promise(r => setTimeout(r, retryDelay));
146
+ }
147
+ }
148
+ }
149
+
150
+ return {
151
+ raw: '',
152
+ parsed: null,
153
+ exitCode: 1,
154
+ error: lastError?.message || 'API call failed',
155
+ tokenUsage: null,
156
+ cost: null,
157
+ };
158
+ }
159
+
160
+ /**
161
+ * Create an API provider instance
162
+ * @param {Object} config - Provider configuration
163
+ * @returns {Object} Provider instance
164
+ */
165
+ export function createAPIProvider(config) {
166
+ const runner = async (prompt, opts) => {
167
+ if (!config.apiKey && !process.env[`${config.name.toUpperCase()}_API_KEY`]) {
168
+ throw new Error(`API key not configured for ${config.name}`);
169
+ }
170
+
171
+ return callAPI({
172
+ baseUrl: config.baseUrl,
173
+ model: config.model || config.name,
174
+ prompt,
175
+ apiKey: config.apiKey || process.env[`${config.name.toUpperCase()}_API_KEY`],
176
+ outputSchema: opts.outputSchema,
177
+ });
178
+ };
179
+
180
+ return createProvider({
181
+ ...config,
182
+ type: PROVIDER_TYPES.API,
183
+ devserverOnly: config.devserverOnly ?? true,
184
+ runner,
185
+ });
186
+ }