@vheins/local-memory-mcp 0.1.33 → 0.1.35

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 (72) hide show
  1. package/README.md +21 -128
  2. package/dist/dashboard/dashboard.test.js +1 -1
  3. package/dist/dashboard/dashboard.test.js.map +1 -1
  4. package/dist/dashboard/public/app.js +39 -8
  5. package/dist/dashboard/public/index.html +50 -25
  6. package/dist/dashboard/server.js +46 -1
  7. package/dist/dashboard/server.js.map +1 -1
  8. package/dist/e2e.test.d.ts +2 -0
  9. package/dist/e2e.test.d.ts.map +1 -0
  10. package/dist/e2e.test.js +230 -0
  11. package/dist/e2e.test.js.map +1 -0
  12. package/dist/prompts/registry.d.ts.map +1 -1
  13. package/dist/prompts/registry.js +41 -29
  14. package/dist/prompts/registry.js.map +1 -1
  15. package/dist/resources/index.d.ts +1 -1
  16. package/dist/resources/index.d.ts.map +1 -1
  17. package/dist/resources/index.js +55 -28
  18. package/dist/resources/index.js.map +1 -1
  19. package/dist/resources/index.test.js +4 -0
  20. package/dist/resources/index.test.js.map +1 -1
  21. package/dist/router.d.ts.map +1 -1
  22. package/dist/router.js +3 -0
  23. package/dist/router.js.map +1 -1
  24. package/dist/router.test.js +5 -2
  25. package/dist/router.test.js.map +1 -1
  26. package/dist/server.js +32 -2
  27. package/dist/server.js.map +1 -1
  28. package/dist/storage/sqlite.d.ts +27 -83
  29. package/dist/storage/sqlite.d.ts.map +1 -1
  30. package/dist/storage/sqlite.js +280 -351
  31. package/dist/storage/sqlite.js.map +1 -1
  32. package/dist/storage/sqlite.test.js +6 -2
  33. package/dist/storage/sqlite.test.js.map +1 -1
  34. package/dist/storage/vectors.d.ts +14 -0
  35. package/dist/storage/vectors.d.ts.map +1 -0
  36. package/dist/storage/vectors.js +69 -0
  37. package/dist/storage/vectors.js.map +1 -0
  38. package/dist/tools/memory.acknowledge.d.ts +4 -0
  39. package/dist/tools/memory.acknowledge.d.ts.map +1 -0
  40. package/dist/tools/memory.acknowledge.js +32 -0
  41. package/dist/tools/memory.acknowledge.js.map +1 -0
  42. package/dist/tools/memory.search.d.ts.map +1 -1
  43. package/dist/tools/memory.search.js +86 -159
  44. package/dist/tools/memory.search.js.map +1 -1
  45. package/dist/tools/memory.store.d.ts.map +1 -1
  46. package/dist/tools/memory.store.js +34 -0
  47. package/dist/tools/memory.store.js.map +1 -1
  48. package/dist/tools/memory.update.d.ts.map +1 -1
  49. package/dist/tools/memory.update.js +11 -6
  50. package/dist/tools/memory.update.js.map +1 -1
  51. package/dist/tools/schemas.d.ts +168 -14
  52. package/dist/tools/schemas.d.ts.map +1 -1
  53. package/dist/tools/schemas.js +77 -103
  54. package/dist/tools/schemas.js.map +1 -1
  55. package/dist/types.d.ts +5 -1
  56. package/dist/types.d.ts.map +1 -1
  57. package/dist/utils/mcp-response.d.ts +2 -0
  58. package/dist/utils/mcp-response.d.ts.map +1 -1
  59. package/dist/utils/mcp-response.js +4 -1
  60. package/dist/utils/mcp-response.js.map +1 -1
  61. package/dist/utils/normalize.js +2 -2
  62. package/dist/utils/normalize.js.map +1 -1
  63. package/dist/utils/query-expander.d.ts.map +1 -1
  64. package/dist/utils/query-expander.js +24 -45
  65. package/dist/utils/query-expander.js.map +1 -1
  66. package/dist/utils/query-expander.test.js +10 -12
  67. package/dist/utils/query-expander.test.js.map +1 -1
  68. package/dist/v2-features.test.d.ts +2 -0
  69. package/dist/v2-features.test.d.ts.map +1 -0
  70. package/dist/v2-features.test.js +106 -0
  71. package/dist/v2-features.test.js.map +1 -0
  72. package/package.json +2 -1
@@ -26,10 +26,6 @@ function resolveDbPath() {
26
26
  return path.join(cwdStorage, "memory.db");
27
27
  }
28
28
  const projectRootStorage = path.join(__dirname, "../../storage");
29
- // If we are in a production/installed environment and none of the above exist,
30
- // we SHOULD use the ~/.config path as the default location to create new DBs,
31
- // instead of potentially writing into a read-only node_modules/dist folder.
32
- // But for backward compatibility with existing project-root storage, we check it last.
33
29
  if (fs.existsSync(projectRootStorage)) {
34
30
  return path.join(projectRootStorage, "memory.db");
35
31
  }
@@ -45,6 +41,9 @@ export class SQLiteStore {
45
41
  fs.mkdirSync(dbDir, { recursive: true });
46
42
  }
47
43
  this.db = new Database(finalPath);
44
+ this.db.pragma("journal_mode = WAL");
45
+ this.db.pragma("synchronous = NORMAL");
46
+ this.db.pragma("busy_timeout = 5000");
48
47
  this.migrate();
49
48
  }
50
49
  getDbPath() {
@@ -101,9 +100,7 @@ export class SQLiteStore {
101
100
  try {
102
101
  this.db.exec(`ALTER TABLE memories ADD COLUMN title TEXT`);
103
102
  }
104
- catch (e) {
105
- // Column already exists, ignore
106
- }
103
+ catch (e) { }
107
104
  this.db.exec(`
108
105
  CREATE TABLE IF NOT EXISTS memories_archive (
109
106
  id TEXT PRIMARY KEY,
@@ -135,171 +132,177 @@ export class SQLiteStore {
135
132
  CREATE INDEX IF NOT EXISTS idx_action_log_repo ON action_log(repo);
136
133
  CREATE INDEX IF NOT EXISTS idx_action_log_created_at ON action_log(created_at);
137
134
  `);
138
- // Sub-task 2.1: Use PRAGMA table_info to safely add columns
135
+ // Add missing columns safely
139
136
  const existingColumns = this.db.prepare("PRAGMA table_info(memories)").all().map((col) => col.name);
140
137
  const columnsToAdd = [
141
- { name: "hit_count", definition: "ALTER TABLE memories ADD COLUMN hit_count INTEGER NOT NULL DEFAULT 0" },
142
- { name: "recall_count", definition: "ALTER TABLE memories ADD COLUMN recall_count INTEGER NOT NULL DEFAULT 0" },
143
- { name: "last_used_at", definition: "ALTER TABLE memories ADD COLUMN last_used_at TEXT" },
144
- { name: "expires_at", definition: "ALTER TABLE memories ADD COLUMN expires_at TEXT" },
138
+ { name: "hit_count", table: "memories", definition: "ALTER TABLE memories ADD COLUMN hit_count INTEGER NOT NULL DEFAULT 0" },
139
+ { name: "recall_count", table: "memories", definition: "ALTER TABLE memories ADD COLUMN recall_count INTEGER NOT NULL DEFAULT 0" },
140
+ { name: "last_used_at", table: "memories", definition: "ALTER TABLE memories ADD COLUMN last_used_at TEXT" },
141
+ { name: "expires_at", table: "memories", definition: "ALTER TABLE memories ADD COLUMN expires_at TEXT" },
142
+ { name: "supersedes", table: "memories", definition: "ALTER TABLE memories ADD COLUMN supersedes TEXT" },
143
+ { name: "status", table: "memories", definition: "ALTER TABLE memories ADD COLUMN status TEXT NOT NULL DEFAULT 'active'" },
144
+ { name: "is_global", table: "memories", definition: "ALTER TABLE memories ADD COLUMN is_global INTEGER NOT NULL DEFAULT 0" },
145
+ { name: "tags", table: "memories", definition: "ALTER TABLE memories ADD COLUMN tags TEXT" },
146
+ { name: "vector_version", table: "memory_vectors", definition: "ALTER TABLE memory_vectors ADD COLUMN vector_version INTEGER NOT NULL DEFAULT 1" },
145
147
  ];
146
148
  for (const col of columnsToAdd) {
147
- if (!existingColumns.includes(col.name)) {
149
+ const existingTableColumns = this.db.prepare(`PRAGMA table_info(${col.table})`).all().map((c) => c.name);
150
+ if (!existingTableColumns.includes(col.name)) {
148
151
  this.db.exec(col.definition);
149
152
  }
150
153
  }
154
+ this.db.exec(`
155
+ CREATE INDEX IF NOT EXISTS idx_memories_status ON memories(status);
156
+ CREATE INDEX IF NOT EXISTS idx_memories_supersedes ON memories(supersedes);
157
+ CREATE INDEX IF NOT EXISTS idx_memories_is_global ON memories(is_global);
158
+ `);
151
159
  }
152
160
  insert(entry) {
153
- if (!entry.title) {
154
- throw new Error("Title is required for memory entry");
155
- }
156
161
  const stmt = this.db.prepare(`
157
162
  INSERT INTO memories (
158
163
  id, repo, type, title, content, importance, folder, language,
159
- created_at, updated_at, hit_count, recall_count, last_used_at, expires_at
160
- ) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, 0, 0, NULL, ?)
164
+ created_at, updated_at, hit_count, recall_count, last_used_at, expires_at,
165
+ supersedes, status, is_global, tags
166
+ ) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, 0, 0, NULL, ?, ?, ?, ?, ?)
161
167
  `);
162
- stmt.run(entry.id, entry.scope.repo, entry.type, entry.title, entry.content, entry.importance, entry.scope.folder || null, entry.scope.language || null, entry.created_at, entry.updated_at, entry.expires_at ?? null);
168
+ stmt.run(entry.id, entry.scope.repo, entry.type, entry.title || null, entry.content, entry.importance, entry.scope.folder || null, entry.scope.language || null, entry.created_at, entry.updated_at, entry.expires_at ?? null, entry.supersedes ?? null, entry.status || "active", entry.is_global ? 1 : 0, entry.tags ? JSON.stringify(entry.tags) : null);
163
169
  }
164
170
  update(id, updates) {
165
171
  const fields = [];
166
172
  const values = [];
167
- if (updates.title !== undefined) {
168
- if (updates.title.length < 3) {
169
- throw new Error("Title must be at least 3 characters");
173
+ Object.keys(updates).forEach(key => {
174
+ if (updates[key] !== undefined) {
175
+ if (key === 'tags') {
176
+ fields.push(`tags = ?`);
177
+ values.push(JSON.stringify(updates[key]));
178
+ }
179
+ else if (key === 'is_global') {
180
+ fields.push(`is_global = ?`);
181
+ values.push(updates[key] ? 1 : 0);
182
+ }
183
+ else {
184
+ fields.push(`${key} = ?`);
185
+ values.push(updates[key]);
186
+ }
170
187
  }
171
- fields.push("title = ?");
172
- values.push(updates.title);
173
- }
174
- if (updates.content !== undefined) {
175
- fields.push("content = ?");
176
- values.push(updates.content);
177
- }
178
- if (updates.importance !== undefined) {
179
- fields.push("importance = ?");
180
- values.push(updates.importance);
181
- }
182
- if (fields.length === 0) {
183
- return; // Nothing to update
184
- }
188
+ });
189
+ if (fields.length === 0)
190
+ return;
185
191
  fields.push("updated_at = ?");
186
192
  values.push(new Date().toISOString());
187
193
  values.push(id);
188
- const stmt = this.db.prepare(`
189
- UPDATE memories
190
- SET ${fields.join(", ")}
191
- WHERE id = ?
192
- `);
194
+ const stmt = this.db.prepare(`UPDATE memories SET ${fields.join(", ")} WHERE id = ?`);
193
195
  stmt.run(...values);
194
196
  }
195
- // Sub-task 2.4: Exclude expired memories from searchByRepo
197
+ getById(id) {
198
+ const row = this.db.prepare("SELECT * FROM memories WHERE id = ?").get(id);
199
+ return row ? this.rowToMemoryEntry(row) : null;
200
+ }
201
+ getByIdWithStats(id) {
202
+ const row = this.db.prepare(`
203
+ SELECT *, CASE WHEN hit_count > 0 THEN CAST(recall_count AS REAL) / hit_count ELSE 0 END AS recall_rate
204
+ FROM memories WHERE id = ?
205
+ `).get(id);
206
+ return row ? { ...this.rowToMemoryEntry(row), recall_rate: row.recall_rate ?? 0 } : null;
207
+ }
196
208
  searchByRepo(repo, options = {}) {
197
- let query = `SELECT * FROM memories WHERE repo = ?
198
- AND (expires_at IS NULL OR expires_at > datetime('now'))`;
209
+ let where = ["(repo = ? OR is_global = 1)"];
199
210
  const params = [repo];
200
- if (options.types && options.types.length > 0) {
201
- const placeholders = options.types.map(() => "?").join(", ");
202
- query += ` AND type IN (${placeholders})`;
211
+ if (options.tags?.length) {
212
+ const tagConditions = options.tags.map(() => "tags LIKE ?").join(" OR ");
213
+ where.push(`(${tagConditions})`);
214
+ options.tags.forEach((tag) => params.push(`%${tag}%`));
215
+ }
216
+ let query = `SELECT * FROM memories WHERE ${where.join(" AND ")} AND (expires_at IS NULL OR expires_at > datetime('now'))`;
217
+ if (!options.includeArchived)
218
+ query += " AND status = 'active'";
219
+ if (options.types?.length) {
220
+ query += ` AND type IN (${options.types.map(() => "?").join(",")})`;
203
221
  params.push(...options.types);
204
222
  }
205
- if (options.minImportance !== undefined) {
223
+ if (options.minImportance) {
206
224
  query += " AND importance >= ?";
207
225
  params.push(options.minImportance);
208
226
  }
209
- query += " ORDER BY importance DESC, created_at DESC";
210
- if (options.limit !== undefined) {
227
+ query += " ORDER BY CASE WHEN repo = ? THEN 0 ELSE 1 END, importance DESC, created_at DESC";
228
+ params.push(repo);
229
+ if (options.limit) {
211
230
  query += " LIMIT ?";
212
231
  params.push(options.limit);
213
232
  }
214
- const stmt = this.db.prepare(query);
215
- const rows = stmt.all(...params);
216
- return rows.map((row) => this.rowToMemoryEntry(row));
233
+ const rows = this.db.prepare(query).all(...params);
234
+ return rows.map(r => this.rowToMemoryEntry(r));
217
235
  }
218
- getById(id) {
219
- const stmt = this.db.prepare("SELECT * FROM memories WHERE id = ?");
220
- const row = stmt.get(id);
221
- if (!row)
222
- return null;
223
- return this.rowToMemoryEntry(row);
236
+ searchBySimilarity(query, repo, limit = 10, includeArchived = false, currentTags = []) {
237
+ const queryVector = this.computeVector(query);
238
+ const now = new Date();
239
+ let where = ["(repo = ? OR is_global = 1)"];
240
+ const params = [repo];
241
+ if (currentTags.length > 0) {
242
+ const tagConditions = currentTags.map(() => "tags LIKE ?").join(" OR ");
243
+ where.push(`(${tagConditions})`);
244
+ currentTags.forEach(tag => params.push(`%${tag}%`));
245
+ }
246
+ let sql = `SELECT * FROM memories WHERE (${where.join(" OR ")}) AND (expires_at IS NULL OR expires_at > ?)`;
247
+ if (!includeArchived)
248
+ sql += " AND status = 'active'";
249
+ sql += ` ORDER BY CASE WHEN repo = ? THEN 0 ELSE 1 END, importance DESC, created_at DESC LIMIT 100`;
250
+ let candidates = this.db.prepare(sql).all(...params, now.toISOString(), repo);
251
+ // Ensure we have at least some candidates for re-ranking
252
+ if (candidates.length < 5) {
253
+ const recentSql = `SELECT * FROM memories WHERE (${where.join(" OR ")}) AND status = 'active' AND (expires_at IS NULL OR expires_at > ?) ORDER BY created_at DESC LIMIT 10`;
254
+ const recent = this.db.prepare(recentSql).all(...params, now.toISOString());
255
+ for (const r of recent) {
256
+ if (!candidates.find(c => c.id === r.id))
257
+ candidates.push(r);
258
+ }
259
+ }
260
+ return candidates.map(row => {
261
+ const memory = this.rowToMemoryEntry(row);
262
+ // Strict validity check
263
+ const isExpired = row.expires_at && new Date(row.expires_at) <= now;
264
+ const isArchived = row.status === 'archived' && !includeArchived;
265
+ if (isExpired || isArchived) {
266
+ return { ...memory, similarity: 0 };
267
+ }
268
+ const similarity = this.cosineSimilarity(queryVector, this.computeVector(memory.content));
269
+ let score = similarity;
270
+ if (!score) {
271
+ score = 0.16; // Baseline for active candidates
272
+ }
273
+ if (row.repo === repo)
274
+ score += 0.1;
275
+ return { ...memory, similarity: score };
276
+ }).filter(r => r.similarity > 0)
277
+ .sort((a, b) => b.similarity - a.similarity)
278
+ .slice(0, limit);
224
279
  }
225
- getByIdWithStats(id) {
226
- const stmt = this.db.prepare(`
227
- SELECT *,
228
- CASE WHEN hit_count > 0 THEN CAST(recall_count AS REAL) / hit_count ELSE 0 END AS recall_rate
229
- FROM memories
230
- WHERE id = ?
231
- `);
232
- const row = stmt.get(id);
233
- if (!row)
234
- return null;
235
- return {
236
- ...this.rowToMemoryEntry(row),
237
- recall_rate: row.recall_rate ?? 0,
238
- };
280
+ archiveExpiredMemories(force = false) {
281
+ if (process.env.ENABLE_AUTO_ARCHIVE !== "true" && !force)
282
+ return 0;
283
+ const now = new Date().toISOString();
284
+ const result = this.db.prepare(`
285
+ UPDATE memories SET status = 'archived', updated_at = ?
286
+ WHERE expires_at IS NOT NULL AND expires_at <= ? AND status = 'active'
287
+ `).run(now, now);
288
+ return result.changes;
289
+ }
290
+ archiveLowScoreMemories(force = false) {
291
+ if (process.env.ENABLE_AUTO_ARCHIVE !== "true" && !force)
292
+ return 0;
293
+ const result = this.db.prepare(`
294
+ UPDATE memories SET status = 'archived', updated_at = ?
295
+ WHERE status = 'active' AND (
296
+ (julianday('now') - julianday(COALESCE(last_used_at, created_at)) > 90 AND importance < 3)
297
+ OR (hit_count > 10 AND recall_count = 0)
298
+ )
299
+ `).run(new Date().toISOString());
300
+ return result.changes;
239
301
  }
240
302
  listRecent(limit = 10) {
241
- const stmt = this.db.prepare(`
242
- SELECT id, type, repo
243
- FROM memories
244
- ORDER BY created_at DESC
245
- LIMIT ?
246
- `);
303
+ const stmt = this.db.prepare("SELECT id, type, repo FROM memories ORDER BY created_at DESC LIMIT ?");
247
304
  return stmt.all(limit);
248
305
  }
249
- getSummary(repo) {
250
- const stmt = this.db.prepare("SELECT summary, updated_at FROM memory_summary WHERE repo = ?");
251
- return stmt.get(repo);
252
- }
253
- upsertSummary(repo, summary) {
254
- const stmt = this.db.prepare(`
255
- INSERT INTO memory_summary (repo, summary, updated_at)
256
- VALUES (?, ?, ?)
257
- ON CONFLICT(repo) DO UPDATE SET
258
- summary = excluded.summary,
259
- updated_at = excluded.updated_at
260
- `);
261
- stmt.run(repo, summary, new Date().toISOString());
262
- }
263
- delete(id) {
264
- const stmt = this.db.prepare("DELETE FROM memories WHERE id = ?");
265
- stmt.run(id);
266
- }
267
- // Sub-task 2.5: Renamed from listAllRepos to listRepos
268
- listRepos() {
269
- const stmt = this.db.prepare("SELECT DISTINCT repo FROM memories ORDER BY repo");
270
- const rows = stmt.all();
271
- return rows.map((row) => row.repo);
272
- }
273
- listRepoNavigation() {
274
- const stmt = this.db.prepare(`
275
- SELECT
276
- repo,
277
- COUNT(*) AS memory_count,
278
- MAX(COALESCE(updated_at, created_at)) AS last_updated_at
279
- FROM memories
280
- GROUP BY repo
281
- ORDER BY last_updated_at DESC, repo ASC
282
- `);
283
- return stmt.all();
284
- }
285
- incrementHitCount(id) {
286
- const stmt = this.db.prepare(`
287
- UPDATE memories
288
- SET hit_count = hit_count + 1,
289
- last_used_at = ?
290
- WHERE id = ?
291
- `);
292
- stmt.run(new Date().toISOString(), id);
293
- }
294
- incrementRecallCount(id) {
295
- const stmt = this.db.prepare(`
296
- UPDATE memories
297
- SET recall_count = recall_count + 1,
298
- last_used_at = ?
299
- WHERE id = ?
300
- `);
301
- stmt.run(new Date().toISOString(), id);
302
- }
303
306
  getStats(repo) {
304
307
  let query = "SELECT type, COUNT(*) as count FROM memories";
305
308
  const params = [];
@@ -308,267 +311,193 @@ export class SQLiteStore {
308
311
  params.push(repo);
309
312
  }
310
313
  query += " GROUP BY type";
311
- const stmt = this.db.prepare(query);
312
- const rows = stmt.all(...params);
314
+ const rows = this.db.prepare(query).all(...params);
313
315
  const byType = {};
314
316
  let total = 0;
315
317
  for (const row of rows) {
316
318
  byType[row.type] = row.count;
317
319
  total += row.count;
318
320
  }
319
- let unusedQuery = "SELECT COUNT(*) as count FROM memories WHERE hit_count = 0";
320
- if (repo) {
321
- unusedQuery += " AND repo = ?";
322
- }
323
- const unusedStmt = this.db.prepare(unusedQuery);
324
- const unusedRow = (repo ? unusedStmt.get(repo) : unusedStmt.get());
325
- const unused = unusedRow?.count || 0;
326
- return { total, byType, unused };
321
+ const unusedStmt = repo
322
+ ? this.db.prepare("SELECT COUNT(*) as count FROM memories WHERE hit_count = 0 AND repo = ?")
323
+ : this.db.prepare("SELECT COUNT(*) as count FROM memories WHERE hit_count = 0");
324
+ const unused = (repo ? unusedStmt.get(repo) : unusedStmt.get());
325
+ return { total, byType, unused: unused?.count || 0 };
327
326
  }
328
- // Sub-task 2.2: Public method for Memory_Recap
329
- getRecentMemories(repo, limit, offset = 0) {
330
- const stmt = this.db.prepare(`
331
- SELECT * FROM memories
332
- WHERE repo = ?
333
- ORDER BY created_at DESC
334
- LIMIT ? OFFSET ?
335
- `);
336
- const rows = stmt.all(repo, limit, offset);
337
- return rows.map((row) => this.rowToMemoryEntry(row));
327
+ listRepoNavigation() {
328
+ return this.db.prepare(`
329
+ SELECT repo, COUNT(*) AS memory_count, MAX(COALESCE(updated_at, created_at)) AS last_updated_at
330
+ FROM memories GROUP BY repo ORDER BY last_updated_at DESC, repo ASC
331
+ `).all();
338
332
  }
339
- // Sub-task 2.2: Public method for total count
340
- getTotalCount(repo) {
341
- const stmt = this.db.prepare("SELECT COUNT(*) as count FROM memories WHERE repo = ?");
342
- const row = stmt.get(repo);
343
- return row?.count ?? 0;
333
+ getAllMemoriesWithStats(repo) {
334
+ let sql = `SELECT *, CASE WHEN hit_count > 0 THEN CAST(recall_count AS REAL) / hit_count ELSE 0 END AS recall_rate FROM memories`;
335
+ if (repo)
336
+ return this.db.prepare(sql + " WHERE repo = ?").all(repo);
337
+ return this.db.prepare(sql).all();
344
338
  }
345
- // Sub-task 2.3: searchBySimilarity with pre-filter SQL + exclude expired
346
- searchBySimilarity(query, repo, limit = 10) {
347
- const queryVector = this.computeVector(query);
348
- const now = new Date().toISOString();
349
- // Pre-filter: fetch at most limit*10 candidates, exclude expired
350
- const stmt = this.db.prepare(`
351
- SELECT * FROM memories
352
- WHERE repo = ?
353
- AND (expires_at IS NULL OR expires_at > ?)
354
- ORDER BY importance DESC, created_at DESC
355
- LIMIT ?
356
- `);
357
- const candidates = stmt.all(repo, now, limit * 10);
358
- const withSimilarity = candidates.map((row) => {
359
- const memory = this.rowToMemoryEntry(row);
360
- const memoryVector = this.computeVector(memory.content);
361
- const similarity = this.cosineSimilarity(queryVector, memoryVector);
362
- return {
363
- ...memory,
364
- similarity
365
- };
366
- });
367
- return withSimilarity
368
- .sort((a, b) => b.similarity - a.similarity)
369
- .slice(0, limit);
339
+ getLastActionId() {
340
+ const row = this.db.prepare("SELECT MAX(id) as id FROM action_log").get();
341
+ return row?.id || 0;
370
342
  }
371
- // Sub-task 2.5: Archive expired memories
372
- archiveExpiredMemories() {
373
- const now = new Date().toISOString();
374
- const insertArchive = this.db.prepare(`
375
- INSERT OR IGNORE INTO memories_archive
376
- (id, repo, type, content, importance, folder, language,
377
- created_at, updated_at, hit_count, recall_count, last_used_at, expires_at, archived_at)
378
- SELECT id, repo, type, content, importance, folder, language,
379
- created_at, updated_at, hit_count, recall_count, last_used_at, expires_at, ?
380
- FROM memories
381
- WHERE expires_at IS NOT NULL AND expires_at <= ?
382
- `);
383
- const deleteExpired = this.db.prepare(`
384
- DELETE FROM memories
385
- WHERE expires_at IS NOT NULL AND expires_at <= ?
386
- `);
387
- const archive = this.db.transaction(() => {
388
- insertArchive.run(now, now);
389
- const result = deleteExpired.run(now);
390
- return result.changes;
391
- });
392
- return archive();
343
+ getActionsAfter(id) {
344
+ return this.db.prepare(`
345
+ SELECT a.*, m.title as memory_title, m.type as memory_type
346
+ FROM action_log a LEFT JOIN memories m ON a.memory_id = m.id
347
+ WHERE a.id > ? ORDER BY a.created_at ASC
348
+ `).all(id);
393
349
  }
394
- // Get all memories with stats (for dashboard)
395
- getAllMemoriesWithStats(repo) {
396
- let query = "SELECT * FROM memories";
350
+ listMemoriesForDashboard(options) {
351
+ const { repo, type, tag, isGlobal, minImportance, search, offset = 0, limit = 50, sortBy = 'created_at', sortOrder = 'DESC' } = options;
352
+ let where = ["1=1"];
397
353
  const params = [];
398
354
  if (repo) {
399
- query += " WHERE repo = ?";
355
+ where.push("repo = ?");
400
356
  params.push(repo);
401
357
  }
402
- query += " ORDER BY hit_count DESC, importance DESC, created_at DESC";
403
- const stmt = this.db.prepare(query);
404
- const rows = stmt.all(...params);
405
- return rows.map((row) => ({
406
- ...this.rowToMemoryEntry(row),
407
- hit_count: row.hit_count || 0,
408
- recall_count: row.recall_count || 0,
409
- recall_rate: row.hit_count > 0 ? row.recall_count / row.hit_count : 0,
410
- last_used_at: row.last_used_at ?? null,
411
- }));
412
- }
413
- listMemoriesForDashboard(options) {
414
- const where = ["repo = ?"];
415
- const params = [options.repo];
416
- if (options.type) {
358
+ if (type) {
417
359
  where.push("type = ?");
418
- params.push(options.type);
360
+ params.push(type);
419
361
  }
420
- if (options.search) {
421
- where.push("(LOWER(COALESCE(title, '')) LIKE ? OR LOWER(content) LIKE ? OR LOWER(id) LIKE ?)");
422
- const term = `%${options.search.toLowerCase()}%`;
423
- params.push(term, term, term);
362
+ if (tag) {
363
+ where.push("tags LIKE ?");
364
+ params.push(`%${tag}%`);
424
365
  }
425
- if (options.minImportance !== undefined) {
366
+ if (isGlobal !== undefined) {
367
+ where.push("is_global = ?");
368
+ params.push(isGlobal ? 1 : 0);
369
+ }
370
+ if (minImportance) {
426
371
  where.push("importance >= ?");
427
- params.push(options.minImportance);
372
+ params.push(minImportance);
428
373
  }
429
- if (options.maxImportance !== undefined) {
430
- where.push("importance <= ?");
431
- params.push(options.maxImportance);
374
+ if (search) {
375
+ where.push("(title LIKE ? OR content LIKE ? OR tags LIKE ?)");
376
+ params.push(`%${search}%`, `%${search}%`, `%${search}%`);
432
377
  }
433
- const whereSql = where.length > 0 ? `WHERE ${where.join(" AND ")}` : "";
434
- const sortableColumns = {
435
- id: "id",
436
- title: "COALESCE(title, content)",
437
- type: "type",
438
- importance: "importance",
439
- hit_count: "hit_count",
440
- recall_rate: "recall_rate",
441
- created_at: "created_at",
442
- updated_at: "updated_at",
443
- };
444
- const sortBy = sortableColumns[options.sortBy ?? "hit_count"] ?? sortableColumns.hit_count;
445
- const sortOrder = options.sortOrder === "asc" ? "ASC" : "DESC";
446
- const limit = options.limit ?? 25;
447
- const offset = options.offset ?? 0;
448
- const countStmt = this.db.prepare(`SELECT COUNT(*) AS count FROM memories ${whereSql}`);
449
- const total = countStmt.get(...params)?.count ?? 0;
450
- const listStmt = this.db.prepare(`
451
- SELECT *,
452
- CASE WHEN hit_count > 0 THEN CAST(recall_count AS REAL) / hit_count ELSE 0 END AS recall_rate
453
- FROM memories
454
- ${whereSql}
455
- ORDER BY ${sortBy} ${sortOrder}, created_at DESC
456
- LIMIT ? OFFSET ?
378
+ const countStmt = this.db.prepare(`SELECT COUNT(*) as count FROM memories WHERE ${where.join(" AND ")}`);
379
+ const total = countStmt.get(...params).count;
380
+ const dataStmt = this.db.prepare(`
381
+ SELECT *, CASE WHEN hit_count > 0 THEN CAST(recall_count AS REAL) / hit_count ELSE 0 END AS recall_rate
382
+ FROM memories WHERE ${where.join(" AND ")}
383
+ ORDER BY ${sortBy} ${sortOrder} LIMIT ? OFFSET ?
457
384
  `);
458
- const rows = listStmt.all(...params, limit, offset);
459
- return {
460
- items: rows.map((row) => ({
461
- ...this.rowToMemoryEntry(row),
462
- hit_count: row.hit_count || 0,
463
- recall_count: row.recall_count || 0,
464
- recall_rate: row.recall_rate ?? 0,
465
- last_used_at: row.last_used_at ?? null,
466
- })),
467
- total,
468
- };
385
+ const items = dataStmt.all(...params, limit, offset);
386
+ return { items, memories: items, total, limit, offset };
469
387
  }
470
- upsertVectorEmbedding(memoryId, vector) {
471
- const stmt = this.db.prepare(`
472
- INSERT INTO memory_vectors (memory_id, vector, updated_at)
473
- VALUES (?, ?, ?)
474
- ON CONFLICT(memory_id) DO UPDATE SET
475
- vector = excluded.vector,
476
- updated_at = excluded.updated_at
477
- `);
478
- stmt.run(memoryId, JSON.stringify(vector), new Date().toISOString());
388
+ getSummary(repo) {
389
+ return this.db.prepare("SELECT summary, updated_at FROM memory_summary WHERE repo = ?").get(repo);
479
390
  }
480
- getVectorEmbedding(memoryId) {
481
- const stmt = this.db.prepare("SELECT vector FROM memory_vectors WHERE memory_id = ?");
482
- const row = stmt.get(memoryId);
483
- if (!row)
484
- return null;
485
- try {
486
- return JSON.parse(row.vector);
391
+ upsertSummary(repo, summary) {
392
+ this.db.prepare(`
393
+ INSERT INTO memory_summary (repo, summary, updated_at) VALUES (?, ?, ?)
394
+ ON CONFLICT(repo) DO UPDATE SET summary = excluded.summary, updated_at = excluded.updated_at
395
+ `).run(repo, summary, new Date().toISOString());
396
+ }
397
+ delete(id) {
398
+ this.db.prepare("DELETE FROM memories WHERE id = ?").run(id);
399
+ }
400
+ listRepos() {
401
+ return this.db.prepare("SELECT DISTINCT repo FROM memories ORDER BY repo").all().map(r => r.repo);
402
+ }
403
+ incrementHitCount(id) {
404
+ this.db.prepare("UPDATE memories SET hit_count = hit_count + 1, last_used_at = ? WHERE id = ?").run(new Date().toISOString(), id);
405
+ }
406
+ incrementRecallCount(id) {
407
+ this.db.prepare("UPDATE memories SET recall_count = recall_count + 1, last_used_at = ? WHERE id = ?").run(new Date().toISOString(), id);
408
+ }
409
+ getRecentMemories(repo, limit, offset = 0, includeArchived = false) {
410
+ let query = "SELECT * FROM memories WHERE repo = ?";
411
+ if (!includeArchived) {
412
+ query += " AND status = 'active'";
413
+ }
414
+ query += " ORDER BY created_at DESC LIMIT ? OFFSET ?";
415
+ const stmt = this.db.prepare(query);
416
+ const rows = stmt.all(repo, limit, offset);
417
+ return rows.map((row) => this.rowToMemoryEntry(row));
418
+ }
419
+ getTotalCount(repo, includeArchived = false) {
420
+ let sql = "SELECT COUNT(*) as count FROM memories WHERE repo = ?";
421
+ if (!includeArchived)
422
+ sql += " AND status = 'active'";
423
+ return this.db.prepare(sql).get(repo).count;
424
+ }
425
+ getVectorCandidates(repo, limit = 100) {
426
+ let sql = `SELECT mv.memory_id, mv.vector FROM memory_vectors mv JOIN memories m ON mv.memory_id = m.id`;
427
+ const params = [];
428
+ if (repo) {
429
+ sql += " WHERE m.repo = ?";
430
+ params.push(repo);
487
431
  }
488
- catch {
489
- return null;
432
+ sql += " LIMIT ?";
433
+ params.push(limit);
434
+ return this.db.prepare(sql).all(...params);
435
+ }
436
+ upsertVectorEmbedding(memoryId, vector) {
437
+ this.db.prepare(`
438
+ INSERT INTO memory_vectors (memory_id, vector, updated_at) VALUES (?, ?, ?)
439
+ ON CONFLICT(memory_id) DO UPDATE SET vector = excluded.vector, updated_at = excluded.updated_at
440
+ `).run(memoryId, JSON.stringify(vector), new Date().toISOString());
441
+ }
442
+ async checkConflicts(content, repo, type, vectors, threshold = 0.55) {
443
+ const vectorResults = await vectors.search(content, 10, repo);
444
+ for (const vr of vectorResults) {
445
+ if (vr.score > threshold) {
446
+ const memory = this.getById(vr.id);
447
+ if (memory && memory.type === type && memory.status === 'active')
448
+ return memory;
449
+ }
490
450
  }
451
+ return null;
491
452
  }
492
- close() {
493
- this.db.close();
453
+ logAction(action, repo, options = {}) {
454
+ this.db.prepare(`INSERT INTO action_log (action, query, memory_id, repo, result_count, created_at) VALUES (?, ?, ?, ?, ?, ?)`).run(action, options.query || null, options.memoryId || null, repo, options.resultCount || 0, new Date().toISOString());
455
+ }
456
+ getRecentActions(repo, limit = 20) {
457
+ let sql = `SELECT a.*, m.title as memory_title, m.type as memory_type FROM action_log a LEFT JOIN memories m ON a.memory_id = m.id`;
458
+ const params = [];
459
+ if (repo) {
460
+ sql += ` WHERE a.repo = ?`;
461
+ params.push(repo);
462
+ }
463
+ sql += ` ORDER BY a.created_at DESC, a.id DESC LIMIT ?`;
464
+ params.push(limit);
465
+ return this.db.prepare(sql).all(...params);
494
466
  }
495
- // Sub-task 2.3: Use tokenize() from normalize.ts (single source of truth)
496
467
  computeVector(text) {
497
468
  const tokens = tokenize(text);
498
469
  const vector = {};
499
- for (const token of tokens) {
470
+ for (const token of tokens)
500
471
  vector[token] = (vector[token] || 0) + 1;
501
- }
502
472
  return vector;
503
473
  }
504
474
  cosineSimilarity(v1, v2) {
505
475
  const keys1 = Object.keys(v1);
506
- const keys2 = Object.keys(v2);
507
- if (keys1.length === 0 || keys2.length === 0)
476
+ if (!keys1.length || !Object.keys(v2).length)
508
477
  return 0;
509
478
  let dotProduct = 0;
510
- for (const key of keys1) {
511
- if (v2[key]) {
479
+ for (const key of keys1)
480
+ if (v2[key])
512
481
  dotProduct += v1[key] * v2[key];
513
- }
514
- }
515
- const mag1 = Math.sqrt(keys1.reduce((sum, key) => sum + v1[key] * v1[key], 0));
516
- const mag2 = Math.sqrt(keys2.reduce((sum, key) => sum + v2[key] * v2[key], 0));
517
- if (mag1 === 0 || mag2 === 0)
518
- return 0;
519
- return dotProduct / (mag1 * mag2);
482
+ const mag1 = Math.sqrt(keys1.reduce((sum, k) => sum + v1[k] * v1[k], 0));
483
+ const mag2 = Math.sqrt(Object.keys(v2).reduce((sum, k) => sum + v2[k] * v2[k], 0));
484
+ return (mag1 && mag2) ? dotProduct / (mag1 * mag2) : 0;
520
485
  }
521
486
  rowToMemoryEntry(row) {
487
+ let tags = [];
488
+ try {
489
+ if (row.tags)
490
+ tags = JSON.parse(row.tags);
491
+ }
492
+ catch (e) { }
522
493
  return {
523
- id: row.id,
524
- type: row.type,
525
- title: row.title || "Untitled Memory",
526
- content: row.content,
527
- importance: row.importance,
528
- scope: {
529
- repo: row.repo,
530
- folder: row.folder || undefined,
531
- language: row.language || undefined,
532
- },
533
- created_at: row.created_at,
534
- updated_at: row.updated_at,
535
- hit_count: row.hit_count ?? 0,
536
- recall_count: row.recall_count ?? 0,
537
- last_used_at: row.last_used_at ?? null,
538
- expires_at: row.expires_at ?? null,
494
+ id: row.id, type: row.type, title: row.title || "Untitled", content: row.content, importance: row.importance,
495
+ scope: { repo: row.repo, folder: row.folder || undefined, language: row.language || undefined },
496
+ created_at: row.created_at, updated_at: row.updated_at, hit_count: row.hit_count ?? 0, recall_count: row.recall_count ?? 0,
497
+ last_used_at: row.last_used_at ?? null, expires_at: row.expires_at ?? null, supersedes: row.supersedes ?? null, status: row.status || "active",
498
+ is_global: row.is_global === 1, tags
539
499
  };
540
500
  }
541
- logAction(action, repo, options) {
542
- const stmt = this.db.prepare(`
543
- INSERT INTO action_log (action, query, memory_id, repo, result_count, created_at)
544
- VALUES (?, ?, ?, ?, ?, ?)
545
- `);
546
- stmt.run(action, options?.query || null, options?.memoryId || null, repo, options?.resultCount || 0, new Date().toISOString());
547
- }
548
- getLastActionId() {
549
- const row = this.db.prepare(`SELECT MAX(id) as max_id FROM action_log`).get();
550
- return row?.max_id ?? 0;
551
- }
552
- getActionsAfter(afterId) {
553
- const rows = this.db.prepare(`SELECT id, action, query, memory_id, repo, result_count, created_at FROM action_log WHERE id > ? ORDER BY id ASC`).all(afterId);
554
- return rows;
555
- }
556
- getRecentActions(repo, limit = 20) {
557
- let sql = `
558
- SELECT a.action, a.query, a.memory_id, a.result_count, a.created_at,
559
- m.title as memory_title, m.type as memory_type
560
- FROM action_log a
561
- LEFT JOIN memories m ON a.memory_id = m.id
562
- `;
563
- const params = [];
564
- if (repo) {
565
- sql += ` WHERE a.repo = ?`;
566
- params.push(repo);
567
- }
568
- sql += ` ORDER BY a.created_at DESC, a.id DESC LIMIT ?`;
569
- params.push(limit);
570
- const rows = this.db.prepare(sql).all(...params);
571
- return rows;
572
- }
501
+ close() { this.db.close(); }
573
502
  }
574
503
  //# sourceMappingURL=sqlite.js.map