@soleri/core 2.4.0 → 2.5.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 (300) hide show
  1. package/dist/brain/brain.d.ts +7 -0
  2. package/dist/brain/brain.d.ts.map +1 -1
  3. package/dist/brain/brain.js +56 -9
  4. package/dist/brain/brain.js.map +1 -1
  5. package/dist/brain/types.d.ts +2 -2
  6. package/dist/brain/types.d.ts.map +1 -1
  7. package/dist/cognee/client.d.ts +3 -0
  8. package/dist/cognee/client.d.ts.map +1 -1
  9. package/dist/cognee/client.js +17 -0
  10. package/dist/cognee/client.js.map +1 -1
  11. package/dist/cognee/sync-manager.d.ts +94 -0
  12. package/dist/cognee/sync-manager.d.ts.map +1 -0
  13. package/dist/cognee/sync-manager.js +293 -0
  14. package/dist/cognee/sync-manager.js.map +1 -0
  15. package/dist/curator/curator.d.ts +8 -1
  16. package/dist/curator/curator.d.ts.map +1 -1
  17. package/dist/curator/curator.js +64 -1
  18. package/dist/curator/curator.js.map +1 -1
  19. package/dist/errors/classify.d.ts +13 -0
  20. package/dist/errors/classify.d.ts.map +1 -0
  21. package/dist/errors/classify.js +97 -0
  22. package/dist/errors/classify.js.map +1 -0
  23. package/dist/errors/index.d.ts +6 -0
  24. package/dist/errors/index.d.ts.map +1 -0
  25. package/dist/errors/index.js +4 -0
  26. package/dist/errors/index.js.map +1 -0
  27. package/dist/errors/retry.d.ts +40 -0
  28. package/dist/errors/retry.d.ts.map +1 -0
  29. package/dist/errors/retry.js +97 -0
  30. package/dist/errors/retry.js.map +1 -0
  31. package/dist/errors/types.d.ts +48 -0
  32. package/dist/errors/types.d.ts.map +1 -0
  33. package/dist/errors/types.js +59 -0
  34. package/dist/errors/types.js.map +1 -0
  35. package/dist/index.d.ts +25 -5
  36. package/dist/index.d.ts.map +1 -1
  37. package/dist/index.js +21 -3
  38. package/dist/index.js.map +1 -1
  39. package/dist/intake/content-classifier.d.ts +14 -0
  40. package/dist/intake/content-classifier.d.ts.map +1 -0
  41. package/dist/intake/content-classifier.js +125 -0
  42. package/dist/intake/content-classifier.js.map +1 -0
  43. package/dist/intake/dedup-gate.d.ts +17 -0
  44. package/dist/intake/dedup-gate.d.ts.map +1 -0
  45. package/dist/intake/dedup-gate.js +66 -0
  46. package/dist/intake/dedup-gate.js.map +1 -0
  47. package/dist/intake/intake-pipeline.d.ts +63 -0
  48. package/dist/intake/intake-pipeline.d.ts.map +1 -0
  49. package/dist/intake/intake-pipeline.js +373 -0
  50. package/dist/intake/intake-pipeline.js.map +1 -0
  51. package/dist/intake/types.d.ts +65 -0
  52. package/dist/intake/types.d.ts.map +1 -0
  53. package/dist/intake/types.js +3 -0
  54. package/dist/intake/types.js.map +1 -0
  55. package/dist/intelligence/loader.js +1 -1
  56. package/dist/intelligence/loader.js.map +1 -1
  57. package/dist/intelligence/types.d.ts +3 -1
  58. package/dist/intelligence/types.d.ts.map +1 -1
  59. package/dist/loop/loop-manager.d.ts +58 -7
  60. package/dist/loop/loop-manager.d.ts.map +1 -1
  61. package/dist/loop/loop-manager.js +280 -6
  62. package/dist/loop/loop-manager.js.map +1 -1
  63. package/dist/loop/types.d.ts +69 -1
  64. package/dist/loop/types.d.ts.map +1 -1
  65. package/dist/loop/types.js +4 -1
  66. package/dist/loop/types.js.map +1 -1
  67. package/dist/persistence/index.d.ts +3 -0
  68. package/dist/persistence/index.d.ts.map +1 -0
  69. package/dist/persistence/index.js +2 -0
  70. package/dist/persistence/index.js.map +1 -0
  71. package/dist/persistence/sqlite-provider.d.ts +25 -0
  72. package/dist/persistence/sqlite-provider.d.ts.map +1 -0
  73. package/dist/persistence/sqlite-provider.js +59 -0
  74. package/dist/persistence/sqlite-provider.js.map +1 -0
  75. package/dist/persistence/types.d.ts +36 -0
  76. package/dist/persistence/types.d.ts.map +1 -0
  77. package/dist/persistence/types.js +8 -0
  78. package/dist/persistence/types.js.map +1 -0
  79. package/dist/planning/gap-analysis.d.ts +47 -4
  80. package/dist/planning/gap-analysis.d.ts.map +1 -1
  81. package/dist/planning/gap-analysis.js +190 -13
  82. package/dist/planning/gap-analysis.js.map +1 -1
  83. package/dist/planning/gap-types.d.ts +1 -1
  84. package/dist/planning/gap-types.d.ts.map +1 -1
  85. package/dist/planning/gap-types.js.map +1 -1
  86. package/dist/planning/planner.d.ts +277 -9
  87. package/dist/planning/planner.d.ts.map +1 -1
  88. package/dist/planning/planner.js +611 -46
  89. package/dist/planning/planner.js.map +1 -1
  90. package/dist/playbooks/generic/brainstorming.d.ts +9 -0
  91. package/dist/playbooks/generic/brainstorming.d.ts.map +1 -0
  92. package/dist/playbooks/generic/brainstorming.js +105 -0
  93. package/dist/playbooks/generic/brainstorming.js.map +1 -0
  94. package/dist/playbooks/generic/code-review.d.ts +11 -0
  95. package/dist/playbooks/generic/code-review.d.ts.map +1 -0
  96. package/dist/playbooks/generic/code-review.js +176 -0
  97. package/dist/playbooks/generic/code-review.js.map +1 -0
  98. package/dist/playbooks/generic/subagent-execution.d.ts +9 -0
  99. package/dist/playbooks/generic/subagent-execution.d.ts.map +1 -0
  100. package/dist/playbooks/generic/subagent-execution.js +68 -0
  101. package/dist/playbooks/generic/subagent-execution.js.map +1 -0
  102. package/dist/playbooks/generic/systematic-debugging.d.ts +9 -0
  103. package/dist/playbooks/generic/systematic-debugging.d.ts.map +1 -0
  104. package/dist/playbooks/generic/systematic-debugging.js +87 -0
  105. package/dist/playbooks/generic/systematic-debugging.js.map +1 -0
  106. package/dist/playbooks/generic/tdd.d.ts +9 -0
  107. package/dist/playbooks/generic/tdd.d.ts.map +1 -0
  108. package/dist/playbooks/generic/tdd.js +70 -0
  109. package/dist/playbooks/generic/tdd.js.map +1 -0
  110. package/dist/playbooks/generic/verification.d.ts +9 -0
  111. package/dist/playbooks/generic/verification.d.ts.map +1 -0
  112. package/dist/playbooks/generic/verification.js +74 -0
  113. package/dist/playbooks/generic/verification.js.map +1 -0
  114. package/dist/playbooks/index.d.ts +4 -0
  115. package/dist/playbooks/index.d.ts.map +1 -0
  116. package/dist/playbooks/index.js +5 -0
  117. package/dist/playbooks/index.js.map +1 -0
  118. package/dist/playbooks/playbook-registry.d.ts +42 -0
  119. package/dist/playbooks/playbook-registry.d.ts.map +1 -0
  120. package/dist/playbooks/playbook-registry.js +227 -0
  121. package/dist/playbooks/playbook-registry.js.map +1 -0
  122. package/dist/playbooks/playbook-seeder.d.ts +47 -0
  123. package/dist/playbooks/playbook-seeder.d.ts.map +1 -0
  124. package/dist/playbooks/playbook-seeder.js +104 -0
  125. package/dist/playbooks/playbook-seeder.js.map +1 -0
  126. package/dist/playbooks/playbook-types.d.ts +132 -0
  127. package/dist/playbooks/playbook-types.d.ts.map +1 -0
  128. package/dist/playbooks/playbook-types.js +12 -0
  129. package/dist/playbooks/playbook-types.js.map +1 -0
  130. package/dist/project/project-registry.d.ts.map +1 -1
  131. package/dist/project/project-registry.js +9 -11
  132. package/dist/project/project-registry.js.map +1 -1
  133. package/dist/prompts/index.d.ts +4 -0
  134. package/dist/prompts/index.d.ts.map +1 -0
  135. package/dist/prompts/index.js +3 -0
  136. package/dist/prompts/index.js.map +1 -0
  137. package/dist/prompts/parser.d.ts +17 -0
  138. package/dist/prompts/parser.d.ts.map +1 -0
  139. package/dist/prompts/parser.js +47 -0
  140. package/dist/prompts/parser.js.map +1 -0
  141. package/dist/prompts/template-manager.d.ts +25 -0
  142. package/dist/prompts/template-manager.d.ts.map +1 -0
  143. package/dist/prompts/template-manager.js +71 -0
  144. package/dist/prompts/template-manager.js.map +1 -0
  145. package/dist/prompts/types.d.ts +26 -0
  146. package/dist/prompts/types.d.ts.map +1 -0
  147. package/dist/prompts/types.js +5 -0
  148. package/dist/prompts/types.js.map +1 -0
  149. package/dist/runtime/admin-extra-ops.d.ts +5 -3
  150. package/dist/runtime/admin-extra-ops.d.ts.map +1 -1
  151. package/dist/runtime/admin-extra-ops.js +322 -11
  152. package/dist/runtime/admin-extra-ops.js.map +1 -1
  153. package/dist/runtime/admin-ops.d.ts.map +1 -1
  154. package/dist/runtime/admin-ops.js +10 -3
  155. package/dist/runtime/admin-ops.js.map +1 -1
  156. package/dist/runtime/capture-ops.d.ts.map +1 -1
  157. package/dist/runtime/capture-ops.js +20 -2
  158. package/dist/runtime/capture-ops.js.map +1 -1
  159. package/dist/runtime/cognee-sync-ops.d.ts +12 -0
  160. package/dist/runtime/cognee-sync-ops.d.ts.map +1 -0
  161. package/dist/runtime/cognee-sync-ops.js +55 -0
  162. package/dist/runtime/cognee-sync-ops.js.map +1 -0
  163. package/dist/runtime/core-ops.d.ts +8 -6
  164. package/dist/runtime/core-ops.d.ts.map +1 -1
  165. package/dist/runtime/core-ops.js +226 -9
  166. package/dist/runtime/core-ops.js.map +1 -1
  167. package/dist/runtime/curator-extra-ops.d.ts +2 -2
  168. package/dist/runtime/curator-extra-ops.d.ts.map +1 -1
  169. package/dist/runtime/curator-extra-ops.js +15 -3
  170. package/dist/runtime/curator-extra-ops.js.map +1 -1
  171. package/dist/runtime/domain-ops.js +2 -2
  172. package/dist/runtime/domain-ops.js.map +1 -1
  173. package/dist/runtime/grading-ops.d.ts.map +1 -1
  174. package/dist/runtime/grading-ops.js.map +1 -1
  175. package/dist/runtime/intake-ops.d.ts +14 -0
  176. package/dist/runtime/intake-ops.d.ts.map +1 -0
  177. package/dist/runtime/intake-ops.js +110 -0
  178. package/dist/runtime/intake-ops.js.map +1 -0
  179. package/dist/runtime/loop-ops.d.ts +5 -4
  180. package/dist/runtime/loop-ops.d.ts.map +1 -1
  181. package/dist/runtime/loop-ops.js +84 -12
  182. package/dist/runtime/loop-ops.js.map +1 -1
  183. package/dist/runtime/memory-cross-project-ops.d.ts.map +1 -1
  184. package/dist/runtime/memory-cross-project-ops.js.map +1 -1
  185. package/dist/runtime/memory-extra-ops.js +5 -5
  186. package/dist/runtime/memory-extra-ops.js.map +1 -1
  187. package/dist/runtime/orchestrate-ops.d.ts.map +1 -1
  188. package/dist/runtime/orchestrate-ops.js +8 -2
  189. package/dist/runtime/orchestrate-ops.js.map +1 -1
  190. package/dist/runtime/planning-extra-ops.d.ts +13 -5
  191. package/dist/runtime/planning-extra-ops.d.ts.map +1 -1
  192. package/dist/runtime/planning-extra-ops.js +381 -18
  193. package/dist/runtime/planning-extra-ops.js.map +1 -1
  194. package/dist/runtime/playbook-ops.d.ts +14 -0
  195. package/dist/runtime/playbook-ops.d.ts.map +1 -0
  196. package/dist/runtime/playbook-ops.js +141 -0
  197. package/dist/runtime/playbook-ops.js.map +1 -0
  198. package/dist/runtime/project-ops.d.ts.map +1 -1
  199. package/dist/runtime/project-ops.js +7 -2
  200. package/dist/runtime/project-ops.js.map +1 -1
  201. package/dist/runtime/runtime.d.ts.map +1 -1
  202. package/dist/runtime/runtime.js +27 -8
  203. package/dist/runtime/runtime.js.map +1 -1
  204. package/dist/runtime/types.d.ts +8 -0
  205. package/dist/runtime/types.d.ts.map +1 -1
  206. package/dist/runtime/vault-extra-ops.d.ts +3 -2
  207. package/dist/runtime/vault-extra-ops.d.ts.map +1 -1
  208. package/dist/runtime/vault-extra-ops.js +345 -4
  209. package/dist/runtime/vault-extra-ops.js.map +1 -1
  210. package/dist/vault/playbook.d.ts +34 -0
  211. package/dist/vault/playbook.d.ts.map +1 -0
  212. package/dist/vault/playbook.js +60 -0
  213. package/dist/vault/playbook.js.map +1 -0
  214. package/dist/vault/vault.d.ts +31 -32
  215. package/dist/vault/vault.d.ts.map +1 -1
  216. package/dist/vault/vault.js +201 -181
  217. package/dist/vault/vault.js.map +1 -1
  218. package/package.json +7 -3
  219. package/src/__tests__/admin-extra-ops.test.ts +62 -15
  220. package/src/__tests__/admin-ops.test.ts +2 -2
  221. package/src/__tests__/brain.test.ts +3 -3
  222. package/src/__tests__/cognee-integration.test.ts +80 -0
  223. package/src/__tests__/cognee-sync-manager.test.ts +103 -0
  224. package/src/__tests__/core-ops.test.ts +30 -4
  225. package/src/__tests__/curator-extra-ops.test.ts +24 -2
  226. package/src/__tests__/errors.test.ts +388 -0
  227. package/src/__tests__/grading-ops.test.ts +28 -7
  228. package/src/__tests__/intake-pipeline.test.ts +162 -0
  229. package/src/__tests__/loop-ops.test.ts +74 -3
  230. package/src/__tests__/memory-cross-project-ops.test.ts +3 -1
  231. package/src/__tests__/orchestrate-ops.test.ts +8 -3
  232. package/src/__tests__/persistence.test.ts +225 -0
  233. package/src/__tests__/planner.test.ts +99 -21
  234. package/src/__tests__/planning-extra-ops.test.ts +168 -10
  235. package/src/__tests__/playbook-registry.test.ts +326 -0
  236. package/src/__tests__/playbook-seeder.test.ts +163 -0
  237. package/src/__tests__/playbook.test.ts +389 -0
  238. package/src/__tests__/project-ops.test.ts +18 -4
  239. package/src/__tests__/template-manager.test.ts +222 -0
  240. package/src/__tests__/vault-extra-ops.test.ts +82 -7
  241. package/src/brain/brain.ts +71 -9
  242. package/src/brain/types.ts +2 -2
  243. package/src/cognee/client.ts +18 -0
  244. package/src/cognee/sync-manager.ts +389 -0
  245. package/src/curator/curator.ts +88 -7
  246. package/src/errors/classify.ts +102 -0
  247. package/src/errors/index.ts +5 -0
  248. package/src/errors/retry.ts +132 -0
  249. package/src/errors/types.ts +81 -0
  250. package/src/index.ts +114 -3
  251. package/src/intake/content-classifier.ts +146 -0
  252. package/src/intake/dedup-gate.ts +92 -0
  253. package/src/intake/intake-pipeline.ts +503 -0
  254. package/src/intake/types.ts +69 -0
  255. package/src/intelligence/loader.ts +1 -1
  256. package/src/intelligence/types.ts +3 -1
  257. package/src/loop/loop-manager.ts +325 -7
  258. package/src/loop/types.ts +72 -1
  259. package/src/persistence/index.ts +7 -0
  260. package/src/persistence/sqlite-provider.ts +62 -0
  261. package/src/persistence/types.ts +44 -0
  262. package/src/planning/gap-analysis.ts +286 -17
  263. package/src/planning/gap-types.ts +4 -1
  264. package/src/planning/planner.ts +828 -55
  265. package/src/playbooks/generic/brainstorming.ts +110 -0
  266. package/src/playbooks/generic/code-review.ts +181 -0
  267. package/src/playbooks/generic/subagent-execution.ts +74 -0
  268. package/src/playbooks/generic/systematic-debugging.ts +92 -0
  269. package/src/playbooks/generic/tdd.ts +75 -0
  270. package/src/playbooks/generic/verification.ts +79 -0
  271. package/src/playbooks/index.ts +27 -0
  272. package/src/playbooks/playbook-registry.ts +284 -0
  273. package/src/playbooks/playbook-seeder.ts +119 -0
  274. package/src/playbooks/playbook-types.ts +162 -0
  275. package/src/project/project-registry.ts +29 -17
  276. package/src/prompts/index.ts +3 -0
  277. package/src/prompts/parser.ts +59 -0
  278. package/src/prompts/template-manager.ts +77 -0
  279. package/src/prompts/types.ts +28 -0
  280. package/src/runtime/admin-extra-ops.ts +358 -13
  281. package/src/runtime/admin-ops.ts +17 -6
  282. package/src/runtime/capture-ops.ts +25 -6
  283. package/src/runtime/cognee-sync-ops.ts +63 -0
  284. package/src/runtime/core-ops.ts +258 -8
  285. package/src/runtime/curator-extra-ops.ts +17 -3
  286. package/src/runtime/domain-ops.ts +2 -2
  287. package/src/runtime/grading-ops.ts +11 -2
  288. package/src/runtime/intake-ops.ts +126 -0
  289. package/src/runtime/loop-ops.ts +96 -13
  290. package/src/runtime/memory-cross-project-ops.ts +1 -2
  291. package/src/runtime/memory-extra-ops.ts +5 -5
  292. package/src/runtime/orchestrate-ops.ts +8 -2
  293. package/src/runtime/planning-extra-ops.ts +414 -23
  294. package/src/runtime/playbook-ops.ts +169 -0
  295. package/src/runtime/project-ops.ts +9 -3
  296. package/src/runtime/runtime.ts +35 -9
  297. package/src/runtime/types.ts +8 -0
  298. package/src/runtime/vault-extra-ops.ts +385 -4
  299. package/src/vault/playbook.ts +87 -0
  300. package/src/vault/vault.ts +301 -235
@@ -1,6 +1,5 @@
1
- import Database from 'better-sqlite3';
2
- import { mkdirSync } from 'node:fs';
3
- import { dirname } from 'node:path';
1
+ import type { PersistenceProvider } from '../persistence/types.js';
2
+ import { SQLitePersistenceProvider } from '../persistence/sqlite-provider.js';
4
3
  import type { IntelligenceEntry } from '../intelligence/types.js';
5
4
 
6
5
  export interface SearchResult {
@@ -39,21 +38,43 @@ export interface MemoryStats {
39
38
  }
40
39
 
41
40
  export class Vault {
42
- private db: Database.Database;
41
+ private provider: PersistenceProvider;
42
+ private sqliteProvider: SQLitePersistenceProvider | null;
43
+ private syncManager: import('../cognee/sync-manager.js').CogneeSyncManager | null = null;
43
44
 
44
- constructor(dbPath: string = ':memory:') {
45
- if (dbPath !== ':memory:') mkdirSync(dirname(dbPath), { recursive: true });
46
- this.db = new Database(dbPath);
47
- this.db.pragma('journal_mode = WAL');
48
- this.db.pragma('foreign_keys = ON');
45
+ /**
46
+ * Create a Vault with a PersistenceProvider or a SQLite path (backward compat).
47
+ */
48
+ constructor(providerOrPath: PersistenceProvider | string = ':memory:') {
49
+ if (typeof providerOrPath === 'string') {
50
+ const sqlite = new SQLitePersistenceProvider(providerOrPath);
51
+ this.provider = sqlite;
52
+ this.sqliteProvider = sqlite;
53
+ // SQLite-specific pragmas
54
+ this.provider.run('PRAGMA journal_mode = WAL');
55
+ this.provider.run('PRAGMA foreign_keys = ON');
56
+ } else {
57
+ this.provider = providerOrPath;
58
+ this.sqliteProvider =
59
+ providerOrPath instanceof SQLitePersistenceProvider ? providerOrPath : null;
60
+ }
49
61
  this.initialize();
50
62
  }
51
63
 
64
+ setSyncManager(mgr: import('../cognee/sync-manager.js').CogneeSyncManager): void {
65
+ this.syncManager = mgr;
66
+ }
67
+
68
+ /** Backward-compatible factory. */
69
+ static createWithSQLite(dbPath: string = ':memory:'): Vault {
70
+ return new Vault(dbPath);
71
+ }
72
+
52
73
  private initialize(): void {
53
- this.db.exec(`
74
+ this.provider.execSql(`
54
75
  CREATE TABLE IF NOT EXISTS entries (
55
76
  id TEXT PRIMARY KEY,
56
- type TEXT NOT NULL CHECK(type IN ('pattern', 'anti-pattern', 'rule')),
77
+ type TEXT NOT NULL CHECK(type IN ('pattern', 'anti-pattern', 'rule', 'playbook')),
57
78
  domain TEXT NOT NULL,
58
79
  title TEXT NOT NULL,
59
80
  severity TEXT NOT NULL CHECK(severity IN ('critical', 'warning', 'suggestion')),
@@ -132,25 +153,29 @@ export class Vault {
132
153
  CREATE INDEX IF NOT EXISTS idx_brain_feedback_query ON brain_feedback(query);
133
154
  `);
134
155
  this.migrateBrainSchema();
156
+ this.migrateTemporalSchema();
157
+ }
158
+
159
+ private migrateTemporalSchema(): void {
160
+ try {
161
+ this.provider.run('ALTER TABLE entries ADD COLUMN valid_from INTEGER');
162
+ } catch {
163
+ // Column already exists
164
+ }
165
+ try {
166
+ this.provider.run('ALTER TABLE entries ADD COLUMN valid_until INTEGER');
167
+ } catch {
168
+ // Column already exists
169
+ }
135
170
  }
136
171
 
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
172
  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
- }>;
173
+ const columns = this.provider.all<{ name: string }>('PRAGMA table_info(brain_feedback)');
147
174
  const hasSource = columns.some((c) => c.name === 'source');
148
175
 
149
176
  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(`
177
+ this.provider.transaction(() => {
178
+ this.provider.run(`
154
179
  CREATE TABLE brain_feedback_new (
155
180
  id INTEGER PRIMARY KEY AUTOINCREMENT,
156
181
  query TEXT NOT NULL,
@@ -163,29 +188,23 @@ export class Vault {
163
188
  reason TEXT,
164
189
  created_at INTEGER NOT NULL DEFAULT (unixepoch())
165
190
  )
166
- `)
167
- .run();
168
- this.db
169
- .prepare(`
191
+ `);
192
+ this.provider.run(`
170
193
  INSERT INTO brain_feedback_new (id, query, entry_id, action, created_at)
171
194
  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
- })();
195
+ `);
196
+ this.provider.run('DROP TABLE brain_feedback');
197
+ this.provider.run('ALTER TABLE brain_feedback_new RENAME TO brain_feedback');
198
+ this.provider.run(
199
+ 'CREATE INDEX IF NOT EXISTS idx_brain_feedback_query ON brain_feedback(query)',
200
+ );
201
+ });
180
202
  }
181
203
 
182
- // Add extracted_at to brain_sessions if it exists but lacks the column
183
204
  try {
184
- const sessionCols = this.db.prepare('PRAGMA table_info(brain_sessions)').all() as Array<{
185
- name: string;
186
- }>;
205
+ const sessionCols = this.provider.all<{ name: string }>('PRAGMA table_info(brain_sessions)');
187
206
  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();
207
+ this.provider.run('ALTER TABLE brain_sessions ADD COLUMN extracted_at TEXT');
189
208
  }
190
209
  } catch {
191
210
  // brain_sessions table doesn't exist yet — BrainIntelligence will create it
@@ -193,17 +212,17 @@ export class Vault {
193
212
  }
194
213
 
195
214
  seed(entries: IntelligenceEntry[]): number {
196
- const upsert = this.db.prepare(`
197
- INSERT INTO entries (id,type,domain,title,severity,description,context,example,counter_example,why,tags,applies_to)
198
- VALUES (@id,@type,@domain,@title,@severity,@description,@context,@example,@counterExample,@why,@tags,@appliesTo)
215
+ const sql = `
216
+ INSERT INTO entries (id,type,domain,title,severity,description,context,example,counter_example,why,tags,applies_to,valid_from,valid_until)
217
+ VALUES (@id,@type,@domain,@title,@severity,@description,@context,@example,@counterExample,@why,@tags,@appliesTo,@validFrom,@validUntil)
199
218
  ON CONFLICT(id) DO UPDATE SET type=excluded.type,domain=excluded.domain,title=excluded.title,severity=excluded.severity,
200
219
  description=excluded.description,context=excluded.context,example=excluded.example,counter_example=excluded.counter_example,
201
- why=excluded.why,tags=excluded.tags,applies_to=excluded.applies_to,updated_at=unixepoch()
202
- `);
203
- const tx = this.db.transaction((items: IntelligenceEntry[]) => {
220
+ why=excluded.why,tags=excluded.tags,applies_to=excluded.applies_to,valid_from=excluded.valid_from,valid_until=excluded.valid_until,updated_at=unixepoch()
221
+ `;
222
+ return this.provider.transaction(() => {
204
223
  let count = 0;
205
- for (const entry of items) {
206
- upsert.run({
224
+ for (const entry of entries) {
225
+ this.provider.run(sql, {
207
226
  id: entry.id,
208
227
  type: entry.type,
209
228
  domain: entry.domain,
@@ -216,21 +235,31 @@ export class Vault {
216
235
  why: entry.why ?? null,
217
236
  tags: JSON.stringify(entry.tags),
218
237
  appliesTo: JSON.stringify(entry.appliesTo ?? []),
238
+ validFrom: entry.validFrom ?? null,
239
+ validUntil: entry.validUntil ?? null,
219
240
  });
220
241
  count++;
242
+ if (this.syncManager) {
243
+ this.syncManager.enqueue('ingest', entry.id, entry);
244
+ }
221
245
  }
222
246
  return count;
223
247
  });
224
- return tx(entries);
225
248
  }
226
249
 
227
250
  search(
228
251
  query: string,
229
- options?: { domain?: string; type?: string; severity?: string; limit?: number },
252
+ options?: {
253
+ domain?: string;
254
+ type?: string;
255
+ severity?: string;
256
+ limit?: number;
257
+ includeExpired?: boolean;
258
+ },
230
259
  ): SearchResult[] {
231
260
  const limit = options?.limit ?? 10;
232
261
  const filters: string[] = [];
233
- const fp: Record<string, string> = {};
262
+ const fp: Record<string, unknown> = {};
234
263
  if (options?.domain) {
235
264
  filters.push('e.domain = @domain');
236
265
  fp.domain = options.domain;
@@ -243,13 +272,18 @@ export class Vault {
243
272
  filters.push('e.severity = @severity');
244
273
  fp.severity = options.severity;
245
274
  }
275
+ if (!options?.includeExpired) {
276
+ const now = Math.floor(Date.now() / 1000);
277
+ filters.push('(e.valid_until IS NULL OR e.valid_until > @now)');
278
+ filters.push('(e.valid_from IS NULL OR e.valid_from <= @now)');
279
+ fp.now = now;
280
+ }
246
281
  const wc = filters.length > 0 ? `AND ${filters.join(' AND ')}` : '';
247
282
  try {
248
- const rows = this.db
249
- .prepare(
250
- `SELECT e.*, -rank as score FROM entries_fts fts JOIN entries e ON e.rowid = fts.rowid WHERE entries_fts MATCH @query ${wc} ORDER BY score DESC LIMIT @limit`,
251
- )
252
- .all({ query, limit, ...fp }) as Array<Record<string, unknown>>;
283
+ const rows = this.provider.all<Record<string, unknown>>(
284
+ `SELECT e.*, -rank as score FROM entries_fts fts JOIN entries e ON e.rowid = fts.rowid WHERE entries_fts MATCH @query ${wc} ORDER BY score DESC LIMIT @limit`,
285
+ { query, limit, ...fp },
286
+ );
253
287
  return rows.map(rowToSearchResult);
254
288
  } catch {
255
289
  return [];
@@ -257,9 +291,9 @@ export class Vault {
257
291
  }
258
292
 
259
293
  get(id: string): IntelligenceEntry | null {
260
- const row = this.db.prepare('SELECT * FROM entries WHERE id = ?').get(id) as
261
- | Record<string, unknown>
262
- | undefined;
294
+ const row = this.provider.get<Record<string, unknown>>('SELECT * FROM entries WHERE id = ?', [
295
+ id,
296
+ ]);
263
297
  return row ? rowToEntry(row) : null;
264
298
  }
265
299
 
@@ -270,6 +304,7 @@ export class Vault {
270
304
  tags?: string[];
271
305
  limit?: number;
272
306
  offset?: number;
307
+ includeExpired?: boolean;
273
308
  }): IntelligenceEntry[] {
274
309
  const filters: string[] = [];
275
310
  const params: Record<string, unknown> = {};
@@ -292,26 +327,29 @@ export class Vault {
292
327
  });
293
328
  filters.push(`(${c.join(' OR ')})`);
294
329
  }
330
+ if (!options?.includeExpired) {
331
+ const now = Math.floor(Date.now() / 1000);
332
+ filters.push('(valid_until IS NULL OR valid_until > @now)');
333
+ filters.push('(valid_from IS NULL OR valid_from <= @now)');
334
+ params.now = now;
335
+ }
295
336
  const wc = filters.length > 0 ? `WHERE ${filters.join(' AND ')}` : '';
296
- const rows = this.db
297
- .prepare(
298
- `SELECT * FROM entries ${wc} ORDER BY severity, domain, title LIMIT @limit OFFSET @offset`,
299
- )
300
- .all({ ...params, limit: options?.limit ?? 50, offset: options?.offset ?? 0 }) as Array<
301
- Record<string, unknown>
302
- >;
337
+ const rows = this.provider.all<Record<string, unknown>>(
338
+ `SELECT * FROM entries ${wc} ORDER BY severity, domain, title LIMIT @limit OFFSET @offset`,
339
+ { ...params, limit: options?.limit ?? 50, offset: options?.offset ?? 0 },
340
+ );
303
341
  return rows.map(rowToEntry);
304
342
  }
305
343
 
306
344
  stats(): VaultStats {
307
- const total = (
308
- this.db.prepare('SELECT COUNT(*) as count FROM entries').get() as { count: number }
309
- ).count;
345
+ const total = this.provider.get<{ count: number }>(
346
+ 'SELECT COUNT(*) as count FROM entries',
347
+ )!.count;
310
348
  return {
311
349
  totalEntries: total,
312
- byType: gc(this.db, 'type'),
313
- byDomain: gc(this.db, 'domain'),
314
- bySeverity: gc(this.db, 'severity'),
350
+ byType: gc(this.provider, 'type'),
351
+ byDomain: gc(this.provider, 'domain'),
352
+ bySeverity: gc(this.provider, 'severity'),
315
353
  };
316
354
  }
317
355
 
@@ -319,13 +357,13 @@ export class Vault {
319
357
  this.seed([entry]);
320
358
  }
321
359
  remove(id: string): boolean {
322
- return this.db.prepare('DELETE FROM entries WHERE id = ?').run(id).changes > 0;
360
+ const deleted = this.provider.run('DELETE FROM entries WHERE id = ?', [id]).changes > 0;
361
+ if (deleted && this.syncManager) {
362
+ this.syncManager.enqueue('delete', id);
363
+ }
364
+ return deleted;
323
365
  }
324
366
 
325
- /**
326
- * Partial update of an existing entry's mutable fields.
327
- * Returns the updated entry or null if not found.
328
- */
329
367
  update(
330
368
  id: string,
331
369
  fields: Partial<
@@ -342,6 +380,8 @@ export class Vault {
342
380
  | 'severity'
343
381
  | 'type'
344
382
  | 'domain'
383
+ | 'validFrom'
384
+ | 'validUntil'
345
385
  >
346
386
  >,
347
387
  ): IntelligenceEntry | null {
@@ -352,27 +392,58 @@ export class Vault {
352
392
  return this.get(id);
353
393
  }
354
394
 
355
- /**
356
- * Remove multiple entries by IDs in a single transaction.
357
- * Returns the number of entries actually removed.
358
- */
395
+ setTemporal(id: string, validFrom?: number, validUntil?: number): boolean {
396
+ const sets: string[] = [];
397
+ const params: Record<string, unknown> = { id };
398
+ if (validFrom !== undefined) {
399
+ sets.push('valid_from = @validFrom');
400
+ params.validFrom = validFrom;
401
+ }
402
+ if (validUntil !== undefined) {
403
+ sets.push('valid_until = @validUntil');
404
+ params.validUntil = validUntil;
405
+ }
406
+ if (sets.length === 0) return false;
407
+ sets.push('updated_at = unixepoch()');
408
+ return (
409
+ this.provider.run(`UPDATE entries SET ${sets.join(', ')} WHERE id = @id`, params).changes > 0
410
+ );
411
+ }
412
+
413
+ findExpiring(withinDays: number): IntelligenceEntry[] {
414
+ const now = Math.floor(Date.now() / 1000);
415
+ const cutoff = now + withinDays * 86400;
416
+ const rows = this.provider.all<Record<string, unknown>>(
417
+ 'SELECT * FROM entries WHERE valid_until IS NOT NULL AND valid_until > @now AND valid_until <= @cutoff ORDER BY valid_until ASC',
418
+ { now, cutoff },
419
+ );
420
+ return rows.map(rowToEntry);
421
+ }
422
+
423
+ findExpired(limit: number = 50): IntelligenceEntry[] {
424
+ const now = Math.floor(Date.now() / 1000);
425
+ const rows = this.provider.all<Record<string, unknown>>(
426
+ 'SELECT * FROM entries WHERE valid_until IS NOT NULL AND valid_until <= @now ORDER BY valid_until DESC LIMIT @limit',
427
+ { now, limit },
428
+ );
429
+ return rows.map(rowToEntry);
430
+ }
431
+
359
432
  bulkRemove(ids: string[]): number {
360
- const stmt = this.db.prepare('DELETE FROM entries WHERE id = ?');
361
- const tx = this.db.transaction((idList: string[]) => {
433
+ return this.provider.transaction(() => {
362
434
  let count = 0;
363
- for (const id of idList) {
364
- count += stmt.run(id).changes;
435
+ for (const id of ids) {
436
+ count += this.provider.run('DELETE FROM entries WHERE id = ?', [id]).changes;
437
+ if (this.syncManager) {
438
+ this.syncManager.enqueue('delete', id);
439
+ }
365
440
  }
366
441
  return count;
367
442
  });
368
- return tx(ids);
369
443
  }
370
444
 
371
- /**
372
- * List all unique tags with their occurrence counts.
373
- */
374
445
  getTags(): Array<{ tag: string; count: number }> {
375
- const rows = this.db.prepare('SELECT tags FROM entries').all() as Array<{ tags: string }>;
446
+ const rows = this.provider.all<{ tags: string }>('SELECT tags FROM entries');
376
447
  const counts = new Map<string, number>();
377
448
  for (const row of rows) {
378
449
  const tags: string[] = JSON.parse(row.tags || '[]');
@@ -385,49 +456,37 @@ export class Vault {
385
456
  .sort((a, b) => b.count - a.count);
386
457
  }
387
458
 
388
- /**
389
- * List all domains with their entry counts.
390
- */
391
459
  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;
460
+ return this.provider.all<{ domain: string; count: number }>(
461
+ 'SELECT domain, COUNT(*) as count FROM entries GROUP BY domain ORDER BY count DESC',
462
+ );
396
463
  }
397
464
 
398
- /**
399
- * Get recently added or updated entries, ordered by updated_at DESC.
400
- */
401
465
  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>>;
466
+ const rows = this.provider.all<Record<string, unknown>>(
467
+ 'SELECT * FROM entries ORDER BY updated_at DESC LIMIT ?',
468
+ [limit],
469
+ );
405
470
  return rows.map(rowToEntry);
406
471
  }
407
472
 
408
- /**
409
- * Export the entire vault as a JSON-serializable bundle.
410
- */
411
473
  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>>;
474
+ const rows = this.provider.all<Record<string, unknown>>(
475
+ 'SELECT * FROM entries ORDER BY domain, title',
476
+ );
415
477
  const entries = rows.map(rowToEntry);
416
478
  return { entries, exportedAt: Math.floor(Date.now() / 1000), count: entries.length };
417
479
  }
418
480
 
419
- /**
420
- * Get entry age distribution — how old entries are, bucketed.
421
- */
422
481
  getAgeReport(): {
423
482
  total: number;
424
483
  buckets: Array<{ label: string; count: number; minDays: number; maxDays: number }>;
425
484
  oldestTimestamp: number | null;
426
485
  newestTimestamp: number | null;
427
486
  } {
428
- const rows = this.db
429
- .prepare('SELECT created_at, updated_at FROM entries')
430
- .all() as Array<{ created_at: number; updated_at: number }>;
487
+ const rows = this.provider.all<{ created_at: number; updated_at: number }>(
488
+ 'SELECT created_at, updated_at FROM entries',
489
+ );
431
490
  const now = Math.floor(Date.now() / 1000);
432
491
  const bucketDefs = [
433
492
  { label: 'today', minDays: 0, maxDays: 1 },
@@ -463,21 +522,21 @@ export class Vault {
463
522
  const projectName = name ?? path.replace(/\/$/, '').split('/').pop() ?? path;
464
523
  const existing = this.getProject(path);
465
524
  if (existing) {
466
- this.db
467
- .prepare(
468
- 'UPDATE projects SET last_seen_at = unixepoch(), session_count = session_count + 1 WHERE path = ?',
469
- )
470
- .run(path);
525
+ this.provider.run(
526
+ 'UPDATE projects SET last_seen_at = unixepoch(), session_count = session_count + 1 WHERE path = ?',
527
+ [path],
528
+ );
471
529
  return this.getProject(path)!;
472
530
  }
473
- this.db.prepare('INSERT INTO projects (path, name) VALUES (?, ?)').run(path, projectName);
531
+ this.provider.run('INSERT INTO projects (path, name) VALUES (?, ?)', [path, projectName]);
474
532
  return this.getProject(path)!;
475
533
  }
476
534
 
477
535
  getProject(path: string): ProjectInfo | null {
478
- const row = this.db.prepare('SELECT * FROM projects WHERE path = ?').get(path) as
479
- | Record<string, unknown>
480
- | undefined;
536
+ const row = this.provider.get<Record<string, unknown>>(
537
+ 'SELECT * FROM projects WHERE path = ?',
538
+ [path],
539
+ );
481
540
  if (!row) return null;
482
541
  return {
483
542
  path: row.path as string,
@@ -489,9 +548,9 @@ export class Vault {
489
548
  }
490
549
 
491
550
  listProjects(): ProjectInfo[] {
492
- const rows = this.db
493
- .prepare('SELECT * FROM projects ORDER BY last_seen_at DESC')
494
- .all() as Array<Record<string, unknown>>;
551
+ const rows = this.provider.all<Record<string, unknown>>(
552
+ 'SELECT * FROM projects ORDER BY last_seen_at DESC',
553
+ );
495
554
  return rows.map((row) => ({
496
555
  path: row.path as string,
497
556
  name: row.name as string,
@@ -503,11 +562,9 @@ export class Vault {
503
562
 
504
563
  captureMemory(memory: Omit<Memory, 'id' | 'createdAt' | 'archivedAt'>): Memory {
505
564
  const id = `mem-${Date.now()}-${Math.random().toString(36).slice(2, 8)}`;
506
- this.db
507
- .prepare(
508
- `INSERT INTO memories (id, project_path, type, context, summary, topics, files_modified, tools_used) VALUES (@id, @projectPath, @type, @context, @summary, @topics, @filesModified, @toolsUsed)`,
509
- )
510
- .run({
565
+ this.provider.run(
566
+ `INSERT INTO memories (id, project_path, type, context, summary, topics, files_modified, tools_used) VALUES (@id, @projectPath, @type, @context, @summary, @topics, @filesModified, @toolsUsed)`,
567
+ {
511
568
  id,
512
569
  projectPath: memory.projectPath,
513
570
  type: memory.type,
@@ -516,7 +573,8 @@ export class Vault {
516
573
  topics: JSON.stringify(memory.topics),
517
574
  filesModified: JSON.stringify(memory.filesModified),
518
575
  toolsUsed: JSON.stringify(memory.toolsUsed),
519
- });
576
+ },
577
+ );
520
578
  return this.getMemory(id)!;
521
579
  }
522
580
 
@@ -537,11 +595,10 @@ export class Vault {
537
595
  }
538
596
  const wc = filters.length > 0 ? `AND ${filters.join(' AND ')}` : '';
539
597
  try {
540
- const rows = this.db
541
- .prepare(
542
- `SELECT m.* FROM memories_fts fts JOIN memories m ON m.rowid = fts.rowid WHERE memories_fts MATCH @query ${wc} ORDER BY rank LIMIT @limit`,
543
- )
544
- .all({ query, limit, ...fp }) as Array<Record<string, unknown>>;
598
+ const rows = this.provider.all<Record<string, unknown>>(
599
+ `SELECT m.* FROM memories_fts fts JOIN memories m ON m.rowid = fts.rowid WHERE memories_fts MATCH @query ${wc} ORDER BY rank LIMIT @limit`,
600
+ { query, limit, ...fp },
601
+ );
545
602
  return rows.map(rowToMemory);
546
603
  } catch {
547
604
  return [];
@@ -565,30 +622,23 @@ export class Vault {
565
622
  params.projectPath = options.projectPath;
566
623
  }
567
624
  const wc = `WHERE ${filters.join(' AND ')}`;
568
- const rows = this.db
569
- .prepare(`SELECT * FROM memories ${wc} ORDER BY created_at DESC LIMIT @limit OFFSET @offset`)
570
- .all({ ...params, limit: options?.limit ?? 50, offset: options?.offset ?? 0 }) as Array<
571
- Record<string, unknown>
572
- >;
625
+ const rows = this.provider.all<Record<string, unknown>>(
626
+ `SELECT * FROM memories ${wc} ORDER BY created_at DESC LIMIT @limit OFFSET @offset`,
627
+ { ...params, limit: options?.limit ?? 50, offset: options?.offset ?? 0 },
628
+ );
573
629
  return rows.map(rowToMemory);
574
630
  }
575
631
 
576
632
  memoryStats(): MemoryStats {
577
- const total = (
578
- this.db.prepare('SELECT COUNT(*) as count FROM memories WHERE archived_at IS NULL').get() as {
579
- count: number;
580
- }
581
- ).count;
582
- const byTypeRows = this.db
583
- .prepare(
584
- 'SELECT type as key, COUNT(*) as count FROM memories WHERE archived_at IS NULL GROUP BY type',
585
- )
586
- .all() as Array<{ key: string; count: number }>;
587
- const byProjectRows = this.db
588
- .prepare(
589
- 'SELECT project_path as key, COUNT(*) as count FROM memories WHERE archived_at IS NULL GROUP BY project_path',
590
- )
591
- .all() as Array<{ key: string; count: number }>;
633
+ const total = this.provider.get<{ count: number }>(
634
+ 'SELECT COUNT(*) as count FROM memories WHERE archived_at IS NULL',
635
+ )!.count;
636
+ const byTypeRows = this.provider.all<{ key: string; count: number }>(
637
+ 'SELECT type as key, COUNT(*) as count FROM memories WHERE archived_at IS NULL GROUP BY type',
638
+ );
639
+ const byProjectRows = this.provider.all<{ key: string; count: number }>(
640
+ 'SELECT project_path as key, COUNT(*) as count FROM memories WHERE archived_at IS NULL GROUP BY project_path',
641
+ );
592
642
  return {
593
643
  total,
594
644
  byType: Object.fromEntries(byTypeRows.map((r) => [r.key, r.count])),
@@ -597,15 +647,14 @@ export class Vault {
597
647
  }
598
648
 
599
649
  getMemory(id: string): Memory | null {
600
- const row = this.db.prepare('SELECT * FROM memories WHERE id = ?').get(id) as
601
- | Record<string, unknown>
602
- | undefined;
650
+ const row = this.provider.get<Record<string, unknown>>('SELECT * FROM memories WHERE id = ?', [
651
+ id,
652
+ ]);
603
653
  return row ? rowToMemory(row) : null;
604
654
  }
605
655
 
606
-
607
656
  deleteMemory(id: string): boolean {
608
- return this.db.prepare('DELETE FROM memories WHERE id = ?').run(id).changes > 0;
657
+ return this.provider.run('DELETE FROM memories WHERE id = ?', [id]).changes > 0;
609
658
  }
610
659
 
611
660
  memoryStatsDetailed(options?: {
@@ -629,35 +678,30 @@ export class Vault {
629
678
  }
630
679
  const wc = filters.length > 0 ? `WHERE ${filters.join(' AND ')}` : '';
631
680
 
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 };
681
+ const total = this.provider.get<{ count: number }>(
682
+ `SELECT COUNT(*) as count FROM memories ${wc}${wc ? ' AND' : ' WHERE'} archived_at IS NULL`,
683
+ params,
684
+ )!.count;
685
+
686
+ const archivedCount = this.provider.get<{ count: number }>(
687
+ `SELECT COUNT(*) as count FROM memories ${wc}${wc ? ' AND' : ' WHERE'} archived_at IS NOT NULL`,
688
+ params,
689
+ )!.count;
690
+
691
+ const byTypeRows = this.provider.all<{ key: string; count: number }>(
692
+ `SELECT type as key, COUNT(*) as count FROM memories ${wc}${wc ? ' AND' : ' WHERE'} archived_at IS NULL GROUP BY type`,
693
+ params,
694
+ );
695
+
696
+ const byProjectRows = this.provider.all<{ key: string; count: number }>(
697
+ `SELECT project_path as key, COUNT(*) as count FROM memories ${wc}${wc ? ' AND' : ' WHERE'} archived_at IS NULL GROUP BY project_path`,
698
+ params,
699
+ );
700
+
701
+ const dateRange = this.provider.get<{ oldest: number | null; newest: number | null }>(
702
+ `SELECT MIN(created_at) as oldest, MAX(created_at) as newest FROM memories ${wc}${wc ? ' AND' : ' WHERE'} archived_at IS NULL`,
703
+ params,
704
+ )!;
661
705
 
662
706
  return {
663
707
  total,
@@ -688,22 +732,23 @@ export class Vault {
688
732
  params.type = options.type;
689
733
  }
690
734
  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>>;
735
+ const rows = this.provider.all<Record<string, unknown>>(
736
+ `SELECT * FROM memories ${wc} ORDER BY created_at ASC`,
737
+ Object.keys(params).length > 0 ? params : undefined,
738
+ );
694
739
  return rows.map(rowToMemory);
695
740
  }
696
741
 
697
742
  importMemories(memories: Memory[]): { imported: number; skipped: number } {
698
- const upsert = this.db.prepare(`
743
+ const sql = `
699
744
  INSERT OR IGNORE INTO memories (id, project_path, type, context, summary, topics, files_modified, tools_used, created_at, archived_at)
700
745
  VALUES (@id, @projectPath, @type, @context, @summary, @topics, @filesModified, @toolsUsed, @createdAt, @archivedAt)
701
- `);
746
+ `;
702
747
  let imported = 0;
703
748
  let skipped = 0;
704
- const tx = this.db.transaction((items: Memory[]) => {
705
- for (const m of items) {
706
- const result = upsert.run({
749
+ this.provider.transaction(() => {
750
+ for (const m of memories) {
751
+ const result = this.provider.run(sql, {
707
752
  id: m.id,
708
753
  projectPath: m.projectPath,
709
754
  type: m.type,
@@ -719,22 +764,20 @@ export class Vault {
719
764
  else skipped++;
720
765
  }
721
766
  });
722
- tx(memories);
723
767
  return { imported, skipped };
724
768
  }
725
769
 
726
770
  pruneMemories(olderThanDays: number): { pruned: number } {
727
771
  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);
772
+ const result = this.provider.run(
773
+ 'DELETE FROM memories WHERE created_at < ? AND archived_at IS NULL',
774
+ [cutoff],
775
+ );
731
776
  return { pruned: result.changes };
732
777
  }
733
778
 
734
779
  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(`
780
+ const dupeRows = this.provider.all<{ id1: string; id2: string }>(`
738
781
  SELECT m1.id as id1, m2.id as id2
739
782
  FROM memories m1
740
783
  JOIN memories m2 ON m1.summary = m2.summary
@@ -743,10 +786,8 @@ export class Vault {
743
786
  AND m1.id < m2.id
744
787
  AND m1.archived_at IS NULL
745
788
  AND m2.archived_at IS NULL
746
- `)
747
- .all() as Array<{ id1: string; id2: string }>;
789
+ `);
748
790
 
749
- // Group: keep the earliest (id1), remove all later duplicates
750
791
  const groupMap = new Map<string, Set<string>>();
751
792
  for (const row of dupeRows) {
752
793
  if (!groupMap.has(row.id1)) groupMap.set(row.id1, new Set());
@@ -764,20 +805,20 @@ export class Vault {
764
805
  }
765
806
 
766
807
  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);
808
+ this.provider.transaction(() => {
809
+ for (const id of toRemove) {
810
+ this.provider.run('DELETE FROM memories WHERE id = ?', [id]);
811
+ }
770
812
  });
771
- tx([...toRemove]);
772
813
  }
773
814
 
774
815
  return { removed: toRemove.size, groups };
775
816
  }
776
817
 
777
818
  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 }>;
819
+ const rows = this.provider.all<{ topics: string }>(
820
+ 'SELECT topics FROM memories WHERE archived_at IS NULL',
821
+ );
781
822
 
782
823
  const topicCounts = new Map<string, number>();
783
824
  for (const row of rows) {
@@ -793,18 +834,15 @@ export class Vault {
793
834
  }
794
835
 
795
836
  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 }>;
837
+ const rows = this.provider.all<{ project: string; count: number }>(
838
+ 'SELECT project_path as project, COUNT(*) as count FROM memories WHERE archived_at IS NULL GROUP BY project_path ORDER BY count DESC',
839
+ );
801
840
 
802
841
  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>>;
842
+ const memories = this.provider.all<Record<string, unknown>>(
843
+ 'SELECT * FROM memories WHERE project_path = ? AND archived_at IS NULL ORDER BY created_at DESC',
844
+ [row.project],
845
+ );
808
846
  return {
809
847
  project: row.project,
810
848
  count: row.count,
@@ -813,19 +851,45 @@ export class Vault {
813
851
  });
814
852
  }
815
853
 
816
- getDb(): Database.Database {
817
- return this.db;
854
+ /**
855
+ * Rebuild the FTS5 index for the entries table.
856
+ * Useful after bulk operations or if the index gets out of sync.
857
+ */
858
+ rebuildFtsIndex(): void {
859
+ try {
860
+ this.provider.run("INSERT INTO entries_fts(entries_fts) VALUES('rebuild')");
861
+ } catch {
862
+ // Graceful degradation — FTS rebuild failed (e.g. table doesn't exist yet)
863
+ }
864
+ }
865
+
866
+ /**
867
+ * Get the underlying persistence provider.
868
+ */
869
+ getProvider(): PersistenceProvider {
870
+ return this.provider;
871
+ }
872
+
873
+ /**
874
+ * Get the raw better-sqlite3 Database (backward compat).
875
+ * Throws if the provider is not SQLite.
876
+ */
877
+ getDb(): import('better-sqlite3').Database {
878
+ if (this.sqliteProvider) {
879
+ return this.sqliteProvider.getDatabase();
880
+ }
881
+ throw new Error('getDb() is only available with SQLite provider');
818
882
  }
819
883
 
820
884
  close(): void {
821
- this.db.close();
885
+ this.provider.close();
822
886
  }
823
887
  }
824
888
 
825
- function gc(db: Database.Database, col: string): Record<string, number> {
826
- const rows = db
827
- .prepare(`SELECT ${col} as key, COUNT(*) as count FROM entries GROUP BY ${col}`)
828
- .all() as Array<{ key: string; count: number }>;
889
+ function gc(provider: PersistenceProvider, col: string): Record<string, number> {
890
+ const rows = provider.all<{ key: string; count: number }>(
891
+ `SELECT ${col} as key, COUNT(*) as count FROM entries GROUP BY ${col}`,
892
+ );
829
893
  return Object.fromEntries(rows.map((r) => [r.key, r.count]));
830
894
  }
831
895
 
@@ -843,6 +907,8 @@ function rowToEntry(row: Record<string, unknown>): IntelligenceEntry {
843
907
  why: (row.why as string) ?? undefined,
844
908
  tags: JSON.parse((row.tags as string) || '[]'),
845
909
  appliesTo: JSON.parse((row.applies_to as string) || '[]'),
910
+ validFrom: (row.valid_from as number) ?? undefined,
911
+ validUntil: (row.valid_until as number) ?? undefined,
846
912
  };
847
913
  }
848
914