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.
- package/README.md +8 -3
- package/dist/{chunk-GZT3YPAP.js → chunk-32UAW3NX.js} +1 -1
- package/dist/chunk-5QCGEPQO.js +238 -0
- package/dist/{chunk-YIXAQ4R2.js → chunk-6NXZKAOV.js} +1 -1
- package/dist/{chunk-XHP3EBR3.js → chunk-776ZY62N.js} +112 -4
- package/dist/{chunk-N4WZWRCI.js → chunk-BXSCYGWE.js} +85 -37
- package/dist/{chunk-AK6RMCAC.js → chunk-CGEMETKQ.js} +44 -30
- package/dist/{chunk-XEKQW4RN.js → chunk-ETTZRJ55.js} +1 -1
- package/dist/{chunk-IKLMHJH6.js → chunk-GIH6E7GJ.js} +62 -1
- package/dist/{chunk-56YJWGAN.js → chunk-ONG4KRYP.js} +60 -47
- package/dist/{chunk-X2H7JJMR.js → chunk-PLSJW7QJ.js} +29 -1
- package/dist/{chunk-O5ZKLFSL.js → chunk-RCE4OWNX.js} +1 -1
- package/dist/{chunk-K4WSC62G.js → chunk-RFNKPAJW.js} +42 -2
- package/dist/{clean-Q4VU662V.js → clean-YWEMYDE5.js} +3 -2
- package/dist/{config-CTTHWJH5.js → config-NG7SICM4.js} +15 -27
- package/dist/{dev-IGHMMEP4.js → dev-PTBNMKZT.js} +3 -3
- package/dist/{disable-HL5OEURT.js → disable-MO7AXYYF.js} +20 -3
- package/dist/{enable-CZIOCKKJ.js → enable-4GXLQEHD.js} +20 -3
- package/dist/{guide-CO2V4ZLZ.js → guide-HCR43LJN.js} +1 -1
- package/dist/{host-NCJMUSME.js → host-EV6QIVTI.js} +3 -3
- package/dist/index.js +62 -48
- package/dist/{init-UABKVDVT.js → init-KYE5CJMW.js} +135 -17
- package/dist/{install-R2HDM3N4.js → install-XAFQY5YF.js} +80 -5
- package/dist/{learn-K64AUUFB.js → learn-MC4VDHZH.js} +2 -2
- package/dist/{mcp-OMQFSJP4.js → mcp-FV6YX3ZT.js} +24 -24
- package/dist/{mcp-serve-3FEPFVVQ.js → mcp-serve-PXREYQRZ.js} +10 -1
- package/dist/{mcp-serve-learn-OF3ZJUJC.js → mcp-serve-learn-UVXIDOEZ.js} +1 -1
- package/dist/{memory-Y6GNALG3.js → memory-DF7OUOTV.js} +7 -7
- package/dist/{permission-I3OFJUYA.js → permission-VISPGMUK.js} +38 -4
- package/dist/{permissions-J46VP6US.js → permissions-2SBACQHV.js} +2 -2
- package/dist/{session-Q2DLQPYP.js → session-QK6OZO3X.js} +3 -2
- package/dist/{status-ML4XEDYL.js → status-FVFTJ6GS.js} +18 -14
- package/dist/{statusline-KKFJPTI7.js → statusline-AOZHEQSV.js} +2 -2
- package/dist/{uninstall-ASLHT3MH.js → uninstall-MF6FRUBO.js} +64 -3
- package/dist/{update-DIOH45TE.js → update-QNUCSDSA.js} +7 -4
- package/dist/{update-check-25SCLHZO.js → update-check-7LKJLJPM.js} +1 -1
- package/lib/cf-memory/src/backends/sqlite/index.ts +142 -85
- package/lib/cf-memory/src/lib/claude-md.ts +82 -70
- package/lib/cf-memory/src/lib/status-frame.ts +2 -2
- package/lib/cf-memory/src/tools/delete.ts +1 -1
- package/lib/cf-memory/src/tools/store.ts +6 -6
- package/lib/cf-memory/src/tools/update.ts +5 -11
- package/package.json +32 -4
- package/lib/cf-memory/README.md +0 -317
- package/lib/cf-memory/src/__tests__/claude-md.test.ts +0 -343
- package/lib/cf-memory/src/__tests__/daemon-client.test.ts +0 -146
- package/lib/cf-memory/src/__tests__/daemon-e2e.test.ts +0 -223
- package/lib/cf-memory/src/__tests__/daemon.test.ts +0 -407
- package/lib/cf-memory/src/__tests__/dedup.test.ts +0 -103
- package/lib/cf-memory/src/__tests__/embeddings.test.ts +0 -292
- package/lib/cf-memory/src/__tests__/lazy-install.test.ts +0 -210
- package/lib/cf-memory/src/__tests__/markdown-backend.test.ts +0 -450
- package/lib/cf-memory/src/__tests__/migration.test.ts +0 -255
- package/lib/cf-memory/src/__tests__/migrations.test.ts +0 -288
- package/lib/cf-memory/src/__tests__/minisearch-backend.test.ts +0 -262
- package/lib/cf-memory/src/__tests__/ollama.test.ts +0 -48
- package/lib/cf-memory/src/__tests__/project-id.test.ts +0 -43
- package/lib/cf-memory/src/__tests__/schema.test.ts +0 -128
- package/lib/cf-memory/src/__tests__/search.test.ts +0 -115
- package/lib/cf-memory/src/__tests__/status-frame.test.ts +0 -136
- package/lib/cf-memory/src/__tests__/temporal-decay.test.ts +0 -54
- package/lib/cf-memory/src/__tests__/tier.test.ts +0 -367
- package/lib/cf-memory/src/__tests__/tools.test.ts +0 -83
- package/lib/cf-memory/vitest.config.ts +0 -7
- package/lib/learn-host/.prettierignore +0 -4
- package/lib/learn-host/.prettierrc +0 -8
- package/lib/learn-host/README.md +0 -113
- package/lib/learn-host/eslint.config.mjs +0 -6
- package/lib/learn-mcp/README.md +0 -187
- package/lib/learn-mcp/src/__tests__/docs.test.ts +0 -117
- 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
|
-
//
|
|
94
|
-
|
|
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(
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
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(
|
|
163
|
-
return createVecTable(
|
|
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
|
|
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 =
|
|
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
|
-
|
|
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
|
-
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
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
|
-
|
|
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
|
-
|
|
386
|
-
createVecTable(
|
|
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
|
-
|
|
472
|
+
db.prepare("BEGIN").run();
|
|
414
473
|
try {
|
|
415
|
-
|
|
474
|
+
db.prepare("DELETE FROM memories").run();
|
|
416
475
|
if (this.vecEnabled) {
|
|
417
476
|
try {
|
|
418
|
-
|
|
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
|
-
|
|
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
|
-
|
|
488
|
+
db.prepare("COMMIT").run();
|
|
430
489
|
} catch (err) {
|
|
431
|
-
|
|
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(
|
|
451
|
-
setMetadata(
|
|
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
|
-
|
|
474
|
-
.
|
|
475
|
-
.
|
|
476
|
-
|
|
477
|
-
|
|
478
|
-
|
|
479
|
-
|
|
480
|
-
|
|
481
|
-
|
|
482
|
-
|
|
483
|
-
|
|
484
|
-
|
|
485
|
-
|
|
486
|
-
|
|
487
|
-
|
|
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
|
-
|
|
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
|
-
|
|
543
|
-
|
|
544
|
-
|
|
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
|
|
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
|
|
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
|
-
):
|
|
147
|
-
const
|
|
148
|
-
const
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
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
|
|
186
|
+
* Remove a convention entry from the project's host instruction files.
|
|
172
187
|
*/
|
|
173
|
-
export function removeFromClaudeMd(docsDir: string, id: string):
|
|
174
|
-
const
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
filePath
|
|
192
|
-
|
|
193
|
-
|
|
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
|
|
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
|
-
):
|
|
205
|
-
const
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
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(`📋
|
|
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(`📋
|
|
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
|
|
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);
|