claudehq 1.0.1 → 1.0.3

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.
@@ -0,0 +1,1010 @@
1
+ /**
2
+ * API Routes Module - HTTP route handlers
3
+ *
4
+ * All API endpoint handlers organized by domain.
5
+ */
6
+
7
+ const { TMUX_SESSION } = require('../core/config');
8
+
9
+ /**
10
+ * Create all API route handlers
11
+ * @param {Object} deps - Dependencies injection
12
+ * @returns {Object} Route handlers
13
+ */
14
+ function createRoutes(deps) {
15
+ const {
16
+ // Core
17
+ sseClients,
18
+ claudeEvents,
19
+ addClaudeEvent,
20
+ broadcastManagedSessions,
21
+
22
+ // Sessions
23
+ getManagedSessions,
24
+ getManagedSession,
25
+ createManagedSession,
26
+ updateManagedSession,
27
+ deleteManagedSession,
28
+ sendPromptToManagedSession,
29
+ restartManagedSession,
30
+ linkClaudeSession,
31
+ checkSessionHealth,
32
+ saveManagedSessions,
33
+ managedSessions,
34
+ hideSession,
35
+ unhideSession,
36
+ permanentDeleteSession,
37
+ loadHiddenSessions,
38
+ loadCustomNames,
39
+ renameSession,
40
+
41
+ // Data
42
+ loadAllTasks,
43
+ updateTask,
44
+ createTask,
45
+ loadAllTodos,
46
+ getTodosForSession,
47
+ updateTodo,
48
+ createTodo,
49
+ loadAllPlans,
50
+ getPlansForSession,
51
+ getPlan,
52
+ updatePlan,
53
+ loadConversation,
54
+
55
+ // Orchestration
56
+ orchestrationData,
57
+ orchestrationExecutor,
58
+
59
+ // Utils
60
+ sendToTmux
61
+ } = deps;
62
+
63
+ return {
64
+ // =========================================================================
65
+ // SSE Events
66
+ // =========================================================================
67
+ handleSSE(req, res) {
68
+ res.writeHead(200, {
69
+ 'Content-Type': 'text/event-stream',
70
+ 'Cache-Control': 'no-cache',
71
+ 'Connection': 'keep-alive',
72
+ 'Access-Control-Allow-Origin': '*'
73
+ });
74
+
75
+ res.write('data: {"type":"connected"}\n\n');
76
+
77
+ const recentEvents = claudeEvents.slice(-50);
78
+ if (recentEvents.length > 0) {
79
+ res.write(`data: ${JSON.stringify({ type: 'history', events: recentEvents })}\n\n`);
80
+ }
81
+
82
+ res.write(`data: ${JSON.stringify({ type: 'sessions', sessions: getManagedSessions() })}\n\n`);
83
+
84
+ sseClients.add(res);
85
+ req.on('close', () => sseClients.delete(res));
86
+ },
87
+
88
+ // =========================================================================
89
+ // Health
90
+ // =========================================================================
91
+ handleHealth(req, res) {
92
+ res.writeHead(200, { 'Content-Type': 'application/json' });
93
+ res.end(JSON.stringify({ ok: true, version: '1.0.0' }));
94
+ },
95
+
96
+ // =========================================================================
97
+ // Claude Events
98
+ // =========================================================================
99
+ handlePostClaudeEvent(req, res) {
100
+ let body = '';
101
+ req.on('data', chunk => body += chunk);
102
+ req.on('end', () => {
103
+ try {
104
+ const event = JSON.parse(body);
105
+ addClaudeEvent(event);
106
+ res.writeHead(200, { 'Content-Type': 'application/json' });
107
+ res.end(JSON.stringify({ ok: true }));
108
+ } catch (e) {
109
+ res.writeHead(400, { 'Content-Type': 'application/json' });
110
+ res.end(JSON.stringify({ ok: false, error: 'Invalid JSON' }));
111
+ }
112
+ });
113
+ },
114
+
115
+ handleGetClaudeEvents(req, res, url) {
116
+ const limit = parseInt(url.searchParams.get('limit') || '500');
117
+ const sessionId = url.searchParams.get('sessionId');
118
+
119
+ let filtered = claudeEvents;
120
+ if (sessionId) {
121
+ filtered = claudeEvents.filter(e => e.sessionId === sessionId);
122
+ }
123
+
124
+ res.writeHead(200, { 'Content-Type': 'application/json' });
125
+ res.end(JSON.stringify({
126
+ ok: true,
127
+ events: filtered.slice(-limit),
128
+ total: filtered.length
129
+ }));
130
+ },
131
+
132
+ handleGetClaudeEventStats(req, res) {
133
+ const toolCounts = {};
134
+ const toolDurations = {};
135
+ const sessionActivity = {};
136
+
137
+ for (const event of claudeEvents) {
138
+ sessionActivity[event.sessionId] = (sessionActivity[event.sessionId] || 0) + 1;
139
+
140
+ if (event.type === 'post_tool_use') {
141
+ toolCounts[event.tool] = (toolCounts[event.tool] || 0) + 1;
142
+ if (event.duration !== undefined) {
143
+ if (!toolDurations[event.tool]) toolDurations[event.tool] = [];
144
+ toolDurations[event.tool].push(event.duration);
145
+ }
146
+ }
147
+ }
148
+
149
+ const avgDurations = {};
150
+ for (const [tool, durations] of Object.entries(toolDurations)) {
151
+ avgDurations[tool] = Math.round(
152
+ durations.reduce((a, b) => a + b, 0) / durations.length
153
+ );
154
+ }
155
+
156
+ res.writeHead(200, { 'Content-Type': 'application/json' });
157
+ res.end(JSON.stringify({
158
+ totalEvents: claudeEvents.length,
159
+ toolCounts,
160
+ avgDurations,
161
+ sessionActivity
162
+ }));
163
+ },
164
+
165
+ // =========================================================================
166
+ // Tasks
167
+ // =========================================================================
168
+ handleGetTasks(req, res) {
169
+ res.writeHead(200, { 'Content-Type': 'application/json' });
170
+ res.end(JSON.stringify(loadAllTasks()));
171
+ },
172
+
173
+ handleBulkUpdateTasks(req, res) {
174
+ let body = '';
175
+ req.on('data', chunk => body += chunk);
176
+ req.on('end', () => {
177
+ try {
178
+ const { sessionId, taskIds, updates } = JSON.parse(body);
179
+ if (!sessionId || !taskIds || !Array.isArray(taskIds) || !updates) {
180
+ res.writeHead(400, { 'Content-Type': 'application/json' });
181
+ res.end(JSON.stringify({ ok: false, error: 'Missing sessionId, taskIds, or updates' }));
182
+ return;
183
+ }
184
+
185
+ const results = [];
186
+ for (const taskId of taskIds) {
187
+ const result = updateTask(sessionId, taskId, updates);
188
+ results.push({ taskId, ...result });
189
+ }
190
+
191
+ const failedCount = results.filter(r => r.error).length;
192
+ res.writeHead(200, { 'Content-Type': 'application/json' });
193
+ res.end(JSON.stringify({
194
+ ok: failedCount === 0,
195
+ updated: results.filter(r => r.success).length,
196
+ failed: failedCount,
197
+ results
198
+ }));
199
+ } catch (e) {
200
+ res.writeHead(400, { 'Content-Type': 'application/json' });
201
+ res.end(JSON.stringify({ ok: false, error: e.message }));
202
+ }
203
+ });
204
+ },
205
+
206
+ handleUpdateTask(req, res, sessionId, taskId) {
207
+ let body = '';
208
+ req.on('data', chunk => body += chunk);
209
+ req.on('end', () => {
210
+ try {
211
+ const updates = JSON.parse(body);
212
+ const result = updateTask(sessionId, taskId, updates);
213
+ res.writeHead(result.error ? 400 : 200, { 'Content-Type': 'application/json' });
214
+ res.end(JSON.stringify(result));
215
+ } catch (e) {
216
+ res.writeHead(400, { 'Content-Type': 'application/json' });
217
+ res.end(JSON.stringify({ error: e.message }));
218
+ }
219
+ });
220
+ },
221
+
222
+ handleCreateTask(req, res, sessionId) {
223
+ let body = '';
224
+ req.on('data', chunk => body += chunk);
225
+ req.on('end', () => {
226
+ try {
227
+ const taskData = JSON.parse(body);
228
+ const result = createTask(sessionId, taskData);
229
+ res.writeHead(result.error ? 400 : 201, { 'Content-Type': 'application/json' });
230
+ res.end(JSON.stringify(result));
231
+ } catch (e) {
232
+ res.writeHead(400, { 'Content-Type': 'application/json' });
233
+ res.end(JSON.stringify({ error: e.message }));
234
+ }
235
+ });
236
+ },
237
+
238
+ // =========================================================================
239
+ // Todos
240
+ // =========================================================================
241
+ handleGetAllTodos(req, res) {
242
+ res.writeHead(200, { 'Content-Type': 'application/json' });
243
+ res.end(JSON.stringify({ ok: true, todosBySession: loadAllTodos() }));
244
+ },
245
+
246
+ handleBulkUpdateTodos(req, res) {
247
+ let body = '';
248
+ req.on('data', chunk => body += chunk);
249
+ req.on('end', () => {
250
+ try {
251
+ const { sessionId, todoIndexes, updates } = JSON.parse(body);
252
+ if (!sessionId || !todoIndexes || !Array.isArray(todoIndexes) || !updates) {
253
+ res.writeHead(400, { 'Content-Type': 'application/json' });
254
+ res.end(JSON.stringify({ ok: false, error: 'Missing sessionId, todoIndexes, or updates' }));
255
+ return;
256
+ }
257
+
258
+ const results = [];
259
+ for (const todoIndex of todoIndexes) {
260
+ const result = updateTodo(sessionId, todoIndex, updates);
261
+ results.push({ todoIndex, ...result });
262
+ }
263
+
264
+ const failedCount = results.filter(r => r.error).length;
265
+ res.writeHead(200, { 'Content-Type': 'application/json' });
266
+ res.end(JSON.stringify({
267
+ ok: failedCount === 0,
268
+ updated: results.filter(r => r.success).length,
269
+ failed: failedCount,
270
+ results
271
+ }));
272
+ } catch (e) {
273
+ res.writeHead(400, { 'Content-Type': 'application/json' });
274
+ res.end(JSON.stringify({ ok: false, error: e.message }));
275
+ }
276
+ });
277
+ },
278
+
279
+ handleGetTodosForSession(req, res, sessionId) {
280
+ const result = getTodosForSession(sessionId);
281
+ res.writeHead(200, { 'Content-Type': 'application/json' });
282
+ res.end(JSON.stringify({ ok: true, ...result }));
283
+ },
284
+
285
+ handleCreateTodo(req, res, sessionId) {
286
+ let body = '';
287
+ req.on('data', chunk => body += chunk);
288
+ req.on('end', () => {
289
+ try {
290
+ const todoData = JSON.parse(body);
291
+ const result = createTodo(sessionId, todoData);
292
+ if (result.success) {
293
+ res.writeHead(201, { 'Content-Type': 'application/json' });
294
+ res.end(JSON.stringify({ ok: true, todo: result.todo }));
295
+ } else {
296
+ res.writeHead(400, { 'Content-Type': 'application/json' });
297
+ res.end(JSON.stringify({ ok: false, error: result.error }));
298
+ }
299
+ } catch (e) {
300
+ res.writeHead(400, { 'Content-Type': 'application/json' });
301
+ res.end(JSON.stringify({ ok: false, error: e.message }));
302
+ }
303
+ });
304
+ },
305
+
306
+ handleUpdateTodo(req, res, sessionId, todoIndex) {
307
+ let body = '';
308
+ req.on('data', chunk => body += chunk);
309
+ req.on('end', () => {
310
+ try {
311
+ const updates = JSON.parse(body);
312
+ const result = updateTodo(sessionId, parseInt(todoIndex), updates);
313
+ if (result.success) {
314
+ res.writeHead(200, { 'Content-Type': 'application/json' });
315
+ res.end(JSON.stringify({ ok: true, todo: result.todo }));
316
+ } else {
317
+ res.writeHead(400, { 'Content-Type': 'application/json' });
318
+ res.end(JSON.stringify({ ok: false, error: result.error }));
319
+ }
320
+ } catch (e) {
321
+ res.writeHead(400, { 'Content-Type': 'application/json' });
322
+ res.end(JSON.stringify({ ok: false, error: e.message }));
323
+ }
324
+ });
325
+ },
326
+
327
+ // =========================================================================
328
+ // Plans
329
+ // =========================================================================
330
+ handleGetAllPlans(req, res) {
331
+ res.writeHead(200, { 'Content-Type': 'application/json' });
332
+ res.end(JSON.stringify({ ok: true, plans: loadAllPlans() }));
333
+ },
334
+
335
+ handleGetPlansForSession(req, res, sessionId) {
336
+ const plans = getPlansForSession(sessionId);
337
+ res.writeHead(200, { 'Content-Type': 'application/json' });
338
+ res.end(JSON.stringify({ ok: true, sessionId, plans }));
339
+ },
340
+
341
+ handleGetPlan(req, res, slug) {
342
+ const result = getPlan(slug);
343
+ if (result.error) {
344
+ res.writeHead(404, { 'Content-Type': 'application/json' });
345
+ res.end(JSON.stringify({ ok: false, error: result.error }));
346
+ } else {
347
+ res.writeHead(200, { 'Content-Type': 'application/json' });
348
+ res.end(JSON.stringify({ ok: true, plan: result }));
349
+ }
350
+ },
351
+
352
+ handleUpdatePlan(req, res, slug) {
353
+ let body = '';
354
+ req.on('data', chunk => body += chunk);
355
+ req.on('end', () => {
356
+ try {
357
+ const { content } = JSON.parse(body);
358
+ const result = updatePlan(slug, content);
359
+ if (result.success) {
360
+ res.writeHead(200, { 'Content-Type': 'application/json' });
361
+ res.end(JSON.stringify({ ok: true, slug: result.slug }));
362
+ } else {
363
+ res.writeHead(400, { 'Content-Type': 'application/json' });
364
+ res.end(JSON.stringify({ ok: false, error: result.error }));
365
+ }
366
+ } catch (e) {
367
+ res.writeHead(400, { 'Content-Type': 'application/json' });
368
+ res.end(JSON.stringify({ ok: false, error: e.message }));
369
+ }
370
+ });
371
+ },
372
+
373
+ // =========================================================================
374
+ // Managed Sessions
375
+ // =========================================================================
376
+ handleGetManagedSessions(req, res) {
377
+ res.writeHead(200, { 'Content-Type': 'application/json' });
378
+ res.end(JSON.stringify({ ok: true, sessions: getManagedSessions() }));
379
+ },
380
+
381
+ handleCreateManagedSession(req, res) {
382
+ let body = '';
383
+ req.on('data', chunk => body += chunk);
384
+ req.on('end', async () => {
385
+ try {
386
+ const { name, cwd, flags } = JSON.parse(body);
387
+ const session = await createManagedSession({ name, cwd, flags });
388
+ res.writeHead(201, { 'Content-Type': 'application/json' });
389
+ res.end(JSON.stringify({ ok: true, session }));
390
+ } catch (e) {
391
+ res.writeHead(400, { 'Content-Type': 'application/json' });
392
+ res.end(JSON.stringify({ ok: false, error: e.message }));
393
+ }
394
+ });
395
+ },
396
+
397
+ handleRefreshSessions(req, res) {
398
+ checkSessionHealth();
399
+ res.writeHead(200, { 'Content-Type': 'application/json' });
400
+ res.end(JSON.stringify({ ok: true, sessions: getManagedSessions() }));
401
+ },
402
+
403
+ handleGetManagedSession(req, res, id) {
404
+ const session = getManagedSession(id);
405
+ if (session) {
406
+ res.writeHead(200, { 'Content-Type': 'application/json' });
407
+ res.end(JSON.stringify({ ok: true, session }));
408
+ } else {
409
+ res.writeHead(404, { 'Content-Type': 'application/json' });
410
+ res.end(JSON.stringify({ ok: false, error: 'Session not found' }));
411
+ }
412
+ },
413
+
414
+ handleUpdateManagedSession(req, res, id) {
415
+ let body = '';
416
+ req.on('data', chunk => body += chunk);
417
+ req.on('end', () => {
418
+ try {
419
+ const updates = JSON.parse(body);
420
+ const session = updateManagedSession(id, updates);
421
+ if (session) {
422
+ res.writeHead(200, { 'Content-Type': 'application/json' });
423
+ res.end(JSON.stringify({ ok: true, session }));
424
+ } else {
425
+ res.writeHead(404, { 'Content-Type': 'application/json' });
426
+ res.end(JSON.stringify({ ok: false, error: 'Session not found' }));
427
+ }
428
+ } catch (e) {
429
+ res.writeHead(400, { 'Content-Type': 'application/json' });
430
+ res.end(JSON.stringify({ ok: false, error: e.message }));
431
+ }
432
+ });
433
+ },
434
+
435
+ handleHideSession(req, res, id) {
436
+ const session = managedSessions.get(id);
437
+ if (session) {
438
+ const idToHide = session.claudeSessionId || id;
439
+ hideSession(idToHide);
440
+ broadcastManagedSessions();
441
+ res.writeHead(200, { 'Content-Type': 'application/json' });
442
+ res.end(JSON.stringify({ ok: true }));
443
+ } else {
444
+ res.writeHead(404, { 'Content-Type': 'application/json' });
445
+ res.end(JSON.stringify({ ok: false, error: 'Session not found' }));
446
+ }
447
+ },
448
+
449
+ handleUnhideSession(req, res, id) {
450
+ unhideSession(id);
451
+ broadcastManagedSessions();
452
+ res.writeHead(200, { 'Content-Type': 'application/json' });
453
+ res.end(JSON.stringify({ ok: true }));
454
+ },
455
+
456
+ handleGetHiddenSessions(req, res) {
457
+ const hiddenIds = loadHiddenSessions();
458
+ const customNames = loadCustomNames();
459
+ const sessions = hiddenIds.map(id => {
460
+ let name = customNames[id] || id.substring(0, 8);
461
+ for (const [managedId, session] of managedSessions) {
462
+ if (session.claudeSessionId === id || managedId === id) {
463
+ name = session.name || name;
464
+ break;
465
+ }
466
+ }
467
+ return { id, name };
468
+ });
469
+ res.writeHead(200, { 'Content-Type': 'application/json' });
470
+ res.end(JSON.stringify({ ok: true, sessions }));
471
+ },
472
+
473
+ handleDeleteSession(req, res, id) {
474
+ const session = managedSessions.get(id);
475
+ if (session) {
476
+ const idToHide = session.claudeSessionId || id;
477
+ hideSession(idToHide);
478
+ broadcastManagedSessions();
479
+ res.writeHead(200, { 'Content-Type': 'application/json' });
480
+ res.end(JSON.stringify({ ok: true }));
481
+ } else {
482
+ res.writeHead(404, { 'Content-Type': 'application/json' });
483
+ res.end(JSON.stringify({ ok: false, error: 'Session not found' }));
484
+ }
485
+ },
486
+
487
+ handlePermanentDeleteSession(req, res, id) {
488
+ const result = permanentDeleteSession(id);
489
+ if (result.success) {
490
+ res.writeHead(200, { 'Content-Type': 'application/json' });
491
+ res.end(JSON.stringify({ ok: true }));
492
+ } else {
493
+ res.writeHead(404, { 'Content-Type': 'application/json' });
494
+ res.end(JSON.stringify({ ok: false, error: result.error }));
495
+ }
496
+ },
497
+
498
+ handleSendPromptToSession(req, res, id) {
499
+ let body = '';
500
+ req.on('data', chunk => body += chunk);
501
+ req.on('end', async () => {
502
+ try {
503
+ const { prompt } = JSON.parse(body);
504
+ if (!prompt) {
505
+ res.writeHead(400, { 'Content-Type': 'application/json' });
506
+ res.end(JSON.stringify({ ok: false, error: 'Prompt is required' }));
507
+ return;
508
+ }
509
+ const result = await sendPromptToManagedSession(id, prompt);
510
+ res.writeHead(result.ok ? 200 : 404, { 'Content-Type': 'application/json' });
511
+ res.end(JSON.stringify(result));
512
+ } catch (e) {
513
+ res.writeHead(500, { 'Content-Type': 'application/json' });
514
+ res.end(JSON.stringify({ ok: false, error: e.message }));
515
+ }
516
+ });
517
+ },
518
+
519
+ handleRestartSession(req, res, id) {
520
+ restartManagedSession(id).then((session) => {
521
+ if (session) {
522
+ res.writeHead(200, { 'Content-Type': 'application/json' });
523
+ res.end(JSON.stringify({ ok: true, session }));
524
+ } else {
525
+ res.writeHead(404, { 'Content-Type': 'application/json' });
526
+ res.end(JSON.stringify({ ok: false, error: 'Session not found' }));
527
+ }
528
+ }).catch((e) => {
529
+ res.writeHead(500, { 'Content-Type': 'application/json' });
530
+ res.end(JSON.stringify({ ok: false, error: e.message }));
531
+ });
532
+ },
533
+
534
+ handleLinkSession(req, res, id) {
535
+ let body = '';
536
+ req.on('data', chunk => body += chunk);
537
+ req.on('end', () => {
538
+ try {
539
+ const { claudeSessionId } = JSON.parse(body);
540
+ if (!claudeSessionId) {
541
+ res.writeHead(400, { 'Content-Type': 'application/json' });
542
+ res.end(JSON.stringify({ ok: false, error: 'claudeSessionId is required' }));
543
+ return;
544
+ }
545
+ const session = getManagedSession(id);
546
+ if (!session) {
547
+ res.writeHead(404, { 'Content-Type': 'application/json' });
548
+ res.end(JSON.stringify({ ok: false, error: 'Session not found' }));
549
+ return;
550
+ }
551
+ linkClaudeSession(claudeSessionId, id);
552
+ broadcastManagedSessions();
553
+ saveManagedSessions();
554
+ res.writeHead(200, { 'Content-Type': 'application/json' });
555
+ res.end(JSON.stringify({ ok: true, session: getManagedSession(id) }));
556
+ } catch (e) {
557
+ res.writeHead(400, { 'Content-Type': 'application/json' });
558
+ res.end(JSON.stringify({ ok: false, error: e.message }));
559
+ }
560
+ });
561
+ },
562
+
563
+ // =========================================================================
564
+ // Conversation
565
+ // =========================================================================
566
+ handleGetConversation(req, res, sessionId) {
567
+ const conversation = loadConversation(sessionId);
568
+ res.writeHead(200, { 'Content-Type': 'application/json' });
569
+ res.end(JSON.stringify(conversation));
570
+ },
571
+
572
+ // =========================================================================
573
+ // Prompt
574
+ // =========================================================================
575
+ handleSendPrompt(req, res) {
576
+ let body = '';
577
+ req.on('data', chunk => body += chunk);
578
+ req.on('end', async () => {
579
+ try {
580
+ const { prompt, tmuxSession } = JSON.parse(body);
581
+ if (!prompt) {
582
+ res.writeHead(400, { 'Content-Type': 'application/json' });
583
+ res.end(JSON.stringify({ ok: false, error: 'Prompt is required' }));
584
+ return;
585
+ }
586
+ const session = tmuxSession || TMUX_SESSION;
587
+ await sendToTmux(session, prompt);
588
+ res.writeHead(200, { 'Content-Type': 'application/json' });
589
+ res.end(JSON.stringify({ ok: true, session }));
590
+ } catch (e) {
591
+ res.writeHead(500, { 'Content-Type': 'application/json' });
592
+ res.end(JSON.stringify({ ok: false, error: e.message }));
593
+ }
594
+ });
595
+ },
596
+
597
+ handleRenameSession(req, res, sessionId) {
598
+ let body = '';
599
+ req.on('data', chunk => body += chunk);
600
+ req.on('end', () => {
601
+ try {
602
+ const { name } = JSON.parse(body);
603
+ const result = renameSession(sessionId, name);
604
+ res.writeHead(200, { 'Content-Type': 'application/json' });
605
+ res.end(JSON.stringify(result));
606
+ } catch (e) {
607
+ res.writeHead(400, { 'Content-Type': 'application/json' });
608
+ res.end(JSON.stringify({ error: e.message }));
609
+ }
610
+ });
611
+ },
612
+
613
+ // =========================================================================
614
+ // Orchestration
615
+ // =========================================================================
616
+ handleGetOrchestrations(req, res) {
617
+ if (!orchestrationData) {
618
+ res.writeHead(501, { 'Content-Type': 'application/json' });
619
+ res.end(JSON.stringify({ ok: false, error: 'Orchestration not available' }));
620
+ return;
621
+ }
622
+ const orchestrations = orchestrationData.listOrchestrations();
623
+ res.writeHead(200, { 'Content-Type': 'application/json' });
624
+ res.end(JSON.stringify({ ok: true, orchestrations }));
625
+ },
626
+
627
+ handleCreateOrchestration(req, res) {
628
+ if (!orchestrationData) {
629
+ res.writeHead(501, { 'Content-Type': 'application/json' });
630
+ res.end(JSON.stringify({ ok: false, error: 'Orchestration not available' }));
631
+ return;
632
+ }
633
+ let body = '';
634
+ req.on('data', chunk => body += chunk);
635
+ req.on('end', () => {
636
+ try {
637
+ const config = JSON.parse(body);
638
+ const result = orchestrationData.createOrchestration(config);
639
+ if (result.error) {
640
+ res.writeHead(400, { 'Content-Type': 'application/json' });
641
+ res.end(JSON.stringify({ ok: false, error: result.error }));
642
+ } else {
643
+ res.writeHead(201, { 'Content-Type': 'application/json' });
644
+ res.end(JSON.stringify({ ok: true, orchestration: result.orchestration }));
645
+ }
646
+ } catch (e) {
647
+ res.writeHead(400, { 'Content-Type': 'application/json' });
648
+ res.end(JSON.stringify({ ok: false, error: e.message }));
649
+ }
650
+ });
651
+ },
652
+
653
+ handleGetOrchestration(req, res, orchestrationId) {
654
+ if (!orchestrationData) {
655
+ res.writeHead(501, { 'Content-Type': 'application/json' });
656
+ res.end(JSON.stringify({ ok: false, error: 'Orchestration not available' }));
657
+ return;
658
+ }
659
+ const orchestration = orchestrationData.getOrchestration(orchestrationId);
660
+ if (!orchestration) {
661
+ res.writeHead(404, { 'Content-Type': 'application/json' });
662
+ res.end(JSON.stringify({ ok: false, error: 'Orchestration not found' }));
663
+ return;
664
+ }
665
+ const stats = orchestrationData.getOrchestrationStats(orchestrationId);
666
+ res.writeHead(200, { 'Content-Type': 'application/json' });
667
+ res.end(JSON.stringify({ ok: true, orchestration, stats }));
668
+ },
669
+
670
+ handleUpdateOrchestration(req, res, orchestrationId) {
671
+ if (!orchestrationData) {
672
+ res.writeHead(501, { 'Content-Type': 'application/json' });
673
+ res.end(JSON.stringify({ ok: false, error: 'Orchestration not available' }));
674
+ return;
675
+ }
676
+ let body = '';
677
+ req.on('data', chunk => body += chunk);
678
+ req.on('end', () => {
679
+ try {
680
+ const updates = JSON.parse(body);
681
+ const result = orchestrationData.updateOrchestration(orchestrationId, updates);
682
+ if (result.error) {
683
+ res.writeHead(400, { 'Content-Type': 'application/json' });
684
+ res.end(JSON.stringify({ ok: false, error: result.error }));
685
+ } else {
686
+ res.writeHead(200, { 'Content-Type': 'application/json' });
687
+ res.end(JSON.stringify({ ok: true, orchestration: result.orchestration }));
688
+ }
689
+ } catch (e) {
690
+ res.writeHead(400, { 'Content-Type': 'application/json' });
691
+ res.end(JSON.stringify({ ok: false, error: e.message }));
692
+ }
693
+ });
694
+ },
695
+
696
+ handleDeleteOrchestration(req, res, orchestrationId) {
697
+ if (!orchestrationData) {
698
+ res.writeHead(501, { 'Content-Type': 'application/json' });
699
+ res.end(JSON.stringify({ ok: false, error: 'Orchestration not available' }));
700
+ return;
701
+ }
702
+ const result = orchestrationData.deleteOrchestration(orchestrationId);
703
+ if (result.error) {
704
+ res.writeHead(404, { 'Content-Type': 'application/json' });
705
+ res.end(JSON.stringify({ ok: false, error: result.error }));
706
+ } else {
707
+ res.writeHead(200, { 'Content-Type': 'application/json' });
708
+ res.end(JSON.stringify({ ok: true }));
709
+ }
710
+ },
711
+
712
+ handleStartOrchestration(req, res, orchestrationId) {
713
+ if (!orchestrationExecutor) {
714
+ res.writeHead(501, { 'Content-Type': 'application/json' });
715
+ res.end(JSON.stringify({ ok: false, error: 'Orchestration executor not available' }));
716
+ return;
717
+ }
718
+ orchestrationExecutor.startOrchestration(orchestrationId).then((result) => {
719
+ if (result.error) {
720
+ res.writeHead(400, { 'Content-Type': 'application/json' });
721
+ res.end(JSON.stringify({ ok: false, error: result.error }));
722
+ } else {
723
+ res.writeHead(200, { 'Content-Type': 'application/json' });
724
+ res.end(JSON.stringify({ ok: true, ...result }));
725
+ }
726
+ }).catch((e) => {
727
+ res.writeHead(500, { 'Content-Type': 'application/json' });
728
+ res.end(JSON.stringify({ ok: false, error: e.message }));
729
+ });
730
+ },
731
+
732
+ handleStopOrchestration(req, res, orchestrationId) {
733
+ if (!orchestrationExecutor) {
734
+ res.writeHead(501, { 'Content-Type': 'application/json' });
735
+ res.end(JSON.stringify({ ok: false, error: 'Orchestration executor not available' }));
736
+ return;
737
+ }
738
+ orchestrationExecutor.stopOrchestration(orchestrationId).then((result) => {
739
+ if (result.error) {
740
+ res.writeHead(400, { 'Content-Type': 'application/json' });
741
+ res.end(JSON.stringify({ ok: false, error: result.error }));
742
+ } else {
743
+ res.writeHead(200, { 'Content-Type': 'application/json' });
744
+ res.end(JSON.stringify({ ok: true, ...result }));
745
+ }
746
+ }).catch((e) => {
747
+ res.writeHead(500, { 'Content-Type': 'application/json' });
748
+ res.end(JSON.stringify({ ok: false, error: e.message }));
749
+ });
750
+ },
751
+
752
+ // Agent routes
753
+ handleAddAgent(req, res, orchestrationId) {
754
+ if (!orchestrationData) {
755
+ res.writeHead(501, { 'Content-Type': 'application/json' });
756
+ res.end(JSON.stringify({ ok: false, error: 'Orchestration not available' }));
757
+ return;
758
+ }
759
+ let body = '';
760
+ req.on('data', chunk => body += chunk);
761
+ req.on('end', () => {
762
+ try {
763
+ const agentConfig = JSON.parse(body);
764
+ const result = orchestrationData.addAgent(orchestrationId, agentConfig);
765
+ if (result.error) {
766
+ res.writeHead(400, { 'Content-Type': 'application/json' });
767
+ res.end(JSON.stringify({ ok: false, error: result.error }));
768
+ } else {
769
+ res.writeHead(201, { 'Content-Type': 'application/json' });
770
+ res.end(JSON.stringify({ ok: true, agent: result.agent }));
771
+ }
772
+ } catch (e) {
773
+ res.writeHead(400, { 'Content-Type': 'application/json' });
774
+ res.end(JSON.stringify({ ok: false, error: e.message }));
775
+ }
776
+ });
777
+ },
778
+
779
+ handleUpdateAgent(req, res, orchestrationId, agentId) {
780
+ if (!orchestrationData) {
781
+ res.writeHead(501, { 'Content-Type': 'application/json' });
782
+ res.end(JSON.stringify({ ok: false, error: 'Orchestration not available' }));
783
+ return;
784
+ }
785
+ let body = '';
786
+ req.on('data', chunk => body += chunk);
787
+ req.on('end', () => {
788
+ try {
789
+ const updates = JSON.parse(body);
790
+ const result = orchestrationData.updateAgent(orchestrationId, agentId, updates);
791
+ if (result.error) {
792
+ res.writeHead(400, { 'Content-Type': 'application/json' });
793
+ res.end(JSON.stringify({ ok: false, error: result.error }));
794
+ } else {
795
+ res.writeHead(200, { 'Content-Type': 'application/json' });
796
+ res.end(JSON.stringify({ ok: true, agent: result.agent }));
797
+ }
798
+ } catch (e) {
799
+ res.writeHead(400, { 'Content-Type': 'application/json' });
800
+ res.end(JSON.stringify({ ok: false, error: e.message }));
801
+ }
802
+ });
803
+ },
804
+
805
+ handleRemoveAgent(req, res, orchestrationId, agentId) {
806
+ if (!orchestrationData) {
807
+ res.writeHead(501, { 'Content-Type': 'application/json' });
808
+ res.end(JSON.stringify({ ok: false, error: 'Orchestration not available' }));
809
+ return;
810
+ }
811
+ const result = orchestrationData.removeAgent(orchestrationId, agentId);
812
+ if (result.error) {
813
+ res.writeHead(400, { 'Content-Type': 'application/json' });
814
+ res.end(JSON.stringify({ ok: false, error: result.error }));
815
+ } else {
816
+ res.writeHead(200, { 'Content-Type': 'application/json' });
817
+ res.end(JSON.stringify({ ok: true }));
818
+ }
819
+ },
820
+
821
+ handleSpawnAgent(req, res, orchestrationId, agentId) {
822
+ if (!orchestrationExecutor) {
823
+ res.writeHead(501, { 'Content-Type': 'application/json' });
824
+ res.end(JSON.stringify({ ok: false, error: 'Orchestration executor not available' }));
825
+ return;
826
+ }
827
+ orchestrationExecutor.spawnAgent(orchestrationId, agentId).then((result) => {
828
+ if (result.error) {
829
+ res.writeHead(400, { 'Content-Type': 'application/json' });
830
+ res.end(JSON.stringify({ ok: false, error: result.error }));
831
+ } else {
832
+ res.writeHead(200, { 'Content-Type': 'application/json' });
833
+ res.end(JSON.stringify({ ok: true, ...result }));
834
+ }
835
+ }).catch((e) => {
836
+ res.writeHead(500, { 'Content-Type': 'application/json' });
837
+ res.end(JSON.stringify({ ok: false, error: e.message }));
838
+ });
839
+ },
840
+
841
+ handleKillAgent(req, res, orchestrationId, agentId) {
842
+ if (!orchestrationExecutor) {
843
+ res.writeHead(501, { 'Content-Type': 'application/json' });
844
+ res.end(JSON.stringify({ ok: false, error: 'Orchestration executor not available' }));
845
+ return;
846
+ }
847
+ orchestrationExecutor.killAgent(orchestrationId, agentId).then((result) => {
848
+ if (result.error) {
849
+ res.writeHead(400, { 'Content-Type': 'application/json' });
850
+ res.end(JSON.stringify({ ok: false, error: result.error }));
851
+ } else {
852
+ res.writeHead(200, { 'Content-Type': 'application/json' });
853
+ res.end(JSON.stringify({ ok: true }));
854
+ }
855
+ }).catch((e) => {
856
+ res.writeHead(500, { 'Content-Type': 'application/json' });
857
+ res.end(JSON.stringify({ ok: false, error: e.message }));
858
+ });
859
+ },
860
+
861
+ handleSendPromptToAgent(req, res, orchestrationId, agentId) {
862
+ if (!orchestrationExecutor) {
863
+ res.writeHead(501, { 'Content-Type': 'application/json' });
864
+ res.end(JSON.stringify({ ok: false, error: 'Orchestration executor not available' }));
865
+ return;
866
+ }
867
+ let body = '';
868
+ req.on('data', chunk => body += chunk);
869
+ req.on('end', async () => {
870
+ try {
871
+ const { prompt } = JSON.parse(body);
872
+ if (!prompt) {
873
+ res.writeHead(400, { 'Content-Type': 'application/json' });
874
+ res.end(JSON.stringify({ ok: false, error: 'Prompt is required' }));
875
+ return;
876
+ }
877
+ const result = await orchestrationExecutor.sendPromptToAgent(orchestrationId, agentId, prompt);
878
+ if (result.error) {
879
+ res.writeHead(400, { 'Content-Type': 'application/json' });
880
+ res.end(JSON.stringify({ ok: false, error: result.error }));
881
+ } else {
882
+ res.writeHead(200, { 'Content-Type': 'application/json' });
883
+ res.end(JSON.stringify({ ok: true }));
884
+ }
885
+ } catch (e) {
886
+ res.writeHead(500, { 'Content-Type': 'application/json' });
887
+ res.end(JSON.stringify({ ok: false, error: e.message }));
888
+ }
889
+ });
890
+ },
891
+
892
+ handleGetAgentStatus(req, res, orchestrationId, agentId) {
893
+ if (!orchestrationExecutor) {
894
+ res.writeHead(501, { 'Content-Type': 'application/json' });
895
+ res.end(JSON.stringify({ ok: false, error: 'Orchestration executor not available' }));
896
+ return;
897
+ }
898
+ orchestrationExecutor.getAgentTmuxStatus(agentId).then((status) => {
899
+ res.writeHead(200, { 'Content-Type': 'application/json' });
900
+ res.end(JSON.stringify({ ok: true, ...status }));
901
+ }).catch((e) => {
902
+ res.writeHead(500, { 'Content-Type': 'application/json' });
903
+ res.end(JSON.stringify({ ok: false, error: e.message }));
904
+ });
905
+ },
906
+
907
+ handleAddAgentDependency(req, res, orchestrationId, agentId) {
908
+ if (!orchestrationData) {
909
+ res.writeHead(501, { 'Content-Type': 'application/json' });
910
+ res.end(JSON.stringify({ ok: false, error: 'Orchestration not available' }));
911
+ return;
912
+ }
913
+ let body = '';
914
+ req.on('data', chunk => body += chunk);
915
+ req.on('end', () => {
916
+ try {
917
+ const { dependsOnAgentId } = JSON.parse(body);
918
+ if (!dependsOnAgentId) {
919
+ res.writeHead(400, { 'Content-Type': 'application/json' });
920
+ res.end(JSON.stringify({ ok: false, error: 'dependsOnAgentId is required' }));
921
+ return;
922
+ }
923
+ const result = orchestrationData.addAgentDependency(orchestrationId, agentId, dependsOnAgentId);
924
+ if (result.error) {
925
+ res.writeHead(400, { 'Content-Type': 'application/json' });
926
+ res.end(JSON.stringify({ ok: false, error: result.error }));
927
+ } else {
928
+ res.writeHead(200, { 'Content-Type': 'application/json' });
929
+ res.end(JSON.stringify({ ok: true }));
930
+ }
931
+ } catch (e) {
932
+ res.writeHead(400, { 'Content-Type': 'application/json' });
933
+ res.end(JSON.stringify({ ok: false, error: e.message }));
934
+ }
935
+ });
936
+ },
937
+
938
+ handleRemoveAgentDependency(req, res, orchestrationId, agentId, dependsOnAgentId) {
939
+ if (!orchestrationData) {
940
+ res.writeHead(501, { 'Content-Type': 'application/json' });
941
+ res.end(JSON.stringify({ ok: false, error: 'Orchestration not available' }));
942
+ return;
943
+ }
944
+ const result = orchestrationData.removeAgentDependency(orchestrationId, agentId, dependsOnAgentId);
945
+ if (result.error) {
946
+ res.writeHead(400, { 'Content-Type': 'application/json' });
947
+ res.end(JSON.stringify({ ok: false, error: result.error }));
948
+ } else {
949
+ res.writeHead(200, { 'Content-Type': 'application/json' });
950
+ res.end(JSON.stringify({ ok: true }));
951
+ }
952
+ },
953
+
954
+ // Template routes
955
+ handleGetTemplates(req, res) {
956
+ if (!orchestrationData) {
957
+ res.writeHead(501, { 'Content-Type': 'application/json' });
958
+ res.end(JSON.stringify({ ok: false, error: 'Orchestration not available' }));
959
+ return;
960
+ }
961
+ const templates = orchestrationData.getTemplates();
962
+ res.writeHead(200, { 'Content-Type': 'application/json' });
963
+ res.end(JSON.stringify({ ok: true, templates }));
964
+ },
965
+
966
+ handleGetTemplate(req, res, templateId) {
967
+ if (!orchestrationData) {
968
+ res.writeHead(501, { 'Content-Type': 'application/json' });
969
+ res.end(JSON.stringify({ ok: false, error: 'Orchestration not available' }));
970
+ return;
971
+ }
972
+ const template = orchestrationData.getTemplate(templateId);
973
+ if (!template) {
974
+ res.writeHead(404, { 'Content-Type': 'application/json' });
975
+ res.end(JSON.stringify({ ok: false, error: 'Template not found' }));
976
+ return;
977
+ }
978
+ res.writeHead(200, { 'Content-Type': 'application/json' });
979
+ res.end(JSON.stringify({ ok: true, template }));
980
+ },
981
+
982
+ handleCreateFromTemplate(req, res, templateId) {
983
+ if (!orchestrationData) {
984
+ res.writeHead(501, { 'Content-Type': 'application/json' });
985
+ res.end(JSON.stringify({ ok: false, error: 'Orchestration not available' }));
986
+ return;
987
+ }
988
+ let body = '';
989
+ req.on('data', chunk => body += chunk);
990
+ req.on('end', () => {
991
+ try {
992
+ const options = body ? JSON.parse(body) : {};
993
+ const result = orchestrationData.createFromTemplate(templateId, options);
994
+ if (result.error) {
995
+ res.writeHead(400, { 'Content-Type': 'application/json' });
996
+ res.end(JSON.stringify({ ok: false, error: result.error }));
997
+ } else {
998
+ res.writeHead(201, { 'Content-Type': 'application/json' });
999
+ res.end(JSON.stringify({ ok: true, orchestration: result.orchestration }));
1000
+ }
1001
+ } catch (e) {
1002
+ res.writeHead(400, { 'Content-Type': 'application/json' });
1003
+ res.end(JSON.stringify({ ok: false, error: e.message }));
1004
+ }
1005
+ });
1006
+ }
1007
+ };
1008
+ }
1009
+
1010
+ module.exports = { createRoutes };