coding-friend-cli 1.37.0 → 1.38.1

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 +7 -2
  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-DP6DFQ4G.js → chunk-BXSCYGWE.js} +2 -2
  6. package/dist/{chunk-W6D3J5DX.js → chunk-CGEMETKQ.js} +8 -6
  7. package/dist/{chunk-XEKQW4RN.js → chunk-ETTZRJ55.js} +1 -1
  8. package/dist/{chunk-IKLMHJH6.js → chunk-GIH6E7GJ.js} +62 -1
  9. package/dist/{chunk-6S7352JG.js → chunk-ONG4KRYP.js} +1 -1
  10. package/dist/{chunk-X2H7JJMR.js → chunk-PLSJW7QJ.js} +29 -1
  11. package/dist/{chunk-O5ZKLFSL.js → chunk-RCE4OWNX.js} +1 -1
  12. package/dist/{chunk-PI4P34KF.js → chunk-RFNKPAJW.js} +3 -1
  13. package/dist/{chunk-676YCW6T.js → chunk-XAYWGZQT.js} +101 -8
  14. package/dist/{clean-WEYQZB34.js → clean-YWEMYDE5.js} +2 -2
  15. package/dist/{config-ZMR2AOBB.js → config-NG7SICM4.js} +12 -24
  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-SSRI4JZV.js → host-EV6QIVTI.js} +2 -2
  21. package/dist/index.js +59 -47
  22. package/dist/{init-6MFQWPOP.js → init-KYE5CJMW.js} +115 -12
  23. package/dist/{install-37NN4CQH.js → install-3YEPLDMD.js} +79 -6
  24. package/dist/{learn-OPH3CI67.js → learn-MC4VDHZH.js} +2 -2
  25. package/dist/{mcp-DWKVMNC7.js → mcp-FV6YX3ZT.js} +7 -7
  26. package/dist/{mcp-serve-5GODJ4DJ.js → mcp-serve-PXREYQRZ.js} +2 -2
  27. package/dist/{mcp-serve-learn-OF3ZJUJC.js → mcp-serve-learn-UVXIDOEZ.js} +1 -1
  28. package/dist/{memory-TRDLADUM.js → memory-DF7OUOTV.js} +6 -6
  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-S4WKA4P5.js → session-QK6OZO3X.js} +2 -2
  32. package/dist/{status-YZEOUX46.js → status-6HQRVZKR.js} +11 -10
  33. package/dist/{statusline-KKFJPTI7.js → statusline-AOZHEQSV.js} +2 -2
  34. package/dist/{uninstall-AVA3J554.js → uninstall-MF6FRUBO.js} +51 -4
  35. package/dist/{update-Q3ACCK4Z.js → update-SMWXKJ3R.js} +6 -5
  36. package/dist/{update-check-25SCLHZO.js → update-check-7LKJLJPM.js} +1 -1
  37. package/lib/cf-memory/src/lib/claude-md.ts +82 -70
  38. package/lib/cf-memory/src/lib/status-frame.ts +2 -2
  39. package/lib/cf-memory/src/tools/delete.ts +1 -1
  40. package/lib/cf-memory/src/tools/store.ts +6 -6
  41. package/lib/cf-memory/src/tools/update.ts +5 -11
  42. package/package.json +32 -4
  43. package/lib/cf-memory/README.md +0 -317
  44. package/lib/cf-memory/src/__tests__/claude-md.test.ts +0 -343
  45. package/lib/cf-memory/src/__tests__/daemon-client.test.ts +0 -146
  46. package/lib/cf-memory/src/__tests__/daemon-e2e.test.ts +0 -223
  47. package/lib/cf-memory/src/__tests__/daemon.test.ts +0 -407
  48. package/lib/cf-memory/src/__tests__/dedup.test.ts +0 -103
  49. package/lib/cf-memory/src/__tests__/embeddings.test.ts +0 -292
  50. package/lib/cf-memory/src/__tests__/lazy-install.test.ts +0 -210
  51. package/lib/cf-memory/src/__tests__/markdown-backend.test.ts +0 -450
  52. package/lib/cf-memory/src/__tests__/migration.test.ts +0 -255
  53. package/lib/cf-memory/src/__tests__/migrations.test.ts +0 -288
  54. package/lib/cf-memory/src/__tests__/minisearch-backend.test.ts +0 -262
  55. package/lib/cf-memory/src/__tests__/ollama.test.ts +0 -48
  56. package/lib/cf-memory/src/__tests__/project-id.test.ts +0 -43
  57. package/lib/cf-memory/src/__tests__/schema.test.ts +0 -128
  58. package/lib/cf-memory/src/__tests__/search.test.ts +0 -115
  59. package/lib/cf-memory/src/__tests__/sqlite-lazy-db.test.ts +0 -213
  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 -388
  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
@@ -1,388 +0,0 @@
1
- import { describe, it, expect, vi, beforeEach, afterEach } from "vitest";
2
- import { mkdirSync, rmSync, existsSync } from "fs";
3
- import { join } from "path";
4
- import { tmpdir } from "os";
5
- import { detectTier, createBackendForTier, TIERS } from "../lib/tier.js";
6
- import { MarkdownBackend } from "../backends/markdown.js";
7
- import { DaemonClient } from "../lib/daemon-client.js";
8
- import type { DaemonPaths } from "../daemon/process.js";
9
- import { areSqliteDepsAvailable } from "../lib/lazy-install.js";
10
-
11
- let testDir: string;
12
-
13
- beforeEach(() => {
14
- testDir = join(tmpdir(), `cf-memory-tier-test-${Date.now()}`);
15
- mkdirSync(testDir, { recursive: true });
16
- });
17
-
18
- afterEach(() => {
19
- rmSync(testDir, { recursive: true, force: true });
20
- });
21
-
22
- describe("detectTier()", () => {
23
- it("returns Tier 3 (markdown) when no daemon is running and no sqlite deps", async () => {
24
- const lazyInstall = await import("../lib/lazy-install.js");
25
- const sqliteSpy = vi
26
- .spyOn(lazyInstall, "areSqliteDepsAvailable")
27
- .mockReturnValue(false);
28
- const daemonProcess = await import("../daemon/process.js");
29
- const daemonSpy = vi
30
- .spyOn(daemonProcess, "isDaemonRunning")
31
- .mockResolvedValue(false);
32
- try {
33
- const tier = await detectTier();
34
- // Without a daemon running and no SQLite deps, should detect Tier 3
35
- expect(tier.name).toBe("markdown");
36
- expect(tier.number).toBe(3);
37
- expect(tier.label).toContain("Tier 3");
38
- } finally {
39
- sqliteSpy.mockRestore();
40
- daemonSpy.mockRestore();
41
- }
42
- });
43
-
44
- it("respects config override to markdown", async () => {
45
- const tier = await detectTier("markdown");
46
- expect(tier.name).toBe("markdown");
47
- });
48
-
49
- it("respects config override to lite", async () => {
50
- const tier = await detectTier("lite");
51
- expect(tier.name).toBe("lite");
52
- expect(tier.number).toBe(2);
53
- });
54
-
55
- it("respects config override to full", async () => {
56
- const tier = await detectTier("full");
57
- expect(tier.name).toBe("full");
58
- expect(tier.number).toBe(1);
59
- });
60
-
61
- it("auto mode defaults to markdown when no daemon and no sqlite deps", async () => {
62
- // Mock areSqliteDepsAvailable to return false
63
- const lazyInstall = await import("../lib/lazy-install.js");
64
- const sqliteSpy = vi
65
- .spyOn(lazyInstall, "areSqliteDepsAvailable")
66
- .mockReturnValue(false);
67
- const daemonProcess = await import("../daemon/process.js");
68
- const daemonSpy = vi
69
- .spyOn(daemonProcess, "isDaemonRunning")
70
- .mockResolvedValue(false);
71
- try {
72
- const tier = await detectTier("auto");
73
- expect(tier.name).toBe("markdown");
74
- } finally {
75
- sqliteSpy.mockRestore();
76
- daemonSpy.mockRestore();
77
- }
78
- });
79
-
80
- it("auto mode returns full when SQLite deps are available", async () => {
81
- const lazyInstall = await import("../lib/lazy-install.js");
82
- const sqliteSpy = vi
83
- .spyOn(lazyInstall, "areSqliteDepsAvailable")
84
- .mockReturnValue(true);
85
- try {
86
- const tier = await detectTier("auto");
87
- expect(tier.name).toBe("full");
88
- expect(tier.number).toBe(1);
89
- } finally {
90
- sqliteSpy.mockRestore();
91
- }
92
- });
93
-
94
- it("auto mode returns lite when daemon is running (no sqlite deps)", async () => {
95
- // Mock areSqliteDepsAvailable to return false, isDaemonRunning to return true
96
- const lazyInstall = await import("../lib/lazy-install.js");
97
- const sqliteSpy = vi
98
- .spyOn(lazyInstall, "areSqliteDepsAvailable")
99
- .mockReturnValue(false);
100
- const daemonProcess = await import("../daemon/process.js");
101
- const spy = vi
102
- .spyOn(daemonProcess, "isDaemonRunning")
103
- .mockResolvedValue(true);
104
- try {
105
- const tier = await detectTier("auto");
106
- expect(tier.name).toBe("lite");
107
- expect(tier.number).toBe(2);
108
- } finally {
109
- spy.mockRestore();
110
- sqliteSpy.mockRestore();
111
- }
112
- });
113
- });
114
-
115
- describe("createBackendForTier()", () => {
116
- it("creates MarkdownBackend for markdown tier", async () => {
117
- const { backend, tier } = await createBackendForTier(testDir, "markdown");
118
- expect(tier.name).toBe("markdown");
119
- expect(backend).toBeInstanceOf(MarkdownBackend);
120
- await backend.close();
121
- });
122
-
123
- it("auto tier falls back to markdown when no daemon and no sqlite deps", async () => {
124
- const lazyInstall = await import("../lib/lazy-install.js");
125
- const sqliteSpy = vi
126
- .spyOn(lazyInstall, "areSqliteDepsAvailable")
127
- .mockReturnValue(false);
128
- const daemonProcess = await import("../daemon/process.js");
129
- const daemonSpy = vi
130
- .spyOn(daemonProcess, "isDaemonRunning")
131
- .mockResolvedValue(false);
132
- try {
133
- const { backend, tier } = await createBackendForTier(testDir, "auto");
134
- expect(tier.name).toBe("markdown");
135
- expect(backend).toBeInstanceOf(MarkdownBackend);
136
- await backend.close();
137
- } finally {
138
- sqliteSpy.mockRestore();
139
- daemonSpy.mockRestore();
140
- }
141
- });
142
-
143
- it("auto tier creates SqliteBackend when sqlite deps available", async () => {
144
- const lazyInstall = await import("../lib/lazy-install.js");
145
- if (!lazyInstall.areSqliteDepsAvailable()) return; // skip if deps not installed
146
- const dbPath = join(testDir, "db.sqlite");
147
- const { backend, tier } = await createBackendForTier(
148
- testDir,
149
- "auto",
150
- undefined,
151
- { dbPath },
152
- );
153
- expect(tier.name).toBe("full");
154
- await backend.close();
155
- });
156
-
157
- it("full tier falls back to markdown when SQLite deps not available", async () => {
158
- // Without lazy-installed deps, SqliteBackend will fail to construct
159
- // and createBackendForTier should fall back to markdown
160
- const dbPath = join(testDir, "db.sqlite");
161
- const { backend, tier } = await createBackendForTier(
162
- testDir,
163
- "full",
164
- undefined,
165
- { dbPath },
166
- );
167
- // Falls back because better-sqlite3 is not installed in test env
168
- expect(["markdown", "full"]).toContain(tier.name);
169
- await backend.close();
170
- });
171
-
172
- it("lite tier with running daemon returns DaemonClient", async () => {
173
- // Start a real daemon to test the full path
174
- const { MiniSearchBackend } = await import("../backends/minisearch.js");
175
- const { startDaemonServer } = await import("../daemon/process.js");
176
-
177
- const docsDir = join(testDir, "docs");
178
- mkdirSync(docsDir, { recursive: true });
179
- const daemonDir = join(testDir, "daemon");
180
- mkdirSync(daemonDir, { recursive: true });
181
-
182
- const paths: DaemonPaths = {
183
- socketPath: join(daemonDir, "daemon.sock"),
184
- pidFile: join(daemonDir, "daemon.pid"),
185
- logFile: join(daemonDir, "daemon.log"),
186
- };
187
-
188
- const backend = new MiniSearchBackend(docsDir);
189
- const handle = startDaemonServer(backend, {
190
- paths,
191
- idleTimeoutMs: 0,
192
- });
193
-
194
- await new Promise<void>((resolve) => {
195
- handle.server.once("listening", resolve);
196
- });
197
-
198
- try {
199
- // Mock getDaemonPaths so createBackendForTier uses our test paths
200
- const daemonProcess = await import("../daemon/process.js");
201
- const pathsSpy = vi
202
- .spyOn(daemonProcess, "getDaemonPaths")
203
- .mockReturnValue(paths);
204
-
205
- const { backend: created, tier } = await createBackendForTier(
206
- docsDir,
207
- "lite",
208
- );
209
- expect(tier.name).toBe("lite");
210
- expect(created).toBeInstanceOf(DaemonClient);
211
- await created.close();
212
- pathsSpy.mockRestore();
213
- } finally {
214
- await new Promise<void>((resolve) => {
215
- handle.server.close(() => resolve());
216
- });
217
- rmSync(paths.socketPath, { force: true });
218
- rmSync(paths.pidFile, { force: true });
219
- }
220
- });
221
-
222
- it("lite tier falls back to markdown when daemon is unreachable mid-session", async () => {
223
- // Simulate: tier detection says "lite" but daemon socket is gone
224
- // Must also mock SQLite deps away so auto tier picks "lite" over "full"
225
- const lazyInstall = await import("../lib/lazy-install.js");
226
- const sqliteSpy = vi
227
- .spyOn(lazyInstall, "areSqliteDepsAvailable")
228
- .mockReturnValue(false);
229
- const daemonProcess = await import("../daemon/process.js");
230
- const spy = vi
231
- .spyOn(daemonProcess, "isDaemonRunning")
232
- .mockResolvedValue(true);
233
- const pathsSpy = vi.spyOn(daemonProcess, "getDaemonPaths").mockReturnValue({
234
- socketPath: "/tmp/nonexistent-cf-test-sock-" + Date.now(),
235
- pidFile: "/tmp/nonexistent-cf-test-pid-" + Date.now(),
236
- logFile: "/tmp/nonexistent-cf-test-log-" + Date.now(),
237
- });
238
-
239
- try {
240
- const { backend: created, tier } = await createBackendForTier(
241
- testDir,
242
- "auto",
243
- );
244
- // DaemonClient.ping() fails → falls back to MarkdownBackend
245
- expect(tier.name).toBe("markdown");
246
- expect(created).toBeInstanceOf(MarkdownBackend);
247
- await created.close();
248
- } finally {
249
- sqliteSpy.mockRestore();
250
- spy.mockRestore();
251
- pathsSpy.mockRestore();
252
- }
253
- });
254
- });
255
-
256
- describe("createBackendForTier() — lazy DB guarantee", () => {
257
- it.skipIf(!areSqliteDepsAvailable())(
258
- "full tier on a non-CF project does not create DB before any write",
259
- async () => {
260
- // Use an explicit tmpdir dbPath so ~/.coding-friend is never touched
261
- const dbPath = join(testDir, "db.sqlite");
262
- const { backend, tier } = await createBackendForTier(
263
- testDir,
264
- "full",
265
- undefined,
266
- { dbPath },
267
- );
268
- expect(tier.name).toBe("full");
269
- // No DB file after construction
270
- expect(existsSync(dbPath)).toBe(false);
271
- await backend.close();
272
- },
273
- );
274
- });
275
-
276
- describe("createBackendForTier() with embeddingConfig", () => {
277
- it("accepts an optional embeddingConfig parameter without error", async () => {
278
- const embeddingConfig = {
279
- provider: "ollama" as const,
280
- model: "nomic-embed-text",
281
- ollamaUrl: "http://localhost:11434",
282
- };
283
- // Should not throw — markdown tier ignores embedding config gracefully
284
- const { backend, tier } = await createBackendForTier(
285
- testDir,
286
- "markdown",
287
- embeddingConfig,
288
- );
289
- expect(tier.name).toBe("markdown");
290
- expect(backend).toBeInstanceOf(MarkdownBackend);
291
- await backend.close();
292
- });
293
-
294
- it("passes embeddingConfig through when full tier falls back to markdown", async () => {
295
- const embeddingConfig = {
296
- provider: "ollama" as const,
297
- model: "nomic-embed-text",
298
- };
299
- const dbPath = join(testDir, "db.sqlite");
300
- // Full tier will fail (no sqlite deps in test env) and fall back
301
- const { backend, tier } = await createBackendForTier(
302
- testDir,
303
- "full",
304
- embeddingConfig,
305
- { dbPath },
306
- );
307
- // Falls back because better-sqlite3 is not installed in test env
308
- expect(["markdown", "full"]).toContain(tier.name);
309
- await backend.close();
310
- });
311
- });
312
-
313
- describe("makeRespawn retry logic", () => {
314
- it("retries up to 3 times before giving up", async () => {
315
- const daemonProcess = await import("../daemon/process.js");
316
-
317
- let attempts = 0;
318
- const spawnSpy = vi
319
- .spyOn(daemonProcess, "spawnDaemon")
320
- .mockImplementation(async () => {
321
- attempts++;
322
- return null; // always fail
323
- });
324
- // Isolate from real daemon state on the host machine
325
- const runningSpy = vi
326
- .spyOn(daemonProcess, "isDaemonRunning")
327
- .mockResolvedValue(false);
328
-
329
- // Use createBackendForTier with "lite" to trigger respawn path
330
- // The respawn callback wraps makeRespawn which calls spawnDaemon
331
- // We need to get the respawn function indirectly
332
- // Instead, test via the exported createRespawnWithRetry
333
- const { createRespawnWithRetry } = await import("../lib/tier.js");
334
- const respawn = createRespawnWithRetry(".", undefined, undefined);
335
- const result = await respawn();
336
-
337
- expect(result).toBe(false);
338
- expect(attempts).toBe(3);
339
-
340
- spawnSpy.mockRestore();
341
- runningSpy.mockRestore();
342
- });
343
-
344
- it("succeeds on second attempt", async () => {
345
- const daemonProcess = await import("../daemon/process.js");
346
-
347
- let attempts = 0;
348
- const spawnSpy = vi
349
- .spyOn(daemonProcess, "spawnDaemon")
350
- .mockImplementation(async () => {
351
- attempts++;
352
- if (attempts === 2) return { pid: 1234 };
353
- return null;
354
- });
355
- // Isolate from real daemon state on the host machine
356
- const runningSpy = vi
357
- .spyOn(daemonProcess, "isDaemonRunning")
358
- .mockResolvedValue(false);
359
-
360
- const { createRespawnWithRetry } = await import("../lib/tier.js");
361
- const respawn = createRespawnWithRetry(".", undefined, undefined);
362
- const result = await respawn();
363
-
364
- expect(result).toBe(true);
365
- expect(attempts).toBe(2);
366
-
367
- spawnSpy.mockRestore();
368
- runningSpy.mockRestore();
369
- });
370
- });
371
-
372
- describe("TIERS constant", () => {
373
- it("has all 3 tier definitions", () => {
374
- expect(Object.keys(TIERS)).toHaveLength(3);
375
- expect(TIERS.full.number).toBe(1);
376
- expect(TIERS.lite.number).toBe(2);
377
- expect(TIERS.markdown.number).toBe(3);
378
- });
379
-
380
- it("each tier has name, label, and number", () => {
381
- for (const tier of Object.values(TIERS)) {
382
- expect(tier.name).toBeTruthy();
383
- expect(tier.label).toBeTruthy();
384
- expect(tier.number).toBeGreaterThanOrEqual(1);
385
- expect(tier.number).toBeLessThanOrEqual(3);
386
- }
387
- });
388
- });
@@ -1,83 +0,0 @@
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 { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
6
- import { MarkdownBackend } from "../backends/markdown.js";
7
- import { registerAllTools } from "../server.js";
8
- import { registerAllResources } from "../resources/index.js";
9
-
10
- let testDir: string;
11
- let server: McpServer;
12
- let backend: MarkdownBackend;
13
-
14
- beforeEach(() => {
15
- testDir = join(tmpdir(), `cf-memory-tools-test-${Date.now()}`);
16
- mkdirSync(testDir, { recursive: true });
17
- backend = new MarkdownBackend(testDir);
18
- server = new McpServer({
19
- name: "test-memory-server",
20
- version: "0.0.1",
21
- });
22
- });
23
-
24
- afterEach(() => {
25
- rmSync(testDir, { recursive: true, force: true });
26
- });
27
-
28
- describe("MCP Tool Registration", () => {
29
- it("registers all 6 tools without error", () => {
30
- expect(() => registerAllTools(server, backend)).not.toThrow();
31
- });
32
-
33
- it("registers all 2 resources without error", () => {
34
- expect(() => registerAllResources(server, backend)).not.toThrow();
35
- });
36
-
37
- it("registers both tools and resources together", () => {
38
- expect(() => {
39
- registerAllTools(server, backend);
40
- registerAllResources(server, backend);
41
- }).not.toThrow();
42
- });
43
- });
44
-
45
- describe("MemoryBackend interface compliance", () => {
46
- it("has all 8 required methods", () => {
47
- expect(typeof backend.store).toBe("function");
48
- expect(typeof backend.search).toBe("function");
49
- expect(typeof backend.retrieve).toBe("function");
50
- expect(typeof backend.list).toBe("function");
51
- expect(typeof backend.update).toBe("function");
52
- expect(typeof backend.delete).toBe("function");
53
- expect(typeof backend.stats).toBe("function");
54
- expect(typeof backend.close).toBe("function");
55
- });
56
- });
57
-
58
- describe("Type validation", () => {
59
- it("MEMORY_TYPES has exactly 5 types", async () => {
60
- const { MEMORY_TYPES } = await import("../lib/types.js");
61
- expect(MEMORY_TYPES).toHaveLength(5);
62
- expect(MEMORY_TYPES).toContain("fact");
63
- expect(MEMORY_TYPES).toContain("preference");
64
- expect(MEMORY_TYPES).toContain("context");
65
- expect(MEMORY_TYPES).toContain("episode");
66
- expect(MEMORY_TYPES).toContain("procedure");
67
- });
68
-
69
- it("MEMORY_CATEGORIES maps all types to folders", async () => {
70
- const { MEMORY_CATEGORIES, MEMORY_TYPES } = await import("../lib/types.js");
71
- for (const type of MEMORY_TYPES) {
72
- expect(MEMORY_CATEGORIES[type]).toBeTruthy();
73
- }
74
- });
75
-
76
- it("CATEGORY_TO_TYPE is inverse of MEMORY_CATEGORIES", async () => {
77
- const { MEMORY_CATEGORIES, CATEGORY_TO_TYPE } =
78
- await import("../lib/types.js");
79
- for (const [type, category] of Object.entries(MEMORY_CATEGORIES)) {
80
- expect(CATEGORY_TO_TYPE[category]).toBe(type);
81
- }
82
- });
83
- });
@@ -1,7 +0,0 @@
1
- import { defineConfig } from "vitest/config";
2
-
3
- export default defineConfig({
4
- test: {
5
- environment: "node",
6
- },
7
- });
@@ -1,4 +0,0 @@
1
- node_modules
2
- .next
3
- out
4
- public/_pagefind
@@ -1,8 +0,0 @@
1
- {
2
- "semi": true,
3
- "singleQuote": false,
4
- "tabWidth": 2,
5
- "trailingComma": "all",
6
- "printWidth": 80,
7
- "plugins": ["prettier-plugin-tailwindcss"]
8
- }
@@ -1,113 +0,0 @@
1
- # coding-friend-learn-host
2
-
3
- Next.js app that renders your `/cf-learn` docs as a browsable website with search, categories, and dark mode.
4
-
5
- ## Usage (via CLI)
6
-
7
- ```bash
8
- cf learn host # serves docs/learn/ on port 3333
9
- cf learn host ./my-docs # serves a custom directory
10
- cf learn host -p 4000 # custom port
11
- ```
12
-
13
- The CLI handles deps install, build, and serving automatically.
14
-
15
- ## Local Development
16
-
17
- Run the app directly without the CLI — useful when working on the UI itself.
18
-
19
- ### 1. Install dependencies
20
-
21
- ```bash
22
- cd cli/lib/learn-host
23
- npm install
24
- ```
25
-
26
- ### 2. Point to a docs directory
27
-
28
- The app reads the docs directory from the `DOCS_DIR` env var, which the CLI sets by resolving (in order):
29
-
30
- 1. Explicit path argument (`cf learn host <path>`)
31
- 2. Global `~/.coding-friend/config.json` → `learn.outputDir`
32
- 3. Default: `~/.coding-friend/learn`
33
-
34
- For local dev, set `DOCS_DIR` to any directory with the expected structure:
35
-
36
- ```
37
- docs/
38
- └── learn/
39
- ├── category-one/
40
- │ ├── my-doc.md
41
- │ └── another-doc.md
42
- └── category-two/
43
- └── some-doc.md
44
- ```
45
-
46
- Each `.md` file should have frontmatter:
47
-
48
- ```md
49
- ---
50
- title: My Doc Title
51
- category: category-one
52
- tags: [typescript, patterns]
53
- created: 2025-01-01
54
- updated: 2025-01-15
55
- ---
56
-
57
- Content here...
58
- ```
59
-
60
- ### 3. Run dev server
61
-
62
- ```bash
63
- # Point to this repo's own learn docs (if they exist)
64
- DOCS_DIR=../../../docs/learn npm run dev
65
-
66
- # Or point to any other project's docs
67
- DOCS_DIR=/path/to/your/project/docs/learn npm run dev
68
- ```
69
-
70
- App runs at `http://localhost:3333`.
71
-
72
- > **Note:** `npm run dev` does not rebuild the Pagefind search index. If you've previously run `npm run build`, search will still work but uses the old index — new or edited docs won't appear in search results until you build again.
73
-
74
- ### 4. Full build (with search)
75
-
76
- ```bash
77
- DOCS_DIR=/path/to/docs npm run build
78
- npx next start -p 3333
79
- ```
80
-
81
- `postbuild` runs `pagefind` to index docs for full-text search.
82
-
83
- ## Structure
84
-
85
- ```
86
- src/
87
- ├── app/
88
- │ ├── page.tsx # Homepage: recent docs, categories, tags
89
- │ ├── [category]/page.tsx # Category listing
90
- │ ├── [category]/[slug]/page.tsx # Individual doc
91
- │ └── layout.tsx
92
- ├── components/
93
- │ ├── MarkdownRenderer.tsx # Renders .md with syntax highlighting
94
- │ ├── TableOfContents.tsx # Auto-generated from headings
95
- │ ├── Sidebar.tsx # Category navigation
96
- │ ├── PagefindSearch.tsx # Full-text search (build-time index)
97
- │ └── ThemeToggle.tsx
98
- └── lib/
99
- ├── docs.ts # getAllDocs, getDocBySlug, etc.
100
- └── types.ts
101
- ```
102
-
103
- ## How It Fits Together
104
-
105
- ```
106
- cf learn host [path]
107
- └─ resolves docs dir
108
- └─ npm install (one-time)
109
- └─ npm run build (with DOCS_DIR env)
110
- └─ npx next start -p 3333 (with DOCS_DIR env)
111
- ```
112
-
113
- ISR (Incremental Static Regeneration) is enabled, so new or edited docs appear on the next page refresh without a rebuild.
@@ -1,6 +0,0 @@
1
- import nextConfig from "eslint-config-next";
2
- import eslintConfigPrettier from "eslint-config-prettier";
3
-
4
- const eslintConfig = [...nextConfig, eslintConfigPrettier];
5
-
6
- export default eslintConfig;