@stevederico/dotbot 0.16.0

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.
Files changed (52) hide show
  1. package/CHANGELOG.md +136 -0
  2. package/README.md +380 -0
  3. package/bin/dotbot.js +461 -0
  4. package/core/agent.js +779 -0
  5. package/core/compaction.js +261 -0
  6. package/core/cron_handler.js +262 -0
  7. package/core/events.js +229 -0
  8. package/core/failover.js +193 -0
  9. package/core/gptoss_tool_parser.js +173 -0
  10. package/core/init.js +154 -0
  11. package/core/normalize.js +324 -0
  12. package/core/trigger_handler.js +148 -0
  13. package/docs/core.md +103 -0
  14. package/docs/protected-files.md +59 -0
  15. package/examples/sqlite-session-example.js +69 -0
  16. package/index.js +341 -0
  17. package/observer/index.js +164 -0
  18. package/package.json +42 -0
  19. package/storage/CronStore.js +145 -0
  20. package/storage/EventStore.js +71 -0
  21. package/storage/MemoryStore.js +175 -0
  22. package/storage/MongoAdapter.js +291 -0
  23. package/storage/MongoCronAdapter.js +347 -0
  24. package/storage/MongoTaskAdapter.js +242 -0
  25. package/storage/MongoTriggerAdapter.js +158 -0
  26. package/storage/SQLiteAdapter.js +382 -0
  27. package/storage/SQLiteCronAdapter.js +562 -0
  28. package/storage/SQLiteEventStore.js +300 -0
  29. package/storage/SQLiteMemoryAdapter.js +240 -0
  30. package/storage/SQLiteTaskAdapter.js +419 -0
  31. package/storage/SQLiteTriggerAdapter.js +262 -0
  32. package/storage/SessionStore.js +149 -0
  33. package/storage/TaskStore.js +100 -0
  34. package/storage/TriggerStore.js +90 -0
  35. package/storage/cron_constants.js +48 -0
  36. package/storage/index.js +21 -0
  37. package/tools/appgen.js +311 -0
  38. package/tools/browser.js +634 -0
  39. package/tools/code.js +101 -0
  40. package/tools/events.js +145 -0
  41. package/tools/files.js +201 -0
  42. package/tools/images.js +253 -0
  43. package/tools/index.js +97 -0
  44. package/tools/jobs.js +159 -0
  45. package/tools/memory.js +332 -0
  46. package/tools/messages.js +135 -0
  47. package/tools/notify.js +42 -0
  48. package/tools/tasks.js +404 -0
  49. package/tools/triggers.js +159 -0
  50. package/tools/weather.js +82 -0
  51. package/tools/web.js +283 -0
  52. package/utils/providers.js +136 -0
@@ -0,0 +1,135 @@
1
+ /**
2
+ * Message/conversation tools
3
+ */
4
+ export const messageTools = [
5
+ {
6
+ name: "message_list",
7
+ description: "List the user's message conversations.",
8
+ parameters: {
9
+ type: "object",
10
+ properties: {},
11
+ },
12
+ execute: async (input, signal, context) => {
13
+ if (!context?.databaseManager) return "Error: database not available";
14
+ try {
15
+ const { databaseManager, dbConfig, userID } = context;
16
+ const conversations = await databaseManager.findConversations(
17
+ dbConfig.dbType, dbConfig.db, dbConfig.connectionString, userID
18
+ );
19
+ if (conversations.length === 0) return "No conversations found.";
20
+ return conversations.map(c =>
21
+ `- ${c.name || c.recipient || 'Unknown'} | ${c.lastMessage?.slice(0, 60) || 'No messages'} | ${c.updatedAt || c.createdAt || ''}`
22
+ ).join('\n');
23
+ } catch (err) {
24
+ return `Error listing conversations: ${err.message}`;
25
+ }
26
+ },
27
+ },
28
+
29
+ {
30
+ name: "message_send",
31
+ description: "Send a message in a conversation. Creates a new conversation if one doesn't exist with the recipient.",
32
+ parameters: {
33
+ type: "object",
34
+ properties: {
35
+ recipient: { type: "string", description: "Recipient name or ID" },
36
+ content: { type: "string", description: "Message content" },
37
+ },
38
+ required: ["recipient", "content"],
39
+ },
40
+ execute: async (input, signal, context) => {
41
+ if (!context?.databaseManager) return "Error: database not available";
42
+ try {
43
+ const { databaseManager, dbConfig, userID } = context;
44
+ const conversations = await databaseManager.findConversations(
45
+ dbConfig.dbType, dbConfig.db, dbConfig.connectionString, userID
46
+ );
47
+ let conv = conversations.find(c =>
48
+ (c.name || c.recipient || '').toLowerCase() === input.recipient.toLowerCase()
49
+ );
50
+ if (!conv) {
51
+ conv = await databaseManager.createConversation(
52
+ dbConfig.dbType, dbConfig.db, dbConfig.connectionString, userID,
53
+ { name: input.recipient, recipient: input.recipient, type: 'direct' }
54
+ );
55
+ }
56
+ const convId = conv.id || conv._id?.toString();
57
+ await databaseManager.createMessage(
58
+ dbConfig.dbType, dbConfig.db, dbConfig.connectionString, convId, userID,
59
+ { content: input.content, sender: 'user', role: 'user' }
60
+ );
61
+ return `Message sent to ${input.recipient}.`;
62
+ } catch (err) {
63
+ return `Error sending message: ${err.message}`;
64
+ }
65
+ },
66
+ },
67
+
68
+ {
69
+ name: "message_delete",
70
+ description: "Delete an entire conversation with a specific person.",
71
+ parameters: {
72
+ type: "object",
73
+ properties: {
74
+ recipient: { type: "string", description: "Person's name whose conversation to delete" },
75
+ },
76
+ required: ["recipient"],
77
+ },
78
+ execute: async (input, signal, context) => {
79
+ if (!context?.databaseManager) return "Error: database not available";
80
+ try {
81
+ const { databaseManager, dbConfig, userID } = context;
82
+ const conversations = await databaseManager.findConversations(
83
+ dbConfig.dbType, dbConfig.db, dbConfig.connectionString, userID
84
+ );
85
+ const conv = conversations.find(c =>
86
+ (c.name || c.recipient || '').toLowerCase() === input.recipient.toLowerCase()
87
+ );
88
+ if (!conv) return `No conversation found with ${input.recipient}.`;
89
+ const convId = conv.id || conv._id?.toString();
90
+ await databaseManager.deleteConversation(
91
+ dbConfig.dbType, dbConfig.db, dbConfig.connectionString, convId, userID
92
+ );
93
+ return `Conversation with ${input.recipient} deleted.`;
94
+ } catch (err) {
95
+ return `Error deleting conversation: ${err.message}`;
96
+ }
97
+ },
98
+ },
99
+
100
+ {
101
+ name: "message_read",
102
+ description: "Read messages in a conversation with a specific person.",
103
+ parameters: {
104
+ type: "object",
105
+ properties: {
106
+ recipient: { type: "string", description: "Person's name to read messages with" },
107
+ limit: { type: "number", description: "Max messages to return (default 20)" },
108
+ },
109
+ required: ["recipient"],
110
+ },
111
+ execute: async (input, signal, context) => {
112
+ if (!context?.databaseManager) return "Error: database not available";
113
+ try {
114
+ const { databaseManager, dbConfig, userID } = context;
115
+ const conversations = await databaseManager.findConversations(
116
+ dbConfig.dbType, dbConfig.db, dbConfig.connectionString, userID
117
+ );
118
+ const conv = conversations.find(c =>
119
+ (c.name || c.recipient || '').toLowerCase() === input.recipient.toLowerCase()
120
+ );
121
+ if (!conv) return `No conversation found with ${input.recipient}.`;
122
+ const convId = conv.id || conv._id?.toString();
123
+ const messages = await databaseManager.findMessages(
124
+ dbConfig.dbType, dbConfig.db, dbConfig.connectionString, convId, userID, input.limit || 20
125
+ );
126
+ if (!messages || messages.length === 0) return `No messages with ${input.recipient}.`;
127
+ return messages.map(m =>
128
+ `[${m.sender || m.role || 'unknown'}] ${m.content}${m.createdAt ? ' — ' + new Date(m.createdAt).toLocaleString() : ''}`
129
+ ).join('\n');
130
+ } catch (err) {
131
+ return `Error reading messages: ${err.message}`;
132
+ }
133
+ },
134
+ },
135
+ ];
@@ -0,0 +1,42 @@
1
+ /**
2
+ * User notification tool
3
+ */
4
+ export const notifyTools = [
5
+ {
6
+ name: "notify_user",
7
+ description:
8
+ "Send a notification to the user. Use this during heartbeat or scheduled tasks to proactively inform the user of something useful. The notification appears in their notification center.",
9
+ parameters: {
10
+ type: "object",
11
+ properties: {
12
+ title: {
13
+ type: "string",
14
+ description: "Short notification title, e.g. 'Weather Alert' or 'Task Reminder'",
15
+ },
16
+ body: {
17
+ type: "string",
18
+ description: "Notification body text with the details",
19
+ },
20
+ type: {
21
+ type: "string",
22
+ description: "Notification type: 'info', 'reminder', 'alert', or 'heartbeat'. Defaults to 'info'.",
23
+ },
24
+ },
25
+ required: ["title", "body"],
26
+ },
27
+ execute: async (input, signal, context) => {
28
+ if (!context?.databaseManager) return "Error: database not available";
29
+ try {
30
+ const { databaseManager, dbConfig, userID } = context;
31
+ await databaseManager.createNotification(
32
+ dbConfig.dbType, dbConfig.db, dbConfig.connectionString,
33
+ userID,
34
+ { title: input.title, body: input.body, type: input.type || "info" }
35
+ );
36
+ return `Notification sent: "${input.title}"`;
37
+ } catch (err) {
38
+ return `Error sending notification: ${err.message}`;
39
+ }
40
+ },
41
+ },
42
+ ];
package/tools/tasks.js ADDED
@@ -0,0 +1,404 @@
1
+ /**
2
+ * Task Management Tools
3
+ *
4
+ * Multi-step autonomous task execution with progress tracking.
5
+ * Tasks can run in auto mode where steps execute sequentially via cron.
6
+ */
7
+
8
+ /** Delay (ms) before scheduling the next auto-mode step via cron. */
9
+ const AUTO_STEP_DELAY_MS = 5 * 1000; // 5 seconds between auto steps
10
+
11
+ export const taskTools = [
12
+ {
13
+ name: "task_create",
14
+ description:
15
+ "Create a new task with optional steps, priority, deadline, and category. " +
16
+ "Use mode='auto' for autonomous execution where steps run sequentially without user prompting.",
17
+ parameters: {
18
+ type: "object",
19
+ properties: {
20
+ description: {
21
+ type: "string",
22
+ description: "What the user wants to achieve",
23
+ },
24
+ steps: {
25
+ type: "array",
26
+ items: { type: "string" },
27
+ description: "Optional list of step descriptions to break the task into subtasks",
28
+ },
29
+ category: {
30
+ type: "string",
31
+ description: "Category: fitness, learning, productivity, creative, health, financial, personal, work, other. Default: general",
32
+ },
33
+ priority: {
34
+ type: "string",
35
+ description: "Priority level: low, medium, high, critical. Default: medium",
36
+ },
37
+ deadline: {
38
+ type: "string",
39
+ description: "Optional deadline as ISO date string, e.g. '2026-03-01'",
40
+ },
41
+ mode: {
42
+ type: "string",
43
+ enum: ["manual", "auto"],
44
+ description: "Execution mode: 'manual' (user-driven) or 'auto' (autonomous). Default: auto",
45
+ },
46
+ },
47
+ required: ["description"],
48
+ },
49
+ execute: async (input, signal, context) => {
50
+ if (!context?.taskStore) return "Error: taskStore not available";
51
+ try {
52
+ const task = await context.taskStore.createTask({
53
+ userId: context.userID,
54
+ description: input.description,
55
+ steps: input.steps || [],
56
+ category: input.category || 'general',
57
+ priority: input.priority || 'medium',
58
+ deadline: input.deadline || null,
59
+ mode: input.mode || 'auto',
60
+ });
61
+
62
+ const taskId = task.id || task._id?.toString();
63
+
64
+ return `Task created: "${input.description}" (ID: ${taskId})\n` +
65
+ `Mode: ${task.mode}, Priority: ${task.priority}, Steps: ${task.steps.length}` +
66
+ (task.mode === 'auto' && task.steps.length > 0 ?
67
+ `\n\nCall task_work with task_id "${taskId}" to start executing steps automatically.` : '');
68
+ } catch (err) {
69
+ return `Error creating task: ${err.message}`;
70
+ }
71
+ },
72
+ },
73
+
74
+ {
75
+ name: "task_list",
76
+ description: "List all tasks for the user, optionally filtered by status or category.",
77
+ parameters: {
78
+ type: "object",
79
+ properties: {
80
+ status: {
81
+ type: "string",
82
+ enum: ["pending", "in_progress", "completed"],
83
+ description: "Filter by status (optional)",
84
+ },
85
+ category: {
86
+ type: "string",
87
+ description: "Filter by category (optional)",
88
+ },
89
+ },
90
+ },
91
+ execute: async (input, signal, context) => {
92
+ if (!context?.taskStore) return "Error: taskStore not available";
93
+ try {
94
+ const filters = {};
95
+ if (input.status) filters.status = input.status;
96
+ if (input.category) filters.category = input.category;
97
+
98
+ const tasks = await context.taskStore.getTasks(context.userID, filters);
99
+
100
+ if (tasks.length === 0) {
101
+ return input.status || input.category
102
+ ? `No tasks found matching filters.`
103
+ : `No tasks yet. Create one with task_create.`;
104
+ }
105
+
106
+ return tasks.map((g, i) => {
107
+ const taskId = g.id || g._id?.toString();
108
+ const doneCount = g.steps?.filter(s => s.done).length || 0;
109
+ const totalSteps = g.steps?.length || 0;
110
+ const progress = totalSteps > 0 ? `${doneCount}/${totalSteps} steps` : 'No steps';
111
+ const status = g.status === 'completed' ? '✓' : g.status === 'in_progress' ? '▶' : '○';
112
+ return `${status} [${taskId}] ${g.description} [${g.priority}] - ${progress} (${g.progress}%)`;
113
+ }).join('\n');
114
+ } catch (err) {
115
+ return `Error listing tasks: ${err.message}`;
116
+ }
117
+ },
118
+ },
119
+
120
+ {
121
+ name: "task_plan",
122
+ description:
123
+ "Break down a task into detailed steps with action prompts. Use this to add or replace steps on an existing task.",
124
+ parameters: {
125
+ type: "object",
126
+ properties: {
127
+ task_id: { type: "string", description: "The task ID" },
128
+ steps: {
129
+ type: "array",
130
+ items: {
131
+ type: "object",
132
+ properties: {
133
+ text: { type: "string", description: "Human-readable step description" },
134
+ action: { type: "string", description: "Detailed action prompt for the agent" },
135
+ },
136
+ required: ["text"],
137
+ },
138
+ description: "Array of step objects with text and optional action prompts",
139
+ },
140
+ },
141
+ required: ["task_id", "steps"],
142
+ },
143
+ execute: async (input, signal, context) => {
144
+ if (!context?.taskStore) return "Error: taskStore not available";
145
+ try {
146
+ const task = await context.taskStore.getTask(context.userID, input.task_id);
147
+ if (!task) return "Task not found.";
148
+
149
+ const normalizedSteps = input.steps.map(s => ({
150
+ text: s.text,
151
+ action: s.action || s.text,
152
+ done: false,
153
+ result: null,
154
+ startedAt: null,
155
+ completedAt: null,
156
+ }));
157
+
158
+ await context.taskStore.updateTask(context.userID, input.task_id, {
159
+ steps: normalizedSteps,
160
+ mode: "auto",
161
+ status: "in_progress",
162
+ currentStep: 0,
163
+ });
164
+
165
+ const stepList = normalizedSteps.map((s, i) => ` ${i + 1}. ${s.text}`).join("\n");
166
+ return `Task planned with ${normalizedSteps.length} steps and set to auto mode:\n${stepList}\n\nCall task_work with task_id "${input.task_id}" to start executing the first step.`;
167
+ } catch (err) {
168
+ return `Error planning task: ${err.message}`;
169
+ }
170
+ },
171
+ },
172
+
173
+ {
174
+ name: "task_work",
175
+ description:
176
+ "Start executing the next pending step on a task. Returns the step's action prompt. " +
177
+ "After completing the action, call task_step_done to record the result.",
178
+ parameters: {
179
+ type: "object",
180
+ properties: {
181
+ task_id: { type: "string", description: "The task ID to work on" },
182
+ },
183
+ required: ["task_id"],
184
+ },
185
+ execute: async (input, signal, context) => {
186
+ if (!context?.taskStore) return "Error: taskStore not available";
187
+ try {
188
+ const task = await context.taskStore.getTask(context.userID, input.task_id);
189
+ if (!task) return "Task not found.";
190
+ if (!task.steps || task.steps.length === 0) {
191
+ return "Task has no steps. Use task_plan to add steps first.";
192
+ }
193
+
194
+ // Find next undone step
195
+ const stepIdx = task.steps.findIndex(s => !s.done);
196
+ if (stepIdx === -1) {
197
+ return "All steps are already complete. Use task_complete to finish the task.";
198
+ }
199
+
200
+ const step = task.steps[stepIdx];
201
+
202
+ // Mark step as started
203
+ const steps = [...task.steps];
204
+ steps[stepIdx] = { ...steps[stepIdx], startedAt: new Date().toISOString() };
205
+
206
+ await context.taskStore.updateTask(context.userID, input.task_id, {
207
+ steps,
208
+ currentStep: stepIdx,
209
+ lastWorkedAt: new Date().toISOString(),
210
+ status: "in_progress",
211
+ });
212
+
213
+ const progress = `Step ${stepIdx + 1} of ${task.steps.length}`;
214
+ return (
215
+ `[${progress}] "${step.text}"\n\n` +
216
+ `Action: ${step.action || step.text}\n\n` +
217
+ `Execute this action now, then call task_step_done with task_id "${input.task_id}" and a result summary.`
218
+ );
219
+ } catch (err) {
220
+ return `Error starting work: ${err.message}`;
221
+ }
222
+ },
223
+ },
224
+
225
+ {
226
+ name: "task_step_done",
227
+ description:
228
+ "Mark the current in-progress step as completed and record its result. " +
229
+ "If the task is in auto mode and more steps remain, schedules the next step via cron.",
230
+ parameters: {
231
+ type: "object",
232
+ properties: {
233
+ task_id: { type: "string", description: "The task ID" },
234
+ result: { type: "string", description: "Summary of what was accomplished in this step" },
235
+ },
236
+ required: ["task_id", "result"],
237
+ },
238
+ execute: async (input, signal, context) => {
239
+ if (!context?.taskStore) return "Error: taskStore not available";
240
+ try {
241
+ const task = await context.taskStore.getTask(context.userID, input.task_id);
242
+ if (!task) return "Task not found.";
243
+
244
+ // Find the current in-progress step
245
+ const stepIdx = task.steps.findIndex(s => s.startedAt && !s.done);
246
+ if (stepIdx === -1) return "No in-progress step found. Call task_work first.";
247
+
248
+ const steps = [...task.steps];
249
+ steps[stepIdx] = {
250
+ ...steps[stepIdx],
251
+ done: true,
252
+ result: input.result,
253
+ completedAt: new Date().toISOString(),
254
+ };
255
+
256
+ const nextStepIdx = steps.findIndex(s => !s.done);
257
+ const allDone = nextStepIdx === -1;
258
+
259
+ const updates = {
260
+ steps,
261
+ currentStep: allDone ? steps.length : nextStepIdx,
262
+ lastWorkedAt: new Date().toISOString(),
263
+ };
264
+
265
+ if (allDone) {
266
+ updates.status = "completed";
267
+ }
268
+
269
+ await context.taskStore.updateTask(context.userID, input.task_id, updates);
270
+
271
+ // In auto mode with remaining steps, schedule next step via cron
272
+ if (!allDone && task.mode === "auto" && context.cronStore) {
273
+ try {
274
+ await context.cronStore.createTask({
275
+ name: "task_step",
276
+ prompt: `Continue working on task ${input.task_id}. Execute the next pending step.`,
277
+ userId: context.userID,
278
+ sessionId: context.sessionId,
279
+ runAt: new Date(Date.now() + AUTO_STEP_DELAY_MS).toISOString(),
280
+ taskId: input.task_id,
281
+ });
282
+ } catch (err) {
283
+ console.error("[tasks] failed to schedule next step:", err.message);
284
+ }
285
+ }
286
+
287
+ const doneCount = steps.filter(s => s.done).length;
288
+ if (allDone) {
289
+ return `Step ${stepIdx + 1} completed. All ${steps.length} steps done — task marked as completed!`;
290
+ }
291
+ return (
292
+ `Step ${stepIdx + 1} completed (${doneCount}/${steps.length}).` +
293
+ (task.mode === "auto" ? " Next step scheduled automatically." : " Call task_work to continue.")
294
+ );
295
+ } catch (err) {
296
+ return `Error completing step: ${err.message}`;
297
+ }
298
+ },
299
+ },
300
+
301
+ {
302
+ name: "task_complete",
303
+ description: "Mark a task as completed.",
304
+ parameters: {
305
+ type: "object",
306
+ properties: {
307
+ task_id: { type: "string", description: "The task ID to complete" },
308
+ },
309
+ required: ["task_id"],
310
+ },
311
+ execute: async (input, signal, context) => {
312
+ if (!context?.taskStore) return "Error: taskStore not available";
313
+ try {
314
+ const result = await context.taskStore.updateTask(
315
+ context.userID,
316
+ input.task_id,
317
+ { status: "completed" }
318
+ );
319
+
320
+ if (result.modifiedCount > 0 || result.changes > 0) {
321
+ return `Task ${input.task_id} marked as completed.`;
322
+ }
323
+ return "Task not found.";
324
+ } catch (err) {
325
+ return `Error completing task: ${err.message}`;
326
+ }
327
+ },
328
+ },
329
+
330
+ {
331
+ name: "task_delete",
332
+ description: "Delete a task permanently.",
333
+ parameters: {
334
+ type: "object",
335
+ properties: {
336
+ task_id: { type: "string", description: "The task ID to delete" },
337
+ },
338
+ required: ["task_id"],
339
+ },
340
+ execute: async (input, signal, context) => {
341
+ if (!context?.taskStore) return "Error: taskStore not available";
342
+ try {
343
+ const result = await context.taskStore.deleteTask(context.userID, input.task_id);
344
+ return (result.deletedCount > 0 || result.changes > 0) ? `Task ${input.task_id} deleted.` : "Task not found.";
345
+ } catch (err) {
346
+ return `Error deleting task: ${err.message}`;
347
+ }
348
+ },
349
+ },
350
+
351
+ {
352
+ name: "task_search",
353
+ description: "Search tasks by text in description or steps.",
354
+ parameters: {
355
+ type: "object",
356
+ properties: {
357
+ query: { type: "string", description: "Search query" },
358
+ },
359
+ required: ["query"],
360
+ },
361
+ execute: async (input, signal, context) => {
362
+ if (!context?.taskStore) return "Error: taskStore not available";
363
+ try {
364
+ const tasks = await context.taskStore.searchTasks(context.userID, input.query);
365
+ if (tasks.length === 0) return `No tasks matching "${input.query}".`;
366
+ return tasks.map((g, i) => {
367
+ const progress = `${g.progress}%`;
368
+ return `${i + 1}. ${g.description} [${g.category}] - ${progress}`;
369
+ }).join('\n');
370
+ } catch (err) {
371
+ return `Error searching tasks: ${err.message}`;
372
+ }
373
+ },
374
+ },
375
+
376
+ {
377
+ name: "task_stats",
378
+ description: "Get task statistics (total, completed, in progress, by category, etc.).",
379
+ parameters: {
380
+ type: "object",
381
+ properties: {},
382
+ },
383
+ execute: async (input, signal, context) => {
384
+ if (!context?.taskStore) return "Error: taskStore not available";
385
+ try {
386
+ const stats = await context.taskStore.getTaskStats(context.userID);
387
+ let output = `Total: ${stats.total}, Pending: ${stats.pending}, In Progress: ${stats.in_progress}, Completed: ${stats.completed}`;
388
+ if (stats.overdue > 0) output += `, Overdue: ${stats.overdue}`;
389
+ if (Object.keys(stats.by_category).length > 0) {
390
+ output += `\n\nBy Category:\n`;
391
+ for (const [cat, count] of Object.entries(stats.by_category)) {
392
+ output += ` ${cat}: ${count}\n`;
393
+ }
394
+ }
395
+ return output;
396
+ } catch (err) {
397
+ return `Error getting stats: ${err.message}`;
398
+ }
399
+ },
400
+ },
401
+ ];
402
+
403
+ // Backwards compatibility: export goalTools as alias
404
+ export const goalTools = taskTools;