kratos-memory 1.0.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 (120) hide show
  1. package/AGENTS.md +25 -0
  2. package/LICENSE +21 -0
  3. package/bin/kratos-cli +7 -0
  4. package/dist/cli/capture-handler.d.ts +13 -0
  5. package/dist/cli/capture-handler.d.ts.map +1 -0
  6. package/dist/cli/capture-handler.js +112 -0
  7. package/dist/cli/capture-handler.js.map +1 -0
  8. package/dist/cli/commands/ask.d.ts +5 -0
  9. package/dist/cli/commands/ask.d.ts.map +1 -0
  10. package/dist/cli/commands/ask.js +64 -0
  11. package/dist/cli/commands/ask.js.map +1 -0
  12. package/dist/cli/commands/capture.d.ts +5 -0
  13. package/dist/cli/commands/capture.d.ts.map +1 -0
  14. package/dist/cli/commands/capture.js +31 -0
  15. package/dist/cli/commands/capture.js.map +1 -0
  16. package/dist/cli/commands/forget.d.ts +3 -0
  17. package/dist/cli/commands/forget.d.ts.map +1 -0
  18. package/dist/cli/commands/forget.js +12 -0
  19. package/dist/cli/commands/forget.js.map +1 -0
  20. package/dist/cli/commands/get.d.ts +3 -0
  21. package/dist/cli/commands/get.d.ts.map +1 -0
  22. package/dist/cli/commands/get.js +28 -0
  23. package/dist/cli/commands/get.js.map +1 -0
  24. package/dist/cli/commands/hooks.d.ts +2 -0
  25. package/dist/cli/commands/hooks.d.ts.map +1 -0
  26. package/dist/cli/commands/hooks.js +136 -0
  27. package/dist/cli/commands/hooks.js.map +1 -0
  28. package/dist/cli/commands/migrate.d.ts +5 -0
  29. package/dist/cli/commands/migrate.d.ts.map +1 -0
  30. package/dist/cli/commands/migrate.js +56 -0
  31. package/dist/cli/commands/migrate.js.map +1 -0
  32. package/dist/cli/commands/recent.d.ts +6 -0
  33. package/dist/cli/commands/recent.d.ts.map +1 -0
  34. package/dist/cli/commands/recent.js +21 -0
  35. package/dist/cli/commands/recent.js.map +1 -0
  36. package/dist/cli/commands/save.d.ts +8 -0
  37. package/dist/cli/commands/save.d.ts.map +1 -0
  38. package/dist/cli/commands/save.js +31 -0
  39. package/dist/cli/commands/save.js.map +1 -0
  40. package/dist/cli/commands/scan.d.ts +5 -0
  41. package/dist/cli/commands/scan.d.ts.map +1 -0
  42. package/dist/cli/commands/scan.js +28 -0
  43. package/dist/cli/commands/scan.js.map +1 -0
  44. package/dist/cli/commands/search.d.ts +8 -0
  45. package/dist/cli/commands/search.d.ts.map +1 -0
  46. package/dist/cli/commands/search.js +45 -0
  47. package/dist/cli/commands/search.js.map +1 -0
  48. package/dist/cli/commands/status.d.ts +3 -0
  49. package/dist/cli/commands/status.d.ts.map +1 -0
  50. package/dist/cli/commands/status.js +89 -0
  51. package/dist/cli/commands/status.js.map +1 -0
  52. package/dist/cli/commands/switch.d.ts +3 -0
  53. package/dist/cli/commands/switch.d.ts.map +1 -0
  54. package/dist/cli/commands/switch.js +18 -0
  55. package/dist/cli/commands/switch.js.map +1 -0
  56. package/dist/cli/core.d.ts +15 -0
  57. package/dist/cli/core.d.ts.map +1 -0
  58. package/dist/cli/core.js +18 -0
  59. package/dist/cli/core.js.map +1 -0
  60. package/dist/cli/index.d.ts +3 -0
  61. package/dist/cli/index.d.ts.map +1 -0
  62. package/dist/cli/index.js +157 -0
  63. package/dist/cli/index.js.map +1 -0
  64. package/dist/cli/output.d.ts +22 -0
  65. package/dist/cli/output.d.ts.map +1 -0
  66. package/dist/cli/output.js +74 -0
  67. package/dist/cli/output.js.map +1 -0
  68. package/dist/compression/factory.d.ts +6 -0
  69. package/dist/compression/factory.d.ts.map +1 -0
  70. package/dist/compression/factory.js +8 -0
  71. package/dist/compression/factory.js.map +1 -0
  72. package/dist/compression/index.d.ts +10 -0
  73. package/dist/compression/index.d.ts.map +1 -0
  74. package/dist/compression/index.js +2 -0
  75. package/dist/compression/index.js.map +1 -0
  76. package/dist/compression/rule-compressor.d.ts +9 -0
  77. package/dist/compression/rule-compressor.d.ts.map +1 -0
  78. package/dist/compression/rule-compressor.js +43 -0
  79. package/dist/compression/rule-compressor.js.map +1 -0
  80. package/dist/memory-server/concept-store-enhanced.d.ts +88 -0
  81. package/dist/memory-server/concept-store-enhanced.d.ts.map +1 -0
  82. package/dist/memory-server/concept-store-enhanced.js +392 -0
  83. package/dist/memory-server/concept-store-enhanced.js.map +1 -0
  84. package/dist/memory-server/concept-store.d.ts +58 -0
  85. package/dist/memory-server/concept-store.d.ts.map +1 -0
  86. package/dist/memory-server/concept-store.js +329 -0
  87. package/dist/memory-server/concept-store.js.map +1 -0
  88. package/dist/memory-server/context-broker.d.ts +63 -0
  89. package/dist/memory-server/context-broker.d.ts.map +1 -0
  90. package/dist/memory-server/context-broker.js +340 -0
  91. package/dist/memory-server/context-broker.js.map +1 -0
  92. package/dist/memory-server/database.d.ts +108 -0
  93. package/dist/memory-server/database.d.ts.map +1 -0
  94. package/dist/memory-server/database.js +690 -0
  95. package/dist/memory-server/database.js.map +1 -0
  96. package/dist/project-manager.d.ts +77 -0
  97. package/dist/project-manager.d.ts.map +1 -0
  98. package/dist/project-manager.js +226 -0
  99. package/dist/project-manager.js.map +1 -0
  100. package/dist/security/data-retention.d.ts +104 -0
  101. package/dist/security/data-retention.d.ts.map +1 -0
  102. package/dist/security/data-retention.js +444 -0
  103. package/dist/security/data-retention.js.map +1 -0
  104. package/dist/security/encryption.d.ts +48 -0
  105. package/dist/security/encryption.d.ts.map +1 -0
  106. package/dist/security/encryption.js +131 -0
  107. package/dist/security/encryption.js.map +1 -0
  108. package/dist/security/pii-detector.d.ts +61 -0
  109. package/dist/security/pii-detector.d.ts.map +1 -0
  110. package/dist/security/pii-detector.js +220 -0
  111. package/dist/security/pii-detector.js.map +1 -0
  112. package/dist/types/index.d.ts +151 -0
  113. package/dist/types/index.d.ts.map +1 -0
  114. package/dist/types/index.js +2 -0
  115. package/dist/types/index.js.map +1 -0
  116. package/dist/utils/logger.d.ts +9 -0
  117. package/dist/utils/logger.d.ts.map +1 -0
  118. package/dist/utils/logger.js +10 -0
  119. package/dist/utils/logger.js.map +1 -0
  120. package/package.json +54 -0
@@ -0,0 +1,690 @@
1
+ import Database from 'better-sqlite3';
2
+ import path from 'path';
3
+ import fs from 'fs-extra';
4
+ import crypto from 'crypto';
5
+ import { Logger } from '../utils/logger.js';
6
+ const logger = new Logger('MemoryDB');
7
+ export class MemoryDatabase {
8
+ db;
9
+ projectId;
10
+ projectRoot;
11
+ constructor(projectRoot, projectId) {
12
+ this.projectRoot = projectRoot;
13
+ this.projectId = projectId;
14
+ // CRITICAL: Each project gets COMPLETELY ISOLATED database
15
+ // Path: ~/.kratos/projects/{project_id}/databases/memories.db
16
+ // This ensures NO cross-contamination between projects
17
+ const kratosHome = path.join(process.env.HOME || process.env.USERPROFILE || '', '.kratos');
18
+ const dbPath = path.join(kratosHome, 'projects', projectId, 'databases', 'memories.db');
19
+ fs.ensureDirSync(path.dirname(dbPath));
20
+ this.db = new Database(dbPath);
21
+ this.db.pragma('journal_mode = WAL');
22
+ this.db.pragma('foreign_keys = ON');
23
+ this.initializeSchema();
24
+ this.setupTriggers();
25
+ logger.info(`Memory database ISOLATED for project: ${projectId} at ${dbPath}`);
26
+ }
27
+ initializeSchema() {
28
+ // Main memories table
29
+ this.db.exec(`
30
+ CREATE TABLE IF NOT EXISTS memories (
31
+ id TEXT PRIMARY KEY,
32
+ project_id TEXT NOT NULL,
33
+ summary TEXT NOT NULL,
34
+ text TEXT NOT NULL,
35
+ tags TEXT DEFAULT '[]',
36
+ paths TEXT DEFAULT '[]',
37
+ importance INTEGER DEFAULT 3 CHECK(importance >= 1 AND importance <= 5),
38
+ created_at INTEGER NOT NULL,
39
+ updated_at INTEGER NOT NULL,
40
+ ttl INTEGER,
41
+ expires_at INTEGER,
42
+ dedupe_hash TEXT
43
+ );
44
+
45
+ CREATE INDEX IF NOT EXISTS idx_mem_project ON memories(project_id);
46
+ CREATE INDEX IF NOT EXISTS idx_mem_expires ON memories(expires_at) WHERE expires_at IS NOT NULL;
47
+ CREATE INDEX IF NOT EXISTS idx_mem_importance ON memories(importance DESC, created_at DESC);
48
+ CREATE INDEX IF NOT EXISTS idx_mem_dedupe ON memories(dedupe_hash);
49
+ `);
50
+ // Full-text search virtual table - INCLUDING TAGS for better search
51
+ this.db.exec(`
52
+ CREATE VIRTUAL TABLE IF NOT EXISTS mem_fts USING fts5(
53
+ summary,
54
+ text,
55
+ tags,
56
+ content='memories',
57
+ content_rowid='rowid',
58
+ tokenize='porter unicode61'
59
+ );
60
+
61
+ -- Triggers to keep FTS in sync - now including tags
62
+ CREATE TRIGGER IF NOT EXISTS mem_fts_insert AFTER INSERT ON memories BEGIN
63
+ INSERT INTO mem_fts(rowid, summary, text, tags)
64
+ VALUES (new.rowid, new.summary, new.text,
65
+ CASE WHEN json_array_length(new.tags) > 0
66
+ THEN (SELECT group_concat(value, ' ') FROM json_each(new.tags))
67
+ ELSE ''
68
+ END);
69
+ END;
70
+
71
+ CREATE TRIGGER IF NOT EXISTS mem_fts_delete AFTER DELETE ON memories BEGIN
72
+ DELETE FROM mem_fts WHERE rowid = old.rowid;
73
+ END;
74
+
75
+ CREATE TRIGGER IF NOT EXISTS mem_fts_update AFTER UPDATE ON memories BEGIN
76
+ DELETE FROM mem_fts WHERE rowid = old.rowid;
77
+ INSERT INTO mem_fts(rowid, summary, text, tags)
78
+ VALUES (new.rowid, new.summary, new.text,
79
+ CASE WHEN json_array_length(new.tags) > 0
80
+ THEN (SELECT group_concat(value, ' ') FROM json_each(new.tags))
81
+ ELSE ''
82
+ END);
83
+ END;
84
+ `);
85
+ }
86
+ setupTriggers() {
87
+ // Auto-cleanup expired memories
88
+ setInterval(() => {
89
+ this.cleanupExpired();
90
+ }, 60 * 60 * 1000); // Every hour
91
+ }
92
+ save(params) {
93
+ // Project isolation is enforced by the database path itself
94
+ // Each project has its own database file, so no cross-contamination is possible
95
+ const now = Date.now();
96
+ const id = this.generateId();
97
+ // Compute dedupe hash
98
+ const dedupeHash = this.computeDedupeHash(params.summary, params.paths || []);
99
+ // Check for duplicates
100
+ const existing = this.db.prepare('SELECT id FROM memories WHERE dedupe_hash = ? AND project_id = ?').get(dedupeHash, this.projectId);
101
+ if (existing && typeof existing === 'object' && 'id' in existing) {
102
+ logger.info(`Duplicate memory detected, updating existing: ${existing.id}`);
103
+ return this.update(existing.id, params);
104
+ }
105
+ // Calculate expiration
106
+ const expires_at = params.ttl ? now + (params.ttl * 1000) : null;
107
+ const stmt = this.db.prepare(`
108
+ INSERT INTO memories (
109
+ id, project_id, summary, text, tags, paths,
110
+ importance, created_at, updated_at, ttl, expires_at, dedupe_hash
111
+ ) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
112
+ `);
113
+ stmt.run(id, this.projectId, params.summary, params.text, JSON.stringify(params.tags || []), JSON.stringify(params.paths || []), params.importance || 3, now, now, params.ttl || null, expires_at, dedupeHash);
114
+ logger.info(`Memory saved: ${id} - ${params.summary}`);
115
+ // Return the complete memory object
116
+ const memory = {
117
+ id,
118
+ project_id: this.projectId,
119
+ summary: params.summary,
120
+ text: params.text,
121
+ tags: params.tags || [],
122
+ paths: params.paths || [],
123
+ importance: params.importance || 3,
124
+ created_at: now,
125
+ updated_at: now,
126
+ ttl: params.ttl,
127
+ expires_at: expires_at || undefined
128
+ };
129
+ return memory;
130
+ }
131
+ search(params) {
132
+ const k = params.k || 10;
133
+ // Try primary search
134
+ try {
135
+ const results = this.executeSearch(params);
136
+ if (results.length > 0) {
137
+ return results;
138
+ }
139
+ }
140
+ catch (error) {
141
+ // Primary search failed, try fallbacks
142
+ console.warn('Primary search failed, trying fallbacks:', error);
143
+ }
144
+ // Fallback 1: Try without special characters
145
+ if (params.q.match(/[^\w\s]/)) {
146
+ try {
147
+ const fallbackQuery = params.q.replace(/[^\w\s]/g, ' ').replace(/\s+/g, ' ').trim();
148
+ const results = this.executeSearch({ ...params, q: fallbackQuery });
149
+ if (results.length > 0) {
150
+ return results;
151
+ }
152
+ }
153
+ catch (error) {
154
+ console.warn('Fallback 1 failed:', error);
155
+ }
156
+ }
157
+ // Fallback 2: Try individual words (OR search)
158
+ const words = params.q.split(/\s+/).filter(word => word.length > 2);
159
+ if (words.length > 1) {
160
+ try {
161
+ const orQuery = words.join(' OR ');
162
+ const results = this.executeSearch({ ...params, q: orQuery });
163
+ if (results.length > 0) {
164
+ return results;
165
+ }
166
+ }
167
+ catch (error) {
168
+ console.warn('Fallback 2 failed:', error);
169
+ }
170
+ }
171
+ // Fallback 3: Try broader search with just the first word
172
+ if (words.length > 0) {
173
+ try {
174
+ const results = this.executeSearch({ ...params, q: words[0] });
175
+ return results; // Return whatever we get, even if empty
176
+ }
177
+ catch (error) {
178
+ console.warn('All fallbacks failed:', error);
179
+ }
180
+ }
181
+ return []; // No results found
182
+ }
183
+ searchWithDebug(params) {
184
+ const startTime = Date.now();
185
+ const queries_tried = [];
186
+ let fallback_used;
187
+ // Try primary search
188
+ queries_tried.push(params.q);
189
+ try {
190
+ const results = this.executeSearch(params);
191
+ if (results.length > 0) {
192
+ return {
193
+ results,
194
+ debug_info: {
195
+ original_query: params.q,
196
+ queries_tried,
197
+ search_time_ms: Date.now() - startTime,
198
+ total_memories_scanned: this.getTotalMemoryCount()
199
+ }
200
+ };
201
+ }
202
+ }
203
+ catch (error) {
204
+ // Continue to fallbacks
205
+ }
206
+ // Fallback 1: Try without special characters
207
+ if (params.q.match(/[^\w\s]/)) {
208
+ const fallbackQuery = params.q.replace(/[^\w\s]/g, ' ').replace(/\s+/g, ' ').trim();
209
+ queries_tried.push(fallbackQuery);
210
+ try {
211
+ const results = this.executeSearch({ ...params, q: fallbackQuery });
212
+ if (results.length > 0) {
213
+ return {
214
+ results,
215
+ debug_info: {
216
+ original_query: params.q,
217
+ queries_tried,
218
+ fallback_used: 'removed_special_chars',
219
+ search_time_ms: Date.now() - startTime,
220
+ total_memories_scanned: this.getTotalMemoryCount()
221
+ }
222
+ };
223
+ }
224
+ }
225
+ catch (error) {
226
+ // Continue to next fallback
227
+ }
228
+ }
229
+ // Fallback 2: Try individual words (OR search)
230
+ const words = params.q.split(/\s+/).filter(word => word.length > 2);
231
+ if (words.length > 1) {
232
+ const orQuery = words.join(' OR ');
233
+ queries_tried.push(orQuery);
234
+ try {
235
+ const results = this.executeSearch({ ...params, q: orQuery });
236
+ if (results.length > 0) {
237
+ return {
238
+ results,
239
+ debug_info: {
240
+ original_query: params.q,
241
+ queries_tried,
242
+ fallback_used: 'or_search',
243
+ search_time_ms: Date.now() - startTime,
244
+ total_memories_scanned: this.getTotalMemoryCount()
245
+ }
246
+ };
247
+ }
248
+ }
249
+ catch (error) {
250
+ // Continue to next fallback
251
+ }
252
+ }
253
+ // Fallback 3: Try broader search with just the first word
254
+ if (words.length > 0) {
255
+ queries_tried.push(words[0]);
256
+ try {
257
+ const results = this.executeSearch({ ...params, q: words[0] });
258
+ return {
259
+ results,
260
+ debug_info: {
261
+ original_query: params.q,
262
+ queries_tried,
263
+ fallback_used: 'broad_search',
264
+ search_time_ms: Date.now() - startTime,
265
+ total_memories_scanned: this.getTotalMemoryCount()
266
+ }
267
+ };
268
+ }
269
+ catch (error) {
270
+ // All fallbacks failed
271
+ }
272
+ }
273
+ return {
274
+ results: [],
275
+ debug_info: {
276
+ original_query: params.q,
277
+ queries_tried,
278
+ fallback_used: 'all_failed',
279
+ search_time_ms: Date.now() - startTime,
280
+ total_memories_scanned: this.getTotalMemoryCount()
281
+ }
282
+ };
283
+ }
284
+ getTotalMemoryCount() {
285
+ const stmt = this.db.prepare('SELECT COUNT(*) as count FROM memories WHERE project_id = ?');
286
+ const result = stmt.get(this.projectId);
287
+ return result.count;
288
+ }
289
+ executeSearch(params) {
290
+ const k = params.k || 10;
291
+ const now = Date.now();
292
+ // Build FTS query
293
+ let query = `
294
+ SELECT
295
+ m.*,
296
+ bm25(mem_fts) as fts_score,
297
+ snippet(mem_fts, 0, '[', ']', '...', 32) as snippet
298
+ FROM memories m
299
+ JOIN mem_fts ON m.rowid = mem_fts.rowid
300
+ WHERE mem_fts MATCH ?
301
+ AND m.project_id = ?
302
+ `;
303
+ const queryParams = [this.escapeQuery(params.q), this.projectId];
304
+ // Add expiration filter
305
+ if (!params.include_expired) {
306
+ query += ' AND (m.expires_at IS NULL OR m.expires_at > ?)';
307
+ queryParams.push(now);
308
+ }
309
+ // Add tag filter
310
+ if (params.tags && params.tags.length > 0) {
311
+ query += ' AND EXISTS (SELECT 1 FROM json_each(m.tags) WHERE value IN (' +
312
+ params.tags.map(() => '?').join(',') + '))';
313
+ queryParams.push(...params.tags);
314
+ }
315
+ // Add path matching filter
316
+ if (params.require_path_match) {
317
+ // Filter by paths that exist relative to current working directory
318
+ const cwd = process.cwd();
319
+ // Use EXISTS to check if any path in the JSON array exists relative to cwd
320
+ query += ` AND EXISTS (
321
+ SELECT 1 FROM json_each(m.paths) as path_item
322
+ WHERE
323
+ -- Check if it's an absolute path under cwd
324
+ (path_item.value LIKE ? || '%') OR
325
+ -- Check if it's a relative path that exists from cwd
326
+ (path_item.value NOT LIKE '/%' AND path_item.value NOT LIKE 'C:%' AND path_item.value NOT LIKE '~%')
327
+ )`;
328
+ queryParams.push(cwd + '/');
329
+ }
330
+ query += ' ORDER BY fts_score DESC, m.importance DESC, m.created_at DESC LIMIT ?';
331
+ queryParams.push(k);
332
+ const stmt = this.db.prepare(query);
333
+ const results = stmt.all(...queryParams);
334
+ return results.map(row => ({
335
+ memory: this.rowToMemory(row),
336
+ score: -row.fts_score, // BM25 returns negative scores
337
+ snippet: row.snippet
338
+ }));
339
+ }
340
+ getRecent(params) {
341
+ const k = params.k || 10;
342
+ const now = Date.now();
343
+ let query = `
344
+ SELECT * FROM memories
345
+ WHERE project_id = ?
346
+ `;
347
+ const queryParams = [this.projectId];
348
+ if (!params.include_expired) {
349
+ query += ' AND (expires_at IS NULL OR expires_at > ?)';
350
+ queryParams.push(now);
351
+ }
352
+ if (params.path_prefix) {
353
+ query += ` AND EXISTS (
354
+ SELECT 1 FROM json_each(paths)
355
+ WHERE value LIKE ? || '%'
356
+ )`;
357
+ queryParams.push(params.path_prefix);
358
+ }
359
+ query += ' ORDER BY created_at DESC LIMIT ?';
360
+ queryParams.push(k);
361
+ const stmt = this.db.prepare(query);
362
+ const results = stmt.all(...queryParams);
363
+ return results.map(row => this.rowToMemory(row));
364
+ }
365
+ // Get a single memory by ID with full text
366
+ get(id) {
367
+ const stmt = this.db.prepare(`
368
+ SELECT * FROM memories
369
+ WHERE id = ? AND project_id = ?
370
+ `);
371
+ const result = stmt.get(id, this.projectId);
372
+ if (!result) {
373
+ return null;
374
+ }
375
+ return this.rowToMemory(result);
376
+ }
377
+ // Get multiple memories by IDs (bulk operation)
378
+ getMultiple(ids) {
379
+ const result = {};
380
+ if (ids.length === 0) {
381
+ return result;
382
+ }
383
+ // Build query with placeholders for all IDs
384
+ const placeholders = ids.map(() => '?').join(',');
385
+ const stmt = this.db.prepare(`
386
+ SELECT * FROM memories
387
+ WHERE id IN (${placeholders}) AND project_id = ?
388
+ `);
389
+ const queryParams = [...ids, this.projectId];
390
+ const results = stmt.all(...queryParams);
391
+ // Initialize all IDs as null (not found)
392
+ ids.forEach(id => {
393
+ result[id] = null;
394
+ });
395
+ // Fill in found memories
396
+ results.forEach(row => {
397
+ const memory = this.rowToMemory(row);
398
+ result[memory.id] = memory;
399
+ });
400
+ return result;
401
+ }
402
+ forget(id) {
403
+ try {
404
+ // Project isolation is enforced - each project has its own database
405
+ // Check if memory exists first
406
+ const checkStmt = this.db.prepare('SELECT id FROM memories WHERE id = ? AND project_id = ?');
407
+ const exists = checkStmt.get(id, this.projectId);
408
+ if (!exists) {
409
+ return {
410
+ ok: false,
411
+ message: `Memory ${id} not found in project ${this.projectId}`
412
+ };
413
+ }
414
+ // Delete from main table
415
+ const stmt = this.db.prepare('DELETE FROM memories WHERE id = ? AND project_id = ?');
416
+ const result = stmt.run(id, this.projectId);
417
+ // Try to delete from FTS index if it exists
418
+ try {
419
+ const ftsExists = this.db.prepare("SELECT name FROM sqlite_master WHERE type='table' AND name='memories_fts'").get();
420
+ if (ftsExists) {
421
+ const ftsStmt = this.db.prepare('DELETE FROM memories_fts WHERE rowid = (SELECT rowid FROM memories WHERE id = ?)');
422
+ ftsStmt.run(id);
423
+ }
424
+ }
425
+ catch (ftsError) {
426
+ // FTS table might not exist, that's okay
427
+ logger.debug('FTS deletion skipped:', ftsError);
428
+ }
429
+ logger.info(`Memory deleted: ${id}`);
430
+ return {
431
+ ok: result.changes > 0,
432
+ message: result.changes > 0 ? 'Memory deleted successfully' : 'Memory not found'
433
+ };
434
+ }
435
+ catch (error) {
436
+ logger.error(`Failed to delete memory ${id}:`, error);
437
+ return {
438
+ ok: false,
439
+ message: `Error: ${error.message}`
440
+ };
441
+ }
442
+ }
443
+ update(id, params) {
444
+ const now = Date.now();
445
+ const updates = ['updated_at = ?'];
446
+ const values = [now];
447
+ if (params.summary !== undefined) {
448
+ updates.push('summary = ?');
449
+ values.push(params.summary);
450
+ }
451
+ if (params.text !== undefined) {
452
+ updates.push('text = ?');
453
+ values.push(params.text);
454
+ }
455
+ if (params.tags !== undefined) {
456
+ updates.push('tags = ?');
457
+ values.push(JSON.stringify(params.tags));
458
+ }
459
+ if (params.paths !== undefined) {
460
+ updates.push('paths = ?');
461
+ values.push(JSON.stringify(params.paths));
462
+ }
463
+ if (params.importance !== undefined) {
464
+ updates.push('importance = ?');
465
+ values.push(params.importance);
466
+ }
467
+ values.push(id, this.projectId);
468
+ const stmt = this.db.prepare(`
469
+ UPDATE memories
470
+ SET ${updates.join(', ')}
471
+ WHERE id = ? AND project_id = ?
472
+ `);
473
+ stmt.run(...values);
474
+ return { id };
475
+ }
476
+ cleanupExpired() {
477
+ const now = Date.now();
478
+ const stmt = this.db.prepare('DELETE FROM memories WHERE expires_at < ?');
479
+ const result = stmt.run(now);
480
+ if (result.changes > 0) {
481
+ logger.info(`Cleaned up ${result.changes} expired memories`);
482
+ }
483
+ }
484
+ searchPreview(params) {
485
+ const startTime = Date.now();
486
+ const now = Date.now();
487
+ const k = params.k || 10;
488
+ // Process query the same way as real search
489
+ const escapedQuery = this.escapeQuery(params.q);
490
+ const terms = params.q.toLowerCase().split(/\s+/).filter(t => t.length > 0);
491
+ let filtersApplied = [];
492
+ let baseQuery = `
493
+ SELECT COUNT(*) as total_matches,
494
+ m.summary,
495
+ bm25(mem_fts) as fts_score,
496
+ snippet(mem_fts, 0, '[', ']', '...', 32) as snippet
497
+ FROM memories m
498
+ JOIN mem_fts ON m.rowid = mem_fts.rowid
499
+ WHERE mem_fts MATCH ?
500
+ AND m.project_id = ?
501
+ `;
502
+ let queryParams = [escapedQuery, this.projectId];
503
+ // Apply filters same as real search
504
+ if (!params.include_expired) {
505
+ baseQuery += ' AND (m.expires_at IS NULL OR m.expires_at > ?)';
506
+ queryParams.push(now);
507
+ filtersApplied.push('Excluding expired memories');
508
+ }
509
+ if (params.tags && params.tags.length > 0) {
510
+ const tagConditions = params.tags.map(() => 'json_extract(m.tags, ?) IS NOT NULL').join(' AND ');
511
+ baseQuery += ` AND (${tagConditions})`;
512
+ params.tags.forEach((tag, i) => queryParams.push(`$[${i}]`));
513
+ filtersApplied.push(`Filtering by tags: ${params.tags.join(', ')}`);
514
+ }
515
+ if (params.require_path_match) {
516
+ const cwd = process.cwd();
517
+ baseQuery += ` AND EXISTS (
518
+ SELECT 1 FROM json_each(m.paths) as path_item
519
+ WHERE
520
+ (path_item.value LIKE ? || '%') OR
521
+ (path_item.value NOT LIKE '/%' AND path_item.value NOT LIKE 'C:%' AND path_item.value NOT LIKE '~%')
522
+ )`;
523
+ queryParams.push(cwd + '/');
524
+ filtersApplied.push(`Requiring path match for current directory`);
525
+ }
526
+ // Build separate queries for count and samples
527
+ let countQuery = `
528
+ SELECT COUNT(*) as total_matches
529
+ FROM memories m
530
+ JOIN mem_fts ON m.rowid = mem_fts.rowid
531
+ WHERE mem_fts MATCH ?
532
+ AND m.project_id = ?
533
+ `;
534
+ let sampleQuery = `
535
+ SELECT m.summary,
536
+ bm25(mem_fts) as fts_score,
537
+ snippet(mem_fts, 0, '[', ']', '...', 32) as snippet
538
+ FROM memories m
539
+ JOIN mem_fts ON m.rowid = mem_fts.rowid
540
+ WHERE mem_fts MATCH ?
541
+ AND m.project_id = ?
542
+ `;
543
+ // Apply the same filters to both queries
544
+ const countParams = [...queryParams];
545
+ const sampleParams = [...queryParams];
546
+ if (!params.include_expired) {
547
+ countQuery += ' AND (m.expires_at IS NULL OR m.expires_at > ?)';
548
+ sampleQuery += ' AND (m.expires_at IS NULL OR m.expires_at > ?)';
549
+ }
550
+ if (params.tags && params.tags.length > 0) {
551
+ const tagConditions = params.tags.map(() => 'json_extract(m.tags, ?) IS NOT NULL').join(' AND ');
552
+ countQuery += ` AND (${tagConditions})`;
553
+ sampleQuery += ` AND (${tagConditions})`;
554
+ }
555
+ if (params.require_path_match) {
556
+ const cwd = process.cwd();
557
+ countQuery += ` AND EXISTS (
558
+ SELECT 1 FROM json_each(m.paths) as path_item
559
+ WHERE
560
+ (path_item.value LIKE ? || '%') OR
561
+ (path_item.value NOT LIKE '/%' AND path_item.value NOT LIKE 'C:%' AND path_item.value NOT LIKE '~%')
562
+ )`;
563
+ countParams.push(cwd + '/');
564
+ sampleQuery += ` AND EXISTS (
565
+ SELECT 1 FROM json_each(m.paths) as path_item
566
+ WHERE
567
+ (path_item.value LIKE ? || '%') OR
568
+ (path_item.value NOT LIKE '/%' AND path_item.value NOT LIKE 'C:%' AND path_item.value NOT LIKE '~%')
569
+ )`;
570
+ sampleParams.push(cwd + '/');
571
+ }
572
+ sampleQuery += ` ORDER BY bm25(mem_fts) LIMIT 3`;
573
+ const countStmt = this.db.prepare(countQuery);
574
+ const sampleStmt = this.db.prepare(sampleQuery);
575
+ let totalMatches = 0;
576
+ let sampleResults = [];
577
+ try {
578
+ const countResult = countStmt.get(...countParams);
579
+ totalMatches = countResult?.total_matches || 0;
580
+ if (totalMatches > 0) {
581
+ sampleResults = sampleStmt.all(...sampleParams);
582
+ }
583
+ }
584
+ catch (error) {
585
+ // Query failed - try fallback explanations
586
+ const suggestions = [
587
+ 'Try removing special characters or using simpler terms',
588
+ 'Check if memories exist with: kratos recent',
589
+ `Query failed: ${error instanceof Error ? error.message : 'Unknown error'}`
590
+ ];
591
+ return {
592
+ preview: {
593
+ would_return: 0,
594
+ search_explanation: `Search would fail with error: ${error instanceof Error ? error.message : 'Unknown error'}`,
595
+ query_breakdown: {
596
+ original: params.q,
597
+ processed: escapedQuery,
598
+ terms: terms,
599
+ filters_applied: filtersApplied
600
+ },
601
+ match_examples: []
602
+ },
603
+ suggestions: suggestions
604
+ };
605
+ }
606
+ // Create explanation
607
+ let explanation = `Search for "${params.q}" would return ${Math.min(totalMatches, k)} of ${totalMatches} total matches.`;
608
+ if (filtersApplied.length > 0) {
609
+ explanation += ` Filters: ${filtersApplied.join(', ')}.`;
610
+ }
611
+ const matchExamples = sampleResults.map((r, i) => ({
612
+ summary: r.summary || 'No summary',
613
+ match_reason: `Matched terms: ${terms.filter(term => r.summary?.toLowerCase().includes(term) || r.snippet?.toLowerCase().includes(term)).join(', ') || 'FTS match'}`,
614
+ score_estimate: r.fts_score > -1 ? 'High relevance' : r.fts_score > -3 ? 'Medium relevance' : 'Low relevance'
615
+ }));
616
+ const suggestions = [];
617
+ if (totalMatches === 0) {
618
+ suggestions.push('Try removing special characters or using broader terms');
619
+ suggestions.push('Check if memories exist with: kratos recent');
620
+ if (params.require_path_match) {
621
+ suggestions.push('Try without require_path_match to search all memories');
622
+ }
623
+ if (params.tags && params.tags.length > 0) {
624
+ suggestions.push('Try without tag filters to broaden search');
625
+ }
626
+ }
627
+ else if (totalMatches < k) {
628
+ suggestions.push(`Consider using broader terms to find more than ${totalMatches} results`);
629
+ }
630
+ return {
631
+ preview: {
632
+ would_return: Math.min(totalMatches, k),
633
+ search_explanation: explanation,
634
+ query_breakdown: {
635
+ original: params.q,
636
+ processed: escapedQuery,
637
+ terms: terms,
638
+ filters_applied: filtersApplied
639
+ },
640
+ match_examples: matchExamples
641
+ },
642
+ suggestions: suggestions
643
+ };
644
+ }
645
+ generateId() {
646
+ return `mem_${Date.now()}_${crypto.randomBytes(4).toString('hex')}`;
647
+ }
648
+ computeDedupeHash(summary, paths) {
649
+ const normalized = summary.toLowerCase().trim() + '|' + paths.sort().join('|');
650
+ return crypto.createHash('md5').update(normalized).digest('hex');
651
+ }
652
+ escapeQuery(query) {
653
+ // Escape FTS5 special characters and wrap in double quotes for phrase search
654
+ // This prevents "grably-desktop" from being interpreted as "grably MINUS desktop"
655
+ const cleaned = query
656
+ .replace(/["]/g, '""') // Escape existing quotes
657
+ .replace(/[^\w\s]/g, ' ') // Replace special chars with spaces
658
+ .replace(/\s+/g, ' ') // Normalize whitespace
659
+ .trim();
660
+ // If query contains spaces or was modified, wrap in quotes for phrase search
661
+ if (cleaned !== query || cleaned.includes(' ')) {
662
+ return `"${cleaned}"`;
663
+ }
664
+ return cleaned;
665
+ }
666
+ rowToMemory(row) {
667
+ return {
668
+ id: row.id,
669
+ project_id: row.project_id,
670
+ summary: row.summary,
671
+ text: row.text,
672
+ tags: JSON.parse(row.tags),
673
+ paths: JSON.parse(row.paths),
674
+ importance: row.importance,
675
+ created_at: row.created_at,
676
+ updated_at: row.updated_at,
677
+ ttl: row.ttl,
678
+ expires_at: row.expires_at
679
+ };
680
+ }
681
+ getActiveProjectId() {
682
+ // Always use the projectId passed to constructor - ensures true isolation
683
+ // No environment variable dependency - each database instance is bound to its project
684
+ return this.projectId;
685
+ }
686
+ close() {
687
+ this.db.close();
688
+ }
689
+ }
690
+ //# sourceMappingURL=database.js.map