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,303 @@
1
+ import { describe, it, expect, beforeEach, vi } from 'vitest';
2
+ import {
3
+ AgentHooks,
4
+ getAgentHooks,
5
+ resetHooks,
6
+ HOOK_TYPES,
7
+ } from './agent-hooks.js';
8
+
9
+ describe('AgentHooks', () => {
10
+ let hooks;
11
+
12
+ beforeEach(() => {
13
+ resetHooks();
14
+ hooks = new AgentHooks();
15
+ });
16
+
17
+ describe('registerHook', () => {
18
+ it('adds handler to hook type', () => {
19
+ const handler = vi.fn();
20
+
21
+ hooks.registerHook('onStart', handler);
22
+
23
+ const handlers = hooks.getHandlers('onStart');
24
+ expect(handlers).toHaveLength(1);
25
+ expect(handlers[0]).toBe(handler);
26
+ });
27
+
28
+ it('validates hook type', () => {
29
+ const handler = vi.fn();
30
+
31
+ expect(() => hooks.registerHook('invalidHook', handler)).toThrow();
32
+ });
33
+
34
+ it('returns unregister function', () => {
35
+ const handler = vi.fn();
36
+
37
+ const unregister = hooks.registerHook('onStart', handler);
38
+
39
+ expect(typeof unregister).toBe('function');
40
+ });
41
+
42
+ it('allows multiple handlers per hook', () => {
43
+ const handler1 = vi.fn();
44
+ const handler2 = vi.fn();
45
+ const handler3 = vi.fn();
46
+
47
+ hooks.registerHook('onComplete', handler1);
48
+ hooks.registerHook('onComplete', handler2);
49
+ hooks.registerHook('onComplete', handler3);
50
+
51
+ const handlers = hooks.getHandlers('onComplete');
52
+ expect(handlers).toHaveLength(3);
53
+ });
54
+ });
55
+
56
+ describe('triggerHook', () => {
57
+ it('calls all handlers for hook type', async () => {
58
+ const handler1 = vi.fn();
59
+ const handler2 = vi.fn();
60
+
61
+ hooks.registerHook('onStart', handler1);
62
+ hooks.registerHook('onStart', handler2);
63
+
64
+ await hooks.triggerHook('onStart', {});
65
+
66
+ expect(handler1).toHaveBeenCalledTimes(1);
67
+ expect(handler2).toHaveBeenCalledTimes(1);
68
+ });
69
+
70
+ it('passes agent context to handlers', async () => {
71
+ const handler = vi.fn();
72
+ const context = {
73
+ agentId: 'agent-123',
74
+ taskId: 'task-456',
75
+ phase: 'build',
76
+ };
77
+
78
+ hooks.registerHook('onStart', handler);
79
+
80
+ await hooks.triggerHook('onStart', context);
81
+
82
+ expect(handler).toHaveBeenCalledWith(context);
83
+ });
84
+
85
+ it('awaits async handlers', async () => {
86
+ const order = [];
87
+ const asyncHandler = vi.fn(async () => {
88
+ await new Promise(resolve => setTimeout(resolve, 10));
89
+ order.push('async');
90
+ });
91
+ const syncHandler = vi.fn(() => {
92
+ order.push('sync');
93
+ });
94
+
95
+ hooks.registerHook('onComplete', asyncHandler);
96
+ hooks.registerHook('onComplete', syncHandler);
97
+
98
+ await hooks.triggerHook('onComplete', {});
99
+
100
+ expect(asyncHandler).toHaveBeenCalled();
101
+ expect(syncHandler).toHaveBeenCalled();
102
+ // Both should complete before triggerHook returns
103
+ expect(order).toContain('async');
104
+ expect(order).toContain('sync');
105
+ });
106
+
107
+ it('continues on handler error', async () => {
108
+ const errorHandler = vi.fn(() => {
109
+ throw new Error('Handler failed');
110
+ });
111
+ const successHandler = vi.fn();
112
+
113
+ hooks.registerHook('onError', errorHandler);
114
+ hooks.registerHook('onError', successHandler);
115
+
116
+ // Should not throw
117
+ await expect(hooks.triggerHook('onError', {})).resolves.not.toThrow();
118
+
119
+ // Both should have been called
120
+ expect(errorHandler).toHaveBeenCalled();
121
+ expect(successHandler).toHaveBeenCalled();
122
+ });
123
+
124
+ it('returns results from all handlers', async () => {
125
+ const handler1 = vi.fn(() => 'result1');
126
+ const handler2 = vi.fn(() => 'result2');
127
+
128
+ hooks.registerHook('onComplete', handler1);
129
+ hooks.registerHook('onComplete', handler2);
130
+
131
+ const results = await hooks.triggerHook('onComplete', {});
132
+
133
+ expect(results).toHaveLength(2);
134
+ expect(results).toContain('result1');
135
+ expect(results).toContain('result2');
136
+ });
137
+
138
+ it('returns empty array for no handlers', async () => {
139
+ const results = await hooks.triggerHook('onStart', {});
140
+
141
+ expect(results).toEqual([]);
142
+ });
143
+ });
144
+
145
+ describe('removeHook', () => {
146
+ it('removes specific handler', () => {
147
+ const handler1 = vi.fn();
148
+ const handler2 = vi.fn();
149
+
150
+ hooks.registerHook('onStart', handler1);
151
+ const unregister = hooks.registerHook('onStart', handler2);
152
+
153
+ unregister();
154
+
155
+ const handlers = hooks.getHandlers('onStart');
156
+ expect(handlers).toHaveLength(1);
157
+ expect(handlers[0]).toBe(handler1);
158
+ });
159
+
160
+ it('does not affect other handlers', async () => {
161
+ const handler1 = vi.fn();
162
+ const handler2 = vi.fn();
163
+ const handler3 = vi.fn();
164
+
165
+ hooks.registerHook('onComplete', handler1);
166
+ const unregister = hooks.registerHook('onComplete', handler2);
167
+ hooks.registerHook('onComplete', handler3);
168
+
169
+ unregister();
170
+
171
+ await hooks.triggerHook('onComplete', {});
172
+
173
+ expect(handler1).toHaveBeenCalled();
174
+ expect(handler2).not.toHaveBeenCalled();
175
+ expect(handler3).toHaveBeenCalled();
176
+ });
177
+ });
178
+
179
+ describe('clearHooks', () => {
180
+ it('removes all handlers for a hook type', () => {
181
+ hooks.registerHook('onStart', vi.fn());
182
+ hooks.registerHook('onStart', vi.fn());
183
+ hooks.registerHook('onStart', vi.fn());
184
+
185
+ hooks.clearHooks('onStart');
186
+
187
+ expect(hooks.getHandlers('onStart')).toHaveLength(0);
188
+ });
189
+
190
+ it('removes all handlers when no type specified', () => {
191
+ hooks.registerHook('onStart', vi.fn());
192
+ hooks.registerHook('onComplete', vi.fn());
193
+ hooks.registerHook('onError', vi.fn());
194
+ hooks.registerHook('onCancel', vi.fn());
195
+
196
+ hooks.clearHooks();
197
+
198
+ expect(hooks.getHandlers('onStart')).toHaveLength(0);
199
+ expect(hooks.getHandlers('onComplete')).toHaveLength(0);
200
+ expect(hooks.getHandlers('onError')).toHaveLength(0);
201
+ expect(hooks.getHandlers('onCancel')).toHaveLength(0);
202
+ });
203
+ });
204
+
205
+ describe('hooks execute in registration order', () => {
206
+ it('calls handlers in order registered', async () => {
207
+ const order = [];
208
+
209
+ hooks.registerHook('onStart', () => order.push(1));
210
+ hooks.registerHook('onStart', () => order.push(2));
211
+ hooks.registerHook('onStart', () => order.push(3));
212
+
213
+ await hooks.triggerHook('onStart', {});
214
+
215
+ expect(order).toEqual([1, 2, 3]);
216
+ });
217
+
218
+ it('maintains order with async handlers', async () => {
219
+ const order = [];
220
+
221
+ hooks.registerHook('onComplete', async () => {
222
+ await new Promise(resolve => setTimeout(resolve, 5));
223
+ order.push(1);
224
+ });
225
+ hooks.registerHook('onComplete', () => order.push(2));
226
+ hooks.registerHook('onComplete', async () => {
227
+ await new Promise(resolve => setTimeout(resolve, 1));
228
+ order.push(3);
229
+ });
230
+
231
+ await hooks.triggerHook('onComplete', {});
232
+
233
+ // Each handler completes in sequence
234
+ expect(order).toEqual([1, 2, 3]);
235
+ });
236
+ });
237
+
238
+ describe('onStart receives task config', () => {
239
+ it('passes full task config to onStart handlers', async () => {
240
+ const handler = vi.fn();
241
+ const taskConfig = {
242
+ agentId: 'agent-001',
243
+ taskId: 'task-001',
244
+ phase: 'build',
245
+ plan: '01-setup',
246
+ taskNumber: 1,
247
+ taskName: 'Initialize project',
248
+ workingDir: '/project',
249
+ timeout: 30000,
250
+ };
251
+
252
+ hooks.registerHook('onStart', handler);
253
+
254
+ await hooks.triggerHook('onStart', taskConfig);
255
+
256
+ expect(handler).toHaveBeenCalledWith(taskConfig);
257
+ const passedConfig = handler.mock.calls[0][0];
258
+ expect(passedConfig.agentId).toBe('agent-001');
259
+ expect(passedConfig.taskName).toBe('Initialize project');
260
+ expect(passedConfig.timeout).toBe(30000);
261
+ });
262
+ });
263
+
264
+ describe('HOOK_TYPES constant', () => {
265
+ it('exports valid hook types', () => {
266
+ expect(HOOK_TYPES).toContain('onStart');
267
+ expect(HOOK_TYPES).toContain('onComplete');
268
+ expect(HOOK_TYPES).toContain('onError');
269
+ expect(HOOK_TYPES).toContain('onCancel');
270
+ });
271
+ });
272
+
273
+ describe('singleton pattern', () => {
274
+ it('returns same instance across calls', () => {
275
+ const instance1 = getAgentHooks();
276
+ const instance2 = getAgentHooks();
277
+
278
+ expect(instance1).toBe(instance2);
279
+ });
280
+
281
+ it('shares state across imports', () => {
282
+ const instance1 = getAgentHooks();
283
+ const handler = vi.fn();
284
+ instance1.registerHook('onStart', handler);
285
+
286
+ const instance2 = getAgentHooks();
287
+ const handlers = instance2.getHandlers('onStart');
288
+
289
+ expect(handlers).toHaveLength(1);
290
+ expect(handlers[0]).toBe(handler);
291
+ });
292
+
293
+ it('resetHooks clears singleton', () => {
294
+ const instance1 = getAgentHooks();
295
+ instance1.registerHook('onStart', vi.fn());
296
+
297
+ resetHooks();
298
+
299
+ const instance2 = getAgentHooks();
300
+ expect(instance2.getHandlers('onStart')).toHaveLength(0);
301
+ });
302
+ });
303
+ });
@@ -0,0 +1,179 @@
1
+ /**
2
+ * Agent Metadata - Tracks execution metadata for agent tasks
3
+ *
4
+ * Provides immutable tracking of:
5
+ * - Model used
6
+ * - Token counts (input/output)
7
+ * - Cost calculation
8
+ * - Duration
9
+ * - Task type and parameters
10
+ */
11
+
12
+ /**
13
+ * Model pricing per million tokens (USD)
14
+ * Updated as of early 2026
15
+ */
16
+ const MODEL_PRICING = {
17
+ 'claude-3-opus': { input: 15, output: 75 },
18
+ 'claude-3-sonnet': { input: 3, output: 15 },
19
+ 'claude-3-haiku': { input: 0.25, output: 1.25 },
20
+ 'claude-3.5-sonnet': { input: 3, output: 15 },
21
+ 'claude-3.5-haiku': { input: 0.25, output: 1.25 },
22
+ 'gpt-4': { input: 30, output: 60 },
23
+ 'gpt-4-turbo': { input: 10, output: 30 },
24
+ 'gpt-3.5-turbo': { input: 0.5, output: 1.5 },
25
+ 'default': { input: 1, output: 3 },
26
+ };
27
+
28
+ /**
29
+ * AgentMetadata class - tracks metadata for a single agent task execution
30
+ */
31
+ class AgentMetadata {
32
+ /**
33
+ * Create a new AgentMetadata instance
34
+ * @param {Object} config - Configuration
35
+ * @param {string} config.model - Model identifier
36
+ * @param {string} config.taskType - Type of task being executed
37
+ * @param {Object} [config.parameters] - Task parameters
38
+ */
39
+ constructor(config) {
40
+ if (!config.model) {
41
+ throw new Error('model is required');
42
+ }
43
+ if (!config.taskType) {
44
+ throw new Error('taskType is required');
45
+ }
46
+
47
+ this.model = config.model;
48
+ this.taskType = config.taskType;
49
+ this.parameters = config.parameters || {};
50
+ this.inputTokens = config.inputTokens || 0;
51
+ this.outputTokens = config.outputTokens || 0;
52
+ this.totalTokens = config.totalTokens || 0;
53
+ this.cost = config.cost || 0;
54
+ this.duration = config.duration || null;
55
+ this.startedAt = config.startedAt || Date.now();
56
+ this.completedAt = config.completedAt || null;
57
+ this.frozen = config.frozen || false;
58
+ }
59
+
60
+ /**
61
+ * Update token counts
62
+ * @param {Object} tokens - Token counts to add
63
+ * @param {number} [tokens.input=0] - Input tokens to add
64
+ * @param {number} [tokens.output=0] - Output tokens to add
65
+ * @returns {AgentMetadata} This instance for chaining
66
+ * @throws {Error} If metadata is frozen
67
+ */
68
+ updateTokens({ input = 0, output = 0 }) {
69
+ if (this.frozen) {
70
+ throw new Error('Cannot update frozen metadata');
71
+ }
72
+
73
+ this.inputTokens += input;
74
+ this.outputTokens += output;
75
+ this.totalTokens = this.inputTokens + this.outputTokens;
76
+
77
+ return this;
78
+ }
79
+
80
+ /**
81
+ * Calculate cost based on model pricing
82
+ * @returns {number} Total cost in USD
83
+ */
84
+ calculateCost() {
85
+ const pricing = MODEL_PRICING[this.model] || MODEL_PRICING['default'];
86
+
87
+ const inputCost = (this.inputTokens * pricing.input) / 1_000_000;
88
+ const outputCost = (this.outputTokens * pricing.output) / 1_000_000;
89
+
90
+ this.cost = inputCost + outputCost;
91
+ return this.cost;
92
+ }
93
+
94
+ /**
95
+ * Set duration based on elapsed time since start
96
+ * @returns {AgentMetadata} This instance for chaining
97
+ * @throws {Error} If metadata is frozen
98
+ */
99
+ setDuration() {
100
+ if (this.frozen) {
101
+ throw new Error('Cannot update frozen metadata');
102
+ }
103
+
104
+ this.completedAt = Date.now();
105
+ this.duration = this.completedAt - this.startedAt;
106
+
107
+ return this;
108
+ }
109
+
110
+ /**
111
+ * Freeze the metadata, preventing further updates
112
+ * Automatically calculates final cost
113
+ * @returns {AgentMetadata} This instance for chaining
114
+ */
115
+ freeze() {
116
+ this.calculateCost();
117
+ this.frozen = true;
118
+ return this;
119
+ }
120
+
121
+ /**
122
+ * Serialize to plain JSON object
123
+ * @returns {Object} Plain object representation
124
+ */
125
+ toJSON() {
126
+ return {
127
+ model: this.model,
128
+ taskType: this.taskType,
129
+ parameters: this.parameters,
130
+ inputTokens: this.inputTokens,
131
+ outputTokens: this.outputTokens,
132
+ totalTokens: this.totalTokens,
133
+ cost: this.cost,
134
+ duration: this.duration,
135
+ startedAt: this.startedAt,
136
+ completedAt: this.completedAt,
137
+ frozen: this.frozen,
138
+ };
139
+ }
140
+
141
+ /**
142
+ * Restore from JSON
143
+ * @param {Object} json - Serialized metadata
144
+ * @returns {AgentMetadata} Restored instance
145
+ */
146
+ static fromJSON(json) {
147
+ return new AgentMetadata({
148
+ model: json.model,
149
+ taskType: json.taskType,
150
+ parameters: json.parameters,
151
+ inputTokens: json.inputTokens,
152
+ outputTokens: json.outputTokens,
153
+ totalTokens: json.totalTokens,
154
+ cost: json.cost,
155
+ duration: json.duration,
156
+ startedAt: json.startedAt,
157
+ completedAt: json.completedAt,
158
+ frozen: json.frozen,
159
+ });
160
+ }
161
+ }
162
+
163
+ /**
164
+ * Create a new metadata instance
165
+ * @param {Object} config - Configuration
166
+ * @param {string} config.model - Model identifier
167
+ * @param {string} config.taskType - Type of task being executed
168
+ * @param {Object} [config.parameters] - Task parameters
169
+ * @returns {AgentMetadata} New metadata instance
170
+ */
171
+ function createMetadata(config) {
172
+ return new AgentMetadata(config);
173
+ }
174
+
175
+ module.exports = {
176
+ AgentMetadata,
177
+ createMetadata,
178
+ MODEL_PRICING,
179
+ };