coding-friend-cli 1.15.0 → 1.17.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 (75) hide show
  1. package/README.md +12 -0
  2. package/dist/{chunk-PYRGNY5P.js → chunk-C5LYVVEI.js} +9 -1
  3. package/dist/{chunk-X5WEODUD.js → chunk-CYQU33FY.js} +1 -0
  4. package/dist/{chunk-ITL5TY3B.js → chunk-G6CEEMAR.js} +3 -3
  5. package/dist/{chunk-4DB4XTSL.js → chunk-KTX4MGMR.js} +15 -1
  6. package/dist/{chunk-KJUGTLPQ.js → chunk-YO6JKGR3.js} +38 -2
  7. package/dist/{config-UQ742WPQ.js → config-LZFXXOI4.js} +276 -14
  8. package/dist/{dev-WJ5QQ35B.js → dev-R3IYWZ3M.js} +2 -2
  9. package/dist/{disable-AOZ7FLZD.js → disable-R6K5YJN4.js} +2 -2
  10. package/dist/{enable-MJVTT3RU.js → enable-HF4PYVJN.js} +2 -2
  11. package/dist/{host-NA7LZ4HX.js → host-SYZH3FVC.js} +4 -4
  12. package/dist/index.js +78 -18
  13. package/dist/{init-AHIEQ27W.js → init-YK6YRTOT.js} +271 -23
  14. package/dist/{install-EIN7Z5V3.js → install-Q4PWEU43.js} +4 -4
  15. package/dist/{mcp-DLS3J6QJ.js → mcp-TBEDYELW.js} +4 -4
  16. package/dist/memory-7RM67ZLS.js +668 -0
  17. package/dist/postinstall.js +1 -1
  18. package/dist/{session-E3CZJJZQ.js → session-H4XW2WXH.js} +1 -1
  19. package/dist/{statusline-6HQCDWBD.js → statusline-6Y2EBAFQ.js} +1 -1
  20. package/dist/{uninstall-2IOZZERP.js → uninstall-3PSUDGI4.js} +3 -3
  21. package/dist/{update-IZ5UEKZN.js → update-WL6SFGGO.js} +4 -4
  22. package/lib/cf-memory/CHANGELOG.md +15 -0
  23. package/lib/cf-memory/README.md +284 -0
  24. package/lib/cf-memory/package-lock.json +2790 -0
  25. package/lib/cf-memory/package.json +31 -0
  26. package/lib/cf-memory/scripts/migrate-frontmatter.ts +134 -0
  27. package/lib/cf-memory/src/__tests__/daemon-e2e.test.ts +223 -0
  28. package/lib/cf-memory/src/__tests__/daemon.test.ts +407 -0
  29. package/lib/cf-memory/src/__tests__/dedup.test.ts +103 -0
  30. package/lib/cf-memory/src/__tests__/embeddings.test.ts +292 -0
  31. package/lib/cf-memory/src/__tests__/lazy-install.test.ts +210 -0
  32. package/lib/cf-memory/src/__tests__/markdown-backend.test.ts +410 -0
  33. package/lib/cf-memory/src/__tests__/migration.test.ts +255 -0
  34. package/lib/cf-memory/src/__tests__/migrations.test.ts +288 -0
  35. package/lib/cf-memory/src/__tests__/minisearch-backend.test.ts +262 -0
  36. package/lib/cf-memory/src/__tests__/ollama.test.ts +48 -0
  37. package/lib/cf-memory/src/__tests__/schema.test.ts +128 -0
  38. package/lib/cf-memory/src/__tests__/search.test.ts +115 -0
  39. package/lib/cf-memory/src/__tests__/temporal-decay.test.ts +54 -0
  40. package/lib/cf-memory/src/__tests__/tier.test.ts +293 -0
  41. package/lib/cf-memory/src/__tests__/tools.test.ts +83 -0
  42. package/lib/cf-memory/src/backends/markdown.ts +318 -0
  43. package/lib/cf-memory/src/backends/minisearch.ts +203 -0
  44. package/lib/cf-memory/src/backends/sqlite/embeddings.ts +286 -0
  45. package/lib/cf-memory/src/backends/sqlite/index.ts +549 -0
  46. package/lib/cf-memory/src/backends/sqlite/migrations.ts +188 -0
  47. package/lib/cf-memory/src/backends/sqlite/schema.ts +120 -0
  48. package/lib/cf-memory/src/backends/sqlite/search.ts +296 -0
  49. package/lib/cf-memory/src/bin/cf-memory.ts +2 -0
  50. package/lib/cf-memory/src/daemon/entry.ts +99 -0
  51. package/lib/cf-memory/src/daemon/process.ts +220 -0
  52. package/lib/cf-memory/src/daemon/server.ts +166 -0
  53. package/lib/cf-memory/src/daemon/watcher.ts +90 -0
  54. package/lib/cf-memory/src/index.ts +45 -0
  55. package/lib/cf-memory/src/lib/backend.ts +23 -0
  56. package/lib/cf-memory/src/lib/daemon-client.ts +163 -0
  57. package/lib/cf-memory/src/lib/dedup.ts +80 -0
  58. package/lib/cf-memory/src/lib/lazy-install.ts +274 -0
  59. package/lib/cf-memory/src/lib/ollama.ts +76 -0
  60. package/lib/cf-memory/src/lib/temporal-decay.ts +19 -0
  61. package/lib/cf-memory/src/lib/tier.ts +107 -0
  62. package/lib/cf-memory/src/lib/types.ts +109 -0
  63. package/lib/cf-memory/src/resources/index.ts +62 -0
  64. package/lib/cf-memory/src/server.ts +20 -0
  65. package/lib/cf-memory/src/tools/delete.ts +38 -0
  66. package/lib/cf-memory/src/tools/list.ts +38 -0
  67. package/lib/cf-memory/src/tools/retrieve.ts +52 -0
  68. package/lib/cf-memory/src/tools/search.ts +47 -0
  69. package/lib/cf-memory/src/tools/store.ts +70 -0
  70. package/lib/cf-memory/src/tools/update.ts +62 -0
  71. package/lib/cf-memory/tsconfig.json +15 -0
  72. package/lib/cf-memory/vitest.config.ts +7 -0
  73. package/lib/learn-host/CHANGELOG.md +4 -1
  74. package/lib/learn-host/package.json +1 -1
  75. package/package.json +1 -1
@@ -0,0 +1,549 @@
1
+ /**
2
+ * SqliteBackend — Tier 1 memory backend with hybrid search.
3
+ *
4
+ * Uses better-sqlite3 for storage, FTS5 for keyword search,
5
+ * sqlite-vec for vector similarity, and RRF for fusion.
6
+ *
7
+ * Markdown files remain the source of truth. SQLite is a derived index
8
+ * that can be rebuilt from markdown at any time.
9
+ *
10
+ * DB path: ~/.coding-friend/memory/projects/{12-char-sha256}/db.sqlite
11
+ */
12
+ import fs from "node:fs";
13
+ import path from "node:path";
14
+ import crypto from "node:crypto";
15
+ import type { MemoryBackend } from "../../lib/backend.js";
16
+ import { MarkdownBackend } from "../markdown.js";
17
+ import {
18
+ makeExcerpt,
19
+ type ListInput,
20
+ type Memory,
21
+ type MemoryFrontmatter,
22
+ type MemoryMeta,
23
+ type MemoryStats,
24
+ type MemoryType,
25
+ type SearchInput,
26
+ type SearchResult,
27
+ type StoreInput,
28
+ type UpdateInput,
29
+ } from "../../lib/types.js";
30
+ import { loadDepSync } from "../../lib/lazy-install.js";
31
+ import {
32
+ migrate,
33
+ createVecTable,
34
+ checkEmbeddingMismatch,
35
+ getMetadata,
36
+ setMetadata,
37
+ type DatabaseLike,
38
+ } from "./migrations.js";
39
+ import {
40
+ EmbeddingPipeline,
41
+ EmbeddingCache,
42
+ contentHash,
43
+ prepareEmbeddingText,
44
+ type EmbeddingConfig,
45
+ } from "./embeddings.js";
46
+ import { hybridSearch } from "./search.js";
47
+
48
+ /**
49
+ * Compute a short hash of the docsDir path for project isolation.
50
+ */
51
+ function projectHash(docsDir: string): string {
52
+ return crypto
53
+ .createHash("sha256")
54
+ .update(path.resolve(docsDir))
55
+ .digest("hex")
56
+ .slice(0, 12);
57
+ }
58
+
59
+ export interface SqliteBackendOptions {
60
+ depsDir?: string;
61
+ dbPath?: string;
62
+ embedding?: Partial<EmbeddingConfig>;
63
+ skipVec?: boolean;
64
+ }
65
+
66
+ export class SqliteBackend implements MemoryBackend {
67
+ private db: DatabaseLike;
68
+ private markdown: MarkdownBackend;
69
+ private pipeline: EmbeddingPipeline | null = null;
70
+ private cache: EmbeddingCache | null = null;
71
+ private vecEnabled = false;
72
+ private needsEmbeddingRebuild = false;
73
+ private dbPath: string;
74
+
75
+ constructor(
76
+ private docsDir: string,
77
+ opts?: SqliteBackendOptions,
78
+ ) {
79
+ this.markdown = new MarkdownBackend(docsDir);
80
+
81
+ // Determine DB path
82
+ if (opts?.dbPath) {
83
+ this.dbPath = opts.dbPath;
84
+ } else {
85
+ const depsDir = opts?.depsDir ?? this.getDefaultDepsDir();
86
+ const hash = projectHash(docsDir);
87
+ const projectDir = path.join(depsDir, "projects", hash);
88
+ this.dbPath = path.join(projectDir, "db.sqlite");
89
+ }
90
+
91
+ // Open database — defers mkdirSync until after dep check to avoid empty dirs
92
+ this.db = this.openDatabase(opts?.depsDir);
93
+
94
+ // Run migrations
95
+ migrate(this.db);
96
+
97
+ // Store the resolved docsDir so `cf memory list --projects` can display the source path
98
+ const resolvedDir = path.resolve(docsDir);
99
+ if (getMetadata(this.db, "source_dir") !== resolvedDir) {
100
+ setMetadata(this.db, "source_dir", resolvedDir);
101
+ }
102
+
103
+ // Try to set up vector search
104
+ if (!opts?.skipVec) {
105
+ this.vecEnabled = this.setupVec(opts?.depsDir);
106
+ }
107
+
108
+ // Set up embedding pipeline and cache
109
+ if (this.vecEnabled) {
110
+ this.pipeline = new EmbeddingPipeline({
111
+ ...opts?.embedding,
112
+ depsDir: opts?.depsDir,
113
+ });
114
+ this.cache = new EmbeddingCache(this.db);
115
+
116
+ // Check if embedding model changed
117
+ const mismatch = checkEmbeddingMismatch(
118
+ this.db,
119
+ this.pipeline.modelName,
120
+ this.pipeline.dims,
121
+ );
122
+ if (mismatch.mismatched) {
123
+ process.stderr.write(
124
+ `[cf-memory] Embedding model changed: ${mismatch.storedModel} (${mismatch.storedDims}d) → ${mismatch.currentModel} (${mismatch.currentDims}d)\n` +
125
+ `[cf-memory] Vector search disabled. Run "cf memory rebuild" to re-embed.\n`,
126
+ );
127
+ this.vecEnabled = false;
128
+ this.needsEmbeddingRebuild = true;
129
+ }
130
+ }
131
+ }
132
+
133
+ private getDefaultDepsDir(): string {
134
+ const home = process.env.HOME ?? process.env.USERPROFILE ?? "/tmp";
135
+ return path.join(home, ".coding-friend", "memory");
136
+ }
137
+
138
+ private openDatabase(depsDir?: string): DatabaseLike {
139
+ const Database = loadDepSync<{ new (path: string): DatabaseLike }>(
140
+ "better-sqlite3",
141
+ depsDir,
142
+ );
143
+ // better-sqlite3 exports the class as default in CJS
144
+ const DbConstructor =
145
+ (Database as unknown as { default: { new (path: string): DatabaseLike } })
146
+ .default ?? Database;
147
+ fs.mkdirSync(path.dirname(this.dbPath), { recursive: true });
148
+ return new DbConstructor(this.dbPath);
149
+ }
150
+
151
+ private setupVec(depsDir?: string): boolean {
152
+ try {
153
+ const sqliteVec = loadDepSync<{ load: (db: unknown) => void }>(
154
+ "sqlite-vec",
155
+ depsDir,
156
+ );
157
+ const loader =
158
+ (sqliteVec as unknown as { default: { load: (db: unknown) => void } })
159
+ .default ?? sqliteVec;
160
+ loader.load(this.db);
161
+ return createVecTable(this.db);
162
+ } catch {
163
+ return false;
164
+ }
165
+ }
166
+
167
+ getDbPath(): string {
168
+ return this.dbPath;
169
+ }
170
+
171
+ isVecEnabled(): boolean {
172
+ return this.vecEnabled;
173
+ }
174
+
175
+ isRebuildNeeded(): boolean {
176
+ return this.needsEmbeddingRebuild;
177
+ }
178
+
179
+ async store(input: StoreInput): Promise<Memory> {
180
+ // 1. Store to markdown (source of truth)
181
+ const memory = await this.markdown.store(input);
182
+
183
+ // 2. Index in SQLite
184
+ const hash = contentHash(
185
+ prepareEmbeddingText({
186
+ title: memory.frontmatter.title,
187
+ description: memory.frontmatter.description,
188
+ tags: memory.frontmatter.tags,
189
+ content: memory.content,
190
+ }),
191
+ );
192
+
193
+ this.upsertRow(memory, hash);
194
+
195
+ // 3. Generate and store embedding (async, non-blocking for store)
196
+ if (this.vecEnabled && this.pipeline) {
197
+ this.embedAndStore(memory.id, memory, hash).catch((err) => {
198
+ process.stderr.write(
199
+ `[cf-memory] Embedding failed for ${memory.id}: ${err instanceof Error ? err.message : String(err)}\n`,
200
+ );
201
+ });
202
+ }
203
+
204
+ return memory;
205
+ }
206
+
207
+ async search(input: SearchInput): Promise<SearchResult[]> {
208
+ const query = input.query?.trim();
209
+
210
+ if (!query) {
211
+ const metas = await this.list({ type: input.type, limit: input.limit });
212
+ return metas.map((m) => ({ memory: m, score: 0, matchedOn: [] }));
213
+ }
214
+
215
+ const limit = input.limit ?? 10;
216
+
217
+ const ranked = await hybridSearch({
218
+ db: this.db,
219
+ query,
220
+ limit,
221
+ pipeline: this.pipeline,
222
+ vecEnabled: this.vecEnabled,
223
+ typeFilter: input.type,
224
+ });
225
+
226
+ // Convert ranked results to SearchResult format
227
+ const results: SearchResult[] = [];
228
+ for (const r of ranked) {
229
+ const row = this.getRow(r.id);
230
+ if (!row) continue;
231
+
232
+ // Apply tag filter
233
+ if (input.tags && input.tags.length > 0) {
234
+ const rowTags = JSON.parse(row.tags as string) as string[];
235
+ const lowerTags = input.tags.map((t) => t.toLowerCase());
236
+ if (
237
+ !lowerTags.some((lt) => rowTags.some((rt) => rt.toLowerCase() === lt))
238
+ ) {
239
+ continue;
240
+ }
241
+ }
242
+
243
+ const frontmatter = this.rowToFrontmatter(row);
244
+ results.push({
245
+ memory: {
246
+ id: String(row.id),
247
+ slug: String(row.slug),
248
+ category: String(row.category),
249
+ frontmatter,
250
+ excerpt: makeExcerpt(String(row.content)),
251
+ },
252
+ score: r.score,
253
+ matchedOn: r.matchedOn,
254
+ });
255
+ }
256
+
257
+ return results;
258
+ }
259
+
260
+ async retrieve(id: string): Promise<Memory | null> {
261
+ // Read from markdown (source of truth)
262
+ return this.markdown.retrieve(id);
263
+ }
264
+
265
+ async list(input: ListInput): Promise<MemoryMeta[]> {
266
+ let sql = "SELECT * FROM memories WHERE 1=1";
267
+ const params: unknown[] = [];
268
+
269
+ if (input.type) {
270
+ sql += " AND type = ?";
271
+ params.push(input.type);
272
+ }
273
+
274
+ if (input.category) {
275
+ sql += " AND category = ?";
276
+ params.push(input.category);
277
+ }
278
+
279
+ sql += " ORDER BY updated DESC";
280
+
281
+ const limit = input.limit ?? 50;
282
+ sql += " LIMIT ?";
283
+ params.push(limit);
284
+
285
+ const stmt = this.db.prepare(sql);
286
+ const rows = (
287
+ stmt as unknown as {
288
+ all(...p: unknown[]): Array<Record<string, unknown>>;
289
+ }
290
+ ).all(...params);
291
+
292
+ return rows.map((row) => ({
293
+ id: String(row.id),
294
+ slug: String(row.slug),
295
+ category: String(row.category),
296
+ frontmatter: this.rowToFrontmatter(row),
297
+ excerpt: makeExcerpt(String(row.content)),
298
+ }));
299
+ }
300
+
301
+ async update(input: UpdateInput): Promise<Memory | null> {
302
+ // 1. Update markdown (source of truth)
303
+ const memory = await this.markdown.update(input);
304
+ if (!memory) return null;
305
+
306
+ // 2. Re-index in SQLite
307
+ const hash = contentHash(
308
+ prepareEmbeddingText({
309
+ title: memory.frontmatter.title,
310
+ description: memory.frontmatter.description,
311
+ tags: memory.frontmatter.tags,
312
+ content: memory.content,
313
+ }),
314
+ );
315
+
316
+ this.upsertRow(memory, hash);
317
+
318
+ // 3. Re-embed
319
+ if (this.vecEnabled && this.pipeline) {
320
+ this.embedAndStore(memory.id, memory, hash).catch((err) => {
321
+ process.stderr.write(
322
+ `[cf-memory] Embedding failed for ${memory.id}: ${err instanceof Error ? err.message : String(err)}\n`,
323
+ );
324
+ });
325
+ }
326
+
327
+ return memory;
328
+ }
329
+
330
+ async delete(id: string): Promise<boolean> {
331
+ // 1. Delete from markdown
332
+ const deleted = await this.markdown.delete(id);
333
+ if (!deleted) return false;
334
+
335
+ // 2. Remove from SQLite
336
+ this.db.prepare("DELETE FROM memories WHERE id = ?").run(id);
337
+
338
+ // 3. Remove from vec table
339
+ if (this.vecEnabled) {
340
+ try {
341
+ this.db.prepare("DELETE FROM vec_memories WHERE memory_id = ?").run(id);
342
+ } catch {
343
+ // Ignore vec errors
344
+ }
345
+ }
346
+
347
+ return true;
348
+ }
349
+
350
+ async stats(): Promise<MemoryStats> {
351
+ const rows = (
352
+ this.db.prepare(
353
+ "SELECT category, type, COUNT(*) as count FROM memories GROUP BY category, type",
354
+ ) as unknown as {
355
+ all(): Array<{ category: string; type: string; count: number }>;
356
+ }
357
+ ).all();
358
+
359
+ const byCategory: Record<string, number> = {};
360
+ const byType: Record<string, number> = {};
361
+ let total = 0;
362
+
363
+ for (const row of rows) {
364
+ byCategory[row.category] = (byCategory[row.category] ?? 0) + row.count;
365
+ byType[row.type] = (byType[row.type] ?? 0) + row.count;
366
+ total += row.count;
367
+ }
368
+
369
+ return { total, byCategory, byType };
370
+ }
371
+
372
+ /**
373
+ * Rebuild the SQLite index from markdown files.
374
+ *
375
+ * Two-pass approach:
376
+ * 1. Synchronous transaction: clear + re-insert all rows (atomic)
377
+ * 2. Async pass: generate embeddings (non-transactional, failures logged)
378
+ */
379
+ async rebuild(): Promise<void> {
380
+ // If embedding model changed, recreate vec table with new dimensions
381
+ if (this.needsEmbeddingRebuild && this.pipeline) {
382
+ try {
383
+ this.db.prepare("DROP TABLE IF EXISTS vec_memories").run();
384
+ createVecTable(this.db, this.pipeline.dims);
385
+ this.vecEnabled = true;
386
+ } catch {
387
+ // Vec setup failed
388
+ }
389
+ }
390
+
391
+ // Read all markdown files first
392
+ const metas = this.markdown.getAllMeta();
393
+ const memories: Array<{ memory: Memory; hash: string }> = [];
394
+
395
+ for (const meta of metas) {
396
+ const full = await this.markdown.retrieve(meta.id);
397
+ if (!full) continue;
398
+
399
+ const hash = contentHash(
400
+ prepareEmbeddingText({
401
+ title: full.frontmatter.title,
402
+ description: full.frontmatter.description,
403
+ tags: full.frontmatter.tags,
404
+ content: full.content,
405
+ }),
406
+ );
407
+ memories.push({ memory: full, hash });
408
+ }
409
+
410
+ // Pass 1: Atomic transaction for all SQLite rows
411
+ this.db.prepare("BEGIN").run();
412
+ try {
413
+ this.db.prepare("DELETE FROM memories").run();
414
+ if (this.vecEnabled) {
415
+ try {
416
+ this.db.prepare("DELETE FROM vec_memories").run();
417
+ } catch {
418
+ // Ignore if vec table doesn't exist
419
+ }
420
+ }
421
+ this.db.prepare("DELETE FROM embedding_cache").run();
422
+
423
+ for (const { memory, hash } of memories) {
424
+ this.upsertRow(memory, hash);
425
+ }
426
+
427
+ this.db.prepare("COMMIT").run();
428
+ } catch (err) {
429
+ this.db.prepare("ROLLBACK").run();
430
+ throw err;
431
+ }
432
+
433
+ // Pass 2: Async embedding generation (non-transactional)
434
+ if (this.vecEnabled && this.pipeline) {
435
+ for (const { memory, hash } of memories) {
436
+ try {
437
+ await this.embedAndStore(memory.id, memory, hash);
438
+ } catch (err) {
439
+ process.stderr.write(
440
+ `[cf-memory] Embedding failed for ${memory.id}: ${err instanceof Error ? err.message : String(err)}\n`,
441
+ );
442
+ }
443
+ }
444
+ }
445
+
446
+ // Update metadata with current embedding model info
447
+ if (this.pipeline) {
448
+ setMetadata(this.db, "embedding_model", this.pipeline.modelName);
449
+ setMetadata(this.db, "embedding_dims", String(this.pipeline.dims));
450
+ this.needsEmbeddingRebuild = false;
451
+ }
452
+ }
453
+
454
+ async close(): Promise<void> {
455
+ try {
456
+ (this.db as unknown as { close(): void }).close();
457
+ } catch {
458
+ // Already closed
459
+ }
460
+ }
461
+
462
+ // --- Private helpers ---
463
+
464
+ private upsertRow(memory: Memory, hash: string): void {
465
+ const sql = `
466
+ INSERT OR REPLACE INTO memories
467
+ (id, slug, category, title, description, type, tags, importance, created, updated, source, content, content_hash)
468
+ VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
469
+ `;
470
+
471
+ this.db
472
+ .prepare(sql)
473
+ .run(
474
+ memory.id,
475
+ memory.slug,
476
+ memory.category,
477
+ memory.frontmatter.title,
478
+ memory.frontmatter.description,
479
+ memory.frontmatter.type,
480
+ JSON.stringify(memory.frontmatter.tags),
481
+ memory.frontmatter.importance,
482
+ memory.frontmatter.created,
483
+ memory.frontmatter.updated,
484
+ memory.frontmatter.source,
485
+ memory.content,
486
+ hash,
487
+ );
488
+ }
489
+
490
+ private getRow(id: string): Record<string, unknown> | null {
491
+ return (
492
+ (
493
+ this.db.prepare("SELECT * FROM memories WHERE id = ?") as unknown as {
494
+ get(id: string): Record<string, unknown> | undefined;
495
+ }
496
+ ).get(id) ?? null
497
+ );
498
+ }
499
+
500
+ private rowToFrontmatter(row: Record<string, unknown>): MemoryFrontmatter {
501
+ return {
502
+ title: String(row.title),
503
+ description: String(row.description),
504
+ type: String(row.type) as MemoryType,
505
+ tags: JSON.parse(String(row.tags ?? "[]")),
506
+ importance: Number(row.importance ?? 3),
507
+ created: String(row.created),
508
+ updated: String(row.updated),
509
+ source: String(row.source ?? "conversation"),
510
+ };
511
+ }
512
+
513
+ private async embedAndStore(
514
+ id: string,
515
+ memory: Memory,
516
+ hash: string,
517
+ ): Promise<void> {
518
+ if (!this.pipeline || !this.cache) return;
519
+
520
+ // Check cache first
521
+ let embedding = this.cache.get(hash, this.pipeline.dims);
522
+ if (!embedding) {
523
+ const text = prepareEmbeddingText({
524
+ title: memory.frontmatter.title,
525
+ description: memory.frontmatter.description,
526
+ tags: memory.frontmatter.tags,
527
+ content: memory.content,
528
+ });
529
+ embedding = await this.pipeline.embed(text);
530
+ this.cache.set(hash, embedding, this.pipeline.modelName);
531
+ }
532
+
533
+ // Store in vec table — use byteOffset/byteLength for correct Float32Array handling
534
+ const buffer = Buffer.from(
535
+ embedding.buffer,
536
+ embedding.byteOffset,
537
+ embedding.byteLength,
538
+ );
539
+ try {
540
+ this.db
541
+ .prepare(
542
+ "INSERT OR REPLACE INTO vec_memories (memory_id, embedding) VALUES (?, ?)",
543
+ )
544
+ .run(id, buffer);
545
+ } catch {
546
+ // sqlite-vec error — ignore
547
+ }
548
+ }
549
+ }