coding-friend-cli 1.16.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-D4EWPGBL.js → chunk-C5LYVVEI.js} +1 -1
  3. package/dist/{chunk-X5WEODUD.js → chunk-CYQU33FY.js} +1 -0
  4. package/dist/{chunk-QNLL3ZDF.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-AIZJJ5D2.js → config-LZFXXOI4.js} +276 -14
  8. package/dist/{dev-WJ5QQ35B.js → dev-R3IYWZ3M.js} +2 -2
  9. package/dist/{disable-JDVOQNZG.js → disable-R6K5YJN4.js} +2 -2
  10. package/dist/{enable-JBJ4Q2S7.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-FZ3GG53E.js → init-YK6YRTOT.js} +102 -6
  14. package/dist/{install-I3GOS56Q.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-JN5YIKKM.js → uninstall-3PSUDGI4.js} +3 -3
  21. package/dist/{update-OWS4IJTG.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 -0
  74. package/lib/learn-host/package.json +1 -1
  75. package/package.json +1 -1
@@ -0,0 +1,407 @@
1
+ import { describe, it, expect, beforeEach, afterEach } from "vitest";
2
+ import { mkdirSync, rmSync, existsSync, writeFileSync } from "fs";
3
+ import { join } from "path";
4
+ import { tmpdir } from "os";
5
+ import http from "node:http";
6
+ import {
7
+ getDaemonPaths,
8
+ isDaemonRunning,
9
+ getDaemonInfo,
10
+ startDaemonServer,
11
+ type DaemonPaths,
12
+ } from "../daemon/process.js";
13
+ import { MiniSearchBackend } from "../backends/minisearch.js";
14
+ import type { StoreInput } from "../lib/types.js";
15
+
16
+ let testDir: string;
17
+ let testPaths: DaemonPaths;
18
+ let counter = 0;
19
+
20
+ function requestJson<T>(
21
+ socketPath: string,
22
+ method: string,
23
+ path: string,
24
+ body?: unknown,
25
+ ): Promise<{ status: number; data: T }> {
26
+ return new Promise((resolve, reject) => {
27
+ const options: http.RequestOptions = {
28
+ socketPath,
29
+ path,
30
+ method,
31
+ headers: { "Content-Type": "application/json" },
32
+ };
33
+ const req = http.request(options, (res) => {
34
+ let data = "";
35
+ res.on("data", (chunk: Buffer) => {
36
+ data += chunk.toString();
37
+ });
38
+ res.on("end", () => {
39
+ try {
40
+ resolve({ status: res.statusCode ?? 0, data: JSON.parse(data) as T });
41
+ } catch {
42
+ reject(new Error(`Invalid JSON: ${data}`));
43
+ }
44
+ });
45
+ });
46
+ req.on("error", reject);
47
+ if (body) req.write(JSON.stringify(body));
48
+ req.end();
49
+ });
50
+ }
51
+
52
+ beforeEach(() => {
53
+ testDir = join(tmpdir(), `cf-memory-daemon-test-${Date.now()}-${++counter}`);
54
+ mkdirSync(testDir, { recursive: true });
55
+
56
+ const daemonDir = join(testDir, "daemon");
57
+ mkdirSync(daemonDir, { recursive: true });
58
+
59
+ testPaths = {
60
+ socketPath: join(daemonDir, "daemon.sock"),
61
+ pidFile: join(daemonDir, "daemon.pid"),
62
+ logFile: join(daemonDir, "daemon.log"),
63
+ };
64
+ });
65
+
66
+ afterEach(() => {
67
+ rmSync(testDir, { recursive: true, force: true });
68
+ });
69
+
70
+ describe("Daemon process helpers", () => {
71
+ it("isDaemonRunning returns false when no PID file", async () => {
72
+ const running = await isDaemonRunning(testPaths);
73
+ expect(running).toBe(false);
74
+ });
75
+
76
+ it("isDaemonRunning returns false for stale PID file", async () => {
77
+ writeFileSync(testPaths.pidFile, "99999999\n1234567890", "utf-8");
78
+ const running = await isDaemonRunning(testPaths);
79
+ expect(running).toBe(false);
80
+ // Should clean up stale PID file
81
+ expect(existsSync(testPaths.pidFile)).toBe(false);
82
+ });
83
+
84
+ it("getDaemonInfo returns null when no PID file", () => {
85
+ const info = getDaemonInfo(testPaths);
86
+ expect(info).toBeNull();
87
+ });
88
+
89
+ it("getDaemonInfo reads PID and startedAt", () => {
90
+ const now = Date.now();
91
+ writeFileSync(testPaths.pidFile, `12345\n${now}`, "utf-8");
92
+ const info = getDaemonInfo(testPaths);
93
+ expect(info).not.toBeNull();
94
+ expect(info!.pid).toBe(12345);
95
+ expect(info!.startedAt).toBe(now);
96
+ });
97
+
98
+ it("getDaemonPaths returns expected structure", () => {
99
+ const paths = getDaemonPaths();
100
+ expect(paths.socketPath).toContain("daemon.sock");
101
+ expect(paths.pidFile).toContain("daemon.pid");
102
+ expect(paths.logFile).toContain("daemon.log");
103
+ });
104
+ });
105
+
106
+ describe("Daemon server", () => {
107
+ let docsDir: string;
108
+ let handle: ReturnType<typeof startDaemonServer> | null = null;
109
+
110
+ beforeEach(() => {
111
+ docsDir = join(testDir, "docs");
112
+ mkdirSync(docsDir, { recursive: true });
113
+ });
114
+
115
+ afterEach(async () => {
116
+ if (handle) {
117
+ // Clean up server without exiting process
118
+ await new Promise<void>((resolve) => {
119
+ handle!.server.close(() => resolve());
120
+ });
121
+ try {
122
+ rmSync(testPaths.socketPath, { force: true });
123
+ rmSync(testPaths.pidFile, { force: true });
124
+ } catch {
125
+ // ignore
126
+ }
127
+ handle = null;
128
+ }
129
+ });
130
+
131
+ function startTestDaemon() {
132
+ const backend = new MiniSearchBackend(docsDir);
133
+ handle = startDaemonServer(backend, {
134
+ paths: testPaths,
135
+ idleTimeoutMs: 0, // Disable idle timeout for tests
136
+ });
137
+ return new Promise<void>((resolve) => {
138
+ // Wait for server to be listening
139
+ handle!.server.once("listening", resolve);
140
+ });
141
+ }
142
+
143
+ it("health endpoint responds with status ok", async () => {
144
+ await startTestDaemon();
145
+ const { status, data } = await requestJson<{ status: string }>(
146
+ testPaths.socketPath,
147
+ "GET",
148
+ "/health",
149
+ );
150
+ expect(status).toBe(200);
151
+ expect(data.status).toBe("ok");
152
+ });
153
+
154
+ it("stats endpoint returns memory stats", async () => {
155
+ await startTestDaemon();
156
+ const { status, data } = await requestJson<{ total: number }>(
157
+ testPaths.socketPath,
158
+ "GET",
159
+ "/stats",
160
+ );
161
+ expect(status).toBe(200);
162
+ expect(data.total).toBe(0);
163
+ });
164
+
165
+ it("store and retrieve a memory via HTTP", async () => {
166
+ await startTestDaemon();
167
+
168
+ const input: StoreInput = {
169
+ title: "Test Memory",
170
+ description: "A test memory",
171
+ type: "fact",
172
+ tags: ["test"],
173
+ content: "Test content.",
174
+ };
175
+
176
+ // Store
177
+ const storeResult = await requestJson<{
178
+ id: string;
179
+ stored: boolean;
180
+ }>(testPaths.socketPath, "POST", "/memory", input);
181
+ expect(storeResult.status).toBe(201);
182
+ expect(storeResult.data.stored).toBe(true);
183
+ expect(storeResult.data.id).toBe("features/test-memory");
184
+
185
+ // Retrieve
186
+ const { status, data } = await requestJson<{
187
+ id: string;
188
+ frontmatter: { title: string };
189
+ }>(testPaths.socketPath, "GET", "/memory/features/test-memory");
190
+ expect(status).toBe(200);
191
+ expect(data.frontmatter.title).toBe("Test Memory");
192
+ });
193
+
194
+ it("search returns results", async () => {
195
+ await startTestDaemon();
196
+
197
+ await requestJson(testPaths.socketPath, "POST", "/memory", {
198
+ title: "JWT Authentication",
199
+ description: "Auth pattern",
200
+ type: "fact",
201
+ tags: ["auth"],
202
+ content: "JWT tokens.",
203
+ });
204
+
205
+ const { status, data } = await requestJson<
206
+ Array<{ memory: { frontmatter: { title: string } } }>
207
+ >(testPaths.socketPath, "GET", "/memory/search?query=JWT");
208
+ expect(status).toBe(200);
209
+ expect(data.length).toBeGreaterThan(0);
210
+ expect(data[0].memory.frontmatter.title).toContain("JWT");
211
+ });
212
+
213
+ it("list returns all memories", async () => {
214
+ await startTestDaemon();
215
+
216
+ await requestJson(testPaths.socketPath, "POST", "/memory", {
217
+ title: "Memory One",
218
+ description: "First",
219
+ type: "fact",
220
+ tags: [],
221
+ content: "One.",
222
+ });
223
+ await requestJson(testPaths.socketPath, "POST", "/memory", {
224
+ title: "Memory Two",
225
+ description: "Second",
226
+ type: "episode",
227
+ tags: [],
228
+ content: "Two.",
229
+ });
230
+
231
+ const { status, data } = await requestJson<Array<{ id: string }>>(
232
+ testPaths.socketPath,
233
+ "GET",
234
+ "/memory",
235
+ );
236
+ expect(status).toBe(200);
237
+ expect(data.length).toBe(2);
238
+ });
239
+
240
+ it("update modifies a memory", async () => {
241
+ await startTestDaemon();
242
+
243
+ await requestJson(testPaths.socketPath, "POST", "/memory", {
244
+ title: "Original Title",
245
+ description: "Description",
246
+ type: "fact",
247
+ tags: [],
248
+ content: "Content.",
249
+ });
250
+
251
+ const { status, data } = await requestJson<{
252
+ updated: boolean;
253
+ title: string;
254
+ }>(testPaths.socketPath, "PATCH", "/memory/features/original-title", {
255
+ title: "New Title",
256
+ });
257
+ expect(status).toBe(200);
258
+ expect(data.updated).toBe(true);
259
+ });
260
+
261
+ it("delete removes a memory", async () => {
262
+ await startTestDaemon();
263
+
264
+ await requestJson(testPaths.socketPath, "POST", "/memory", {
265
+ title: "Delete Me",
266
+ description: "To be deleted",
267
+ type: "fact",
268
+ tags: [],
269
+ content: "Content.",
270
+ });
271
+
272
+ const { status, data } = await requestJson<{ deleted: boolean }>(
273
+ testPaths.socketPath,
274
+ "DELETE",
275
+ "/memory/features/delete-me",
276
+ );
277
+ expect(status).toBe(200);
278
+ expect(data.deleted).toBe(true);
279
+
280
+ // Verify gone
281
+ const { status: getStatus } = await requestJson(
282
+ testPaths.socketPath,
283
+ "GET",
284
+ "/memory/features/delete-me",
285
+ );
286
+ expect(getStatus).toBe(404);
287
+ });
288
+
289
+ it("rebuild endpoint works", async () => {
290
+ await startTestDaemon();
291
+
292
+ const { status, data } = await requestJson<{ rebuilt: boolean }>(
293
+ testPaths.socketPath,
294
+ "POST",
295
+ "/rebuild",
296
+ );
297
+ expect(status).toBe(200);
298
+ expect(data.rebuilt).toBe(true);
299
+ });
300
+
301
+ it("returns 404 for non-existent memory", async () => {
302
+ await startTestDaemon();
303
+
304
+ const { status } = await requestJson(
305
+ testPaths.socketPath,
306
+ "GET",
307
+ "/memory/features/nonexistent",
308
+ );
309
+ expect(status).toBe(404);
310
+ });
311
+
312
+ it("writes PID file on start", async () => {
313
+ await startTestDaemon();
314
+ expect(existsSync(testPaths.pidFile)).toBe(true);
315
+ });
316
+
317
+ it("rejects store with invalid body (missing required fields)", async () => {
318
+ await startTestDaemon();
319
+
320
+ const { status, data } = await requestJson<{ error: string }>(
321
+ testPaths.socketPath,
322
+ "POST",
323
+ "/memory",
324
+ { title: "Test" }, // missing type, description, tags, content
325
+ );
326
+ expect(status).toBe(400);
327
+ expect(data.error).toContain("Validation failed");
328
+ });
329
+
330
+ it("rejects store with invalid type", async () => {
331
+ await startTestDaemon();
332
+
333
+ const { status, data } = await requestJson<{ error: string }>(
334
+ testPaths.socketPath,
335
+ "POST",
336
+ "/memory",
337
+ {
338
+ title: "Test",
339
+ description: "desc",
340
+ type: "invalid_type",
341
+ tags: [],
342
+ content: "content",
343
+ },
344
+ );
345
+ expect(status).toBe(400);
346
+ expect(data.error).toContain("Validation failed");
347
+ });
348
+
349
+ it("rejects update with invalid importance", async () => {
350
+ await startTestDaemon();
351
+
352
+ // First store a valid memory
353
+ await requestJson(testPaths.socketPath, "POST", "/memory", {
354
+ title: "Test Memory",
355
+ description: "desc",
356
+ type: "fact",
357
+ tags: [],
358
+ content: "content",
359
+ });
360
+
361
+ const { status, data } = await requestJson<{ error: string }>(
362
+ testPaths.socketPath,
363
+ "PATCH",
364
+ "/memory/features/test-memory",
365
+ { importance: 99 }, // out of range
366
+ );
367
+ expect(status).toBe(400);
368
+ expect(data.error).toContain("Validation failed");
369
+ });
370
+
371
+ it("ignores invalid type query param on search (returns results)", async () => {
372
+ await startTestDaemon();
373
+
374
+ await requestJson(testPaths.socketPath, "POST", "/memory", {
375
+ title: "Searchable",
376
+ description: "A searchable memory",
377
+ type: "fact",
378
+ tags: ["test"],
379
+ content: "content",
380
+ });
381
+
382
+ // Invalid type is ignored (parsed as undefined)
383
+ const { status, data } = await requestJson<Array<unknown>>(
384
+ testPaths.socketPath,
385
+ "GET",
386
+ "/memory/search?query=searchable&type=bogus",
387
+ );
388
+ expect(status).toBe(200);
389
+ // No type filter applied → results returned
390
+ expect(data.length).toBeGreaterThan(0);
391
+ });
392
+
393
+ it("global error handler returns JSON 500 on unhandled errors", async () => {
394
+ await startTestDaemon();
395
+
396
+ // Send malformed JSON to trigger parse error
397
+ const { status, data } = await requestJson<{ error: string }>(
398
+ testPaths.socketPath,
399
+ "POST",
400
+ "/memory",
401
+ "not valid json body -- this gets stringified so let's test empty object parse",
402
+ );
403
+ // The body is valid JSON (string gets stringified), but Zod rejects it
404
+ expect(status).toBe(400);
405
+ expect(data.error).toBeDefined();
406
+ });
407
+ });
@@ -0,0 +1,103 @@
1
+ import { describe, it, expect, beforeEach, afterEach } from "vitest";
2
+ import { mkdirSync, rmSync } from "fs";
3
+ import { join } from "path";
4
+ import { tmpdir } from "os";
5
+ import { MarkdownBackend } from "../backends/markdown.js";
6
+ import { checkDuplicate, textSimilarity } from "../lib/dedup.js";
7
+ import type { StoreInput } from "../lib/types.js";
8
+
9
+ let testDir: string;
10
+ let backend: MarkdownBackend;
11
+ let counter = 0;
12
+
13
+ beforeEach(() => {
14
+ testDir = join(tmpdir(), `cf-memory-dedup-${Date.now()}-${++counter}`);
15
+ mkdirSync(testDir, { recursive: true });
16
+ backend = new MarkdownBackend(testDir);
17
+ });
18
+
19
+ afterEach(() => {
20
+ rmSync(testDir, { recursive: true, force: true });
21
+ });
22
+
23
+ const sampleInput: StoreInput = {
24
+ title: "API Authentication Pattern",
25
+ description: "Auth module uses JWT tokens stored in httpOnly cookies",
26
+ type: "fact",
27
+ tags: ["auth", "jwt", "api"],
28
+ content: "# API Auth\n\nThe project uses JWT tokens with refresh rotation.",
29
+ };
30
+
31
+ describe("textSimilarity()", () => {
32
+ it("returns 1 for identical strings", () => {
33
+ expect(textSimilarity("hello world", "hello world")).toBe(1);
34
+ });
35
+
36
+ it("returns 0 for completely different strings", () => {
37
+ expect(textSimilarity("alpha beta", "gamma delta")).toBe(0);
38
+ });
39
+
40
+ it("returns partial score for overlapping words", () => {
41
+ const sim = textSimilarity("hello world foo", "hello world bar");
42
+ expect(sim).toBeGreaterThan(0.3);
43
+ expect(sim).toBeLessThan(1);
44
+ });
45
+
46
+ it("is case insensitive", () => {
47
+ expect(textSimilarity("Hello World", "hello world")).toBe(1);
48
+ });
49
+
50
+ it("returns 1 for two empty strings", () => {
51
+ expect(textSimilarity("", "")).toBe(1);
52
+ });
53
+
54
+ it("returns 0 when one is empty", () => {
55
+ expect(textSimilarity("hello", "")).toBe(0);
56
+ });
57
+ });
58
+
59
+ describe("checkDuplicate()", () => {
60
+ it("returns no duplicate when backend is empty", async () => {
61
+ const result = await checkDuplicate(backend, sampleInput);
62
+ expect(result.isDuplicate).toBe(false);
63
+ expect(result.similarity).toBe(0);
64
+ });
65
+
66
+ it("detects near-duplicate with same title", async () => {
67
+ await backend.store(sampleInput);
68
+
69
+ const nearDup: StoreInput = {
70
+ ...sampleInput,
71
+ content: "Different content entirely.",
72
+ };
73
+
74
+ const result = await checkDuplicate(backend, nearDup);
75
+ expect(result.isDuplicate).toBe(true);
76
+ expect(result.similarId).toBeDefined();
77
+ expect(result.similarity).toBeGreaterThan(0.8);
78
+ });
79
+
80
+ it("does not flag unrelated memories as duplicates", async () => {
81
+ await backend.store(sampleInput);
82
+
83
+ const different: StoreInput = {
84
+ title: "Database Migration Strategy",
85
+ description: "How we handle PostgreSQL schema migrations",
86
+ type: "procedure",
87
+ tags: ["database", "migration"],
88
+ content: "# Migration\n\nUse Prisma for schema migrations.",
89
+ };
90
+
91
+ const result = await checkDuplicate(backend, different);
92
+ expect(result.isDuplicate).toBe(false);
93
+ });
94
+
95
+ it("returns warning but still allows store (non-blocking)", async () => {
96
+ await backend.store(sampleInput);
97
+
98
+ const result = await checkDuplicate(backend, sampleInput);
99
+ // Even if duplicate, the function just reports — doesn't block
100
+ expect(typeof result.isDuplicate).toBe("boolean");
101
+ expect(typeof result.similarity).toBe("number");
102
+ });
103
+ });