@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.
- package/README.md +21 -128
- package/dist/dashboard/dashboard.test.js +1 -1
- package/dist/dashboard/dashboard.test.js.map +1 -1
- package/dist/dashboard/public/app.js +39 -8
- package/dist/dashboard/public/index.html +50 -25
- package/dist/dashboard/server.js +46 -1
- package/dist/dashboard/server.js.map +1 -1
- package/dist/e2e.test.d.ts +2 -0
- package/dist/e2e.test.d.ts.map +1 -0
- package/dist/e2e.test.js +230 -0
- package/dist/e2e.test.js.map +1 -0
- package/dist/prompts/registry.d.ts.map +1 -1
- package/dist/prompts/registry.js +41 -29
- package/dist/prompts/registry.js.map +1 -1
- package/dist/resources/index.d.ts +1 -1
- package/dist/resources/index.d.ts.map +1 -1
- package/dist/resources/index.js +55 -28
- package/dist/resources/index.js.map +1 -1
- package/dist/resources/index.test.js +4 -0
- package/dist/resources/index.test.js.map +1 -1
- package/dist/router.d.ts.map +1 -1
- package/dist/router.js +3 -0
- package/dist/router.js.map +1 -1
- package/dist/router.test.js +5 -2
- package/dist/router.test.js.map +1 -1
- package/dist/server.js +32 -2
- package/dist/server.js.map +1 -1
- package/dist/storage/sqlite.d.ts +27 -83
- package/dist/storage/sqlite.d.ts.map +1 -1
- package/dist/storage/sqlite.js +280 -351
- package/dist/storage/sqlite.js.map +1 -1
- package/dist/storage/sqlite.test.js +6 -2
- package/dist/storage/sqlite.test.js.map +1 -1
- package/dist/storage/vectors.d.ts +14 -0
- package/dist/storage/vectors.d.ts.map +1 -0
- package/dist/storage/vectors.js +69 -0
- package/dist/storage/vectors.js.map +1 -0
- package/dist/tools/memory.acknowledge.d.ts +4 -0
- package/dist/tools/memory.acknowledge.d.ts.map +1 -0
- package/dist/tools/memory.acknowledge.js +32 -0
- package/dist/tools/memory.acknowledge.js.map +1 -0
- package/dist/tools/memory.search.d.ts.map +1 -1
- package/dist/tools/memory.search.js +86 -159
- package/dist/tools/memory.search.js.map +1 -1
- package/dist/tools/memory.store.d.ts.map +1 -1
- package/dist/tools/memory.store.js +34 -0
- package/dist/tools/memory.store.js.map +1 -1
- package/dist/tools/memory.update.d.ts.map +1 -1
- package/dist/tools/memory.update.js +11 -6
- package/dist/tools/memory.update.js.map +1 -1
- package/dist/tools/schemas.d.ts +168 -14
- package/dist/tools/schemas.d.ts.map +1 -1
- package/dist/tools/schemas.js +77 -103
- package/dist/tools/schemas.js.map +1 -1
- package/dist/types.d.ts +5 -1
- package/dist/types.d.ts.map +1 -1
- package/dist/utils/mcp-response.d.ts +2 -0
- package/dist/utils/mcp-response.d.ts.map +1 -1
- package/dist/utils/mcp-response.js +4 -1
- package/dist/utils/mcp-response.js.map +1 -1
- package/dist/utils/normalize.js +2 -2
- package/dist/utils/normalize.js.map +1 -1
- package/dist/utils/query-expander.d.ts.map +1 -1
- package/dist/utils/query-expander.js +24 -45
- package/dist/utils/query-expander.js.map +1 -1
- package/dist/utils/query-expander.test.js +10 -12
- package/dist/utils/query-expander.test.js.map +1 -1
- package/dist/v2-features.test.d.ts +2 -0
- package/dist/v2-features.test.d.ts.map +1 -0
- package/dist/v2-features.test.js +106 -0
- package/dist/v2-features.test.js.map +1 -0
- package/package.json +2 -1
package/dist/storage/sqlite.js
CHANGED
|
@@ -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
|
-
//
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
168
|
-
if (updates
|
|
169
|
-
|
|
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
|
-
|
|
172
|
-
|
|
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
|
-
|
|
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
|
|
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.
|
|
201
|
-
const
|
|
202
|
-
|
|
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
|
|
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
|
-
|
|
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
|
|
215
|
-
|
|
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
|
-
|
|
219
|
-
const
|
|
220
|
-
const
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
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
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
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
|
|
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
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
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
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
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
|
-
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
return
|
|
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
|
-
|
|
346
|
-
|
|
347
|
-
|
|
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
|
-
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
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
|
-
|
|
395
|
-
|
|
396
|
-
let
|
|
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
|
-
|
|
355
|
+
where.push("repo = ?");
|
|
400
356
|
params.push(repo);
|
|
401
357
|
}
|
|
402
|
-
|
|
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(
|
|
360
|
+
params.push(type);
|
|
419
361
|
}
|
|
420
|
-
if (
|
|
421
|
-
where.push("
|
|
422
|
-
|
|
423
|
-
params.push(term, term, term);
|
|
362
|
+
if (tag) {
|
|
363
|
+
where.push("tags LIKE ?");
|
|
364
|
+
params.push(`%${tag}%`);
|
|
424
365
|
}
|
|
425
|
-
if (
|
|
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(
|
|
372
|
+
params.push(minImportance);
|
|
428
373
|
}
|
|
429
|
-
if (
|
|
430
|
-
where.push("
|
|
431
|
-
params.push(
|
|
374
|
+
if (search) {
|
|
375
|
+
where.push("(title LIKE ? OR content LIKE ? OR tags LIKE ?)");
|
|
376
|
+
params.push(`%${search}%`, `%${search}%`, `%${search}%`);
|
|
432
377
|
}
|
|
433
|
-
const
|
|
434
|
-
const
|
|
435
|
-
|
|
436
|
-
|
|
437
|
-
|
|
438
|
-
|
|
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
|
|
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
|
-
|
|
471
|
-
|
|
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
|
-
|
|
481
|
-
|
|
482
|
-
|
|
483
|
-
|
|
484
|
-
|
|
485
|
-
|
|
486
|
-
|
|
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
|
-
|
|
489
|
-
|
|
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
|
-
|
|
493
|
-
this.db.
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
525
|
-
|
|
526
|
-
|
|
527
|
-
|
|
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
|
-
|
|
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
|