coding-friend-cli 1.16.0 → 1.17.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (75) hide show
  1. package/README.md +12 -0
  2. package/dist/{chunk-D4EWPGBL.js → chunk-C5LYVVEI.js} +1 -1
  3. package/dist/{chunk-X5WEODUD.js → chunk-CYQU33FY.js} +1 -0
  4. package/dist/{chunk-QNLL3ZDF.js → chunk-G6CEEMAR.js} +3 -3
  5. package/dist/{chunk-4DB4XTSL.js → chunk-KTX4MGMR.js} +15 -1
  6. package/dist/{chunk-KJUGTLPQ.js → chunk-YO6JKGR3.js} +38 -2
  7. package/dist/{config-AIZJJ5D2.js → config-LZFXXOI4.js} +276 -14
  8. package/dist/{dev-WJ5QQ35B.js → dev-R3IYWZ3M.js} +2 -2
  9. package/dist/{disable-JDVOQNZG.js → disable-R6K5YJN4.js} +2 -2
  10. package/dist/{enable-JBJ4Q2S7.js → enable-HF4PYVJN.js} +2 -2
  11. package/dist/{host-NA7LZ4HX.js → host-SYZH3FVC.js} +4 -4
  12. package/dist/index.js +78 -18
  13. package/dist/{init-FZ3GG53E.js → init-YK6YRTOT.js} +102 -6
  14. package/dist/{install-I3GOS56Q.js → install-Q4PWEU43.js} +4 -4
  15. package/dist/{mcp-DLS3J6QJ.js → mcp-TBEDYELW.js} +4 -4
  16. package/dist/memory-7RM67ZLS.js +668 -0
  17. package/dist/postinstall.js +1 -1
  18. package/dist/{session-E3CZJJZQ.js → session-H4XW2WXH.js} +1 -1
  19. package/dist/{statusline-6HQCDWBD.js → statusline-6Y2EBAFQ.js} +1 -1
  20. package/dist/{uninstall-JN5YIKKM.js → uninstall-3PSUDGI4.js} +3 -3
  21. package/dist/{update-OWS4IJTG.js → update-WL6SFGGO.js} +4 -4
  22. package/lib/cf-memory/CHANGELOG.md +15 -0
  23. package/lib/cf-memory/README.md +284 -0
  24. package/lib/cf-memory/package-lock.json +2790 -0
  25. package/lib/cf-memory/package.json +31 -0
  26. package/lib/cf-memory/scripts/migrate-frontmatter.ts +134 -0
  27. package/lib/cf-memory/src/__tests__/daemon-e2e.test.ts +223 -0
  28. package/lib/cf-memory/src/__tests__/daemon.test.ts +407 -0
  29. package/lib/cf-memory/src/__tests__/dedup.test.ts +103 -0
  30. package/lib/cf-memory/src/__tests__/embeddings.test.ts +292 -0
  31. package/lib/cf-memory/src/__tests__/lazy-install.test.ts +210 -0
  32. package/lib/cf-memory/src/__tests__/markdown-backend.test.ts +410 -0
  33. package/lib/cf-memory/src/__tests__/migration.test.ts +255 -0
  34. package/lib/cf-memory/src/__tests__/migrations.test.ts +288 -0
  35. package/lib/cf-memory/src/__tests__/minisearch-backend.test.ts +262 -0
  36. package/lib/cf-memory/src/__tests__/ollama.test.ts +48 -0
  37. package/lib/cf-memory/src/__tests__/schema.test.ts +128 -0
  38. package/lib/cf-memory/src/__tests__/search.test.ts +115 -0
  39. package/lib/cf-memory/src/__tests__/temporal-decay.test.ts +54 -0
  40. package/lib/cf-memory/src/__tests__/tier.test.ts +293 -0
  41. package/lib/cf-memory/src/__tests__/tools.test.ts +83 -0
  42. package/lib/cf-memory/src/backends/markdown.ts +318 -0
  43. package/lib/cf-memory/src/backends/minisearch.ts +203 -0
  44. package/lib/cf-memory/src/backends/sqlite/embeddings.ts +286 -0
  45. package/lib/cf-memory/src/backends/sqlite/index.ts +549 -0
  46. package/lib/cf-memory/src/backends/sqlite/migrations.ts +188 -0
  47. package/lib/cf-memory/src/backends/sqlite/schema.ts +120 -0
  48. package/lib/cf-memory/src/backends/sqlite/search.ts +296 -0
  49. package/lib/cf-memory/src/bin/cf-memory.ts +2 -0
  50. package/lib/cf-memory/src/daemon/entry.ts +99 -0
  51. package/lib/cf-memory/src/daemon/process.ts +220 -0
  52. package/lib/cf-memory/src/daemon/server.ts +166 -0
  53. package/lib/cf-memory/src/daemon/watcher.ts +90 -0
  54. package/lib/cf-memory/src/index.ts +45 -0
  55. package/lib/cf-memory/src/lib/backend.ts +23 -0
  56. package/lib/cf-memory/src/lib/daemon-client.ts +163 -0
  57. package/lib/cf-memory/src/lib/dedup.ts +80 -0
  58. package/lib/cf-memory/src/lib/lazy-install.ts +274 -0
  59. package/lib/cf-memory/src/lib/ollama.ts +76 -0
  60. package/lib/cf-memory/src/lib/temporal-decay.ts +19 -0
  61. package/lib/cf-memory/src/lib/tier.ts +107 -0
  62. package/lib/cf-memory/src/lib/types.ts +109 -0
  63. package/lib/cf-memory/src/resources/index.ts +62 -0
  64. package/lib/cf-memory/src/server.ts +20 -0
  65. package/lib/cf-memory/src/tools/delete.ts +38 -0
  66. package/lib/cf-memory/src/tools/list.ts +38 -0
  67. package/lib/cf-memory/src/tools/retrieve.ts +52 -0
  68. package/lib/cf-memory/src/tools/search.ts +47 -0
  69. package/lib/cf-memory/src/tools/store.ts +70 -0
  70. package/lib/cf-memory/src/tools/update.ts +62 -0
  71. package/lib/cf-memory/tsconfig.json +15 -0
  72. package/lib/cf-memory/vitest.config.ts +7 -0
  73. package/lib/learn-host/CHANGELOG.md +4 -0
  74. package/lib/learn-host/package.json +1 -1
  75. package/package.json +1 -1
@@ -0,0 +1,318 @@
1
+ import fs from "node:fs";
2
+ import path from "node:path";
3
+ import matter from "gray-matter";
4
+ import type { MemoryBackend } from "../lib/backend.js";
5
+ import {
6
+ CATEGORY_TO_TYPE,
7
+ MEMORY_CATEGORIES,
8
+ makeExcerpt,
9
+ type ListInput,
10
+ type Memory,
11
+ type MemoryFrontmatter,
12
+ type MemoryMeta,
13
+ type MemoryStats,
14
+ type MemoryType,
15
+ type SearchInput,
16
+ type SearchResult,
17
+ type StoreInput,
18
+ type UpdateInput,
19
+ } from "../lib/types.js";
20
+
21
+ /**
22
+ * Validate that a resolved path stays within docsDir to prevent path traversal.
23
+ */
24
+ function safePath(docsDir: string, ...segments: string[]): string | null {
25
+ const resolved = path.resolve(docsDir, ...segments);
26
+ if (
27
+ !resolved.startsWith(path.resolve(docsDir) + path.sep) &&
28
+ resolved !== path.resolve(docsDir)
29
+ ) {
30
+ return null;
31
+ }
32
+ return resolved;
33
+ }
34
+
35
+ function slugify(title: string): string {
36
+ return title
37
+ .toLowerCase()
38
+ .replace(/[^a-z0-9]+/g, "-")
39
+ .replace(/^-|-$/g, "");
40
+ }
41
+
42
+ function parseFrontmatter(
43
+ raw: matter.GrayMatterFile<string>,
44
+ category: string,
45
+ ): MemoryFrontmatter {
46
+ const d = raw.data;
47
+ return {
48
+ title: String(d.title ?? "Untitled"),
49
+ description: String(d.description ?? ""),
50
+ type: (d.type as MemoryType) ?? CATEGORY_TO_TYPE[category] ?? "fact",
51
+ tags: Array.isArray(d.tags) ? d.tags.map(String) : [],
52
+ importance: Number(d.importance ?? 3),
53
+ created: String(d.created ?? ""),
54
+ updated: String(d.updated ?? ""),
55
+ source: String(d.source ?? "conversation"),
56
+ };
57
+ }
58
+
59
+ function today(): string {
60
+ return new Date().toISOString().split("T")[0];
61
+ }
62
+
63
+ export class MarkdownBackend implements MemoryBackend {
64
+ constructor(private docsDir: string) {}
65
+
66
+ private getCategories(): string[] {
67
+ if (!fs.existsSync(this.docsDir)) return [];
68
+ return fs
69
+ .readdirSync(this.docsDir, { withFileTypes: true })
70
+ .filter((d) => d.isDirectory() && !d.name.startsWith("."))
71
+ .map((d) => d.name);
72
+ }
73
+
74
+ getAllMeta(): MemoryMeta[] {
75
+ const metas: MemoryMeta[] = [];
76
+
77
+ for (const category of this.getCategories()) {
78
+ const catPath = path.join(this.docsDir, category);
79
+ const files = fs
80
+ .readdirSync(catPath)
81
+ .filter((f) => f.endsWith(".md") && f !== "README.md");
82
+
83
+ for (const file of files) {
84
+ const filePath = path.join(catPath, file);
85
+ const raw = matter(fs.readFileSync(filePath, "utf-8"));
86
+ const frontmatter = parseFrontmatter(raw, category);
87
+ const slug = path.basename(file, ".md");
88
+
89
+ metas.push({
90
+ id: `${category}/${slug}`,
91
+ slug,
92
+ category,
93
+ frontmatter,
94
+ excerpt: makeExcerpt(raw.content),
95
+ });
96
+ }
97
+ }
98
+
99
+ return metas.sort(
100
+ (a, b) =>
101
+ new Date(b.frontmatter.updated || b.frontmatter.created).getTime() -
102
+ new Date(a.frontmatter.updated || a.frontmatter.created).getTime(),
103
+ );
104
+ }
105
+
106
+ async store(input: StoreInput): Promise<Memory> {
107
+ const category = MEMORY_CATEGORIES[input.type];
108
+ const catDir = path.join(this.docsDir, category);
109
+ if (!fs.existsSync(catDir)) {
110
+ fs.mkdirSync(catDir, { recursive: true });
111
+ }
112
+
113
+ let slug = slugify(input.title);
114
+ const filePath = path.join(catDir, `${slug}.md`);
115
+
116
+ // Handle duplicate slugs
117
+ if (fs.existsSync(filePath)) {
118
+ slug = `${slug}-${Date.now()}`;
119
+ }
120
+
121
+ const finalPath = path.join(catDir, `${slug}.md`);
122
+ const now = today();
123
+
124
+ const frontmatter: MemoryFrontmatter = {
125
+ title: input.title,
126
+ description: input.description,
127
+ type: input.type,
128
+ tags: input.tags,
129
+ importance: input.importance ?? 3,
130
+ created: now,
131
+ updated: now,
132
+ source: input.source ?? "conversation",
133
+ };
134
+
135
+ const doc = matter.stringify(
136
+ input.content,
137
+ frontmatter as unknown as Record<string, unknown>,
138
+ );
139
+ fs.writeFileSync(finalPath, doc, "utf-8");
140
+
141
+ return {
142
+ id: `${category}/${slug}`,
143
+ slug,
144
+ category,
145
+ frontmatter,
146
+ content: input.content,
147
+ };
148
+ }
149
+
150
+ async search(input: SearchInput): Promise<SearchResult[]> {
151
+ const query = input.query.toLowerCase();
152
+ let metas = this.getAllMeta();
153
+
154
+ if (input.type) {
155
+ metas = metas.filter((m) => m.frontmatter.type === input.type);
156
+ }
157
+
158
+ if (input.tags && input.tags.length > 0) {
159
+ const lowerTags = input.tags.map((t) => t.toLowerCase());
160
+ metas = metas.filter((m) =>
161
+ lowerTags.some((lt) =>
162
+ m.frontmatter.tags.some((t) => t.toLowerCase() === lt),
163
+ ),
164
+ );
165
+ }
166
+
167
+ const results: SearchResult[] = [];
168
+
169
+ for (const meta of metas) {
170
+ const matchedOn: string[] = [];
171
+ let score = 0;
172
+
173
+ // Title match (highest weight)
174
+ if (meta.frontmatter.title.toLowerCase().includes(query)) {
175
+ matchedOn.push("title");
176
+ score += 10;
177
+ }
178
+
179
+ // Description match
180
+ if (meta.frontmatter.description.toLowerCase().includes(query)) {
181
+ matchedOn.push("description");
182
+ score += 8;
183
+ }
184
+
185
+ // Tag match
186
+ if (meta.frontmatter.tags.some((t) => t.toLowerCase().includes(query))) {
187
+ matchedOn.push("tags");
188
+ score += 6;
189
+ }
190
+
191
+ // Content match (full file read — heavier)
192
+ if (matchedOn.length === 0) {
193
+ const full = await this.retrieve(meta.id);
194
+ if (full && full.content.toLowerCase().includes(query)) {
195
+ matchedOn.push("content");
196
+ score += 2;
197
+ }
198
+ }
199
+
200
+ if (matchedOn.length > 0) {
201
+ results.push({ memory: meta, score, matchedOn });
202
+ }
203
+ }
204
+
205
+ results.sort((a, b) => b.score - a.score);
206
+
207
+ const limit = input.limit ?? 10;
208
+ return results.slice(0, limit);
209
+ }
210
+
211
+ retrieveSync(id: string): Memory | null {
212
+ const [category, slug] = id.split("/");
213
+ if (!category || !slug) return null;
214
+
215
+ const filePath = safePath(this.docsDir, category, `${slug}.md`);
216
+ if (!filePath || !fs.existsSync(filePath)) return null;
217
+
218
+ const raw = matter(fs.readFileSync(filePath, "utf-8"));
219
+ const frontmatter = parseFrontmatter(raw, category);
220
+
221
+ return {
222
+ id,
223
+ slug,
224
+ category,
225
+ frontmatter,
226
+ content: raw.content,
227
+ };
228
+ }
229
+
230
+ async retrieve(id: string): Promise<Memory | null> {
231
+ return this.retrieveSync(id);
232
+ }
233
+
234
+ async list(input: ListInput): Promise<MemoryMeta[]> {
235
+ let metas = this.getAllMeta();
236
+
237
+ if (input.type) {
238
+ metas = metas.filter((m) => m.frontmatter.type === input.type);
239
+ }
240
+
241
+ if (input.category) {
242
+ metas = metas.filter((m) => m.category === input.category);
243
+ }
244
+
245
+ const limit = input.limit ?? 50;
246
+ return metas.slice(0, limit);
247
+ }
248
+
249
+ async update(input: UpdateInput): Promise<Memory | null> {
250
+ const [category, slug] = input.id.split("/");
251
+ if (!category || !slug) return null;
252
+
253
+ const filePath = safePath(this.docsDir, category, `${slug}.md`);
254
+ if (!filePath || !fs.existsSync(filePath)) return null;
255
+
256
+ const raw = matter(fs.readFileSync(filePath, "utf-8"));
257
+ const now = today();
258
+
259
+ if (input.title) raw.data.title = input.title;
260
+ if (input.description) raw.data.description = input.description;
261
+ if (input.tags) {
262
+ raw.data.tags = [...new Set([...(raw.data.tags || []), ...input.tags])];
263
+ }
264
+ if (input.importance !== undefined) raw.data.importance = input.importance;
265
+ raw.data.updated = now;
266
+
267
+ const newContent = input.content
268
+ ? raw.content + "\n\n" + input.content
269
+ : raw.content;
270
+
271
+ fs.writeFileSync(filePath, matter.stringify(newContent, raw.data), "utf-8");
272
+
273
+ const frontmatter = parseFrontmatter(
274
+ matter(fs.readFileSync(filePath, "utf-8")),
275
+ category,
276
+ );
277
+
278
+ return {
279
+ id: input.id,
280
+ slug,
281
+ category,
282
+ frontmatter,
283
+ content: newContent,
284
+ };
285
+ }
286
+
287
+ async delete(id: string): Promise<boolean> {
288
+ const [category, slug] = id.split("/");
289
+ if (!category || !slug) return false;
290
+
291
+ const filePath = safePath(this.docsDir, category, `${slug}.md`);
292
+ if (!filePath || !fs.existsSync(filePath)) return false;
293
+
294
+ fs.unlinkSync(filePath);
295
+ return true;
296
+ }
297
+
298
+ async stats(): Promise<MemoryStats> {
299
+ const metas = this.getAllMeta();
300
+ const byCategory: Record<string, number> = {};
301
+ const byType: Record<string, number> = {};
302
+
303
+ for (const meta of metas) {
304
+ byCategory[meta.category] = (byCategory[meta.category] ?? 0) + 1;
305
+ byType[meta.frontmatter.type] = (byType[meta.frontmatter.type] ?? 0) + 1;
306
+ }
307
+
308
+ return {
309
+ total: metas.length,
310
+ byCategory,
311
+ byType,
312
+ };
313
+ }
314
+
315
+ async close(): Promise<void> {
316
+ // No-op for markdown backend
317
+ }
318
+ }
@@ -0,0 +1,203 @@
1
+ import MiniSearch from "minisearch";
2
+ import { MarkdownBackend } from "./markdown.js";
3
+ import type { MemoryBackend } from "../lib/backend.js";
4
+ import type {
5
+ ListInput,
6
+ Memory,
7
+ MemoryMeta,
8
+ MemoryStats,
9
+ SearchInput,
10
+ SearchResult,
11
+ StoreInput,
12
+ UpdateInput,
13
+ } from "../lib/types.js";
14
+
15
+ interface IndexedDoc {
16
+ id: string;
17
+ title: string;
18
+ description: string;
19
+ tags: string;
20
+ content: string;
21
+ type: string;
22
+ category: string;
23
+ importance: number;
24
+ }
25
+
26
+ export class MiniSearchBackend implements MemoryBackend {
27
+ private markdown: MarkdownBackend;
28
+ private index: MiniSearch<IndexedDoc>;
29
+
30
+ constructor(docsDir: string) {
31
+ this.markdown = new MarkdownBackend(docsDir);
32
+ this.index = this.createIndex();
33
+ this.buildIndex();
34
+ }
35
+
36
+ private createIndex(): MiniSearch<IndexedDoc> {
37
+ return new MiniSearch<IndexedDoc>({
38
+ fields: ["title", "description", "tags", "content"],
39
+ storeFields: [
40
+ "id",
41
+ "title",
42
+ "description",
43
+ "tags",
44
+ "type",
45
+ "category",
46
+ "importance",
47
+ ],
48
+ searchOptions: {
49
+ boost: { title: 10, tags: 6, description: 4, content: 1 },
50
+ fuzzy: 0.2,
51
+ prefix: true,
52
+ },
53
+ });
54
+ }
55
+
56
+ private buildIndex(): void {
57
+ this.index.removeAll();
58
+ const metas = this.markdown.getAllMeta();
59
+ for (const meta of metas) {
60
+ const full = this.markdown.retrieveSync(meta.id);
61
+ this.index.add({
62
+ id: meta.id,
63
+ title: meta.frontmatter.title,
64
+ description: meta.frontmatter.description,
65
+ tags: meta.frontmatter.tags.join(" "),
66
+ content: full?.content ?? "",
67
+ type: meta.frontmatter.type,
68
+ category: meta.category,
69
+ importance: meta.frontmatter.importance,
70
+ });
71
+ }
72
+ }
73
+
74
+ /**
75
+ * Rebuild the in-memory index from markdown files.
76
+ */
77
+ async rebuild(): Promise<void> {
78
+ this.index = this.createIndex();
79
+ this.buildIndex();
80
+ }
81
+
82
+ async store(input: StoreInput): Promise<Memory> {
83
+ const memory = await this.markdown.store(input);
84
+ // Add to index
85
+ this.index.add({
86
+ id: memory.id,
87
+ title: memory.frontmatter.title,
88
+ description: memory.frontmatter.description,
89
+ tags: memory.frontmatter.tags.join(" "),
90
+ content: memory.content,
91
+ type: memory.frontmatter.type,
92
+ category: memory.category,
93
+ importance: memory.frontmatter.importance,
94
+ });
95
+ return memory;
96
+ }
97
+
98
+ async search(input: SearchInput): Promise<SearchResult[]> {
99
+ const limit = input.limit ?? 10;
100
+ const query = input.query;
101
+
102
+ if (!query.trim()) {
103
+ // Empty query — fall back to list
104
+ const metas = await this.list({
105
+ type: input.type,
106
+ limit,
107
+ });
108
+ return metas.map((m) => ({
109
+ memory: m,
110
+ score: 0,
111
+ matchedOn: [],
112
+ }));
113
+ }
114
+
115
+ let results = this.index.search(query);
116
+
117
+ // Filter by type
118
+ if (input.type) {
119
+ results = results.filter((r) => {
120
+ const stored = r as unknown as { type: string };
121
+ return stored.type === input.type;
122
+ });
123
+ }
124
+
125
+ // Filter by tags
126
+ if (input.tags && input.tags.length > 0) {
127
+ const lowerTags = input.tags.map((t) => t.toLowerCase());
128
+ results = results.filter((r) => {
129
+ const stored = r as unknown as { tags: string };
130
+ const docTags = stored.tags.toLowerCase().split(" ");
131
+ return lowerTags.some((lt) => docTags.some((dt) => dt.includes(lt)));
132
+ });
133
+ }
134
+
135
+ const limited = results.slice(0, limit);
136
+
137
+ // Convert to SearchResult format
138
+ const output: SearchResult[] = [];
139
+ for (const r of limited) {
140
+ const memory = await this.markdown.retrieve(r.id);
141
+ if (!memory) continue;
142
+
143
+ const matchedOn = Object.keys(r.match);
144
+ const meta: MemoryMeta = {
145
+ id: memory.id,
146
+ slug: memory.slug,
147
+ category: memory.category,
148
+ frontmatter: memory.frontmatter,
149
+ excerpt: memory.content.slice(0, 160),
150
+ };
151
+ output.push({
152
+ memory: meta,
153
+ score: r.score,
154
+ matchedOn,
155
+ });
156
+ }
157
+
158
+ return output;
159
+ }
160
+
161
+ async retrieve(id: string): Promise<Memory | null> {
162
+ return this.markdown.retrieve(id);
163
+ }
164
+
165
+ async list(input: ListInput): Promise<MemoryMeta[]> {
166
+ return this.markdown.list(input);
167
+ }
168
+
169
+ async update(input: UpdateInput): Promise<Memory | null> {
170
+ const memory = await this.markdown.update(input);
171
+ if (memory) {
172
+ // Re-index: remove old, add new
173
+ this.index.discard(input.id);
174
+ this.index.add({
175
+ id: memory.id,
176
+ title: memory.frontmatter.title,
177
+ description: memory.frontmatter.description,
178
+ tags: memory.frontmatter.tags.join(" "),
179
+ content: memory.content,
180
+ type: memory.frontmatter.type,
181
+ category: memory.category,
182
+ importance: memory.frontmatter.importance,
183
+ });
184
+ }
185
+ return memory;
186
+ }
187
+
188
+ async delete(id: string): Promise<boolean> {
189
+ const deleted = await this.markdown.delete(id);
190
+ if (deleted) {
191
+ this.index.discard(id);
192
+ }
193
+ return deleted;
194
+ }
195
+
196
+ async stats(): Promise<MemoryStats> {
197
+ return this.markdown.stats();
198
+ }
199
+
200
+ async close(): Promise<void> {
201
+ await this.markdown.close();
202
+ }
203
+ }