@soleri/core 2.0.2 → 2.4.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (226) hide show
  1. package/dist/brain/brain.d.ts +14 -50
  2. package/dist/brain/brain.d.ts.map +1 -1
  3. package/dist/brain/brain.js +207 -16
  4. package/dist/brain/brain.js.map +1 -1
  5. package/dist/brain/intelligence.d.ts +86 -0
  6. package/dist/brain/intelligence.d.ts.map +1 -0
  7. package/dist/brain/intelligence.js +771 -0
  8. package/dist/brain/intelligence.js.map +1 -0
  9. package/dist/brain/types.d.ts +197 -0
  10. package/dist/brain/types.d.ts.map +1 -0
  11. package/dist/brain/types.js +2 -0
  12. package/dist/brain/types.js.map +1 -0
  13. package/dist/cognee/client.d.ts +35 -0
  14. package/dist/cognee/client.d.ts.map +1 -0
  15. package/dist/cognee/client.js +291 -0
  16. package/dist/cognee/client.js.map +1 -0
  17. package/dist/cognee/types.d.ts +46 -0
  18. package/dist/cognee/types.d.ts.map +1 -0
  19. package/dist/cognee/types.js +3 -0
  20. package/dist/cognee/types.js.map +1 -0
  21. package/dist/control/identity-manager.d.ts +22 -0
  22. package/dist/control/identity-manager.d.ts.map +1 -0
  23. package/dist/control/identity-manager.js +233 -0
  24. package/dist/control/identity-manager.js.map +1 -0
  25. package/dist/control/intent-router.d.ts +32 -0
  26. package/dist/control/intent-router.d.ts.map +1 -0
  27. package/dist/control/intent-router.js +242 -0
  28. package/dist/control/intent-router.js.map +1 -0
  29. package/dist/control/types.d.ts +68 -0
  30. package/dist/control/types.d.ts.map +1 -0
  31. package/dist/control/types.js +9 -0
  32. package/dist/control/types.js.map +1 -0
  33. package/dist/curator/curator.d.ts +29 -0
  34. package/dist/curator/curator.d.ts.map +1 -1
  35. package/dist/curator/curator.js +142 -5
  36. package/dist/curator/curator.js.map +1 -1
  37. package/dist/facades/types.d.ts +1 -1
  38. package/dist/governance/governance.d.ts +42 -0
  39. package/dist/governance/governance.d.ts.map +1 -0
  40. package/dist/governance/governance.js +488 -0
  41. package/dist/governance/governance.js.map +1 -0
  42. package/dist/governance/index.d.ts +3 -0
  43. package/dist/governance/index.d.ts.map +1 -0
  44. package/dist/governance/index.js +2 -0
  45. package/dist/governance/index.js.map +1 -0
  46. package/dist/governance/types.d.ts +102 -0
  47. package/dist/governance/types.d.ts.map +1 -0
  48. package/dist/governance/types.js +3 -0
  49. package/dist/governance/types.js.map +1 -0
  50. package/dist/index.d.ts +35 -3
  51. package/dist/index.d.ts.map +1 -1
  52. package/dist/index.js +32 -1
  53. package/dist/index.js.map +1 -1
  54. package/dist/llm/llm-client.d.ts.map +1 -1
  55. package/dist/llm/llm-client.js +9 -2
  56. package/dist/llm/llm-client.js.map +1 -1
  57. package/dist/logging/logger.d.ts +37 -0
  58. package/dist/logging/logger.d.ts.map +1 -0
  59. package/dist/logging/logger.js +145 -0
  60. package/dist/logging/logger.js.map +1 -0
  61. package/dist/logging/types.d.ts +19 -0
  62. package/dist/logging/types.d.ts.map +1 -0
  63. package/dist/logging/types.js +2 -0
  64. package/dist/logging/types.js.map +1 -0
  65. package/dist/loop/loop-manager.d.ts +49 -0
  66. package/dist/loop/loop-manager.d.ts.map +1 -0
  67. package/dist/loop/loop-manager.js +105 -0
  68. package/dist/loop/loop-manager.js.map +1 -0
  69. package/dist/loop/types.d.ts +35 -0
  70. package/dist/loop/types.d.ts.map +1 -0
  71. package/dist/loop/types.js +8 -0
  72. package/dist/loop/types.js.map +1 -0
  73. package/dist/planning/gap-analysis.d.ts +29 -0
  74. package/dist/planning/gap-analysis.d.ts.map +1 -0
  75. package/dist/planning/gap-analysis.js +265 -0
  76. package/dist/planning/gap-analysis.js.map +1 -0
  77. package/dist/planning/gap-types.d.ts +29 -0
  78. package/dist/planning/gap-types.d.ts.map +1 -0
  79. package/dist/planning/gap-types.js +28 -0
  80. package/dist/planning/gap-types.js.map +1 -0
  81. package/dist/planning/planner.d.ts +150 -1
  82. package/dist/planning/planner.d.ts.map +1 -1
  83. package/dist/planning/planner.js +365 -2
  84. package/dist/planning/planner.js.map +1 -1
  85. package/dist/project/project-registry.d.ts +79 -0
  86. package/dist/project/project-registry.d.ts.map +1 -0
  87. package/dist/project/project-registry.js +276 -0
  88. package/dist/project/project-registry.js.map +1 -0
  89. package/dist/project/types.d.ts +28 -0
  90. package/dist/project/types.d.ts.map +1 -0
  91. package/dist/project/types.js +5 -0
  92. package/dist/project/types.js.map +1 -0
  93. package/dist/runtime/admin-extra-ops.d.ts +13 -0
  94. package/dist/runtime/admin-extra-ops.d.ts.map +1 -0
  95. package/dist/runtime/admin-extra-ops.js +284 -0
  96. package/dist/runtime/admin-extra-ops.js.map +1 -0
  97. package/dist/runtime/admin-ops.d.ts +15 -0
  98. package/dist/runtime/admin-ops.d.ts.map +1 -0
  99. package/dist/runtime/admin-ops.js +322 -0
  100. package/dist/runtime/admin-ops.js.map +1 -0
  101. package/dist/runtime/capture-ops.d.ts +15 -0
  102. package/dist/runtime/capture-ops.d.ts.map +1 -0
  103. package/dist/runtime/capture-ops.js +345 -0
  104. package/dist/runtime/capture-ops.js.map +1 -0
  105. package/dist/runtime/core-ops.d.ts +7 -3
  106. package/dist/runtime/core-ops.d.ts.map +1 -1
  107. package/dist/runtime/core-ops.js +646 -15
  108. package/dist/runtime/core-ops.js.map +1 -1
  109. package/dist/runtime/curator-extra-ops.d.ts +9 -0
  110. package/dist/runtime/curator-extra-ops.d.ts.map +1 -0
  111. package/dist/runtime/curator-extra-ops.js +59 -0
  112. package/dist/runtime/curator-extra-ops.js.map +1 -0
  113. package/dist/runtime/domain-ops.d.ts.map +1 -1
  114. package/dist/runtime/domain-ops.js +59 -13
  115. package/dist/runtime/domain-ops.js.map +1 -1
  116. package/dist/runtime/grading-ops.d.ts +14 -0
  117. package/dist/runtime/grading-ops.d.ts.map +1 -0
  118. package/dist/runtime/grading-ops.js +105 -0
  119. package/dist/runtime/grading-ops.js.map +1 -0
  120. package/dist/runtime/loop-ops.d.ts +13 -0
  121. package/dist/runtime/loop-ops.d.ts.map +1 -0
  122. package/dist/runtime/loop-ops.js +179 -0
  123. package/dist/runtime/loop-ops.js.map +1 -0
  124. package/dist/runtime/memory-cross-project-ops.d.ts +12 -0
  125. package/dist/runtime/memory-cross-project-ops.d.ts.map +1 -0
  126. package/dist/runtime/memory-cross-project-ops.js +165 -0
  127. package/dist/runtime/memory-cross-project-ops.js.map +1 -0
  128. package/dist/runtime/memory-extra-ops.d.ts +13 -0
  129. package/dist/runtime/memory-extra-ops.d.ts.map +1 -0
  130. package/dist/runtime/memory-extra-ops.js +173 -0
  131. package/dist/runtime/memory-extra-ops.js.map +1 -0
  132. package/dist/runtime/orchestrate-ops.d.ts +17 -0
  133. package/dist/runtime/orchestrate-ops.d.ts.map +1 -0
  134. package/dist/runtime/orchestrate-ops.js +240 -0
  135. package/dist/runtime/orchestrate-ops.js.map +1 -0
  136. package/dist/runtime/planning-extra-ops.d.ts +17 -0
  137. package/dist/runtime/planning-extra-ops.d.ts.map +1 -0
  138. package/dist/runtime/planning-extra-ops.js +300 -0
  139. package/dist/runtime/planning-extra-ops.js.map +1 -0
  140. package/dist/runtime/project-ops.d.ts +15 -0
  141. package/dist/runtime/project-ops.d.ts.map +1 -0
  142. package/dist/runtime/project-ops.js +181 -0
  143. package/dist/runtime/project-ops.js.map +1 -0
  144. package/dist/runtime/runtime.d.ts.map +1 -1
  145. package/dist/runtime/runtime.js +48 -1
  146. package/dist/runtime/runtime.js.map +1 -1
  147. package/dist/runtime/types.d.ts +23 -0
  148. package/dist/runtime/types.d.ts.map +1 -1
  149. package/dist/runtime/vault-extra-ops.d.ts +9 -0
  150. package/dist/runtime/vault-extra-ops.d.ts.map +1 -0
  151. package/dist/runtime/vault-extra-ops.js +195 -0
  152. package/dist/runtime/vault-extra-ops.js.map +1 -0
  153. package/dist/telemetry/telemetry.d.ts +48 -0
  154. package/dist/telemetry/telemetry.d.ts.map +1 -0
  155. package/dist/telemetry/telemetry.js +87 -0
  156. package/dist/telemetry/telemetry.js.map +1 -0
  157. package/dist/vault/vault.d.ts +94 -0
  158. package/dist/vault/vault.d.ts.map +1 -1
  159. package/dist/vault/vault.js +340 -1
  160. package/dist/vault/vault.js.map +1 -1
  161. package/package.json +1 -1
  162. package/src/__tests__/admin-extra-ops.test.ts +420 -0
  163. package/src/__tests__/admin-ops.test.ts +271 -0
  164. package/src/__tests__/brain-intelligence.test.ts +828 -0
  165. package/src/__tests__/brain.test.ts +396 -27
  166. package/src/__tests__/capture-ops.test.ts +509 -0
  167. package/src/__tests__/cognee-client.test.ts +524 -0
  168. package/src/__tests__/core-ops.test.ts +341 -49
  169. package/src/__tests__/curator-extra-ops.test.ts +359 -0
  170. package/src/__tests__/curator.test.ts +126 -31
  171. package/src/__tests__/domain-ops.test.ts +111 -9
  172. package/src/__tests__/governance.test.ts +522 -0
  173. package/src/__tests__/grading-ops.test.ts +340 -0
  174. package/src/__tests__/identity-manager.test.ts +243 -0
  175. package/src/__tests__/intent-router.test.ts +222 -0
  176. package/src/__tests__/logger.test.ts +200 -0
  177. package/src/__tests__/loop-ops.test.ts +398 -0
  178. package/src/__tests__/memory-cross-project-ops.test.ts +246 -0
  179. package/src/__tests__/memory-extra-ops.test.ts +352 -0
  180. package/src/__tests__/orchestrate-ops.test.ts +284 -0
  181. package/src/__tests__/planner.test.ts +331 -0
  182. package/src/__tests__/planning-extra-ops.test.ts +548 -0
  183. package/src/__tests__/project-ops.test.ts +367 -0
  184. package/src/__tests__/runtime.test.ts +13 -11
  185. package/src/__tests__/vault-extra-ops.test.ts +407 -0
  186. package/src/brain/brain.ts +308 -72
  187. package/src/brain/intelligence.ts +1230 -0
  188. package/src/brain/types.ts +214 -0
  189. package/src/cognee/client.ts +352 -0
  190. package/src/cognee/types.ts +62 -0
  191. package/src/control/identity-manager.ts +354 -0
  192. package/src/control/intent-router.ts +326 -0
  193. package/src/control/types.ts +102 -0
  194. package/src/curator/curator.ts +265 -15
  195. package/src/governance/governance.ts +698 -0
  196. package/src/governance/index.ts +18 -0
  197. package/src/governance/types.ts +111 -0
  198. package/src/index.ts +128 -3
  199. package/src/llm/llm-client.ts +18 -24
  200. package/src/logging/logger.ts +154 -0
  201. package/src/logging/types.ts +21 -0
  202. package/src/loop/loop-manager.ts +130 -0
  203. package/src/loop/types.ts +44 -0
  204. package/src/planning/gap-analysis.ts +506 -0
  205. package/src/planning/gap-types.ts +58 -0
  206. package/src/planning/planner.ts +478 -2
  207. package/src/project/project-registry.ts +358 -0
  208. package/src/project/types.ts +31 -0
  209. package/src/runtime/admin-extra-ops.ts +307 -0
  210. package/src/runtime/admin-ops.ts +329 -0
  211. package/src/runtime/capture-ops.ts +385 -0
  212. package/src/runtime/core-ops.ts +747 -26
  213. package/src/runtime/curator-extra-ops.ts +71 -0
  214. package/src/runtime/domain-ops.ts +65 -13
  215. package/src/runtime/grading-ops.ts +121 -0
  216. package/src/runtime/loop-ops.ts +194 -0
  217. package/src/runtime/memory-cross-project-ops.ts +192 -0
  218. package/src/runtime/memory-extra-ops.ts +186 -0
  219. package/src/runtime/orchestrate-ops.ts +272 -0
  220. package/src/runtime/planning-extra-ops.ts +327 -0
  221. package/src/runtime/project-ops.ts +196 -0
  222. package/src/runtime/runtime.ts +54 -1
  223. package/src/runtime/types.ts +23 -0
  224. package/src/runtime/vault-extra-ops.ts +225 -0
  225. package/src/telemetry/telemetry.ts +118 -0
  226. package/src/vault/vault.ts +412 -1
@@ -121,11 +121,75 @@ export class Vault {
121
121
  id INTEGER PRIMARY KEY AUTOINCREMENT,
122
122
  query TEXT NOT NULL,
123
123
  entry_id TEXT NOT NULL,
124
- action TEXT NOT NULL CHECK(action IN ('accepted', 'dismissed')),
124
+ action TEXT NOT NULL CHECK(action IN ('accepted', 'dismissed', 'modified', 'failed')),
125
+ source TEXT NOT NULL DEFAULT 'search',
126
+ confidence REAL NOT NULL DEFAULT 0.6,
127
+ duration INTEGER,
128
+ context TEXT NOT NULL DEFAULT '{}',
129
+ reason TEXT,
125
130
  created_at INTEGER NOT NULL DEFAULT (unixepoch())
126
131
  );
127
132
  CREATE INDEX IF NOT EXISTS idx_brain_feedback_query ON brain_feedback(query);
128
133
  `);
134
+ this.migrateBrainSchema();
135
+ }
136
+
137
+ /**
138
+ * Migrate brain_feedback table from old schema (accepted/dismissed only)
139
+ * to new schema with source, confidence, duration, context, reason columns.
140
+ * Also adds extracted_at to brain_sessions if it exists.
141
+ */
142
+ private migrateBrainSchema(): void {
143
+ // Check if brain_feedback needs migration (old schema lacks 'source' column)
144
+ const columns = this.db.prepare('PRAGMA table_info(brain_feedback)').all() as Array<{
145
+ name: string;
146
+ }>;
147
+ const hasSource = columns.some((c) => c.name === 'source');
148
+
149
+ if (!hasSource && columns.length > 0) {
150
+ // Old table exists without new columns — rebuild with expanded schema
151
+ this.db.transaction(() => {
152
+ this.db
153
+ .prepare(`
154
+ CREATE TABLE brain_feedback_new (
155
+ id INTEGER PRIMARY KEY AUTOINCREMENT,
156
+ query TEXT NOT NULL,
157
+ entry_id TEXT NOT NULL,
158
+ action TEXT NOT NULL CHECK(action IN ('accepted', 'dismissed', 'modified', 'failed')),
159
+ source TEXT NOT NULL DEFAULT 'search',
160
+ confidence REAL NOT NULL DEFAULT 0.6,
161
+ duration INTEGER,
162
+ context TEXT NOT NULL DEFAULT '{}',
163
+ reason TEXT,
164
+ created_at INTEGER NOT NULL DEFAULT (unixepoch())
165
+ )
166
+ `)
167
+ .run();
168
+ this.db
169
+ .prepare(`
170
+ INSERT INTO brain_feedback_new (id, query, entry_id, action, created_at)
171
+ SELECT id, query, entry_id, action, created_at FROM brain_feedback
172
+ `)
173
+ .run();
174
+ this.db.prepare('DROP TABLE brain_feedback').run();
175
+ this.db.prepare('ALTER TABLE brain_feedback_new RENAME TO brain_feedback').run();
176
+ this.db
177
+ .prepare('CREATE INDEX IF NOT EXISTS idx_brain_feedback_query ON brain_feedback(query)')
178
+ .run();
179
+ })();
180
+ }
181
+
182
+ // Add extracted_at to brain_sessions if it exists but lacks the column
183
+ try {
184
+ const sessionCols = this.db.prepare('PRAGMA table_info(brain_sessions)').all() as Array<{
185
+ name: string;
186
+ }>;
187
+ if (sessionCols.length > 0 && !sessionCols.some((c) => c.name === 'extracted_at')) {
188
+ this.db.prepare('ALTER TABLE brain_sessions ADD COLUMN extracted_at TEXT').run();
189
+ }
190
+ } catch {
191
+ // brain_sessions table doesn't exist yet — BrainIntelligence will create it
192
+ }
129
193
  }
130
194
 
131
195
  seed(entries: IntelligenceEntry[]): number {
@@ -258,6 +322,143 @@ export class Vault {
258
322
  return this.db.prepare('DELETE FROM entries WHERE id = ?').run(id).changes > 0;
259
323
  }
260
324
 
325
+ /**
326
+ * Partial update of an existing entry's mutable fields.
327
+ * Returns the updated entry or null if not found.
328
+ */
329
+ update(
330
+ id: string,
331
+ fields: Partial<
332
+ Pick<
333
+ IntelligenceEntry,
334
+ | 'title'
335
+ | 'description'
336
+ | 'context'
337
+ | 'example'
338
+ | 'counterExample'
339
+ | 'why'
340
+ | 'tags'
341
+ | 'appliesTo'
342
+ | 'severity'
343
+ | 'type'
344
+ | 'domain'
345
+ >
346
+ >,
347
+ ): IntelligenceEntry | null {
348
+ const existing = this.get(id);
349
+ if (!existing) return null;
350
+ const merged: IntelligenceEntry = { ...existing, ...fields };
351
+ this.seed([merged]);
352
+ return this.get(id);
353
+ }
354
+
355
+ /**
356
+ * Remove multiple entries by IDs in a single transaction.
357
+ * Returns the number of entries actually removed.
358
+ */
359
+ bulkRemove(ids: string[]): number {
360
+ const stmt = this.db.prepare('DELETE FROM entries WHERE id = ?');
361
+ const tx = this.db.transaction((idList: string[]) => {
362
+ let count = 0;
363
+ for (const id of idList) {
364
+ count += stmt.run(id).changes;
365
+ }
366
+ return count;
367
+ });
368
+ return tx(ids);
369
+ }
370
+
371
+ /**
372
+ * List all unique tags with their occurrence counts.
373
+ */
374
+ getTags(): Array<{ tag: string; count: number }> {
375
+ const rows = this.db.prepare('SELECT tags FROM entries').all() as Array<{ tags: string }>;
376
+ const counts = new Map<string, number>();
377
+ for (const row of rows) {
378
+ const tags: string[] = JSON.parse(row.tags || '[]');
379
+ for (const tag of tags) {
380
+ counts.set(tag, (counts.get(tag) ?? 0) + 1);
381
+ }
382
+ }
383
+ return Array.from(counts.entries())
384
+ .map(([tag, count]) => ({ tag, count }))
385
+ .sort((a, b) => b.count - a.count);
386
+ }
387
+
388
+ /**
389
+ * List all domains with their entry counts.
390
+ */
391
+ getDomains(): Array<{ domain: string; count: number }> {
392
+ const rows = this.db
393
+ .prepare('SELECT domain, COUNT(*) as count FROM entries GROUP BY domain ORDER BY count DESC')
394
+ .all() as Array<{ domain: string; count: number }>;
395
+ return rows;
396
+ }
397
+
398
+ /**
399
+ * Get recently added or updated entries, ordered by updated_at DESC.
400
+ */
401
+ getRecent(limit: number = 20): IntelligenceEntry[] {
402
+ const rows = this.db
403
+ .prepare('SELECT * FROM entries ORDER BY updated_at DESC LIMIT ?')
404
+ .all(limit) as Array<Record<string, unknown>>;
405
+ return rows.map(rowToEntry);
406
+ }
407
+
408
+ /**
409
+ * Export the entire vault as a JSON-serializable bundle.
410
+ */
411
+ exportAll(): { entries: IntelligenceEntry[]; exportedAt: number; count: number } {
412
+ const rows = this.db
413
+ .prepare('SELECT * FROM entries ORDER BY domain, title')
414
+ .all() as Array<Record<string, unknown>>;
415
+ const entries = rows.map(rowToEntry);
416
+ return { entries, exportedAt: Math.floor(Date.now() / 1000), count: entries.length };
417
+ }
418
+
419
+ /**
420
+ * Get entry age distribution — how old entries are, bucketed.
421
+ */
422
+ getAgeReport(): {
423
+ total: number;
424
+ buckets: Array<{ label: string; count: number; minDays: number; maxDays: number }>;
425
+ oldestTimestamp: number | null;
426
+ newestTimestamp: number | null;
427
+ } {
428
+ const rows = this.db
429
+ .prepare('SELECT created_at, updated_at FROM entries')
430
+ .all() as Array<{ created_at: number; updated_at: number }>;
431
+ const now = Math.floor(Date.now() / 1000);
432
+ const bucketDefs = [
433
+ { label: 'today', minDays: 0, maxDays: 1 },
434
+ { label: 'this_week', minDays: 1, maxDays: 7 },
435
+ { label: 'this_month', minDays: 7, maxDays: 30 },
436
+ { label: 'this_quarter', minDays: 30, maxDays: 90 },
437
+ { label: 'older', minDays: 90, maxDays: Infinity },
438
+ ];
439
+ const counts = new Array(bucketDefs.length).fill(0) as number[];
440
+ let oldest: number | null = null;
441
+ let newest: number | null = null;
442
+ for (const row of rows) {
443
+ const ts = row.created_at;
444
+ if (oldest === null || ts < oldest) oldest = ts;
445
+ if (newest === null || ts > newest) newest = ts;
446
+ const ageDays = (now - ts) / 86400;
447
+ for (let i = 0; i < bucketDefs.length; i++) {
448
+ if (ageDays >= bucketDefs[i].minDays && ageDays < bucketDefs[i].maxDays) {
449
+ counts[i]++;
450
+ break;
451
+ }
452
+ }
453
+ }
454
+ return {
455
+ total: rows.length,
456
+ buckets: bucketDefs.map((b, i) => ({ ...b, count: counts[i] })),
457
+ oldestTimestamp: oldest,
458
+ newestTimestamp: newest,
459
+ };
460
+ }
461
+
261
462
  registerProject(path: string, name?: string): ProjectInfo {
262
463
  const projectName = name ?? path.replace(/\/$/, '').split('/').pop() ?? path;
263
464
  const existing = this.getProject(path);
@@ -402,6 +603,216 @@ export class Vault {
402
603
  return row ? rowToMemory(row) : null;
403
604
  }
404
605
 
606
+
607
+ deleteMemory(id: string): boolean {
608
+ return this.db.prepare('DELETE FROM memories WHERE id = ?').run(id).changes > 0;
609
+ }
610
+
611
+ memoryStatsDetailed(options?: {
612
+ projectPath?: string;
613
+ fromDate?: number;
614
+ toDate?: number;
615
+ }): MemoryStats & { oldest: number | null; newest: number | null; archivedCount: number } {
616
+ const filters: string[] = [];
617
+ const params: Record<string, unknown> = {};
618
+ if (options?.projectPath) {
619
+ filters.push('project_path = @projectPath');
620
+ params.projectPath = options.projectPath;
621
+ }
622
+ if (options?.fromDate) {
623
+ filters.push('created_at >= @fromDate');
624
+ params.fromDate = options.fromDate;
625
+ }
626
+ if (options?.toDate) {
627
+ filters.push('created_at <= @toDate');
628
+ params.toDate = options.toDate;
629
+ }
630
+ const wc = filters.length > 0 ? `WHERE ${filters.join(' AND ')}` : '';
631
+
632
+ const total = (
633
+ this.db
634
+ .prepare(`SELECT COUNT(*) as count FROM memories ${wc}${wc ? ' AND' : ' WHERE'} archived_at IS NULL`)
635
+ .get(params) as { count: number }
636
+ ).count;
637
+
638
+ const archivedCount = (
639
+ this.db
640
+ .prepare(`SELECT COUNT(*) as count FROM memories ${wc}${wc ? ' AND' : ' WHERE'} archived_at IS NOT NULL`)
641
+ .get(params) as { count: number }
642
+ ).count;
643
+
644
+ const byTypeRows = this.db
645
+ .prepare(
646
+ `SELECT type as key, COUNT(*) as count FROM memories ${wc}${wc ? ' AND' : ' WHERE'} archived_at IS NULL GROUP BY type`,
647
+ )
648
+ .all(params) as Array<{ key: string; count: number }>;
649
+
650
+ const byProjectRows = this.db
651
+ .prepare(
652
+ `SELECT project_path as key, COUNT(*) as count FROM memories ${wc}${wc ? ' AND' : ' WHERE'} archived_at IS NULL GROUP BY project_path`,
653
+ )
654
+ .all(params) as Array<{ key: string; count: number }>;
655
+
656
+ const dateRange = this.db
657
+ .prepare(
658
+ `SELECT MIN(created_at) as oldest, MAX(created_at) as newest FROM memories ${wc}${wc ? ' AND' : ' WHERE'} archived_at IS NULL`,
659
+ )
660
+ .get(params) as { oldest: number | null; newest: number | null };
661
+
662
+ return {
663
+ total,
664
+ byType: Object.fromEntries(byTypeRows.map((r) => [r.key, r.count])),
665
+ byProject: Object.fromEntries(byProjectRows.map((r) => [r.key, r.count])),
666
+ oldest: dateRange.oldest,
667
+ newest: dateRange.newest,
668
+ archivedCount,
669
+ };
670
+ }
671
+
672
+ exportMemories(options?: {
673
+ projectPath?: string;
674
+ type?: string;
675
+ includeArchived?: boolean;
676
+ }): Memory[] {
677
+ const filters: string[] = [];
678
+ const params: Record<string, unknown> = {};
679
+ if (!options?.includeArchived) {
680
+ filters.push('archived_at IS NULL');
681
+ }
682
+ if (options?.projectPath) {
683
+ filters.push('project_path = @projectPath');
684
+ params.projectPath = options.projectPath;
685
+ }
686
+ if (options?.type) {
687
+ filters.push('type = @type');
688
+ params.type = options.type;
689
+ }
690
+ const wc = filters.length > 0 ? `WHERE ${filters.join(' AND ')}` : '';
691
+ const rows = this.db
692
+ .prepare(`SELECT * FROM memories ${wc} ORDER BY created_at ASC`)
693
+ .all(params) as Array<Record<string, unknown>>;
694
+ return rows.map(rowToMemory);
695
+ }
696
+
697
+ importMemories(memories: Memory[]): { imported: number; skipped: number } {
698
+ const upsert = this.db.prepare(`
699
+ INSERT OR IGNORE INTO memories (id, project_path, type, context, summary, topics, files_modified, tools_used, created_at, archived_at)
700
+ VALUES (@id, @projectPath, @type, @context, @summary, @topics, @filesModified, @toolsUsed, @createdAt, @archivedAt)
701
+ `);
702
+ let imported = 0;
703
+ let skipped = 0;
704
+ const tx = this.db.transaction((items: Memory[]) => {
705
+ for (const m of items) {
706
+ const result = upsert.run({
707
+ id: m.id,
708
+ projectPath: m.projectPath,
709
+ type: m.type,
710
+ context: m.context,
711
+ summary: m.summary,
712
+ topics: JSON.stringify(m.topics),
713
+ filesModified: JSON.stringify(m.filesModified),
714
+ toolsUsed: JSON.stringify(m.toolsUsed),
715
+ createdAt: m.createdAt,
716
+ archivedAt: m.archivedAt,
717
+ });
718
+ if (result.changes > 0) imported++;
719
+ else skipped++;
720
+ }
721
+ });
722
+ tx(memories);
723
+ return { imported, skipped };
724
+ }
725
+
726
+ pruneMemories(olderThanDays: number): { pruned: number } {
727
+ const cutoff = Math.floor(Date.now() / 1000) - olderThanDays * 86400;
728
+ const result = this.db
729
+ .prepare('DELETE FROM memories WHERE created_at < ? AND archived_at IS NULL')
730
+ .run(cutoff);
731
+ return { pruned: result.changes };
732
+ }
733
+
734
+ deduplicateMemories(): { removed: number; groups: Array<{ kept: string; removed: string[] }> } {
735
+ // Find duplicates by matching summary + project_path + type
736
+ const dupeRows = this.db
737
+ .prepare(`
738
+ SELECT m1.id as id1, m2.id as id2
739
+ FROM memories m1
740
+ JOIN memories m2 ON m1.summary = m2.summary
741
+ AND m1.project_path = m2.project_path
742
+ AND m1.type = m2.type
743
+ AND m1.id < m2.id
744
+ AND m1.archived_at IS NULL
745
+ AND m2.archived_at IS NULL
746
+ `)
747
+ .all() as Array<{ id1: string; id2: string }>;
748
+
749
+ // Group: keep the earliest (id1), remove all later duplicates
750
+ const groupMap = new Map<string, Set<string>>();
751
+ for (const row of dupeRows) {
752
+ if (!groupMap.has(row.id1)) groupMap.set(row.id1, new Set());
753
+ groupMap.get(row.id1)!.add(row.id2);
754
+ }
755
+
756
+ const groups: Array<{ kept: string; removed: string[] }> = [];
757
+ const toRemove = new Set<string>();
758
+ for (const [kept, removedSet] of groupMap) {
759
+ const removed = [...removedSet].filter((id) => !toRemove.has(id));
760
+ if (removed.length > 0) {
761
+ groups.push({ kept, removed });
762
+ for (const id of removed) toRemove.add(id);
763
+ }
764
+ }
765
+
766
+ if (toRemove.size > 0) {
767
+ const del = this.db.prepare('DELETE FROM memories WHERE id = ?');
768
+ const tx = this.db.transaction((ids: string[]) => {
769
+ for (const id of ids) del.run(id);
770
+ });
771
+ tx([...toRemove]);
772
+ }
773
+
774
+ return { removed: toRemove.size, groups };
775
+ }
776
+
777
+ memoryTopics(): Array<{ topic: string; count: number }> {
778
+ const rows = this.db
779
+ .prepare('SELECT topics FROM memories WHERE archived_at IS NULL')
780
+ .all() as Array<{ topics: string }>;
781
+
782
+ const topicCounts = new Map<string, number>();
783
+ for (const row of rows) {
784
+ const topics: string[] = JSON.parse(row.topics || '[]');
785
+ for (const topic of topics) {
786
+ topicCounts.set(topic, (topicCounts.get(topic) ?? 0) + 1);
787
+ }
788
+ }
789
+
790
+ return [...topicCounts.entries()]
791
+ .map(([topic, count]) => ({ topic, count }))
792
+ .sort((a, b) => b.count - a.count);
793
+ }
794
+
795
+ memoriesByProject(): Array<{ project: string; count: number; memories: Memory[] }> {
796
+ const rows = this.db
797
+ .prepare(
798
+ 'SELECT project_path as project, COUNT(*) as count FROM memories WHERE archived_at IS NULL GROUP BY project_path ORDER BY count DESC',
799
+ )
800
+ .all() as Array<{ project: string; count: number }>;
801
+
802
+ return rows.map((row) => {
803
+ const memories = this.db
804
+ .prepare(
805
+ 'SELECT * FROM memories WHERE project_path = ? AND archived_at IS NULL ORDER BY created_at DESC',
806
+ )
807
+ .all(row.project) as Array<Record<string, unknown>>;
808
+ return {
809
+ project: row.project,
810
+ count: row.count,
811
+ memories: memories.map(rowToMemory),
812
+ };
813
+ });
814
+ }
815
+
405
816
  getDb(): Database.Database {
406
817
  return this.db;
407
818
  }