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.
- package/README.md +12 -0
- package/dist/{chunk-D4EWPGBL.js → chunk-C5LYVVEI.js} +1 -1
- package/dist/{chunk-X5WEODUD.js → chunk-CYQU33FY.js} +1 -0
- package/dist/{chunk-QNLL3ZDF.js → chunk-G6CEEMAR.js} +3 -3
- package/dist/{chunk-4DB4XTSL.js → chunk-KTX4MGMR.js} +15 -1
- package/dist/{chunk-KJUGTLPQ.js → chunk-YO6JKGR3.js} +38 -2
- package/dist/{config-AIZJJ5D2.js → config-LZFXXOI4.js} +276 -14
- package/dist/{dev-WJ5QQ35B.js → dev-R3IYWZ3M.js} +2 -2
- package/dist/{disable-JDVOQNZG.js → disable-R6K5YJN4.js} +2 -2
- package/dist/{enable-JBJ4Q2S7.js → enable-HF4PYVJN.js} +2 -2
- package/dist/{host-NA7LZ4HX.js → host-SYZH3FVC.js} +4 -4
- package/dist/index.js +78 -18
- package/dist/{init-FZ3GG53E.js → init-YK6YRTOT.js} +102 -6
- package/dist/{install-I3GOS56Q.js → install-Q4PWEU43.js} +4 -4
- package/dist/{mcp-DLS3J6QJ.js → mcp-TBEDYELW.js} +4 -4
- package/dist/memory-7RM67ZLS.js +668 -0
- package/dist/postinstall.js +1 -1
- package/dist/{session-E3CZJJZQ.js → session-H4XW2WXH.js} +1 -1
- package/dist/{statusline-6HQCDWBD.js → statusline-6Y2EBAFQ.js} +1 -1
- package/dist/{uninstall-JN5YIKKM.js → uninstall-3PSUDGI4.js} +3 -3
- package/dist/{update-OWS4IJTG.js → update-WL6SFGGO.js} +4 -4
- package/lib/cf-memory/CHANGELOG.md +15 -0
- package/lib/cf-memory/README.md +284 -0
- package/lib/cf-memory/package-lock.json +2790 -0
- package/lib/cf-memory/package.json +31 -0
- package/lib/cf-memory/scripts/migrate-frontmatter.ts +134 -0
- package/lib/cf-memory/src/__tests__/daemon-e2e.test.ts +223 -0
- package/lib/cf-memory/src/__tests__/daemon.test.ts +407 -0
- package/lib/cf-memory/src/__tests__/dedup.test.ts +103 -0
- package/lib/cf-memory/src/__tests__/embeddings.test.ts +292 -0
- package/lib/cf-memory/src/__tests__/lazy-install.test.ts +210 -0
- package/lib/cf-memory/src/__tests__/markdown-backend.test.ts +410 -0
- package/lib/cf-memory/src/__tests__/migration.test.ts +255 -0
- package/lib/cf-memory/src/__tests__/migrations.test.ts +288 -0
- package/lib/cf-memory/src/__tests__/minisearch-backend.test.ts +262 -0
- package/lib/cf-memory/src/__tests__/ollama.test.ts +48 -0
- package/lib/cf-memory/src/__tests__/schema.test.ts +128 -0
- package/lib/cf-memory/src/__tests__/search.test.ts +115 -0
- package/lib/cf-memory/src/__tests__/temporal-decay.test.ts +54 -0
- package/lib/cf-memory/src/__tests__/tier.test.ts +293 -0
- package/lib/cf-memory/src/__tests__/tools.test.ts +83 -0
- package/lib/cf-memory/src/backends/markdown.ts +318 -0
- package/lib/cf-memory/src/backends/minisearch.ts +203 -0
- package/lib/cf-memory/src/backends/sqlite/embeddings.ts +286 -0
- package/lib/cf-memory/src/backends/sqlite/index.ts +549 -0
- package/lib/cf-memory/src/backends/sqlite/migrations.ts +188 -0
- package/lib/cf-memory/src/backends/sqlite/schema.ts +120 -0
- package/lib/cf-memory/src/backends/sqlite/search.ts +296 -0
- package/lib/cf-memory/src/bin/cf-memory.ts +2 -0
- package/lib/cf-memory/src/daemon/entry.ts +99 -0
- package/lib/cf-memory/src/daemon/process.ts +220 -0
- package/lib/cf-memory/src/daemon/server.ts +166 -0
- package/lib/cf-memory/src/daemon/watcher.ts +90 -0
- package/lib/cf-memory/src/index.ts +45 -0
- package/lib/cf-memory/src/lib/backend.ts +23 -0
- package/lib/cf-memory/src/lib/daemon-client.ts +163 -0
- package/lib/cf-memory/src/lib/dedup.ts +80 -0
- package/lib/cf-memory/src/lib/lazy-install.ts +274 -0
- package/lib/cf-memory/src/lib/ollama.ts +76 -0
- package/lib/cf-memory/src/lib/temporal-decay.ts +19 -0
- package/lib/cf-memory/src/lib/tier.ts +107 -0
- package/lib/cf-memory/src/lib/types.ts +109 -0
- package/lib/cf-memory/src/resources/index.ts +62 -0
- package/lib/cf-memory/src/server.ts +20 -0
- package/lib/cf-memory/src/tools/delete.ts +38 -0
- package/lib/cf-memory/src/tools/list.ts +38 -0
- package/lib/cf-memory/src/tools/retrieve.ts +52 -0
- package/lib/cf-memory/src/tools/search.ts +47 -0
- package/lib/cf-memory/src/tools/store.ts +70 -0
- package/lib/cf-memory/src/tools/update.ts +62 -0
- package/lib/cf-memory/tsconfig.json +15 -0
- package/lib/cf-memory/vitest.config.ts +7 -0
- package/lib/learn-host/CHANGELOG.md +4 -0
- package/lib/learn-host/package.json +1 -1
- 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
|
+
}
|