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.
package/lib/index.js ADDED
@@ -0,0 +1,400 @@
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
+
29
+ // Routes
30
+ const { createRoutes } = require('./routes/api');
31
+
32
+ // =============================================================================
33
+ // Module Wiring - Connect modules that need callbacks from other modules
34
+ // =============================================================================
35
+
36
+ // Set up conversation file finder for claude-events module
37
+ claudeEvents.setConversationFileCallback(conversation.findConversationFile);
38
+
39
+ // Set up session update callback for claude-events module
40
+ claudeEvents.setSessionUpdateCallback(sessionManager.updateSessionFromEvent);
41
+
42
+ // Set up metadata callback for session manager
43
+ sessionManager.setMetadataCallback(conversation.getSessionMetadata);
44
+
45
+ // Set up plan cache callbacks for watchers
46
+ setPlanCacheCallbacks(plans.buildPlanSessionCache, plans.getSessionForPlan);
47
+
48
+ // Set up tasks module callbacks
49
+ tasks.setCustomNamesCallback(sessionManager.loadCustomNames);
50
+ tasks.setMetadataCallback(conversation.getSessionMetadata);
51
+
52
+ // =============================================================================
53
+ // EventBus Handlers - Register event handlers for broadcasts and logging
54
+ // =============================================================================
55
+
56
+ function registerBroadcastHandlers() {
57
+ // Task broadcast handlers
58
+ eventBus.on(EventTypes.TASK_CREATED, (payload) =>
59
+ broadcastUpdate('task_created', payload));
60
+ eventBus.on(EventTypes.TASK_UPDATED, (payload) =>
61
+ broadcastUpdate('task_updated', payload));
62
+ eventBus.on(EventTypes.TASK_STATUS_CHANGED, (payload) =>
63
+ broadcastUpdate('task_status_changed', payload));
64
+ eventBus.on(EventTypes.SESSION_RENAMED, (payload) =>
65
+ broadcastUpdate('session_renamed', payload));
66
+ eventBus.on(EventTypes.FILE_CHANGED, () => broadcastUpdate());
67
+
68
+ // Todo broadcast handlers
69
+ eventBus.on(EventTypes.TODOS_CHANGED, (payload) =>
70
+ broadcastUpdate('todos_changed', payload));
71
+ eventBus.on(EventTypes.TODO_CREATED, (payload) =>
72
+ broadcastUpdate('todo_created', payload));
73
+ eventBus.on(EventTypes.TODO_UPDATED, (payload) =>
74
+ broadcastUpdate('todo_updated', payload));
75
+ eventBus.on(EventTypes.TODO_STATUS_CHANGED, (payload) =>
76
+ broadcastUpdate('todo_status_changed', payload));
77
+
78
+ // Plan broadcast handlers
79
+ eventBus.on(EventTypes.PLANS_CHANGED, (payload) =>
80
+ broadcastUpdate('plans_changed', payload));
81
+ eventBus.on(EventTypes.PLAN_CREATED, (payload) =>
82
+ broadcastUpdate('plan_created', payload));
83
+ eventBus.on(EventTypes.PLAN_UPDATED, (payload) =>
84
+ broadcastUpdate('plan_updated', payload));
85
+ }
86
+
87
+ function registerLoggingHandlers() {
88
+ eventBus.on(EventTypes.TASK_CREATED, (payload) => {
89
+ console.log(`[${new Date().toLocaleTimeString()}] Task created: ${payload.task?.subject || payload.taskId}`);
90
+ });
91
+
92
+ eventBus.on(EventTypes.TASK_STATUS_CHANGED, (payload) => {
93
+ console.log(`[${new Date().toLocaleTimeString()}] Task #${payload.taskId} status: ${payload.status}`);
94
+ });
95
+
96
+ eventBus.on(EventTypes.PROMPT_SENT, (payload) => {
97
+ console.log(`[${new Date().toLocaleTimeString()}] Prompt sent to ${payload.tmuxSession}`);
98
+ });
99
+
100
+ eventBus.on(EventTypes.CLAUDE_EVENT, (payload) => {
101
+ console.log(`[${new Date().toLocaleTimeString()}] Claude event: ${payload.type} (${payload.tool || payload.sessionId})`);
102
+ });
103
+ }
104
+
105
+ function registerAllHandlers() {
106
+ registerBroadcastHandlers();
107
+ registerLoggingHandlers();
108
+ }
109
+
110
+ // =============================================================================
111
+ // HTML Template - Loaded from legacy server.js or served inline
112
+ // =============================================================================
113
+
114
+ // For now, we'll read the HTML from the old server.js
115
+ // In future iterations, this should be moved to a separate file
116
+ let HTML = '';
117
+
118
+ function loadHTML() {
119
+ // Try to load from a separate HTML file first
120
+ const htmlPath = path.join(__dirname, '..', 'public', 'index.html');
121
+ if (fs.existsSync(htmlPath)) {
122
+ HTML = fs.readFileSync(htmlPath, 'utf-8');
123
+ console.log(` Loaded HTML from: ${htmlPath}`);
124
+ return;
125
+ }
126
+
127
+ // Fall back to extracting from old server.js
128
+ const oldServerPath = path.join(__dirname, 'server.js');
129
+ if (fs.existsSync(oldServerPath)) {
130
+ const content = fs.readFileSync(oldServerPath, 'utf-8');
131
+ const htmlStart = content.indexOf('const HTML = `<!DOCTYPE html>');
132
+ const htmlEnd = content.indexOf('</html>`;', htmlStart);
133
+ if (htmlStart !== -1 && htmlEnd !== -1) {
134
+ HTML = content.substring(htmlStart + 14, htmlEnd + 7);
135
+ console.log(` Extracted HTML template from legacy server.js`);
136
+ return;
137
+ }
138
+ }
139
+
140
+ // Minimal fallback HTML
141
+ HTML = '<!DOCTYPE html><html><head><title>Claude Tasks Board</title></head><body><h1>Claude Tasks Board</h1><p>HTML template not found. Please check your installation.</p></body></html>';
142
+ console.log(' Warning: Using minimal fallback HTML template');
143
+ }
144
+
145
+ // =============================================================================
146
+ // Create Route Handlers
147
+ // =============================================================================
148
+
149
+ const routes = createRoutes({
150
+ // Core
151
+ sseClients,
152
+ claudeEvents: claudeEvents.claudeEvents,
153
+ addClaudeEvent: claudeEvents.addClaudeEvent,
154
+ broadcastManagedSessions: sessionManager.broadcastManagedSessions,
155
+
156
+ // Sessions
157
+ getManagedSessions: sessionManager.getManagedSessions,
158
+ getManagedSession: sessionManager.getManagedSession,
159
+ createManagedSession: sessionManager.createManagedSession,
160
+ updateManagedSession: sessionManager.updateManagedSession,
161
+ deleteManagedSession: sessionManager.deleteManagedSession,
162
+ sendPromptToManagedSession: sessionManager.sendPromptToManagedSession,
163
+ restartManagedSession: sessionManager.restartManagedSession,
164
+ linkClaudeSession: sessionManager.linkClaudeSession,
165
+ checkSessionHealth: sessionManager.checkSessionHealth,
166
+ saveManagedSessions: sessionManager.saveManagedSessions,
167
+ managedSessions: sessionManager.managedSessions,
168
+ hideSession: sessionManager.hideSession,
169
+ unhideSession: sessionManager.unhideSession,
170
+ permanentDeleteSession: sessionManager.permanentDeleteSession,
171
+ loadHiddenSessions: sessionManager.loadHiddenSessions,
172
+ loadCustomNames: sessionManager.loadCustomNames,
173
+ renameSession: sessionManager.renameSession,
174
+
175
+ // Data
176
+ loadAllTasks: tasks.loadAllTasks,
177
+ updateTask: tasks.updateTask,
178
+ createTask: tasks.createTask,
179
+ loadAllTodos: todos.loadAllTodos,
180
+ getTodosForSession: todos.getTodosForSession,
181
+ updateTodo: todos.updateTodo,
182
+ createTodo: todos.createTodo,
183
+ loadAllPlans: plans.loadAllPlans,
184
+ getPlansForSession: plans.getPlansForSession,
185
+ getPlan: plans.getPlan,
186
+ updatePlan: plans.updatePlan,
187
+ loadConversation: conversation.loadConversation,
188
+
189
+ // Utils
190
+ sendToTmux: sessionManager.sendToTmux
191
+ });
192
+
193
+ // =============================================================================
194
+ // HTTP Server
195
+ // =============================================================================
196
+
197
+ const server = http.createServer((req, res) => {
198
+ const url = new URL(req.url, `http://${req.headers.host}`);
199
+
200
+ // SSE endpoint
201
+ if (url.pathname === '/events') {
202
+ return routes.handleSSE(req, res);
203
+ }
204
+
205
+ // Health check
206
+ if (url.pathname === '/api/health' && req.method === 'GET') {
207
+ return routes.handleHealth(req, res);
208
+ }
209
+
210
+ // Claude Events
211
+ if (url.pathname === '/api/claude-events' && req.method === 'POST') {
212
+ return routes.handlePostClaudeEvent(req, res);
213
+ }
214
+ if (url.pathname === '/api/claude-events' && req.method === 'GET') {
215
+ return routes.handleGetClaudeEvents(req, res, url);
216
+ }
217
+ if (url.pathname === '/api/claude-events/stats' && req.method === 'GET') {
218
+ return routes.handleGetClaudeEventStats(req, res);
219
+ }
220
+
221
+ // Tasks
222
+ if (url.pathname === '/api/tasks' && req.method === 'GET') {
223
+ return routes.handleGetTasks(req, res);
224
+ }
225
+ if (url.pathname === '/api/tasks/bulk-update' && req.method === 'POST') {
226
+ return routes.handleBulkUpdateTasks(req, res);
227
+ }
228
+
229
+ // Todos
230
+ if (url.pathname === '/api/todos' && req.method === 'GET') {
231
+ return routes.handleGetAllTodos(req, res);
232
+ }
233
+ if (url.pathname === '/api/todos/bulk-update' && req.method === 'POST') {
234
+ return routes.handleBulkUpdateTodos(req, res);
235
+ }
236
+
237
+ const getTodosMatch = url.pathname.match(/^\/api\/todos\/([a-f0-9-]+)$/);
238
+ if (getTodosMatch && req.method === 'GET') {
239
+ return routes.handleGetTodosForSession(req, res, getTodosMatch[1]);
240
+ }
241
+ if (getTodosMatch && req.method === 'POST') {
242
+ return routes.handleCreateTodo(req, res, getTodosMatch[1]);
243
+ }
244
+
245
+ const updateTodoMatch = url.pathname.match(/^\/api\/todos\/([a-f0-9-]+)\/(\d+)$/);
246
+ if (updateTodoMatch && req.method === 'PATCH') {
247
+ return routes.handleUpdateTodo(req, res, updateTodoMatch[1], updateTodoMatch[2]);
248
+ }
249
+
250
+ // Plans
251
+ if (url.pathname === '/api/plans' && req.method === 'GET') {
252
+ return routes.handleGetAllPlans(req, res);
253
+ }
254
+
255
+ const getPlansForSessionMatch = url.pathname.match(/^\/api\/plans\/session\/([a-f0-9-]+)$/);
256
+ if (getPlansForSessionMatch && req.method === 'GET') {
257
+ return routes.handleGetPlansForSession(req, res, getPlansForSessionMatch[1]);
258
+ }
259
+
260
+ const getPlanMatch = url.pathname.match(/^\/api\/plans\/([a-z0-9-]+)$/);
261
+ if (getPlanMatch && req.method === 'GET') {
262
+ return routes.handleGetPlan(req, res, getPlanMatch[1]);
263
+ }
264
+ if (getPlanMatch && req.method === 'PUT') {
265
+ return routes.handleUpdatePlan(req, res, getPlanMatch[1]);
266
+ }
267
+
268
+ // Managed Sessions
269
+ if (url.pathname === '/api/managed-sessions' && req.method === 'GET') {
270
+ return routes.handleGetManagedSessions(req, res);
271
+ }
272
+ if (url.pathname === '/api/managed-sessions' && req.method === 'POST') {
273
+ return routes.handleCreateManagedSession(req, res);
274
+ }
275
+ if (url.pathname === '/api/managed-sessions/refresh' && req.method === 'POST') {
276
+ return routes.handleRefreshSessions(req, res);
277
+ }
278
+
279
+ const getManagedMatch = url.pathname.match(/^\/api\/managed-sessions\/([^/]+)$/);
280
+ if (getManagedMatch && req.method === 'GET') {
281
+ return routes.handleGetManagedSession(req, res, getManagedMatch[1]);
282
+ }
283
+ if (getManagedMatch && req.method === 'PATCH') {
284
+ return routes.handleUpdateManagedSession(req, res, getManagedMatch[1]);
285
+ }
286
+ if (getManagedMatch && req.method === 'DELETE') {
287
+ return routes.handleDeleteSession(req, res, getManagedMatch[1]);
288
+ }
289
+
290
+ const hideManagedMatch = url.pathname.match(/^\/api\/managed-sessions\/([^/]+)\/hide$/);
291
+ if (hideManagedMatch && req.method === 'POST') {
292
+ return routes.handleHideSession(req, res, hideManagedMatch[1]);
293
+ }
294
+
295
+ const unhideManagedMatch = url.pathname.match(/^\/api\/managed-sessions\/([^/]+)\/unhide$/);
296
+ if (unhideManagedMatch && req.method === 'POST') {
297
+ return routes.handleUnhideSession(req, res, unhideManagedMatch[1]);
298
+ }
299
+
300
+ if (url.pathname === '/api/hidden-sessions' && req.method === 'GET') {
301
+ return routes.handleGetHiddenSessions(req, res);
302
+ }
303
+
304
+ const permanentDeleteMatch = url.pathname.match(/^\/api\/managed-sessions\/([^/]+)\/permanent$/);
305
+ if (permanentDeleteMatch && req.method === 'DELETE') {
306
+ return routes.handlePermanentDeleteSession(req, res, permanentDeleteMatch[1]);
307
+ }
308
+
309
+ const promptManagedMatch = url.pathname.match(/^\/api\/managed-sessions\/([^/]+)\/prompt$/);
310
+ if (promptManagedMatch && req.method === 'POST') {
311
+ return routes.handleSendPromptToSession(req, res, promptManagedMatch[1]);
312
+ }
313
+
314
+ const restartManagedMatch = url.pathname.match(/^\/api\/managed-sessions\/([^/]+)\/restart$/);
315
+ if (restartManagedMatch && req.method === 'POST') {
316
+ return routes.handleRestartSession(req, res, restartManagedMatch[1]);
317
+ }
318
+
319
+ const linkManagedMatch = url.pathname.match(/^\/api\/managed-sessions\/([^/]+)\/link$/);
320
+ if (linkManagedMatch && req.method === 'POST') {
321
+ return routes.handleLinkSession(req, res, linkManagedMatch[1]);
322
+ }
323
+
324
+ // Conversation
325
+ const convMatch = url.pathname.match(/^\/api\/conversation\/([^/]+)$/);
326
+ if (convMatch && req.method === 'GET') {
327
+ return routes.handleGetConversation(req, res, convMatch[1]);
328
+ }
329
+
330
+ // Prompt
331
+ if (url.pathname === '/api/prompt' && req.method === 'POST') {
332
+ return routes.handleSendPrompt(req, res);
333
+ }
334
+
335
+ // Session rename
336
+ const renameMatch = url.pathname.match(/^\/api\/sessions\/([^/]+)\/rename$/);
337
+ if (renameMatch && req.method === 'POST') {
338
+ return routes.handleRenameSession(req, res, renameMatch[1]);
339
+ }
340
+
341
+ // Task routes
342
+ const taskMatch = url.pathname.match(/^\/api\/tasks\/([^/]+)\/(\d+)$/);
343
+ if (taskMatch && req.method === 'PATCH') {
344
+ return routes.handleUpdateTask(req, res, taskMatch[1], taskMatch[2]);
345
+ }
346
+
347
+ const createMatch = url.pathname.match(/^\/api\/tasks\/([^/]+)$/);
348
+ if (createMatch && req.method === 'POST') {
349
+ return routes.handleCreateTask(req, res, createMatch[1]);
350
+ }
351
+
352
+ // Serve HTML
353
+ res.writeHead(200, { 'Content-Type': 'text/html' });
354
+ res.end(HTML);
355
+ });
356
+
357
+ // =============================================================================
358
+ // Server Startup
359
+ // =============================================================================
360
+
361
+ function start() {
362
+ const { PORT, TASKS_DIR, TMUX_SESSION } = config;
363
+
364
+ server.listen(PORT, () => {
365
+ console.log(`\n Claude Tasks running at:\n`);
366
+ console.log(` → http://localhost:${PORT}\n`);
367
+ console.log(` Reading tasks from: ${TASKS_DIR}`);
368
+ console.log(` Sending prompts to tmux session: ${TMUX_SESSION}`);
369
+ console.log(` (Set TMUX_SESSION env var to change)\n`);
370
+
371
+ // Load HTML template
372
+ loadHTML();
373
+
374
+ // Initialize EventBus handlers
375
+ registerAllHandlers();
376
+ console.log(` EventBus initialized with ${eventBus.getHandlerCount()} handlers`);
377
+
378
+ // Initialize Claude events module
379
+ claudeEvents.init();
380
+
381
+ // Initialize session manager
382
+ sessionManager.init();
383
+
384
+ // Set up file watchers
385
+ setupWatchers();
386
+
387
+ // Build plan session cache
388
+ plans.buildPlanSessionCache();
389
+
390
+ console.log(` Press Ctrl+C to stop\n`);
391
+ });
392
+ }
393
+
394
+ // Export for testing
395
+ module.exports = { server, start };
396
+
397
+ // Start if run directly
398
+ if (require.main === module) {
399
+ start();
400
+ }