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.
- package/AGENTS.md +25 -0
- package/LICENSE +21 -0
- package/bin/kratos-cli +7 -0
- package/dist/cli/capture-handler.d.ts +13 -0
- package/dist/cli/capture-handler.d.ts.map +1 -0
- package/dist/cli/capture-handler.js +112 -0
- package/dist/cli/capture-handler.js.map +1 -0
- package/dist/cli/commands/ask.d.ts +5 -0
- package/dist/cli/commands/ask.d.ts.map +1 -0
- package/dist/cli/commands/ask.js +64 -0
- package/dist/cli/commands/ask.js.map +1 -0
- package/dist/cli/commands/capture.d.ts +5 -0
- package/dist/cli/commands/capture.d.ts.map +1 -0
- package/dist/cli/commands/capture.js +31 -0
- package/dist/cli/commands/capture.js.map +1 -0
- package/dist/cli/commands/forget.d.ts +3 -0
- package/dist/cli/commands/forget.d.ts.map +1 -0
- package/dist/cli/commands/forget.js +12 -0
- package/dist/cli/commands/forget.js.map +1 -0
- package/dist/cli/commands/get.d.ts +3 -0
- package/dist/cli/commands/get.d.ts.map +1 -0
- package/dist/cli/commands/get.js +28 -0
- package/dist/cli/commands/get.js.map +1 -0
- package/dist/cli/commands/hooks.d.ts +2 -0
- package/dist/cli/commands/hooks.d.ts.map +1 -0
- package/dist/cli/commands/hooks.js +136 -0
- package/dist/cli/commands/hooks.js.map +1 -0
- package/dist/cli/commands/migrate.d.ts +5 -0
- package/dist/cli/commands/migrate.d.ts.map +1 -0
- package/dist/cli/commands/migrate.js +56 -0
- package/dist/cli/commands/migrate.js.map +1 -0
- package/dist/cli/commands/recent.d.ts +6 -0
- package/dist/cli/commands/recent.d.ts.map +1 -0
- package/dist/cli/commands/recent.js +21 -0
- package/dist/cli/commands/recent.js.map +1 -0
- package/dist/cli/commands/save.d.ts +8 -0
- package/dist/cli/commands/save.d.ts.map +1 -0
- package/dist/cli/commands/save.js +31 -0
- package/dist/cli/commands/save.js.map +1 -0
- package/dist/cli/commands/scan.d.ts +5 -0
- package/dist/cli/commands/scan.d.ts.map +1 -0
- package/dist/cli/commands/scan.js +28 -0
- package/dist/cli/commands/scan.js.map +1 -0
- package/dist/cli/commands/search.d.ts +8 -0
- package/dist/cli/commands/search.d.ts.map +1 -0
- package/dist/cli/commands/search.js +45 -0
- package/dist/cli/commands/search.js.map +1 -0
- package/dist/cli/commands/status.d.ts +3 -0
- package/dist/cli/commands/status.d.ts.map +1 -0
- package/dist/cli/commands/status.js +89 -0
- package/dist/cli/commands/status.js.map +1 -0
- package/dist/cli/commands/switch.d.ts +3 -0
- package/dist/cli/commands/switch.d.ts.map +1 -0
- package/dist/cli/commands/switch.js +18 -0
- package/dist/cli/commands/switch.js.map +1 -0
- package/dist/cli/core.d.ts +15 -0
- package/dist/cli/core.d.ts.map +1 -0
- package/dist/cli/core.js +18 -0
- package/dist/cli/core.js.map +1 -0
- package/dist/cli/index.d.ts +3 -0
- package/dist/cli/index.d.ts.map +1 -0
- package/dist/cli/index.js +157 -0
- package/dist/cli/index.js.map +1 -0
- package/dist/cli/output.d.ts +22 -0
- package/dist/cli/output.d.ts.map +1 -0
- package/dist/cli/output.js +74 -0
- package/dist/cli/output.js.map +1 -0
- package/dist/compression/factory.d.ts +6 -0
- package/dist/compression/factory.d.ts.map +1 -0
- package/dist/compression/factory.js +8 -0
- package/dist/compression/factory.js.map +1 -0
- package/dist/compression/index.d.ts +10 -0
- package/dist/compression/index.d.ts.map +1 -0
- package/dist/compression/index.js +2 -0
- package/dist/compression/index.js.map +1 -0
- package/dist/compression/rule-compressor.d.ts +9 -0
- package/dist/compression/rule-compressor.d.ts.map +1 -0
- package/dist/compression/rule-compressor.js +43 -0
- package/dist/compression/rule-compressor.js.map +1 -0
- package/dist/memory-server/concept-store-enhanced.d.ts +88 -0
- package/dist/memory-server/concept-store-enhanced.d.ts.map +1 -0
- package/dist/memory-server/concept-store-enhanced.js +392 -0
- package/dist/memory-server/concept-store-enhanced.js.map +1 -0
- package/dist/memory-server/concept-store.d.ts +58 -0
- package/dist/memory-server/concept-store.d.ts.map +1 -0
- package/dist/memory-server/concept-store.js +329 -0
- package/dist/memory-server/concept-store.js.map +1 -0
- package/dist/memory-server/context-broker.d.ts +63 -0
- package/dist/memory-server/context-broker.d.ts.map +1 -0
- package/dist/memory-server/context-broker.js +340 -0
- package/dist/memory-server/context-broker.js.map +1 -0
- package/dist/memory-server/database.d.ts +108 -0
- package/dist/memory-server/database.d.ts.map +1 -0
- package/dist/memory-server/database.js +690 -0
- package/dist/memory-server/database.js.map +1 -0
- package/dist/project-manager.d.ts +77 -0
- package/dist/project-manager.d.ts.map +1 -0
- package/dist/project-manager.js +226 -0
- package/dist/project-manager.js.map +1 -0
- package/dist/security/data-retention.d.ts +104 -0
- package/dist/security/data-retention.d.ts.map +1 -0
- package/dist/security/data-retention.js +444 -0
- package/dist/security/data-retention.js.map +1 -0
- package/dist/security/encryption.d.ts +48 -0
- package/dist/security/encryption.d.ts.map +1 -0
- package/dist/security/encryption.js +131 -0
- package/dist/security/encryption.js.map +1 -0
- package/dist/security/pii-detector.d.ts +61 -0
- package/dist/security/pii-detector.d.ts.map +1 -0
- package/dist/security/pii-detector.js +220 -0
- package/dist/security/pii-detector.js.map +1 -0
- package/dist/types/index.d.ts +151 -0
- package/dist/types/index.d.ts.map +1 -0
- package/dist/types/index.js +2 -0
- package/dist/types/index.js.map +1 -0
- package/dist/utils/logger.d.ts +9 -0
- package/dist/utils/logger.d.ts.map +1 -0
- package/dist/utils/logger.js +10 -0
- package/dist/utils/logger.js.map +1 -0
- 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
|