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
@@ -1,367 +0,0 @@
1
- import { describe, it, expect, vi, beforeEach, afterEach } from "vitest";
2
- import { mkdirSync, rmSync } 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
-
10
- let testDir: string;
11
-
12
- beforeEach(() => {
13
- testDir = join(tmpdir(), `cf-memory-tier-test-${Date.now()}`);
14
- mkdirSync(testDir, { recursive: true });
15
- });
16
-
17
- afterEach(() => {
18
- rmSync(testDir, { recursive: true, force: true });
19
- });
20
-
21
- describe("detectTier()", () => {
22
- it("returns Tier 3 (markdown) when no daemon is running and no sqlite deps", async () => {
23
- const lazyInstall = await import("../lib/lazy-install.js");
24
- const sqliteSpy = vi
25
- .spyOn(lazyInstall, "areSqliteDepsAvailable")
26
- .mockReturnValue(false);
27
- const daemonProcess = await import("../daemon/process.js");
28
- const daemonSpy = vi
29
- .spyOn(daemonProcess, "isDaemonRunning")
30
- .mockResolvedValue(false);
31
- try {
32
- const tier = await detectTier();
33
- // Without a daemon running and no SQLite deps, should detect Tier 3
34
- expect(tier.name).toBe("markdown");
35
- expect(tier.number).toBe(3);
36
- expect(tier.label).toContain("Tier 3");
37
- } finally {
38
- sqliteSpy.mockRestore();
39
- daemonSpy.mockRestore();
40
- }
41
- });
42
-
43
- it("respects config override to markdown", async () => {
44
- const tier = await detectTier("markdown");
45
- expect(tier.name).toBe("markdown");
46
- });
47
-
48
- it("respects config override to lite", async () => {
49
- const tier = await detectTier("lite");
50
- expect(tier.name).toBe("lite");
51
- expect(tier.number).toBe(2);
52
- });
53
-
54
- it("respects config override to full", async () => {
55
- const tier = await detectTier("full");
56
- expect(tier.name).toBe("full");
57
- expect(tier.number).toBe(1);
58
- });
59
-
60
- it("auto mode defaults to markdown when no daemon and no sqlite deps", async () => {
61
- // Mock areSqliteDepsAvailable to return false
62
- const lazyInstall = await import("../lib/lazy-install.js");
63
- const sqliteSpy = vi
64
- .spyOn(lazyInstall, "areSqliteDepsAvailable")
65
- .mockReturnValue(false);
66
- const daemonProcess = await import("../daemon/process.js");
67
- const daemonSpy = vi
68
- .spyOn(daemonProcess, "isDaemonRunning")
69
- .mockResolvedValue(false);
70
- try {
71
- const tier = await detectTier("auto");
72
- expect(tier.name).toBe("markdown");
73
- } finally {
74
- sqliteSpy.mockRestore();
75
- daemonSpy.mockRestore();
76
- }
77
- });
78
-
79
- it("auto mode returns full when SQLite deps are available", async () => {
80
- const lazyInstall = await import("../lib/lazy-install.js");
81
- const sqliteSpy = vi
82
- .spyOn(lazyInstall, "areSqliteDepsAvailable")
83
- .mockReturnValue(true);
84
- try {
85
- const tier = await detectTier("auto");
86
- expect(tier.name).toBe("full");
87
- expect(tier.number).toBe(1);
88
- } finally {
89
- sqliteSpy.mockRestore();
90
- }
91
- });
92
-
93
- it("auto mode returns lite when daemon is running (no sqlite deps)", async () => {
94
- // Mock areSqliteDepsAvailable to return false, isDaemonRunning to return true
95
- const lazyInstall = await import("../lib/lazy-install.js");
96
- const sqliteSpy = vi
97
- .spyOn(lazyInstall, "areSqliteDepsAvailable")
98
- .mockReturnValue(false);
99
- const daemonProcess = await import("../daemon/process.js");
100
- const spy = vi
101
- .spyOn(daemonProcess, "isDaemonRunning")
102
- .mockResolvedValue(true);
103
- try {
104
- const tier = await detectTier("auto");
105
- expect(tier.name).toBe("lite");
106
- expect(tier.number).toBe(2);
107
- } finally {
108
- spy.mockRestore();
109
- sqliteSpy.mockRestore();
110
- }
111
- });
112
- });
113
-
114
- describe("createBackendForTier()", () => {
115
- it("creates MarkdownBackend for markdown tier", async () => {
116
- const { backend, tier } = await createBackendForTier(testDir, "markdown");
117
- expect(tier.name).toBe("markdown");
118
- expect(backend).toBeInstanceOf(MarkdownBackend);
119
- await backend.close();
120
- });
121
-
122
- it("auto tier falls back to markdown when no daemon and no sqlite deps", async () => {
123
- const lazyInstall = await import("../lib/lazy-install.js");
124
- const sqliteSpy = vi
125
- .spyOn(lazyInstall, "areSqliteDepsAvailable")
126
- .mockReturnValue(false);
127
- const daemonProcess = await import("../daemon/process.js");
128
- const daemonSpy = vi
129
- .spyOn(daemonProcess, "isDaemonRunning")
130
- .mockResolvedValue(false);
131
- try {
132
- const { backend, tier } = await createBackendForTier(testDir, "auto");
133
- expect(tier.name).toBe("markdown");
134
- expect(backend).toBeInstanceOf(MarkdownBackend);
135
- await backend.close();
136
- } finally {
137
- sqliteSpy.mockRestore();
138
- daemonSpy.mockRestore();
139
- }
140
- });
141
-
142
- it("auto tier creates SqliteBackend when sqlite deps available", async () => {
143
- const lazyInstall = await import("../lib/lazy-install.js");
144
- if (!lazyInstall.areSqliteDepsAvailable()) return; // skip if deps not installed
145
- const dbPath = join(testDir, "db.sqlite");
146
- const { backend, tier } = await createBackendForTier(
147
- testDir,
148
- "auto",
149
- undefined,
150
- { dbPath },
151
- );
152
- expect(tier.name).toBe("full");
153
- await backend.close();
154
- });
155
-
156
- it("full tier falls back to markdown when SQLite deps not available", async () => {
157
- // Without lazy-installed deps, SqliteBackend will fail to construct
158
- // and createBackendForTier should fall back to markdown
159
- const dbPath = join(testDir, "db.sqlite");
160
- const { backend, tier } = await createBackendForTier(
161
- testDir,
162
- "full",
163
- undefined,
164
- { dbPath },
165
- );
166
- // Falls back because better-sqlite3 is not installed in test env
167
- expect(["markdown", "full"]).toContain(tier.name);
168
- await backend.close();
169
- });
170
-
171
- it("lite tier with running daemon returns DaemonClient", async () => {
172
- // Start a real daemon to test the full path
173
- const { MiniSearchBackend } = await import("../backends/minisearch.js");
174
- const { startDaemonServer } = await import("../daemon/process.js");
175
-
176
- const docsDir = join(testDir, "docs");
177
- mkdirSync(docsDir, { recursive: true });
178
- const daemonDir = join(testDir, "daemon");
179
- mkdirSync(daemonDir, { recursive: true });
180
-
181
- const paths: DaemonPaths = {
182
- socketPath: join(daemonDir, "daemon.sock"),
183
- pidFile: join(daemonDir, "daemon.pid"),
184
- logFile: join(daemonDir, "daemon.log"),
185
- };
186
-
187
- const backend = new MiniSearchBackend(docsDir);
188
- const handle = startDaemonServer(backend, {
189
- paths,
190
- idleTimeoutMs: 0,
191
- });
192
-
193
- await new Promise<void>((resolve) => {
194
- handle.server.once("listening", resolve);
195
- });
196
-
197
- try {
198
- // Mock getDaemonPaths so createBackendForTier uses our test paths
199
- const daemonProcess = await import("../daemon/process.js");
200
- const pathsSpy = vi
201
- .spyOn(daemonProcess, "getDaemonPaths")
202
- .mockReturnValue(paths);
203
-
204
- const { backend: created, tier } = await createBackendForTier(
205
- docsDir,
206
- "lite",
207
- );
208
- expect(tier.name).toBe("lite");
209
- expect(created).toBeInstanceOf(DaemonClient);
210
- await created.close();
211
- pathsSpy.mockRestore();
212
- } finally {
213
- await new Promise<void>((resolve) => {
214
- handle.server.close(() => resolve());
215
- });
216
- rmSync(paths.socketPath, { force: true });
217
- rmSync(paths.pidFile, { force: true });
218
- }
219
- });
220
-
221
- it("lite tier falls back to markdown when daemon is unreachable mid-session", async () => {
222
- // Simulate: tier detection says "lite" but daemon socket is gone
223
- // Must also mock SQLite deps away so auto tier picks "lite" over "full"
224
- const lazyInstall = await import("../lib/lazy-install.js");
225
- const sqliteSpy = vi
226
- .spyOn(lazyInstall, "areSqliteDepsAvailable")
227
- .mockReturnValue(false);
228
- const daemonProcess = await import("../daemon/process.js");
229
- const spy = vi
230
- .spyOn(daemonProcess, "isDaemonRunning")
231
- .mockResolvedValue(true);
232
- const pathsSpy = vi.spyOn(daemonProcess, "getDaemonPaths").mockReturnValue({
233
- socketPath: "/tmp/nonexistent-cf-test-sock-" + Date.now(),
234
- pidFile: "/tmp/nonexistent-cf-test-pid-" + Date.now(),
235
- logFile: "/tmp/nonexistent-cf-test-log-" + Date.now(),
236
- });
237
-
238
- try {
239
- const { backend: created, tier } = await createBackendForTier(
240
- testDir,
241
- "auto",
242
- );
243
- // DaemonClient.ping() fails → falls back to MarkdownBackend
244
- expect(tier.name).toBe("markdown");
245
- expect(created).toBeInstanceOf(MarkdownBackend);
246
- await created.close();
247
- } finally {
248
- sqliteSpy.mockRestore();
249
- spy.mockRestore();
250
- pathsSpy.mockRestore();
251
- }
252
- });
253
- });
254
-
255
- describe("createBackendForTier() with embeddingConfig", () => {
256
- it("accepts an optional embeddingConfig parameter without error", async () => {
257
- const embeddingConfig = {
258
- provider: "ollama" as const,
259
- model: "nomic-embed-text",
260
- ollamaUrl: "http://localhost:11434",
261
- };
262
- // Should not throw — markdown tier ignores embedding config gracefully
263
- const { backend, tier } = await createBackendForTier(
264
- testDir,
265
- "markdown",
266
- embeddingConfig,
267
- );
268
- expect(tier.name).toBe("markdown");
269
- expect(backend).toBeInstanceOf(MarkdownBackend);
270
- await backend.close();
271
- });
272
-
273
- it("passes embeddingConfig through when full tier falls back to markdown", async () => {
274
- const embeddingConfig = {
275
- provider: "ollama" as const,
276
- model: "nomic-embed-text",
277
- };
278
- const dbPath = join(testDir, "db.sqlite");
279
- // Full tier will fail (no sqlite deps in test env) and fall back
280
- const { backend, tier } = await createBackendForTier(
281
- testDir,
282
- "full",
283
- embeddingConfig,
284
- { dbPath },
285
- );
286
- // Falls back because better-sqlite3 is not installed in test env
287
- expect(["markdown", "full"]).toContain(tier.name);
288
- await backend.close();
289
- });
290
- });
291
-
292
- describe("makeRespawn retry logic", () => {
293
- it("retries up to 3 times before giving up", async () => {
294
- const daemonProcess = await import("../daemon/process.js");
295
-
296
- let attempts = 0;
297
- const spawnSpy = vi
298
- .spyOn(daemonProcess, "spawnDaemon")
299
- .mockImplementation(async () => {
300
- attempts++;
301
- return null; // always fail
302
- });
303
- // Isolate from real daemon state on the host machine
304
- const runningSpy = vi
305
- .spyOn(daemonProcess, "isDaemonRunning")
306
- .mockResolvedValue(false);
307
-
308
- // Use createBackendForTier with "lite" to trigger respawn path
309
- // The respawn callback wraps makeRespawn which calls spawnDaemon
310
- // We need to get the respawn function indirectly
311
- // Instead, test via the exported createRespawnWithRetry
312
- const { createRespawnWithRetry } = await import("../lib/tier.js");
313
- const respawn = createRespawnWithRetry(".", undefined, undefined);
314
- const result = await respawn();
315
-
316
- expect(result).toBe(false);
317
- expect(attempts).toBe(3);
318
-
319
- spawnSpy.mockRestore();
320
- runningSpy.mockRestore();
321
- });
322
-
323
- it("succeeds on second attempt", async () => {
324
- const daemonProcess = await import("../daemon/process.js");
325
-
326
- let attempts = 0;
327
- const spawnSpy = vi
328
- .spyOn(daemonProcess, "spawnDaemon")
329
- .mockImplementation(async () => {
330
- attempts++;
331
- if (attempts === 2) return { pid: 1234 };
332
- return null;
333
- });
334
- // Isolate from real daemon state on the host machine
335
- const runningSpy = vi
336
- .spyOn(daemonProcess, "isDaemonRunning")
337
- .mockResolvedValue(false);
338
-
339
- const { createRespawnWithRetry } = await import("../lib/tier.js");
340
- const respawn = createRespawnWithRetry(".", undefined, undefined);
341
- const result = await respawn();
342
-
343
- expect(result).toBe(true);
344
- expect(attempts).toBe(2);
345
-
346
- spawnSpy.mockRestore();
347
- runningSpy.mockRestore();
348
- });
349
- });
350
-
351
- describe("TIERS constant", () => {
352
- it("has all 3 tier definitions", () => {
353
- expect(Object.keys(TIERS)).toHaveLength(3);
354
- expect(TIERS.full.number).toBe(1);
355
- expect(TIERS.lite.number).toBe(2);
356
- expect(TIERS.markdown.number).toBe(3);
357
- });
358
-
359
- it("each tier has name, label, and number", () => {
360
- for (const tier of Object.values(TIERS)) {
361
- expect(tier.name).toBeTruthy();
362
- expect(tier.label).toBeTruthy();
363
- expect(tier.number).toBeGreaterThanOrEqual(1);
364
- expect(tier.number).toBeLessThanOrEqual(3);
365
- }
366
- });
367
- });
@@ -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;