@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,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
|
+
}
|