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.
@@ -0,0 +1,247 @@
1
+ /**
2
+ * Plans Data Module - Plan loading, updating, and session mapping
3
+ *
4
+ * Handles reading and writing plan files from the Claude plans directory.
5
+ */
6
+
7
+ const fs = require('fs');
8
+ const path = require('path');
9
+
10
+ const { PLANS_DIR, PROJECTS_DIR } = require('../core/config');
11
+ const { eventBus, EventTypes } = require('../core/event-bus');
12
+
13
+ // Cache for plan slug -> sessionId mapping
14
+ const planSessionCache = new Map();
15
+ let planCacheBuilt = false;
16
+
17
+ /**
18
+ * Build the plan->session mapping by scanning JSONL files
19
+ */
20
+ function buildPlanSessionCache() {
21
+ console.log(' Building plan-session mapping cache...');
22
+ const startTime = Date.now();
23
+ planSessionCache.clear();
24
+
25
+ if (!fs.existsSync(PLANS_DIR) || !fs.existsSync(PROJECTS_DIR)) {
26
+ planCacheBuilt = true;
27
+ return;
28
+ }
29
+
30
+ // Get all plan slugs
31
+ const planFiles = fs.readdirSync(PLANS_DIR).filter(f => f.endsWith('.md'));
32
+ const slugs = planFiles.map(f => f.replace('.md', ''));
33
+
34
+ // Search through project directories for slug references
35
+ const projectDirs = fs.readdirSync(PROJECTS_DIR);
36
+
37
+ for (const projectDir of projectDirs) {
38
+ const projectPath = path.join(PROJECTS_DIR, projectDir);
39
+ if (!fs.statSync(projectPath).isDirectory()) continue;
40
+
41
+ // Check main jsonl files
42
+ const jsonlFiles = fs.readdirSync(projectPath).filter(f => f.endsWith('.jsonl'));
43
+ for (const jsonlFile of jsonlFiles) {
44
+ try {
45
+ const content = fs.readFileSync(path.join(projectPath, jsonlFile), 'utf-8');
46
+ for (const slug of slugs) {
47
+ if (planSessionCache.has(slug)) continue;
48
+ if (content.includes(`"slug":"${slug}"`)) {
49
+ // Find the sessionId in this file
50
+ const lines = content.split('\n');
51
+ for (const line of lines) {
52
+ if (line.includes(`"slug":"${slug}"`)) {
53
+ try {
54
+ const entry = JSON.parse(line);
55
+ if (entry.sessionId) {
56
+ planSessionCache.set(slug, entry.sessionId);
57
+ break;
58
+ }
59
+ } catch (e) {}
60
+ }
61
+ }
62
+ }
63
+ }
64
+ } catch (e) {}
65
+ }
66
+
67
+ // Check subagents directory
68
+ const subagentsPath = path.join(projectPath, 'subagents');
69
+ if (fs.existsSync(subagentsPath)) {
70
+ const subagentFiles = fs.readdirSync(subagentsPath).filter(f => f.endsWith('.jsonl'));
71
+ for (const jsonlFile of subagentFiles) {
72
+ try {
73
+ const content = fs.readFileSync(path.join(subagentsPath, jsonlFile), 'utf-8');
74
+ for (const slug of slugs) {
75
+ if (planSessionCache.has(slug)) continue;
76
+ if (content.includes(`"slug":"${slug}"`)) {
77
+ const lines = content.split('\n');
78
+ for (const line of lines) {
79
+ if (line.includes(`"slug":"${slug}"`)) {
80
+ try {
81
+ const entry = JSON.parse(line);
82
+ if (entry.sessionId) {
83
+ planSessionCache.set(slug, entry.sessionId);
84
+ break;
85
+ }
86
+ } catch (e) {}
87
+ }
88
+ }
89
+ }
90
+ }
91
+ } catch (e) {}
92
+ }
93
+ }
94
+ }
95
+
96
+ planCacheBuilt = true;
97
+ console.log(` Plan cache built: ${planSessionCache.size} plans mapped in ${Date.now() - startTime}ms`);
98
+ }
99
+
100
+ /**
101
+ * Get session ID for a plan slug
102
+ * @param {string} slug - Plan slug
103
+ * @returns {string|null} Session ID or null
104
+ */
105
+ function getSessionForPlan(slug) {
106
+ if (!planCacheBuilt) buildPlanSessionCache();
107
+ return planSessionCache.get(slug) || null;
108
+ }
109
+
110
+ /**
111
+ * Load all plans with their session mappings
112
+ * @returns {Array} Array of plan objects
113
+ */
114
+ function loadAllPlans() {
115
+ if (!planCacheBuilt) buildPlanSessionCache();
116
+
117
+ const plans = [];
118
+
119
+ if (!fs.existsSync(PLANS_DIR)) {
120
+ return plans;
121
+ }
122
+
123
+ const planFiles = fs.readdirSync(PLANS_DIR).filter(f => f.endsWith('.md'));
124
+
125
+ for (const file of planFiles) {
126
+ try {
127
+ const slug = file.replace('.md', '');
128
+ const filepath = path.join(PLANS_DIR, file);
129
+ const stats = fs.statSync(filepath);
130
+ const content = fs.readFileSync(filepath, 'utf-8');
131
+
132
+ // Extract title from first heading
133
+ const titleMatch = content.match(/^#\s+(.+?)(?:\n|$)/m);
134
+ const title = titleMatch ? titleMatch[1].replace(/^Plan:\s*/i, '') : slug;
135
+
136
+ plans.push({
137
+ slug,
138
+ title,
139
+ sessionId: planSessionCache.get(slug) || null,
140
+ filepath,
141
+ size: stats.size,
142
+ modified: stats.mtime.toISOString(),
143
+ preview: content.substring(0, 200).replace(/\n/g, ' ').trim()
144
+ });
145
+ } catch (e) {
146
+ console.error(`Error loading plan ${file}:`, e.message);
147
+ }
148
+ }
149
+
150
+ // Sort by modified date, newest first
151
+ plans.sort((a, b) => new Date(b.modified) - new Date(a.modified));
152
+
153
+ return plans;
154
+ }
155
+
156
+ /**
157
+ * Get plans for a specific session
158
+ * @param {string} sessionId - Session ID
159
+ * @returns {Array} Array of plans for the session
160
+ */
161
+ function getPlansForSession(sessionId) {
162
+ const allPlans = loadAllPlans();
163
+ return allPlans.filter(p => p.sessionId === sessionId);
164
+ }
165
+
166
+ /**
167
+ * Get a single plan by slug
168
+ * @param {string} slug - Plan slug
169
+ * @returns {Object} Plan object or error
170
+ */
171
+ function getPlan(slug) {
172
+ const filepath = path.join(PLANS_DIR, `${slug}.md`);
173
+
174
+ if (!fs.existsSync(filepath)) {
175
+ return { error: 'Plan not found' };
176
+ }
177
+
178
+ try {
179
+ const content = fs.readFileSync(filepath, 'utf-8');
180
+ const stats = fs.statSync(filepath);
181
+ const titleMatch = content.match(/^#\s+(.+?)(?:\n|$)/m);
182
+ const title = titleMatch ? titleMatch[1].replace(/^Plan:\s*/i, '') : slug;
183
+
184
+ return {
185
+ slug,
186
+ title,
187
+ sessionId: getSessionForPlan(slug),
188
+ content,
189
+ modified: stats.mtime.toISOString()
190
+ };
191
+ } catch (e) {
192
+ return { error: e.message };
193
+ }
194
+ }
195
+
196
+ /**
197
+ * Update a plan's content
198
+ * @param {string} slug - Plan slug
199
+ * @param {string} content - New content
200
+ * @returns {Object} Result with success flag or error
201
+ */
202
+ function updatePlan(slug, content) {
203
+ const filepath = path.join(PLANS_DIR, `${slug}.md`);
204
+
205
+ if (!fs.existsSync(filepath)) {
206
+ return { error: 'Plan not found' };
207
+ }
208
+
209
+ try {
210
+ fs.writeFileSync(filepath, content);
211
+ const sessionId = getSessionForPlan(slug);
212
+
213
+ eventBus.emit(EventTypes.PLAN_UPDATED, { slug, sessionId });
214
+
215
+ return { success: true, slug };
216
+ } catch (e) {
217
+ return { error: e.message };
218
+ }
219
+ }
220
+
221
+ /**
222
+ * Check if plan session cache has been built
223
+ * @returns {boolean}
224
+ */
225
+ function isCacheBuilt() {
226
+ return planCacheBuilt;
227
+ }
228
+
229
+ /**
230
+ * Check if a plan slug is in the cache
231
+ * @param {string} slug - Plan slug
232
+ * @returns {boolean}
233
+ */
234
+ function hasPlanInCache(slug) {
235
+ return planSessionCache.has(slug);
236
+ }
237
+
238
+ module.exports = {
239
+ buildPlanSessionCache,
240
+ getSessionForPlan,
241
+ loadAllPlans,
242
+ getPlansForSession,
243
+ getPlan,
244
+ updatePlan,
245
+ isCacheBuilt,
246
+ hasPlanInCache
247
+ };
@@ -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
+ };