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.
- package/README.md +7 -2
- 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-DP6DFQ4G.js → chunk-BXSCYGWE.js} +2 -2
- package/dist/{chunk-W6D3J5DX.js → chunk-CGEMETKQ.js} +8 -6
- package/dist/{chunk-XEKQW4RN.js → chunk-ETTZRJ55.js} +1 -1
- package/dist/{chunk-IKLMHJH6.js → chunk-GIH6E7GJ.js} +62 -1
- package/dist/{chunk-6S7352JG.js → chunk-ONG4KRYP.js} +1 -1
- package/dist/{chunk-X2H7JJMR.js → chunk-PLSJW7QJ.js} +29 -1
- package/dist/{chunk-O5ZKLFSL.js → chunk-RCE4OWNX.js} +1 -1
- package/dist/{chunk-PI4P34KF.js → chunk-RFNKPAJW.js} +3 -1
- package/dist/{chunk-676YCW6T.js → chunk-XAYWGZQT.js} +101 -8
- package/dist/{clean-WEYQZB34.js → clean-YWEMYDE5.js} +2 -2
- package/dist/{config-ZMR2AOBB.js → config-NG7SICM4.js} +12 -24
- 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-SSRI4JZV.js → host-EV6QIVTI.js} +2 -2
- package/dist/index.js +59 -47
- package/dist/{init-6MFQWPOP.js → init-KYE5CJMW.js} +115 -12
- package/dist/{install-37NN4CQH.js → install-3YEPLDMD.js} +79 -6
- package/dist/{learn-OPH3CI67.js → learn-MC4VDHZH.js} +2 -2
- package/dist/{mcp-DWKVMNC7.js → mcp-FV6YX3ZT.js} +7 -7
- package/dist/{mcp-serve-5GODJ4DJ.js → mcp-serve-PXREYQRZ.js} +2 -2
- package/dist/{mcp-serve-learn-OF3ZJUJC.js → mcp-serve-learn-UVXIDOEZ.js} +1 -1
- package/dist/{memory-TRDLADUM.js → memory-DF7OUOTV.js} +6 -6
- package/dist/{permission-I3OFJUYA.js → permission-VISPGMUK.js} +38 -4
- package/dist/{permissions-J46VP6US.js → permissions-2SBACQHV.js} +2 -2
- package/dist/{session-S4WKA4P5.js → session-QK6OZO3X.js} +2 -2
- package/dist/{status-YZEOUX46.js → status-6HQRVZKR.js} +11 -10
- package/dist/{statusline-KKFJPTI7.js → statusline-AOZHEQSV.js} +2 -2
- package/dist/{uninstall-AVA3J554.js → uninstall-MF6FRUBO.js} +51 -4
- package/dist/{update-Q3ACCK4Z.js → update-SMWXKJ3R.js} +6 -5
- package/dist/{update-check-25SCLHZO.js → update-check-7LKJLJPM.js} +1 -1
- 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__/sqlite-lazy-db.test.ts +0 -213
- 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 -388
- 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
|
@@ -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
|
-
});
|
package/lib/learn-host/README.md
DELETED
|
@@ -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.
|