@lotargo/memory_plugin 1.6.3 → 1.6.5
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/CHANGELOG.md +64 -1
- package/README.md +28 -20
- package/mcp-server/admin/snapshot.js +93 -26
- package/mcp-server/cli/direct_commands.js +5 -3
- package/mcp-server/cli/handlers/storage_actions.js +3 -1
- package/mcp-server/config/config_manager.js +0 -1
- package/mcp-server/db/migrations.js +26 -5
- package/mcp-server/db/sync_queue.js +118 -44
- package/mcp-server/graph/knowledge_linker.js +126 -19
- package/mcp-server/ingest/exporter.js +38 -9
- package/mcp-server/ingest/pipeline.js +146 -48
- package/mcp-server/memory.js +13 -3
- package/mcp-server/prompt_manager.js +10 -7
- package/mcp-server/retrieval/retriever.js +62 -38
- package/mcp-server/setup.js +18 -12
- package/mcp-server/tools/core/memory_core.js +465 -393
- package/mcp-server/tools/identity_tools.js +25 -6
- package/mcp-server/tools/memory_tools.js +138 -123
- package/mcp-server/tools/rag_tools.js +313 -249
- package/opencode-plugin/index.js +558 -440
- package/package.json +5 -5
- package/skills/using-memory/SKILL.md +152 -117
|
@@ -1,393 +1,465 @@
|
|
|
1
|
-
import { readFile } from "node:fs/promises";
|
|
2
|
-
import { join } from "node:path";
|
|
3
|
-
import {
|
|
4
|
-
readMemory,
|
|
5
|
-
readMemoryRaw,
|
|
6
|
-
writeMemory,
|
|
7
|
-
today,
|
|
8
|
-
MEMORY_DIR,
|
|
9
|
-
GLOBAL_KEY,
|
|
10
|
-
scopeKey,
|
|
11
|
-
projectKey,
|
|
12
|
-
projectName,
|
|
13
|
-
canonicalPath,
|
|
14
|
-
listProjectStores,
|
|
15
|
-
storeFilePath,
|
|
16
|
-
} from "../../memory.js";
|
|
17
|
-
import {
|
|
18
|
-
parseFactEntry,
|
|
19
|
-
factText,
|
|
20
|
-
factMeta,
|
|
21
|
-
withMeta,
|
|
22
|
-
nextFactId,
|
|
23
|
-
isKeepFact,
|
|
24
|
-
isExpiredLine,
|
|
25
|
-
isSuperseded,
|
|
26
|
-
formatFactEntry,
|
|
27
|
-
matchesQuery,
|
|
28
|
-
matchesTags,
|
|
29
|
-
inDateRange,
|
|
30
|
-
factTitle,
|
|
31
|
-
factBody,
|
|
32
|
-
autoGenerateTitle,
|
|
33
|
-
} from "../../fact_format.js";
|
|
34
|
-
import { requireProjectKey, resolveFactIndex } from "../helpers.js";
|
|
35
|
-
|
|
36
|
-
// Single implementation of the Notebook tools, shared by the MCP server
|
|
37
|
-
// (mcp-server/tools/memory_tools.js) and the OpenCode plugin
|
|
38
|
-
// (opencode-plugin/index.js). Both used to carry their own copy, so bug fixes
|
|
39
|
-
// in one never reached the other. Every function returns a plain string; the
|
|
40
|
-
// callers wrap it in whatever envelope their host expects.
|
|
41
|
-
//
|
|
42
|
-
// `ctx` carries the host's notion of the current location:
|
|
43
|
-
// { worktree, directory } — the MCP server passes nothing and falls back to cwd.
|
|
44
|
-
|
|
45
|
-
const TITLE_PATTERN = /^\*\*([^*]+)\*\*\s*(?:—|--|-|:)?\s*(.*)$/;
|
|
46
|
-
|
|
47
|
-
function splitTitle(rawText, explicitTitle) {
|
|
48
|
-
let finalTitle = explicitTitle ? explicitTitle.trim() : null;
|
|
49
|
-
let finalFact = String(rawText || "").trim();
|
|
50
|
-
const match = TITLE_PATTERN.exec(finalFact);
|
|
51
|
-
if (match) {
|
|
52
|
-
if (!finalTitle) finalTitle = match[1].trim();
|
|
53
|
-
finalFact = match[2].trim();
|
|
54
|
-
}
|
|
55
|
-
return { finalTitle, finalFact };
|
|
56
|
-
}
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
return
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
)
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
}
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
const
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
}
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
}
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
const
|
|
260
|
-
const
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
if (!
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
}
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
const
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
const
|
|
316
|
-
const
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
const
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
const
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
?
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
|
|
1
|
+
import { readFile } from "node:fs/promises";
|
|
2
|
+
import { join } from "node:path";
|
|
3
|
+
import {
|
|
4
|
+
readMemory,
|
|
5
|
+
readMemoryRaw,
|
|
6
|
+
writeMemory,
|
|
7
|
+
today,
|
|
8
|
+
MEMORY_DIR,
|
|
9
|
+
GLOBAL_KEY,
|
|
10
|
+
scopeKey,
|
|
11
|
+
projectKey,
|
|
12
|
+
projectName,
|
|
13
|
+
canonicalPath,
|
|
14
|
+
listProjectStores,
|
|
15
|
+
storeFilePath,
|
|
16
|
+
} from "../../memory.js";
|
|
17
|
+
import {
|
|
18
|
+
parseFactEntry,
|
|
19
|
+
factText,
|
|
20
|
+
factMeta,
|
|
21
|
+
withMeta,
|
|
22
|
+
nextFactId,
|
|
23
|
+
isKeepFact,
|
|
24
|
+
isExpiredLine,
|
|
25
|
+
isSuperseded,
|
|
26
|
+
formatFactEntry,
|
|
27
|
+
matchesQuery,
|
|
28
|
+
matchesTags,
|
|
29
|
+
inDateRange,
|
|
30
|
+
factTitle,
|
|
31
|
+
factBody,
|
|
32
|
+
autoGenerateTitle,
|
|
33
|
+
} from "../../fact_format.js";
|
|
34
|
+
import { requireProjectKey, resolveFactIndex } from "../helpers.js";
|
|
35
|
+
|
|
36
|
+
// Single implementation of the Notebook tools, shared by the MCP server
|
|
37
|
+
// (mcp-server/tools/memory_tools.js) and the OpenCode plugin
|
|
38
|
+
// (opencode-plugin/index.js). Both used to carry their own copy, so bug fixes
|
|
39
|
+
// in one never reached the other. Every function returns a plain string; the
|
|
40
|
+
// callers wrap it in whatever envelope their host expects.
|
|
41
|
+
//
|
|
42
|
+
// `ctx` carries the host's notion of the current location:
|
|
43
|
+
// { worktree, directory } — the MCP server passes nothing and falls back to cwd.
|
|
44
|
+
|
|
45
|
+
const TITLE_PATTERN = /^\*\*([^*]+)\*\*\s*(?:—|--|-|:)?\s*(.*)$/;
|
|
46
|
+
|
|
47
|
+
function splitTitle(rawText, explicitTitle) {
|
|
48
|
+
let finalTitle = explicitTitle ? explicitTitle.trim() : null;
|
|
49
|
+
let finalFact = String(rawText || "").trim();
|
|
50
|
+
const match = TITLE_PATTERN.exec(finalFact);
|
|
51
|
+
if (match) {
|
|
52
|
+
if (!finalTitle) finalTitle = match[1].trim();
|
|
53
|
+
finalFact = match[2].trim();
|
|
54
|
+
}
|
|
55
|
+
return { finalTitle, finalFact };
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
function extractEffectiveDir(args = {}, ctx = {}) {
|
|
59
|
+
return (
|
|
60
|
+
args?.directory ||
|
|
61
|
+
args?.project ||
|
|
62
|
+
ctx?.directory ||
|
|
63
|
+
ctx?.worktree ||
|
|
64
|
+
null
|
|
65
|
+
);
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
async function resolveScopeKey(scope, args = {}, ctx = {}) {
|
|
69
|
+
const dir = extractEffectiveDir(args, ctx);
|
|
70
|
+
return await scopeKey(scope || "project", ctx?.worktree ?? null, dir);
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
export async function rememberFact(
|
|
74
|
+
{ fact, title, scope, directory, project, docId, startLine, endLine, relationType, ttl, keep, tags, supersedes },
|
|
75
|
+
ctx = {}
|
|
76
|
+
) {
|
|
77
|
+
const key = requireProjectKey(await resolveScopeKey(scope, { directory, project }, ctx));
|
|
78
|
+
const entries = await readMemory(key);
|
|
79
|
+
|
|
80
|
+
let { finalTitle, finalFact } = splitTitle(fact, title);
|
|
81
|
+
if (!finalTitle) finalTitle = autoGenerateTitle(finalFact);
|
|
82
|
+
|
|
83
|
+
const text = `**${finalTitle}** — ${finalFact}`;
|
|
84
|
+
const factBodyNormalized = finalFact.toLowerCase();
|
|
85
|
+
const duplicate = entries.some((e) => factBody(e).toLowerCase().trim() === factBodyNormalized);
|
|
86
|
+
|
|
87
|
+
let supersededInfo = "";
|
|
88
|
+
if (!duplicate) {
|
|
89
|
+
const [date, time] = today().split(" ");
|
|
90
|
+
const meta = { ttl, tags };
|
|
91
|
+
if (keep) meta.keep = "1";
|
|
92
|
+
if (supersedes) {
|
|
93
|
+
const targetIdx = resolveFactIndex(entries, supersedes);
|
|
94
|
+
if (targetIdx !== -1) {
|
|
95
|
+
let targetId = factMeta(entries[targetIdx]).id;
|
|
96
|
+
if (!targetId) {
|
|
97
|
+
targetId = nextFactId(entries);
|
|
98
|
+
entries[targetIdx] = withMeta(entries[targetIdx], { id: targetId });
|
|
99
|
+
}
|
|
100
|
+
const newId = nextFactId(entries);
|
|
101
|
+
entries[targetIdx] = withMeta(entries[targetIdx], { id: targetId, supersededBy: newId });
|
|
102
|
+
meta.id = newId;
|
|
103
|
+
meta.supersedes = targetId;
|
|
104
|
+
supersededInfo = ` [superseded: "${factText(entries[targetIdx]).slice(0, 60)}"]`;
|
|
105
|
+
} else {
|
|
106
|
+
supersededInfo = " (note: supersedes target not found)";
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
if (!meta.id) meta.id = nextFactId(entries);
|
|
110
|
+
entries.push(formatFactEntry({ date, time, text, meta }));
|
|
111
|
+
await writeMemory(key, entries);
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
let linkInfo = "";
|
|
115
|
+
if (docId) {
|
|
116
|
+
try {
|
|
117
|
+
const { linkFactToDocument } = await import("../../graph/knowledge_linker.js");
|
|
118
|
+
const linkRes = await linkFactToDocument({
|
|
119
|
+
factKey: key,
|
|
120
|
+
factText: finalFact,
|
|
121
|
+
docId,
|
|
122
|
+
startLine,
|
|
123
|
+
endLine,
|
|
124
|
+
relationType: relationType || "LINKS_TO",
|
|
125
|
+
});
|
|
126
|
+
const linesStr = startLine ? `:L${startLine}${endLine ? `-${endLine}` : ""}` : "";
|
|
127
|
+
linkInfo = ` [Linked to Doc: "${linkRes.docTitle}"${linesStr}]`;
|
|
128
|
+
} catch (err) {
|
|
129
|
+
linkInfo = ` (Note: Fact saved, but document link failed: ${err.message})`;
|
|
130
|
+
}
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
return `Memory updated${supersededInfo}${linkInfo}`;
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
async function resolveTargetKey(projectPath) {
|
|
137
|
+
if (!projectPath) return null;
|
|
138
|
+
if (typeof projectPath === "string" && (projectPath.startsWith("git:") || projectPath.startsWith("git_") || projectPath === GLOBAL_KEY)) {
|
|
139
|
+
return projectPath;
|
|
140
|
+
}
|
|
141
|
+
try {
|
|
142
|
+
const { resolveProjectIdentity } = await import("../../identity.js");
|
|
143
|
+
const identity = await resolveProjectIdentity(projectPath);
|
|
144
|
+
if (identity) return identity.key;
|
|
145
|
+
} catch {}
|
|
146
|
+
return canonicalPath(projectPath);
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
export async function recallFacts(
|
|
150
|
+
{ scope, directory, project, query, tags, since, until, mode, offset, limit, includeSuperseded = false },
|
|
151
|
+
ctx = {}
|
|
152
|
+
) {
|
|
153
|
+
const results = [];
|
|
154
|
+
const now = Date.now();
|
|
155
|
+
const targetMode = mode || "full";
|
|
156
|
+
const targetOffset = offset !== undefined && offset !== null ? offset : 0;
|
|
157
|
+
const targetProjectInput = directory || project || ctx.directory || ctx.worktree || null;
|
|
158
|
+
|
|
159
|
+
if (scope === "list_projects") {
|
|
160
|
+
const stores = await listProjectStores();
|
|
161
|
+
if (!stores.length) return "No project memory stores found.";
|
|
162
|
+
const lines = stores.map(
|
|
163
|
+
(s, i) =>
|
|
164
|
+
`${i + 1}. ${s.basename} — ${s.count} fact(s) [${s.file}]${
|
|
165
|
+
s.path ? ` (bound to ${s.path})` : " (unbound legacy store)"
|
|
166
|
+
}`
|
|
167
|
+
);
|
|
168
|
+
return `Project Memory Stores:\n${lines.join(
|
|
169
|
+
"\n"
|
|
170
|
+
)}\n\nUse recall(scope: "project", directory: "<path>") to read a specific store.\n\nMemory dir: ${MEMORY_DIR}`;
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
let getLinksForFact = null;
|
|
174
|
+
try {
|
|
175
|
+
({ getLinksForFact } = await import("../../graph/knowledge_linker.js"));
|
|
176
|
+
} catch {}
|
|
177
|
+
|
|
178
|
+
const target =
|
|
179
|
+
(await resolveTargetKey(targetProjectInput)) ?? (await projectKey(ctx.worktree ?? null, targetProjectInput));
|
|
180
|
+
const label = targetProjectInput ? target : await projectName(ctx.worktree ?? null, targetProjectInput);
|
|
181
|
+
|
|
182
|
+
const formatFactWithLinks = async (factLine, index, key) => {
|
|
183
|
+
const p = parseFactEntry(factLine);
|
|
184
|
+
if (!p) return factLine;
|
|
185
|
+
|
|
186
|
+
const meta = p.meta;
|
|
187
|
+
const badges = [];
|
|
188
|
+
if (isExpiredLine(factLine, now)) badges.push("EXPIRED");
|
|
189
|
+
if (isKeepFact(factLine)) badges.push("KEEP");
|
|
190
|
+
if (isSuperseded(factLine)) badges.push("SUPERSEDED");
|
|
191
|
+
if (meta.inject === "1") badges.push("INJECT");
|
|
192
|
+
if (meta.id) badges.push(`id:${meta.id}`);
|
|
193
|
+
if (meta.tags) badges.push(`tags:${meta.tags}`);
|
|
194
|
+
badges.push(`${p.date} ${p.time}`);
|
|
195
|
+
const badgesStr = badges.length ? ` [${badges.join("] [")}]` : "";
|
|
196
|
+
|
|
197
|
+
let lineText =
|
|
198
|
+
targetMode === "headers" ? `**${factTitle(factLine)}**${badgesStr}` : `${p.text}${badgesStr}`;
|
|
199
|
+
|
|
200
|
+
if (getLinksForFact) {
|
|
201
|
+
try {
|
|
202
|
+
const links = await getLinksForFact(key, p.text);
|
|
203
|
+
if (links && links.length > 0) {
|
|
204
|
+
const docStr = links
|
|
205
|
+
.map((l) => {
|
|
206
|
+
const range = l.start_line ? `:L${l.start_line}${l.end_line ? `-${l.end_line}` : ""}` : "";
|
|
207
|
+
return `${l.doc_title || l.doc_path}${range}`;
|
|
208
|
+
})
|
|
209
|
+
.join(", ");
|
|
210
|
+
lineText += ` 🔗 [Linked Docs: ${docStr}]`;
|
|
211
|
+
}
|
|
212
|
+
} catch {}
|
|
213
|
+
}
|
|
214
|
+
return `${index}. ${lineText}`;
|
|
215
|
+
};
|
|
216
|
+
|
|
217
|
+
const collect = async (entries, key) => {
|
|
218
|
+
const matched = entries
|
|
219
|
+
.map((entry, storageIndex) => ({ entry, storageIndex }))
|
|
220
|
+
.filter(
|
|
221
|
+
({ entry }) =>
|
|
222
|
+
(includeSuperseded || !isSuperseded(entry)) &&
|
|
223
|
+
matchesQuery(entry, query) &&
|
|
224
|
+
matchesTags(entry, tags) &&
|
|
225
|
+
inDateRange(entry, since, until)
|
|
226
|
+
);
|
|
227
|
+
if (!matched.length) return;
|
|
228
|
+
if (results.length) results.push("");
|
|
229
|
+
results.push(`--- ${key === GLOBAL_KEY ? "Global" : `Project: ${key === target ? label : key}`} ---`);
|
|
230
|
+
|
|
231
|
+
const hasLimit = limit !== undefined && limit !== null;
|
|
232
|
+
const targetLimit = hasLimit ? limit : matched.length;
|
|
233
|
+
const paginated = matched.slice(targetOffset, targetOffset + targetLimit);
|
|
234
|
+
for (let i = 0; i < paginated.length; i++) {
|
|
235
|
+
results.push(
|
|
236
|
+
await formatFactWithLinks(paginated[i].entry, paginated[i].storageIndex + 1, key)
|
|
237
|
+
);
|
|
238
|
+
}
|
|
239
|
+
if (hasLimit && matched.length > targetLimit) {
|
|
240
|
+
results.push(
|
|
241
|
+
`Showing entries ${targetOffset + 1}-${Math.min(targetOffset + targetLimit, matched.length)} of ${matched.length}`
|
|
242
|
+
);
|
|
243
|
+
}
|
|
244
|
+
results.push(`Store file: ${storeFilePath(key)}`);
|
|
245
|
+
};
|
|
246
|
+
|
|
247
|
+
if (scope !== "project") await collect(await readMemory(GLOBAL_KEY), GLOBAL_KEY);
|
|
248
|
+
if (scope !== "global" && target) await collect(await readMemory(target), target);
|
|
249
|
+
|
|
250
|
+
const filtered = Boolean(query || tags || since || until);
|
|
251
|
+
if (!results.length) return filtered ? "No facts match the search." : "Memory is empty.";
|
|
252
|
+
return `${results.join("\n")}\n\nMemory dir: ${MEMORY_DIR}`;
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
export async function getFactById({ id, scope, directory, project }, ctx = {}) {
|
|
256
|
+
const targetId = String(id || "").trim();
|
|
257
|
+
if (!targetId) throw new Error("ID parameter is required.");
|
|
258
|
+
|
|
259
|
+
const targetDir = extractEffectiveDir({ directory, project }, ctx);
|
|
260
|
+
const results = [];
|
|
261
|
+
const check = async (key) => {
|
|
262
|
+
const entries = await readMemory(key);
|
|
263
|
+
const match = entries.find((e) => factMeta(e).id === targetId);
|
|
264
|
+
if (match) {
|
|
265
|
+
results.push({ key, title: factTitle(match), body: factBody(match), meta: factMeta(match), line: match });
|
|
266
|
+
}
|
|
267
|
+
};
|
|
268
|
+
|
|
269
|
+
if (scope !== "project") await check(GLOBAL_KEY);
|
|
270
|
+
if (scope !== "global") {
|
|
271
|
+
const projKey = await projectKey(ctx.worktree ?? null, targetDir);
|
|
272
|
+
if (projKey) await check(projKey);
|
|
273
|
+
}
|
|
274
|
+
|
|
275
|
+
if (!results.length) return `Fact with ID "${targetId}" not found.`;
|
|
276
|
+
|
|
277
|
+
return results
|
|
278
|
+
.map((r) => {
|
|
279
|
+
const metaStr = Object.entries(r.meta)
|
|
280
|
+
.map(([k, v]) => `${k}:${v}`)
|
|
281
|
+
.join(", ");
|
|
282
|
+
return `[Store: ${r.key === GLOBAL_KEY ? "Global" : "Project"}]\nTitle: ${r.title}\nBody: ${r.body}\nMetadata: ${
|
|
283
|
+
metaStr ? `<!-- ${metaStr} -->` : "none"
|
|
284
|
+
}`;
|
|
285
|
+
})
|
|
286
|
+
.join("\n\n");
|
|
287
|
+
}
|
|
288
|
+
|
|
289
|
+
export async function forgetFacts({ query, scope, force, directory, project }, ctx = {}) {
|
|
290
|
+
const key = requireProjectKey(await resolveScopeKey(scope, { directory, project }, ctx));
|
|
291
|
+
const entries = await readMemory(key);
|
|
292
|
+
|
|
293
|
+
const rangeMatch = /^\s*(\d+)\s*-\s*(\d+)\s*$/.exec(query);
|
|
294
|
+
let indices = [];
|
|
295
|
+
if (rangeMatch) {
|
|
296
|
+
const from = parseInt(rangeMatch[1], 10);
|
|
297
|
+
const to = parseInt(rangeMatch[2], 10);
|
|
298
|
+
if (from > 0 && to >= from && to <= entries.length) {
|
|
299
|
+
for (let i = from - 1; i < to; i++) indices.push(i);
|
|
300
|
+
}
|
|
301
|
+
}
|
|
302
|
+
if (!indices.length && /^\s*\d+\s*$/.test(String(query))) {
|
|
303
|
+
const num = parseInt(query, 10);
|
|
304
|
+
if (num > 0 && num <= entries.length) indices.push(num - 1);
|
|
305
|
+
}
|
|
306
|
+
if (!indices.length) {
|
|
307
|
+
const q = String(query).toLowerCase();
|
|
308
|
+
indices = entries.reduce((acc, e, i) => (e.toLowerCase().includes(q) ? acc.concat(i) : acc), []);
|
|
309
|
+
}
|
|
310
|
+
if (!indices.length) return "Not found.";
|
|
311
|
+
|
|
312
|
+
const removable = indices.filter((i) => force || !isKeepFact(entries[i]));
|
|
313
|
+
const protectedCount = indices.length - removable.length;
|
|
314
|
+
if (removable.length) {
|
|
315
|
+
const removedBodies = removable.map((i) => factBody(entries[i]) || factText(entries[i]));
|
|
316
|
+
for (const i of removable.sort((a, b) => b - a)) entries.splice(i, 1);
|
|
317
|
+
await writeMemory(key, entries);
|
|
318
|
+
try {
|
|
319
|
+
const { getDatabase } = await import("../../db/database.js");
|
|
320
|
+
const { deleteLinksForFacts } = await import("../../graph/knowledge_linker.js");
|
|
321
|
+
await deleteLinksForFacts(await getDatabase(), key, removedBodies);
|
|
322
|
+
} catch {}
|
|
323
|
+
}
|
|
324
|
+
let text = removable.length ? "Memory updated" : "Nothing removed.";
|
|
325
|
+
if (protectedCount) text += ` (${protectedCount} protected fact(s) skipped; use force=true to override)`;
|
|
326
|
+
return text;
|
|
327
|
+
}
|
|
328
|
+
|
|
329
|
+
export async function updateFactText({ id, newText, title, scope, directory, project }, ctx = {}) {
|
|
330
|
+
const key = requireProjectKey(await resolveScopeKey(scope, { directory, project }, ctx));
|
|
331
|
+
const entries = await readMemory(key);
|
|
332
|
+
const idx = resolveFactIndex(entries, id);
|
|
333
|
+
if (idx === -1) throw new Error(`Fact not found: ${id}`);
|
|
334
|
+
|
|
335
|
+
const p = parseFactEntry(entries[idx]);
|
|
336
|
+
const oldText = p ? p.text : entries[idx];
|
|
337
|
+
const oldBody = factBody(entries[idx]) || oldText;
|
|
338
|
+
|
|
339
|
+
let { finalTitle, finalFact } = splitTitle(newText, title);
|
|
340
|
+
if (!finalTitle) finalTitle = factTitle(entries[idx]) || autoGenerateTitle(finalFact);
|
|
341
|
+
|
|
342
|
+
entries[idx] = formatFactEntry({
|
|
343
|
+
date: p.date,
|
|
344
|
+
time: p.time,
|
|
345
|
+
text: `**${finalTitle}** — ${finalFact}`,
|
|
346
|
+
meta: p.meta,
|
|
347
|
+
});
|
|
348
|
+
await writeMemory(key, entries);
|
|
349
|
+
|
|
350
|
+
let linksUpdated = 0;
|
|
351
|
+
try {
|
|
352
|
+
const { getDatabase } = await import("../../db/database.js");
|
|
353
|
+
const db = await getDatabase();
|
|
354
|
+
const linkedRows = await db
|
|
355
|
+
.prepare("SELECT * FROM knowledge_links WHERE fact_key = ? AND fact_text = ?")
|
|
356
|
+
.all(key, oldBody);
|
|
357
|
+
const res = await db
|
|
358
|
+
.prepare("UPDATE knowledge_links SET fact_text = ? WHERE fact_key = ? AND fact_text = ?")
|
|
359
|
+
.run(finalFact, key, oldBody);
|
|
360
|
+
linksUpdated = res.changes;
|
|
361
|
+
if (linksUpdated) {
|
|
362
|
+
const { queueDocumentSyncIfNeeded } = await import("../../graph/knowledge_linker.js");
|
|
363
|
+
const docIds = new Set();
|
|
364
|
+
for (const link of linkedRows) {
|
|
365
|
+
const targetSpec = link.start_line
|
|
366
|
+
? `${link.doc_id}:L${link.start_line}-${link.end_line || link.start_line}`
|
|
367
|
+
: link.doc_id;
|
|
368
|
+
await db.prepare(
|
|
369
|
+
"DELETE FROM graph_edges WHERE source_id = ? AND target_id = ? AND relation_type = ?"
|
|
370
|
+
).run(
|
|
371
|
+
`fact:${key}:${oldBody.substring(0, 30)}`,
|
|
372
|
+
targetSpec,
|
|
373
|
+
link.relation_type || "LINKS_TO"
|
|
374
|
+
);
|
|
375
|
+
await db.prepare(`
|
|
376
|
+
INSERT OR IGNORE INTO graph_edges (source_id, target_id, relation_type, metadata_json, created_at)
|
|
377
|
+
VALUES (?, ?, ?, ?, ?)
|
|
378
|
+
`).run(
|
|
379
|
+
`fact:${key}:${finalFact.substring(0, 30)}`,
|
|
380
|
+
targetSpec,
|
|
381
|
+
link.relation_type || "LINKS_TO",
|
|
382
|
+
link.metadata_json || JSON.stringify({ linkId: link.id }),
|
|
383
|
+
link.created_at || Date.now()
|
|
384
|
+
);
|
|
385
|
+
docIds.add(link.doc_id);
|
|
386
|
+
}
|
|
387
|
+
for (const docId of docIds) await queueDocumentSyncIfNeeded(db, docId);
|
|
388
|
+
}
|
|
389
|
+
} catch {}
|
|
390
|
+
|
|
391
|
+
return `Fact updated${linksUpdated ? `, ${linksUpdated} doc link(s) updated` : ""}`;
|
|
392
|
+
}
|
|
393
|
+
|
|
394
|
+
export async function memoryInfo(_args = {}, ctx = {}) {
|
|
395
|
+
const effectiveDir = extractEffectiveDir(_args, ctx) || process.cwd();
|
|
396
|
+
const dbPath = join(MEMORY_DIR, "storage", "memory.sqlite");
|
|
397
|
+
const activeKey = await projectKey(ctx.worktree ?? null, effectiveDir);
|
|
398
|
+
const globalFile = storeFilePath(GLOBAL_KEY);
|
|
399
|
+
const projectFile = activeKey ? storeFilePath(activeKey) : null;
|
|
400
|
+
|
|
401
|
+
let version = "unknown";
|
|
402
|
+
try {
|
|
403
|
+
version = JSON.parse(await readFile(new URL("../../../package.json", import.meta.url), "utf-8")).version;
|
|
404
|
+
} catch {}
|
|
405
|
+
|
|
406
|
+
const rag = {};
|
|
407
|
+
try {
|
|
408
|
+
const { getDatabase } = await import("../../db/database.js");
|
|
409
|
+
const db = await getDatabase();
|
|
410
|
+
const count = async (table) => {
|
|
411
|
+
const row = await db.prepare(`SELECT COUNT(*) AS c FROM ${table}`).get();
|
|
412
|
+
return row ? row.c : 0;
|
|
413
|
+
};
|
|
414
|
+
rag.documents = await count("documents");
|
|
415
|
+
rag.sections = await count("sections");
|
|
416
|
+
rag.chunks = await count("micro_chunks");
|
|
417
|
+
rag.edges = await count("graph_edges");
|
|
418
|
+
rag.links = await count("knowledge_links");
|
|
419
|
+
} catch (e) {
|
|
420
|
+
rag.error = e.message;
|
|
421
|
+
}
|
|
422
|
+
|
|
423
|
+
const stores = await listProjectStores();
|
|
424
|
+
|
|
425
|
+
const identityLines = [];
|
|
426
|
+
try {
|
|
427
|
+
const { getDatabase } = await import("../../db/database.js");
|
|
428
|
+
const { resolveProjectIdentity, listIdentities } = await import("../../identity.js");
|
|
429
|
+
const db = await getDatabase();
|
|
430
|
+
const identity = await resolveProjectIdentity(effectiveDir);
|
|
431
|
+
const all = await listIdentities(db);
|
|
432
|
+
const registered = identity ? all.find((item) => item.key === identity.key) : null;
|
|
433
|
+
identityLines.push(
|
|
434
|
+
`Identity: ${identity ? "git" : "no-git"}` +
|
|
435
|
+
(identity
|
|
436
|
+
? ` | key: ${identity.key} | name: ${identity.name}${
|
|
437
|
+
identity.primaryRemote ? ` | remote: ${identity.primaryRemote}` : ""
|
|
438
|
+
}`
|
|
439
|
+
: ""),
|
|
440
|
+
`Registry: ${identity ? (registered ? "linked" : "unlinked") : "not-applicable"}` +
|
|
441
|
+
(registered ? ` | aliases: ${registered.aliases.length}` : ""),
|
|
442
|
+
`Known identities: ${all.length}`
|
|
443
|
+
);
|
|
444
|
+
} catch (e) {
|
|
445
|
+
identityLines.push(`Identity: unavailable (${e.message})`);
|
|
446
|
+
}
|
|
447
|
+
|
|
448
|
+
const lines = [
|
|
449
|
+
`Version: ${version}`,
|
|
450
|
+
`MEMORY_DIR: ${MEMORY_DIR}`,
|
|
451
|
+
`SQLite DB: ${dbPath}`,
|
|
452
|
+
`Global store: ${globalFile}`,
|
|
453
|
+
`Project store: ${projectFile || "not applicable (outside Git)"}`,
|
|
454
|
+
`Project stores: ${stores.length}`,
|
|
455
|
+
`Facts (global): ${(await readMemoryRaw(GLOBAL_KEY)).length}`,
|
|
456
|
+
`Facts (project): ${(activeKey ? await readMemoryRaw(activeKey) : []).length}`,
|
|
457
|
+
...identityLines,
|
|
458
|
+
];
|
|
459
|
+
if (rag.error) lines.push(`RAG: unavailable (${rag.error})`);
|
|
460
|
+
else
|
|
461
|
+
lines.push(
|
|
462
|
+
`RAG: ${rag.documents} doc(s), ${rag.sections} section(s), ${rag.chunks} chunk(s), ${rag.edges} edge(s), ${rag.links} memory link(s)`
|
|
463
|
+
);
|
|
464
|
+
return lines.join("\n");
|
|
465
|
+
}
|