claudehq 1.0.0 → 1.0.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.
@@ -0,0 +1,611 @@
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
+ // Utils
56
+ sendToTmux
57
+ } = deps;
58
+
59
+ return {
60
+ // =========================================================================
61
+ // SSE Events
62
+ // =========================================================================
63
+ handleSSE(req, res) {
64
+ res.writeHead(200, {
65
+ 'Content-Type': 'text/event-stream',
66
+ 'Cache-Control': 'no-cache',
67
+ 'Connection': 'keep-alive',
68
+ 'Access-Control-Allow-Origin': '*'
69
+ });
70
+
71
+ res.write('data: {"type":"connected"}\n\n');
72
+
73
+ const recentEvents = claudeEvents.slice(-50);
74
+ if (recentEvents.length > 0) {
75
+ res.write(`data: ${JSON.stringify({ type: 'history', events: recentEvents })}\n\n`);
76
+ }
77
+
78
+ res.write(`data: ${JSON.stringify({ type: 'sessions', sessions: getManagedSessions() })}\n\n`);
79
+
80
+ sseClients.add(res);
81
+ req.on('close', () => sseClients.delete(res));
82
+ },
83
+
84
+ // =========================================================================
85
+ // Health
86
+ // =========================================================================
87
+ handleHealth(req, res) {
88
+ res.writeHead(200, { 'Content-Type': 'application/json' });
89
+ res.end(JSON.stringify({ ok: true, version: '1.0.0' }));
90
+ },
91
+
92
+ // =========================================================================
93
+ // Claude Events
94
+ // =========================================================================
95
+ handlePostClaudeEvent(req, res) {
96
+ let body = '';
97
+ req.on('data', chunk => body += chunk);
98
+ req.on('end', () => {
99
+ try {
100
+ const event = JSON.parse(body);
101
+ addClaudeEvent(event);
102
+ res.writeHead(200, { 'Content-Type': 'application/json' });
103
+ res.end(JSON.stringify({ ok: true }));
104
+ } catch (e) {
105
+ res.writeHead(400, { 'Content-Type': 'application/json' });
106
+ res.end(JSON.stringify({ ok: false, error: 'Invalid JSON' }));
107
+ }
108
+ });
109
+ },
110
+
111
+ handleGetClaudeEvents(req, res, url) {
112
+ const limit = parseInt(url.searchParams.get('limit') || '500');
113
+ const sessionId = url.searchParams.get('sessionId');
114
+
115
+ let filtered = claudeEvents;
116
+ if (sessionId) {
117
+ filtered = claudeEvents.filter(e => e.sessionId === sessionId);
118
+ }
119
+
120
+ res.writeHead(200, { 'Content-Type': 'application/json' });
121
+ res.end(JSON.stringify({
122
+ ok: true,
123
+ events: filtered.slice(-limit),
124
+ total: filtered.length
125
+ }));
126
+ },
127
+
128
+ handleGetClaudeEventStats(req, res) {
129
+ const toolCounts = {};
130
+ const toolDurations = {};
131
+ const sessionActivity = {};
132
+
133
+ for (const event of claudeEvents) {
134
+ sessionActivity[event.sessionId] = (sessionActivity[event.sessionId] || 0) + 1;
135
+
136
+ if (event.type === 'post_tool_use') {
137
+ toolCounts[event.tool] = (toolCounts[event.tool] || 0) + 1;
138
+ if (event.duration !== undefined) {
139
+ if (!toolDurations[event.tool]) toolDurations[event.tool] = [];
140
+ toolDurations[event.tool].push(event.duration);
141
+ }
142
+ }
143
+ }
144
+
145
+ const avgDurations = {};
146
+ for (const [tool, durations] of Object.entries(toolDurations)) {
147
+ avgDurations[tool] = Math.round(
148
+ durations.reduce((a, b) => a + b, 0) / durations.length
149
+ );
150
+ }
151
+
152
+ res.writeHead(200, { 'Content-Type': 'application/json' });
153
+ res.end(JSON.stringify({
154
+ totalEvents: claudeEvents.length,
155
+ toolCounts,
156
+ avgDurations,
157
+ sessionActivity
158
+ }));
159
+ },
160
+
161
+ // =========================================================================
162
+ // Tasks
163
+ // =========================================================================
164
+ handleGetTasks(req, res) {
165
+ res.writeHead(200, { 'Content-Type': 'application/json' });
166
+ res.end(JSON.stringify(loadAllTasks()));
167
+ },
168
+
169
+ handleBulkUpdateTasks(req, res) {
170
+ let body = '';
171
+ req.on('data', chunk => body += chunk);
172
+ req.on('end', () => {
173
+ try {
174
+ const { sessionId, taskIds, updates } = JSON.parse(body);
175
+ if (!sessionId || !taskIds || !Array.isArray(taskIds) || !updates) {
176
+ res.writeHead(400, { 'Content-Type': 'application/json' });
177
+ res.end(JSON.stringify({ ok: false, error: 'Missing sessionId, taskIds, or updates' }));
178
+ return;
179
+ }
180
+
181
+ const results = [];
182
+ for (const taskId of taskIds) {
183
+ const result = updateTask(sessionId, taskId, updates);
184
+ results.push({ taskId, ...result });
185
+ }
186
+
187
+ const failedCount = results.filter(r => r.error).length;
188
+ res.writeHead(200, { 'Content-Type': 'application/json' });
189
+ res.end(JSON.stringify({
190
+ ok: failedCount === 0,
191
+ updated: results.filter(r => r.success).length,
192
+ failed: failedCount,
193
+ results
194
+ }));
195
+ } catch (e) {
196
+ res.writeHead(400, { 'Content-Type': 'application/json' });
197
+ res.end(JSON.stringify({ ok: false, error: e.message }));
198
+ }
199
+ });
200
+ },
201
+
202
+ handleUpdateTask(req, res, sessionId, taskId) {
203
+ let body = '';
204
+ req.on('data', chunk => body += chunk);
205
+ req.on('end', () => {
206
+ try {
207
+ const updates = JSON.parse(body);
208
+ const result = updateTask(sessionId, taskId, updates);
209
+ res.writeHead(result.error ? 400 : 200, { 'Content-Type': 'application/json' });
210
+ res.end(JSON.stringify(result));
211
+ } catch (e) {
212
+ res.writeHead(400, { 'Content-Type': 'application/json' });
213
+ res.end(JSON.stringify({ error: e.message }));
214
+ }
215
+ });
216
+ },
217
+
218
+ handleCreateTask(req, res, sessionId) {
219
+ let body = '';
220
+ req.on('data', chunk => body += chunk);
221
+ req.on('end', () => {
222
+ try {
223
+ const taskData = JSON.parse(body);
224
+ const result = createTask(sessionId, taskData);
225
+ res.writeHead(result.error ? 400 : 201, { 'Content-Type': 'application/json' });
226
+ res.end(JSON.stringify(result));
227
+ } catch (e) {
228
+ res.writeHead(400, { 'Content-Type': 'application/json' });
229
+ res.end(JSON.stringify({ error: e.message }));
230
+ }
231
+ });
232
+ },
233
+
234
+ // =========================================================================
235
+ // Todos
236
+ // =========================================================================
237
+ handleGetAllTodos(req, res) {
238
+ res.writeHead(200, { 'Content-Type': 'application/json' });
239
+ res.end(JSON.stringify({ ok: true, todosBySession: loadAllTodos() }));
240
+ },
241
+
242
+ handleBulkUpdateTodos(req, res) {
243
+ let body = '';
244
+ req.on('data', chunk => body += chunk);
245
+ req.on('end', () => {
246
+ try {
247
+ const { sessionId, todoIndexes, updates } = JSON.parse(body);
248
+ if (!sessionId || !todoIndexes || !Array.isArray(todoIndexes) || !updates) {
249
+ res.writeHead(400, { 'Content-Type': 'application/json' });
250
+ res.end(JSON.stringify({ ok: false, error: 'Missing sessionId, todoIndexes, or updates' }));
251
+ return;
252
+ }
253
+
254
+ const results = [];
255
+ for (const todoIndex of todoIndexes) {
256
+ const result = updateTodo(sessionId, todoIndex, updates);
257
+ results.push({ todoIndex, ...result });
258
+ }
259
+
260
+ const failedCount = results.filter(r => r.error).length;
261
+ res.writeHead(200, { 'Content-Type': 'application/json' });
262
+ res.end(JSON.stringify({
263
+ ok: failedCount === 0,
264
+ updated: results.filter(r => r.success).length,
265
+ failed: failedCount,
266
+ results
267
+ }));
268
+ } catch (e) {
269
+ res.writeHead(400, { 'Content-Type': 'application/json' });
270
+ res.end(JSON.stringify({ ok: false, error: e.message }));
271
+ }
272
+ });
273
+ },
274
+
275
+ handleGetTodosForSession(req, res, sessionId) {
276
+ const result = getTodosForSession(sessionId);
277
+ res.writeHead(200, { 'Content-Type': 'application/json' });
278
+ res.end(JSON.stringify({ ok: true, ...result }));
279
+ },
280
+
281
+ handleCreateTodo(req, res, sessionId) {
282
+ let body = '';
283
+ req.on('data', chunk => body += chunk);
284
+ req.on('end', () => {
285
+ try {
286
+ const todoData = JSON.parse(body);
287
+ const result = createTodo(sessionId, todoData);
288
+ if (result.success) {
289
+ res.writeHead(201, { 'Content-Type': 'application/json' });
290
+ res.end(JSON.stringify({ ok: true, todo: result.todo }));
291
+ } else {
292
+ res.writeHead(400, { 'Content-Type': 'application/json' });
293
+ res.end(JSON.stringify({ ok: false, error: result.error }));
294
+ }
295
+ } catch (e) {
296
+ res.writeHead(400, { 'Content-Type': 'application/json' });
297
+ res.end(JSON.stringify({ ok: false, error: e.message }));
298
+ }
299
+ });
300
+ },
301
+
302
+ handleUpdateTodo(req, res, sessionId, todoIndex) {
303
+ let body = '';
304
+ req.on('data', chunk => body += chunk);
305
+ req.on('end', () => {
306
+ try {
307
+ const updates = JSON.parse(body);
308
+ const result = updateTodo(sessionId, parseInt(todoIndex), updates);
309
+ if (result.success) {
310
+ res.writeHead(200, { 'Content-Type': 'application/json' });
311
+ res.end(JSON.stringify({ ok: true, todo: result.todo }));
312
+ } else {
313
+ res.writeHead(400, { 'Content-Type': 'application/json' });
314
+ res.end(JSON.stringify({ ok: false, error: result.error }));
315
+ }
316
+ } catch (e) {
317
+ res.writeHead(400, { 'Content-Type': 'application/json' });
318
+ res.end(JSON.stringify({ ok: false, error: e.message }));
319
+ }
320
+ });
321
+ },
322
+
323
+ // =========================================================================
324
+ // Plans
325
+ // =========================================================================
326
+ handleGetAllPlans(req, res) {
327
+ res.writeHead(200, { 'Content-Type': 'application/json' });
328
+ res.end(JSON.stringify({ ok: true, plans: loadAllPlans() }));
329
+ },
330
+
331
+ handleGetPlansForSession(req, res, sessionId) {
332
+ const plans = getPlansForSession(sessionId);
333
+ res.writeHead(200, { 'Content-Type': 'application/json' });
334
+ res.end(JSON.stringify({ ok: true, sessionId, plans }));
335
+ },
336
+
337
+ handleGetPlan(req, res, slug) {
338
+ const result = getPlan(slug);
339
+ if (result.error) {
340
+ res.writeHead(404, { 'Content-Type': 'application/json' });
341
+ res.end(JSON.stringify({ ok: false, error: result.error }));
342
+ } else {
343
+ res.writeHead(200, { 'Content-Type': 'application/json' });
344
+ res.end(JSON.stringify({ ok: true, plan: result }));
345
+ }
346
+ },
347
+
348
+ handleUpdatePlan(req, res, slug) {
349
+ let body = '';
350
+ req.on('data', chunk => body += chunk);
351
+ req.on('end', () => {
352
+ try {
353
+ const { content } = JSON.parse(body);
354
+ const result = updatePlan(slug, content);
355
+ if (result.success) {
356
+ res.writeHead(200, { 'Content-Type': 'application/json' });
357
+ res.end(JSON.stringify({ ok: true, slug: result.slug }));
358
+ } else {
359
+ res.writeHead(400, { 'Content-Type': 'application/json' });
360
+ res.end(JSON.stringify({ ok: false, error: result.error }));
361
+ }
362
+ } catch (e) {
363
+ res.writeHead(400, { 'Content-Type': 'application/json' });
364
+ res.end(JSON.stringify({ ok: false, error: e.message }));
365
+ }
366
+ });
367
+ },
368
+
369
+ // =========================================================================
370
+ // Managed Sessions
371
+ // =========================================================================
372
+ handleGetManagedSessions(req, res) {
373
+ res.writeHead(200, { 'Content-Type': 'application/json' });
374
+ res.end(JSON.stringify({ ok: true, sessions: getManagedSessions() }));
375
+ },
376
+
377
+ handleCreateManagedSession(req, res) {
378
+ let body = '';
379
+ req.on('data', chunk => body += chunk);
380
+ req.on('end', async () => {
381
+ try {
382
+ const { name, cwd, flags } = JSON.parse(body);
383
+ const session = await createManagedSession({ name, cwd, flags });
384
+ res.writeHead(201, { 'Content-Type': 'application/json' });
385
+ res.end(JSON.stringify({ ok: true, session }));
386
+ } catch (e) {
387
+ res.writeHead(400, { 'Content-Type': 'application/json' });
388
+ res.end(JSON.stringify({ ok: false, error: e.message }));
389
+ }
390
+ });
391
+ },
392
+
393
+ handleRefreshSessions(req, res) {
394
+ checkSessionHealth();
395
+ res.writeHead(200, { 'Content-Type': 'application/json' });
396
+ res.end(JSON.stringify({ ok: true, sessions: getManagedSessions() }));
397
+ },
398
+
399
+ handleGetManagedSession(req, res, id) {
400
+ const session = getManagedSession(id);
401
+ if (session) {
402
+ res.writeHead(200, { 'Content-Type': 'application/json' });
403
+ res.end(JSON.stringify({ ok: true, session }));
404
+ } else {
405
+ res.writeHead(404, { 'Content-Type': 'application/json' });
406
+ res.end(JSON.stringify({ ok: false, error: 'Session not found' }));
407
+ }
408
+ },
409
+
410
+ handleUpdateManagedSession(req, res, id) {
411
+ let body = '';
412
+ req.on('data', chunk => body += chunk);
413
+ req.on('end', () => {
414
+ try {
415
+ const updates = JSON.parse(body);
416
+ const session = updateManagedSession(id, updates);
417
+ if (session) {
418
+ res.writeHead(200, { 'Content-Type': 'application/json' });
419
+ res.end(JSON.stringify({ ok: true, session }));
420
+ } else {
421
+ res.writeHead(404, { 'Content-Type': 'application/json' });
422
+ res.end(JSON.stringify({ ok: false, error: 'Session not found' }));
423
+ }
424
+ } catch (e) {
425
+ res.writeHead(400, { 'Content-Type': 'application/json' });
426
+ res.end(JSON.stringify({ ok: false, error: e.message }));
427
+ }
428
+ });
429
+ },
430
+
431
+ handleHideSession(req, res, id) {
432
+ const session = managedSessions.get(id);
433
+ if (session) {
434
+ const idToHide = session.claudeSessionId || id;
435
+ hideSession(idToHide);
436
+ broadcastManagedSessions();
437
+ res.writeHead(200, { 'Content-Type': 'application/json' });
438
+ res.end(JSON.stringify({ ok: true }));
439
+ } else {
440
+ res.writeHead(404, { 'Content-Type': 'application/json' });
441
+ res.end(JSON.stringify({ ok: false, error: 'Session not found' }));
442
+ }
443
+ },
444
+
445
+ handleUnhideSession(req, res, id) {
446
+ unhideSession(id);
447
+ broadcastManagedSessions();
448
+ res.writeHead(200, { 'Content-Type': 'application/json' });
449
+ res.end(JSON.stringify({ ok: true }));
450
+ },
451
+
452
+ handleGetHiddenSessions(req, res) {
453
+ const hiddenIds = loadHiddenSessions();
454
+ const customNames = loadCustomNames();
455
+ const sessions = hiddenIds.map(id => {
456
+ let name = customNames[id] || id.substring(0, 8);
457
+ for (const [managedId, session] of managedSessions) {
458
+ if (session.claudeSessionId === id || managedId === id) {
459
+ name = session.name || name;
460
+ break;
461
+ }
462
+ }
463
+ return { id, name };
464
+ });
465
+ res.writeHead(200, { 'Content-Type': 'application/json' });
466
+ res.end(JSON.stringify({ ok: true, sessions }));
467
+ },
468
+
469
+ handleDeleteSession(req, res, id) {
470
+ const session = managedSessions.get(id);
471
+ if (session) {
472
+ const idToHide = session.claudeSessionId || id;
473
+ hideSession(idToHide);
474
+ broadcastManagedSessions();
475
+ res.writeHead(200, { 'Content-Type': 'application/json' });
476
+ res.end(JSON.stringify({ ok: true }));
477
+ } else {
478
+ res.writeHead(404, { 'Content-Type': 'application/json' });
479
+ res.end(JSON.stringify({ ok: false, error: 'Session not found' }));
480
+ }
481
+ },
482
+
483
+ handlePermanentDeleteSession(req, res, id) {
484
+ const result = permanentDeleteSession(id);
485
+ if (result.success) {
486
+ res.writeHead(200, { 'Content-Type': 'application/json' });
487
+ res.end(JSON.stringify({ ok: true }));
488
+ } else {
489
+ res.writeHead(404, { 'Content-Type': 'application/json' });
490
+ res.end(JSON.stringify({ ok: false, error: result.error }));
491
+ }
492
+ },
493
+
494
+ handleSendPromptToSession(req, res, id) {
495
+ let body = '';
496
+ req.on('data', chunk => body += chunk);
497
+ req.on('end', async () => {
498
+ try {
499
+ const { prompt } = JSON.parse(body);
500
+ if (!prompt) {
501
+ res.writeHead(400, { 'Content-Type': 'application/json' });
502
+ res.end(JSON.stringify({ ok: false, error: 'Prompt is required' }));
503
+ return;
504
+ }
505
+ const result = await sendPromptToManagedSession(id, prompt);
506
+ res.writeHead(result.ok ? 200 : 404, { 'Content-Type': 'application/json' });
507
+ res.end(JSON.stringify(result));
508
+ } catch (e) {
509
+ res.writeHead(500, { 'Content-Type': 'application/json' });
510
+ res.end(JSON.stringify({ ok: false, error: e.message }));
511
+ }
512
+ });
513
+ },
514
+
515
+ handleRestartSession(req, res, id) {
516
+ restartManagedSession(id).then((session) => {
517
+ if (session) {
518
+ res.writeHead(200, { 'Content-Type': 'application/json' });
519
+ res.end(JSON.stringify({ ok: true, session }));
520
+ } else {
521
+ res.writeHead(404, { 'Content-Type': 'application/json' });
522
+ res.end(JSON.stringify({ ok: false, error: 'Session not found' }));
523
+ }
524
+ }).catch((e) => {
525
+ res.writeHead(500, { 'Content-Type': 'application/json' });
526
+ res.end(JSON.stringify({ ok: false, error: e.message }));
527
+ });
528
+ },
529
+
530
+ handleLinkSession(req, res, id) {
531
+ let body = '';
532
+ req.on('data', chunk => body += chunk);
533
+ req.on('end', () => {
534
+ try {
535
+ const { claudeSessionId } = JSON.parse(body);
536
+ if (!claudeSessionId) {
537
+ res.writeHead(400, { 'Content-Type': 'application/json' });
538
+ res.end(JSON.stringify({ ok: false, error: 'claudeSessionId is required' }));
539
+ return;
540
+ }
541
+ const session = getManagedSession(id);
542
+ if (!session) {
543
+ res.writeHead(404, { 'Content-Type': 'application/json' });
544
+ res.end(JSON.stringify({ ok: false, error: 'Session not found' }));
545
+ return;
546
+ }
547
+ linkClaudeSession(claudeSessionId, id);
548
+ broadcastManagedSessions();
549
+ saveManagedSessions();
550
+ res.writeHead(200, { 'Content-Type': 'application/json' });
551
+ res.end(JSON.stringify({ ok: true, session: getManagedSession(id) }));
552
+ } catch (e) {
553
+ res.writeHead(400, { 'Content-Type': 'application/json' });
554
+ res.end(JSON.stringify({ ok: false, error: e.message }));
555
+ }
556
+ });
557
+ },
558
+
559
+ // =========================================================================
560
+ // Conversation
561
+ // =========================================================================
562
+ handleGetConversation(req, res, sessionId) {
563
+ const conversation = loadConversation(sessionId);
564
+ res.writeHead(200, { 'Content-Type': 'application/json' });
565
+ res.end(JSON.stringify(conversation));
566
+ },
567
+
568
+ // =========================================================================
569
+ // Prompt
570
+ // =========================================================================
571
+ handleSendPrompt(req, res) {
572
+ let body = '';
573
+ req.on('data', chunk => body += chunk);
574
+ req.on('end', async () => {
575
+ try {
576
+ const { prompt, tmuxSession } = JSON.parse(body);
577
+ if (!prompt) {
578
+ res.writeHead(400, { 'Content-Type': 'application/json' });
579
+ res.end(JSON.stringify({ ok: false, error: 'Prompt is required' }));
580
+ return;
581
+ }
582
+ const session = tmuxSession || TMUX_SESSION;
583
+ await sendToTmux(session, prompt);
584
+ res.writeHead(200, { 'Content-Type': 'application/json' });
585
+ res.end(JSON.stringify({ ok: true, session }));
586
+ } catch (e) {
587
+ res.writeHead(500, { 'Content-Type': 'application/json' });
588
+ res.end(JSON.stringify({ ok: false, error: e.message }));
589
+ }
590
+ });
591
+ },
592
+
593
+ handleRenameSession(req, res, sessionId) {
594
+ let body = '';
595
+ req.on('data', chunk => body += chunk);
596
+ req.on('end', () => {
597
+ try {
598
+ const { name } = JSON.parse(body);
599
+ const result = renameSession(sessionId, name);
600
+ res.writeHead(200, { 'Content-Type': 'application/json' });
601
+ res.end(JSON.stringify(result));
602
+ } catch (e) {
603
+ res.writeHead(400, { 'Content-Type': 'application/json' });
604
+ res.end(JSON.stringify({ error: e.message }));
605
+ }
606
+ });
607
+ }
608
+ };
609
+ }
610
+
611
+ module.exports = { createRoutes };