tlc-claude-code 1.4.0 → 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.
- 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/package.json +5 -2
- 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,383 @@
|
|
|
1
|
+
import { describe, it, expect, beforeEach, vi } from 'vitest';
|
|
2
|
+
import {
|
|
3
|
+
AgentMetadata,
|
|
4
|
+
createMetadata,
|
|
5
|
+
MODEL_PRICING,
|
|
6
|
+
} from './agent-metadata.js';
|
|
7
|
+
|
|
8
|
+
describe('AgentMetadata', () => {
|
|
9
|
+
let metadata;
|
|
10
|
+
|
|
11
|
+
beforeEach(() => {
|
|
12
|
+
vi.useFakeTimers();
|
|
13
|
+
vi.setSystemTime(new Date('2026-02-02T12:00:00Z'));
|
|
14
|
+
});
|
|
15
|
+
|
|
16
|
+
describe('createMetadata', () => {
|
|
17
|
+
it('initializes with model and task', () => {
|
|
18
|
+
metadata = createMetadata({
|
|
19
|
+
model: 'claude-3-opus',
|
|
20
|
+
taskType: 'code-review',
|
|
21
|
+
parameters: { files: ['test.js'] },
|
|
22
|
+
});
|
|
23
|
+
|
|
24
|
+
expect(metadata.model).toBe('claude-3-opus');
|
|
25
|
+
expect(metadata.taskType).toBe('code-review');
|
|
26
|
+
expect(metadata.parameters).toEqual({ files: ['test.js'] });
|
|
27
|
+
});
|
|
28
|
+
|
|
29
|
+
it('sets initial token counts to zero', () => {
|
|
30
|
+
metadata = createMetadata({
|
|
31
|
+
model: 'claude-3-opus',
|
|
32
|
+
taskType: 'analysis',
|
|
33
|
+
});
|
|
34
|
+
|
|
35
|
+
expect(metadata.inputTokens).toBe(0);
|
|
36
|
+
expect(metadata.outputTokens).toBe(0);
|
|
37
|
+
expect(metadata.totalTokens).toBe(0);
|
|
38
|
+
});
|
|
39
|
+
|
|
40
|
+
it('records start timestamp', () => {
|
|
41
|
+
metadata = createMetadata({
|
|
42
|
+
model: 'claude-3-opus',
|
|
43
|
+
taskType: 'generation',
|
|
44
|
+
});
|
|
45
|
+
|
|
46
|
+
expect(metadata.startedAt).toBe(new Date('2026-02-02T12:00:00Z').getTime());
|
|
47
|
+
});
|
|
48
|
+
|
|
49
|
+
it('initializes cost to zero', () => {
|
|
50
|
+
metadata = createMetadata({
|
|
51
|
+
model: 'claude-3-opus',
|
|
52
|
+
taskType: 'analysis',
|
|
53
|
+
});
|
|
54
|
+
|
|
55
|
+
expect(metadata.cost).toBe(0);
|
|
56
|
+
});
|
|
57
|
+
|
|
58
|
+
it('sets frozen to false initially', () => {
|
|
59
|
+
metadata = createMetadata({
|
|
60
|
+
model: 'claude-3-opus',
|
|
61
|
+
taskType: 'generation',
|
|
62
|
+
});
|
|
63
|
+
|
|
64
|
+
expect(metadata.frozen).toBe(false);
|
|
65
|
+
});
|
|
66
|
+
});
|
|
67
|
+
|
|
68
|
+
describe('updateTokens', () => {
|
|
69
|
+
beforeEach(() => {
|
|
70
|
+
metadata = createMetadata({
|
|
71
|
+
model: 'claude-3-opus',
|
|
72
|
+
taskType: 'analysis',
|
|
73
|
+
});
|
|
74
|
+
});
|
|
75
|
+
|
|
76
|
+
it('adds input tokens', () => {
|
|
77
|
+
metadata.updateTokens({ input: 100 });
|
|
78
|
+
|
|
79
|
+
expect(metadata.inputTokens).toBe(100);
|
|
80
|
+
});
|
|
81
|
+
|
|
82
|
+
it('adds output tokens', () => {
|
|
83
|
+
metadata.updateTokens({ output: 50 });
|
|
84
|
+
|
|
85
|
+
expect(metadata.outputTokens).toBe(50);
|
|
86
|
+
});
|
|
87
|
+
|
|
88
|
+
it('calculates total tokens', () => {
|
|
89
|
+
metadata.updateTokens({ input: 100, output: 50 });
|
|
90
|
+
|
|
91
|
+
expect(metadata.totalTokens).toBe(150);
|
|
92
|
+
});
|
|
93
|
+
|
|
94
|
+
it('accumulates across multiple updates', () => {
|
|
95
|
+
metadata.updateTokens({ input: 100, output: 50 });
|
|
96
|
+
metadata.updateTokens({ input: 200, output: 75 });
|
|
97
|
+
|
|
98
|
+
expect(metadata.inputTokens).toBe(300);
|
|
99
|
+
expect(metadata.outputTokens).toBe(125);
|
|
100
|
+
expect(metadata.totalTokens).toBe(425);
|
|
101
|
+
});
|
|
102
|
+
|
|
103
|
+
it('returns self for chaining', () => {
|
|
104
|
+
const result = metadata.updateTokens({ input: 100 });
|
|
105
|
+
|
|
106
|
+
expect(result).toBe(metadata);
|
|
107
|
+
});
|
|
108
|
+
});
|
|
109
|
+
|
|
110
|
+
describe('calculateCost', () => {
|
|
111
|
+
it('uses model pricing for claude-3-opus', () => {
|
|
112
|
+
metadata = createMetadata({
|
|
113
|
+
model: 'claude-3-opus',
|
|
114
|
+
taskType: 'analysis',
|
|
115
|
+
});
|
|
116
|
+
metadata.updateTokens({ input: 1000, output: 500 });
|
|
117
|
+
|
|
118
|
+
const cost = metadata.calculateCost();
|
|
119
|
+
|
|
120
|
+
// claude-3-opus: $15/1M input, $75/1M output
|
|
121
|
+
const expectedCost = (1000 * 15 / 1_000_000) + (500 * 75 / 1_000_000);
|
|
122
|
+
expect(cost).toBeCloseTo(expectedCost, 6);
|
|
123
|
+
expect(metadata.cost).toBeCloseTo(expectedCost, 6);
|
|
124
|
+
});
|
|
125
|
+
|
|
126
|
+
it('uses model pricing for claude-3-sonnet', () => {
|
|
127
|
+
metadata = createMetadata({
|
|
128
|
+
model: 'claude-3-sonnet',
|
|
129
|
+
taskType: 'analysis',
|
|
130
|
+
});
|
|
131
|
+
metadata.updateTokens({ input: 1000, output: 500 });
|
|
132
|
+
|
|
133
|
+
const cost = metadata.calculateCost();
|
|
134
|
+
|
|
135
|
+
// claude-3-sonnet: $3/1M input, $15/1M output
|
|
136
|
+
const expectedCost = (1000 * 3 / 1_000_000) + (500 * 15 / 1_000_000);
|
|
137
|
+
expect(cost).toBeCloseTo(expectedCost, 6);
|
|
138
|
+
});
|
|
139
|
+
|
|
140
|
+
it('uses model pricing for claude-3-haiku', () => {
|
|
141
|
+
metadata = createMetadata({
|
|
142
|
+
model: 'claude-3-haiku',
|
|
143
|
+
taskType: 'analysis',
|
|
144
|
+
});
|
|
145
|
+
metadata.updateTokens({ input: 1000, output: 500 });
|
|
146
|
+
|
|
147
|
+
const cost = metadata.calculateCost();
|
|
148
|
+
|
|
149
|
+
// claude-3-haiku: $0.25/1M input, $1.25/1M output
|
|
150
|
+
const expectedCost = (1000 * 0.25 / 1_000_000) + (500 * 1.25 / 1_000_000);
|
|
151
|
+
expect(cost).toBeCloseTo(expectedCost, 6);
|
|
152
|
+
});
|
|
153
|
+
|
|
154
|
+
it('handles unknown models with default pricing', () => {
|
|
155
|
+
metadata = createMetadata({
|
|
156
|
+
model: 'unknown-model',
|
|
157
|
+
taskType: 'analysis',
|
|
158
|
+
});
|
|
159
|
+
metadata.updateTokens({ input: 1000, output: 500 });
|
|
160
|
+
|
|
161
|
+
const cost = metadata.calculateCost();
|
|
162
|
+
|
|
163
|
+
// default: $1/1M input, $3/1M output
|
|
164
|
+
const expectedCost = (1000 * 1 / 1_000_000) + (500 * 3 / 1_000_000);
|
|
165
|
+
expect(cost).toBeCloseTo(expectedCost, 6);
|
|
166
|
+
});
|
|
167
|
+
|
|
168
|
+
it('returns zero cost when no tokens', () => {
|
|
169
|
+
metadata = createMetadata({
|
|
170
|
+
model: 'claude-3-opus',
|
|
171
|
+
taskType: 'analysis',
|
|
172
|
+
});
|
|
173
|
+
|
|
174
|
+
const cost = metadata.calculateCost();
|
|
175
|
+
|
|
176
|
+
expect(cost).toBe(0);
|
|
177
|
+
});
|
|
178
|
+
});
|
|
179
|
+
|
|
180
|
+
describe('setDuration', () => {
|
|
181
|
+
beforeEach(() => {
|
|
182
|
+
metadata = createMetadata({
|
|
183
|
+
model: 'claude-3-opus',
|
|
184
|
+
taskType: 'analysis',
|
|
185
|
+
});
|
|
186
|
+
});
|
|
187
|
+
|
|
188
|
+
it('records elapsed time in milliseconds', () => {
|
|
189
|
+
// Advance time by 5 seconds
|
|
190
|
+
vi.advanceTimersByTime(5000);
|
|
191
|
+
|
|
192
|
+
metadata.setDuration();
|
|
193
|
+
|
|
194
|
+
expect(metadata.duration).toBe(5000);
|
|
195
|
+
});
|
|
196
|
+
|
|
197
|
+
it('records completion timestamp', () => {
|
|
198
|
+
vi.advanceTimersByTime(5000);
|
|
199
|
+
|
|
200
|
+
metadata.setDuration();
|
|
201
|
+
|
|
202
|
+
expect(metadata.completedAt).toBe(new Date('2026-02-02T12:00:05Z').getTime());
|
|
203
|
+
});
|
|
204
|
+
|
|
205
|
+
it('returns self for chaining', () => {
|
|
206
|
+
const result = metadata.setDuration();
|
|
207
|
+
|
|
208
|
+
expect(result).toBe(metadata);
|
|
209
|
+
});
|
|
210
|
+
});
|
|
211
|
+
|
|
212
|
+
describe('freeze', () => {
|
|
213
|
+
beforeEach(() => {
|
|
214
|
+
metadata = createMetadata({
|
|
215
|
+
model: 'claude-3-opus',
|
|
216
|
+
taskType: 'analysis',
|
|
217
|
+
});
|
|
218
|
+
metadata.updateTokens({ input: 100, output: 50 });
|
|
219
|
+
});
|
|
220
|
+
|
|
221
|
+
it('prevents further token updates', () => {
|
|
222
|
+
metadata.freeze();
|
|
223
|
+
|
|
224
|
+
expect(() => metadata.updateTokens({ input: 100 })).toThrow();
|
|
225
|
+
expect(metadata.inputTokens).toBe(100);
|
|
226
|
+
});
|
|
227
|
+
|
|
228
|
+
it('prevents setting duration again', () => {
|
|
229
|
+
metadata.setDuration();
|
|
230
|
+
metadata.freeze();
|
|
231
|
+
|
|
232
|
+
expect(() => metadata.setDuration()).toThrow();
|
|
233
|
+
});
|
|
234
|
+
|
|
235
|
+
it('sets frozen flag to true', () => {
|
|
236
|
+
metadata.freeze();
|
|
237
|
+
|
|
238
|
+
expect(metadata.frozen).toBe(true);
|
|
239
|
+
});
|
|
240
|
+
|
|
241
|
+
it('automatically calculates final cost', () => {
|
|
242
|
+
metadata.freeze();
|
|
243
|
+
|
|
244
|
+
expect(metadata.cost).toBeGreaterThan(0);
|
|
245
|
+
});
|
|
246
|
+
|
|
247
|
+
it('returns self for chaining', () => {
|
|
248
|
+
const result = metadata.freeze();
|
|
249
|
+
|
|
250
|
+
expect(result).toBe(metadata);
|
|
251
|
+
});
|
|
252
|
+
});
|
|
253
|
+
|
|
254
|
+
describe('toJSON', () => {
|
|
255
|
+
it('serializes all fields', () => {
|
|
256
|
+
metadata = createMetadata({
|
|
257
|
+
model: 'claude-3-opus',
|
|
258
|
+
taskType: 'code-review',
|
|
259
|
+
parameters: { files: ['test.js'] },
|
|
260
|
+
});
|
|
261
|
+
metadata.updateTokens({ input: 1000, output: 500 });
|
|
262
|
+
vi.advanceTimersByTime(5000);
|
|
263
|
+
metadata.setDuration();
|
|
264
|
+
metadata.freeze();
|
|
265
|
+
|
|
266
|
+
const json = metadata.toJSON();
|
|
267
|
+
|
|
268
|
+
expect(json).toMatchObject({
|
|
269
|
+
model: 'claude-3-opus',
|
|
270
|
+
taskType: 'code-review',
|
|
271
|
+
parameters: { files: ['test.js'] },
|
|
272
|
+
inputTokens: 1000,
|
|
273
|
+
outputTokens: 500,
|
|
274
|
+
totalTokens: 1500,
|
|
275
|
+
duration: 5000,
|
|
276
|
+
frozen: true,
|
|
277
|
+
});
|
|
278
|
+
expect(json.startedAt).toBeDefined();
|
|
279
|
+
expect(json.completedAt).toBeDefined();
|
|
280
|
+
expect(json.cost).toBeGreaterThan(0);
|
|
281
|
+
});
|
|
282
|
+
|
|
283
|
+
it('returns plain object, not instance', () => {
|
|
284
|
+
metadata = createMetadata({
|
|
285
|
+
model: 'claude-3-opus',
|
|
286
|
+
taskType: 'analysis',
|
|
287
|
+
});
|
|
288
|
+
|
|
289
|
+
const json = metadata.toJSON();
|
|
290
|
+
|
|
291
|
+
expect(json).not.toBeInstanceOf(AgentMetadata);
|
|
292
|
+
expect(typeof json.updateTokens).toBe('undefined');
|
|
293
|
+
});
|
|
294
|
+
});
|
|
295
|
+
|
|
296
|
+
describe('fromJSON', () => {
|
|
297
|
+
it('deserializes correctly', () => {
|
|
298
|
+
const original = createMetadata({
|
|
299
|
+
model: 'claude-3-opus',
|
|
300
|
+
taskType: 'code-review',
|
|
301
|
+
parameters: { files: ['test.js'] },
|
|
302
|
+
});
|
|
303
|
+
original.updateTokens({ input: 1000, output: 500 });
|
|
304
|
+
vi.advanceTimersByTime(5000);
|
|
305
|
+
original.setDuration();
|
|
306
|
+
original.freeze();
|
|
307
|
+
|
|
308
|
+
const json = original.toJSON();
|
|
309
|
+
const restored = AgentMetadata.fromJSON(json);
|
|
310
|
+
|
|
311
|
+
expect(restored.model).toBe('claude-3-opus');
|
|
312
|
+
expect(restored.taskType).toBe('code-review');
|
|
313
|
+
expect(restored.parameters).toEqual({ files: ['test.js'] });
|
|
314
|
+
expect(restored.inputTokens).toBe(1000);
|
|
315
|
+
expect(restored.outputTokens).toBe(500);
|
|
316
|
+
expect(restored.totalTokens).toBe(1500);
|
|
317
|
+
expect(restored.duration).toBe(5000);
|
|
318
|
+
expect(restored.frozen).toBe(true);
|
|
319
|
+
});
|
|
320
|
+
|
|
321
|
+
it('restored metadata is immutable if original was frozen', () => {
|
|
322
|
+
const original = createMetadata({
|
|
323
|
+
model: 'claude-3-opus',
|
|
324
|
+
taskType: 'analysis',
|
|
325
|
+
});
|
|
326
|
+
original.freeze();
|
|
327
|
+
|
|
328
|
+
const restored = AgentMetadata.fromJSON(original.toJSON());
|
|
329
|
+
|
|
330
|
+
expect(() => restored.updateTokens({ input: 100 })).toThrow();
|
|
331
|
+
});
|
|
332
|
+
|
|
333
|
+
it('restored metadata is mutable if original was not frozen', () => {
|
|
334
|
+
const original = createMetadata({
|
|
335
|
+
model: 'claude-3-opus',
|
|
336
|
+
taskType: 'analysis',
|
|
337
|
+
});
|
|
338
|
+
|
|
339
|
+
const restored = AgentMetadata.fromJSON(original.toJSON());
|
|
340
|
+
restored.updateTokens({ input: 100 });
|
|
341
|
+
|
|
342
|
+
expect(restored.inputTokens).toBe(100);
|
|
343
|
+
});
|
|
344
|
+
});
|
|
345
|
+
|
|
346
|
+
describe('validates required fields', () => {
|
|
347
|
+
it('throws if model is missing', () => {
|
|
348
|
+
expect(() => createMetadata({
|
|
349
|
+
taskType: 'analysis',
|
|
350
|
+
})).toThrow('model is required');
|
|
351
|
+
});
|
|
352
|
+
|
|
353
|
+
it('throws if taskType is missing', () => {
|
|
354
|
+
expect(() => createMetadata({
|
|
355
|
+
model: 'claude-3-opus',
|
|
356
|
+
})).toThrow('taskType is required');
|
|
357
|
+
});
|
|
358
|
+
|
|
359
|
+
it('accepts valid minimal config', () => {
|
|
360
|
+
expect(() => createMetadata({
|
|
361
|
+
model: 'claude-3-opus',
|
|
362
|
+
taskType: 'analysis',
|
|
363
|
+
})).not.toThrow();
|
|
364
|
+
});
|
|
365
|
+
});
|
|
366
|
+
|
|
367
|
+
describe('MODEL_PRICING', () => {
|
|
368
|
+
it('exports pricing constants', () => {
|
|
369
|
+
expect(MODEL_PRICING).toBeDefined();
|
|
370
|
+
expect(MODEL_PRICING['claude-3-opus']).toBeDefined();
|
|
371
|
+
expect(MODEL_PRICING['claude-3-sonnet']).toBeDefined();
|
|
372
|
+
expect(MODEL_PRICING['claude-3-haiku']).toBeDefined();
|
|
373
|
+
expect(MODEL_PRICING['default']).toBeDefined();
|
|
374
|
+
});
|
|
375
|
+
|
|
376
|
+
it('has input and output rates for each model', () => {
|
|
377
|
+
for (const model of Object.keys(MODEL_PRICING)) {
|
|
378
|
+
expect(MODEL_PRICING[model].input).toBeTypeOf('number');
|
|
379
|
+
expect(MODEL_PRICING[model].output).toBeTypeOf('number');
|
|
380
|
+
}
|
|
381
|
+
});
|
|
382
|
+
});
|
|
383
|
+
});
|
|
@@ -0,0 +1,191 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Agent Persistence - Save and restore agent state across sessions
|
|
3
|
+
*
|
|
4
|
+
* Provides file-based persistence for agent data in the .tlc/agents/ directory.
|
|
5
|
+
* Uses atomic writes (write to temp file, then rename) to prevent corruption.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
const fs = require('fs');
|
|
9
|
+
const path = require('path');
|
|
10
|
+
|
|
11
|
+
/**
|
|
12
|
+
* Default directory for storing agent files
|
|
13
|
+
*/
|
|
14
|
+
const AGENTS_DIR = '.tlc/agents';
|
|
15
|
+
|
|
16
|
+
/**
|
|
17
|
+
* Default max age for cleanup (7 days in milliseconds)
|
|
18
|
+
*/
|
|
19
|
+
const DEFAULT_MAX_AGE_MS = 7 * 24 * 60 * 60 * 1000;
|
|
20
|
+
|
|
21
|
+
/**
|
|
22
|
+
* Get the storage path for an agent
|
|
23
|
+
* @param {string|null} projectRoot - Project root directory (uses TLC_PROJECT_ROOT env if null)
|
|
24
|
+
* @param {string} [agentId] - Optional agent ID for specific file path
|
|
25
|
+
* @returns {string} Full path to agent file or agents directory
|
|
26
|
+
*/
|
|
27
|
+
function getStoragePath(projectRoot, agentId) {
|
|
28
|
+
const root = projectRoot || process.env.TLC_PROJECT_ROOT || process.cwd();
|
|
29
|
+
const agentsDir = path.join(root, AGENTS_DIR);
|
|
30
|
+
|
|
31
|
+
if (agentId) {
|
|
32
|
+
return path.join(agentsDir, `${agentId}.json`);
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
return agentsDir;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
/**
|
|
39
|
+
* Ensure the agents directory exists
|
|
40
|
+
* @param {string} projectRoot - Project root directory
|
|
41
|
+
*/
|
|
42
|
+
function ensureAgentsDir(projectRoot) {
|
|
43
|
+
const agentsDir = getStoragePath(projectRoot);
|
|
44
|
+
if (!fs.existsSync(agentsDir)) {
|
|
45
|
+
fs.mkdirSync(agentsDir, { recursive: true });
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
/**
|
|
50
|
+
* Save agent data to file using atomic write
|
|
51
|
+
* @param {string} projectRoot - Project root directory
|
|
52
|
+
* @param {Object} agentData - Agent data to save
|
|
53
|
+
* @param {string} agentData.id - Agent ID (required)
|
|
54
|
+
* @param {string} agentData.state - Agent state
|
|
55
|
+
* @param {Object} agentData.metadata - Agent metadata
|
|
56
|
+
* @param {number} agentData.createdAt - Creation timestamp
|
|
57
|
+
* @param {number} agentData.updatedAt - Last update timestamp
|
|
58
|
+
* @returns {Promise<void>}
|
|
59
|
+
* @throws {Error} If agent ID is missing
|
|
60
|
+
*/
|
|
61
|
+
async function saveAgent(projectRoot, agentData) {
|
|
62
|
+
if (!agentData || !agentData.id) {
|
|
63
|
+
throw new Error('Agent ID is required');
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
ensureAgentsDir(projectRoot);
|
|
67
|
+
|
|
68
|
+
const filePath = getStoragePath(projectRoot, agentData.id);
|
|
69
|
+
const tempPath = `${filePath}.tmp`;
|
|
70
|
+
|
|
71
|
+
// Write to temp file first
|
|
72
|
+
const jsonData = JSON.stringify(agentData, null, 2);
|
|
73
|
+
fs.writeFileSync(tempPath, jsonData, 'utf8');
|
|
74
|
+
|
|
75
|
+
// Atomic rename
|
|
76
|
+
fs.renameSync(tempPath, filePath);
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
/**
|
|
80
|
+
* Load agent data from file
|
|
81
|
+
* @param {string} projectRoot - Project root directory
|
|
82
|
+
* @param {string} agentId - Agent ID to load
|
|
83
|
+
* @returns {Promise<Object|null>} Agent data or null if not found/corrupted
|
|
84
|
+
*/
|
|
85
|
+
async function loadAgent(projectRoot, agentId) {
|
|
86
|
+
const filePath = getStoragePath(projectRoot, agentId);
|
|
87
|
+
|
|
88
|
+
if (!fs.existsSync(filePath)) {
|
|
89
|
+
return null;
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
try {
|
|
93
|
+
const content = fs.readFileSync(filePath, 'utf8');
|
|
94
|
+
if (!content || content.trim() === '') {
|
|
95
|
+
return null;
|
|
96
|
+
}
|
|
97
|
+
return JSON.parse(content);
|
|
98
|
+
} catch (err) {
|
|
99
|
+
// Handle corrupted files gracefully
|
|
100
|
+
return null;
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
/**
|
|
105
|
+
* Load all saved agents
|
|
106
|
+
* @param {string} projectRoot - Project root directory
|
|
107
|
+
* @returns {Promise<Array>} Array of agent data objects
|
|
108
|
+
*/
|
|
109
|
+
async function loadAllAgents(projectRoot) {
|
|
110
|
+
const agentsDir = getStoragePath(projectRoot);
|
|
111
|
+
|
|
112
|
+
if (!fs.existsSync(agentsDir)) {
|
|
113
|
+
return [];
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
const files = fs.readdirSync(agentsDir);
|
|
117
|
+
const agents = [];
|
|
118
|
+
|
|
119
|
+
for (const file of files) {
|
|
120
|
+
if (!file.endsWith('.json')) {
|
|
121
|
+
continue;
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
const agentId = path.basename(file, '.json');
|
|
125
|
+
const agent = await loadAgent(projectRoot, agentId);
|
|
126
|
+
|
|
127
|
+
if (agent) {
|
|
128
|
+
agents.push(agent);
|
|
129
|
+
}
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
return agents;
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
/**
|
|
136
|
+
* Delete an agent file
|
|
137
|
+
* @param {string} projectRoot - Project root directory
|
|
138
|
+
* @param {string} agentId - Agent ID to delete
|
|
139
|
+
* @returns {Promise<boolean>} True if deleted, false if not found
|
|
140
|
+
*/
|
|
141
|
+
async function deleteAgent(projectRoot, agentId) {
|
|
142
|
+
const filePath = getStoragePath(projectRoot, agentId);
|
|
143
|
+
|
|
144
|
+
if (!fs.existsSync(filePath)) {
|
|
145
|
+
return false;
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
fs.unlinkSync(filePath);
|
|
149
|
+
return true;
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
/**
|
|
153
|
+
* Clean up old agent files
|
|
154
|
+
* @param {string} projectRoot - Project root directory
|
|
155
|
+
* @param {number} [maxAgeMs] - Maximum age in milliseconds (default: 7 days)
|
|
156
|
+
* @returns {Promise<number>} Number of agents cleaned up
|
|
157
|
+
*/
|
|
158
|
+
async function cleanupOldAgents(projectRoot, maxAgeMs = DEFAULT_MAX_AGE_MS) {
|
|
159
|
+
const agents = await loadAllAgents(projectRoot);
|
|
160
|
+
const now = Date.now();
|
|
161
|
+
let cleaned = 0;
|
|
162
|
+
|
|
163
|
+
for (const agent of agents) {
|
|
164
|
+
// Skip running agents - they might still be active
|
|
165
|
+
if (agent.state === 'running' || agent.state === 'pending') {
|
|
166
|
+
continue;
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
// Check age based on updatedAt (or createdAt if updatedAt is missing)
|
|
170
|
+
const agentTime = agent.updatedAt || agent.createdAt;
|
|
171
|
+
const age = now - agentTime;
|
|
172
|
+
|
|
173
|
+
if (age > maxAgeMs) {
|
|
174
|
+
await deleteAgent(projectRoot, agent.id);
|
|
175
|
+
cleaned++;
|
|
176
|
+
}
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
return cleaned;
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
module.exports = {
|
|
183
|
+
saveAgent,
|
|
184
|
+
loadAgent,
|
|
185
|
+
loadAllAgents,
|
|
186
|
+
deleteAgent,
|
|
187
|
+
getStoragePath,
|
|
188
|
+
cleanupOldAgents,
|
|
189
|
+
AGENTS_DIR,
|
|
190
|
+
DEFAULT_MAX_AGE_MS,
|
|
191
|
+
};
|