@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.
- package/CHANGELOG.md +136 -0
- package/README.md +380 -0
- package/bin/dotbot.js +461 -0
- package/core/agent.js +779 -0
- package/core/compaction.js +261 -0
- package/core/cron_handler.js +262 -0
- package/core/events.js +229 -0
- package/core/failover.js +193 -0
- package/core/gptoss_tool_parser.js +173 -0
- package/core/init.js +154 -0
- package/core/normalize.js +324 -0
- package/core/trigger_handler.js +148 -0
- package/docs/core.md +103 -0
- package/docs/protected-files.md +59 -0
- package/examples/sqlite-session-example.js +69 -0
- package/index.js +341 -0
- package/observer/index.js +164 -0
- package/package.json +42 -0
- package/storage/CronStore.js +145 -0
- package/storage/EventStore.js +71 -0
- package/storage/MemoryStore.js +175 -0
- package/storage/MongoAdapter.js +291 -0
- package/storage/MongoCronAdapter.js +347 -0
- package/storage/MongoTaskAdapter.js +242 -0
- package/storage/MongoTriggerAdapter.js +158 -0
- package/storage/SQLiteAdapter.js +382 -0
- package/storage/SQLiteCronAdapter.js +562 -0
- package/storage/SQLiteEventStore.js +300 -0
- package/storage/SQLiteMemoryAdapter.js +240 -0
- package/storage/SQLiteTaskAdapter.js +419 -0
- package/storage/SQLiteTriggerAdapter.js +262 -0
- package/storage/SessionStore.js +149 -0
- package/storage/TaskStore.js +100 -0
- package/storage/TriggerStore.js +90 -0
- package/storage/cron_constants.js +48 -0
- package/storage/index.js +21 -0
- package/tools/appgen.js +311 -0
- package/tools/browser.js +634 -0
- package/tools/code.js +101 -0
- package/tools/events.js +145 -0
- package/tools/files.js +201 -0
- package/tools/images.js +253 -0
- package/tools/index.js +97 -0
- package/tools/jobs.js +159 -0
- package/tools/memory.js +332 -0
- package/tools/messages.js +135 -0
- package/tools/notify.js +42 -0
- package/tools/tasks.js +404 -0
- package/tools/triggers.js +159 -0
- package/tools/weather.js +82 -0
- package/tools/web.js +283 -0
- package/utils/providers.js +136 -0
|
@@ -0,0 +1,419 @@
|
|
|
1
|
+
import crypto from 'crypto';
|
|
2
|
+
import { DatabaseSync } from 'node:sqlite';
|
|
3
|
+
import { TaskStore } from './TaskStore.js';
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* SQLite-backed TaskStore implementation
|
|
7
|
+
*
|
|
8
|
+
* Uses Node.js 22.5+ built-in sqlite module for zero-dependency task storage.
|
|
9
|
+
* Dates stored as INTEGER (Unix ms), steps as JSON TEXT column.
|
|
10
|
+
*/
|
|
11
|
+
export class SQLiteTaskStore extends TaskStore {
|
|
12
|
+
constructor() {
|
|
13
|
+
super();
|
|
14
|
+
this.db = null;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
/**
|
|
18
|
+
* Initialize SQLite task 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
|
+
// Migration: goals → tasks table
|
|
27
|
+
const goalsExists = this.db.prepare(
|
|
28
|
+
"SELECT name FROM sqlite_master WHERE type='table' AND name='goals'"
|
|
29
|
+
).get();
|
|
30
|
+
const tasksExists = this.db.prepare(
|
|
31
|
+
"SELECT name FROM sqlite_master WHERE type='table' AND name='tasks'"
|
|
32
|
+
).get();
|
|
33
|
+
|
|
34
|
+
if (goalsExists && !tasksExists) {
|
|
35
|
+
console.log('[tasks] migrating goals table to tasks...');
|
|
36
|
+
this.db.exec('ALTER TABLE goals RENAME TO tasks');
|
|
37
|
+
// Also rename indexes
|
|
38
|
+
this.db.exec('DROP INDEX IF EXISTS idx_goals_user_status');
|
|
39
|
+
this.db.exec('DROP INDEX IF EXISTS idx_goals_user_category');
|
|
40
|
+
this.db.exec('DROP INDEX IF EXISTS idx_goals_user_priority');
|
|
41
|
+
this.db.exec('DROP INDEX IF EXISTS idx_goals_user_deadline');
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
this.db.exec(`
|
|
45
|
+
CREATE TABLE IF NOT EXISTS tasks (
|
|
46
|
+
id TEXT PRIMARY KEY,
|
|
47
|
+
user_id TEXT NOT NULL,
|
|
48
|
+
description TEXT NOT NULL,
|
|
49
|
+
steps TEXT NOT NULL,
|
|
50
|
+
category TEXT DEFAULT 'general',
|
|
51
|
+
priority TEXT DEFAULT 'medium',
|
|
52
|
+
deadline INTEGER,
|
|
53
|
+
mode TEXT DEFAULT 'auto',
|
|
54
|
+
status TEXT DEFAULT 'pending',
|
|
55
|
+
current_step INTEGER DEFAULT 0,
|
|
56
|
+
progress INTEGER DEFAULT 0,
|
|
57
|
+
created_at INTEGER NOT NULL,
|
|
58
|
+
updated_at INTEGER NOT NULL,
|
|
59
|
+
last_worked_at INTEGER
|
|
60
|
+
);
|
|
61
|
+
|
|
62
|
+
CREATE INDEX IF NOT EXISTS idx_tasks_user_status ON tasks(user_id, status);
|
|
63
|
+
CREATE INDEX IF NOT EXISTS idx_tasks_user_category ON tasks(user_id, category);
|
|
64
|
+
CREATE INDEX IF NOT EXISTS idx_tasks_user_priority ON tasks(user_id, priority);
|
|
65
|
+
CREATE INDEX IF NOT EXISTS idx_tasks_user_deadline ON tasks(user_id, deadline);
|
|
66
|
+
`);
|
|
67
|
+
|
|
68
|
+
console.log('[tasks] SQLiteTaskStore initialized');
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
/**
|
|
72
|
+
* Create a new task
|
|
73
|
+
*
|
|
74
|
+
* @param {Object} params
|
|
75
|
+
* @param {string} params.userId - Owner user ID
|
|
76
|
+
* @param {string} params.description - Task description
|
|
77
|
+
* @param {Array<string|Object>} [params.steps=[]] - Step descriptions or step objects
|
|
78
|
+
* @param {string} [params.category='general'] - Task category
|
|
79
|
+
* @param {string} [params.priority='medium'] - Priority: low, medium, high
|
|
80
|
+
* @param {number|null} [params.deadline=null] - Unix ms deadline or null
|
|
81
|
+
* @param {string} [params.mode='auto'] - Execution mode: auto or manual
|
|
82
|
+
* @returns {Promise<Object>} Created task document
|
|
83
|
+
*/
|
|
84
|
+
async createTask({ userId, description, steps = [], category = 'general', priority = 'medium', deadline = null, mode = 'auto' }) {
|
|
85
|
+
if (!this.db) throw new Error('Tasks not initialized. Call init() first.');
|
|
86
|
+
|
|
87
|
+
const normalizedSteps = steps.map(step => {
|
|
88
|
+
if (typeof step === 'string') {
|
|
89
|
+
return {
|
|
90
|
+
text: step,
|
|
91
|
+
action: step,
|
|
92
|
+
done: false,
|
|
93
|
+
result: null,
|
|
94
|
+
startedAt: null,
|
|
95
|
+
completedAt: null,
|
|
96
|
+
};
|
|
97
|
+
}
|
|
98
|
+
return {
|
|
99
|
+
text: step.text || step.description || '',
|
|
100
|
+
action: step.action || step.text || '',
|
|
101
|
+
done: step.done || false,
|
|
102
|
+
result: step.result || null,
|
|
103
|
+
startedAt: step.startedAt || null,
|
|
104
|
+
completedAt: step.completedAt || null,
|
|
105
|
+
};
|
|
106
|
+
});
|
|
107
|
+
|
|
108
|
+
const now = Date.now();
|
|
109
|
+
const task = {
|
|
110
|
+
id: crypto.randomUUID(),
|
|
111
|
+
userId,
|
|
112
|
+
description,
|
|
113
|
+
steps: normalizedSteps,
|
|
114
|
+
category,
|
|
115
|
+
priority,
|
|
116
|
+
deadline,
|
|
117
|
+
mode,
|
|
118
|
+
status: 'pending',
|
|
119
|
+
currentStep: 0,
|
|
120
|
+
progress: 0,
|
|
121
|
+
createdAt: now,
|
|
122
|
+
updatedAt: now,
|
|
123
|
+
lastWorkedAt: null,
|
|
124
|
+
};
|
|
125
|
+
|
|
126
|
+
const stmt = this.db.prepare(`
|
|
127
|
+
INSERT INTO tasks (id, user_id, description, steps, category, priority, deadline, mode, status, current_step, progress, created_at, updated_at, last_worked_at)
|
|
128
|
+
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
|
|
129
|
+
`);
|
|
130
|
+
|
|
131
|
+
stmt.run(
|
|
132
|
+
task.id,
|
|
133
|
+
task.userId,
|
|
134
|
+
task.description,
|
|
135
|
+
JSON.stringify(task.steps),
|
|
136
|
+
task.category,
|
|
137
|
+
task.priority,
|
|
138
|
+
task.deadline,
|
|
139
|
+
task.mode,
|
|
140
|
+
task.status,
|
|
141
|
+
task.currentStep,
|
|
142
|
+
task.progress,
|
|
143
|
+
task.createdAt,
|
|
144
|
+
task.updatedAt,
|
|
145
|
+
task.lastWorkedAt
|
|
146
|
+
);
|
|
147
|
+
|
|
148
|
+
return task;
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
/**
|
|
152
|
+
* Get tasks for a user, optionally filtered by status/category/priority
|
|
153
|
+
*
|
|
154
|
+
* @param {string} userId - User ID
|
|
155
|
+
* @param {Object} [filters={}] - Optional filters
|
|
156
|
+
* @param {string} [filters.status] - Filter by status
|
|
157
|
+
* @param {string} [filters.category] - Filter by category
|
|
158
|
+
* @param {string} [filters.priority] - Filter by priority
|
|
159
|
+
* @returns {Promise<Array>} Task list with computed progress
|
|
160
|
+
*/
|
|
161
|
+
async getTasks(userId, filters = {}) {
|
|
162
|
+
if (!this.db) throw new Error('Tasks not initialized. Call init() first.');
|
|
163
|
+
|
|
164
|
+
let sql = 'SELECT * FROM tasks WHERE user_id = ?';
|
|
165
|
+
const params = [userId];
|
|
166
|
+
|
|
167
|
+
if (filters.status) {
|
|
168
|
+
sql += ' AND status = ?';
|
|
169
|
+
params.push(filters.status);
|
|
170
|
+
}
|
|
171
|
+
if (filters.category) {
|
|
172
|
+
sql += ' AND category = ?';
|
|
173
|
+
params.push(filters.category);
|
|
174
|
+
}
|
|
175
|
+
if (filters.priority) {
|
|
176
|
+
sql += ' AND priority = ?';
|
|
177
|
+
params.push(filters.priority);
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
sql += ' ORDER BY created_at DESC';
|
|
181
|
+
|
|
182
|
+
const stmt = this.db.prepare(sql);
|
|
183
|
+
const rows = stmt.all(...params);
|
|
184
|
+
|
|
185
|
+
return rows.map(row => {
|
|
186
|
+
const task = this._rowToTask(row);
|
|
187
|
+
task.progress = this._calculateProgress(task);
|
|
188
|
+
return task;
|
|
189
|
+
});
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
/**
|
|
193
|
+
* Get a single task by ID
|
|
194
|
+
*
|
|
195
|
+
* @param {string} userId - User ID
|
|
196
|
+
* @param {string} taskId - Task UUID
|
|
197
|
+
* @returns {Promise<Object|null>} Task document or null
|
|
198
|
+
*/
|
|
199
|
+
async getTask(userId, taskId) {
|
|
200
|
+
if (!this.db) throw new Error('Tasks not initialized. Call init() first.');
|
|
201
|
+
|
|
202
|
+
const stmt = this.db.prepare('SELECT * FROM tasks WHERE id = ? AND user_id = ?');
|
|
203
|
+
const row = stmt.get(taskId, userId);
|
|
204
|
+
|
|
205
|
+
if (!row) return null;
|
|
206
|
+
|
|
207
|
+
const task = this._rowToTask(row);
|
|
208
|
+
task.progress = this._calculateProgress(task);
|
|
209
|
+
return task;
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
/**
|
|
213
|
+
* Update a task with allowed fields
|
|
214
|
+
*
|
|
215
|
+
* @param {string} userId - User ID
|
|
216
|
+
* @param {string} taskId - Task UUID
|
|
217
|
+
* @param {Object} updates - Fields to update
|
|
218
|
+
* @returns {Promise<Object>} Result with changes count
|
|
219
|
+
*/
|
|
220
|
+
async updateTask(userId, taskId, updates) {
|
|
221
|
+
if (!this.db) throw new Error('Tasks not initialized. Call init() first.');
|
|
222
|
+
|
|
223
|
+
const allowedFields = [
|
|
224
|
+
'description', 'steps', 'category', 'priority', 'deadline',
|
|
225
|
+
'mode', 'status', 'currentStep', 'lastWorkedAt'
|
|
226
|
+
];
|
|
227
|
+
|
|
228
|
+
// Map camelCase to snake_case for SQL columns
|
|
229
|
+
const fieldMap = {
|
|
230
|
+
description: 'description',
|
|
231
|
+
steps: 'steps',
|
|
232
|
+
category: 'category',
|
|
233
|
+
priority: 'priority',
|
|
234
|
+
deadline: 'deadline',
|
|
235
|
+
mode: 'mode',
|
|
236
|
+
status: 'status',
|
|
237
|
+
currentStep: 'current_step',
|
|
238
|
+
lastWorkedAt: 'last_worked_at',
|
|
239
|
+
};
|
|
240
|
+
|
|
241
|
+
const setClauses = [];
|
|
242
|
+
const values = [];
|
|
243
|
+
|
|
244
|
+
for (const field of allowedFields) {
|
|
245
|
+
if (updates[field] !== undefined) {
|
|
246
|
+
const col = fieldMap[field];
|
|
247
|
+
setClauses.push(`${col} = ?`);
|
|
248
|
+
if (field === 'steps') {
|
|
249
|
+
values.push(JSON.stringify(updates[field]));
|
|
250
|
+
} else {
|
|
251
|
+
values.push(updates[field]);
|
|
252
|
+
}
|
|
253
|
+
}
|
|
254
|
+
}
|
|
255
|
+
|
|
256
|
+
if (updates.steps) {
|
|
257
|
+
setClauses.push('progress = ?');
|
|
258
|
+
values.push(this._calculateProgressFromSteps(updates.steps));
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
setClauses.push('updated_at = ?');
|
|
262
|
+
values.push(Date.now());
|
|
263
|
+
|
|
264
|
+
const sql = `UPDATE tasks SET ${setClauses.join(', ')} WHERE id = ? AND user_id = ?`;
|
|
265
|
+
values.push(taskId, userId);
|
|
266
|
+
|
|
267
|
+
const stmt = this.db.prepare(sql);
|
|
268
|
+
const result = stmt.run(...values);
|
|
269
|
+
|
|
270
|
+
return { changes: result.changes };
|
|
271
|
+
}
|
|
272
|
+
|
|
273
|
+
/**
|
|
274
|
+
* Delete a task
|
|
275
|
+
*
|
|
276
|
+
* @param {string} userId - User ID
|
|
277
|
+
* @param {string} taskId - Task UUID
|
|
278
|
+
* @returns {Promise<Object>} Result with deletedCount
|
|
279
|
+
*/
|
|
280
|
+
async deleteTask(userId, taskId) {
|
|
281
|
+
if (!this.db) throw new Error('Tasks not initialized. Call init() first.');
|
|
282
|
+
|
|
283
|
+
const stmt = this.db.prepare('DELETE FROM tasks WHERE id = ? AND user_id = ?');
|
|
284
|
+
const result = stmt.run(taskId, userId);
|
|
285
|
+
|
|
286
|
+
return { deletedCount: result.changes };
|
|
287
|
+
}
|
|
288
|
+
|
|
289
|
+
/**
|
|
290
|
+
* Search tasks by description or step text (case-insensitive LIKE)
|
|
291
|
+
*
|
|
292
|
+
* @param {string} userId - User ID
|
|
293
|
+
* @param {string} query - Search text
|
|
294
|
+
* @returns {Promise<Array>} Matching tasks with computed progress
|
|
295
|
+
*/
|
|
296
|
+
async searchTasks(userId, query) {
|
|
297
|
+
if (!this.db) throw new Error('Tasks not initialized. Call init() first.');
|
|
298
|
+
|
|
299
|
+
const pattern = `%${query}%`;
|
|
300
|
+
const stmt = this.db.prepare(`
|
|
301
|
+
SELECT * FROM tasks
|
|
302
|
+
WHERE user_id = ? AND (description LIKE ? OR steps LIKE ?)
|
|
303
|
+
ORDER BY created_at DESC
|
|
304
|
+
`);
|
|
305
|
+
|
|
306
|
+
const rows = stmt.all(userId, pattern, pattern);
|
|
307
|
+
|
|
308
|
+
return rows.map(row => {
|
|
309
|
+
const task = this._rowToTask(row);
|
|
310
|
+
task.progress = this._calculateProgress(task);
|
|
311
|
+
return task;
|
|
312
|
+
});
|
|
313
|
+
}
|
|
314
|
+
|
|
315
|
+
/**
|
|
316
|
+
* Get aggregate task statistics for a user
|
|
317
|
+
*
|
|
318
|
+
* @param {string} userId - User ID
|
|
319
|
+
* @returns {Promise<Object>} Stats object with totals, breakdowns, and overdue count
|
|
320
|
+
*/
|
|
321
|
+
async getTaskStats(userId) {
|
|
322
|
+
if (!this.db) throw new Error('Tasks not initialized. Call init() first.');
|
|
323
|
+
|
|
324
|
+
const stmt = this.db.prepare('SELECT * FROM tasks WHERE user_id = ?');
|
|
325
|
+
const rows = stmt.all(userId);
|
|
326
|
+
const tasks = rows.map(row => this._rowToTask(row));
|
|
327
|
+
|
|
328
|
+
const now = Date.now();
|
|
329
|
+
const stats = {
|
|
330
|
+
total: tasks.length,
|
|
331
|
+
pending: tasks.filter(g => g.status === 'pending').length,
|
|
332
|
+
in_progress: tasks.filter(g => g.status === 'in_progress').length,
|
|
333
|
+
completed: tasks.filter(g => g.status === 'completed').length,
|
|
334
|
+
by_category: {},
|
|
335
|
+
by_priority: {},
|
|
336
|
+
overdue: 0,
|
|
337
|
+
};
|
|
338
|
+
|
|
339
|
+
for (const task of tasks) {
|
|
340
|
+
const cat = task.category || 'general';
|
|
341
|
+
stats.by_category[cat] = (stats.by_category[cat] || 0) + 1;
|
|
342
|
+
|
|
343
|
+
const pri = task.priority || 'medium';
|
|
344
|
+
stats.by_priority[pri] = (stats.by_priority[pri] || 0) + 1;
|
|
345
|
+
|
|
346
|
+
if (task.deadline && task.deadline < now && task.status !== 'completed') {
|
|
347
|
+
stats.overdue++;
|
|
348
|
+
}
|
|
349
|
+
}
|
|
350
|
+
|
|
351
|
+
return stats;
|
|
352
|
+
}
|
|
353
|
+
|
|
354
|
+
/**
|
|
355
|
+
* Convert SQLite row (snake_case) to task object (camelCase)
|
|
356
|
+
*
|
|
357
|
+
* @private
|
|
358
|
+
* @param {Object} row - Raw SQLite row
|
|
359
|
+
* @returns {Object} Task object with parsed steps and camelCase keys
|
|
360
|
+
*/
|
|
361
|
+
_rowToTask(row) {
|
|
362
|
+
return {
|
|
363
|
+
id: row.id,
|
|
364
|
+
userId: row.user_id,
|
|
365
|
+
description: row.description,
|
|
366
|
+
steps: JSON.parse(row.steps),
|
|
367
|
+
category: row.category,
|
|
368
|
+
priority: row.priority,
|
|
369
|
+
deadline: row.deadline,
|
|
370
|
+
mode: row.mode,
|
|
371
|
+
status: row.status,
|
|
372
|
+
currentStep: row.current_step,
|
|
373
|
+
progress: row.progress,
|
|
374
|
+
createdAt: row.created_at,
|
|
375
|
+
updatedAt: row.updated_at,
|
|
376
|
+
lastWorkedAt: row.last_worked_at,
|
|
377
|
+
};
|
|
378
|
+
}
|
|
379
|
+
|
|
380
|
+
/**
|
|
381
|
+
* Calculate progress percentage from a task object
|
|
382
|
+
* @private
|
|
383
|
+
* @param {Object} task - Task with steps array
|
|
384
|
+
* @returns {number} Progress 0-100
|
|
385
|
+
*/
|
|
386
|
+
_calculateProgress(task) {
|
|
387
|
+
return this._calculateProgressFromSteps(task.steps || []);
|
|
388
|
+
}
|
|
389
|
+
|
|
390
|
+
/**
|
|
391
|
+
* Calculate progress percentage from a steps array
|
|
392
|
+
* @private
|
|
393
|
+
* @param {Array} steps - Steps array
|
|
394
|
+
* @returns {number} Progress 0-100
|
|
395
|
+
*/
|
|
396
|
+
_calculateProgressFromSteps(steps) {
|
|
397
|
+
if (!steps || steps.length === 0) return 0;
|
|
398
|
+
const doneCount = steps.filter(s => s.done).length;
|
|
399
|
+
return Math.round((doneCount / steps.length) * 100);
|
|
400
|
+
}
|
|
401
|
+
|
|
402
|
+
/**
|
|
403
|
+
* Close the database connection and checkpoint WAL.
|
|
404
|
+
* Should be called on shutdown to ensure all changes are persisted.
|
|
405
|
+
*/
|
|
406
|
+
close() {
|
|
407
|
+
if (this.db) {
|
|
408
|
+
try {
|
|
409
|
+
// Force WAL checkpoint before closing
|
|
410
|
+
this.db.exec('PRAGMA wal_checkpoint(TRUNCATE)');
|
|
411
|
+
this.db.close();
|
|
412
|
+
this.db = null;
|
|
413
|
+
console.log('[tasks] SQLiteTaskStore closed');
|
|
414
|
+
} catch (err) {
|
|
415
|
+
console.error('[tasks] Error closing database:', err.message);
|
|
416
|
+
}
|
|
417
|
+
}
|
|
418
|
+
}
|
|
419
|
+
}
|
|
@@ -0,0 +1,262 @@
|
|
|
1
|
+
import crypto from 'crypto';
|
|
2
|
+
import { DatabaseSync } from 'node:sqlite';
|
|
3
|
+
import { TriggerStore } from './TriggerStore.js';
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* SQLite-backed TriggerStore implementation
|
|
7
|
+
*
|
|
8
|
+
* Uses Node.js 22.5+ built-in sqlite module for zero-dependency trigger storage.
|
|
9
|
+
* Dates stored as INTEGER (Unix ms timestamps), metadata as JSON TEXT.
|
|
10
|
+
*/
|
|
11
|
+
export class SQLiteTriggerStore extends TriggerStore {
|
|
12
|
+
constructor() {
|
|
13
|
+
super();
|
|
14
|
+
this.db = null;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
/**
|
|
18
|
+
* Initialize SQLite trigger store
|
|
19
|
+
*
|
|
20
|
+
* @param {Object} config - Configuration object
|
|
21
|
+
* @param {string} config.dbPath - Path to SQLite database file
|
|
22
|
+
* @param {Object} [options={}] - Reserved for future use
|
|
23
|
+
*/
|
|
24
|
+
async init({ dbPath }, options = {}) {
|
|
25
|
+
this.db = new DatabaseSync(dbPath);
|
|
26
|
+
|
|
27
|
+
this.db.exec(`
|
|
28
|
+
CREATE TABLE IF NOT EXISTS triggers (
|
|
29
|
+
id TEXT PRIMARY KEY,
|
|
30
|
+
user_id TEXT NOT NULL,
|
|
31
|
+
event_type TEXT NOT NULL,
|
|
32
|
+
prompt TEXT NOT NULL,
|
|
33
|
+
cooldown_ms INTEGER DEFAULT 0,
|
|
34
|
+
metadata TEXT,
|
|
35
|
+
enabled INTEGER DEFAULT 1,
|
|
36
|
+
last_fired_at INTEGER,
|
|
37
|
+
fire_count INTEGER DEFAULT 0,
|
|
38
|
+
created_at INTEGER NOT NULL,
|
|
39
|
+
updated_at INTEGER NOT NULL
|
|
40
|
+
);
|
|
41
|
+
|
|
42
|
+
CREATE INDEX IF NOT EXISTS idx_triggers_user_event ON triggers(user_id, event_type);
|
|
43
|
+
CREATE INDEX IF NOT EXISTS idx_triggers_user_enabled ON triggers(user_id, enabled);
|
|
44
|
+
`);
|
|
45
|
+
|
|
46
|
+
console.log('[triggers] SQLiteTriggerStore initialized');
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
/**
|
|
50
|
+
* Create an event trigger
|
|
51
|
+
*
|
|
52
|
+
* @param {Object} params
|
|
53
|
+
* @param {string} params.userId - Owner user ID
|
|
54
|
+
* @param {string} params.eventType - Event type to trigger on
|
|
55
|
+
* @param {string} params.prompt - Prompt to inject when event fires
|
|
56
|
+
* @param {number} [params.cooldownMs=0] - Cooldown period in milliseconds
|
|
57
|
+
* @param {Object} [params.metadata={}] - Additional metadata
|
|
58
|
+
* @param {boolean} [params.enabled=true] - Whether trigger is enabled
|
|
59
|
+
* @returns {Promise<Object>} Created trigger document
|
|
60
|
+
*/
|
|
61
|
+
async createTrigger({ userId, eventType, prompt, cooldownMs = 0, metadata = {}, enabled = true }) {
|
|
62
|
+
if (!this.db) throw new Error('Triggers not initialized. Call init() first.');
|
|
63
|
+
|
|
64
|
+
const now = Date.now();
|
|
65
|
+
const doc = {
|
|
66
|
+
id: crypto.randomUUID(),
|
|
67
|
+
userId,
|
|
68
|
+
eventType,
|
|
69
|
+
prompt,
|
|
70
|
+
cooldownMs,
|
|
71
|
+
metadata,
|
|
72
|
+
enabled,
|
|
73
|
+
lastFiredAt: null,
|
|
74
|
+
fireCount: 0,
|
|
75
|
+
createdAt: new Date(now),
|
|
76
|
+
updatedAt: new Date(now),
|
|
77
|
+
};
|
|
78
|
+
|
|
79
|
+
const stmt = this.db.prepare(`
|
|
80
|
+
INSERT INTO triggers (id, user_id, event_type, prompt, cooldown_ms, metadata, enabled, last_fired_at, fire_count, created_at, updated_at)
|
|
81
|
+
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
|
|
82
|
+
`);
|
|
83
|
+
|
|
84
|
+
stmt.run(
|
|
85
|
+
doc.id,
|
|
86
|
+
userId,
|
|
87
|
+
eventType,
|
|
88
|
+
prompt,
|
|
89
|
+
cooldownMs,
|
|
90
|
+
Object.keys(metadata).length > 0 ? JSON.stringify(metadata) : null,
|
|
91
|
+
enabled ? 1 : 0,
|
|
92
|
+
null,
|
|
93
|
+
0,
|
|
94
|
+
now,
|
|
95
|
+
now
|
|
96
|
+
);
|
|
97
|
+
|
|
98
|
+
return doc;
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
/**
|
|
102
|
+
* List triggers for a user
|
|
103
|
+
*
|
|
104
|
+
* @param {string} userId - User ID
|
|
105
|
+
* @param {Object} [filters={}] - Optional filters
|
|
106
|
+
* @param {boolean} [filters.enabled] - Filter by enabled state
|
|
107
|
+
* @param {string} [filters.eventType] - Filter by event type
|
|
108
|
+
* @returns {Promise<Array>} Trigger list sorted by created_at DESC
|
|
109
|
+
*/
|
|
110
|
+
async listTriggers(userId, filters = {}) {
|
|
111
|
+
if (!this.db) throw new Error('Triggers not initialized. Call init() first.');
|
|
112
|
+
|
|
113
|
+
let sql = 'SELECT * FROM triggers WHERE user_id = ?';
|
|
114
|
+
const params = [userId];
|
|
115
|
+
|
|
116
|
+
if (filters.enabled !== undefined) {
|
|
117
|
+
sql += ' AND enabled = ?';
|
|
118
|
+
params.push(filters.enabled ? 1 : 0);
|
|
119
|
+
}
|
|
120
|
+
if (filters.eventType) {
|
|
121
|
+
sql += ' AND event_type = ?';
|
|
122
|
+
params.push(filters.eventType);
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
sql += ' ORDER BY created_at DESC';
|
|
126
|
+
|
|
127
|
+
const stmt = this.db.prepare(sql);
|
|
128
|
+
const rows = stmt.all(...params);
|
|
129
|
+
|
|
130
|
+
return rows.map(row => this._rowToTrigger(row));
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
/**
|
|
134
|
+
* Find enabled triggers matching userId and eventType, filtering out
|
|
135
|
+
* those still within cooldown period
|
|
136
|
+
*
|
|
137
|
+
* @param {string} userId - User ID
|
|
138
|
+
* @param {string} eventType - Event type to match
|
|
139
|
+
* @param {Object} [metadata={}] - Event metadata for matching
|
|
140
|
+
* @returns {Promise<Array>} Matching trigger documents
|
|
141
|
+
*/
|
|
142
|
+
async findMatchingTriggers(userId, eventType, metadata = {}) {
|
|
143
|
+
if (!this.db) throw new Error('Triggers not initialized. Call init() first.');
|
|
144
|
+
|
|
145
|
+
const now = Date.now();
|
|
146
|
+
|
|
147
|
+
const stmt = this.db.prepare(
|
|
148
|
+
'SELECT * FROM triggers WHERE user_id = ? AND event_type = ? AND enabled = 1'
|
|
149
|
+
);
|
|
150
|
+
const rows = stmt.all(userId, eventType);
|
|
151
|
+
const triggers = rows.map(row => this._rowToTrigger(row));
|
|
152
|
+
|
|
153
|
+
// Filter by cooldown
|
|
154
|
+
const activeTriggers = triggers.filter(trigger => {
|
|
155
|
+
if (!trigger.cooldownMs) return true;
|
|
156
|
+
if (!trigger.lastFiredAt) return true;
|
|
157
|
+
const lastFiredTime = trigger.lastFiredAt.getTime();
|
|
158
|
+
return (lastFiredTime + trigger.cooldownMs) <= now;
|
|
159
|
+
});
|
|
160
|
+
|
|
161
|
+
// Filter by metadata requirements
|
|
162
|
+
const matchedTriggers = activeTriggers.filter(trigger => {
|
|
163
|
+
if (!trigger.metadata || Object.keys(trigger.metadata).length === 0) {
|
|
164
|
+
return true;
|
|
165
|
+
}
|
|
166
|
+
for (const [key, value] of Object.entries(trigger.metadata)) {
|
|
167
|
+
if (metadata[key] !== value) return false;
|
|
168
|
+
}
|
|
169
|
+
return true;
|
|
170
|
+
});
|
|
171
|
+
|
|
172
|
+
return matchedTriggers;
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
/**
|
|
176
|
+
* Toggle a trigger on/off
|
|
177
|
+
*
|
|
178
|
+
* @param {string} userId - User ID
|
|
179
|
+
* @param {string} triggerId - Trigger ID
|
|
180
|
+
* @param {boolean} enabled - Whether to enable or disable
|
|
181
|
+
* @returns {Promise<Object>} Update result with changes count
|
|
182
|
+
*/
|
|
183
|
+
async toggleTrigger(userId, triggerId, enabled) {
|
|
184
|
+
if (!this.db) throw new Error('Triggers not initialized. Call init() first.');
|
|
185
|
+
|
|
186
|
+
const stmt = this.db.prepare(
|
|
187
|
+
'UPDATE triggers SET enabled = ?, updated_at = ? WHERE id = ? AND user_id = ?'
|
|
188
|
+
);
|
|
189
|
+
const result = stmt.run(enabled ? 1 : 0, Date.now(), triggerId, userId);
|
|
190
|
+
return { changes: result.changes };
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
/**
|
|
194
|
+
* Delete a trigger
|
|
195
|
+
*
|
|
196
|
+
* @param {string} userId - User ID
|
|
197
|
+
* @param {string} triggerId - Trigger ID
|
|
198
|
+
* @returns {Promise<Object>} Delete result with deletedCount
|
|
199
|
+
*/
|
|
200
|
+
async deleteTrigger(userId, triggerId) {
|
|
201
|
+
if (!this.db) throw new Error('Triggers not initialized. Call init() first.');
|
|
202
|
+
|
|
203
|
+
const stmt = this.db.prepare(
|
|
204
|
+
'DELETE FROM triggers WHERE id = ? AND user_id = ?'
|
|
205
|
+
);
|
|
206
|
+
const result = stmt.run(triggerId, userId);
|
|
207
|
+
return { deletedCount: result.changes };
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
/**
|
|
211
|
+
* Record that a trigger has fired
|
|
212
|
+
*
|
|
213
|
+
* @param {string} triggerId - Trigger ID
|
|
214
|
+
*/
|
|
215
|
+
async markTriggerFired(triggerId) {
|
|
216
|
+
if (!this.db) throw new Error('Triggers not initialized. Call init() first.');
|
|
217
|
+
|
|
218
|
+
const stmt = this.db.prepare(
|
|
219
|
+
'UPDATE triggers SET last_fired_at = ?, fire_count = fire_count + 1 WHERE id = ?'
|
|
220
|
+
);
|
|
221
|
+
stmt.run(Date.now(), triggerId);
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
/**
|
|
225
|
+
* Convert SQLite row to trigger object
|
|
226
|
+
*
|
|
227
|
+
* @private
|
|
228
|
+
* @param {Object} row - Raw SQLite row
|
|
229
|
+
* @returns {Object} Trigger object with parsed dates and metadata
|
|
230
|
+
*/
|
|
231
|
+
_rowToTrigger(row) {
|
|
232
|
+
return {
|
|
233
|
+
id: row.id,
|
|
234
|
+
userId: row.user_id,
|
|
235
|
+
eventType: row.event_type,
|
|
236
|
+
prompt: row.prompt,
|
|
237
|
+
cooldownMs: row.cooldown_ms,
|
|
238
|
+
metadata: row.metadata ? JSON.parse(row.metadata) : {},
|
|
239
|
+
enabled: row.enabled === 1,
|
|
240
|
+
lastFiredAt: row.last_fired_at ? new Date(row.last_fired_at) : null,
|
|
241
|
+
fireCount: row.fire_count,
|
|
242
|
+
createdAt: new Date(row.created_at),
|
|
243
|
+
updatedAt: new Date(row.updated_at),
|
|
244
|
+
};
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
/**
|
|
248
|
+
* Close the database connection and checkpoint WAL.
|
|
249
|
+
*/
|
|
250
|
+
close() {
|
|
251
|
+
if (this.db) {
|
|
252
|
+
try {
|
|
253
|
+
this.db.exec('PRAGMA wal_checkpoint(TRUNCATE)');
|
|
254
|
+
this.db.close();
|
|
255
|
+
this.db = null;
|
|
256
|
+
console.log('[triggers] SQLiteTriggerStore closed');
|
|
257
|
+
} catch (err) {
|
|
258
|
+
console.error('[triggers] Error closing database:', err.message);
|
|
259
|
+
}
|
|
260
|
+
}
|
|
261
|
+
}
|
|
262
|
+
}
|