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/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,103 @@
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
+ const ORCHESTRATIONS_DIR = path.join(CLAUDE_DIR, 'tasks-board', 'orchestrations');
20
+
21
+ // Data files
22
+ const CUSTOM_NAMES_FILE = path.join(TASKS_DIR, 'session-names.json');
23
+ const HIDDEN_SESSIONS_FILE = path.join(EVENTS_DIR, 'hidden-sessions.json');
24
+ const SESSIONS_FILE = path.join(EVENTS_DIR, 'sessions.json');
25
+ const EVENTS_FILE = path.join(EVENTS_DIR, 'events.json');
26
+
27
+ // Server configuration
28
+ const PORT = process.env.PORT || 3456;
29
+ const TMUX_SESSION = process.env.TMUX_SESSION || 'main';
30
+
31
+ // Session status constants
32
+ const SESSION_STATUS = {
33
+ IDLE: 'idle',
34
+ WORKING: 'working',
35
+ WAITING: 'waiting',
36
+ OFFLINE: 'offline'
37
+ };
38
+
39
+ // Agent status constants (for orchestration)
40
+ const AGENT_STATUS = {
41
+ PENDING: 'pending',
42
+ SPAWNING: 'spawning',
43
+ RUNNING: 'running',
44
+ WAITING: 'waiting',
45
+ COMPLETED: 'completed',
46
+ FAILED: 'failed',
47
+ CANCELLED: 'cancelled'
48
+ };
49
+
50
+ // Orchestration status constants
51
+ const ORCHESTRATION_STATUS = {
52
+ DRAFT: 'draft',
53
+ RUNNING: 'running',
54
+ PAUSED: 'paused',
55
+ COMPLETED: 'completed',
56
+ FAILED: 'failed'
57
+ };
58
+
59
+ // Timeouts and intervals
60
+ const HEALTH_CHECK_INTERVAL = 5000; // 5 seconds
61
+ const WORKING_TIMEOUT = 5 * 60 * 1000; // 5 minutes
62
+ const PERMISSION_POLL_INTERVAL = 1000; // 1 second
63
+ const EVENT_DEBOUNCE_DELAY = 100; // 100ms
64
+
65
+ // Limits
66
+ const MAX_EVENTS_IN_MEMORY = 1000;
67
+ const MAX_EVENTS_TO_BROADCAST = 500;
68
+
69
+ module.exports = {
70
+ // Directories
71
+ CLAUDE_DIR,
72
+ TASKS_DIR,
73
+ TODOS_DIR,
74
+ PLANS_DIR,
75
+ PROJECTS_DIR,
76
+ EVENTS_DIR,
77
+ ORCHESTRATIONS_DIR,
78
+
79
+ // Files
80
+ CUSTOM_NAMES_FILE,
81
+ HIDDEN_SESSIONS_FILE,
82
+ SESSIONS_FILE,
83
+ EVENTS_FILE,
84
+
85
+ // Server
86
+ PORT,
87
+ TMUX_SESSION,
88
+
89
+ // Status
90
+ SESSION_STATUS,
91
+ AGENT_STATUS,
92
+ ORCHESTRATION_STATUS,
93
+
94
+ // Timing
95
+ HEALTH_CHECK_INTERVAL,
96
+ WORKING_TIMEOUT,
97
+ PERMISSION_POLL_INTERVAL,
98
+ EVENT_DEBOUNCE_DELAY,
99
+
100
+ // Limits
101
+ MAX_EVENTS_IN_MEMORY,
102
+ MAX_EVENTS_TO_BROADCAST
103
+ };
@@ -0,0 +1,145 @@
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
+ // Orchestration events
51
+ ORCHESTRATION_CREATED: 'orchestration:created',
52
+ ORCHESTRATION_UPDATED: 'orchestration:updated',
53
+ ORCHESTRATION_STARTED: 'orchestration:started',
54
+ ORCHESTRATION_PAUSED: 'orchestration:paused',
55
+ ORCHESTRATION_COMPLETED: 'orchestration:completed',
56
+ ORCHESTRATION_FAILED: 'orchestration:failed',
57
+ ORCHESTRATION_DELETED: 'orchestration:deleted',
58
+
59
+ // Agent events (within orchestrations)
60
+ AGENT_CREATED: 'agent:created',
61
+ AGENT_SPAWNED: 'agent:spawned',
62
+ AGENT_STATUS_CHANGED: 'agent:status_changed',
63
+ AGENT_OUTPUT: 'agent:output',
64
+ AGENT_COMPLETED: 'agent:completed',
65
+ AGENT_FAILED: 'agent:failed',
66
+ AGENT_KILLED: 'agent:killed',
67
+ };
68
+
69
+ class EventBus {
70
+ constructor() {
71
+ this.handlers = new Map();
72
+ }
73
+
74
+ /**
75
+ * Subscribe to an event type
76
+ * @param {string} type - Event type from EventTypes
77
+ * @param {Function} handler - Handler function
78
+ * @returns {Function} Unsubscribe function
79
+ */
80
+ on(type, handler) {
81
+ if (!this.handlers.has(type)) {
82
+ this.handlers.set(type, new Set());
83
+ }
84
+ this.handlers.get(type).add(handler);
85
+ // Return unsubscribe function
86
+ return () => this.handlers.get(type)?.delete(handler);
87
+ }
88
+
89
+ /**
90
+ * Emit an event to all subscribers
91
+ * @param {string} type - Event type
92
+ * @param {Object} payload - Event data
93
+ */
94
+ emit(type, payload = {}) {
95
+ const handlers = this.handlers.get(type);
96
+ if (!handlers) return;
97
+ for (const handler of handlers) {
98
+ try {
99
+ handler(payload);
100
+ } catch (error) {
101
+ console.error(`[EventBus] Error in handler for ${type}:`, error);
102
+ }
103
+ }
104
+ }
105
+
106
+ /**
107
+ * Remove all handlers for an event type
108
+ * @param {string} type - Event type
109
+ */
110
+ off(type) {
111
+ this.handlers.delete(type);
112
+ }
113
+
114
+ /**
115
+ * Clear all handlers
116
+ */
117
+ clear() {
118
+ this.handlers.clear();
119
+ }
120
+
121
+ /**
122
+ * Get handler count for debugging
123
+ * @param {string} [type] - Optional event type
124
+ * @returns {number} Handler count
125
+ */
126
+ getHandlerCount(type) {
127
+ if (type) {
128
+ return this.handlers.get(type)?.size ?? 0;
129
+ }
130
+ let total = 0;
131
+ for (const handlers of this.handlers.values()) {
132
+ total += handlers.size;
133
+ }
134
+ return total;
135
+ }
136
+ }
137
+
138
+ // Singleton instance
139
+ const eventBus = new EventBus();
140
+
141
+ module.exports = {
142
+ EventBus,
143
+ EventTypes,
144
+ eventBus
145
+ };
@@ -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
+ };