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 CHANGED
@@ -17,7 +17,7 @@ program
17
17
  .action((options) => {
18
18
  process.env.PORT = options.port;
19
19
  process.env.NO_OPEN = options.open ? '' : '1';
20
- require('../lib/server.js');
20
+ require('../lib/index.js');
21
21
  });
22
22
 
23
23
  program
@@ -50,7 +50,7 @@ program
50
50
  .action(() => {
51
51
  // If no command specified, run start
52
52
  process.env.PORT = process.env.PORT || '3456';
53
- require('../lib/server.js');
53
+ require('../lib/index.js');
54
54
  });
55
55
 
56
56
  program.parse();
@@ -0,0 +1,308 @@
1
+ /**
2
+ * Claude Events - Real-time event pipeline for Claude Code hooks
3
+ *
4
+ * Captures, processes, stores, and broadcasts Claude Code events.
5
+ * Events come from hooks (PreToolUse, PostToolUse, Stop, etc.)
6
+ */
7
+
8
+ const fs = require('fs');
9
+ const path = require('path');
10
+ const { EVENTS_DIR, MAX_EVENTS_IN_MEMORY } = require('./config');
11
+ const { eventBus, EventTypes } = require('./event-bus');
12
+ const { broadcastUpdate } = require('./sse');
13
+
14
+ const EVENTS_FILE = path.join(EVENTS_DIR, 'events.jsonl');
15
+ const MAX_EVENTS = MAX_EVENTS_IN_MEMORY || 1000;
16
+
17
+ // Event state
18
+ const claudeEvents = [];
19
+ const seenEventIds = new Set();
20
+ const pendingToolUses = new Map(); // Track pre_tool_use for duration calculation
21
+ let lastEventsFileSize = 0;
22
+
23
+ // Session update callback (set by sessions module to avoid circular dep)
24
+ let sessionUpdateCallback = null;
25
+
26
+ // Conversation file finder callback (set by conversation module)
27
+ let findConversationFileCallback = null;
28
+
29
+ /**
30
+ * Set callback for updating sessions from events
31
+ * @param {Function} callback - Function to call with event
32
+ */
33
+ function setSessionUpdateCallback(callback) {
34
+ sessionUpdateCallback = callback;
35
+ }
36
+
37
+ /**
38
+ * Set callback for finding conversation files
39
+ * @param {Function} callback - Function to call with sessionId
40
+ */
41
+ function setConversationFileCallback(callback) {
42
+ findConversationFileCallback = callback;
43
+ }
44
+
45
+ /**
46
+ * Get the latest assistant text from a conversation JSONL file
47
+ * @param {string} sessionId - Claude session ID
48
+ * @returns {string} Latest assistant text or empty string
49
+ */
50
+ function getLatestAssistantText(sessionId) {
51
+ if (!findConversationFileCallback) return '';
52
+
53
+ const filePath = findConversationFileCallback(sessionId);
54
+ if (!filePath) return '';
55
+
56
+ try {
57
+ const fileData = fs.readFileSync(filePath, 'utf-8');
58
+ const lines = fileData.trim().split('\n');
59
+
60
+ // Parse backwards to find the most recent assistant text
61
+ for (let i = lines.length - 1; i >= 0; i--) {
62
+ try {
63
+ const entry = JSON.parse(lines[i]);
64
+ if (entry.type === 'assistant' && Array.isArray(entry.message?.content)) {
65
+ for (const block of entry.message.content) {
66
+ if (block.type === 'text' && block.text) {
67
+ return block.text;
68
+ }
69
+ }
70
+ }
71
+ } catch (e) {
72
+ // Skip malformed lines
73
+ }
74
+ }
75
+ } catch (e) {
76
+ // File read error
77
+ }
78
+ return '';
79
+ }
80
+
81
+ /**
82
+ * Process and enrich event (calculate tool duration, etc.)
83
+ * @param {Object} event - Raw event
84
+ * @returns {Object} Processed event
85
+ */
86
+ function processClaudeEvent(event) {
87
+ // Track pre_tool_use for duration calculation
88
+ if (event.type === 'pre_tool_use' && event.toolUseId) {
89
+ pendingToolUses.set(event.toolUseId, event);
90
+ }
91
+
92
+ // Calculate duration for post_tool_use
93
+ if (event.type === 'post_tool_use' && event.toolUseId) {
94
+ const preEvent = pendingToolUses.get(event.toolUseId);
95
+ if (preEvent) {
96
+ event.duration = event.timestamp - preEvent.timestamp;
97
+ pendingToolUses.delete(event.toolUseId);
98
+ }
99
+ }
100
+
101
+ // Enrich stop events with assistant response from conversation file
102
+ if (event.type === 'stop' && !event.response && event.sessionId) {
103
+ event.response = getLatestAssistantText(event.sessionId);
104
+ }
105
+
106
+ return event;
107
+ }
108
+
109
+ /**
110
+ * Add event to memory and broadcast
111
+ * @param {Object} event - Event to add
112
+ */
113
+ function addClaudeEvent(event) {
114
+ // Skip duplicates
115
+ if (seenEventIds.has(event.id)) return;
116
+ seenEventIds.add(event.id);
117
+
118
+ // Trim old IDs to prevent memory leak
119
+ if (seenEventIds.size > MAX_EVENTS * 2) {
120
+ const idsToKeep = [...seenEventIds].slice(-MAX_EVENTS);
121
+ seenEventIds.clear();
122
+ idsToKeep.forEach(id => seenEventIds.add(id));
123
+ }
124
+
125
+ const processed = processClaudeEvent(event);
126
+ claudeEvents.push(processed);
127
+
128
+ // Trim old events
129
+ if (claudeEvents.length > MAX_EVENTS) {
130
+ claudeEvents.splice(0, claudeEvents.length - MAX_EVENTS);
131
+ }
132
+
133
+ // Emit through EventBus
134
+ eventBus.emit(EventTypes.CLAUDE_EVENT, processed);
135
+
136
+ // Update managed session status based on event
137
+ if (sessionUpdateCallback) {
138
+ sessionUpdateCallback(processed);
139
+ }
140
+
141
+ // Broadcast to all SSE clients
142
+ broadcastClaudeEvent(processed);
143
+ }
144
+
145
+ /**
146
+ * Broadcast a single Claude event to all SSE clients
147
+ * @param {Object} event - Event to broadcast
148
+ */
149
+ function broadcastClaudeEvent(event) {
150
+ broadcastUpdate('claude_event', { event });
151
+ }
152
+
153
+ /**
154
+ * Load existing events from file
155
+ */
156
+ function loadEventsFromFile() {
157
+ if (!fs.existsSync(EVENTS_FILE)) return;
158
+
159
+ try {
160
+ const content = fs.readFileSync(EVENTS_FILE, 'utf-8');
161
+ const lines = content.trim().split('\n').filter(Boolean);
162
+
163
+ for (const line of lines) {
164
+ try {
165
+ const event = JSON.parse(line);
166
+ processClaudeEvent(event);
167
+ claudeEvents.push(event);
168
+ seenEventIds.add(event.id);
169
+ } catch (e) {
170
+ // Skip invalid lines
171
+ }
172
+ }
173
+
174
+ lastEventsFileSize = content.length;
175
+ console.log(` Loaded ${claudeEvents.length} Claude events from: ${EVENTS_FILE}`);
176
+
177
+ // Discover sessions from loaded events (process unique session IDs)
178
+ if (sessionUpdateCallback) {
179
+ const sessionIds = new Set();
180
+ for (const event of claudeEvents) {
181
+ if (event.sessionId && !sessionIds.has(event.sessionId)) {
182
+ sessionIds.add(event.sessionId);
183
+ sessionUpdateCallback(event);
184
+ }
185
+ }
186
+ }
187
+ } catch (e) {
188
+ console.error('Error loading events:', e.message);
189
+ }
190
+ }
191
+
192
+ /**
193
+ * Watch events file for changes
194
+ */
195
+ function watchEventsFile() {
196
+ // Ensure directory exists
197
+ if (!fs.existsSync(EVENTS_DIR)) {
198
+ fs.mkdirSync(EVENTS_DIR, { recursive: true });
199
+ }
200
+
201
+ // Create file if it doesn't exist
202
+ if (!fs.existsSync(EVENTS_FILE)) {
203
+ fs.writeFileSync(EVENTS_FILE, '');
204
+ }
205
+
206
+ // Use polling for reliability (works across all platforms)
207
+ fs.watchFile(EVENTS_FILE, { interval: 100 }, (curr, prev) => {
208
+ if (curr.size <= prev.size) return; // File hasn't grown
209
+
210
+ try {
211
+ const content = fs.readFileSync(EVENTS_FILE, 'utf-8');
212
+
213
+ // Only process new content
214
+ if (content.length > lastEventsFileSize) {
215
+ const newContent = content.slice(lastEventsFileSize);
216
+ const newLines = newContent.trim().split('\n').filter(Boolean);
217
+
218
+ for (const line of newLines) {
219
+ try {
220
+ const event = JSON.parse(line);
221
+ addClaudeEvent(event);
222
+ } catch (e) {
223
+ // Skip invalid lines
224
+ }
225
+ }
226
+
227
+ lastEventsFileSize = content.length;
228
+ }
229
+ } catch (e) {
230
+ // Ignore read errors
231
+ }
232
+ });
233
+
234
+ console.log(` Watching for Claude events in: ${EVENTS_FILE}`);
235
+ }
236
+
237
+ /**
238
+ * Get all events
239
+ * @returns {Array} All events in memory
240
+ */
241
+ function getEvents() {
242
+ return claudeEvents;
243
+ }
244
+
245
+ /**
246
+ * Get events filtered by session
247
+ * @param {string} sessionId - Session ID to filter by
248
+ * @returns {Array} Filtered events
249
+ */
250
+ function getEventsBySession(sessionId) {
251
+ return claudeEvents.filter(e => e.sessionId === sessionId);
252
+ }
253
+
254
+ /**
255
+ * Get event statistics
256
+ * @returns {Object} Event stats
257
+ */
258
+ function getEventStats() {
259
+ const toolCounts = {};
260
+ const typeCounts = {};
261
+ const sessionCounts = {};
262
+
263
+ for (const event of claudeEvents) {
264
+ // Count by tool
265
+ if (event.tool) {
266
+ toolCounts[event.tool] = (toolCounts[event.tool] || 0) + 1;
267
+ }
268
+ // Count by type
269
+ typeCounts[event.type] = (typeCounts[event.type] || 0) + 1;
270
+ // Count by session
271
+ if (event.sessionId) {
272
+ sessionCounts[event.sessionId] = (sessionCounts[event.sessionId] || 0) + 1;
273
+ }
274
+ }
275
+
276
+ return {
277
+ totalEvents: claudeEvents.length,
278
+ toolCounts,
279
+ typeCounts,
280
+ sessionCounts
281
+ };
282
+ }
283
+
284
+ /**
285
+ * Initialize the events module
286
+ */
287
+ function init() {
288
+ loadEventsFromFile();
289
+ watchEventsFile();
290
+ }
291
+
292
+ module.exports = {
293
+ // Core functions
294
+ init,
295
+ addClaudeEvent,
296
+ getEvents,
297
+ getEventsBySession,
298
+ getEventStats,
299
+ processClaudeEvent,
300
+ broadcastClaudeEvent,
301
+
302
+ // Callbacks for dependency injection
303
+ setSessionUpdateCallback,
304
+ setConversationFileCallback,
305
+
306
+ // Direct access (for SSE history, etc.)
307
+ claudeEvents
308
+ };
@@ -0,0 +1,79 @@
1
+ /**
2
+ * Configuration - Centralized config for Claude HQ
3
+ *
4
+ * All paths, constants, and environment configuration in one place.
5
+ */
6
+
7
+ const path = require('path');
8
+ const os = require('os');
9
+
10
+ // Base directories
11
+ const CLAUDE_DIR = path.join(os.homedir(), '.claude');
12
+
13
+ // Data directories
14
+ const TASKS_DIR = path.join(CLAUDE_DIR, 'tasks');
15
+ const TODOS_DIR = path.join(CLAUDE_DIR, 'todos');
16
+ const PLANS_DIR = path.join(CLAUDE_DIR, 'plans');
17
+ const PROJECTS_DIR = path.join(CLAUDE_DIR, 'projects');
18
+ const EVENTS_DIR = path.join(CLAUDE_DIR, 'tasks-board');
19
+
20
+ // Data files
21
+ const CUSTOM_NAMES_FILE = path.join(TASKS_DIR, 'session-names.json');
22
+ const HIDDEN_SESSIONS_FILE = path.join(EVENTS_DIR, 'hidden-sessions.json');
23
+ const SESSIONS_FILE = path.join(EVENTS_DIR, 'sessions.json');
24
+ const EVENTS_FILE = path.join(EVENTS_DIR, 'events.json');
25
+
26
+ // Server configuration
27
+ const PORT = process.env.PORT || 3456;
28
+ const TMUX_SESSION = process.env.TMUX_SESSION || 'main';
29
+
30
+ // Session status constants
31
+ const SESSION_STATUS = {
32
+ IDLE: 'idle',
33
+ WORKING: 'working',
34
+ WAITING: 'waiting',
35
+ OFFLINE: 'offline'
36
+ };
37
+
38
+ // Timeouts and intervals
39
+ const HEALTH_CHECK_INTERVAL = 5000; // 5 seconds
40
+ const WORKING_TIMEOUT = 5 * 60 * 1000; // 5 minutes
41
+ const PERMISSION_POLL_INTERVAL = 1000; // 1 second
42
+ const EVENT_DEBOUNCE_DELAY = 100; // 100ms
43
+
44
+ // Limits
45
+ const MAX_EVENTS_IN_MEMORY = 1000;
46
+ const MAX_EVENTS_TO_BROADCAST = 500;
47
+
48
+ module.exports = {
49
+ // Directories
50
+ CLAUDE_DIR,
51
+ TASKS_DIR,
52
+ TODOS_DIR,
53
+ PLANS_DIR,
54
+ PROJECTS_DIR,
55
+ EVENTS_DIR,
56
+
57
+ // Files
58
+ CUSTOM_NAMES_FILE,
59
+ HIDDEN_SESSIONS_FILE,
60
+ SESSIONS_FILE,
61
+ EVENTS_FILE,
62
+
63
+ // Server
64
+ PORT,
65
+ TMUX_SESSION,
66
+
67
+ // Status
68
+ SESSION_STATUS,
69
+
70
+ // Timing
71
+ HEALTH_CHECK_INTERVAL,
72
+ WORKING_TIMEOUT,
73
+ PERMISSION_POLL_INTERVAL,
74
+ EVENT_DEBOUNCE_DELAY,
75
+
76
+ // Limits
77
+ MAX_EVENTS_IN_MEMORY,
78
+ MAX_EVENTS_TO_BROADCAST
79
+ };
@@ -0,0 +1,127 @@
1
+ /**
2
+ * EventBus - Decoupled event handling for Claude HQ
3
+ *
4
+ * Provides pub/sub pattern for communication between modules without tight coupling.
5
+ * Used for broadcasting updates, logging, and inter-module communication.
6
+ */
7
+
8
+ const EventTypes = {
9
+ // Task events
10
+ TASK_CREATED: 'task:created',
11
+ TASK_UPDATED: 'task:updated',
12
+ TASK_STATUS_CHANGED: 'task:status_changed',
13
+
14
+ // Todo events
15
+ TODO_CREATED: 'todo:created',
16
+ TODO_UPDATED: 'todo:updated',
17
+ TODO_STATUS_CHANGED: 'todo:status_changed',
18
+ TODOS_CHANGED: 'todos:changed',
19
+
20
+ // Plan events
21
+ PLAN_CREATED: 'plan:created',
22
+ PLAN_UPDATED: 'plan:updated',
23
+ PLANS_CHANGED: 'plans:changed',
24
+
25
+ // Session events
26
+ SESSION_RENAMED: 'session:renamed',
27
+ SESSION_SELECTED: 'session:selected',
28
+
29
+ // Prompt events
30
+ PROMPT_SENT: 'prompt:sent',
31
+ PROMPT_SUCCEEDED: 'prompt:succeeded',
32
+ PROMPT_FAILED: 'prompt:failed',
33
+
34
+ // Claude Code events (from hooks)
35
+ CLAUDE_EVENT: 'claude:event',
36
+ PRE_TOOL_USE: 'claude:pre_tool_use',
37
+ POST_TOOL_USE: 'claude:post_tool_use',
38
+ STOP: 'claude:stop',
39
+ USER_PROMPT_SUBMIT: 'claude:user_prompt_submit',
40
+ SUBAGENT_SPAWN: 'claude:subagent_spawn',
41
+ SUBAGENT_COMPLETE: 'claude:subagent_complete',
42
+
43
+ // File system events
44
+ FILE_CHANGED: 'file:changed',
45
+
46
+ // Attention events
47
+ ATTENTION_NEEDED: 'attention:needed',
48
+ ATTENTION_CLEARED: 'attention:cleared',
49
+ };
50
+
51
+ class EventBus {
52
+ constructor() {
53
+ this.handlers = new Map();
54
+ }
55
+
56
+ /**
57
+ * Subscribe to an event type
58
+ * @param {string} type - Event type from EventTypes
59
+ * @param {Function} handler - Handler function
60
+ * @returns {Function} Unsubscribe function
61
+ */
62
+ on(type, handler) {
63
+ if (!this.handlers.has(type)) {
64
+ this.handlers.set(type, new Set());
65
+ }
66
+ this.handlers.get(type).add(handler);
67
+ // Return unsubscribe function
68
+ return () => this.handlers.get(type)?.delete(handler);
69
+ }
70
+
71
+ /**
72
+ * Emit an event to all subscribers
73
+ * @param {string} type - Event type
74
+ * @param {Object} payload - Event data
75
+ */
76
+ emit(type, payload = {}) {
77
+ const handlers = this.handlers.get(type);
78
+ if (!handlers) return;
79
+ for (const handler of handlers) {
80
+ try {
81
+ handler(payload);
82
+ } catch (error) {
83
+ console.error(`[EventBus] Error in handler for ${type}:`, error);
84
+ }
85
+ }
86
+ }
87
+
88
+ /**
89
+ * Remove all handlers for an event type
90
+ * @param {string} type - Event type
91
+ */
92
+ off(type) {
93
+ this.handlers.delete(type);
94
+ }
95
+
96
+ /**
97
+ * Clear all handlers
98
+ */
99
+ clear() {
100
+ this.handlers.clear();
101
+ }
102
+
103
+ /**
104
+ * Get handler count for debugging
105
+ * @param {string} [type] - Optional event type
106
+ * @returns {number} Handler count
107
+ */
108
+ getHandlerCount(type) {
109
+ if (type) {
110
+ return this.handlers.get(type)?.size ?? 0;
111
+ }
112
+ let total = 0;
113
+ for (const handlers of this.handlers.values()) {
114
+ total += handlers.size;
115
+ }
116
+ return total;
117
+ }
118
+ }
119
+
120
+ // Singleton instance
121
+ const eventBus = new EventBus();
122
+
123
+ module.exports = {
124
+ EventBus,
125
+ EventTypes,
126
+ eventBus
127
+ };
@@ -0,0 +1,96 @@
1
+ /**
2
+ * SSE (Server-Sent Events) - Real-time client communication
3
+ *
4
+ * Manages SSE connections and broadcasts updates to all connected clients.
5
+ */
6
+
7
+ // Connected SSE clients
8
+ const sseClients = new Set();
9
+
10
+ /**
11
+ * Add a new SSE client
12
+ * @param {http.ServerResponse} client - Response object to write SSE events to
13
+ */
14
+ function addClient(client) {
15
+ sseClients.add(client);
16
+ }
17
+
18
+ /**
19
+ * Remove an SSE client
20
+ * @param {http.ServerResponse} client - Client to remove
21
+ */
22
+ function removeClient(client) {
23
+ sseClients.delete(client);
24
+ }
25
+
26
+ /**
27
+ * Get current client count
28
+ * @returns {number} Number of connected clients
29
+ */
30
+ function getClientCount() {
31
+ return sseClients.size;
32
+ }
33
+
34
+ /**
35
+ * Broadcast an update to all connected clients
36
+ * @param {string} eventType - Type of event (e.g., 'update', 'task_created')
37
+ * @param {Object} payload - Event data
38
+ */
39
+ function broadcastUpdate(eventType = 'update', payload = {}) {
40
+ const data = JSON.stringify({
41
+ type: eventType,
42
+ timestamp: Date.now(),
43
+ ...payload
44
+ });
45
+ for (const client of sseClients) {
46
+ try {
47
+ client.write(`data: ${data}\n\n`);
48
+ } catch (e) {
49
+ // Client disconnected, remove it
50
+ sseClients.delete(client);
51
+ }
52
+ }
53
+ }
54
+
55
+ /**
56
+ * Broadcast raw data to all clients
57
+ * @param {string} data - Pre-formatted SSE data
58
+ */
59
+ function broadcastRaw(data) {
60
+ for (const client of sseClients) {
61
+ try {
62
+ client.write(data);
63
+ } catch (e) {
64
+ sseClients.delete(client);
65
+ }
66
+ }
67
+ }
68
+
69
+ /**
70
+ * Send data to a specific client
71
+ * @param {http.ServerResponse} client - Target client
72
+ * @param {string} eventType - Type of event
73
+ * @param {Object} payload - Event data
74
+ */
75
+ function sendToClient(client, eventType, payload = {}) {
76
+ const data = JSON.stringify({
77
+ type: eventType,
78
+ timestamp: Date.now(),
79
+ ...payload
80
+ });
81
+ try {
82
+ client.write(`data: ${data}\n\n`);
83
+ } catch (e) {
84
+ sseClients.delete(client);
85
+ }
86
+ }
87
+
88
+ module.exports = {
89
+ sseClients,
90
+ addClient,
91
+ removeClient,
92
+ getClientCount,
93
+ broadcastUpdate,
94
+ broadcastRaw,
95
+ sendToClient
96
+ };