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.
@@ -0,0 +1,263 @@
1
+ /**
2
+ * Tasks Data Module - Task loading, updating, and creation
3
+ *
4
+ * Handles reading and writing task files from the Claude tasks directory.
5
+ */
6
+
7
+ const fs = require('fs');
8
+ const path = require('path');
9
+
10
+ const { TASKS_DIR } = require('../core/config');
11
+ const { eventBus, EventTypes } = require('../core/event-bus');
12
+
13
+ // Callbacks for enrichment (set by other modules)
14
+ let loadCustomNamesCallback = null;
15
+ let getSessionMetadataCallback = null;
16
+
17
+ /**
18
+ * Set callback for loading custom names
19
+ * @param {Function} callback
20
+ */
21
+ function setCustomNamesCallback(callback) {
22
+ loadCustomNamesCallback = callback;
23
+ }
24
+
25
+ /**
26
+ * Set callback for getting session metadata
27
+ * @param {Function} callback
28
+ */
29
+ function setMetadataCallback(callback) {
30
+ getSessionMetadataCallback = callback;
31
+ }
32
+
33
+ /**
34
+ * Load all tasks from all sessions
35
+ * @returns {Array} Array of session objects with tasks
36
+ */
37
+ function loadAllTasks() {
38
+ const sessions = [];
39
+
40
+ if (!fs.existsSync(TASKS_DIR)) {
41
+ return sessions;
42
+ }
43
+
44
+ const customNames = loadCustomNamesCallback ? loadCustomNamesCallback() : {};
45
+
46
+ const sessionDirs = fs.readdirSync(TASKS_DIR).filter(f => {
47
+ const fullPath = path.join(TASKS_DIR, f);
48
+ return fs.statSync(fullPath).isDirectory();
49
+ });
50
+
51
+ for (const sessionId of sessionDirs) {
52
+ const sessionPath = path.join(TASKS_DIR, sessionId);
53
+ const tasks = [];
54
+
55
+ const files = fs.readdirSync(sessionPath).filter(f => f.endsWith('.json'));
56
+ for (const file of files) {
57
+ try {
58
+ const content = fs.readFileSync(path.join(sessionPath, file), 'utf-8');
59
+ const task = JSON.parse(content);
60
+ task.sessionId = sessionId;
61
+ tasks.push(task);
62
+ } catch (e) {
63
+ console.error(`Error reading ${file}:`, e.message);
64
+ }
65
+ }
66
+
67
+ tasks.sort((a, b) => parseInt(a.id) - parseInt(b.id));
68
+
69
+ const metadata = getSessionMetadataCallback ? getSessionMetadataCallback(sessionId) : { repoName: null, firstPrompt: null };
70
+ const hasInProgress = tasks.some(t => t.status === 'in_progress');
71
+ const sessionStatus = hasInProgress ? 'working' : 'idle';
72
+
73
+ sessions.push({
74
+ sessionId,
75
+ tasks,
76
+ customName: customNames[sessionId] || null,
77
+ repoName: metadata.repoName,
78
+ firstPrompt: metadata.firstPrompt,
79
+ status: sessionStatus,
80
+ stats: {
81
+ total: tasks.length,
82
+ pending: tasks.filter(t => t.status === 'pending').length,
83
+ in_progress: tasks.filter(t => t.status === 'in_progress').length,
84
+ completed: tasks.filter(t => t.status === 'completed').length
85
+ }
86
+ });
87
+ }
88
+
89
+ sessions.sort((a, b) => b.tasks.length - a.tasks.length);
90
+ return sessions;
91
+ }
92
+
93
+ /**
94
+ * Get tasks for a specific session
95
+ * @param {string} sessionId - Session ID
96
+ * @returns {Object} Session object with tasks
97
+ */
98
+ function getTasksForSession(sessionId) {
99
+ const sessionPath = path.join(TASKS_DIR, sessionId);
100
+
101
+ if (!fs.existsSync(sessionPath)) {
102
+ return null;
103
+ }
104
+
105
+ const tasks = [];
106
+ const files = fs.readdirSync(sessionPath).filter(f => f.endsWith('.json'));
107
+
108
+ for (const file of files) {
109
+ try {
110
+ const content = fs.readFileSync(path.join(sessionPath, file), 'utf-8');
111
+ const task = JSON.parse(content);
112
+ task.sessionId = sessionId;
113
+ tasks.push(task);
114
+ } catch (e) {
115
+ console.error(`Error reading ${file}:`, e.message);
116
+ }
117
+ }
118
+
119
+ tasks.sort((a, b) => parseInt(a.id) - parseInt(b.id));
120
+
121
+ return {
122
+ sessionId,
123
+ tasks,
124
+ stats: {
125
+ total: tasks.length,
126
+ pending: tasks.filter(t => t.status === 'pending').length,
127
+ in_progress: tasks.filter(t => t.status === 'in_progress').length,
128
+ completed: tasks.filter(t => t.status === 'completed').length
129
+ }
130
+ };
131
+ }
132
+
133
+ /**
134
+ * Update a task
135
+ * @param {string} sessionId - Session ID
136
+ * @param {string} taskId - Task ID
137
+ * @param {Object} updates - Updates to apply
138
+ * @returns {Object} Result with success flag or error
139
+ */
140
+ function updateTask(sessionId, taskId, updates) {
141
+ const taskPath = path.join(TASKS_DIR, sessionId, `${taskId}.json`);
142
+
143
+ if (!fs.existsSync(taskPath)) {
144
+ return { error: 'Task not found' };
145
+ }
146
+
147
+ try {
148
+ const content = fs.readFileSync(taskPath, 'utf-8');
149
+ const task = JSON.parse(content);
150
+ const previousStatus = task.status;
151
+
152
+ if (updates.status) task.status = updates.status;
153
+ if (updates.subject !== undefined) task.subject = updates.subject;
154
+ if (updates.description !== undefined) task.description = updates.description;
155
+ if (updates.activeForm !== undefined) task.activeForm = updates.activeForm;
156
+ if (updates.blocks !== undefined) task.blocks = updates.blocks;
157
+ if (updates.blockedBy !== undefined) task.blockedBy = updates.blockedBy;
158
+
159
+ fs.writeFileSync(taskPath, JSON.stringify(task, null, 2));
160
+
161
+ eventBus.emit(EventTypes.TASK_UPDATED, { sessionId, taskId, task, updates });
162
+
163
+ if (updates.status && updates.status !== previousStatus) {
164
+ eventBus.emit(EventTypes.TASK_STATUS_CHANGED, {
165
+ sessionId, taskId, status: updates.status, previousStatus, task
166
+ });
167
+ }
168
+
169
+ return { success: true, task };
170
+ } catch (e) {
171
+ return { error: e.message };
172
+ }
173
+ }
174
+
175
+ /**
176
+ * Bulk update multiple tasks
177
+ * @param {string} sessionId - Session ID
178
+ * @param {Array} taskIds - Array of task IDs
179
+ * @param {Object} updates - Updates to apply
180
+ * @returns {Object} Result with count of updated tasks
181
+ */
182
+ function bulkUpdateTasks(sessionId, taskIds, updates) {
183
+ let updated = 0;
184
+
185
+ for (const taskId of taskIds) {
186
+ const result = updateTask(sessionId, taskId, updates);
187
+ if (result.success) {
188
+ updated++;
189
+ }
190
+ }
191
+
192
+ return { ok: true, updated };
193
+ }
194
+
195
+ /**
196
+ * Create a new task
197
+ * @param {string} sessionId - Session ID
198
+ * @param {Object} taskData - Task data
199
+ * @returns {Object} Result with success flag and task
200
+ */
201
+ function createTask(sessionId, taskData) {
202
+ const sessionPath = path.join(TASKS_DIR, sessionId);
203
+
204
+ if (!fs.existsSync(sessionPath)) {
205
+ return { error: 'Session not found' };
206
+ }
207
+
208
+ try {
209
+ const files = fs.readdirSync(sessionPath).filter(f => f.endsWith('.json'));
210
+ const ids = files.map(f => parseInt(f.replace('.json', ''))).filter(n => !isNaN(n));
211
+ const nextId = ids.length > 0 ? Math.max(...ids) + 1 : 1;
212
+
213
+ const task = {
214
+ id: String(nextId),
215
+ subject: taskData.subject || 'New Task',
216
+ description: taskData.description || '',
217
+ activeForm: taskData.activeForm || '',
218
+ status: taskData.status || 'pending',
219
+ blocks: taskData.blocks || [],
220
+ blockedBy: taskData.blockedBy || []
221
+ };
222
+
223
+ fs.writeFileSync(path.join(sessionPath, `${nextId}.json`), JSON.stringify(task, null, 2));
224
+
225
+ eventBus.emit(EventTypes.TASK_CREATED, { sessionId, task });
226
+
227
+ return { success: true, task };
228
+ } catch (e) {
229
+ return { error: e.message };
230
+ }
231
+ }
232
+
233
+ /**
234
+ * Delete a task
235
+ * @param {string} sessionId - Session ID
236
+ * @param {string} taskId - Task ID
237
+ * @returns {Object} Result with success flag
238
+ */
239
+ function deleteTask(sessionId, taskId) {
240
+ const taskPath = path.join(TASKS_DIR, sessionId, `${taskId}.json`);
241
+
242
+ if (!fs.existsSync(taskPath)) {
243
+ return { error: 'Task not found' };
244
+ }
245
+
246
+ try {
247
+ fs.unlinkSync(taskPath);
248
+ return { success: true };
249
+ } catch (e) {
250
+ return { error: e.message };
251
+ }
252
+ }
253
+
254
+ module.exports = {
255
+ loadAllTasks,
256
+ getTasksForSession,
257
+ updateTask,
258
+ bulkUpdateTasks,
259
+ createTask,
260
+ deleteTask,
261
+ setCustomNamesCallback,
262
+ setMetadataCallback
263
+ };
@@ -0,0 +1,205 @@
1
+ /**
2
+ * Todos Data Module - Todo loading, updating, and creation
3
+ *
4
+ * Handles reading and writing todo files from the Claude todos directory.
5
+ */
6
+
7
+ const fs = require('fs');
8
+ const path = require('path');
9
+
10
+ const { TODOS_DIR } = require('../core/config');
11
+ const { eventBus, EventTypes } = require('../core/event-bus');
12
+
13
+ /**
14
+ * Load all todos from all sessions
15
+ * @returns {Object} Object mapping sessionId to todo data
16
+ */
17
+ function loadAllTodos() {
18
+ const todosBySession = {};
19
+
20
+ if (!fs.existsSync(TODOS_DIR)) {
21
+ return todosBySession;
22
+ }
23
+
24
+ try {
25
+ const files = fs.readdirSync(TODOS_DIR).filter(f => f.endsWith('.json'));
26
+
27
+ for (const file of files) {
28
+ try {
29
+ // Extract session ID from filename (format: {sessionId}-agent-{sessionId}.json)
30
+ const match = file.match(/^([a-f0-9-]+)-agent-/);
31
+ if (!match) continue;
32
+
33
+ const sessionId = match[1];
34
+ const content = fs.readFileSync(path.join(TODOS_DIR, file), 'utf-8');
35
+ const todos = JSON.parse(content);
36
+
37
+ // Skip empty arrays
38
+ if (!Array.isArray(todos) || todos.length === 0) continue;
39
+
40
+ // Add index-based IDs if not present
41
+ const todosWithIds = todos.map((todo, idx) => ({
42
+ ...todo,
43
+ id: todo.id || String(idx + 1)
44
+ }));
45
+
46
+ todosBySession[sessionId] = {
47
+ sessionId,
48
+ todos: todosWithIds,
49
+ stats: {
50
+ total: todosWithIds.length,
51
+ pending: todosWithIds.filter(t => t.status === 'pending').length,
52
+ in_progress: todosWithIds.filter(t => t.status === 'in_progress').length,
53
+ completed: todosWithIds.filter(t => t.status === 'completed').length
54
+ }
55
+ };
56
+ } catch (e) {
57
+ // Skip invalid files
58
+ }
59
+ }
60
+ } catch (e) {
61
+ console.error('Error loading todos:', e.message);
62
+ }
63
+
64
+ return todosBySession;
65
+ }
66
+
67
+ /**
68
+ * Get todos for a specific session
69
+ * @param {string} sessionId - Session ID
70
+ * @returns {Object} Session object with todos
71
+ */
72
+ function getTodosForSession(sessionId) {
73
+ const emptyResult = { sessionId, todos: [], stats: { total: 0, pending: 0, in_progress: 0, completed: 0 } };
74
+
75
+ if (!fs.existsSync(TODOS_DIR)) {
76
+ return emptyResult;
77
+ }
78
+
79
+ // Look for file matching the session ID pattern
80
+ const filename = `${sessionId}-agent-${sessionId}.json`;
81
+ const filepath = path.join(TODOS_DIR, filename);
82
+
83
+ if (!fs.existsSync(filepath)) {
84
+ return emptyResult;
85
+ }
86
+
87
+ try {
88
+ const content = fs.readFileSync(filepath, 'utf-8');
89
+ const todos = JSON.parse(content);
90
+
91
+ if (!Array.isArray(todos)) {
92
+ return emptyResult;
93
+ }
94
+
95
+ const todosWithIds = todos.map((todo, idx) => ({
96
+ ...todo,
97
+ id: todo.id || String(idx + 1)
98
+ }));
99
+
100
+ return {
101
+ sessionId,
102
+ todos: todosWithIds,
103
+ stats: {
104
+ total: todosWithIds.length,
105
+ pending: todosWithIds.filter(t => t.status === 'pending').length,
106
+ in_progress: todosWithIds.filter(t => t.status === 'in_progress').length,
107
+ completed: todosWithIds.filter(t => t.status === 'completed').length
108
+ }
109
+ };
110
+ } catch (e) {
111
+ return emptyResult;
112
+ }
113
+ }
114
+
115
+ /**
116
+ * Update a todo
117
+ * @param {string} sessionId - Session ID
118
+ * @param {number} todoIndex - Index of the todo
119
+ * @param {Object} updates - Updates to apply
120
+ * @returns {Object} Result with success flag or error
121
+ */
122
+ function updateTodo(sessionId, todoIndex, updates) {
123
+ const filename = `${sessionId}-agent-${sessionId}.json`;
124
+ const filepath = path.join(TODOS_DIR, filename);
125
+
126
+ if (!fs.existsSync(filepath)) {
127
+ return { error: 'Todo file not found' };
128
+ }
129
+
130
+ try {
131
+ const content = fs.readFileSync(filepath, 'utf-8');
132
+ const todos = JSON.parse(content);
133
+
134
+ if (!Array.isArray(todos) || todoIndex >= todos.length) {
135
+ return { error: 'Todo not found' };
136
+ }
137
+
138
+ const previousStatus = todos[todoIndex].status;
139
+
140
+ if (updates.status) todos[todoIndex].status = updates.status;
141
+ if (updates.content !== undefined) todos[todoIndex].content = updates.content;
142
+ if (updates.activeForm !== undefined) todos[todoIndex].activeForm = updates.activeForm;
143
+
144
+ fs.writeFileSync(filepath, JSON.stringify(todos, null, 2));
145
+
146
+ const todo = { ...todos[todoIndex], id: todos[todoIndex].id || String(todoIndex + 1) };
147
+
148
+ // Emit events
149
+ eventBus.emit(EventTypes.TODO_UPDATED, { sessionId, todoIndex, todo, updates });
150
+
151
+ if (updates.status && updates.status !== previousStatus) {
152
+ eventBus.emit(EventTypes.TODO_STATUS_CHANGED, {
153
+ sessionId, todoIndex, status: updates.status, previousStatus, todo
154
+ });
155
+ }
156
+
157
+ return { success: true, todo };
158
+ } catch (e) {
159
+ return { error: e.message };
160
+ }
161
+ }
162
+
163
+ /**
164
+ * Create a new todo
165
+ * @param {string} sessionId - Session ID
166
+ * @param {Object} todoData - Todo data
167
+ * @returns {Object} Result with success flag and todo
168
+ */
169
+ function createTodo(sessionId, todoData) {
170
+ const filename = `${sessionId}-agent-${sessionId}.json`;
171
+ const filepath = path.join(TODOS_DIR, filename);
172
+
173
+ try {
174
+ let todos = [];
175
+ if (fs.existsSync(filepath)) {
176
+ const content = fs.readFileSync(filepath, 'utf-8');
177
+ todos = JSON.parse(content);
178
+ if (!Array.isArray(todos)) todos = [];
179
+ }
180
+
181
+ const todo = {
182
+ content: todoData.content || 'New todo',
183
+ status: todoData.status || 'pending',
184
+ activeForm: todoData.activeForm || ''
185
+ };
186
+
187
+ todos.push(todo);
188
+ fs.writeFileSync(filepath, JSON.stringify(todos, null, 2));
189
+
190
+ const todoWithId = { ...todo, id: String(todos.length) };
191
+
192
+ eventBus.emit(EventTypes.TODO_CREATED, { sessionId, todo: todoWithId });
193
+
194
+ return { success: true, todo: todoWithId };
195
+ } catch (e) {
196
+ return { error: e.message };
197
+ }
198
+ }
199
+
200
+ module.exports = {
201
+ loadAllTodos,
202
+ getTodosForSession,
203
+ updateTodo,
204
+ createTodo
205
+ };