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.
package/lib/index.js ADDED
@@ -0,0 +1,481 @@
1
+ #!/usr/bin/env node
2
+ /**
3
+ * Claude Tasks Board - Main Entry Point
4
+ *
5
+ * This is the main entry point that initializes all modules
6
+ * and starts the HTTP server.
7
+ */
8
+
9
+ const http = require('http');
10
+ const fs = require('fs');
11
+ const path = require('path');
12
+
13
+ // Core modules
14
+ const config = require('./core/config');
15
+ const { eventBus, EventTypes } = require('./core/event-bus');
16
+ const { sseClients, addClient, removeClient, broadcastUpdate } = require('./core/sse');
17
+ const claudeEvents = require('./core/claude-events');
18
+ const { setupWatchers, setPlanCacheCallbacks } = require('./core/watchers');
19
+
20
+ // Session manager
21
+ const sessionManager = require('./sessions/manager');
22
+
23
+ // Data modules
24
+ const tasks = require('./data/tasks');
25
+ const todos = require('./data/todos');
26
+ const plans = require('./data/plans');
27
+ const conversation = require('./data/conversation');
28
+ const orchestrationData = require('./data/orchestration');
29
+
30
+ // Orchestration
31
+ const orchestrationExecutor = require('./orchestration/executor');
32
+
33
+ // Routes
34
+ const { createRoutes } = require('./routes/api');
35
+
36
+ // =============================================================================
37
+ // Module Wiring - Connect modules that need callbacks from other modules
38
+ // =============================================================================
39
+
40
+ // Set up conversation file finder for claude-events module
41
+ claudeEvents.setConversationFileCallback(conversation.findConversationFile);
42
+
43
+ // Set up session update callback for claude-events module
44
+ claudeEvents.setSessionUpdateCallback(sessionManager.updateSessionFromEvent);
45
+
46
+ // Set up metadata callback for session manager
47
+ sessionManager.setMetadataCallback(conversation.getSessionMetadata);
48
+
49
+ // Set up plan cache callbacks for watchers
50
+ setPlanCacheCallbacks(plans.buildPlanSessionCache, plans.getSessionForPlan);
51
+
52
+ // Set up tasks module callbacks
53
+ tasks.setCustomNamesCallback(sessionManager.loadCustomNames);
54
+ tasks.setMetadataCallback(conversation.getSessionMetadata);
55
+
56
+ // =============================================================================
57
+ // EventBus Handlers - Register event handlers for broadcasts and logging
58
+ // =============================================================================
59
+
60
+ function registerBroadcastHandlers() {
61
+ // Task broadcast handlers
62
+ eventBus.on(EventTypes.TASK_CREATED, (payload) =>
63
+ broadcastUpdate('task_created', payload));
64
+ eventBus.on(EventTypes.TASK_UPDATED, (payload) =>
65
+ broadcastUpdate('task_updated', payload));
66
+ eventBus.on(EventTypes.TASK_STATUS_CHANGED, (payload) =>
67
+ broadcastUpdate('task_status_changed', payload));
68
+ eventBus.on(EventTypes.SESSION_RENAMED, (payload) =>
69
+ broadcastUpdate('session_renamed', payload));
70
+ eventBus.on(EventTypes.FILE_CHANGED, () => broadcastUpdate());
71
+
72
+ // Todo broadcast handlers
73
+ eventBus.on(EventTypes.TODOS_CHANGED, (payload) =>
74
+ broadcastUpdate('todos_changed', payload));
75
+ eventBus.on(EventTypes.TODO_CREATED, (payload) =>
76
+ broadcastUpdate('todo_created', payload));
77
+ eventBus.on(EventTypes.TODO_UPDATED, (payload) =>
78
+ broadcastUpdate('todo_updated', payload));
79
+ eventBus.on(EventTypes.TODO_STATUS_CHANGED, (payload) =>
80
+ broadcastUpdate('todo_status_changed', payload));
81
+
82
+ // Plan broadcast handlers
83
+ eventBus.on(EventTypes.PLANS_CHANGED, (payload) =>
84
+ broadcastUpdate('plans_changed', payload));
85
+ eventBus.on(EventTypes.PLAN_CREATED, (payload) =>
86
+ broadcastUpdate('plan_created', payload));
87
+ eventBus.on(EventTypes.PLAN_UPDATED, (payload) =>
88
+ broadcastUpdate('plan_updated', payload));
89
+ }
90
+
91
+ function registerLoggingHandlers() {
92
+ eventBus.on(EventTypes.TASK_CREATED, (payload) => {
93
+ console.log(`[${new Date().toLocaleTimeString()}] Task created: ${payload.task?.subject || payload.taskId}`);
94
+ });
95
+
96
+ eventBus.on(EventTypes.TASK_STATUS_CHANGED, (payload) => {
97
+ console.log(`[${new Date().toLocaleTimeString()}] Task #${payload.taskId} status: ${payload.status}`);
98
+ });
99
+
100
+ eventBus.on(EventTypes.PROMPT_SENT, (payload) => {
101
+ console.log(`[${new Date().toLocaleTimeString()}] Prompt sent to ${payload.tmuxSession}`);
102
+ });
103
+
104
+ eventBus.on(EventTypes.CLAUDE_EVENT, (payload) => {
105
+ console.log(`[${new Date().toLocaleTimeString()}] Claude event: ${payload.type} (${payload.tool || payload.sessionId})`);
106
+ });
107
+ }
108
+
109
+ function registerAllHandlers() {
110
+ registerBroadcastHandlers();
111
+ registerLoggingHandlers();
112
+ }
113
+
114
+ // =============================================================================
115
+ // HTML Template - Loaded from public/index.html
116
+ // =============================================================================
117
+
118
+ let HTML = '';
119
+
120
+ function loadHTML() {
121
+ const htmlPath = path.join(__dirname, '..', 'public', 'index.html');
122
+ if (fs.existsSync(htmlPath)) {
123
+ HTML = fs.readFileSync(htmlPath, 'utf-8');
124
+ console.log(` Loaded HTML from: ${htmlPath}`);
125
+ return;
126
+ }
127
+
128
+ // Fallback HTML if public/index.html is missing
129
+ HTML = '<!DOCTYPE html><html><head><title>Claude HQ</title></head><body><h1>Claude HQ</h1><p>HTML template not found. Please check your installation.</p></body></html>';
130
+ console.log(' Warning: Using minimal fallback HTML template');
131
+ }
132
+
133
+ // =============================================================================
134
+ // Create Route Handlers
135
+ // =============================================================================
136
+
137
+ const routes = createRoutes({
138
+ // Core
139
+ sseClients,
140
+ claudeEvents: claudeEvents.claudeEvents,
141
+ addClaudeEvent: claudeEvents.addClaudeEvent,
142
+ broadcastManagedSessions: sessionManager.broadcastManagedSessions,
143
+
144
+ // Sessions
145
+ getManagedSessions: sessionManager.getManagedSessions,
146
+ getManagedSession: sessionManager.getManagedSession,
147
+ createManagedSession: sessionManager.createManagedSession,
148
+ updateManagedSession: sessionManager.updateManagedSession,
149
+ deleteManagedSession: sessionManager.deleteManagedSession,
150
+ sendPromptToManagedSession: sessionManager.sendPromptToManagedSession,
151
+ restartManagedSession: sessionManager.restartManagedSession,
152
+ linkClaudeSession: sessionManager.linkClaudeSession,
153
+ checkSessionHealth: sessionManager.checkSessionHealth,
154
+ saveManagedSessions: sessionManager.saveManagedSessions,
155
+ managedSessions: sessionManager.managedSessions,
156
+ hideSession: sessionManager.hideSession,
157
+ unhideSession: sessionManager.unhideSession,
158
+ permanentDeleteSession: sessionManager.permanentDeleteSession,
159
+ loadHiddenSessions: sessionManager.loadHiddenSessions,
160
+ loadCustomNames: sessionManager.loadCustomNames,
161
+ renameSession: sessionManager.renameSession,
162
+
163
+ // Data
164
+ loadAllTasks: tasks.loadAllTasks,
165
+ updateTask: tasks.updateTask,
166
+ createTask: tasks.createTask,
167
+ loadAllTodos: todos.loadAllTodos,
168
+ getTodosForSession: todos.getTodosForSession,
169
+ updateTodo: todos.updateTodo,
170
+ createTodo: todos.createTodo,
171
+ loadAllPlans: plans.loadAllPlans,
172
+ getPlansForSession: plans.getPlansForSession,
173
+ getPlan: plans.getPlan,
174
+ updatePlan: plans.updatePlan,
175
+ loadConversation: conversation.loadConversation,
176
+
177
+ // Orchestration
178
+ orchestrationData,
179
+ orchestrationExecutor,
180
+
181
+ // Utils
182
+ sendToTmux: sessionManager.sendToTmux
183
+ });
184
+
185
+ // =============================================================================
186
+ // HTTP Server
187
+ // =============================================================================
188
+
189
+ const server = http.createServer((req, res) => {
190
+ const url = new URL(req.url, `http://${req.headers.host}`);
191
+
192
+ // SSE endpoint
193
+ if (url.pathname === '/events') {
194
+ return routes.handleSSE(req, res);
195
+ }
196
+
197
+ // Health check
198
+ if (url.pathname === '/api/health' && req.method === 'GET') {
199
+ return routes.handleHealth(req, res);
200
+ }
201
+
202
+ // Claude Events
203
+ if (url.pathname === '/api/claude-events' && req.method === 'POST') {
204
+ return routes.handlePostClaudeEvent(req, res);
205
+ }
206
+ if (url.pathname === '/api/claude-events' && req.method === 'GET') {
207
+ return routes.handleGetClaudeEvents(req, res, url);
208
+ }
209
+ if (url.pathname === '/api/claude-events/stats' && req.method === 'GET') {
210
+ return routes.handleGetClaudeEventStats(req, res);
211
+ }
212
+
213
+ // Tasks
214
+ if (url.pathname === '/api/tasks' && req.method === 'GET') {
215
+ return routes.handleGetTasks(req, res);
216
+ }
217
+ if (url.pathname === '/api/tasks/bulk-update' && req.method === 'POST') {
218
+ return routes.handleBulkUpdateTasks(req, res);
219
+ }
220
+
221
+ // Todos
222
+ if (url.pathname === '/api/todos' && req.method === 'GET') {
223
+ return routes.handleGetAllTodos(req, res);
224
+ }
225
+ if (url.pathname === '/api/todos/bulk-update' && req.method === 'POST') {
226
+ return routes.handleBulkUpdateTodos(req, res);
227
+ }
228
+
229
+ const getTodosMatch = url.pathname.match(/^\/api\/todos\/([a-f0-9-]+)$/);
230
+ if (getTodosMatch && req.method === 'GET') {
231
+ return routes.handleGetTodosForSession(req, res, getTodosMatch[1]);
232
+ }
233
+ if (getTodosMatch && req.method === 'POST') {
234
+ return routes.handleCreateTodo(req, res, getTodosMatch[1]);
235
+ }
236
+
237
+ const updateTodoMatch = url.pathname.match(/^\/api\/todos\/([a-f0-9-]+)\/(\d+)$/);
238
+ if (updateTodoMatch && req.method === 'PATCH') {
239
+ return routes.handleUpdateTodo(req, res, updateTodoMatch[1], updateTodoMatch[2]);
240
+ }
241
+
242
+ // Plans
243
+ if (url.pathname === '/api/plans' && req.method === 'GET') {
244
+ return routes.handleGetAllPlans(req, res);
245
+ }
246
+
247
+ const getPlansForSessionMatch = url.pathname.match(/^\/api\/plans\/session\/([a-f0-9-]+)$/);
248
+ if (getPlansForSessionMatch && req.method === 'GET') {
249
+ return routes.handleGetPlansForSession(req, res, getPlansForSessionMatch[1]);
250
+ }
251
+
252
+ const getPlanMatch = url.pathname.match(/^\/api\/plans\/([a-z0-9-]+)$/);
253
+ if (getPlanMatch && req.method === 'GET') {
254
+ return routes.handleGetPlan(req, res, getPlanMatch[1]);
255
+ }
256
+ if (getPlanMatch && req.method === 'PUT') {
257
+ return routes.handleUpdatePlan(req, res, getPlanMatch[1]);
258
+ }
259
+
260
+ // Managed Sessions
261
+ if (url.pathname === '/api/managed-sessions' && req.method === 'GET') {
262
+ return routes.handleGetManagedSessions(req, res);
263
+ }
264
+ if (url.pathname === '/api/managed-sessions' && req.method === 'POST') {
265
+ return routes.handleCreateManagedSession(req, res);
266
+ }
267
+ if (url.pathname === '/api/managed-sessions/refresh' && req.method === 'POST') {
268
+ return routes.handleRefreshSessions(req, res);
269
+ }
270
+
271
+ const getManagedMatch = url.pathname.match(/^\/api\/managed-sessions\/([^/]+)$/);
272
+ if (getManagedMatch && req.method === 'GET') {
273
+ return routes.handleGetManagedSession(req, res, getManagedMatch[1]);
274
+ }
275
+ if (getManagedMatch && req.method === 'PATCH') {
276
+ return routes.handleUpdateManagedSession(req, res, getManagedMatch[1]);
277
+ }
278
+ if (getManagedMatch && req.method === 'DELETE') {
279
+ return routes.handleDeleteSession(req, res, getManagedMatch[1]);
280
+ }
281
+
282
+ const hideManagedMatch = url.pathname.match(/^\/api\/managed-sessions\/([^/]+)\/hide$/);
283
+ if (hideManagedMatch && req.method === 'POST') {
284
+ return routes.handleHideSession(req, res, hideManagedMatch[1]);
285
+ }
286
+
287
+ const unhideManagedMatch = url.pathname.match(/^\/api\/managed-sessions\/([^/]+)\/unhide$/);
288
+ if (unhideManagedMatch && req.method === 'POST') {
289
+ return routes.handleUnhideSession(req, res, unhideManagedMatch[1]);
290
+ }
291
+
292
+ if (url.pathname === '/api/hidden-sessions' && req.method === 'GET') {
293
+ return routes.handleGetHiddenSessions(req, res);
294
+ }
295
+
296
+ const permanentDeleteMatch = url.pathname.match(/^\/api\/managed-sessions\/([^/]+)\/permanent$/);
297
+ if (permanentDeleteMatch && req.method === 'DELETE') {
298
+ return routes.handlePermanentDeleteSession(req, res, permanentDeleteMatch[1]);
299
+ }
300
+
301
+ const promptManagedMatch = url.pathname.match(/^\/api\/managed-sessions\/([^/]+)\/prompt$/);
302
+ if (promptManagedMatch && req.method === 'POST') {
303
+ return routes.handleSendPromptToSession(req, res, promptManagedMatch[1]);
304
+ }
305
+
306
+ const restartManagedMatch = url.pathname.match(/^\/api\/managed-sessions\/([^/]+)\/restart$/);
307
+ if (restartManagedMatch && req.method === 'POST') {
308
+ return routes.handleRestartSession(req, res, restartManagedMatch[1]);
309
+ }
310
+
311
+ const linkManagedMatch = url.pathname.match(/^\/api\/managed-sessions\/([^/]+)\/link$/);
312
+ if (linkManagedMatch && req.method === 'POST') {
313
+ return routes.handleLinkSession(req, res, linkManagedMatch[1]);
314
+ }
315
+
316
+ // Conversation
317
+ const convMatch = url.pathname.match(/^\/api\/conversation\/([^/]+)$/);
318
+ if (convMatch && req.method === 'GET') {
319
+ return routes.handleGetConversation(req, res, convMatch[1]);
320
+ }
321
+
322
+ // Prompt
323
+ if (url.pathname === '/api/prompt' && req.method === 'POST') {
324
+ return routes.handleSendPrompt(req, res);
325
+ }
326
+
327
+ // Session rename
328
+ const renameMatch = url.pathname.match(/^\/api\/sessions\/([^/]+)\/rename$/);
329
+ if (renameMatch && req.method === 'POST') {
330
+ return routes.handleRenameSession(req, res, renameMatch[1]);
331
+ }
332
+
333
+ // Orchestration routes
334
+ if (url.pathname === '/api/orchestrations' && req.method === 'GET') {
335
+ return routes.handleGetOrchestrations(req, res);
336
+ }
337
+ if (url.pathname === '/api/orchestrations' && req.method === 'POST') {
338
+ return routes.handleCreateOrchestration(req, res);
339
+ }
340
+
341
+ const orchestrationMatch = url.pathname.match(/^\/api\/orchestrations\/([^/]+)$/);
342
+ if (orchestrationMatch && req.method === 'GET') {
343
+ return routes.handleGetOrchestration(req, res, orchestrationMatch[1]);
344
+ }
345
+ if (orchestrationMatch && req.method === 'PATCH') {
346
+ return routes.handleUpdateOrchestration(req, res, orchestrationMatch[1]);
347
+ }
348
+ if (orchestrationMatch && req.method === 'DELETE') {
349
+ return routes.handleDeleteOrchestration(req, res, orchestrationMatch[1]);
350
+ }
351
+
352
+ const orchestrationStartMatch = url.pathname.match(/^\/api\/orchestrations\/([^/]+)\/start$/);
353
+ if (orchestrationStartMatch && req.method === 'POST') {
354
+ return routes.handleStartOrchestration(req, res, orchestrationStartMatch[1]);
355
+ }
356
+
357
+ const orchestrationStopMatch = url.pathname.match(/^\/api\/orchestrations\/([^/]+)\/stop$/);
358
+ if (orchestrationStopMatch && req.method === 'POST') {
359
+ return routes.handleStopOrchestration(req, res, orchestrationStopMatch[1]);
360
+ }
361
+
362
+ // Agent routes
363
+ const agentsMatch = url.pathname.match(/^\/api\/orchestrations\/([^/]+)\/agents$/);
364
+ if (agentsMatch && req.method === 'POST') {
365
+ return routes.handleAddAgent(req, res, agentsMatch[1]);
366
+ }
367
+
368
+ const agentMatch = url.pathname.match(/^\/api\/orchestrations\/([^/]+)\/agents\/([^/]+)$/);
369
+ if (agentMatch && req.method === 'PATCH') {
370
+ return routes.handleUpdateAgent(req, res, agentMatch[1], agentMatch[2]);
371
+ }
372
+ if (agentMatch && req.method === 'DELETE') {
373
+ return routes.handleRemoveAgent(req, res, agentMatch[1], agentMatch[2]);
374
+ }
375
+
376
+ const agentSpawnMatch = url.pathname.match(/^\/api\/orchestrations\/([^/]+)\/agents\/([^/]+)\/spawn$/);
377
+ if (agentSpawnMatch && req.method === 'POST') {
378
+ return routes.handleSpawnAgent(req, res, agentSpawnMatch[1], agentSpawnMatch[2]);
379
+ }
380
+
381
+ const agentKillMatch = url.pathname.match(/^\/api\/orchestrations\/([^/]+)\/agents\/([^/]+)\/kill$/);
382
+ if (agentKillMatch && req.method === 'POST') {
383
+ return routes.handleKillAgent(req, res, agentKillMatch[1], agentKillMatch[2]);
384
+ }
385
+
386
+ const agentPromptMatch = url.pathname.match(/^\/api\/orchestrations\/([^/]+)\/agents\/([^/]+)\/prompt$/);
387
+ if (agentPromptMatch && req.method === 'POST') {
388
+ return routes.handleSendPromptToAgent(req, res, agentPromptMatch[1], agentPromptMatch[2]);
389
+ }
390
+
391
+ const agentStatusMatch = url.pathname.match(/^\/api\/orchestrations\/([^/]+)\/agents\/([^/]+)\/status$/);
392
+ if (agentStatusMatch && req.method === 'GET') {
393
+ return routes.handleGetAgentStatus(req, res, agentStatusMatch[1], agentStatusMatch[2]);
394
+ }
395
+
396
+ const agentDependencyMatch = url.pathname.match(/^\/api\/orchestrations\/([^/]+)\/agents\/([^/]+)\/dependencies$/);
397
+ if (agentDependencyMatch && req.method === 'POST') {
398
+ return routes.handleAddAgentDependency(req, res, agentDependencyMatch[1], agentDependencyMatch[2]);
399
+ }
400
+
401
+ const agentDependencyRemoveMatch = url.pathname.match(/^\/api\/orchestrations\/([^/]+)\/agents\/([^/]+)\/dependencies\/([^/]+)$/);
402
+ if (agentDependencyRemoveMatch && req.method === 'DELETE') {
403
+ return routes.handleRemoveAgentDependency(req, res, agentDependencyRemoveMatch[1], agentDependencyRemoveMatch[2], agentDependencyRemoveMatch[3]);
404
+ }
405
+
406
+ // Orchestration template routes
407
+ if (url.pathname === '/api/orchestration-templates' && req.method === 'GET') {
408
+ return routes.handleGetTemplates(req, res);
409
+ }
410
+
411
+ const templateMatch = url.pathname.match(/^\/api\/orchestration-templates\/([^/]+)$/);
412
+ if (templateMatch && req.method === 'GET') {
413
+ return routes.handleGetTemplate(req, res, templateMatch[1]);
414
+ }
415
+ if (templateMatch && req.method === 'POST') {
416
+ return routes.handleCreateFromTemplate(req, res, templateMatch[1]);
417
+ }
418
+
419
+ // Task routes
420
+ const taskMatch = url.pathname.match(/^\/api\/tasks\/([^/]+)\/(\d+)$/);
421
+ if (taskMatch && req.method === 'PATCH') {
422
+ return routes.handleUpdateTask(req, res, taskMatch[1], taskMatch[2]);
423
+ }
424
+
425
+ const createMatch = url.pathname.match(/^\/api\/tasks\/([^/]+)$/);
426
+ if (createMatch && req.method === 'POST') {
427
+ return routes.handleCreateTask(req, res, createMatch[1]);
428
+ }
429
+
430
+ // Serve HTML
431
+ res.writeHead(200, { 'Content-Type': 'text/html' });
432
+ res.end(HTML);
433
+ });
434
+
435
+ // =============================================================================
436
+ // Server Startup
437
+ // =============================================================================
438
+
439
+ function start() {
440
+ const { PORT, TASKS_DIR, TMUX_SESSION } = config;
441
+
442
+ server.listen(PORT, () => {
443
+ console.log(`\n Claude Tasks running at:\n`);
444
+ console.log(` → http://localhost:${PORT}\n`);
445
+ console.log(` Reading tasks from: ${TASKS_DIR}`);
446
+ console.log(` Sending prompts to tmux session: ${TMUX_SESSION}`);
447
+ console.log(` (Set TMUX_SESSION env var to change)\n`);
448
+
449
+ // Load HTML template
450
+ loadHTML();
451
+
452
+ // Initialize EventBus handlers
453
+ registerAllHandlers();
454
+ console.log(` EventBus initialized with ${eventBus.getHandlerCount()} handlers`);
455
+
456
+ // Initialize Claude events module
457
+ claudeEvents.init();
458
+
459
+ // Initialize session manager
460
+ sessionManager.init();
461
+
462
+ // Initialize orchestration executor
463
+ orchestrationExecutor.init();
464
+
465
+ // Set up file watchers
466
+ setupWatchers();
467
+
468
+ // Build plan session cache
469
+ plans.buildPlanSessionCache();
470
+
471
+ console.log(` Press Ctrl+C to stop\n`);
472
+ });
473
+ }
474
+
475
+ // Export for testing
476
+ module.exports = { server, start };
477
+
478
+ // Start if run directly
479
+ if (require.main === module) {
480
+ start();
481
+ }