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 +2 -2
- package/lib/core/claude-events.js +308 -0
- package/lib/core/config.js +103 -0
- package/lib/core/event-bus.js +145 -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/orchestration.js +941 -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 +481 -0
- package/lib/orchestration/executor.js +635 -0
- package/lib/routes/api.js +1010 -0
- package/lib/sessions/manager.js +870 -0
- package/package.json +10 -4
- package/{lib/server.js → public/index.html} +1984 -2644
|
@@ -0,0 +1,127 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* File Watchers Module - Watch for file system changes
|
|
3
|
+
*
|
|
4
|
+
* Watches task, todo, plan, and conversation directories for changes
|
|
5
|
+
* and emits events through the EventBus.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
const fs = require('fs');
|
|
9
|
+
const {
|
|
10
|
+
TASKS_DIR,
|
|
11
|
+
TODOS_DIR,
|
|
12
|
+
PLANS_DIR,
|
|
13
|
+
PROJECTS_DIR,
|
|
14
|
+
EVENT_DEBOUNCE_DELAY
|
|
15
|
+
} = require('./config');
|
|
16
|
+
const { eventBus, EventTypes } = require('./event-bus');
|
|
17
|
+
|
|
18
|
+
let watchDebounce = null;
|
|
19
|
+
|
|
20
|
+
// Callback for rebuilding plan cache (set by plans module)
|
|
21
|
+
let rebuildPlanCacheCallback = null;
|
|
22
|
+
let getSessionForPlanCallback = null;
|
|
23
|
+
|
|
24
|
+
/**
|
|
25
|
+
* Set callback for rebuilding plan cache
|
|
26
|
+
* @param {Function} callback
|
|
27
|
+
*/
|
|
28
|
+
function setPlanCacheCallbacks(rebuildCallback, getSessionCallback) {
|
|
29
|
+
rebuildPlanCacheCallback = rebuildCallback;
|
|
30
|
+
getSessionForPlanCallback = getSessionCallback;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
/**
|
|
34
|
+
* Setup all file watchers
|
|
35
|
+
*/
|
|
36
|
+
function setupWatchers() {
|
|
37
|
+
// Watch tasks directory
|
|
38
|
+
if (fs.existsSync(TASKS_DIR)) {
|
|
39
|
+
fs.watch(TASKS_DIR, { recursive: true }, (eventType, filename) => {
|
|
40
|
+
if (filename && filename.endsWith('.json')) {
|
|
41
|
+
clearTimeout(watchDebounce);
|
|
42
|
+
watchDebounce = setTimeout(() => {
|
|
43
|
+
eventBus.emit(EventTypes.FILE_CHANGED, {
|
|
44
|
+
type: 'task',
|
|
45
|
+
filename,
|
|
46
|
+
directory: TASKS_DIR
|
|
47
|
+
});
|
|
48
|
+
}, EVENT_DEBOUNCE_DELAY);
|
|
49
|
+
}
|
|
50
|
+
});
|
|
51
|
+
console.log(` Watching for changes in: ${TASKS_DIR}`);
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
// Watch projects directory for conversation updates
|
|
55
|
+
if (fs.existsSync(PROJECTS_DIR)) {
|
|
56
|
+
fs.watch(PROJECTS_DIR, { recursive: true }, (eventType, filename) => {
|
|
57
|
+
if (filename && filename.endsWith('.jsonl')) {
|
|
58
|
+
clearTimeout(watchDebounce);
|
|
59
|
+
watchDebounce = setTimeout(() => {
|
|
60
|
+
eventBus.emit(EventTypes.FILE_CHANGED, {
|
|
61
|
+
type: 'conversation',
|
|
62
|
+
filename,
|
|
63
|
+
directory: PROJECTS_DIR
|
|
64
|
+
});
|
|
65
|
+
}, 200);
|
|
66
|
+
}
|
|
67
|
+
});
|
|
68
|
+
console.log(` Watching for conversation updates in: ${PROJECTS_DIR}`);
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
// Watch todos directory for changes
|
|
72
|
+
if (fs.existsSync(TODOS_DIR)) {
|
|
73
|
+
fs.watch(TODOS_DIR, (eventType, filename) => {
|
|
74
|
+
if (filename && filename.endsWith('.json')) {
|
|
75
|
+
clearTimeout(watchDebounce);
|
|
76
|
+
watchDebounce = setTimeout(() => {
|
|
77
|
+
// Extract session ID from filename
|
|
78
|
+
const match = filename.match(/^([a-f0-9-]+)-agent-/);
|
|
79
|
+
const sessionId = match ? match[1] : null;
|
|
80
|
+
|
|
81
|
+
eventBus.emit(EventTypes.TODOS_CHANGED, {
|
|
82
|
+
type: 'todo',
|
|
83
|
+
filename,
|
|
84
|
+
sessionId,
|
|
85
|
+
directory: TODOS_DIR
|
|
86
|
+
});
|
|
87
|
+
}, EVENT_DEBOUNCE_DELAY);
|
|
88
|
+
}
|
|
89
|
+
});
|
|
90
|
+
console.log(` Watching for todo changes in: ${TODOS_DIR}`);
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
// Watch plans directory for changes
|
|
94
|
+
if (fs.existsSync(PLANS_DIR)) {
|
|
95
|
+
fs.watch(PLANS_DIR, (eventType, filename) => {
|
|
96
|
+
if (filename && filename.endsWith('.md')) {
|
|
97
|
+
clearTimeout(watchDebounce);
|
|
98
|
+
watchDebounce = setTimeout(() => {
|
|
99
|
+
const slug = filename.replace('.md', '');
|
|
100
|
+
|
|
101
|
+
// If it's a new plan, rebuild cache to find its session
|
|
102
|
+
if (eventType === 'rename' && rebuildPlanCacheCallback) {
|
|
103
|
+
rebuildPlanCacheCallback();
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
const sessionId = getSessionForPlanCallback ? getSessionForPlanCallback(slug) : null;
|
|
107
|
+
|
|
108
|
+
eventBus.emit(EventTypes.PLANS_CHANGED, {
|
|
109
|
+
type: 'plan',
|
|
110
|
+
slug,
|
|
111
|
+
filename,
|
|
112
|
+
sessionId,
|
|
113
|
+
directory: PLANS_DIR
|
|
114
|
+
});
|
|
115
|
+
}, EVENT_DEBOUNCE_DELAY);
|
|
116
|
+
}
|
|
117
|
+
});
|
|
118
|
+
console.log(` Watching for plan changes in: ${PLANS_DIR}`);
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
console.log('');
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
module.exports = {
|
|
125
|
+
setupWatchers,
|
|
126
|
+
setPlanCacheCallbacks
|
|
127
|
+
};
|
|
@@ -0,0 +1,195 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Conversation Data Module - Conversation file loading and parsing
|
|
3
|
+
*
|
|
4
|
+
* Handles reading conversation files from the Claude projects directory.
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
const fs = require('fs');
|
|
8
|
+
const path = require('path');
|
|
9
|
+
|
|
10
|
+
const { PROJECTS_DIR } = require('../core/config');
|
|
11
|
+
|
|
12
|
+
/**
|
|
13
|
+
* Find conversation file for a session
|
|
14
|
+
* @param {string} sessionId - Session ID
|
|
15
|
+
* @returns {string|null} Path to conversation file or null
|
|
16
|
+
*/
|
|
17
|
+
function findConversationFile(sessionId) {
|
|
18
|
+
// Search through all project directories for a matching conversation file
|
|
19
|
+
if (!fs.existsSync(PROJECTS_DIR)) return null;
|
|
20
|
+
|
|
21
|
+
const projectDirs = fs.readdirSync(PROJECTS_DIR);
|
|
22
|
+
for (const dir of projectDirs) {
|
|
23
|
+
const conversationPath = path.join(PROJECTS_DIR, dir, `${sessionId}.jsonl`);
|
|
24
|
+
if (fs.existsSync(conversationPath)) {
|
|
25
|
+
return conversationPath;
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
return null;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
/**
|
|
32
|
+
* Load and parse conversation for a session
|
|
33
|
+
* @param {string} sessionId - Session ID
|
|
34
|
+
* @returns {Object} Conversation data with messages array
|
|
35
|
+
*/
|
|
36
|
+
function loadConversation(sessionId) {
|
|
37
|
+
const filePath = findConversationFile(sessionId);
|
|
38
|
+
if (!filePath) return { messages: [], found: false };
|
|
39
|
+
|
|
40
|
+
try {
|
|
41
|
+
const fileData = fs.readFileSync(filePath, 'utf-8');
|
|
42
|
+
const lines = fileData.trim().split('\n');
|
|
43
|
+
const messages = [];
|
|
44
|
+
|
|
45
|
+
for (const line of lines) {
|
|
46
|
+
try {
|
|
47
|
+
const entry = JSON.parse(line);
|
|
48
|
+
|
|
49
|
+
// Skip non-message entries
|
|
50
|
+
if (!entry.type || !entry.message) continue;
|
|
51
|
+
|
|
52
|
+
const msgContent = entry.message.content;
|
|
53
|
+
const ts = entry.timestamp;
|
|
54
|
+
const id = entry.uuid;
|
|
55
|
+
|
|
56
|
+
if (entry.type === 'user') {
|
|
57
|
+
// User message: string or array of content blocks
|
|
58
|
+
if (typeof msgContent === 'string') {
|
|
59
|
+
messages.push({ id: `user-${id}`, type: 'user', content: msgContent, timestamp: ts });
|
|
60
|
+
} else if (Array.isArray(msgContent)) {
|
|
61
|
+
for (let i = 0; i < msgContent.length; i++) {
|
|
62
|
+
const block = msgContent[i];
|
|
63
|
+
if (block.type === 'tool_result') {
|
|
64
|
+
// Tool result response
|
|
65
|
+
const txt = typeof block.content === 'string' ? block.content : JSON.stringify(block.content);
|
|
66
|
+
messages.push({
|
|
67
|
+
id: `result-${block.tool_use_id}`,
|
|
68
|
+
type: 'tool_result',
|
|
69
|
+
content: txt.length > 200 ? txt.slice(0, 200) + '...' : txt,
|
|
70
|
+
timestamp: ts
|
|
71
|
+
});
|
|
72
|
+
} else if (block.type === 'text' && block.text) {
|
|
73
|
+
// User text
|
|
74
|
+
messages.push({ id: `user-${id}-${i}`, type: 'user', content: block.text, timestamp: ts });
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
} else if (entry.type === 'assistant') {
|
|
79
|
+
// Assistant message: array of content blocks
|
|
80
|
+
if (Array.isArray(msgContent)) {
|
|
81
|
+
for (let i = 0; i < msgContent.length; i++) {
|
|
82
|
+
const block = msgContent[i];
|
|
83
|
+
if (block.type === 'text' && block.text) {
|
|
84
|
+
messages.push({ id: `asst-${id}-${i}`, type: 'assistant', content: block.text, timestamp: ts });
|
|
85
|
+
} else if (block.type === 'tool_use') {
|
|
86
|
+
messages.push({
|
|
87
|
+
id: `tool-${block.id}`,
|
|
88
|
+
type: 'tool_use',
|
|
89
|
+
tool: block.name,
|
|
90
|
+
input: block.input,
|
|
91
|
+
timestamp: ts
|
|
92
|
+
});
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
} catch (e) {
|
|
98
|
+
// Skip malformed lines
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
return { messages, found: true, path: filePath };
|
|
103
|
+
} catch (e) {
|
|
104
|
+
return { messages: [], found: false, error: e.message };
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
/**
|
|
109
|
+
* Get the latest assistant text from a conversation
|
|
110
|
+
* @param {string} sessionId - Session ID
|
|
111
|
+
* @returns {string} Latest assistant text or empty string
|
|
112
|
+
*/
|
|
113
|
+
function getLatestAssistantText(sessionId) {
|
|
114
|
+
const filePath = findConversationFile(sessionId);
|
|
115
|
+
if (!filePath) return '';
|
|
116
|
+
|
|
117
|
+
try {
|
|
118
|
+
const fileData = fs.readFileSync(filePath, 'utf-8');
|
|
119
|
+
const lines = fileData.trim().split('\n');
|
|
120
|
+
|
|
121
|
+
// Parse backwards to find the most recent assistant text
|
|
122
|
+
for (let i = lines.length - 1; i >= 0; i--) {
|
|
123
|
+
try {
|
|
124
|
+
const entry = JSON.parse(lines[i]);
|
|
125
|
+
if (entry.type === 'assistant' && Array.isArray(entry.message?.content)) {
|
|
126
|
+
for (const block of entry.message.content) {
|
|
127
|
+
if (block.type === 'text' && block.text) {
|
|
128
|
+
return block.text;
|
|
129
|
+
}
|
|
130
|
+
}
|
|
131
|
+
}
|
|
132
|
+
} catch (e) {
|
|
133
|
+
// Skip malformed lines
|
|
134
|
+
}
|
|
135
|
+
}
|
|
136
|
+
} catch (e) {
|
|
137
|
+
// File read error
|
|
138
|
+
}
|
|
139
|
+
return '';
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
/**
|
|
143
|
+
* Get session metadata (repo name and first prompt) from conversation
|
|
144
|
+
* @param {string} sessionId - Session ID
|
|
145
|
+
* @returns {Object} Metadata with repoName and firstPrompt
|
|
146
|
+
*/
|
|
147
|
+
function getSessionMetadata(sessionId) {
|
|
148
|
+
const filePath = findConversationFile(sessionId);
|
|
149
|
+
if (!filePath) return { repoName: null, firstPrompt: null };
|
|
150
|
+
|
|
151
|
+
try {
|
|
152
|
+
// Extract repo name from path
|
|
153
|
+
const parts = filePath.split(path.sep);
|
|
154
|
+
const projectsIdx = parts.indexOf('projects');
|
|
155
|
+
const repoName = projectsIdx >= 0 && parts[projectsIdx + 1] ? parts[projectsIdx + 1] : null;
|
|
156
|
+
|
|
157
|
+
// Find first user prompt
|
|
158
|
+
const fileData = fs.readFileSync(filePath, 'utf-8');
|
|
159
|
+
const lines = fileData.trim().split('\n');
|
|
160
|
+
let firstPrompt = null;
|
|
161
|
+
|
|
162
|
+
for (const line of lines) {
|
|
163
|
+
try {
|
|
164
|
+
const entry = JSON.parse(line);
|
|
165
|
+
if (entry.type === 'user' && entry.message?.content) {
|
|
166
|
+
const content = entry.message.content;
|
|
167
|
+
if (typeof content === 'string') {
|
|
168
|
+
firstPrompt = content.substring(0, 200);
|
|
169
|
+
} else if (Array.isArray(content)) {
|
|
170
|
+
for (const block of content) {
|
|
171
|
+
if (block.type === 'text' && block.text) {
|
|
172
|
+
firstPrompt = block.text.substring(0, 200);
|
|
173
|
+
break;
|
|
174
|
+
}
|
|
175
|
+
}
|
|
176
|
+
}
|
|
177
|
+
if (firstPrompt) break;
|
|
178
|
+
}
|
|
179
|
+
} catch (e) {
|
|
180
|
+
// Skip malformed lines
|
|
181
|
+
}
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
return { repoName, firstPrompt };
|
|
185
|
+
} catch (e) {
|
|
186
|
+
return { repoName: null, firstPrompt: null };
|
|
187
|
+
}
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
module.exports = {
|
|
191
|
+
findConversationFile,
|
|
192
|
+
loadConversation,
|
|
193
|
+
getLatestAssistantText,
|
|
194
|
+
getSessionMetadata
|
|
195
|
+
};
|