@lotargo/memory_plugin 1.6.6 → 1.6.7
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 +28 -0
- package/README.md +576 -443
- package/mcp-server/cli/direct_commands.js +39 -0
- package/mcp-server/cli.js +16 -5
- package/mcp-server/cli_boot.js +4 -1
- package/mcp-server/client_cli.js +73 -0
- package/mcp-server/client_paths.js +44 -0
- package/mcp-server/client_registration.js +38 -0
- package/mcp-server/codex_config.js +86 -8
- package/mcp-server/db/database.js +14 -21
- package/mcp-server/db/migrations.js +66 -77
- package/mcp-server/db/rag_blob_transport.js +143 -0
- package/mcp-server/db/rag_sync.js +284 -0
- package/mcp-server/db/sync_queue.js +219 -307
- package/mcp-server/dev_link.js +142 -0
- package/mcp-server/fact_format.js +44 -12
- package/mcp-server/index.js +17 -7
- package/mcp-server/ingest/exporter.js +44 -38
- package/mcp-server/ingest/pipeline.js +260 -248
- package/mcp-server/persona_migration.js +39 -0
- package/mcp-server/prompt_manager.js +162 -55
- package/mcp-server/retrieval/retriever.js +99 -64
- package/mcp-server/setup.js +150 -100
- package/mcp-server/storage/blob_store.js +53 -1
- package/mcp-server/tools/core/knowledge_read_core.js +163 -0
- package/mcp-server/tools/core/memory_core.js +24 -4
- package/mcp-server/tools/core/memory_routing.js +10 -0
- package/mcp-server/tools/core/note_core.js +53 -0
- package/mcp-server/tools/core/rag_query_core.js +169 -0
- package/mcp-server/tools/index.js +11 -9
- package/mcp-server/tools/memory_tools.js +4 -1
- package/mcp-server/tools/note_tools.js +35 -0
- package/mcp-server/tools/rag_tools.js +211 -364
- package/mcp-server/uninstall.js +627 -0
- package/opencode-plugin/index.js +80 -12
- package/opencode-plugin/main.js +136 -0
- package/package.json +16 -12
- package/skills/using-memory/SKILL.md +28 -19
|
@@ -30,6 +30,7 @@ import {
|
|
|
30
30
|
factTitle,
|
|
31
31
|
factBody,
|
|
32
32
|
autoGenerateTitle,
|
|
33
|
+
isDirectiveFact,
|
|
33
34
|
} from "../../fact_format.js";
|
|
34
35
|
import { requireProjectKey, resolveFactIndex } from "../helpers.js";
|
|
35
36
|
|
|
@@ -71,9 +72,10 @@ async function resolveScopeKey(scope, args = {}, ctx = {}) {
|
|
|
71
72
|
}
|
|
72
73
|
|
|
73
74
|
export async function rememberFact(
|
|
74
|
-
{ fact, title, scope, directory, project, docId, startLine, endLine, relationType, ttl, keep, tags, supersedes },
|
|
75
|
+
{ fact, title, kind = "fact", scope, directory, project, docId, startLine, endLine, relationType, ttl, keep, tags, supersedes },
|
|
75
76
|
ctx = {}
|
|
76
77
|
) {
|
|
78
|
+
if (!['fact', 'directive'].includes(kind)) throw new Error("kind must be 'fact' or 'directive'.");
|
|
77
79
|
const key = requireProjectKey(await resolveScopeKey(scope, { directory, project }, ctx));
|
|
78
80
|
const entries = await readMemory(key);
|
|
79
81
|
|
|
@@ -87,7 +89,7 @@ export async function rememberFact(
|
|
|
87
89
|
let supersededInfo = "";
|
|
88
90
|
if (!duplicate) {
|
|
89
91
|
const [date, time] = today().split(" ");
|
|
90
|
-
const meta = { ttl, tags };
|
|
92
|
+
const meta = { ttl, tags, kind };
|
|
91
93
|
if (keep) meta.keep = "1";
|
|
92
94
|
if (supersedes) {
|
|
93
95
|
const targetIdx = resolveFactIndex(entries, supersedes);
|
|
@@ -109,6 +111,7 @@ export async function rememberFact(
|
|
|
109
111
|
if (!meta.id) meta.id = nextFactId(entries);
|
|
110
112
|
entries.push(formatFactEntry({ date, time, text, meta }));
|
|
111
113
|
await writeMemory(key, entries);
|
|
114
|
+
if (key === GLOBAL_KEY && kind === "directive") await syncGlobalPersonaPrompts();
|
|
112
115
|
}
|
|
113
116
|
|
|
114
117
|
let linkInfo = "";
|
|
@@ -189,6 +192,7 @@ export async function recallFacts(
|
|
|
189
192
|
if (isKeepFact(factLine)) badges.push("KEEP");
|
|
190
193
|
if (isSuperseded(factLine)) badges.push("SUPERSEDED");
|
|
191
194
|
if (meta.inject === "1") badges.push("INJECT");
|
|
195
|
+
if (isDirectiveFact(factLine)) badges.push("DIRECTIVE");
|
|
192
196
|
if (meta.id) badges.push(`id:${meta.id}`);
|
|
193
197
|
if (meta.tags) badges.push(`tags:${meta.tags}`);
|
|
194
198
|
badges.push(`${p.date} ${p.time}`);
|
|
@@ -310,11 +314,13 @@ export async function forgetFacts({ query, scope, force, directory, project }, c
|
|
|
310
314
|
if (!indices.length) return "Not found.";
|
|
311
315
|
|
|
312
316
|
const removable = indices.filter((i) => force || !isKeepFact(entries[i]));
|
|
317
|
+
const removedGlobalDirective = key === GLOBAL_KEY && removable.some((i) => isDirectiveFact(entries[i]));
|
|
313
318
|
const protectedCount = indices.length - removable.length;
|
|
314
319
|
if (removable.length) {
|
|
315
320
|
const removedBodies = removable.map((i) => factBody(entries[i]) || factText(entries[i]));
|
|
316
321
|
for (const i of removable.sort((a, b) => b - a)) entries.splice(i, 1);
|
|
317
322
|
await writeMemory(key, entries);
|
|
323
|
+
if (removedGlobalDirective) await syncGlobalPersonaPrompts();
|
|
318
324
|
try {
|
|
319
325
|
const { getDatabase } = await import("../../db/database.js");
|
|
320
326
|
const { deleteLinksForFacts } = await import("../../graph/knowledge_linker.js");
|
|
@@ -326,7 +332,8 @@ export async function forgetFacts({ query, scope, force, directory, project }, c
|
|
|
326
332
|
return text;
|
|
327
333
|
}
|
|
328
334
|
|
|
329
|
-
export async function updateFactText({ id, newText, title, scope, directory, project }, ctx = {}) {
|
|
335
|
+
export async function updateFactText({ id, newText, title, kind, scope, directory, project }, ctx = {}) {
|
|
336
|
+
if (kind && !['fact', 'directive'].includes(kind)) throw new Error("kind must be 'fact' or 'directive'.");
|
|
330
337
|
const key = requireProjectKey(await resolveScopeKey(scope, { directory, project }, ctx));
|
|
331
338
|
const entries = await readMemory(key);
|
|
332
339
|
const idx = resolveFactIndex(entries, id);
|
|
@@ -335,6 +342,7 @@ export async function updateFactText({ id, newText, title, scope, directory, pro
|
|
|
335
342
|
const p = parseFactEntry(entries[idx]);
|
|
336
343
|
const oldText = p ? p.text : entries[idx];
|
|
337
344
|
const oldBody = factBody(entries[idx]) || oldText;
|
|
345
|
+
const wasDirective = isDirectiveFact(entries[idx]);
|
|
338
346
|
|
|
339
347
|
let { finalTitle, finalFact } = splitTitle(newText, title);
|
|
340
348
|
if (!finalTitle) finalTitle = factTitle(entries[idx]) || autoGenerateTitle(finalFact);
|
|
@@ -343,9 +351,10 @@ export async function updateFactText({ id, newText, title, scope, directory, pro
|
|
|
343
351
|
date: p.date,
|
|
344
352
|
time: p.time,
|
|
345
353
|
text: `**${finalTitle}** — ${finalFact}`,
|
|
346
|
-
meta: p.meta,
|
|
354
|
+
meta: kind ? { ...p.meta, kind } : p.meta,
|
|
347
355
|
});
|
|
348
356
|
await writeMemory(key, entries);
|
|
357
|
+
if (key === GLOBAL_KEY && (wasDirective || isDirectiveFact(entries[idx]))) await syncGlobalPersonaPrompts();
|
|
349
358
|
|
|
350
359
|
let linksUpdated = 0;
|
|
351
360
|
try {
|
|
@@ -391,6 +400,17 @@ export async function updateFactText({ id, newText, title, scope, directory, pro
|
|
|
391
400
|
return `Fact updated${linksUpdated ? `, ${linksUpdated} doc link(s) updated` : ""}`;
|
|
392
401
|
}
|
|
393
402
|
|
|
403
|
+
async function syncGlobalPersonaPrompts() {
|
|
404
|
+
if (process.env.MEMORY_DISABLE_PERSONA_SYNC === "1") return;
|
|
405
|
+
try {
|
|
406
|
+
const { syncPersonaPrompts } = await import("../../prompt_manager.js");
|
|
407
|
+
await syncPersonaPrompts();
|
|
408
|
+
} catch {
|
|
409
|
+
// Memory persistence must not fail because an optional client config is
|
|
410
|
+
// unavailable or read-only. Explicit sync-persona surfaces such failures.
|
|
411
|
+
}
|
|
412
|
+
}
|
|
413
|
+
|
|
394
414
|
export async function memoryInfo(_args = {}, ctx = {}) {
|
|
395
415
|
const effectiveDir = extractEffectiveDir(_args, ctx) || process.cwd();
|
|
396
416
|
const dbPath = join(MEMORY_DIR, "storage", "memory.sqlite");
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
export const MEMORY_ROUTING_POLICY = `MEMORY ROUTING DIRECTIVE:
|
|
2
|
+
- Use remember for concise durable facts, preferences, constraints, conventions, and decisions that should remain hot and frequently available.
|
|
3
|
+
- Use remember_note for high-value long-form internal memory: decision rationale, investigations, research/experiment results, implementation context, and handoffs that may matter later but should not be auto-injected every session.
|
|
4
|
+
- Use ingest_document for external reusable source material such as files, documentation, codebases, URLs, reports, specifications, and authoritative research sources.
|
|
5
|
+
- Do not save transient conversation noise, routine progress chatter, or disposable intermediate output into either Notebook memory or RAG Memory Notes.
|
|
6
|
+
- Do not duplicate a long note body into remember. When both hot orientation and detailed cold context are useful, save one concise Notebook fact and one detailed RAG Memory Note, then link the fact to the note/document when appropriate.
|
|
7
|
+
- When searching cold memory or knowledge and you first need to identify the right source, prefer query_knowledge_base with resultMode="index", inspect the compact candidates, then expand only the selected doc_id with manage_knowledge_base(action="read_document"). Use resultMode="snippet" when you already want retrieved passage content.`;
|
|
8
|
+
|
|
9
|
+
export const MEMORY_ROUTING_SHORT =
|
|
10
|
+
"remember = concise hot durable fact; remember_note = long-form cold internal memory; ingest_document = external reusable source. Avoid transient noise and duplicate hot/cold bodies.";
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
import { resolveRagScopeKey } from "../../rag_scope.js";
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Shared RAG Memory Note implementation used by MCP and native OpenCode surfaces.
|
|
5
|
+
*
|
|
6
|
+
* The host agent decides what deserves cold/episodic memory. This helper only
|
|
7
|
+
* resolves visibility and routes the note through the existing RAG ingestion
|
|
8
|
+
* pipeline so both tool surfaces keep identical storage semantics.
|
|
9
|
+
*/
|
|
10
|
+
export async function rememberNote(
|
|
11
|
+
{
|
|
12
|
+
title,
|
|
13
|
+
content,
|
|
14
|
+
scope = "project",
|
|
15
|
+
kind = "note",
|
|
16
|
+
tags = null,
|
|
17
|
+
directory = null,
|
|
18
|
+
project = null,
|
|
19
|
+
generateEmbeddings = true,
|
|
20
|
+
},
|
|
21
|
+
ctx = {}
|
|
22
|
+
) {
|
|
23
|
+
const effectiveDirectory = directory || project || ctx.directory || null;
|
|
24
|
+
const projectScope = await resolveRagScopeKey(scope || "project", {
|
|
25
|
+
worktree: ctx.worktree ?? null,
|
|
26
|
+
directory: effectiveDirectory,
|
|
27
|
+
});
|
|
28
|
+
|
|
29
|
+
const { ingestNote } = await import("../../ingest/pipeline.js");
|
|
30
|
+
const result = await ingestNote({
|
|
31
|
+
title,
|
|
32
|
+
content,
|
|
33
|
+
kind,
|
|
34
|
+
tags,
|
|
35
|
+
generateEmbeddings: generateEmbeddings !== false,
|
|
36
|
+
projectScope,
|
|
37
|
+
});
|
|
38
|
+
|
|
39
|
+
return {
|
|
40
|
+
status: "success",
|
|
41
|
+
docId: result.docId,
|
|
42
|
+
blobHash: result.blobHash,
|
|
43
|
+
path: result.path,
|
|
44
|
+
title: result.title,
|
|
45
|
+
sourceType: result.sourceType,
|
|
46
|
+
kind: result.kind,
|
|
47
|
+
tags: result.tags,
|
|
48
|
+
sectionsCount: result.sectionsCount,
|
|
49
|
+
microChunksCount: result.microChunksCount,
|
|
50
|
+
deduplicated: result.deduplicated,
|
|
51
|
+
scope: result.projectScope,
|
|
52
|
+
};
|
|
53
|
+
}
|
|
@@ -0,0 +1,169 @@
|
|
|
1
|
+
import { resolveRagScopeKeys } from "../../rag_scope.js";
|
|
2
|
+
import { getConfig } from "../../config/config_manager.js";
|
|
3
|
+
import { hybridQuery, batchHybridQuery } from "../../retrieval/retriever.js";
|
|
4
|
+
|
|
5
|
+
function normalizeResultMode(resultMode) {
|
|
6
|
+
return resultMode === "index" ? "index" : "snippet";
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
function formatTimestamp(value) {
|
|
10
|
+
if (value === null || value === undefined || value === "") return null;
|
|
11
|
+
const numeric = Number(value);
|
|
12
|
+
const date = Number.isFinite(numeric) ? new Date(numeric) : new Date(value);
|
|
13
|
+
return Number.isNaN(date.getTime()) ? String(value) : date.toISOString();
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
async function ensureRagFresh() {
|
|
17
|
+
if (getConfig().mode !== "hybrid-sync") return;
|
|
18
|
+
const { ensureReverseSync } = await import("../../db/sync_queue.js");
|
|
19
|
+
await ensureReverseSync();
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
function formatIdentityLines(result) {
|
|
23
|
+
const lines = [];
|
|
24
|
+
if (result.doc_id) lines.push(`Doc ID: ${result.doc_id}`);
|
|
25
|
+
if (result.source_type) lines.push(`Source: ${result.source_type}`);
|
|
26
|
+
if (result.note_kind) lines.push(`Kind: ${result.note_kind}`);
|
|
27
|
+
if (Array.isArray(result.tags) && result.tags.length > 0) {
|
|
28
|
+
lines.push(`Tags: ${result.tags.join(", ")}`);
|
|
29
|
+
}
|
|
30
|
+
return lines;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
export function formatSnippetResult(result, rank, headingLevel = 3) {
|
|
34
|
+
const hashes = "#".repeat(Math.max(1, headingLevel));
|
|
35
|
+
let header = `${hashes} [${rank}] ${result.doc_title || "Untitled"}`;
|
|
36
|
+
if (result.heading) header += ` > ${result.heading}`;
|
|
37
|
+
if (result.breadcrumbs) header += ` (${result.breadcrumbs})`;
|
|
38
|
+
|
|
39
|
+
const bodyLines = [
|
|
40
|
+
...formatIdentityLines(result),
|
|
41
|
+
`Score: ${(result.score || 0).toFixed(4)}`,
|
|
42
|
+
];
|
|
43
|
+
|
|
44
|
+
if (result.defined_symbols && result.defined_symbols.length > 0) {
|
|
45
|
+
bodyLines.push(`Defined Symbols: ${result.defined_symbols.join(", ")}`);
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
const body = result.snippet || result.full_section_content || "";
|
|
49
|
+
return `${header}\n${bodyLines.join("\n")}\n\n${body}`;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
export function formatIndexResult(result, rank, headingLevel = 3) {
|
|
53
|
+
const hashes = "#".repeat(Math.max(1, headingLevel));
|
|
54
|
+
let header = `${hashes} [${rank}] ${result.doc_title || "Untitled"}`;
|
|
55
|
+
if (result.heading) header += ` > ${result.heading}`;
|
|
56
|
+
if (result.breadcrumbs) header += ` (${result.breadcrumbs})`;
|
|
57
|
+
|
|
58
|
+
const lines = [
|
|
59
|
+
...formatIdentityLines(result),
|
|
60
|
+
`Score: ${(result.score || 0).toFixed(4)}`,
|
|
61
|
+
];
|
|
62
|
+
|
|
63
|
+
if (result.retrieval_policy) lines.push(`Policy: ${result.retrieval_policy}`);
|
|
64
|
+
const createdAt = formatTimestamp(result.doc_created_at);
|
|
65
|
+
const updatedAt = formatTimestamp(result.doc_updated_at);
|
|
66
|
+
if (createdAt) lines.push(`Created: ${createdAt}`);
|
|
67
|
+
if (updatedAt) lines.push(`Updated: ${updatedAt}`);
|
|
68
|
+
|
|
69
|
+
return `${header}\n${lines.join("\n")}`;
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
function formatResult(result, rank, resultMode, headingLevel) {
|
|
73
|
+
return resultMode === "index"
|
|
74
|
+
? formatIndexResult(result, rank, headingLevel)
|
|
75
|
+
: formatSnippetResult(result, rank, headingLevel);
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
export async function runSingleRagQuery(
|
|
79
|
+
{
|
|
80
|
+
query,
|
|
81
|
+
limit = 5,
|
|
82
|
+
instruction = null,
|
|
83
|
+
generateEmbeddings = true,
|
|
84
|
+
scope = "all",
|
|
85
|
+
directory = null,
|
|
86
|
+
project = null,
|
|
87
|
+
resultMode = "snippet",
|
|
88
|
+
},
|
|
89
|
+
ctx = {}
|
|
90
|
+
) {
|
|
91
|
+
const mode = normalizeResultMode(resultMode);
|
|
92
|
+
const activeConfig = getConfig();
|
|
93
|
+
await ensureRagFresh();
|
|
94
|
+
|
|
95
|
+
const effectiveDirectory = directory || project || ctx.directory || null;
|
|
96
|
+
const scopeKeys = await resolveRagScopeKeys(scope || "all", {
|
|
97
|
+
worktree: ctx.worktree ?? null,
|
|
98
|
+
directory: effectiveDirectory,
|
|
99
|
+
});
|
|
100
|
+
|
|
101
|
+
const results = await hybridQuery({
|
|
102
|
+
query,
|
|
103
|
+
limit,
|
|
104
|
+
generateEmbeddings: generateEmbeddings !== false,
|
|
105
|
+
instruction: instruction || null,
|
|
106
|
+
scopeKeys,
|
|
107
|
+
includeGraphContext: mode !== "index",
|
|
108
|
+
policyExpansion: mode !== "index",
|
|
109
|
+
});
|
|
110
|
+
|
|
111
|
+
if (!results || results.length === 0) {
|
|
112
|
+
return `[Active Model: ${activeConfig.embeddingModel} | Mode: ${mode}]\nNo matching knowledge found for query.`;
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
const header = `[Active Model: ${activeConfig.embeddingModel} | Fusion: ${activeConfig.fusionAlgorithm.toUpperCase()} | Mode: ${mode}]\n\n`;
|
|
116
|
+
const formatted = results
|
|
117
|
+
.map((result, index) => formatResult(result, index + 1, mode, 3))
|
|
118
|
+
.join("\n\n---\n\n");
|
|
119
|
+
|
|
120
|
+
return header + formatted;
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
export async function runBatchRagQuery(
|
|
124
|
+
{
|
|
125
|
+
queries,
|
|
126
|
+
limit = 5,
|
|
127
|
+
instruction = null,
|
|
128
|
+
generateEmbeddings = true,
|
|
129
|
+
scope = "all",
|
|
130
|
+
directory = null,
|
|
131
|
+
project = null,
|
|
132
|
+
resultMode = "snippet",
|
|
133
|
+
},
|
|
134
|
+
ctx = {}
|
|
135
|
+
) {
|
|
136
|
+
const mode = normalizeResultMode(resultMode);
|
|
137
|
+
const activeConfig = getConfig();
|
|
138
|
+
await ensureRagFresh();
|
|
139
|
+
|
|
140
|
+
const effectiveDirectory = directory || project || ctx.directory || null;
|
|
141
|
+
const scopeKeys = await resolveRagScopeKeys(scope || "all", {
|
|
142
|
+
worktree: ctx.worktree ?? null,
|
|
143
|
+
directory: effectiveDirectory,
|
|
144
|
+
});
|
|
145
|
+
|
|
146
|
+
const allResults = await batchHybridQuery(queries, {
|
|
147
|
+
limit,
|
|
148
|
+
generateEmbeddings: generateEmbeddings !== false,
|
|
149
|
+
instruction: instruction || null,
|
|
150
|
+
scopeKeys,
|
|
151
|
+
includeGraphContext: mode !== "index",
|
|
152
|
+
policyExpansion: mode !== "index",
|
|
153
|
+
});
|
|
154
|
+
|
|
155
|
+
const formatted = allResults
|
|
156
|
+
.map((results, queryIndex) => {
|
|
157
|
+
const queryHeader = `### Query [${queryIndex + 1}]: "${queries[queryIndex]}"\n\n`;
|
|
158
|
+
if (!results || results.length === 0) {
|
|
159
|
+
return queryHeader + "No matching knowledge found for this query.";
|
|
160
|
+
}
|
|
161
|
+
const items = results
|
|
162
|
+
.map((result, resultIndex) => formatResult(result, resultIndex + 1, mode, 4))
|
|
163
|
+
.join("\n\n---\n\n");
|
|
164
|
+
return queryHeader + items;
|
|
165
|
+
})
|
|
166
|
+
.join("\n\n===\n\n");
|
|
167
|
+
|
|
168
|
+
return `[Active Model: ${activeConfig.embeddingModel} | Fusion: ${activeConfig.fusionAlgorithm.toUpperCase()} | Mode: ${mode} | ${queries.length} queries]\n\n${formatted}`;
|
|
169
|
+
}
|
|
@@ -1,9 +1,11 @@
|
|
|
1
|
-
import { registerMemoryTools } from "./memory_tools.js";
|
|
2
|
-
import { registerIdentityTools } from "./identity_tools.js";
|
|
3
|
-
import { registerRagTools } from "./rag_tools.js";
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
1
|
+
import { registerMemoryTools } from "./memory_tools.js";
|
|
2
|
+
import { registerIdentityTools } from "./identity_tools.js";
|
|
3
|
+
import { registerRagTools } from "./rag_tools.js";
|
|
4
|
+
import { registerNoteTools } from "./note_tools.js";
|
|
5
|
+
|
|
6
|
+
export function registerAllTools(server) {
|
|
7
|
+
registerMemoryTools(server);
|
|
8
|
+
registerIdentityTools(server);
|
|
9
|
+
registerRagTools(server);
|
|
10
|
+
registerNoteTools(server);
|
|
11
|
+
}
|
|
@@ -16,6 +16,7 @@ export function registerMemoryTools(server) {
|
|
|
16
16
|
description:
|
|
17
17
|
"Save an important, durable fact to memory. Only use for high-signal information " +
|
|
18
18
|
"(name, goals, constraints, tech preferences, project conventions). " +
|
|
19
|
+
"Set kind='directive' only for active user-approved personality, behavior, tone, style, preference, or working instructions; use kind='fact' for descriptive context. " +
|
|
19
20
|
"directory: optional workspace/project directory path to target (ensures saving into the project store even from external cwd). " +
|
|
20
21
|
"docId/startLine/endLine/relationType are OPTIONAL and only used to link the fact to a " +
|
|
21
22
|
"Knowledge Base document or line range; omit them when no linking is needed. " +
|
|
@@ -29,6 +30,7 @@ export function registerMemoryTools(server) {
|
|
|
29
30
|
inputSchema: z.object({
|
|
30
31
|
fact: z.string().describe("The fact to remember, written in English"),
|
|
31
32
|
title: optStr().describe("Optional title for the fact. If not specified, one is auto-generated."),
|
|
33
|
+
kind: z.enum(["fact", "directive"]).nullish().transform((v) => v || "fact").describe("'fact' (context) or 'directive' (active personalization/working instruction)"),
|
|
32
34
|
scope: defStr("project").describe("'project' (default) or 'global'"),
|
|
33
35
|
directory: optStr().describe("Optional workspace/project directory path to target when scope='project' (e.g. 'F:/projects/my-app')"),
|
|
34
36
|
project: optStr().describe("Alias for directory"),
|
|
@@ -109,11 +111,12 @@ export function registerMemoryTools(server) {
|
|
|
109
111
|
{
|
|
110
112
|
description:
|
|
111
113
|
"Update the text of an existing fact by number (from recall), id, or text match, " +
|
|
112
|
-
"preserving its original date and metadata. Linked Knowledge Base documents are re-pointed to the new text.",
|
|
114
|
+
"preserving its original date and metadata. kind can optionally reclassify it as context ('fact') or active personalization ('directive'). Linked Knowledge Base documents are re-pointed to the new text.",
|
|
113
115
|
inputSchema: z.object({
|
|
114
116
|
id: z.string().describe("Number (from recall), metadata id, or text of the fact to update"),
|
|
115
117
|
newText: z.string().describe("New fact text"),
|
|
116
118
|
title: optStr().describe("Optional new title for the fact"),
|
|
119
|
+
kind: z.enum(["fact", "directive"]).nullish().describe("Optional new semantic kind: 'fact' or 'directive'"),
|
|
117
120
|
scope: defStr("project").describe("'project' (default) or 'global'"),
|
|
118
121
|
directory: optStr().describe("Optional workspace/project directory path"),
|
|
119
122
|
project: optStr().describe("Alias for directory"),
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
import * as z from "zod/v4";
|
|
2
|
+
import { optStr, defBool } from "./helpers.js";
|
|
3
|
+
import { rememberNote } from "./core/note_core.js";
|
|
4
|
+
|
|
5
|
+
const NOTE_KINDS = ["decision", "research", "context", "handoff", "note"];
|
|
6
|
+
|
|
7
|
+
export function registerNoteTools(server) {
|
|
8
|
+
server.registerTool(
|
|
9
|
+
"remember_note",
|
|
10
|
+
{
|
|
11
|
+
description:
|
|
12
|
+
"Save high-value long-form or episodic context as a cold RAG Memory Note. " +
|
|
13
|
+
"Use this for decisions with rationale, research/experiment results, investigations, handoffs, and detailed context that may matter later but should NOT be injected into every session. " +
|
|
14
|
+
"Use remember() instead for concise durable facts that should stay hot/automatically available. " +
|
|
15
|
+
"Use ingest_document() instead for external reusable truth sources such as files, URLs, documentation, reports, or codebases. " +
|
|
16
|
+
"The note is indexed in the existing RAG knowledge base and can later be found semantically and expanded by document ID.",
|
|
17
|
+
inputSchema: z.object({
|
|
18
|
+
title: z.string().trim().min(1).describe("Concise descriptive title for the memory note"),
|
|
19
|
+
content: z.string().refine((value) => value.trim().length > 0, "RAG Memory Note content must not be empty").describe("Full long-form note content to preserve"),
|
|
20
|
+
scope: z.enum(["project", "global"]).nullish().transform((v) => v || "project").describe("Visibility: current Git project (default) or global"),
|
|
21
|
+
kind: z.enum(NOTE_KINDS).catch("note").nullish().transform((v) => v || "note").describe("Note kind: decision, research, context, handoff, or note"),
|
|
22
|
+
tags: optStr().describe("Optional comma-separated tags; normalized to lowercase unique values"),
|
|
23
|
+
directory: optStr().describe("Optional workspace/project directory path to target"),
|
|
24
|
+
project: optStr().describe("Alias for directory"),
|
|
25
|
+
generateEmbeddings: defBool(true).describe("Compute dense vector embeddings; set false for offline/tests"),
|
|
26
|
+
}),
|
|
27
|
+
},
|
|
28
|
+
async (args) => {
|
|
29
|
+
const result = await rememberNote(args);
|
|
30
|
+
return {
|
|
31
|
+
content: [{ type: "text", text: JSON.stringify(result, null, 2) }],
|
|
32
|
+
};
|
|
33
|
+
}
|
|
34
|
+
);
|
|
35
|
+
}
|