@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,300 @@
1
+ import crypto from 'crypto';
2
+ import { DatabaseSync } from 'node:sqlite';
3
+ import { EventStore } from './EventStore.js';
4
+
5
+ /**
6
+ * SQLite-backed EventStore implementation
7
+ *
8
+ * Uses Node.js 22.5+ built-in sqlite module for zero-dependency event storage.
9
+ * Timestamps stored as INTEGER (Unix ms), data as JSON TEXT column.
10
+ */
11
+ export class SQLiteEventStore extends EventStore {
12
+ constructor() {
13
+ super();
14
+ this.db = null;
15
+ }
16
+
17
+ /**
18
+ * Initialize SQLite event store
19
+ *
20
+ * @param {Object} config - Configuration object
21
+ * @param {string} config.dbPath - Path to SQLite database file
22
+ */
23
+ async init({ dbPath }) {
24
+ this.db = new DatabaseSync(dbPath);
25
+
26
+ this.db.exec(`
27
+ CREATE TABLE IF NOT EXISTS events (
28
+ id TEXT PRIMARY KEY,
29
+ user_id TEXT NOT NULL,
30
+ type TEXT NOT NULL,
31
+ data TEXT,
32
+ timestamp INTEGER NOT NULL,
33
+ created_at INTEGER DEFAULT (strftime('%s', 'now') * 1000)
34
+ );
35
+
36
+ CREATE INDEX IF NOT EXISTS idx_events_user_type ON events(user_id, type);
37
+ CREATE INDEX IF NOT EXISTS idx_events_user_timestamp ON events(user_id, timestamp);
38
+ `);
39
+
40
+ console.log('[events] SQLiteEventStore initialized');
41
+ }
42
+
43
+ /**
44
+ * Log an event
45
+ *
46
+ * @param {Object} params
47
+ * @param {string} params.userId - User ID
48
+ * @param {string} params.type - Event type
49
+ * @param {Object} [params.data={}] - Event-specific data
50
+ * @param {number} [params.timestamp] - Unix ms timestamp (defaults to now)
51
+ * @returns {Promise<Object>} Created event document
52
+ */
53
+ async logEvent({ userId, type, data = {}, timestamp }) {
54
+ if (!this.db) throw new Error('Events not initialized. Call init() first.');
55
+
56
+ const event = {
57
+ id: crypto.randomUUID(),
58
+ userId,
59
+ type,
60
+ data,
61
+ timestamp: timestamp || Date.now(),
62
+ };
63
+
64
+ const stmt = this.db.prepare(`
65
+ INSERT INTO events (id, user_id, type, data, timestamp)
66
+ VALUES (?, ?, ?, ?, ?)
67
+ `);
68
+
69
+ stmt.run(
70
+ event.id,
71
+ event.userId,
72
+ event.type,
73
+ JSON.stringify(event.data),
74
+ event.timestamp
75
+ );
76
+
77
+ return event;
78
+ }
79
+
80
+ /**
81
+ * Query events with filters
82
+ *
83
+ * @param {Object} params
84
+ * @param {string} params.userId - User ID
85
+ * @param {string} [params.type] - Filter by event type
86
+ * @param {string} [params.startDate] - ISO date start (inclusive)
87
+ * @param {string} [params.endDate] - ISO date end (inclusive)
88
+ * @param {number} [params.limit=100] - Max results
89
+ * @returns {Promise<Array>} Matching events sorted by timestamp desc
90
+ */
91
+ async query({ userId, type, startDate, endDate, limit = 100 }) {
92
+ if (!this.db) throw new Error('Events not initialized. Call init() first.');
93
+
94
+ let sql = 'SELECT * FROM events WHERE user_id = ?';
95
+ const params = [userId];
96
+
97
+ if (type) {
98
+ sql += ' AND type = ?';
99
+ params.push(type);
100
+ }
101
+
102
+ if (startDate) {
103
+ const startTs = new Date(startDate).getTime();
104
+ sql += ' AND timestamp >= ?';
105
+ params.push(startTs);
106
+ }
107
+
108
+ if (endDate) {
109
+ // End of day for endDate
110
+ const endTs = new Date(endDate).getTime() + 86400000 - 1;
111
+ sql += ' AND timestamp <= ?';
112
+ params.push(endTs);
113
+ }
114
+
115
+ sql += ' ORDER BY timestamp DESC LIMIT ?';
116
+ params.push(limit);
117
+
118
+ const stmt = this.db.prepare(sql);
119
+ const rows = stmt.all(...params);
120
+
121
+ return rows.map(row => this._rowToEvent(row));
122
+ }
123
+
124
+ /**
125
+ * Get aggregated usage statistics
126
+ *
127
+ * @param {Object} params
128
+ * @param {string} params.userId - User ID
129
+ * @param {string} [params.startDate] - ISO date start
130
+ * @param {string} [params.endDate] - ISO date end
131
+ * @param {string} [params.groupBy='type'] - Group by: type, day, week, month
132
+ * @returns {Promise<Object>} Summary with total count and grouped breakdown
133
+ */
134
+ async summary({ userId, startDate, endDate, groupBy = 'type' }) {
135
+ if (!this.db) throw new Error('Events not initialized. Call init() first.');
136
+
137
+ // Build WHERE clause
138
+ let whereClause = 'WHERE user_id = ?';
139
+ const params = [userId];
140
+
141
+ if (startDate) {
142
+ const startTs = new Date(startDate).getTime();
143
+ whereClause += ' AND timestamp >= ?';
144
+ params.push(startTs);
145
+ }
146
+
147
+ if (endDate) {
148
+ const endTs = new Date(endDate).getTime() + 86400000 - 1;
149
+ whereClause += ' AND timestamp <= ?';
150
+ params.push(endTs);
151
+ }
152
+
153
+ // Total count
154
+ const countStmt = this.db.prepare(`SELECT COUNT(*) as total FROM events ${whereClause}`);
155
+ const countResult = countStmt.get(...params);
156
+ const total = countResult.total;
157
+
158
+ // Group by clause varies by groupBy parameter
159
+ let groupByClause, selectExpr;
160
+ switch (groupBy) {
161
+ case 'day':
162
+ selectExpr = "date(timestamp / 1000, 'unixepoch') as period";
163
+ groupByClause = "date(timestamp / 1000, 'unixepoch')";
164
+ break;
165
+ case 'week':
166
+ selectExpr = "strftime('%Y-W%W', timestamp / 1000, 'unixepoch') as period";
167
+ groupByClause = "strftime('%Y-W%W', timestamp / 1000, 'unixepoch')";
168
+ break;
169
+ case 'month':
170
+ selectExpr = "strftime('%Y-%m', timestamp / 1000, 'unixepoch') as period";
171
+ groupByClause = "strftime('%Y-%m', timestamp / 1000, 'unixepoch')";
172
+ break;
173
+ case 'type':
174
+ default:
175
+ selectExpr = 'type as period';
176
+ groupByClause = 'type';
177
+ break;
178
+ }
179
+
180
+ const groupStmt = this.db.prepare(`
181
+ SELECT ${selectExpr}, COUNT(*) as count
182
+ FROM events ${whereClause}
183
+ GROUP BY ${groupByClause}
184
+ ORDER BY count DESC
185
+ `);
186
+ const groups = groupStmt.all(...params);
187
+
188
+ // Convert to object for type grouping, array for time-based grouping
189
+ let breakdown;
190
+ if (groupBy === 'type') {
191
+ breakdown = {};
192
+ for (const row of groups) {
193
+ breakdown[row.period] = row.count;
194
+ }
195
+ } else {
196
+ breakdown = groups.map(row => ({
197
+ period: row.period,
198
+ count: row.count,
199
+ }));
200
+ }
201
+
202
+ // Tool usage breakdown (if tool_call events exist)
203
+ const toolStmt = this.db.prepare(`
204
+ SELECT data FROM events ${whereClause} AND type = 'tool_call'
205
+ `);
206
+ const toolParams = [...params, 'tool_call'];
207
+ // Rebuild params for tool query
208
+ const toolWhereParams = [...params];
209
+
210
+ const toolCountStmt = this.db.prepare(`
211
+ SELECT data FROM events ${whereClause.replace('user_id = ?', 'user_id = ? AND type = ?')}
212
+ `.replace('AND type = ?', ''));
213
+
214
+ // Simpler approach: get all tool_call events and aggregate in JS
215
+ const toolEventsStmt = this.db.prepare(`
216
+ SELECT data FROM events WHERE user_id = ? AND type = 'tool_call'
217
+ ${startDate ? 'AND timestamp >= ?' : ''}
218
+ ${endDate ? 'AND timestamp <= ?' : ''}
219
+ `);
220
+ const toolEventParams = [userId];
221
+ if (startDate) toolEventParams.push(new Date(startDate).getTime());
222
+ if (endDate) toolEventParams.push(new Date(endDate).getTime() + 86400000 - 1);
223
+
224
+ const toolEvents = toolEventsStmt.all(...toolEventParams);
225
+ const toolCounts = {};
226
+ for (const row of toolEvents) {
227
+ try {
228
+ const data = JSON.parse(row.data);
229
+ const toolName = data.tool || 'unknown';
230
+ toolCounts[toolName] = (toolCounts[toolName] || 0) + 1;
231
+ } catch {
232
+ // Skip malformed data
233
+ }
234
+ }
235
+
236
+ return {
237
+ total,
238
+ breakdown,
239
+ toolUsage: Object.keys(toolCounts).length > 0 ? toolCounts : undefined,
240
+ };
241
+ }
242
+
243
+ /**
244
+ * Delete events older than a given date
245
+ *
246
+ * @param {string} userId - User ID
247
+ * @param {string} beforeDate - ISO date cutoff
248
+ * @returns {Promise<Object>} Delete result with count
249
+ */
250
+ async deleteOldEvents(userId, beforeDate) {
251
+ if (!this.db) throw new Error('Events not initialized. Call init() first.');
252
+
253
+ const cutoffTs = new Date(beforeDate).getTime();
254
+ const stmt = this.db.prepare('DELETE FROM events WHERE user_id = ? AND timestamp < ?');
255
+ const result = stmt.run(userId, cutoffTs);
256
+
257
+ return { deletedCount: result.changes };
258
+ }
259
+
260
+ /**
261
+ * Convert SQLite row to event object
262
+ *
263
+ * @private
264
+ * @param {Object} row - Raw SQLite row
265
+ * @returns {Object} Event object with parsed data and camelCase keys
266
+ */
267
+ _rowToEvent(row) {
268
+ let data = {};
269
+ try {
270
+ data = JSON.parse(row.data);
271
+ } catch {
272
+ // Keep empty object
273
+ }
274
+
275
+ return {
276
+ id: row.id,
277
+ userId: row.user_id,
278
+ type: row.type,
279
+ data,
280
+ timestamp: row.timestamp,
281
+ createdAt: row.created_at,
282
+ };
283
+ }
284
+
285
+ /**
286
+ * Close the database connection and checkpoint WAL.
287
+ */
288
+ close() {
289
+ if (this.db) {
290
+ try {
291
+ this.db.exec('PRAGMA wal_checkpoint(TRUNCATE)');
292
+ this.db.close();
293
+ this.db = null;
294
+ console.log('[events] SQLiteEventStore closed');
295
+ } catch (err) {
296
+ console.error('[events] Error closing database:', err.message);
297
+ }
298
+ }
299
+ }
300
+ }
@@ -0,0 +1,240 @@
1
+ import { DatabaseSync } from 'node:sqlite';
2
+
3
+ /**
4
+ * SQLite-backed MemoryStore implementation
5
+ *
6
+ * Uses Node.js 22.5+ built-in sqlite module for zero-dependency memory storage.
7
+ * Provides the same interface as the MongoMemoryStore for interchangeable use.
8
+ */
9
+ export class SQLiteMemoryStore {
10
+ constructor() {
11
+ this.db = null;
12
+ }
13
+
14
+ /**
15
+ * Initialize SQLite memory store
16
+ *
17
+ * @param {Object} config - Configuration object
18
+ * @param {string} config.dbPath - Path to SQLite database file
19
+ */
20
+ async init({ dbPath }) {
21
+ this.db = new DatabaseSync(dbPath);
22
+
23
+ this.db.exec(`
24
+ CREATE TABLE IF NOT EXISTS memories (
25
+ id INTEGER PRIMARY KEY AUTOINCREMENT,
26
+ user_id TEXT NOT NULL,
27
+ key TEXT NOT NULL,
28
+ value TEXT NOT NULL,
29
+ app_id TEXT DEFAULT 'agent',
30
+ created_at TEXT NOT NULL,
31
+ updated_at TEXT NOT NULL,
32
+ UNIQUE(user_id, key)
33
+ );
34
+
35
+ CREATE INDEX IF NOT EXISTS idx_memories_user_key ON memories(user_id, key);
36
+ `);
37
+
38
+ console.log('[memory] SQLiteMemoryStore initialized');
39
+ }
40
+
41
+ /**
42
+ * Write or update a memory entry
43
+ *
44
+ * @param {string} userId - User identifier
45
+ * @param {string} key - Memory key
46
+ * @param {Object|string} value - Value to store (will be JSON-stringified if object)
47
+ * @param {string} [appId='agent'] - Source application
48
+ * @returns {Promise<Object>} Created/updated memory object
49
+ */
50
+ async writeMemory(userId, key, value, appId = 'agent') {
51
+ if (!this.db) throw new Error('MemoryStore not initialized. Call init() first.');
52
+
53
+ const now = new Date().toISOString();
54
+ const valueStr = typeof value === 'object' ? JSON.stringify(value) : String(value);
55
+
56
+ const stmt = this.db.prepare(`
57
+ INSERT INTO memories (user_id, key, value, app_id, created_at, updated_at)
58
+ VALUES (?, ?, ?, ?, ?, ?)
59
+ ON CONFLICT(user_id, key) DO UPDATE SET
60
+ value = excluded.value,
61
+ app_id = excluded.app_id,
62
+ updated_at = excluded.updated_at
63
+ `);
64
+
65
+ stmt.run(userId, key, valueStr, appId, now, now);
66
+
67
+ return { user_id: userId, key, value: valueStr, app_id: appId, updated_at: now };
68
+ }
69
+
70
+ /**
71
+ * Read a specific memory by key
72
+ *
73
+ * @param {string} userId - User identifier
74
+ * @param {string} key - Memory key
75
+ * @returns {Promise<Object|null>} Memory object or null if not found
76
+ */
77
+ async readMemory(userId, key) {
78
+ if (!this.db) throw new Error('MemoryStore not initialized. Call init() first.');
79
+
80
+ const stmt = this.db.prepare('SELECT * FROM memories WHERE user_id = ? AND key = ?');
81
+ const row = stmt.get(userId, key);
82
+
83
+ if (!row) return null;
84
+
85
+ return {
86
+ key: row.key,
87
+ value: this._parseValue(row.value),
88
+ app_id: row.app_id,
89
+ created_at: row.created_at,
90
+ updated_at: row.updated_at,
91
+ };
92
+ }
93
+
94
+ /**
95
+ * Read memories matching a pattern
96
+ *
97
+ * Supports both regex patterns (e.g., ".*") and SQL LIKE wildcards (e.g., "%foo%").
98
+ * For simple wildcard matching, use SQL LIKE syntax with %.
99
+ *
100
+ * @param {string} userId - User identifier
101
+ * @param {string} pattern - Pattern to match keys (regex or SQL LIKE)
102
+ * @returns {Promise<Array>} Array of matching memory objects
103
+ */
104
+ async readMemoryPattern(userId, pattern) {
105
+ if (!this.db) throw new Error('MemoryStore not initialized. Call init() first.');
106
+
107
+ // If pattern uses SQL LIKE syntax (contains %), use SQL directly
108
+ if (pattern.includes('%')) {
109
+ const stmt = this.db.prepare(`
110
+ SELECT * FROM memories WHERE user_id = ? AND key LIKE ? ORDER BY updated_at DESC
111
+ `);
112
+ const rows = stmt.all(userId, pattern);
113
+ return rows.map(row => ({
114
+ key: row.key,
115
+ value: this._parseValue(row.value),
116
+ app_id: row.app_id,
117
+ created_at: row.created_at,
118
+ updated_at: row.updated_at,
119
+ }));
120
+ }
121
+
122
+ // Otherwise, treat as regex pattern
123
+ const stmt = this.db.prepare(`
124
+ SELECT * FROM memories WHERE user_id = ? ORDER BY updated_at DESC
125
+ `);
126
+ const rows = stmt.all(userId);
127
+ const regex = new RegExp(pattern);
128
+
129
+ return rows
130
+ .filter(row => regex.test(row.key))
131
+ .map(row => ({
132
+ key: row.key,
133
+ value: this._parseValue(row.value),
134
+ app_id: row.app_id,
135
+ created_at: row.created_at,
136
+ updated_at: row.updated_at,
137
+ }));
138
+ }
139
+
140
+ /**
141
+ * Delete a memory by key
142
+ *
143
+ * @param {string} userId - User identifier
144
+ * @param {string} key - Memory key to delete
145
+ * @returns {Promise<Object>} Result with deletedCount
146
+ */
147
+ async deleteMemory(userId, key) {
148
+ if (!this.db) throw new Error('MemoryStore not initialized. Call init() first.');
149
+
150
+ const stmt = this.db.prepare('DELETE FROM memories WHERE user_id = ? AND key = ?');
151
+ const result = stmt.run(userId, key);
152
+
153
+ return { deletedCount: result.changes };
154
+ }
155
+
156
+ /**
157
+ * List all memory keys for a user
158
+ *
159
+ * @param {string} userId - User identifier
160
+ * @returns {Promise<Array>} Array of { key, updated_at } objects
161
+ */
162
+ async listMemories(userId) {
163
+ if (!this.db) throw new Error('MemoryStore not initialized. Call init() first.');
164
+
165
+ const stmt = this.db.prepare(`
166
+ SELECT key, updated_at FROM memories WHERE user_id = ? ORDER BY updated_at DESC
167
+ `);
168
+
169
+ return stmt.all(userId);
170
+ }
171
+
172
+ /**
173
+ * Get all memories for a user with full content
174
+ *
175
+ * @param {string} userId - User identifier
176
+ * @returns {Promise<Array>} Array of full memory objects
177
+ */
178
+ async getAllMemories(userId) {
179
+ if (!this.db) throw new Error('MemoryStore not initialized. Call init() first.');
180
+
181
+ const stmt = this.db.prepare(`
182
+ SELECT * FROM memories WHERE user_id = ? ORDER BY updated_at DESC
183
+ `);
184
+ const rows = stmt.all(userId);
185
+
186
+ return rows.map(row => ({
187
+ key: row.key,
188
+ value: this._parseValue(row.value),
189
+ app_id: row.app_id,
190
+ created_at: row.created_at,
191
+ updated_at: row.updated_at,
192
+ }));
193
+ }
194
+
195
+ /**
196
+ * Delete all memories for a user
197
+ *
198
+ * @param {string} userId - User identifier
199
+ * @returns {Promise<Object>} Result with deletedCount
200
+ */
201
+ async clearMemories(userId) {
202
+ if (!this.db) throw new Error('MemoryStore not initialized. Call init() first.');
203
+
204
+ const stmt = this.db.prepare('DELETE FROM memories WHERE user_id = ?');
205
+ const result = stmt.run(userId);
206
+
207
+ return { deletedCount: result.changes };
208
+ }
209
+
210
+ /**
211
+ * Parse stored value back to object if it's JSON
212
+ *
213
+ * @private
214
+ * @param {string} value - Raw string value from database
215
+ * @returns {Object|string} Parsed value
216
+ */
217
+ _parseValue(value) {
218
+ try {
219
+ return JSON.parse(value);
220
+ } catch {
221
+ return value;
222
+ }
223
+ }
224
+
225
+ /**
226
+ * Close the database connection and checkpoint WAL.
227
+ */
228
+ close() {
229
+ if (this.db) {
230
+ try {
231
+ this.db.exec('PRAGMA wal_checkpoint(TRUNCATE)');
232
+ this.db.close();
233
+ this.db = null;
234
+ console.log('[memory] SQLiteMemoryStore closed');
235
+ } catch (err) {
236
+ console.error('[memory] Error closing database:', err.message);
237
+ }
238
+ }
239
+ }
240
+ }