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.
- package/README.md +25 -1
- package/bin/cli.js +2 -2
- package/lib/core/claude-events.js +308 -0
- package/lib/core/config.js +79 -0
- package/lib/core/event-bus.js +127 -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/plans.js +247 -0
- package/lib/data/tasks.js +263 -0
- package/lib/data/todos.js +205 -0
- package/lib/index.js +400 -0
- package/lib/routes/api.js +611 -0
- package/lib/server.js +778 -84
- package/lib/sessions/manager.js +870 -0
- package/package.json +11 -4
- package/public/index.html +6765 -0
- package/screenshots/activity-feed.png +0 -0
- package/screenshots/create-task.png +0 -0
- package/screenshots/task-detail.png +0 -0
- package/screenshots/task-list.png +0 -0
- package/screenshots/task-status.png +0 -0
|
@@ -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
|
+
};
|