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.
- 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,349 @@
|
|
|
1
|
+
import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest';
|
|
2
|
+
import {
|
|
3
|
+
createCLIProvider,
|
|
4
|
+
buildArgs,
|
|
5
|
+
parseOutput,
|
|
6
|
+
runLocal,
|
|
7
|
+
runViaDevserver,
|
|
8
|
+
} from './cli-provider.js';
|
|
9
|
+
|
|
10
|
+
// Mock child_process
|
|
11
|
+
vi.mock('child_process', () => ({
|
|
12
|
+
spawn: vi.fn(),
|
|
13
|
+
execSync: vi.fn(),
|
|
14
|
+
}));
|
|
15
|
+
|
|
16
|
+
// Mock fetch for devserver calls
|
|
17
|
+
global.fetch = vi.fn();
|
|
18
|
+
|
|
19
|
+
import { spawn } from 'child_process';
|
|
20
|
+
|
|
21
|
+
describe('cli-provider', () => {
|
|
22
|
+
beforeEach(() => {
|
|
23
|
+
vi.clearAllMocks();
|
|
24
|
+
});
|
|
25
|
+
|
|
26
|
+
describe('createCLIProvider', () => {
|
|
27
|
+
it('creates provider with CLI type', () => {
|
|
28
|
+
const provider = createCLIProvider({
|
|
29
|
+
name: 'claude',
|
|
30
|
+
command: 'claude',
|
|
31
|
+
headlessArgs: ['-p', '--output-format', 'json'],
|
|
32
|
+
capabilities: ['review', 'code-gen'],
|
|
33
|
+
});
|
|
34
|
+
|
|
35
|
+
expect(provider.type).toBe('cli');
|
|
36
|
+
expect(provider.name).toBe('claude');
|
|
37
|
+
});
|
|
38
|
+
|
|
39
|
+
it('sets detected based on CLI detection', () => {
|
|
40
|
+
const provider = createCLIProvider({
|
|
41
|
+
name: 'claude',
|
|
42
|
+
command: 'claude',
|
|
43
|
+
detected: true,
|
|
44
|
+
});
|
|
45
|
+
|
|
46
|
+
expect(provider.detected).toBe(true);
|
|
47
|
+
});
|
|
48
|
+
|
|
49
|
+
it('defaults detected to false', () => {
|
|
50
|
+
const provider = createCLIProvider({
|
|
51
|
+
name: 'claude',
|
|
52
|
+
command: 'claude',
|
|
53
|
+
});
|
|
54
|
+
|
|
55
|
+
expect(provider.detected).toBe(false);
|
|
56
|
+
});
|
|
57
|
+
});
|
|
58
|
+
|
|
59
|
+
describe('runLocal', () => {
|
|
60
|
+
it('spawns claude -p with args', async () => {
|
|
61
|
+
const mockProcess = {
|
|
62
|
+
stdout: { on: vi.fn() },
|
|
63
|
+
stderr: { on: vi.fn() },
|
|
64
|
+
on: vi.fn(),
|
|
65
|
+
};
|
|
66
|
+
|
|
67
|
+
spawn.mockReturnValue(mockProcess);
|
|
68
|
+
|
|
69
|
+
// Simulate process completion
|
|
70
|
+
setTimeout(() => {
|
|
71
|
+
const stdoutCallback = mockProcess.stdout.on.mock.calls.find(c => c[0] === 'data')[1];
|
|
72
|
+
stdoutCallback(Buffer.from('{"result": "ok"}'));
|
|
73
|
+
|
|
74
|
+
const closeCallback = mockProcess.on.mock.calls.find(c => c[0] === 'close')[1];
|
|
75
|
+
closeCallback(0);
|
|
76
|
+
}, 10);
|
|
77
|
+
|
|
78
|
+
const result = await runLocal('claude', 'test prompt', {
|
|
79
|
+
headlessArgs: ['-p', '--output-format', 'json'],
|
|
80
|
+
});
|
|
81
|
+
|
|
82
|
+
expect(spawn).toHaveBeenCalledWith(
|
|
83
|
+
'claude',
|
|
84
|
+
expect.arrayContaining(['-p', '--output-format', 'json']),
|
|
85
|
+
expect.any(Object)
|
|
86
|
+
);
|
|
87
|
+
expect(result.exitCode).toBe(0);
|
|
88
|
+
});
|
|
89
|
+
|
|
90
|
+
it('spawns codex exec with args', async () => {
|
|
91
|
+
const mockProcess = {
|
|
92
|
+
stdout: { on: vi.fn() },
|
|
93
|
+
stderr: { on: vi.fn() },
|
|
94
|
+
on: vi.fn(),
|
|
95
|
+
};
|
|
96
|
+
|
|
97
|
+
spawn.mockReturnValue(mockProcess);
|
|
98
|
+
|
|
99
|
+
setTimeout(() => {
|
|
100
|
+
const stdoutCallback = mockProcess.stdout.on.mock.calls.find(c => c[0] === 'data')[1];
|
|
101
|
+
stdoutCallback(Buffer.from('{"result": "ok"}'));
|
|
102
|
+
|
|
103
|
+
const closeCallback = mockProcess.on.mock.calls.find(c => c[0] === 'close')[1];
|
|
104
|
+
closeCallback(0);
|
|
105
|
+
}, 10);
|
|
106
|
+
|
|
107
|
+
await runLocal('codex', 'test prompt', {
|
|
108
|
+
headlessArgs: ['exec', '--json', '--sandbox', 'read-only'],
|
|
109
|
+
});
|
|
110
|
+
|
|
111
|
+
expect(spawn).toHaveBeenCalledWith(
|
|
112
|
+
'codex',
|
|
113
|
+
expect.arrayContaining(['exec', '--json', '--sandbox', 'read-only']),
|
|
114
|
+
expect.any(Object)
|
|
115
|
+
);
|
|
116
|
+
});
|
|
117
|
+
|
|
118
|
+
it('spawns gemini -p with args', async () => {
|
|
119
|
+
const mockProcess = {
|
|
120
|
+
stdout: { on: vi.fn() },
|
|
121
|
+
stderr: { on: vi.fn() },
|
|
122
|
+
on: vi.fn(),
|
|
123
|
+
};
|
|
124
|
+
|
|
125
|
+
spawn.mockReturnValue(mockProcess);
|
|
126
|
+
|
|
127
|
+
setTimeout(() => {
|
|
128
|
+
const stdoutCallback = mockProcess.stdout.on.mock.calls.find(c => c[0] === 'data')[1];
|
|
129
|
+
stdoutCallback(Buffer.from('{"result": "ok"}'));
|
|
130
|
+
|
|
131
|
+
const closeCallback = mockProcess.on.mock.calls.find(c => c[0] === 'close')[1];
|
|
132
|
+
closeCallback(0);
|
|
133
|
+
}, 10);
|
|
134
|
+
|
|
135
|
+
await runLocal('gemini', 'test prompt', {
|
|
136
|
+
headlessArgs: ['-p', '--output-format', 'json'],
|
|
137
|
+
});
|
|
138
|
+
|
|
139
|
+
expect(spawn).toHaveBeenCalledWith(
|
|
140
|
+
'gemini',
|
|
141
|
+
expect.arrayContaining(['-p', '--output-format', 'json']),
|
|
142
|
+
expect.any(Object)
|
|
143
|
+
);
|
|
144
|
+
});
|
|
145
|
+
|
|
146
|
+
it('parses JSON output', async () => {
|
|
147
|
+
const mockProcess = {
|
|
148
|
+
stdout: { on: vi.fn() },
|
|
149
|
+
stderr: { on: vi.fn() },
|
|
150
|
+
on: vi.fn(),
|
|
151
|
+
};
|
|
152
|
+
|
|
153
|
+
spawn.mockReturnValue(mockProcess);
|
|
154
|
+
|
|
155
|
+
setTimeout(() => {
|
|
156
|
+
const stdoutCallback = mockProcess.stdout.on.mock.calls.find(c => c[0] === 'data')[1];
|
|
157
|
+
stdoutCallback(Buffer.from('{"summary": "LGTM", "score": 85}'));
|
|
158
|
+
|
|
159
|
+
const closeCallback = mockProcess.on.mock.calls.find(c => c[0] === 'close')[1];
|
|
160
|
+
closeCallback(0);
|
|
161
|
+
}, 10);
|
|
162
|
+
|
|
163
|
+
const result = await runLocal('claude', 'test', { headlessArgs: ['-p'] });
|
|
164
|
+
|
|
165
|
+
expect(result.parsed).toEqual({ summary: 'LGTM', score: 85 });
|
|
166
|
+
});
|
|
167
|
+
|
|
168
|
+
it('handles non-JSON output', async () => {
|
|
169
|
+
const mockProcess = {
|
|
170
|
+
stdout: { on: vi.fn() },
|
|
171
|
+
stderr: { on: vi.fn() },
|
|
172
|
+
on: vi.fn(),
|
|
173
|
+
};
|
|
174
|
+
|
|
175
|
+
spawn.mockReturnValue(mockProcess);
|
|
176
|
+
|
|
177
|
+
setTimeout(() => {
|
|
178
|
+
const stdoutCallback = mockProcess.stdout.on.mock.calls.find(c => c[0] === 'data')[1];
|
|
179
|
+
stdoutCallback(Buffer.from('Plain text output'));
|
|
180
|
+
|
|
181
|
+
const closeCallback = mockProcess.on.mock.calls.find(c => c[0] === 'close')[1];
|
|
182
|
+
closeCallback(0);
|
|
183
|
+
}, 10);
|
|
184
|
+
|
|
185
|
+
const result = await runLocal('claude', 'test', { headlessArgs: ['-p'] });
|
|
186
|
+
|
|
187
|
+
expect(result.raw).toBe('Plain text output');
|
|
188
|
+
expect(result.parsed).toBeNull();
|
|
189
|
+
});
|
|
190
|
+
|
|
191
|
+
it('respects timeout', async () => {
|
|
192
|
+
const mockProcess = {
|
|
193
|
+
stdout: { on: vi.fn() },
|
|
194
|
+
stderr: { on: vi.fn() },
|
|
195
|
+
on: vi.fn(),
|
|
196
|
+
kill: vi.fn(),
|
|
197
|
+
};
|
|
198
|
+
|
|
199
|
+
spawn.mockReturnValue(mockProcess);
|
|
200
|
+
|
|
201
|
+
// Don't complete the process - let it timeout
|
|
202
|
+
const promise = runLocal('claude', 'test', {
|
|
203
|
+
headlessArgs: ['-p'],
|
|
204
|
+
timeout: 50,
|
|
205
|
+
});
|
|
206
|
+
|
|
207
|
+
await expect(promise).rejects.toThrow(/timeout/i);
|
|
208
|
+
});
|
|
209
|
+
});
|
|
210
|
+
|
|
211
|
+
describe('runViaDevserver', () => {
|
|
212
|
+
it('posts to devserver API', async () => {
|
|
213
|
+
global.fetch.mockResolvedValue({
|
|
214
|
+
ok: true,
|
|
215
|
+
json: () => Promise.resolve({ taskId: 'task-123' }),
|
|
216
|
+
});
|
|
217
|
+
|
|
218
|
+
// Mock polling response
|
|
219
|
+
global.fetch
|
|
220
|
+
.mockResolvedValueOnce({
|
|
221
|
+
ok: true,
|
|
222
|
+
json: () => Promise.resolve({ taskId: 'task-123' }),
|
|
223
|
+
})
|
|
224
|
+
.mockResolvedValueOnce({
|
|
225
|
+
ok: true,
|
|
226
|
+
json: () => Promise.resolve({
|
|
227
|
+
status: 'completed',
|
|
228
|
+
result: { raw: '{}', parsed: {}, exitCode: 0 },
|
|
229
|
+
}),
|
|
230
|
+
});
|
|
231
|
+
|
|
232
|
+
const result = await runViaDevserver({
|
|
233
|
+
devserverUrl: 'https://devserver.example.com',
|
|
234
|
+
provider: 'claude',
|
|
235
|
+
prompt: 'test prompt',
|
|
236
|
+
opts: {},
|
|
237
|
+
});
|
|
238
|
+
|
|
239
|
+
expect(global.fetch).toHaveBeenCalledWith(
|
|
240
|
+
'https://devserver.example.com/api/run',
|
|
241
|
+
expect.objectContaining({
|
|
242
|
+
method: 'POST',
|
|
243
|
+
})
|
|
244
|
+
);
|
|
245
|
+
});
|
|
246
|
+
|
|
247
|
+
it('polls for result', async () => {
|
|
248
|
+
global.fetch
|
|
249
|
+
.mockResolvedValueOnce({
|
|
250
|
+
ok: true,
|
|
251
|
+
json: () => Promise.resolve({ taskId: 'task-123' }),
|
|
252
|
+
})
|
|
253
|
+
.mockResolvedValueOnce({
|
|
254
|
+
ok: true,
|
|
255
|
+
json: () => Promise.resolve({ status: 'running' }),
|
|
256
|
+
})
|
|
257
|
+
.mockResolvedValueOnce({
|
|
258
|
+
ok: true,
|
|
259
|
+
json: () => Promise.resolve({
|
|
260
|
+
status: 'completed',
|
|
261
|
+
result: { raw: '{"done": true}', parsed: { done: true }, exitCode: 0 },
|
|
262
|
+
}),
|
|
263
|
+
});
|
|
264
|
+
|
|
265
|
+
const result = await runViaDevserver({
|
|
266
|
+
devserverUrl: 'https://devserver.example.com',
|
|
267
|
+
provider: 'claude',
|
|
268
|
+
prompt: 'test',
|
|
269
|
+
opts: {},
|
|
270
|
+
pollInterval: 10,
|
|
271
|
+
});
|
|
272
|
+
|
|
273
|
+
expect(global.fetch).toHaveBeenCalledTimes(3);
|
|
274
|
+
expect(result.parsed).toEqual({ done: true });
|
|
275
|
+
});
|
|
276
|
+
});
|
|
277
|
+
|
|
278
|
+
describe('buildArgs', () => {
|
|
279
|
+
it('includes output-format json', () => {
|
|
280
|
+
const args = buildArgs('claude', 'test prompt', {
|
|
281
|
+
headlessArgs: ['-p', '--output-format', 'json'],
|
|
282
|
+
});
|
|
283
|
+
|
|
284
|
+
expect(args).toContain('--output-format');
|
|
285
|
+
expect(args).toContain('json');
|
|
286
|
+
});
|
|
287
|
+
|
|
288
|
+
it('includes sandbox for codex', () => {
|
|
289
|
+
const args = buildArgs('codex', 'test prompt', {
|
|
290
|
+
headlessArgs: ['exec', '--json', '--sandbox', 'read-only'],
|
|
291
|
+
});
|
|
292
|
+
|
|
293
|
+
expect(args).toContain('--sandbox');
|
|
294
|
+
expect(args).toContain('read-only');
|
|
295
|
+
});
|
|
296
|
+
|
|
297
|
+
it('includes prompt in args', () => {
|
|
298
|
+
const args = buildArgs('claude', 'review this code', {
|
|
299
|
+
headlessArgs: ['-p'],
|
|
300
|
+
});
|
|
301
|
+
|
|
302
|
+
expect(args).toContain('review this code');
|
|
303
|
+
});
|
|
304
|
+
|
|
305
|
+
it('includes cwd option', () => {
|
|
306
|
+
const args = buildArgs('claude', 'test', {
|
|
307
|
+
headlessArgs: ['-p'],
|
|
308
|
+
cwd: '/project/dir',
|
|
309
|
+
});
|
|
310
|
+
|
|
311
|
+
// cwd is passed to spawn options, not args
|
|
312
|
+
// But buildArgs should handle it
|
|
313
|
+
expect(args).toBeDefined();
|
|
314
|
+
});
|
|
315
|
+
});
|
|
316
|
+
|
|
317
|
+
describe('parseOutput', () => {
|
|
318
|
+
it('parses valid JSON', () => {
|
|
319
|
+
const result = parseOutput('{"key": "value"}');
|
|
320
|
+
expect(result).toEqual({ key: 'value' });
|
|
321
|
+
});
|
|
322
|
+
|
|
323
|
+
it('returns null for invalid JSON', () => {
|
|
324
|
+
const result = parseOutput('not json');
|
|
325
|
+
expect(result).toBeNull();
|
|
326
|
+
});
|
|
327
|
+
|
|
328
|
+
it('handles empty output', () => {
|
|
329
|
+
const result = parseOutput('');
|
|
330
|
+
expect(result).toBeNull();
|
|
331
|
+
});
|
|
332
|
+
|
|
333
|
+
it('handles multiline JSON', () => {
|
|
334
|
+
const result = parseOutput(`{
|
|
335
|
+
"key": "value",
|
|
336
|
+
"nested": {
|
|
337
|
+
"array": [1, 2, 3]
|
|
338
|
+
}
|
|
339
|
+
}`);
|
|
340
|
+
expect(result.nested.array).toEqual([1, 2, 3]);
|
|
341
|
+
});
|
|
342
|
+
|
|
343
|
+
it('extracts JSON from mixed output', () => {
|
|
344
|
+
// Some CLIs may output text before/after JSON
|
|
345
|
+
const result = parseOutput('Some text\n{"result": "ok"}\nMore text');
|
|
346
|
+
expect(result).toEqual({ result: 'ok' });
|
|
347
|
+
});
|
|
348
|
+
});
|
|
349
|
+
});
|
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
import { describe, it, expect, beforeEach, afterEach, vi } from 'vitest';
|
|
2
|
+
import {
|
|
3
|
+
findOrphanedAgents,
|
|
4
|
+
resetCleanup,
|
|
5
|
+
} from './server/lib/agent-cleanup.js';
|
|
6
|
+
import { getAgentRegistry, resetRegistry } from './server/lib/agent-registry.js';
|
|
7
|
+
import { STATES } from './server/lib/agent-state.js';
|
|
8
|
+
|
|
9
|
+
describe('debug', () => {
|
|
10
|
+
const BASE_TIME = new Date('2025-01-01T12:00:00Z').getTime();
|
|
11
|
+
|
|
12
|
+
beforeEach(() => {
|
|
13
|
+
vi.useFakeTimers();
|
|
14
|
+
vi.setSystemTime(BASE_TIME);
|
|
15
|
+
resetRegistry();
|
|
16
|
+
resetCleanup();
|
|
17
|
+
});
|
|
18
|
+
|
|
19
|
+
afterEach(() => {
|
|
20
|
+
vi.useRealTimers();
|
|
21
|
+
});
|
|
22
|
+
|
|
23
|
+
it('debug test', () => {
|
|
24
|
+
const registry = getAgentRegistry();
|
|
25
|
+
|
|
26
|
+
console.log('BASE_TIME:', BASE_TIME);
|
|
27
|
+
console.log('Date.now() in test:', Date.now());
|
|
28
|
+
|
|
29
|
+
const lastActivity = Date.now() - 35 * 60 * 1000;
|
|
30
|
+
console.log('lastActivity:', lastActivity);
|
|
31
|
+
|
|
32
|
+
const id = registry.registerAgent({
|
|
33
|
+
name: 'stuck-agent',
|
|
34
|
+
model: 'claude-3',
|
|
35
|
+
type: 'worker',
|
|
36
|
+
status: STATES.RUNNING,
|
|
37
|
+
lastActivity: lastActivity,
|
|
38
|
+
});
|
|
39
|
+
|
|
40
|
+
const agent = registry.getAgent(id);
|
|
41
|
+
console.log('Registered agent:', agent);
|
|
42
|
+
console.log('Agent status:', agent.status);
|
|
43
|
+
console.log('Agent lastActivity:', agent.lastActivity);
|
|
44
|
+
|
|
45
|
+
const running = registry.listAgents({ status: STATES.RUNNING });
|
|
46
|
+
console.log('Running agents:', running.length);
|
|
47
|
+
|
|
48
|
+
const orphans = findOrphanedAgents();
|
|
49
|
+
console.log('Orphans found:', orphans.length);
|
|
50
|
+
|
|
51
|
+
// Let's see what Date.now() returns inside the filter
|
|
52
|
+
const timeout = 30 * 60 * 1000;
|
|
53
|
+
const now = Date.now();
|
|
54
|
+
console.log('now in test scope:', now);
|
|
55
|
+
console.log('lastActivity:', agent.lastActivity);
|
|
56
|
+
console.log('inactiveTime would be:', now - agent.lastActivity);
|
|
57
|
+
console.log('timeout:', timeout);
|
|
58
|
+
console.log('inactiveTime > timeout:', (now - agent.lastActivity) > timeout);
|
|
59
|
+
|
|
60
|
+
expect(orphans).toHaveLength(1);
|
|
61
|
+
});
|
|
62
|
+
});
|
|
@@ -0,0 +1,249 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Devserver Router API - HTTP endpoints for task execution
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
import { createRouter } from './model-router.js';
|
|
6
|
+
import { createQueue } from './provider-queue.js';
|
|
7
|
+
|
|
8
|
+
/**
|
|
9
|
+
* Create router API handlers
|
|
10
|
+
* @param {Object} options - API options
|
|
11
|
+
* @param {string} options.secret - Authentication secret
|
|
12
|
+
* @param {Object} [options.routerConfig] - Router configuration
|
|
13
|
+
* @param {Object} [options.queueConfig] - Queue configuration
|
|
14
|
+
* @returns {Promise<Object>} API handlers
|
|
15
|
+
*/
|
|
16
|
+
export async function createRouterAPI(options = {}) {
|
|
17
|
+
const { secret, routerConfig, queueConfig } = options;
|
|
18
|
+
|
|
19
|
+
// Initialize router and queue
|
|
20
|
+
const router = await createRouter(routerConfig);
|
|
21
|
+
const queue = createQueue(queueConfig);
|
|
22
|
+
|
|
23
|
+
return {
|
|
24
|
+
handleRun: handleRun(router, queue),
|
|
25
|
+
handleTaskStatus: handleTaskStatus(queue),
|
|
26
|
+
handleReview: handleReview(router),
|
|
27
|
+
handleDesign: handleDesign(router),
|
|
28
|
+
handleHealth: handleHealth(router),
|
|
29
|
+
validateAuth: validateAuth(secret),
|
|
30
|
+
router,
|
|
31
|
+
queue,
|
|
32
|
+
};
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
/**
|
|
36
|
+
* Handle POST /api/run - Queue a task
|
|
37
|
+
* @param {Object} router - Router instance
|
|
38
|
+
* @param {Object} queue - Queue instance
|
|
39
|
+
* @returns {Function} Express handler
|
|
40
|
+
*/
|
|
41
|
+
export function handleRun(router, queue) {
|
|
42
|
+
return async (req, res) => {
|
|
43
|
+
try {
|
|
44
|
+
const { capability, prompt, options = {} } = req.body;
|
|
45
|
+
|
|
46
|
+
// Create task function
|
|
47
|
+
const taskFn = async () => {
|
|
48
|
+
return await router.run(capability, prompt, options);
|
|
49
|
+
};
|
|
50
|
+
|
|
51
|
+
// Enqueue the task
|
|
52
|
+
const taskId = await queue.enqueue(taskFn, {
|
|
53
|
+
priority: options.priority || 5,
|
|
54
|
+
capability,
|
|
55
|
+
});
|
|
56
|
+
|
|
57
|
+
res.json({ taskId });
|
|
58
|
+
} catch (err) {
|
|
59
|
+
res.status(500).json({ error: err.message });
|
|
60
|
+
}
|
|
61
|
+
};
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
/**
|
|
65
|
+
* Handle GET /api/task/:taskId - Get task status
|
|
66
|
+
* @param {Object} queue - Queue instance
|
|
67
|
+
* @returns {Function} Express handler
|
|
68
|
+
*/
|
|
69
|
+
export function handleTaskStatus(queue) {
|
|
70
|
+
return async (req, res) => {
|
|
71
|
+
try {
|
|
72
|
+
const { taskId } = req.params;
|
|
73
|
+
const task = queue.getTask(taskId);
|
|
74
|
+
|
|
75
|
+
if (!task) {
|
|
76
|
+
return res.status(404).json({ error: 'Task not found' });
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
res.json({
|
|
80
|
+
status: task.status,
|
|
81
|
+
result: task.result,
|
|
82
|
+
error: task.error,
|
|
83
|
+
createdAt: task.createdAt,
|
|
84
|
+
completedAt: task.completedAt,
|
|
85
|
+
});
|
|
86
|
+
} catch (err) {
|
|
87
|
+
res.status(500).json({ error: err.message });
|
|
88
|
+
}
|
|
89
|
+
};
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
/**
|
|
93
|
+
* Handle POST /api/review - Multi-model review
|
|
94
|
+
* @param {Object} router - Router instance
|
|
95
|
+
* @returns {Function} Express handler
|
|
96
|
+
*/
|
|
97
|
+
export function handleReview(router) {
|
|
98
|
+
return async (req, res) => {
|
|
99
|
+
try {
|
|
100
|
+
const { code, prompt = 'Review this code', options = {} } = req.body;
|
|
101
|
+
|
|
102
|
+
const fullPrompt = code ? `${prompt}\n\n\`\`\`\n${code}\n\`\`\`` : prompt;
|
|
103
|
+
|
|
104
|
+
const results = await router.run('review', fullPrompt, options);
|
|
105
|
+
|
|
106
|
+
// Calculate consensus
|
|
107
|
+
const consensus = calculateConsensus(results);
|
|
108
|
+
|
|
109
|
+
res.json({
|
|
110
|
+
consensus,
|
|
111
|
+
results,
|
|
112
|
+
});
|
|
113
|
+
} catch (err) {
|
|
114
|
+
res.status(500).json({ error: err.message });
|
|
115
|
+
}
|
|
116
|
+
};
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
/**
|
|
120
|
+
* Handle POST /api/design - Design generation
|
|
121
|
+
* @param {Object} router - Router instance
|
|
122
|
+
* @returns {Function} Express handler
|
|
123
|
+
*/
|
|
124
|
+
export function handleDesign(router) {
|
|
125
|
+
return async (req, res) => {
|
|
126
|
+
try {
|
|
127
|
+
const { prompt, options = {} } = req.body;
|
|
128
|
+
|
|
129
|
+
const results = await router.run('design', prompt, options);
|
|
130
|
+
|
|
131
|
+
// Return first successful result
|
|
132
|
+
const successful = results.find((r) => r.success);
|
|
133
|
+
|
|
134
|
+
res.json({
|
|
135
|
+
result: successful?.result || null,
|
|
136
|
+
provider: successful?.provider,
|
|
137
|
+
error: successful ? null : 'No successful design generated',
|
|
138
|
+
});
|
|
139
|
+
} catch (err) {
|
|
140
|
+
res.status(500).json({ error: err.message });
|
|
141
|
+
}
|
|
142
|
+
};
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
/**
|
|
146
|
+
* Handle GET /api/health - Provider health
|
|
147
|
+
* @param {Object} router - Router instance
|
|
148
|
+
* @returns {Function} Express handler
|
|
149
|
+
*/
|
|
150
|
+
export function handleHealth(router) {
|
|
151
|
+
return async (req, res) => {
|
|
152
|
+
try {
|
|
153
|
+
const status = router.getStatus();
|
|
154
|
+
|
|
155
|
+
// Check if at least one provider is available
|
|
156
|
+
const hasAvailable = Object.values(status.providers || {}).some(
|
|
157
|
+
(p) => p.detected || p.type === 'api'
|
|
158
|
+
);
|
|
159
|
+
|
|
160
|
+
res.json({
|
|
161
|
+
healthy: hasAvailable,
|
|
162
|
+
providers: status.providers,
|
|
163
|
+
devserver: status.devserver,
|
|
164
|
+
});
|
|
165
|
+
} catch (err) {
|
|
166
|
+
res.status(500).json({ error: err.message, healthy: false });
|
|
167
|
+
}
|
|
168
|
+
};
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
/**
|
|
172
|
+
* Authentication middleware
|
|
173
|
+
* @param {string} secret - Expected secret
|
|
174
|
+
* @returns {Function} Express middleware
|
|
175
|
+
*/
|
|
176
|
+
export function validateAuth(secret) {
|
|
177
|
+
return (req, res, next) => {
|
|
178
|
+
const authHeader = req.headers.authorization;
|
|
179
|
+
|
|
180
|
+
if (!authHeader) {
|
|
181
|
+
return res.status(401).json({ error: 'Authorization header required' });
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
const token = authHeader.replace('Bearer ', '');
|
|
185
|
+
|
|
186
|
+
if (token !== secret) {
|
|
187
|
+
return res.status(401).json({ error: 'Invalid authorization token' });
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
next();
|
|
191
|
+
};
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
/**
|
|
195
|
+
* Request body validation middleware
|
|
196
|
+
* @param {string[]} requiredFields - Required field names
|
|
197
|
+
* @returns {Function} Express middleware
|
|
198
|
+
*/
|
|
199
|
+
export function validateRequestBody(requiredFields) {
|
|
200
|
+
return (req, res, next) => {
|
|
201
|
+
const body = req.body || {};
|
|
202
|
+
const missing = requiredFields.filter((field) => !body[field]);
|
|
203
|
+
|
|
204
|
+
if (missing.length > 0) {
|
|
205
|
+
return res.status(400).json({
|
|
206
|
+
error: `Missing required fields: ${missing.join(', ')}`,
|
|
207
|
+
});
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
next();
|
|
211
|
+
};
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
/**
|
|
215
|
+
* Calculate consensus from multiple provider results
|
|
216
|
+
* @param {Object[]} results - Provider results
|
|
217
|
+
* @returns {Object} Consensus result
|
|
218
|
+
*/
|
|
219
|
+
function calculateConsensus(results) {
|
|
220
|
+
const successful = results.filter((r) => r.success);
|
|
221
|
+
|
|
222
|
+
if (successful.length === 0) {
|
|
223
|
+
return { approved: false, reason: 'No successful reviews' };
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
// Count approvals
|
|
227
|
+
const approvals = successful.filter((r) => r.result?.approved).length;
|
|
228
|
+
const total = successful.length;
|
|
229
|
+
|
|
230
|
+
// Majority vote
|
|
231
|
+
const approved = approvals > total / 2;
|
|
232
|
+
|
|
233
|
+
// Average score if available
|
|
234
|
+
const scores = successful
|
|
235
|
+
.map((r) => r.result?.score)
|
|
236
|
+
.filter((s) => typeof s === 'number');
|
|
237
|
+
|
|
238
|
+
const averageScore =
|
|
239
|
+
scores.length > 0
|
|
240
|
+
? Math.round(scores.reduce((a, b) => a + b, 0) / scores.length)
|
|
241
|
+
: null;
|
|
242
|
+
|
|
243
|
+
return {
|
|
244
|
+
approved,
|
|
245
|
+
votes: { approve: approvals, reject: total - approvals },
|
|
246
|
+
averageScore,
|
|
247
|
+
providers: successful.map((r) => r.provider),
|
|
248
|
+
};
|
|
249
|
+
}
|