coding-friend-cli 1.26.0 → 1.27.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/{chunk-K2PSF7SF.js → chunk-BX3DRKTU.js} +7 -7
- package/dist/{chunk-HEBLANJO.js → chunk-JVYZ72EP.js} +81 -13
- package/dist/{chunk-WAFKFCAB.js → chunk-TOM2FI3W.js} +10 -10
- package/dist/{chunk-OARRVZL4.js → chunk-YCWRBOB3.js} +5 -5
- package/dist/{config-MSVSD3HA.js → config-N55LTJ76.js} +33 -20
- package/dist/{dev-KTEINOFU.js → dev-BSXR55PZ.js} +7 -7
- package/dist/{disable-7BVJGSIP.js → disable-CDOM7SSI.js} +3 -3
- package/dist/{enable-RO2FBKS6.js → enable-SF7742UR.js} +3 -3
- package/dist/{host-USZC44PR.js → host-JVLWYY5A.js} +2 -2
- package/dist/index.js +30 -30
- package/dist/{init-KPUEY6NC.js → init-QP57Q2C6.js} +43 -30
- package/dist/{install-65JCZGCB.js → install-G4XYNVL3.js} +6 -6
- package/dist/{mcp-V7PM4WTE.js → mcp-24NCFFAF.js} +5 -5
- package/dist/{memory-36YZTSSE.js → memory-HLMKDGWY.js} +5 -5
- package/dist/{permission-36BMHDYX.js → permission-K54DWLHN.js} +77 -10
- package/dist/permissions-O24R7WUU.js +37 -0
- package/dist/{session-CZ4ZJP7Q.js → session-DMQZXZ7F.js} +5 -5
- package/dist/{status-IJH4EZBG.js → status-K7TBUAXT.js} +16 -16
- package/dist/{statusline-PUPTHPBR.js → statusline-CDGADB3C.js} +2 -2
- package/dist/{uninstall-5MINSH35.js → uninstall-BOVKPNYN.js} +6 -6
- package/dist/{update-IO27DSZW.js → update-6M3YFHS6.js} +5 -5
- package/lib/cf-memory/README.md +2 -0
- package/lib/cf-memory/src/__tests__/claude-md.test.ts +238 -0
- package/lib/cf-memory/src/__tests__/status-frame.test.ts +136 -0
- package/lib/cf-memory/src/index.ts +9 -1
- package/lib/cf-memory/src/lib/claude-md.ts +217 -0
- package/lib/cf-memory/src/lib/status-frame.ts +63 -0
- package/lib/cf-memory/src/server.ts +9 -3
- package/lib/cf-memory/src/tools/delete.ts +12 -0
- package/lib/cf-memory/src/tools/store.ts +46 -10
- package/lib/cf-memory/src/tools/update.ts +54 -11
- package/lib/learn-mcp/package-lock.json +1385 -166
- package/lib/learn-mcp/package.json +5 -2
- package/lib/learn-mcp/src/__tests__/docs.test.ts +117 -0
- package/lib/learn-mcp/src/lib/docs.ts +23 -6
- package/lib/learn-mcp/src/tools/create-doc.ts +21 -9
- package/lib/learn-mcp/vitest.config.ts +8 -0
- package/package.json +1 -1
- package/dist/{chunk-KFNKH5Z7.js → chunk-2HQVNQB7.js} +3 -3
- package/dist/{chunk-3HTLOI34.js → chunk-GDZTU2Z4.js} +5 -5
- package/dist/{chunk-N2Q224HO.js → chunk-HPNRQYLM.js} +3 -3
- package/dist/{chunk-2TFHVNBI.js → chunk-R6HDMRYL.js} +4 -4
|
@@ -0,0 +1,238 @@
|
|
|
1
|
+
import { describe, it, expect, beforeEach, afterEach } from "vitest";
|
|
2
|
+
import { mkdirSync, rmSync, writeFileSync, readFileSync, existsSync } from "fs";
|
|
3
|
+
import { join } from "path";
|
|
4
|
+
import { tmpdir } from "os";
|
|
5
|
+
import {
|
|
6
|
+
syncToClaudeMd,
|
|
7
|
+
removeFromClaudeMd,
|
|
8
|
+
updateInClaudeMd,
|
|
9
|
+
claudeMdPath,
|
|
10
|
+
SECTION_HEADER,
|
|
11
|
+
} from "../lib/claude-md.js";
|
|
12
|
+
|
|
13
|
+
let testDir: string;
|
|
14
|
+
let claudeMdFile: string;
|
|
15
|
+
let counter = 0;
|
|
16
|
+
|
|
17
|
+
beforeEach(() => {
|
|
18
|
+
testDir = join(tmpdir(), `cf-claude-md-test-${Date.now()}-${++counter}`);
|
|
19
|
+
mkdirSync(testDir, { recursive: true });
|
|
20
|
+
claudeMdFile = join(testDir, "CLAUDE.md");
|
|
21
|
+
});
|
|
22
|
+
|
|
23
|
+
afterEach(() => {
|
|
24
|
+
rmSync(testDir, { recursive: true, force: true });
|
|
25
|
+
});
|
|
26
|
+
|
|
27
|
+
describe("syncToClaudeMd", () => {
|
|
28
|
+
it("creates CLAUDE.md with section if it does not exist", () => {
|
|
29
|
+
syncToClaudeMd(
|
|
30
|
+
testDir,
|
|
31
|
+
"conventions/code-style",
|
|
32
|
+
"Use 2-space indentation",
|
|
33
|
+
);
|
|
34
|
+
|
|
35
|
+
expect(existsSync(claudeMdFile)).toBe(true);
|
|
36
|
+
const content = readFileSync(claudeMdFile, "utf-8");
|
|
37
|
+
expect(content).toContain(SECTION_HEADER);
|
|
38
|
+
expect(content).toContain("Use 2-space indentation");
|
|
39
|
+
});
|
|
40
|
+
|
|
41
|
+
it("appends section to existing CLAUDE.md that has no memory section", () => {
|
|
42
|
+
writeFileSync(claudeMdFile, "# My Project\n\nSome existing content.\n");
|
|
43
|
+
|
|
44
|
+
syncToClaudeMd(
|
|
45
|
+
testDir,
|
|
46
|
+
"conventions/code-style",
|
|
47
|
+
"Use 2-space indentation",
|
|
48
|
+
);
|
|
49
|
+
|
|
50
|
+
const content = readFileSync(claudeMdFile, "utf-8");
|
|
51
|
+
expect(content).toContain("# My Project");
|
|
52
|
+
expect(content).toContain("Some existing content.");
|
|
53
|
+
expect(content).toContain(SECTION_HEADER);
|
|
54
|
+
expect(content).toContain("Use 2-space indentation");
|
|
55
|
+
});
|
|
56
|
+
|
|
57
|
+
it("adds entry to existing memory section", () => {
|
|
58
|
+
writeFileSync(
|
|
59
|
+
claudeMdFile,
|
|
60
|
+
`# My Project\n\n${SECTION_HEADER}\n\n- Use 2-space indentation\n`,
|
|
61
|
+
);
|
|
62
|
+
|
|
63
|
+
syncToClaudeMd(
|
|
64
|
+
testDir,
|
|
65
|
+
"conventions/naming",
|
|
66
|
+
"Use camelCase for variables",
|
|
67
|
+
);
|
|
68
|
+
|
|
69
|
+
const content = readFileSync(claudeMdFile, "utf-8");
|
|
70
|
+
expect(content).toContain("Use 2-space indentation");
|
|
71
|
+
expect(content).toContain("Use camelCase for variables");
|
|
72
|
+
});
|
|
73
|
+
|
|
74
|
+
it("does not duplicate entries with same ID", () => {
|
|
75
|
+
syncToClaudeMd(
|
|
76
|
+
testDir,
|
|
77
|
+
"conventions/code-style",
|
|
78
|
+
"Use 2-space indentation",
|
|
79
|
+
);
|
|
80
|
+
syncToClaudeMd(
|
|
81
|
+
testDir,
|
|
82
|
+
"conventions/code-style",
|
|
83
|
+
"Use 4-space indentation",
|
|
84
|
+
);
|
|
85
|
+
|
|
86
|
+
const content = readFileSync(claudeMdFile, "utf-8");
|
|
87
|
+
// Should have the updated version, not both
|
|
88
|
+
expect(content).toContain("Use 4-space indentation");
|
|
89
|
+
expect(content).not.toContain("Use 2-space indentation");
|
|
90
|
+
});
|
|
91
|
+
|
|
92
|
+
it("stores entry with ID as HTML comment for tracking", () => {
|
|
93
|
+
syncToClaudeMd(
|
|
94
|
+
testDir,
|
|
95
|
+
"conventions/code-style",
|
|
96
|
+
"Use 2-space indentation",
|
|
97
|
+
);
|
|
98
|
+
|
|
99
|
+
const content = readFileSync(claudeMdFile, "utf-8");
|
|
100
|
+
expect(content).toContain("<!-- cf:conventions/code-style -->");
|
|
101
|
+
});
|
|
102
|
+
|
|
103
|
+
it("handles multi-line description by taking first line only", () => {
|
|
104
|
+
syncToClaudeMd(
|
|
105
|
+
testDir,
|
|
106
|
+
"conventions/style",
|
|
107
|
+
"Line one\nLine two\nLine three",
|
|
108
|
+
);
|
|
109
|
+
|
|
110
|
+
const content = readFileSync(claudeMdFile, "utf-8");
|
|
111
|
+
expect(content).toContain("Line one");
|
|
112
|
+
// Multi-line content should be joined into a single bullet
|
|
113
|
+
expect(content.split(SECTION_HEADER)[1]).not.toContain("\nLine two");
|
|
114
|
+
});
|
|
115
|
+
|
|
116
|
+
it("preserves content after the memory section", () => {
|
|
117
|
+
writeFileSync(
|
|
118
|
+
claudeMdFile,
|
|
119
|
+
`# My Project\n\n${SECTION_HEADER}\n\n- Old entry <!-- cf:conventions/old -->\n\n## Other Section\n\nMore content.\n`,
|
|
120
|
+
);
|
|
121
|
+
|
|
122
|
+
syncToClaudeMd(testDir, "conventions/new-rule", "New convention");
|
|
123
|
+
|
|
124
|
+
const content = readFileSync(claudeMdFile, "utf-8");
|
|
125
|
+
expect(content).toContain("## Other Section");
|
|
126
|
+
expect(content).toContain("More content.");
|
|
127
|
+
expect(content).toContain("New convention");
|
|
128
|
+
});
|
|
129
|
+
});
|
|
130
|
+
|
|
131
|
+
describe("removeFromClaudeMd", () => {
|
|
132
|
+
it("removes an entry by ID", () => {
|
|
133
|
+
syncToClaudeMd(
|
|
134
|
+
testDir,
|
|
135
|
+
"conventions/code-style",
|
|
136
|
+
"Use 2-space indentation",
|
|
137
|
+
);
|
|
138
|
+
syncToClaudeMd(testDir, "conventions/naming", "Use camelCase");
|
|
139
|
+
|
|
140
|
+
removeFromClaudeMd(testDir, "conventions/code-style");
|
|
141
|
+
|
|
142
|
+
const content = readFileSync(claudeMdFile, "utf-8");
|
|
143
|
+
expect(content).not.toContain("Use 2-space indentation");
|
|
144
|
+
expect(content).not.toContain("conventions/code-style");
|
|
145
|
+
expect(content).toContain("Use camelCase");
|
|
146
|
+
});
|
|
147
|
+
|
|
148
|
+
it("does nothing if CLAUDE.md does not exist", () => {
|
|
149
|
+
// Should not throw
|
|
150
|
+
removeFromClaudeMd(testDir, "conventions/code-style");
|
|
151
|
+
expect(existsSync(claudeMdFile)).toBe(false);
|
|
152
|
+
});
|
|
153
|
+
|
|
154
|
+
it("does nothing if ID is not found", () => {
|
|
155
|
+
writeFileSync(
|
|
156
|
+
claudeMdFile,
|
|
157
|
+
`# Project\n\n${SECTION_HEADER}\n\n- Some entry <!-- cf:conventions/other -->\n`,
|
|
158
|
+
);
|
|
159
|
+
|
|
160
|
+
removeFromClaudeMd(testDir, "conventions/nonexistent");
|
|
161
|
+
|
|
162
|
+
const content = readFileSync(claudeMdFile, "utf-8");
|
|
163
|
+
expect(content).toContain("Some entry");
|
|
164
|
+
});
|
|
165
|
+
|
|
166
|
+
it("removes section header when last entry is removed", () => {
|
|
167
|
+
syncToClaudeMd(testDir, "conventions/only-one", "The only convention");
|
|
168
|
+
|
|
169
|
+
removeFromClaudeMd(testDir, "conventions/only-one");
|
|
170
|
+
|
|
171
|
+
const content = readFileSync(claudeMdFile, "utf-8");
|
|
172
|
+
expect(content).not.toContain(SECTION_HEADER);
|
|
173
|
+
});
|
|
174
|
+
});
|
|
175
|
+
|
|
176
|
+
describe("updateInClaudeMd", () => {
|
|
177
|
+
it("updates the description of an existing entry", () => {
|
|
178
|
+
syncToClaudeMd(testDir, "conventions/style", "Old style guide");
|
|
179
|
+
|
|
180
|
+
updateInClaudeMd(testDir, "conventions/style", "New style guide");
|
|
181
|
+
|
|
182
|
+
const content = readFileSync(claudeMdFile, "utf-8");
|
|
183
|
+
expect(content).toContain("New style guide");
|
|
184
|
+
expect(content).not.toContain("Old style guide");
|
|
185
|
+
});
|
|
186
|
+
|
|
187
|
+
it("does nothing if ID is not found (no upsert)", () => {
|
|
188
|
+
writeFileSync(claudeMdFile, "# Project\n");
|
|
189
|
+
|
|
190
|
+
updateInClaudeMd(testDir, "conventions/nonexistent", "Something");
|
|
191
|
+
|
|
192
|
+
const content = readFileSync(claudeMdFile, "utf-8");
|
|
193
|
+
expect(content).not.toContain("Something");
|
|
194
|
+
});
|
|
195
|
+
|
|
196
|
+
it("does nothing if CLAUDE.md does not exist", () => {
|
|
197
|
+
updateInClaudeMd(testDir, "conventions/style", "Something");
|
|
198
|
+
expect(existsSync(claudeMdFile)).toBe(false);
|
|
199
|
+
});
|
|
200
|
+
});
|
|
201
|
+
|
|
202
|
+
describe("claudeMdPath", () => {
|
|
203
|
+
it("strips /docs/memory suffix to derive project root", () => {
|
|
204
|
+
const docsDir = join(testDir, "docs", "memory");
|
|
205
|
+
mkdirSync(docsDir, { recursive: true });
|
|
206
|
+
|
|
207
|
+
const result = claudeMdPath(docsDir);
|
|
208
|
+
expect(result).toBe(join(testDir, "CLAUDE.md"));
|
|
209
|
+
});
|
|
210
|
+
|
|
211
|
+
it("strips /memory suffix as fallback", () => {
|
|
212
|
+
const docsDir = join(testDir, "memory");
|
|
213
|
+
mkdirSync(docsDir, { recursive: true });
|
|
214
|
+
|
|
215
|
+
const result = claudeMdPath(docsDir);
|
|
216
|
+
expect(result).toBe(join(testDir, "CLAUDE.md"));
|
|
217
|
+
});
|
|
218
|
+
|
|
219
|
+
it("uses docsDir as-is when no known suffix matches", () => {
|
|
220
|
+
const result = claudeMdPath(testDir);
|
|
221
|
+
expect(result).toBe(join(testDir, "CLAUDE.md"));
|
|
222
|
+
});
|
|
223
|
+
});
|
|
224
|
+
|
|
225
|
+
describe("syncToClaudeMd with realistic docsDir", () => {
|
|
226
|
+
it("writes CLAUDE.md to project root when docsDir is docs/memory", () => {
|
|
227
|
+
const docsDir = join(testDir, "docs", "memory");
|
|
228
|
+
mkdirSync(docsDir, { recursive: true });
|
|
229
|
+
|
|
230
|
+
syncToClaudeMd(docsDir, "conventions/style", "Use tabs");
|
|
231
|
+
|
|
232
|
+
const expectedPath = join(testDir, "CLAUDE.md");
|
|
233
|
+
expect(existsSync(expectedPath)).toBe(true);
|
|
234
|
+
const content = readFileSync(expectedPath, "utf-8");
|
|
235
|
+
expect(content).toContain("Use tabs");
|
|
236
|
+
expect(content).toContain(SECTION_HEADER);
|
|
237
|
+
});
|
|
238
|
+
});
|
|
@@ -0,0 +1,136 @@
|
|
|
1
|
+
import { describe, it, expect } from "vitest";
|
|
2
|
+
import { buildStoreStatus, buildUpdateStatus } from "../lib/status-frame.js";
|
|
3
|
+
|
|
4
|
+
describe("buildStoreStatus", () => {
|
|
5
|
+
it("returns a list with markdown and database info", () => {
|
|
6
|
+
const result = buildStoreStatus({
|
|
7
|
+
id: "features/auth-pattern",
|
|
8
|
+
title: "Auth Pattern",
|
|
9
|
+
markdownPath: "/docs/memory/features/auth-pattern.md",
|
|
10
|
+
dbPath: "/home/.coding-friend/memory/projects/-foo/db.sqlite",
|
|
11
|
+
});
|
|
12
|
+
|
|
13
|
+
expect(result).toContain("features/auth-pattern");
|
|
14
|
+
expect(result).toContain("Auth Pattern");
|
|
15
|
+
expect(result).toContain("/docs/memory/features/auth-pattern.md");
|
|
16
|
+
expect(result).toContain("db.sqlite");
|
|
17
|
+
// No box-drawing characters
|
|
18
|
+
expect(result).not.toContain("╭");
|
|
19
|
+
expect(result).not.toContain("╰");
|
|
20
|
+
expect(result).not.toContain("│");
|
|
21
|
+
});
|
|
22
|
+
|
|
23
|
+
it("shows markdown-only when dbPath is null", () => {
|
|
24
|
+
const result = buildStoreStatus({
|
|
25
|
+
id: "features/auth-pattern",
|
|
26
|
+
title: "Auth Pattern",
|
|
27
|
+
markdownPath: "/docs/memory/features/auth-pattern.md",
|
|
28
|
+
dbPath: null,
|
|
29
|
+
});
|
|
30
|
+
|
|
31
|
+
expect(result).toContain("/docs/memory/features/auth-pattern.md");
|
|
32
|
+
expect(result).not.toContain("db.sqlite");
|
|
33
|
+
});
|
|
34
|
+
|
|
35
|
+
it("shows CLAUDE.md updated message when claudeMdUpdated is true", () => {
|
|
36
|
+
const result = buildStoreStatus({
|
|
37
|
+
id: "conventions/code-style",
|
|
38
|
+
title: "Code Style",
|
|
39
|
+
markdownPath: "/docs/memory/conventions/code-style.md",
|
|
40
|
+
dbPath: null,
|
|
41
|
+
claudeMdUpdated: true,
|
|
42
|
+
});
|
|
43
|
+
|
|
44
|
+
expect(result).toContain("CLAUDE.md updated");
|
|
45
|
+
});
|
|
46
|
+
|
|
47
|
+
it("omits CLAUDE.md line when claudeMdUpdated is false", () => {
|
|
48
|
+
const result = buildStoreStatus({
|
|
49
|
+
id: "features/auth-pattern",
|
|
50
|
+
title: "Auth Pattern",
|
|
51
|
+
markdownPath: "/docs/memory/features/auth-pattern.md",
|
|
52
|
+
dbPath: null,
|
|
53
|
+
claudeMdUpdated: false,
|
|
54
|
+
});
|
|
55
|
+
|
|
56
|
+
expect(result).not.toContain("CLAUDE.md");
|
|
57
|
+
});
|
|
58
|
+
|
|
59
|
+
it("omits CLAUDE.md line when claudeMdUpdated is not provided", () => {
|
|
60
|
+
const result = buildStoreStatus({
|
|
61
|
+
id: "features/auth-pattern",
|
|
62
|
+
title: "Auth Pattern",
|
|
63
|
+
markdownPath: "/docs/memory/features/auth-pattern.md",
|
|
64
|
+
dbPath: null,
|
|
65
|
+
});
|
|
66
|
+
|
|
67
|
+
expect(result).not.toContain("CLAUDE.md");
|
|
68
|
+
});
|
|
69
|
+
|
|
70
|
+
it("includes duplicate warning when provided", () => {
|
|
71
|
+
const result = buildStoreStatus({
|
|
72
|
+
id: "features/auth-pattern",
|
|
73
|
+
title: "Auth Pattern",
|
|
74
|
+
markdownPath: "/docs/memory/features/auth-pattern.md",
|
|
75
|
+
dbPath: null,
|
|
76
|
+
warning: "Near-duplicate found: features/old-auth (similarity: 0.82)",
|
|
77
|
+
});
|
|
78
|
+
|
|
79
|
+
expect(result).toContain("Near-duplicate");
|
|
80
|
+
expect(result).toContain("⚠");
|
|
81
|
+
});
|
|
82
|
+
});
|
|
83
|
+
|
|
84
|
+
describe("buildUpdateStatus", () => {
|
|
85
|
+
it("returns a list with markdown and database info", () => {
|
|
86
|
+
const result = buildUpdateStatus({
|
|
87
|
+
id: "features/auth-pattern",
|
|
88
|
+
title: "Auth Pattern",
|
|
89
|
+
markdownPath: "/docs/memory/features/auth-pattern.md",
|
|
90
|
+
dbPath: "/home/.coding-friend/memory/projects/-foo/db.sqlite",
|
|
91
|
+
});
|
|
92
|
+
|
|
93
|
+
expect(result).toContain("features/auth-pattern");
|
|
94
|
+
expect(result).toContain("Auth Pattern");
|
|
95
|
+
expect(result).toContain("/docs/memory/features/auth-pattern.md");
|
|
96
|
+
expect(result).toContain("db.sqlite");
|
|
97
|
+
expect(result).not.toContain("╭");
|
|
98
|
+
expect(result).not.toContain("╰");
|
|
99
|
+
});
|
|
100
|
+
|
|
101
|
+
it("shows markdown-only when dbPath is null", () => {
|
|
102
|
+
const result = buildUpdateStatus({
|
|
103
|
+
id: "features/auth-pattern",
|
|
104
|
+
title: "Auth Pattern",
|
|
105
|
+
markdownPath: "/docs/memory/features/auth-pattern.md",
|
|
106
|
+
dbPath: null,
|
|
107
|
+
});
|
|
108
|
+
|
|
109
|
+
expect(result).toContain("/docs/memory/features/auth-pattern.md");
|
|
110
|
+
expect(result).not.toContain("db.sqlite");
|
|
111
|
+
});
|
|
112
|
+
|
|
113
|
+
it("shows CLAUDE.md updated message when claudeMdUpdated is true", () => {
|
|
114
|
+
const result = buildUpdateStatus({
|
|
115
|
+
id: "conventions/code-style",
|
|
116
|
+
title: "Code Style",
|
|
117
|
+
markdownPath: "/docs/memory/conventions/code-style.md",
|
|
118
|
+
dbPath: null,
|
|
119
|
+
claudeMdUpdated: true,
|
|
120
|
+
});
|
|
121
|
+
|
|
122
|
+
expect(result).toContain("CLAUDE.md updated");
|
|
123
|
+
});
|
|
124
|
+
|
|
125
|
+
it("omits CLAUDE.md line when claudeMdUpdated is false", () => {
|
|
126
|
+
const result = buildUpdateStatus({
|
|
127
|
+
id: "conventions/code-style",
|
|
128
|
+
title: "Code Style",
|
|
129
|
+
markdownPath: "/docs/memory/conventions/code-style.md",
|
|
130
|
+
dbPath: null,
|
|
131
|
+
claudeMdUpdated: false,
|
|
132
|
+
});
|
|
133
|
+
|
|
134
|
+
expect(result).not.toContain("CLAUDE.md");
|
|
135
|
+
});
|
|
136
|
+
});
|
|
@@ -65,7 +65,15 @@ const server = new McpServer({
|
|
|
65
65
|
version: cliPkg.version,
|
|
66
66
|
});
|
|
67
67
|
|
|
68
|
-
|
|
68
|
+
// Resolve DB path for status display
|
|
69
|
+
const dbPath = (() => {
|
|
70
|
+
if ("getDbPath" in backend && typeof backend.getDbPath === "function") {
|
|
71
|
+
return (backend as { getDbPath(): string }).getDbPath();
|
|
72
|
+
}
|
|
73
|
+
return null;
|
|
74
|
+
})();
|
|
75
|
+
|
|
76
|
+
registerAllTools(server, backend, { docsDir, dbPath });
|
|
69
77
|
registerAllResources(server, backend);
|
|
70
78
|
|
|
71
79
|
const transport = new StdioServerTransport();
|
|
@@ -0,0 +1,217 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Sync convention memories to the project's CLAUDE.md file.
|
|
3
|
+
*
|
|
4
|
+
* When a memory of type "preference" (category "conventions") is stored,
|
|
5
|
+
* updated, or deleted, the corresponding entry in CLAUDE.md is kept in sync.
|
|
6
|
+
*
|
|
7
|
+
* Entries are tracked via HTML comments: <!-- cf:<memory-id> -->
|
|
8
|
+
* All entries live under a dedicated section header.
|
|
9
|
+
*
|
|
10
|
+
* Note: The read-modify-write cycle assumes single-threaded MCP tool
|
|
11
|
+
* execution (sequential tool calls). No file-level locking is needed
|
|
12
|
+
* under this model.
|
|
13
|
+
*/
|
|
14
|
+
import fs from "node:fs";
|
|
15
|
+
import path from "node:path";
|
|
16
|
+
|
|
17
|
+
export const SECTION_HEADER = "## CF Memory: Conventions";
|
|
18
|
+
|
|
19
|
+
// Tested per-line via .split("\n"), not against the full document,
|
|
20
|
+
// so the $ anchor correctly matches end-of-line without the `m` flag.
|
|
21
|
+
const ENTRY_RE = /^- .+ <!-- cf:(.+?) -->$/;
|
|
22
|
+
|
|
23
|
+
/**
|
|
24
|
+
* Derive project root from docsDir by stripping known suffixes.
|
|
25
|
+
*
|
|
26
|
+
* Expected docsDir formats:
|
|
27
|
+
* - `<project>/docs/memory` (standard layout, preferred)
|
|
28
|
+
* - `<project>/memory` (fallback for non-standard layouts)
|
|
29
|
+
* - Any other path is used as-is (CLAUDE.md written alongside docsDir)
|
|
30
|
+
*/
|
|
31
|
+
function projectRoot(docsDir: string): string {
|
|
32
|
+
let root = path.resolve(docsDir);
|
|
33
|
+
root = root.replace(/\/docs\/memory$/, "").replace(/\/memory$/, "");
|
|
34
|
+
return root;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
/**
|
|
38
|
+
* Resolve the CLAUDE.md path for a given docsDir.
|
|
39
|
+
* Exported so tool handlers can display the path in status frames
|
|
40
|
+
* without reimplementing the projectRoot derivation.
|
|
41
|
+
*/
|
|
42
|
+
export function claudeMdPath(docsDir: string): string {
|
|
43
|
+
return path.join(projectRoot(docsDir), "CLAUDE.md");
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
/**
|
|
47
|
+
* Parse a CLAUDE.md file into: before the section, section entries, and after the section.
|
|
48
|
+
*/
|
|
49
|
+
function parseClaudeMd(content: string): {
|
|
50
|
+
before: string;
|
|
51
|
+
entries: string[];
|
|
52
|
+
after: string;
|
|
53
|
+
untrackedLines: string[];
|
|
54
|
+
} {
|
|
55
|
+
const headerIdx = content.indexOf(SECTION_HEADER);
|
|
56
|
+
if (headerIdx === -1) {
|
|
57
|
+
return { before: content, entries: [], after: "", untrackedLines: [] };
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
const before = content.slice(0, headerIdx);
|
|
61
|
+
const rest = content.slice(headerIdx + SECTION_HEADER.length);
|
|
62
|
+
|
|
63
|
+
// Find the next section header (## ) to determine where our section ends
|
|
64
|
+
const nextSectionMatch = rest.match(/\n(## )/);
|
|
65
|
+
let sectionBody: string;
|
|
66
|
+
let after: string;
|
|
67
|
+
|
|
68
|
+
if (nextSectionMatch && nextSectionMatch.index !== undefined) {
|
|
69
|
+
sectionBody = rest.slice(0, nextSectionMatch.index);
|
|
70
|
+
after = rest.slice(nextSectionMatch.index + 1); // +1 to skip the \n
|
|
71
|
+
} else {
|
|
72
|
+
sectionBody = rest;
|
|
73
|
+
after = "";
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
// Extract tracked entries (lines matching "- ... <!-- cf:... -->")
|
|
77
|
+
// and preserve untracked lines within the section
|
|
78
|
+
const lines = sectionBody.split("\n");
|
|
79
|
+
const entries: string[] = [];
|
|
80
|
+
const untrackedLines: string[] = [];
|
|
81
|
+
|
|
82
|
+
for (const line of lines) {
|
|
83
|
+
if (ENTRY_RE.test(line)) {
|
|
84
|
+
entries.push(line);
|
|
85
|
+
} else if (line.trim()) {
|
|
86
|
+
untrackedLines.push(line);
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
return { before, entries, after, untrackedLines };
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
/**
|
|
94
|
+
* Rebuild the CLAUDE.md content from its parts.
|
|
95
|
+
*/
|
|
96
|
+
function buildClaudeMd(
|
|
97
|
+
before: string,
|
|
98
|
+
entries: string[],
|
|
99
|
+
after: string,
|
|
100
|
+
untrackedLines: string[] = [],
|
|
101
|
+
): string {
|
|
102
|
+
// If no entries and no untracked lines, remove the section entirely
|
|
103
|
+
if (entries.length === 0 && untrackedLines.length === 0) {
|
|
104
|
+
const result = before.trimEnd() + (after ? "\n\n" + after : "\n");
|
|
105
|
+
return result;
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
let result = before.trimEnd() + "\n\n" + SECTION_HEADER + "\n\n";
|
|
109
|
+
if (untrackedLines.length > 0) {
|
|
110
|
+
result += untrackedLines.join("\n") + "\n";
|
|
111
|
+
}
|
|
112
|
+
if (entries.length > 0) {
|
|
113
|
+
result += entries.join("\n") + "\n";
|
|
114
|
+
}
|
|
115
|
+
if (after) {
|
|
116
|
+
result += "\n" + after;
|
|
117
|
+
}
|
|
118
|
+
return result;
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
/**
|
|
122
|
+
* Format a concise single-line entry for CLAUDE.md.
|
|
123
|
+
*/
|
|
124
|
+
function formatEntry(id: string, description: string): string {
|
|
125
|
+
// Take only first line and trim
|
|
126
|
+
const oneLiner = description.split("\n")[0].trim();
|
|
127
|
+
return `- ${oneLiner} <!-- cf:${id} -->`;
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
/**
|
|
131
|
+
* Add or update a convention entry in the project's CLAUDE.md.
|
|
132
|
+
*/
|
|
133
|
+
export function syncToClaudeMd(
|
|
134
|
+
docsDir: string,
|
|
135
|
+
id: string,
|
|
136
|
+
description: string,
|
|
137
|
+
): void {
|
|
138
|
+
const filePath = claudeMdPath(docsDir);
|
|
139
|
+
const existing = fs.existsSync(filePath)
|
|
140
|
+
? fs.readFileSync(filePath, "utf-8")
|
|
141
|
+
: "";
|
|
142
|
+
|
|
143
|
+
const { before, entries, after, untrackedLines } = parseClaudeMd(existing);
|
|
144
|
+
|
|
145
|
+
// Remove any existing entry with the same ID
|
|
146
|
+
const filtered = entries.filter((e) => {
|
|
147
|
+
const match = e.match(ENTRY_RE);
|
|
148
|
+
return !match || match[1] !== id;
|
|
149
|
+
});
|
|
150
|
+
|
|
151
|
+
// Add the new/updated entry
|
|
152
|
+
filtered.push(formatEntry(id, description));
|
|
153
|
+
|
|
154
|
+
fs.writeFileSync(
|
|
155
|
+
filePath,
|
|
156
|
+
buildClaudeMd(before, filtered, after, untrackedLines),
|
|
157
|
+
"utf-8",
|
|
158
|
+
);
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
/**
|
|
162
|
+
* Remove a convention entry from the project's CLAUDE.md.
|
|
163
|
+
*/
|
|
164
|
+
export function removeFromClaudeMd(docsDir: string, id: string): void {
|
|
165
|
+
const filePath = claudeMdPath(docsDir);
|
|
166
|
+
if (!fs.existsSync(filePath)) return;
|
|
167
|
+
|
|
168
|
+
const content = fs.readFileSync(filePath, "utf-8");
|
|
169
|
+
const { before, entries, after, untrackedLines } = parseClaudeMd(content);
|
|
170
|
+
|
|
171
|
+
if (entries.length === 0) return;
|
|
172
|
+
|
|
173
|
+
const filtered = entries.filter((e) => {
|
|
174
|
+
const match = e.match(ENTRY_RE);
|
|
175
|
+
return !match || match[1] !== id;
|
|
176
|
+
});
|
|
177
|
+
|
|
178
|
+
// Only write if something actually changed
|
|
179
|
+
if (filtered.length === entries.length) return;
|
|
180
|
+
|
|
181
|
+
fs.writeFileSync(
|
|
182
|
+
filePath,
|
|
183
|
+
buildClaudeMd(before, filtered, after, untrackedLines),
|
|
184
|
+
"utf-8",
|
|
185
|
+
);
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
/**
|
|
189
|
+
* Update an existing convention entry in CLAUDE.md (no upsert).
|
|
190
|
+
*/
|
|
191
|
+
export function updateInClaudeMd(
|
|
192
|
+
docsDir: string,
|
|
193
|
+
id: string,
|
|
194
|
+
description: string,
|
|
195
|
+
): void {
|
|
196
|
+
const filePath = claudeMdPath(docsDir);
|
|
197
|
+
if (!fs.existsSync(filePath)) return;
|
|
198
|
+
|
|
199
|
+
const content = fs.readFileSync(filePath, "utf-8");
|
|
200
|
+
const { before, entries, after, untrackedLines } = parseClaudeMd(content);
|
|
201
|
+
|
|
202
|
+
// Check if entry exists
|
|
203
|
+
const idx = entries.findIndex((e) => {
|
|
204
|
+
const match = e.match(ENTRY_RE);
|
|
205
|
+
return match && match[1] === id;
|
|
206
|
+
});
|
|
207
|
+
|
|
208
|
+
if (idx === -1) return;
|
|
209
|
+
|
|
210
|
+
entries[idx] = formatEntry(id, description);
|
|
211
|
+
|
|
212
|
+
fs.writeFileSync(
|
|
213
|
+
filePath,
|
|
214
|
+
buildClaudeMd(before, entries, after, untrackedLines),
|
|
215
|
+
"utf-8",
|
|
216
|
+
);
|
|
217
|
+
}
|
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Build status output for memory store/update operations.
|
|
3
|
+
* Shows what was saved (markdown file, database) as a simple list with icons.
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
interface StoreStatusInput {
|
|
7
|
+
id: string;
|
|
8
|
+
title: string;
|
|
9
|
+
markdownPath: string;
|
|
10
|
+
dbPath: string | null;
|
|
11
|
+
claudeMdUpdated?: boolean;
|
|
12
|
+
warning?: string;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
interface UpdateStatusInput {
|
|
16
|
+
id: string;
|
|
17
|
+
title: string;
|
|
18
|
+
markdownPath: string;
|
|
19
|
+
dbPath: string | null;
|
|
20
|
+
claudeMdUpdated?: boolean;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
export function buildStoreStatus(input: StoreStatusInput): string {
|
|
24
|
+
const lines: string[] = [
|
|
25
|
+
`✅ Memory stored`,
|
|
26
|
+
` ID: ${input.id}`,
|
|
27
|
+
` Title: ${input.title}`,
|
|
28
|
+
`📄 Markdown: ${input.markdownPath}`,
|
|
29
|
+
];
|
|
30
|
+
|
|
31
|
+
if (input.dbPath) {
|
|
32
|
+
lines.push(`🗄️ Database: ${input.dbPath}`);
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
if (input.claudeMdUpdated) {
|
|
36
|
+
lines.push(`📋 CLAUDE.md updated`);
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
if (input.warning) {
|
|
40
|
+
lines.push(`⚠ ${input.warning}`);
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
return lines.join("\n");
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
export function buildUpdateStatus(input: UpdateStatusInput): string {
|
|
47
|
+
const lines: string[] = [
|
|
48
|
+
`✅ Memory updated`,
|
|
49
|
+
` ID: ${input.id}`,
|
|
50
|
+
` Title: ${input.title}`,
|
|
51
|
+
`📄 Markdown: ${input.markdownPath}`,
|
|
52
|
+
];
|
|
53
|
+
|
|
54
|
+
if (input.dbPath) {
|
|
55
|
+
lines.push(`🗄️ Database: ${input.dbPath}`);
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
if (input.claudeMdUpdated) {
|
|
59
|
+
lines.push(`📋 CLAUDE.md updated`);
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
return lines.join("\n");
|
|
63
|
+
}
|
|
@@ -7,14 +7,20 @@ import { registerList } from "./tools/list.js";
|
|
|
7
7
|
import { registerUpdate } from "./tools/update.js";
|
|
8
8
|
import { registerDelete } from "./tools/delete.js";
|
|
9
9
|
|
|
10
|
+
export interface ToolRegistrationContext {
|
|
11
|
+
docsDir: string;
|
|
12
|
+
dbPath?: string | null;
|
|
13
|
+
}
|
|
14
|
+
|
|
10
15
|
export function registerAllTools(
|
|
11
16
|
server: McpServer,
|
|
12
17
|
backend: MemoryBackend,
|
|
18
|
+
ctx?: ToolRegistrationContext,
|
|
13
19
|
): void {
|
|
14
|
-
registerStore(server, backend);
|
|
20
|
+
registerStore(server, backend, ctx);
|
|
15
21
|
registerSearch(server, backend);
|
|
16
22
|
registerRetrieve(server, backend);
|
|
17
23
|
registerList(server, backend);
|
|
18
|
-
registerUpdate(server, backend);
|
|
19
|
-
registerDelete(server, backend);
|
|
24
|
+
registerUpdate(server, backend, ctx);
|
|
25
|
+
registerDelete(server, backend, ctx);
|
|
20
26
|
}
|