coding-friend-cli 1.36.4 → 1.38.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 (71) hide show
  1. package/README.md +8 -3
  2. package/dist/{chunk-GZT3YPAP.js → chunk-32UAW3NX.js} +1 -1
  3. package/dist/chunk-5QCGEPQO.js +238 -0
  4. package/dist/{chunk-YIXAQ4R2.js → chunk-6NXZKAOV.js} +1 -1
  5. package/dist/{chunk-XHP3EBR3.js → chunk-776ZY62N.js} +112 -4
  6. package/dist/{chunk-N4WZWRCI.js → chunk-BXSCYGWE.js} +85 -37
  7. package/dist/{chunk-AK6RMCAC.js → chunk-CGEMETKQ.js} +44 -30
  8. package/dist/{chunk-XEKQW4RN.js → chunk-ETTZRJ55.js} +1 -1
  9. package/dist/{chunk-IKLMHJH6.js → chunk-GIH6E7GJ.js} +62 -1
  10. package/dist/{chunk-56YJWGAN.js → chunk-ONG4KRYP.js} +60 -47
  11. package/dist/{chunk-X2H7JJMR.js → chunk-PLSJW7QJ.js} +29 -1
  12. package/dist/{chunk-O5ZKLFSL.js → chunk-RCE4OWNX.js} +1 -1
  13. package/dist/{chunk-K4WSC62G.js → chunk-RFNKPAJW.js} +42 -2
  14. package/dist/{clean-Q4VU662V.js → clean-YWEMYDE5.js} +3 -2
  15. package/dist/{config-CTTHWJH5.js → config-NG7SICM4.js} +15 -27
  16. package/dist/{dev-IGHMMEP4.js → dev-PTBNMKZT.js} +3 -3
  17. package/dist/{disable-HL5OEURT.js → disable-MO7AXYYF.js} +20 -3
  18. package/dist/{enable-CZIOCKKJ.js → enable-4GXLQEHD.js} +20 -3
  19. package/dist/{guide-CO2V4ZLZ.js → guide-HCR43LJN.js} +1 -1
  20. package/dist/{host-NCJMUSME.js → host-EV6QIVTI.js} +3 -3
  21. package/dist/index.js +62 -48
  22. package/dist/{init-UABKVDVT.js → init-KYE5CJMW.js} +135 -17
  23. package/dist/{install-R2HDM3N4.js → install-XAFQY5YF.js} +80 -5
  24. package/dist/{learn-K64AUUFB.js → learn-MC4VDHZH.js} +2 -2
  25. package/dist/{mcp-OMQFSJP4.js → mcp-FV6YX3ZT.js} +24 -24
  26. package/dist/{mcp-serve-3FEPFVVQ.js → mcp-serve-PXREYQRZ.js} +10 -1
  27. package/dist/{mcp-serve-learn-OF3ZJUJC.js → mcp-serve-learn-UVXIDOEZ.js} +1 -1
  28. package/dist/{memory-Y6GNALG3.js → memory-DF7OUOTV.js} +7 -7
  29. package/dist/{permission-I3OFJUYA.js → permission-VISPGMUK.js} +38 -4
  30. package/dist/{permissions-J46VP6US.js → permissions-2SBACQHV.js} +2 -2
  31. package/dist/{session-Q2DLQPYP.js → session-QK6OZO3X.js} +3 -2
  32. package/dist/{status-ML4XEDYL.js → status-FVFTJ6GS.js} +18 -14
  33. package/dist/{statusline-KKFJPTI7.js → statusline-AOZHEQSV.js} +2 -2
  34. package/dist/{uninstall-ASLHT3MH.js → uninstall-MF6FRUBO.js} +64 -3
  35. package/dist/{update-DIOH45TE.js → update-QNUCSDSA.js} +7 -4
  36. package/dist/{update-check-25SCLHZO.js → update-check-7LKJLJPM.js} +1 -1
  37. package/lib/cf-memory/src/backends/sqlite/index.ts +142 -85
  38. package/lib/cf-memory/src/lib/claude-md.ts +82 -70
  39. package/lib/cf-memory/src/lib/status-frame.ts +2 -2
  40. package/lib/cf-memory/src/tools/delete.ts +1 -1
  41. package/lib/cf-memory/src/tools/store.ts +6 -6
  42. package/lib/cf-memory/src/tools/update.ts +5 -11
  43. package/package.json +32 -4
  44. package/lib/cf-memory/README.md +0 -317
  45. package/lib/cf-memory/src/__tests__/claude-md.test.ts +0 -343
  46. package/lib/cf-memory/src/__tests__/daemon-client.test.ts +0 -146
  47. package/lib/cf-memory/src/__tests__/daemon-e2e.test.ts +0 -223
  48. package/lib/cf-memory/src/__tests__/daemon.test.ts +0 -407
  49. package/lib/cf-memory/src/__tests__/dedup.test.ts +0 -103
  50. package/lib/cf-memory/src/__tests__/embeddings.test.ts +0 -292
  51. package/lib/cf-memory/src/__tests__/lazy-install.test.ts +0 -210
  52. package/lib/cf-memory/src/__tests__/markdown-backend.test.ts +0 -450
  53. package/lib/cf-memory/src/__tests__/migration.test.ts +0 -255
  54. package/lib/cf-memory/src/__tests__/migrations.test.ts +0 -288
  55. package/lib/cf-memory/src/__tests__/minisearch-backend.test.ts +0 -262
  56. package/lib/cf-memory/src/__tests__/ollama.test.ts +0 -48
  57. package/lib/cf-memory/src/__tests__/project-id.test.ts +0 -43
  58. package/lib/cf-memory/src/__tests__/schema.test.ts +0 -128
  59. package/lib/cf-memory/src/__tests__/search.test.ts +0 -115
  60. package/lib/cf-memory/src/__tests__/status-frame.test.ts +0 -136
  61. package/lib/cf-memory/src/__tests__/temporal-decay.test.ts +0 -54
  62. package/lib/cf-memory/src/__tests__/tier.test.ts +0 -367
  63. package/lib/cf-memory/src/__tests__/tools.test.ts +0 -83
  64. package/lib/cf-memory/vitest.config.ts +0 -7
  65. package/lib/learn-host/.prettierignore +0 -4
  66. package/lib/learn-host/.prettierrc +0 -8
  67. package/lib/learn-host/README.md +0 -113
  68. package/lib/learn-host/eslint.config.mjs +0 -6
  69. package/lib/learn-mcp/README.md +0 -187
  70. package/lib/learn-mcp/src/__tests__/docs.test.ts +0 -117
  71. package/lib/learn-mcp/vitest.config.ts +0 -8
@@ -66,21 +66,23 @@ export interface SqliteBackendOptions {
66
66
  }
67
67
 
68
68
  export class SqliteBackend implements MemoryBackend {
69
- private db: DatabaseLike;
69
+ private db: DatabaseLike | null = null;
70
70
  private markdown: MarkdownBackend;
71
71
  private pipeline: EmbeddingPipeline | null = null;
72
72
  private cache: EmbeddingCache | null = null;
73
73
  private vecEnabled = false;
74
74
  private needsEmbeddingRebuild = false;
75
75
  private dbPath: string;
76
+ private opts: SqliteBackendOptions | undefined;
76
77
 
77
78
  constructor(
78
79
  private docsDir: string,
79
80
  opts?: SqliteBackendOptions,
80
81
  ) {
81
82
  this.markdown = new MarkdownBackend(docsDir);
83
+ this.opts = opts;
82
84
 
83
- // Determine DB path
85
+ // Determine DB path — pure computation, no filesystem access
84
86
  if (opts?.dbPath) {
85
87
  this.dbPath = opts.dbPath;
86
88
  } else {
@@ -90,28 +92,67 @@ export class SqliteBackend implements MemoryBackend {
90
92
  this.dbPath = path.join(projectDir, "db.sqlite");
91
93
  }
92
94
 
93
- // Open database defers mkdirSync until after dep check to avoid empty dirs
94
- this.db = this.openDatabase(opts?.depsDir);
95
+ // Validate that the native better-sqlite3 binary loads correctly.
96
+ // Open and immediately close an in-memory DB — no file is created on disk.
97
+ // If the native module is missing or has an ABI mismatch, the error
98
+ // propagates out of the constructor so tier.ts's try/catch can fall back
99
+ // to the daemon client or MarkdownBackend.
100
+ const Database = loadDepSync<{ new (path: string): DatabaseLike }>(
101
+ "better-sqlite3",
102
+ opts?.depsDir,
103
+ );
104
+ const DbConstructor =
105
+ (Database as unknown as { default: { new (path: string): DatabaseLike } })
106
+ .default ?? Database;
107
+ const probe = new DbConstructor(":memory:");
108
+ (probe as unknown as { close(): void }).close();
109
+ }
110
+
111
+ private getDefaultDepsDir(): string {
112
+ const home = process.env.HOME ?? process.env.USERPROFILE ?? "/tmp";
113
+ return path.join(home, ".coding-friend", "memory");
114
+ }
115
+
116
+ /**
117
+ * Open the DB, run migrations, and set up vec/embedding. Memoized.
118
+ * Always creates the DB file and directory. Call only from write operations.
119
+ */
120
+ private ensureDb(): DatabaseLike {
121
+ if (this.db) return this.db;
122
+
123
+ const depsDir = this.opts?.depsDir;
124
+
125
+ // Open database (creates dir + file)
126
+ const Database = loadDepSync<{ new (path: string): DatabaseLike }>(
127
+ "better-sqlite3",
128
+ depsDir,
129
+ );
130
+ // better-sqlite3 exports the class as default in CJS
131
+ const DbConstructor =
132
+ (Database as unknown as { default: { new (path: string): DatabaseLike } })
133
+ .default ?? Database;
134
+ fs.mkdirSync(path.dirname(this.dbPath), { recursive: true });
135
+ this.db = new DbConstructor(this.dbPath);
95
136
 
96
137
  // Run migrations
97
138
  migrate(this.db);
98
139
 
99
140
  // Store the resolved docsDir so `cf memory list --projects` can display the source path
100
- const resolvedDir = path.resolve(docsDir);
141
+ const resolvedDir = path.resolve(this.docsDir);
101
142
  if (getMetadata(this.db, "source_dir") !== resolvedDir) {
102
143
  setMetadata(this.db, "source_dir", resolvedDir);
103
144
  }
104
145
 
105
146
  // Try to set up vector search
106
- if (!opts?.skipVec) {
107
- this.vecEnabled = this.setupVec(opts?.depsDir);
147
+ if (!this.opts?.skipVec) {
148
+ this.vecEnabled = this.setupVec(this.db, depsDir);
108
149
  }
109
150
 
110
151
  // Set up embedding pipeline and cache
111
152
  if (this.vecEnabled) {
112
153
  this.pipeline = new EmbeddingPipeline({
113
- ...opts?.embedding,
114
- depsDir: opts?.depsDir,
154
+ ...this.opts?.embedding,
155
+ depsDir,
115
156
  });
116
157
  this.cache = new EmbeddingCache(this.db);
117
158
 
@@ -130,27 +171,21 @@ export class SqliteBackend implements MemoryBackend {
130
171
  this.needsEmbeddingRebuild = true;
131
172
  }
132
173
  }
133
- }
134
174
 
135
- private getDefaultDepsDir(): string {
136
- const home = process.env.HOME ?? process.env.USERPROFILE ?? "/tmp";
137
- return path.join(home, ".coding-friend", "memory");
175
+ return this.db;
138
176
  }
139
177
 
140
- private openDatabase(depsDir?: string): DatabaseLike {
141
- const Database = loadDepSync<{ new (path: string): DatabaseLike }>(
142
- "better-sqlite3",
143
- depsDir,
144
- );
145
- // better-sqlite3 exports the class as default in CJS
146
- const DbConstructor =
147
- (Database as unknown as { default: { new (path: string): DatabaseLike } })
148
- .default ?? Database;
149
- fs.mkdirSync(path.dirname(this.dbPath), { recursive: true });
150
- return new DbConstructor(this.dbPath);
178
+ /**
179
+ * Return the open DB if it already exists on disk, or null if it doesn't.
180
+ * For read operations: avoids creating an empty DB on projects with no memories.
181
+ */
182
+ private openIfExists(): DatabaseLike | null {
183
+ if (this.db) return this.db;
184
+ if (!fs.existsSync(this.dbPath)) return null;
185
+ return this.ensureDb();
151
186
  }
152
187
 
153
- private setupVec(depsDir?: string): boolean {
188
+ private setupVec(db: DatabaseLike, depsDir?: string): boolean {
154
189
  try {
155
190
  const sqliteVec = loadDepSync<{ load: (db: unknown) => void }>(
156
191
  "sqlite-vec",
@@ -159,8 +194,8 @@ export class SqliteBackend implements MemoryBackend {
159
194
  const loader =
160
195
  (sqliteVec as unknown as { default: { load: (db: unknown) => void } })
161
196
  .default ?? sqliteVec;
162
- loader.load(this.db);
163
- return createVecTable(this.db);
197
+ loader.load(db);
198
+ return createVecTable(db);
164
199
  } catch {
165
200
  return false;
166
201
  }
@@ -182,7 +217,8 @@ export class SqliteBackend implements MemoryBackend {
182
217
  // 1. Store to markdown (source of truth)
183
218
  const memory = await this.markdown.store(input);
184
219
 
185
- // 2. Index in SQLite
220
+ // 2. Index in SQLite (creates DB on first store)
221
+ const db = this.ensureDb();
186
222
  const hash = contentHash(
187
223
  prepareEmbeddingText({
188
224
  title: memory.frontmatter.title,
@@ -192,11 +228,11 @@ export class SqliteBackend implements MemoryBackend {
192
228
  }),
193
229
  );
194
230
 
195
- this.upsertRow(memory, hash);
231
+ this.upsertRow(db, memory, hash);
196
232
 
197
233
  // 3. Generate and store embedding (async, non-blocking for store)
198
234
  if (this.vecEnabled && this.pipeline) {
199
- this.embedAndStore(memory.id, memory, hash).catch((err) => {
235
+ this.embedAndStore(db, memory.id, memory, hash).catch((err) => {
200
236
  process.stderr.write(
201
237
  `[cf-memory] Embedding failed for ${memory.id}: ${err instanceof Error ? err.message : String(err)}\n`,
202
238
  );
@@ -214,10 +250,14 @@ export class SqliteBackend implements MemoryBackend {
214
250
  return metas.map((m) => ({ memory: m, score: 0, matchedOn: [] }));
215
251
  }
216
252
 
253
+ // No DB yet → no indexed results
254
+ const db = this.openIfExists();
255
+ if (!db) return [];
256
+
217
257
  const limit = input.limit ?? 10;
218
258
 
219
259
  const ranked = await hybridSearch({
220
- db: this.db,
260
+ db,
221
261
  query,
222
262
  limit,
223
263
  pipeline: this.pipeline,
@@ -228,7 +268,7 @@ export class SqliteBackend implements MemoryBackend {
228
268
  // Convert ranked results to SearchResult format
229
269
  const results: SearchResult[] = [];
230
270
  for (const r of ranked) {
231
- const row = this.getRow(r.id);
271
+ const row = this.getRow(db, r.id);
232
272
  if (!row) continue;
233
273
 
234
274
  // Apply tag filter
@@ -260,11 +300,15 @@ export class SqliteBackend implements MemoryBackend {
260
300
  }
261
301
 
262
302
  async retrieve(id: string): Promise<Memory | null> {
263
- // Read from markdown (source of truth)
303
+ // Read from markdown (source of truth) — no DB access needed
264
304
  return this.markdown.retrieve(id);
265
305
  }
266
306
 
267
307
  async list(input: ListInput): Promise<MemoryMeta[]> {
308
+ // No DB yet → no indexed results
309
+ const db = this.openIfExists();
310
+ if (!db) return [];
311
+
268
312
  let sql = "SELECT * FROM memories WHERE 1=1";
269
313
  const params: unknown[] = [];
270
314
 
@@ -284,7 +328,7 @@ export class SqliteBackend implements MemoryBackend {
284
328
  sql += " LIMIT ?";
285
329
  params.push(limit);
286
330
 
287
- const stmt = this.db.prepare(sql);
331
+ const stmt = db.prepare(sql);
288
332
  const rows = (
289
333
  stmt as unknown as {
290
334
  all(...p: unknown[]): Array<Record<string, unknown>>;
@@ -305,7 +349,8 @@ export class SqliteBackend implements MemoryBackend {
305
349
  const memory = await this.markdown.update(input);
306
350
  if (!memory) return null;
307
351
 
308
- // 2. Re-index in SQLite
352
+ // 2. Re-index in SQLite (creates DB if not yet open)
353
+ const db = this.ensureDb();
309
354
  const hash = contentHash(
310
355
  prepareEmbeddingText({
311
356
  title: memory.frontmatter.title,
@@ -315,11 +360,11 @@ export class SqliteBackend implements MemoryBackend {
315
360
  }),
316
361
  );
317
362
 
318
- this.upsertRow(memory, hash);
363
+ this.upsertRow(db, memory, hash);
319
364
 
320
365
  // 3. Re-embed
321
366
  if (this.vecEnabled && this.pipeline) {
322
- this.embedAndStore(memory.id, memory, hash).catch((err) => {
367
+ this.embedAndStore(db, memory.id, memory, hash).catch((err) => {
323
368
  process.stderr.write(
324
369
  `[cf-memory] Embedding failed for ${memory.id}: ${err instanceof Error ? err.message : String(err)}\n`,
325
370
  );
@@ -334,15 +379,18 @@ export class SqliteBackend implements MemoryBackend {
334
379
  const deleted = await this.markdown.delete(id);
335
380
  if (!deleted) return false;
336
381
 
337
- // 2. Remove from SQLite
338
- this.db.prepare("DELETE FROM memories WHERE id = ?").run(id);
382
+ // 2. Remove from SQLite index if the DB exists
383
+ const db = this.openIfExists();
384
+ if (db) {
385
+ db.prepare("DELETE FROM memories WHERE id = ?").run(id);
339
386
 
340
- // 3. Remove from vec table
341
- if (this.vecEnabled) {
342
- try {
343
- this.db.prepare("DELETE FROM vec_memories WHERE memory_id = ?").run(id);
344
- } catch {
345
- // Ignore vec errors
387
+ // 3. Remove from vec table
388
+ if (this.vecEnabled) {
389
+ try {
390
+ db.prepare("DELETE FROM vec_memories WHERE memory_id = ?").run(id);
391
+ } catch {
392
+ // Ignore vec errors
393
+ }
346
394
  }
347
395
  }
348
396
 
@@ -350,8 +398,12 @@ export class SqliteBackend implements MemoryBackend {
350
398
  }
351
399
 
352
400
  async stats(): Promise<MemoryStats> {
401
+ // No DB yet → zeroed stats
402
+ const db = this.openIfExists();
403
+ if (!db) return { total: 0, byCategory: {}, byType: {} };
404
+
353
405
  const rows = (
354
- this.db.prepare(
406
+ db.prepare(
355
407
  "SELECT category, type, COUNT(*) as count FROM memories GROUP BY category, type",
356
408
  ) as unknown as {
357
409
  all(): Array<{ category: string; type: string; count: number }>;
@@ -379,19 +431,26 @@ export class SqliteBackend implements MemoryBackend {
379
431
  * 2. Async pass: generate embeddings (non-transactional, failures logged)
380
432
  */
381
433
  async rebuild(): Promise<void> {
434
+ // Read markdown metas FIRST — before touching the DB.
435
+ // If there is nothing to index (and no pending embedding rebuild),
436
+ // return early without creating the DB file or directory.
437
+ const metas = this.markdown.getAllMeta();
438
+ if (metas.length === 0 && !this.needsEmbeddingRebuild) return;
439
+
440
+ // Ensure DB is open (creates it if needed)
441
+ const db = this.ensureDb();
442
+
382
443
  // If embedding model changed, recreate vec table with new dimensions
383
444
  if (this.needsEmbeddingRebuild && this.pipeline) {
384
445
  try {
385
- this.db.prepare("DROP TABLE IF EXISTS vec_memories").run();
386
- createVecTable(this.db, this.pipeline.dims);
446
+ db.prepare("DROP TABLE IF EXISTS vec_memories").run();
447
+ createVecTable(db, this.pipeline.dims);
387
448
  this.vecEnabled = true;
388
449
  } catch {
389
450
  // Vec setup failed
390
451
  }
391
452
  }
392
453
 
393
- // Read all markdown files first
394
- const metas = this.markdown.getAllMeta();
395
454
  const memories: Array<{ memory: Memory; hash: string }> = [];
396
455
 
397
456
  for (const meta of metas) {
@@ -410,25 +469,25 @@ export class SqliteBackend implements MemoryBackend {
410
469
  }
411
470
 
412
471
  // Pass 1: Atomic transaction for all SQLite rows
413
- this.db.prepare("BEGIN").run();
472
+ db.prepare("BEGIN").run();
414
473
  try {
415
- this.db.prepare("DELETE FROM memories").run();
474
+ db.prepare("DELETE FROM memories").run();
416
475
  if (this.vecEnabled) {
417
476
  try {
418
- this.db.prepare("DELETE FROM vec_memories").run();
477
+ db.prepare("DELETE FROM vec_memories").run();
419
478
  } catch {
420
479
  // Ignore if vec table doesn't exist
421
480
  }
422
481
  }
423
- this.db.prepare("DELETE FROM embedding_cache").run();
482
+ db.prepare("DELETE FROM embedding_cache").run();
424
483
 
425
484
  for (const { memory, hash } of memories) {
426
- this.upsertRow(memory, hash);
485
+ this.upsertRow(db, memory, hash);
427
486
  }
428
487
 
429
- this.db.prepare("COMMIT").run();
488
+ db.prepare("COMMIT").run();
430
489
  } catch (err) {
431
- this.db.prepare("ROLLBACK").run();
490
+ db.prepare("ROLLBACK").run();
432
491
  throw err;
433
492
  }
434
493
 
@@ -436,7 +495,7 @@ export class SqliteBackend implements MemoryBackend {
436
495
  if (this.vecEnabled && this.pipeline) {
437
496
  for (const { memory, hash } of memories) {
438
497
  try {
439
- await this.embedAndStore(memory.id, memory, hash);
498
+ await this.embedAndStore(db, memory.id, memory, hash);
440
499
  } catch (err) {
441
500
  process.stderr.write(
442
501
  `[cf-memory] Embedding failed for ${memory.id}: ${err instanceof Error ? err.message : String(err)}\n`,
@@ -447,13 +506,14 @@ export class SqliteBackend implements MemoryBackend {
447
506
 
448
507
  // Update metadata with current embedding model info
449
508
  if (this.pipeline) {
450
- setMetadata(this.db, "embedding_model", this.pipeline.modelName);
451
- setMetadata(this.db, "embedding_dims", String(this.pipeline.dims));
509
+ setMetadata(db, "embedding_model", this.pipeline.modelName);
510
+ setMetadata(db, "embedding_dims", String(this.pipeline.dims));
452
511
  this.needsEmbeddingRebuild = false;
453
512
  }
454
513
  }
455
514
 
456
515
  async close(): Promise<void> {
516
+ if (!this.db) return;
457
517
  try {
458
518
  (this.db as unknown as { close(): void }).close();
459
519
  } catch {
@@ -463,36 +523,34 @@ export class SqliteBackend implements MemoryBackend {
463
523
 
464
524
  // --- Private helpers ---
465
525
 
466
- private upsertRow(memory: Memory, hash: string): void {
526
+ private upsertRow(db: DatabaseLike, memory: Memory, hash: string): void {
467
527
  const sql = `
468
528
  INSERT OR REPLACE INTO memories
469
529
  (id, slug, category, title, description, type, tags, importance, created, updated, source, content, content_hash)
470
530
  VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
471
531
  `;
472
532
 
473
- this.db
474
- .prepare(sql)
475
- .run(
476
- memory.id,
477
- memory.slug,
478
- memory.category,
479
- memory.frontmatter.title,
480
- memory.frontmatter.description,
481
- memory.frontmatter.type,
482
- JSON.stringify(memory.frontmatter.tags),
483
- memory.frontmatter.importance,
484
- memory.frontmatter.created,
485
- memory.frontmatter.updated,
486
- memory.frontmatter.source,
487
- memory.content,
488
- hash,
489
- );
533
+ db.prepare(sql).run(
534
+ memory.id,
535
+ memory.slug,
536
+ memory.category,
537
+ memory.frontmatter.title,
538
+ memory.frontmatter.description,
539
+ memory.frontmatter.type,
540
+ JSON.stringify(memory.frontmatter.tags),
541
+ memory.frontmatter.importance,
542
+ memory.frontmatter.created,
543
+ memory.frontmatter.updated,
544
+ memory.frontmatter.source,
545
+ memory.content,
546
+ hash,
547
+ );
490
548
  }
491
549
 
492
- private getRow(id: string): Record<string, unknown> | null {
550
+ private getRow(db: DatabaseLike, id: string): Record<string, unknown> | null {
493
551
  return (
494
552
  (
495
- this.db.prepare("SELECT * FROM memories WHERE id = ?") as unknown as {
553
+ db.prepare("SELECT * FROM memories WHERE id = ?") as unknown as {
496
554
  get(id: string): Record<string, unknown> | undefined;
497
555
  }
498
556
  ).get(id) ?? null
@@ -513,6 +571,7 @@ export class SqliteBackend implements MemoryBackend {
513
571
  }
514
572
 
515
573
  private async embedAndStore(
574
+ db: DatabaseLike,
516
575
  id: string,
517
576
  memory: Memory,
518
577
  hash: string,
@@ -539,11 +598,9 @@ export class SqliteBackend implements MemoryBackend {
539
598
  embedding.byteLength,
540
599
  );
541
600
  try {
542
- this.db
543
- .prepare(
544
- "INSERT OR REPLACE INTO vec_memories (memory_id, embedding) VALUES (?, ?)",
545
- )
546
- .run(id, buffer);
601
+ db.prepare(
602
+ "INSERT OR REPLACE INTO vec_memories (memory_id, embedding) VALUES (?, ?)",
603
+ ).run(id, buffer);
547
604
  } catch {
548
605
  // sqlite-vec error — ignore
549
606
  }
@@ -1,8 +1,9 @@
1
1
  /**
2
- * Sync convention memories to the project's CLAUDE.md file.
2
+ * Sync convention memories to the project's host instruction files.
3
3
  *
4
4
  * When a memory of type "preference" (category "conventions") is stored,
5
- * updated, or deleted, the corresponding entry in CLAUDE.md is kept in sync.
5
+ * updated, or deleted, the corresponding entry in CLAUDE.md and/or AGENTS.md
6
+ * is kept in sync.
6
7
  *
7
8
  * Entries are tracked via HTML comments: <!-- cf:<memory-id> -->
8
9
  * All entries live under a dedicated section header.
@@ -46,6 +47,20 @@ export function claudeMdPath(docsDir: string): string {
46
47
  return path.join(projectRoot(docsDir), "CLAUDE.md");
47
48
  }
48
49
 
50
+ export function agentsMdPath(docsDir: string): string {
51
+ return path.join(projectRoot(docsDir), "AGENTS.md");
52
+ }
53
+
54
+ function instructionFilePaths(
55
+ docsDir: string,
56
+ createClaudeFallback: boolean,
57
+ ): string[] {
58
+ const candidates = [claudeMdPath(docsDir), agentsMdPath(docsDir)];
59
+ const existing = candidates.filter((filePath) => fs.existsSync(filePath));
60
+ if (existing.length > 0 || !createClaudeFallback) return existing;
61
+ return [candidates[0]];
62
+ }
63
+
49
64
  /**
50
65
  * Parse a CLAUDE.md file into: before the section, section entries, and after the section.
51
66
  */
@@ -143,84 +158,81 @@ export function syncToClaudeMd(
143
158
  docsDir: string,
144
159
  id: string,
145
160
  description: string,
146
- ): void {
147
- const filePath = claudeMdPath(docsDir);
148
- const existing = fs.existsSync(filePath)
149
- ? fs.readFileSync(filePath, "utf-8")
150
- : "";
151
-
152
- const { before, entries, after, untrackedLines } = parseClaudeMd(existing);
153
-
154
- // Remove any existing entry with the same ID
155
- const filtered = entries.filter((e) => {
156
- const match = e.match(ENTRY_RE);
157
- return !match || match[1] !== id;
158
- });
159
-
160
- // Add the new/updated entry
161
- filtered.push(formatEntry(id, description));
162
-
163
- fs.writeFileSync(
164
- filePath,
165
- buildClaudeMd(before, filtered, after, untrackedLines),
166
- "utf-8",
167
- );
161
+ ): string[] {
162
+ const updated: string[] = [];
163
+ for (const filePath of instructionFilePaths(docsDir, true)) {
164
+ const existing = fs.existsSync(filePath)
165
+ ? fs.readFileSync(filePath, "utf-8")
166
+ : "";
167
+
168
+ const { before, entries, after, untrackedLines } = parseClaudeMd(existing);
169
+ const filtered = entries.filter((entry) => {
170
+ const match = entry.match(ENTRY_RE);
171
+ return !match || match[1] !== id;
172
+ });
173
+ filtered.push(formatEntry(id, description));
174
+
175
+ fs.writeFileSync(
176
+ filePath,
177
+ buildClaudeMd(before, filtered, after, untrackedLines),
178
+ "utf-8",
179
+ );
180
+ updated.push(filePath);
181
+ }
182
+ return updated;
168
183
  }
169
184
 
170
185
  /**
171
- * Remove a convention entry from the project's CLAUDE.md.
186
+ * Remove a convention entry from the project's host instruction files.
172
187
  */
173
- export function removeFromClaudeMd(docsDir: string, id: string): void {
174
- const filePath = claudeMdPath(docsDir);
175
- if (!fs.existsSync(filePath)) return;
176
-
177
- const content = fs.readFileSync(filePath, "utf-8");
178
- const { before, entries, after, untrackedLines } = parseClaudeMd(content);
179
-
180
- if (entries.length === 0) return;
181
-
182
- const filtered = entries.filter((e) => {
183
- const match = e.match(ENTRY_RE);
184
- return !match || match[1] !== id;
185
- });
186
-
187
- // Only write if something actually changed
188
- if (filtered.length === entries.length) return;
189
-
190
- fs.writeFileSync(
191
- filePath,
192
- buildClaudeMd(before, filtered, after, untrackedLines),
193
- "utf-8",
194
- );
188
+ export function removeFromClaudeMd(docsDir: string, id: string): string[] {
189
+ const updated: string[] = [];
190
+ for (const filePath of instructionFilePaths(docsDir, false)) {
191
+ const content = fs.readFileSync(filePath, "utf-8");
192
+ const { before, entries, after, untrackedLines } = parseClaudeMd(content);
193
+ if (entries.length === 0) continue;
194
+
195
+ const filtered = entries.filter((entry) => {
196
+ const match = entry.match(ENTRY_RE);
197
+ return !match || match[1] !== id;
198
+ });
199
+ if (filtered.length === entries.length) continue;
200
+
201
+ fs.writeFileSync(
202
+ filePath,
203
+ buildClaudeMd(before, filtered, after, untrackedLines),
204
+ "utf-8",
205
+ );
206
+ updated.push(filePath);
207
+ }
208
+ return updated;
195
209
  }
196
210
 
197
211
  /**
198
- * Update an existing convention entry in CLAUDE.md (no upsert).
212
+ * Update an existing convention entry in the host instruction files (no upsert).
199
213
  */
200
214
  export function updateInClaudeMd(
201
215
  docsDir: string,
202
216
  id: string,
203
217
  description: string,
204
- ): void {
205
- const filePath = claudeMdPath(docsDir);
206
- if (!fs.existsSync(filePath)) return;
207
-
208
- const content = fs.readFileSync(filePath, "utf-8");
209
- const { before, entries, after, untrackedLines } = parseClaudeMd(content);
210
-
211
- // Check if entry exists
212
- const idx = entries.findIndex((e) => {
213
- const match = e.match(ENTRY_RE);
214
- return match && match[1] === id;
215
- });
216
-
217
- if (idx === -1) return;
218
-
219
- entries[idx] = formatEntry(id, description);
220
-
221
- fs.writeFileSync(
222
- filePath,
223
- buildClaudeMd(before, entries, after, untrackedLines),
224
- "utf-8",
225
- );
218
+ ): string[] {
219
+ const updated: string[] = [];
220
+ for (const filePath of instructionFilePaths(docsDir, false)) {
221
+ const content = fs.readFileSync(filePath, "utf-8");
222
+ const { before, entries, after, untrackedLines } = parseClaudeMd(content);
223
+ const index = entries.findIndex((entry) => {
224
+ const match = entry.match(ENTRY_RE);
225
+ return match && match[1] === id;
226
+ });
227
+ if (index === -1) continue;
228
+
229
+ entries[index] = formatEntry(id, description);
230
+ fs.writeFileSync(
231
+ filePath,
232
+ buildClaudeMd(before, entries, after, untrackedLines),
233
+ "utf-8",
234
+ );
235
+ updated.push(filePath);
236
+ }
237
+ return updated;
226
238
  }
@@ -33,7 +33,7 @@ export function buildStoreStatus(input: StoreStatusInput): string {
33
33
  }
34
34
 
35
35
  if (input.claudeMdUpdated) {
36
- lines.push(`📋 CLAUDE.md updated`);
36
+ lines.push(`📋 Project instructions updated`);
37
37
  }
38
38
 
39
39
  if (input.warning) {
@@ -56,7 +56,7 @@ export function buildUpdateStatus(input: UpdateStatusInput): string {
56
56
  }
57
57
 
58
58
  if (input.claudeMdUpdated) {
59
- lines.push(`📋 CLAUDE.md updated`);
59
+ lines.push(`📋 Project instructions updated`);
60
60
  }
61
61
 
62
62
  return lines.join("\n");
@@ -28,7 +28,7 @@ export function registerDelete(
28
28
  isError: true,
29
29
  };
30
30
  }
31
- // Remove from CLAUDE.md (any category may have been synced via sync_to_claude_md)
31
+ // Remove from host instruction files if this memory was synced.
32
32
  if (ctx?.docsDir) {
33
33
  try {
34
34
  removeFromClaudeMd(ctx.docsDir, id);