@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
package/tools/index.js ADDED
@@ -0,0 +1,97 @@
1
+ /**
2
+ * Tool Registry
3
+ *
4
+ * Central registry for agent tools with dynamic registration API.
5
+ */
6
+
7
+ import { memoryTools } from './memory.js';
8
+ import { webTools } from './web.js';
9
+ import { codeTools } from './code.js';
10
+ import { fileTools } from './files.js';
11
+ import { messageTools } from './messages.js';
12
+ import { imageTools } from './images.js';
13
+ import { weatherTools } from './weather.js';
14
+ import { notifyTools } from './notify.js';
15
+ import { browserTools, createBrowserTools } from './browser.js';
16
+ import { taskTools, goalTools } from './tasks.js';
17
+ import { triggerTools } from './triggers.js';
18
+ import { jobTools, cronTools } from './jobs.js';
19
+ import { eventTools } from './events.js';
20
+ import { appgenTools } from './appgen.js';
21
+
22
+ /**
23
+ * Core tools included in the library by default
24
+ */
25
+ export const coreTools = [
26
+ ...memoryTools,
27
+ ...webTools,
28
+ ...codeTools,
29
+ ...fileTools,
30
+ ...messageTools,
31
+ ...imageTools,
32
+ ...weatherTools,
33
+ ...notifyTools,
34
+ ...browserTools,
35
+ ...taskTools,
36
+ ...triggerTools,
37
+ ...jobTools,
38
+ ...eventTools,
39
+ ...appgenTools,
40
+ ];
41
+
42
+ /**
43
+ * Create a tool registry with dynamic registration
44
+ *
45
+ * @returns {Object} Tool registry with register/getAll methods
46
+ */
47
+ export function createToolRegistry() {
48
+ const tools = [];
49
+
50
+ return {
51
+ /**
52
+ * Register one or more tools
53
+ *
54
+ * @param {...Object} newTools - Tool definitions to register
55
+ */
56
+ register(...newTools) {
57
+ tools.push(...newTools);
58
+ },
59
+
60
+ /**
61
+ * Get all registered tools
62
+ *
63
+ * @returns {Array} All registered tools
64
+ */
65
+ getAll() {
66
+ return tools;
67
+ },
68
+
69
+ /**
70
+ * Clear all registered tools
71
+ */
72
+ clear() {
73
+ tools.length = 0;
74
+ },
75
+ };
76
+ }
77
+
78
+ // Re-export individual tool arrays for direct access
79
+ export {
80
+ memoryTools,
81
+ webTools,
82
+ codeTools,
83
+ fileTools,
84
+ messageTools,
85
+ imageTools,
86
+ weatherTools,
87
+ notifyTools,
88
+ browserTools,
89
+ createBrowserTools,
90
+ taskTools,
91
+ goalTools, // backwards compatibility alias
92
+ triggerTools,
93
+ jobTools,
94
+ cronTools, // backwards compatibility alias
95
+ eventTools,
96
+ appgenTools,
97
+ };
package/tools/jobs.js ADDED
@@ -0,0 +1,159 @@
1
+ /**
2
+ * Job Tools
3
+ *
4
+ * Agent tools for scheduling and managing jobs (time-triggered scheduled prompts).
5
+ * Each tool receives (input, signal, context) where context.cronStore
6
+ * provides the storage backend.
7
+ */
8
+
9
+ import { parseInterval } from '../storage/cron_constants.js';
10
+
11
+ /**
12
+ * Agent tools for scheduling and managing jobs
13
+ * @type {Array<{name: string, description: string, parameters: Object, execute: Function}>}
14
+ */
15
+ export const jobTools = [
16
+ {
17
+ name: "schedule_job",
18
+ description:
19
+ "Schedule a job to run later or on a recurring basis. The job will send a message to you (the agent) at the scheduled time, and you will process it like a normal user message. Use this for reminders, periodic checks, daily summaries, etc.",
20
+ parameters: {
21
+ type: "object",
22
+ properties: {
23
+ name: {
24
+ type: "string",
25
+ description: "Short name for the job. e.g. 'daily-news-summary', 'remind-standup'",
26
+ },
27
+ prompt: {
28
+ type: "string",
29
+ description:
30
+ "The message that will be sent to you when the job fires. Write it as if the user is asking you to do something. e.g. 'Give me a summary of today\\'s top tech news' or 'Remind me about the standup meeting in 10 minutes'",
31
+ },
32
+ run_at: {
33
+ type: "string",
34
+ description:
35
+ "When to run. ISO 8601 datetime string. e.g. '2025-01-15T09:00:00' for a specific time.",
36
+ },
37
+ interval: {
38
+ type: "string",
39
+ description:
40
+ "For recurring jobs: interval between runs. e.g. '30m' (30 minutes), '2h' (2 hours), '1d' (daily), '1w' (weekly). Omit for one-shot jobs.",
41
+ },
42
+ },
43
+ required: ["name", "prompt", "run_at"],
44
+ },
45
+ execute: async (input, signal, context) => {
46
+ if (!context?.cronStore) return "Error: cron store not available";
47
+ try {
48
+ const { sessionId, userID } = context || {};
49
+ const intervalMs = input.interval ? parseInterval(input.interval) : null;
50
+ const recurring = !!intervalMs;
51
+
52
+ const task = await context.cronStore.createTask({
53
+ name: input.name,
54
+ prompt: input.prompt,
55
+ sessionId: sessionId || 'default',
56
+ userId: userID,
57
+ runAt: input.run_at,
58
+ intervalMs,
59
+ recurring,
60
+ });
61
+
62
+ const desc = recurring
63
+ ? `Recurring job "${input.name}" scheduled starting ${input.run_at}, repeating every ${input.interval}`
64
+ : `One-time job "${input.name}" scheduled for ${input.run_at}`;
65
+
66
+ return desc;
67
+ } catch (err) {
68
+ return `Error scheduling job: ${err.message}`;
69
+ }
70
+ },
71
+ },
72
+
73
+ {
74
+ name: "list_jobs",
75
+ description: "List all scheduled jobs (active and completed).",
76
+ parameters: {
77
+ type: "object",
78
+ properties: {},
79
+ },
80
+ execute: async (input, signal, context) => {
81
+ if (!context?.cronStore) return "Error: cron store not available";
82
+ try {
83
+ const tasks = await context.cronStore.listTasks();
84
+
85
+ if (tasks.length === 0) {
86
+ return "No scheduled jobs.";
87
+ }
88
+
89
+ return tasks
90
+ .map(
91
+ (t) =>
92
+ `- ${t.name} [${t.enabled ? "active" : "done"}]` +
93
+ `\n prompt: "${t.prompt}"` +
94
+ `\n next: ${t.nextRunAt?.toISOString?.() || t.nextRunAt || "n/a"}` +
95
+ (t.recurring ? `\n repeats every ${t.intervalMs / 60000}m` : "") +
96
+ (t.lastRunAt ? `\n last ran: ${t.lastRunAt.toISOString?.() || t.lastRunAt}` : "")
97
+ )
98
+ .join("\n\n");
99
+ } catch (err) {
100
+ return `Error listing jobs: ${err.message}`;
101
+ }
102
+ },
103
+ },
104
+
105
+ {
106
+ name: "toggle_job",
107
+ description: "Enable or disable a scheduled job without deleting it.",
108
+ parameters: {
109
+ type: "object",
110
+ properties: {
111
+ job_id: { type: "string", description: "The job ID to toggle" },
112
+ enabled: { type: "boolean", description: "true to enable, false to disable" },
113
+ },
114
+ required: ["job_id", "enabled"],
115
+ },
116
+ execute: async (input, signal, context) => {
117
+ if (!context?.cronStore) return "Error: cron store not available";
118
+ try {
119
+ const task = await context.cronStore.getTask(input.job_id);
120
+ if (!task) return `Error: job ${input.job_id} not found.`;
121
+ if (task.name === 'heartbeat') return "Error: cannot modify system jobs.";
122
+ await context.cronStore.toggleTask(input.job_id, input.enabled);
123
+ return `Job ${input.job_id} ${input.enabled ? 'enabled' : 'disabled'}.`;
124
+ } catch (err) {
125
+ return `Error toggling job: ${err.message}`;
126
+ }
127
+ },
128
+ },
129
+
130
+ {
131
+ name: "cancel_job",
132
+ description: "Cancel/delete a scheduled job by its ID.",
133
+ parameters: {
134
+ type: "object",
135
+ properties: {
136
+ job_id: {
137
+ type: "string",
138
+ description: "The job ID to cancel",
139
+ },
140
+ },
141
+ required: ["job_id"],
142
+ },
143
+ execute: async (input, signal, context) => {
144
+ if (!context?.cronStore) return "Error: cron store not available";
145
+ try {
146
+ const task = await context.cronStore.getTask(input.job_id);
147
+ if (!task) return `Error: job ${input.job_id} not found.`;
148
+ if (task.name === 'heartbeat') return "Error: cannot delete system jobs.";
149
+ await context.cronStore.deleteTask(input.job_id);
150
+ return `Job ${input.job_id} cancelled.`;
151
+ } catch (err) {
152
+ return `Error cancelling job: ${err.message}`;
153
+ }
154
+ },
155
+ },
156
+ ];
157
+
158
+ // Backwards compatibility alias
159
+ export const cronTools = jobTools;
@@ -0,0 +1,332 @@
1
+ // agent/memory.js
2
+ // Agent memory tools that write to the shared Memory collection via memoryStore.
3
+ // Uses SQLiteMemoryStore or any compatible store implementation.
4
+
5
+ /**
6
+ * Generate a slug-style key from content text.
7
+ * Takes the first ~50 characters, strips non-alphanumeric chars, and joins with underscores.
8
+ *
9
+ * @param {string} content - Raw content string
10
+ * @returns {string} Cleaned key like "users_favorite_color_is_blue"
11
+ */
12
+ function generateMemoryKey(content) {
13
+ return content
14
+ .slice(0, 50)
15
+ .replace(/[^a-zA-Z0-9 ]/g, "")
16
+ .trim()
17
+ .replace(/\s+/g, "_")
18
+ .toLowerCase();
19
+ }
20
+
21
+ // ── Tool definitions for the agent ──
22
+
23
+ export const memoryTools = [
24
+ {
25
+ name: "memory_save",
26
+ description:
27
+ "Save an important fact, preference, or piece of information to long-term memory. Use this when the user tells you something worth remembering for future conversations — their name, preferences, projects, goals, important dates, etc. Be selective: only save things that would be useful to recall later.",
28
+ parameters: {
29
+ type: "object",
30
+ properties: {
31
+ content: {
32
+ type: "string",
33
+ description:
34
+ "The information to remember. Write it as a clear, standalone fact. e.g. 'User's name is Steve. He is a full-stack developer based in SF.'",
35
+ },
36
+ tags: {
37
+ type: "array",
38
+ items: { type: "string" },
39
+ description:
40
+ "Short tags for categorization. e.g. ['personal', 'name'] or ['project', 'dottie']",
41
+ },
42
+ },
43
+ required: ["content"],
44
+ },
45
+ /**
46
+ * Save a memory via memoryStore.writeMemory.
47
+ *
48
+ * @param {Object} input - Tool input with content and optional tags
49
+ * @param {AbortSignal} signal - Abort signal
50
+ * @param {Object} context - { userID, memoryStore }
51
+ * @returns {Promise<string>} Confirmation message
52
+ */
53
+ execute: async (input, signal, context) => {
54
+ try {
55
+ const { userID, memoryStore } = context;
56
+ if (!memoryStore) {
57
+ return "Error: memoryStore not configured. Memory features are disabled.";
58
+ }
59
+ const key = generateMemoryKey(input.content);
60
+ const value = {
61
+ content: input.content,
62
+ tags: input.tags || [],
63
+ source: "agent",
64
+ };
65
+ await memoryStore.writeMemory(userID, key, value, "agent");
66
+ return `Saved to memory: "${input.content}"`;
67
+ } catch (err) {
68
+ return `Error saving memory: ${err.message}`;
69
+ }
70
+ },
71
+ },
72
+
73
+ {
74
+ name: "memory_search",
75
+ description:
76
+ "Search long-term memory for previously saved information. Use this when the user references something from a past conversation, asks 'do you remember...', or when context from past interactions would help you give a better answer.",
77
+ parameters: {
78
+ type: "object",
79
+ properties: {
80
+ query: {
81
+ type: "string",
82
+ description:
83
+ "What to search for in memory. Use keywords related to the topic.",
84
+ },
85
+ },
86
+ required: ["query"],
87
+ },
88
+ /**
89
+ * Search memories via memoryStore.readMemoryPattern and filter by query.
90
+ *
91
+ * @param {Object} input - Tool input with query string
92
+ * @param {AbortSignal} signal - Abort signal
93
+ * @param {Object} context - { userID, memoryStore }
94
+ * @returns {Promise<string>} Formatted search results or "no matches" message
95
+ */
96
+ execute: async (input, signal, context) => {
97
+ try {
98
+ const { userID, memoryStore } = context;
99
+ if (!memoryStore) {
100
+ return "Error: memoryStore not configured. Memory features are disabled.";
101
+ }
102
+ const all = await memoryStore.readMemoryPattern(userID, ".*");
103
+
104
+ const query = input.query.toLowerCase();
105
+ const matches = all
106
+ .filter((m) => {
107
+ const valStr =
108
+ typeof m.value === "object"
109
+ ? JSON.stringify(m.value)
110
+ : String(m.value);
111
+ return (
112
+ m.key.toLowerCase().includes(query) ||
113
+ valStr.toLowerCase().includes(query)
114
+ );
115
+ })
116
+ .slice(0, 5);
117
+
118
+ if (matches.length === 0) {
119
+ return "No matching memories found.";
120
+ }
121
+
122
+ return matches
123
+ .map((m, i) => {
124
+ const content =
125
+ typeof m.value === "object" && m.value.content
126
+ ? m.value.content
127
+ : JSON.stringify(m.value);
128
+ const tags =
129
+ typeof m.value === "object" && m.value.tags?.length
130
+ ? `\n tags: ${m.value.tags.join(", ")}`
131
+ : "";
132
+ const saved = m.updated_at
133
+ ? `\n saved: ${new Date(m.updated_at).toISOString()}`
134
+ : "";
135
+ return `${i + 1}. ${content}${tags}${saved}`;
136
+ })
137
+ .join("\n\n");
138
+ } catch (err) {
139
+ return `Error searching memory: ${err.message}`;
140
+ }
141
+ },
142
+ },
143
+
144
+ {
145
+ name: "memory_delete",
146
+ description:
147
+ "Delete a specific memory by its key. Use this when the user asks to forget something or remove outdated information.",
148
+ parameters: {
149
+ type: "object",
150
+ properties: {
151
+ key: {
152
+ type: "string",
153
+ description: "The memory key to delete. Use memory_search first to find the key.",
154
+ },
155
+ },
156
+ required: ["key"],
157
+ },
158
+ /**
159
+ * Delete a memory via memoryStore.deleteMemory.
160
+ *
161
+ * @param {Object} input - Tool input with key
162
+ * @param {AbortSignal} signal - Abort signal
163
+ * @param {Object} context - { userID, memoryStore }
164
+ * @returns {Promise<string>} Confirmation message
165
+ */
166
+ execute: async (input, signal, context) => {
167
+ try {
168
+ const { userID, memoryStore } = context;
169
+ if (!memoryStore) {
170
+ return "Error: memoryStore not configured. Memory features are disabled.";
171
+ }
172
+ const result = await memoryStore.deleteMemory(userID, input.key);
173
+ return result.deletedCount > 0 ? `Memory "${input.key}" deleted.` : "Memory not found.";
174
+ } catch (err) {
175
+ return `Error deleting memory: ${err.message}`;
176
+ }
177
+ },
178
+ },
179
+
180
+ {
181
+ name: "memory_list",
182
+ description:
183
+ "List all memories saved in the knowledge graph. Returns all memory keys and their content. Use this when you need to see everything that's stored, or when the user asks 'what do you remember about me' or 'show me all my memories'.",
184
+ parameters: {
185
+ type: "object",
186
+ properties: {},
187
+ },
188
+ /**
189
+ * List all memories via memoryStore.readMemoryPattern.
190
+ *
191
+ * @param {Object} input - Tool input (empty)
192
+ * @param {AbortSignal} signal - Abort signal
193
+ * @param {Object} context - { userID, memoryStore }
194
+ * @returns {Promise<string>} Formatted list of all memories
195
+ */
196
+ execute: async (input, signal, context) => {
197
+ try {
198
+ const { userID, memoryStore } = context;
199
+ if (!memoryStore) {
200
+ return "Error: memoryStore not configured. Memory features are disabled.";
201
+ }
202
+ const all = await memoryStore.readMemoryPattern(userID, ".*");
203
+
204
+ if (all.length === 0) {
205
+ return "No memories found.";
206
+ }
207
+
208
+ return `Found ${all.length} ${all.length === 1 ? 'memory' : 'memories'}:\n\n` +
209
+ all.map((m, i) => {
210
+ const content =
211
+ typeof m.value === "object" && m.value.content
212
+ ? m.value.content
213
+ : JSON.stringify(m.value);
214
+ const tags =
215
+ typeof m.value === "object" && m.value.tags?.length
216
+ ? `\n tags: ${m.value.tags.join(", ")}`
217
+ : "";
218
+ const saved = m.updated_at
219
+ ? `\n saved: ${new Date(m.updated_at).toISOString()}`
220
+ : "";
221
+ return `${i + 1}. [${m.key}]\n ${content}${tags}${saved}`;
222
+ }).join("\n\n");
223
+ } catch (err) {
224
+ return `Error listing memories: ${err.message}`;
225
+ }
226
+ },
227
+ },
228
+
229
+ {
230
+ name: "memory_read",
231
+ description:
232
+ "Read a specific memory by its exact key. Use this when you know the key and want to retrieve its full content.",
233
+ parameters: {
234
+ type: "object",
235
+ properties: {
236
+ key: {
237
+ type: "string",
238
+ description: "The exact memory key to read. Use memory_list or memory_search to find keys.",
239
+ },
240
+ },
241
+ required: ["key"],
242
+ },
243
+ /**
244
+ * Read a specific memory via memoryStore.readMemory.
245
+ *
246
+ * @param {Object} input - Tool input with key
247
+ * @param {AbortSignal} signal - Abort signal
248
+ * @param {Object} context - { userID, memoryStore }
249
+ * @returns {Promise<string>} Memory content or not found message
250
+ */
251
+ execute: async (input, signal, context) => {
252
+ try {
253
+ const { userID, memoryStore } = context;
254
+ if (!memoryStore) {
255
+ return "Error: memoryStore not configured. Memory features are disabled.";
256
+ }
257
+ const memory = await memoryStore.readMemory(userID, input.key);
258
+
259
+ if (!memory || !memory.value) {
260
+ return `Memory "${input.key}" not found.`;
261
+ }
262
+
263
+ const content =
264
+ typeof memory.value === "object" && memory.value.content
265
+ ? memory.value.content
266
+ : JSON.stringify(memory.value, null, 2);
267
+ const tags =
268
+ typeof memory.value === "object" && memory.value.tags?.length
269
+ ? `\ntags: ${memory.value.tags.join(", ")}`
270
+ : "";
271
+ const saved = memory.updated_at
272
+ ? `\nsaved: ${new Date(memory.updated_at).toISOString()}`
273
+ : "";
274
+ const app = memory.app_id ? `\nwritten by: ${memory.app_id}` : "";
275
+
276
+ return `[${input.key}]\n${content}${tags}${saved}${app}`;
277
+ } catch (err) {
278
+ return `Error reading memory: ${err.message}`;
279
+ }
280
+ },
281
+ },
282
+
283
+ {
284
+ name: "memory_update",
285
+ description:
286
+ "Update an existing memory or create a new one with a specific key. Use this when you need to modify existing information or when you want full control over the memory key (unlike memory_save which auto-generates keys).",
287
+ parameters: {
288
+ type: "object",
289
+ properties: {
290
+ key: {
291
+ type: "string",
292
+ description: "The memory key to update or create. Use snake_case like 'user_preferences' or 'project_notes'.",
293
+ },
294
+ content: {
295
+ type: "string",
296
+ description: "The new content to store. Can be plain text, markdown, or any string.",
297
+ },
298
+ tags: {
299
+ type: "array",
300
+ items: { type: "string" },
301
+ description: "Optional tags for categorization. e.g. ['personal', 'project']",
302
+ },
303
+ },
304
+ required: ["key", "content"],
305
+ },
306
+ /**
307
+ * Update or create a memory via memoryStore.writeMemory.
308
+ *
309
+ * @param {Object} input - Tool input with key, content, and optional tags
310
+ * @param {AbortSignal} signal - Abort signal
311
+ * @param {Object} context - { userID, memoryStore }
312
+ * @returns {Promise<string>} Confirmation message
313
+ */
314
+ execute: async (input, signal, context) => {
315
+ try {
316
+ const { userID, memoryStore } = context;
317
+ if (!memoryStore) {
318
+ return "Error: memoryStore not configured. Memory features are disabled.";
319
+ }
320
+ const value = {
321
+ content: input.content,
322
+ tags: input.tags || [],
323
+ source: "agent",
324
+ };
325
+ await memoryStore.writeMemory(userID, input.key, value, "agent");
326
+ return `Memory "${input.key}" saved.`;
327
+ } catch (err) {
328
+ return `Error updating memory: ${err.message}`;
329
+ }
330
+ },
331
+ },
332
+ ];