claudehq 1.0.1 → 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/bin/cli.js +2 -2
- package/lib/core/claude-events.js +308 -0
- package/lib/core/config.js +79 -0
- package/lib/core/event-bus.js +127 -0
- package/lib/core/sse.js +96 -0
- package/lib/core/watchers.js +127 -0
- package/lib/data/conversation.js +195 -0
- package/lib/data/plans.js +247 -0
- package/lib/data/tasks.js +263 -0
- package/lib/data/todos.js +205 -0
- package/lib/index.js +400 -0
- package/lib/routes/api.js +611 -0
- package/lib/server.js +778 -79
- package/lib/sessions/manager.js +870 -0
- package/package.json +10 -4
- package/public/index.html +6765 -0
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
|
+
}
|