@membank/core 0.6.1 → 0.8.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/dist/index.cjs CHANGED
@@ -28,6 +28,7 @@ let better_sqlite3 = require("better-sqlite3");
28
28
  better_sqlite3 = __toESM(better_sqlite3, 1);
29
29
  let sqlite_vec = require("sqlite-vec");
30
30
  sqlite_vec = __toESM(sqlite_vec, 1);
31
+ let zod = require("zod");
31
32
  let _huggingface_transformers = require("@huggingface/transformers");
32
33
  let node_crypto = require("node:crypto");
33
34
  let node_child_process = require("node:child_process");
@@ -48,7 +49,8 @@ var DatabaseError = class extends MembankError {
48
49
  //#endregion
49
50
  //#region src/db/manager.ts
50
51
  const DEFAULT_DB_PATH = (0, node_path.join)((0, node_os.homedir)(), ".membank", "memory.db");
51
- const MIGRATIONS$1 = [[1, `
52
+ const MIGRATIONS$1 = [
53
+ [1, `
52
54
  CREATE TABLE IF NOT EXISTS memories (
53
55
  id TEXT PRIMARY KEY,
54
56
  content TEXT NOT NULL,
@@ -66,7 +68,8 @@ CREATE TABLE IF NOT EXISTS memories (
66
68
  CREATE VIRTUAL TABLE IF NOT EXISTS embeddings USING vec0(
67
69
  embedding FLOAT[384]
68
70
  );
69
- `], [2, `
71
+ `],
72
+ [2, `
70
73
  CREATE TABLE IF NOT EXISTS projects (
71
74
  id TEXT PRIMARY KEY,
72
75
  name TEXT NOT NULL,
@@ -99,7 +102,25 @@ JOIN projects p ON p.scope_hash = m.scope
99
102
  WHERE m.scope != 'global';
100
103
 
101
104
  ALTER TABLE memories DROP COLUMN scope;
102
- `]];
105
+ `],
106
+ [3, `
107
+ CREATE TABLE IF NOT EXISTS memory_review_events (
108
+ id TEXT PRIMARY KEY,
109
+ memory_id TEXT NOT NULL REFERENCES memories(id) ON DELETE CASCADE,
110
+ conflicting_memory_id TEXT REFERENCES memories(id) ON DELETE SET NULL,
111
+ similarity REAL NOT NULL,
112
+ conflict_content_snapshot TEXT NOT NULL,
113
+ reason TEXT NOT NULL,
114
+ created_at TEXT NOT NULL,
115
+ resolved_at TEXT
116
+ );
117
+
118
+ CREATE INDEX IF NOT EXISTS idx_review_events_memory_open
119
+ ON memory_review_events(memory_id) WHERE resolved_at IS NULL;
120
+
121
+ ALTER TABLE memories DROP COLUMN needs_review;
122
+ `]
123
+ ];
103
124
  var DatabaseManager = class DatabaseManager {
104
125
  #db;
105
126
  constructor(db) {
@@ -156,22 +177,131 @@ var DatabaseManager = class DatabaseManager {
156
177
  }
157
178
  };
158
179
  //#endregion
180
+ //#region src/schemas.ts
181
+ const MEMORY_TYPE_VALUES = [
182
+ "correction",
183
+ "preference",
184
+ "decision",
185
+ "learning",
186
+ "fact"
187
+ ];
188
+ const MemoryTypeSchema = zod.z.enum(MEMORY_TYPE_VALUES);
189
+ const TagsJsonSchema = zod.z.array(zod.z.string());
190
+ const ProjectSchema = zod.z.object({
191
+ id: zod.z.string(),
192
+ name: zod.z.string(),
193
+ scopeHash: zod.z.string(),
194
+ createdAt: zod.z.string(),
195
+ updatedAt: zod.z.string()
196
+ });
197
+ const ReviewReasonSchema = zod.z.enum(["similarity_dedup"]);
198
+ const ReviewEventSchema = zod.z.object({
199
+ id: zod.z.string(),
200
+ memoryId: zod.z.string(),
201
+ conflictingMemoryId: zod.z.string().nullable(),
202
+ similarity: zod.z.number(),
203
+ conflictContentSnapshot: zod.z.string(),
204
+ reason: ReviewReasonSchema,
205
+ createdAt: zod.z.string(),
206
+ resolvedAt: zod.z.string().nullable()
207
+ });
208
+ const ReviewEventRowSchema = zod.z.object({
209
+ id: zod.z.string(),
210
+ memory_id: zod.z.string(),
211
+ conflicting_memory_id: zod.z.string().nullable(),
212
+ similarity: zod.z.number(),
213
+ conflict_content_snapshot: zod.z.string(),
214
+ reason: ReviewReasonSchema,
215
+ created_at: zod.z.string(),
216
+ resolved_at: zod.z.string().nullable()
217
+ });
218
+ const MemorySchema = zod.z.object({
219
+ id: zod.z.string(),
220
+ content: zod.z.string(),
221
+ type: MemoryTypeSchema,
222
+ tags: zod.z.array(zod.z.string()),
223
+ projects: zod.z.array(ProjectSchema),
224
+ sourceHarness: zod.z.string().nullable(),
225
+ accessCount: zod.z.number().int().nonnegative(),
226
+ pinned: zod.z.boolean(),
227
+ reviewEvents: zod.z.array(ReviewEventSchema),
228
+ createdAt: zod.z.string(),
229
+ updatedAt: zod.z.string()
230
+ });
231
+ const QueryOptionsSchema = zod.z.object({
232
+ query: zod.z.string().min(1),
233
+ type: MemoryTypeSchema.optional(),
234
+ projectHash: zod.z.string().optional(),
235
+ limit: zod.z.number().int().positive().optional(),
236
+ includePinned: zod.z.boolean().optional()
237
+ });
238
+ const SaveOptionsSchema = zod.z.object({
239
+ content: zod.z.string().min(1),
240
+ type: MemoryTypeSchema,
241
+ tags: zod.z.array(zod.z.string()).optional(),
242
+ projectScope: zod.z.object({
243
+ hash: zod.z.string(),
244
+ name: zod.z.string()
245
+ }).optional(),
246
+ sourceHarness: zod.z.string().optional()
247
+ });
248
+ const MemoryPatchSchema = zod.z.object({
249
+ content: zod.z.string().min(1).optional(),
250
+ tags: zod.z.array(zod.z.string()).optional()
251
+ });
252
+ const SessionContextSchema = zod.z.object({
253
+ stats: zod.z.record(MemoryTypeSchema, zod.z.number()),
254
+ pinnedGlobal: zod.z.array(MemorySchema),
255
+ pinnedProject: zod.z.array(MemorySchema)
256
+ });
257
+ const MemoryRowSchema = zod.z.object({
258
+ id: zod.z.string(),
259
+ content: zod.z.string(),
260
+ type: zod.z.string(),
261
+ tags: zod.z.string(),
262
+ source: zod.z.string().nullable(),
263
+ access_count: zod.z.number(),
264
+ pinned: zod.z.number(),
265
+ created_at: zod.z.string(),
266
+ updated_at: zod.z.string()
267
+ });
268
+ const ProjectRowSchema = zod.z.object({
269
+ id: zod.z.string(),
270
+ name: zod.z.string(),
271
+ scope_hash: zod.z.string(),
272
+ created_at: zod.z.string(),
273
+ updated_at: zod.z.string()
274
+ });
275
+ //#endregion
159
276
  //#region src/db/row-types.ts
160
- function rowToMemory(row, projects) {
277
+ function rowToMemory(row, projects, reviewEvents = []) {
161
278
  return {
162
279
  id: row.id,
163
280
  content: row.content,
164
- type: row.type,
165
- tags: JSON.parse(row.tags),
281
+ type: MemoryTypeSchema.parse(row.type),
282
+ tags: TagsJsonSchema.parse(JSON.parse(row.tags)),
166
283
  projects,
167
284
  sourceHarness: row.source,
168
285
  accessCount: row.access_count,
169
286
  pinned: row.pinned !== 0,
170
- needsReview: row.needs_review !== 0,
287
+ reviewEvents,
171
288
  createdAt: row.created_at,
172
289
  updatedAt: row.updated_at
173
290
  };
174
291
  }
292
+ function rowToReviewEvent(row) {
293
+ const parsed = ReviewEventRowSchema.parse(row);
294
+ return {
295
+ id: parsed.id,
296
+ memoryId: parsed.memory_id,
297
+ conflictingMemoryId: parsed.conflicting_memory_id,
298
+ similarity: parsed.similarity,
299
+ conflictContentSnapshot: parsed.conflict_content_snapshot,
300
+ reason: parsed.reason,
301
+ createdAt: parsed.created_at,
302
+ resolvedAt: parsed.resolved_at
303
+ };
304
+ }
175
305
  function rowToProject(row) {
176
306
  return {
177
307
  id: row.id,
@@ -207,15 +337,6 @@ var EmbeddingService = class {
207
337
  }
208
338
  };
209
339
  //#endregion
210
- //#region src/types.ts
211
- const MEMORY_TYPE_VALUES = [
212
- "correction",
213
- "preference",
214
- "decision",
215
- "learning",
216
- "fact"
217
- ];
218
- //#endregion
219
340
  //#region src/memory/repository.ts
220
341
  var MemoryRepository = class {
221
342
  #db;
@@ -227,7 +348,7 @@ var MemoryRepository = class {
227
348
  this.#projects = projects;
228
349
  }
229
350
  async save(options) {
230
- const { content, type, tags = [], projectScope, sourceHarness } = options;
351
+ const { content, type, tags = [], projectScope, sourceHarness } = SaveOptionsSchema.parse(options);
231
352
  const embedding = await this.#embedding.embed(content);
232
353
  const embeddingBlob = Buffer.from(embedding.buffer);
233
354
  let top;
@@ -246,41 +367,50 @@ var MemoryRepository = class {
246
367
  if (top !== void 0 && top.similarity > .92) {
247
368
  this.#db.db.prepare(`UPDATE memories SET content = ?, updated_at = ? WHERE id = ?`).run(content, now, top.id);
248
369
  this.#db.db.prepare(`UPDATE embeddings SET embedding = ? WHERE rowid = ?`).run(embeddingBlob, top.rowid);
249
- return rowToMemory(this.#db.db.prepare(`SELECT * FROM memories WHERE id = ?`).get(top.id), this.#projects.getProjectsForMemories([top.id]).get(top.id) ?? []);
370
+ const updated = MemoryRowSchema.parse(this.#db.db.prepare(`SELECT * FROM memories WHERE id = ?`).get(top.id));
371
+ const projectMap = this.#projects.getProjectsForMemories([top.id]);
372
+ const events = this.#getEventsForMemories([top.id]);
373
+ return rowToMemory(updated, projectMap.get(top.id) ?? [], events.get(top.id) ?? []);
250
374
  }
251
- if (top !== void 0 && top.similarity >= .75) this.#db.db.prepare(`UPDATE memories SET needs_review = 1 WHERE id = ?`).run(top.id);
252
375
  const id = (0, node_crypto.randomUUID)();
253
- this.#db.db.prepare(`INSERT INTO memories (id, content, type, tags, source, access_count, pinned, needs_review, created_at, updated_at)
254
- VALUES (?, ?, ?, ?, ?, 0, 0, 0, ?, ?)`).run(id, content, type, JSON.stringify(tags), sourceHarness ?? null, now, now);
376
+ this.#db.db.prepare(`INSERT INTO memories (id, content, type, tags, source, access_count, pinned, created_at, updated_at)
377
+ VALUES (?, ?, ?, ?, ?, 0, 0, ?, ?)`).run(id, content, type, JSON.stringify(tags), sourceHarness ?? null, now, now);
378
+ if (top !== void 0 && top.similarity >= .75) this.#db.db.prepare(`INSERT INTO memory_review_events
379
+ (id, memory_id, conflicting_memory_id, similarity, conflict_content_snapshot, reason, created_at)
380
+ VALUES (?, ?, ?, ?, ?, 'similarity_dedup', ?)`).run((0, node_crypto.randomUUID)(), top.id, id, top.similarity, content, now);
255
381
  this.#db.db.prepare(`INSERT INTO embeddings (rowid, embedding) SELECT m.rowid, ? FROM memories m WHERE m.id = ?`).run(embeddingBlob, id);
256
382
  if (projectScope !== void 0) {
257
383
  const project = this.#projects.upsertByHash(projectScope.hash, projectScope.name);
258
384
  this.#projects.addAssociation(id, project.id);
259
385
  }
260
- return rowToMemory(this.#db.db.prepare(`SELECT * FROM memories WHERE id = ?`).get(id), this.#projects.getProjectsForMemories([id]).get(id) ?? []);
386
+ return rowToMemory(MemoryRowSchema.parse(this.#db.db.prepare(`SELECT * FROM memories WHERE id = ?`).get(id)), this.#projects.getProjectsForMemories([id]).get(id) ?? [], []);
261
387
  }
262
388
  async update(id, patch) {
389
+ const { content, tags } = MemoryPatchSchema.parse(patch);
263
390
  const existing = this.#db.db.prepare(`SELECT m.rowid, m.* FROM memories m WHERE m.id = ?`).get(id);
264
391
  if (existing === void 0) throw new Error(`Memory not found: ${id}`);
265
392
  const now = (/* @__PURE__ */ new Date()).toISOString();
266
393
  const sets = ["updated_at = ?"];
267
394
  const values = [now];
268
- if (patch.content !== void 0) {
395
+ if (content !== void 0) {
269
396
  sets.push("content = ?");
270
- values.push(patch.content);
397
+ values.push(content);
271
398
  }
272
- if (patch.tags !== void 0) {
399
+ if (tags !== void 0) {
273
400
  sets.push("tags = ?");
274
- values.push(JSON.stringify(patch.tags));
401
+ values.push(JSON.stringify(tags));
275
402
  }
276
403
  values.push(id);
277
404
  this.#db.db.prepare(`UPDATE memories SET ${sets.join(", ")} WHERE id = ?`).run(...values);
278
- if (patch.content !== void 0) {
279
- const embedding = await this.#embedding.embed(patch.content);
405
+ if (content !== void 0) {
406
+ const embedding = await this.#embedding.embed(content);
280
407
  const embeddingBlob = Buffer.from(embedding.buffer);
281
408
  this.#db.db.prepare(`UPDATE embeddings SET embedding = ? WHERE rowid = ?`).run(embeddingBlob, existing.rowid);
282
409
  }
283
- return rowToMemory(this.#db.db.prepare(`SELECT * FROM memories WHERE id = ?`).get(id), this.#projects.getProjectsForMemories([id]).get(id) ?? []);
410
+ const updated = MemoryRowSchema.parse(this.#db.db.prepare(`SELECT * FROM memories WHERE id = ?`).get(id));
411
+ const projectMap = this.#projects.getProjectsForMemories([id]);
412
+ const events = this.#getEventsForMemories([id]);
413
+ return rowToMemory(updated, projectMap.get(id) ?? [], events.get(id) ?? []);
284
414
  }
285
415
  delete(id) {
286
416
  const row = this.#db.db.prepare(`SELECT rowid FROM memories WHERE id = ?`).get(id);
@@ -302,27 +432,69 @@ var MemoryRepository = class {
302
432
  if (rows.length === 0) return [];
303
433
  const ids = rows.map((r) => r.id);
304
434
  const projectMap = this.#projects.getProjectsForMemories(ids);
305
- return rows.map((row) => rowToMemory(row, projectMap.get(row.id) ?? []));
435
+ const eventMap = this.#getEventsForMemories(ids);
436
+ return rows.map((row) => rowToMemory(row, projectMap.get(row.id) ?? [], eventMap.get(row.id) ?? []));
437
+ }
438
+ listFlagged() {
439
+ const rows = this.#db.db.prepare(`SELECT * FROM memories
440
+ WHERE EXISTS (
441
+ SELECT 1 FROM memory_review_events e
442
+ WHERE e.memory_id = memories.id AND e.resolved_at IS NULL
443
+ )
444
+ ORDER BY created_at DESC`).all();
445
+ if (rows.length === 0) return [];
446
+ const ids = rows.map((r) => r.id);
447
+ const projectMap = this.#projects.getProjectsForMemories(ids);
448
+ const eventMap = this.#getEventsForMemories(ids, { unresolvedOnly: true });
449
+ return rows.map((row) => rowToMemory(row, projectMap.get(row.id) ?? [], eventMap.get(row.id) ?? []));
450
+ }
451
+ listReviewEvents(memoryId, opts) {
452
+ const where = opts?.unresolvedOnly === true ? "WHERE memory_id = ? AND resolved_at IS NULL" : "WHERE memory_id = ?";
453
+ return this.#db.db.prepare(`SELECT * FROM memory_review_events ${where} ORDER BY created_at DESC`).all(memoryId).map((r) => rowToReviewEvent(ReviewEventRowSchema.parse(r)));
454
+ }
455
+ resolveReviewEvents(memoryId) {
456
+ const now = (/* @__PURE__ */ new Date()).toISOString();
457
+ this.#db.db.prepare(`UPDATE memory_review_events SET resolved_at = ? WHERE memory_id = ? AND resolved_at IS NULL`).run(now, memoryId);
458
+ }
459
+ #getEventsForMemories(ids, opts) {
460
+ if (ids.length === 0) return /* @__PURE__ */ new Map();
461
+ const placeholders = ids.map(() => "?").join(", ");
462
+ const unresolvedClause = opts?.unresolvedOnly === true ? "AND resolved_at IS NULL" : "";
463
+ const rows = this.#db.db.prepare(`SELECT * FROM memory_review_events
464
+ WHERE memory_id IN (${placeholders}) ${unresolvedClause}
465
+ ORDER BY created_at DESC`).all(...ids);
466
+ const map = /* @__PURE__ */ new Map();
467
+ for (const row of rows) {
468
+ const event = rowToReviewEvent(ReviewEventRowSchema.parse(row));
469
+ const existing = map.get(event.memoryId) ?? [];
470
+ existing.push(event);
471
+ map.set(event.memoryId, existing);
472
+ }
473
+ return map;
306
474
  }
307
475
  stats() {
308
476
  const byType = Object.fromEntries(MEMORY_TYPE_VALUES.map((t) => [t, 0]));
309
477
  const typeRows = this.#db.db.prepare(`SELECT type, COUNT(*) as count FROM memories GROUP BY type`).all();
310
- for (const row of typeRows) if (row.type in byType) byType[row.type] = row.count;
311
- const totals = this.#db.db.prepare(`SELECT COUNT(*) as total, SUM(needs_review) as needsReview FROM memories`).get() ?? {
312
- total: 0,
313
- needsReview: 0
314
- };
478
+ for (const row of typeRows) {
479
+ const parsed = MemoryTypeSchema.safeParse(row.type);
480
+ if (parsed.success) byType[parsed.data] = row.count;
481
+ }
482
+ const totals = this.#db.db.prepare(`SELECT COUNT(*) as total FROM memories`).get() ?? { total: 0 };
483
+ const reviewRow = this.#db.db.prepare(`SELECT COUNT(DISTINCT memory_id) as needsReview FROM memory_review_events WHERE resolved_at IS NULL`).get() ?? { needsReview: 0 };
315
484
  return {
316
485
  byType,
317
486
  total: totals.total,
318
- needsReview: totals.needsReview ?? 0
487
+ needsReview: reviewRow.needsReview
319
488
  };
320
489
  }
321
490
  setPin(id, pinned) {
322
491
  if (this.#db.db.prepare(`SELECT * FROM memories WHERE id = ?`).get(id) === void 0) throw new Error(`Memory not found: ${id}`);
323
492
  const now = (/* @__PURE__ */ new Date()).toISOString();
324
493
  this.#db.db.prepare(`UPDATE memories SET pinned = ?, updated_at = ? WHERE id = ?`).run(pinned ? 1 : 0, now, id);
325
- return rowToMemory(this.#db.db.prepare(`SELECT * FROM memories WHERE id = ?`).get(id), this.#projects.getProjectsForMemories([id]).get(id) ?? []);
494
+ const updated = MemoryRowSchema.parse(this.#db.db.prepare(`SELECT * FROM memories WHERE id = ?`).get(id));
495
+ const projectMap = this.#projects.getProjectsForMemories([id]);
496
+ const events = this.#getEventsForMemories([id]);
497
+ return rowToMemory(updated, projectMap.get(id) ?? [], events.get(id) ?? []);
326
498
  }
327
499
  incrementAccessCount(id) {
328
500
  this.#db.db.prepare(`UPDATE memories SET access_count = access_count + 1 WHERE id = ?`).run(id);
@@ -400,7 +572,7 @@ var ProjectRepository = class {
400
572
  const now = (/* @__PURE__ */ new Date()).toISOString();
401
573
  const id = (0, node_crypto.randomUUID)();
402
574
  this.#db.db.prepare(`INSERT OR IGNORE INTO projects (id, name, scope_hash, created_at, updated_at) VALUES (?, ?, ?, ?, ?)`).run(id, name, hash, now, now);
403
- return rowToProject(this.#db.db.prepare(`SELECT * FROM projects WHERE scope_hash = ?`).get(hash));
575
+ return rowToProject(ProjectRowSchema.parse(this.#db.db.prepare(`SELECT * FROM projects WHERE scope_hash = ?`).get(hash)));
404
576
  }
405
577
  rename(id, name) {
406
578
  const now = (/* @__PURE__ */ new Date()).toISOString();
@@ -459,12 +631,13 @@ var QueryEngine = class {
459
631
  this.#repo = repo;
460
632
  }
461
633
  async query(options) {
462
- const { query, type, projectHash, limit = 10 } = options;
634
+ const { query, type, projectHash, limit = 10, includePinned } = QueryOptionsSchema.parse(options);
463
635
  const queryEmbedding = await this.#embedding.embed(query);
464
636
  const queryBlob = Buffer.from(queryEmbedding.buffer);
465
637
  const whereClauses = [];
466
638
  const params = [queryBlob];
467
639
  let joinClause = "";
640
+ if (!includePinned) whereClauses.push("m.pinned = 0");
468
641
  if (type !== void 0) {
469
642
  whereClauses.push("m.type = ?");
470
643
  params.push(type);
@@ -524,7 +697,10 @@ var SessionContextBuilder = class {
524
697
  WHERE p.scope_hash = ? AND m.pinned = 1`).all(projectHash).map((row) => rowToMemory(row, []));
525
698
  const typeCounts = this.#db.db.prepare("SELECT type, COUNT(*) as count FROM memories GROUP BY type").all();
526
699
  const stats = Object.fromEntries(MEMORY_TYPE_VALUES.map((t) => [t, 0]));
527
- for (const row of typeCounts) if (stats[row.type] !== void 0) stats[row.type] = row.count;
700
+ for (const row of typeCounts) {
701
+ const parsed = MemoryTypeSchema.safeParse(row.type);
702
+ if (parsed.success) stats[parsed.data] = row.count;
703
+ }
528
704
  return {
529
705
  stats,
530
706
  pinnedGlobal,
@@ -539,12 +715,26 @@ exports.EmbeddingService = EmbeddingService;
539
715
  exports.MEMORY_TYPE_VALUES = MEMORY_TYPE_VALUES;
540
716
  exports.MIGRATIONS = MIGRATIONS;
541
717
  exports.MembankError = MembankError;
718
+ exports.MemoryPatchSchema = MemoryPatchSchema;
542
719
  exports.MemoryRepository = MemoryRepository;
720
+ exports.MemoryRowSchema = MemoryRowSchema;
721
+ exports.MemorySchema = MemorySchema;
722
+ exports.MemoryTypeSchema = MemoryTypeSchema;
543
723
  exports.ProjectRepository = ProjectRepository;
724
+ exports.ProjectRowSchema = ProjectRowSchema;
725
+ exports.ProjectSchema = ProjectSchema;
544
726
  exports.QueryEngine = QueryEngine;
727
+ exports.QueryOptionsSchema = QueryOptionsSchema;
728
+ exports.ReviewEventRowSchema = ReviewEventRowSchema;
729
+ exports.ReviewEventSchema = ReviewEventSchema;
730
+ exports.ReviewReasonSchema = ReviewReasonSchema;
731
+ exports.SaveOptionsSchema = SaveOptionsSchema;
545
732
  exports.SessionContextBuilder = SessionContextBuilder;
733
+ exports.SessionContextSchema = SessionContextSchema;
734
+ exports.TagsJsonSchema = TagsJsonSchema;
546
735
  exports.listMemoryTypes = listMemoryTypes;
547
736
  exports.resolveProject = resolveProject;
548
737
  exports.resolveScope = resolveScope;
549
738
  exports.rowToMemory = rowToMemory;
739
+ exports.rowToProject = rowToProject;
550
740
  exports.runScopeToProjectsMigration = runScopeToProjectsMigration;