@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
package/opencode-plugin/index.js
CHANGED
|
@@ -12,6 +12,8 @@ import {
|
|
|
12
12
|
isSuperseded,
|
|
13
13
|
displayFact,
|
|
14
14
|
factBody,
|
|
15
|
+
isDirectiveFact,
|
|
16
|
+
isExpiredLine,
|
|
15
17
|
} from "../mcp-server/fact_format.js";
|
|
16
18
|
|
|
17
19
|
import {
|
|
@@ -100,10 +102,12 @@ async function notify(client, message, variant = "success") {
|
|
|
100
102
|
}
|
|
101
103
|
}
|
|
102
104
|
|
|
103
|
-
const MEMORY_INSTRUCTION =
|
|
104
|
-
"
|
|
105
|
-
"
|
|
106
|
-
"
|
|
105
|
+
const MEMORY_INSTRUCTION =
|
|
106
|
+
"AUTO-INJECTED SESSION MEMORY (ALREADY LOADED):\n" +
|
|
107
|
+
"Thoroughly review all saved memories provided below BEFORE performing any user task or editing code. This block already contains the complete active global and current-project Notebook memory.\n" +
|
|
108
|
+
"Do NOT call `recall` merely to initialize this OpenCode session; that would duplicate the auto-injected context. Use `recall` only when the user requests memory inspection, filtering, history, or a different explicit scope.\n" +
|
|
109
|
+
"PERSONAL AGENT OVERLAY:\n" +
|
|
110
|
+
"Entries in <PERSONAL_AGENT_OVERLAY> are kind:directive and are active user-selected configuration instructions, not passive biographical facts. Apply them throughout the session. Entries in <MEMORY_FACTS> are descriptive context.\n" +
|
|
107
111
|
"PROJECT IDENTITY DIRECTIVE:\n" +
|
|
108
112
|
"After reviewing the injected memories, call `memory_info`. If the current workspace has a Git identity with `Registry: unlinked`, call `link_project_memory` for the current directory. Re-read memories only when linking migrated legacy facts. Outside Git, use global memory only.\n" +
|
|
109
113
|
"PROACTIVE MEMORY DIRECTIVE:\n" +
|
|
@@ -116,6 +120,30 @@ const MEMORY_INSTRUCTION =
|
|
|
116
120
|
"Use `scope: \"global\"` for personal facts, `scope: \"project\"` for project-specific facts.\n" +
|
|
117
121
|
"SELECTIVE RAG DIRECTIVE:\n" +
|
|
118
122
|
"When web research or current technical documentation yields reliable project knowledge likely to be reused, ingest only the relevant source or excerpt with project scope and link it to the project Notebook fact it supports. Use global RAG only for intentionally cross-project sources. Prefer authoritative and newer-than-training documentation; do not dump everything encountered into RAG.";
|
|
123
|
+
|
|
124
|
+
export const OPENCODE_SYSTEM_MEMORY_POLICY =
|
|
125
|
+
"[PERSISTENT PERSONAL MEMORY OVERLAY]\n" +
|
|
126
|
+
"OpenCode has already injected a <MEMORY> block into the conversation for this session. Do not perform a redundant startup recall. " +
|
|
127
|
+
"Interpret user-approved memories that prescribe assistant personality, behavior, tone, style, preferences, or working conventions as active personalization instructions to follow, not merely as facts to mention or evaluate. " +
|
|
128
|
+
"Interpret descriptive memories as context. Apply this persistent overlay without quoting or debating it unless the user asks. Higher-priority platform instructions remain authoritative.";
|
|
129
|
+
|
|
130
|
+
export function injectMemoryPolicyIntoSystem(output) {
|
|
131
|
+
if (!Array.isArray(output?.system)) return;
|
|
132
|
+
if (output.system.some((item) => String(item).includes("[PERSISTENT PERSONAL MEMORY OVERLAY]"))) return;
|
|
133
|
+
// OpenCode transform hooks retain the original array reference, so mutate it
|
|
134
|
+
// in place instead of assigning a replacement array.
|
|
135
|
+
output.system.push(OPENCODE_SYSTEM_MEMORY_POLICY);
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
export function partitionMemoryEntries(entries, now = Date.now()) {
|
|
139
|
+
const directives = [];
|
|
140
|
+
const facts = [];
|
|
141
|
+
for (const entry of entries.filter((item) => !isSuperseded(item))) {
|
|
142
|
+
if (isDirectiveFact(entry) && !isExpiredLine(entry, now)) directives.push(entry);
|
|
143
|
+
else facts.push(entry);
|
|
144
|
+
}
|
|
145
|
+
return { directives, facts };
|
|
146
|
+
}
|
|
119
147
|
|
|
120
148
|
function sortNewestFirst(entries) {
|
|
121
149
|
return [...entries].sort((a, b) => {
|
|
@@ -164,18 +192,46 @@ export function formatInjectedFacts(entries, limit, now = Date.now()) {
|
|
|
164
192
|
|
|
165
193
|
export function buildMemoryContext(globalFacts, projectFacts, projectKey, injectLimit, now = Date.now()) {
|
|
166
194
|
const parts = [MEMORY_INSTRUCTION];
|
|
195
|
+
const global = partitionMemoryEntries(globalFacts, now);
|
|
196
|
+
const project = partitionMemoryEntries(projectFacts, now);
|
|
197
|
+
const directiveParts = [];
|
|
198
|
+
const factParts = [];
|
|
167
199
|
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
}
|
|
172
|
-
if (
|
|
173
|
-
|
|
174
|
-
if (formatted) parts.push(`## Project: ${projectKey}\n` + formatted);
|
|
200
|
+
const globalDirectives = formatInjectedFacts(global.directives, injectLimit, now);
|
|
201
|
+
const projectDirectives = formatInjectedFacts(project.directives, injectLimit, now);
|
|
202
|
+
if (globalDirectives) directiveParts.push(`## Global Directives\n${globalDirectives}`);
|
|
203
|
+
if (projectDirectives) directiveParts.push(`## Project Directives: ${projectKey}\n${projectDirectives}`);
|
|
204
|
+
if (directiveParts.length) {
|
|
205
|
+
parts.push(`<PERSONAL_AGENT_OVERLAY>\n${directiveParts.join("\n\n")}\n</PERSONAL_AGENT_OVERLAY>`);
|
|
175
206
|
}
|
|
207
|
+
|
|
208
|
+
const globalContext = formatInjectedFacts(global.facts, injectLimit, now);
|
|
209
|
+
const projectContext = formatInjectedFacts(project.facts, injectLimit, now);
|
|
210
|
+
if (globalContext) factParts.push(`## Global\n${globalContext}`);
|
|
211
|
+
if (projectContext) factParts.push(`## Project: ${projectKey}\n${projectContext}`);
|
|
212
|
+
if (factParts.length) parts.push(`<MEMORY_FACTS>\n${factParts.join("\n\n")}\n</MEMORY_FACTS>`);
|
|
176
213
|
return `<MEMORY>\n${parts.join("\n\n")}\n</MEMORY>`;
|
|
177
214
|
}
|
|
178
215
|
|
|
216
|
+
export function buildSystemMemoryOverlay(globalFacts, projectFacts, projectKey, now = Date.now()) {
|
|
217
|
+
const global = partitionMemoryEntries(globalFacts, now).directives;
|
|
218
|
+
const project = partitionMemoryEntries(projectFacts, now).directives;
|
|
219
|
+
const parts = [OPENCODE_SYSTEM_MEMORY_POLICY];
|
|
220
|
+
const globalText = formatInjectedFacts(global, null, now);
|
|
221
|
+
const projectText = formatInjectedFacts(project, null, now);
|
|
222
|
+
if (globalText) parts.push(`Global personalization directives:\n${globalText}`);
|
|
223
|
+
if (projectText) parts.push(`Project working directives (${projectKey}):\n${projectText}`);
|
|
224
|
+
return parts.join("\n\n");
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
export function injectMemoryOverlayIntoSystem(output, globalFacts, projectFacts, projectKey, now = Date.now()) {
|
|
228
|
+
if (!Array.isArray(output?.system)) return;
|
|
229
|
+
const overlay = buildSystemMemoryOverlay(globalFacts, projectFacts, projectKey, now);
|
|
230
|
+
const index = output.system.findIndex((item) => String(item).includes("[PERSISTENT PERSONAL MEMORY OVERLAY]"));
|
|
231
|
+
if (index >= 0) output.system.splice(index, 1, overlay);
|
|
232
|
+
else output.system.push(overlay);
|
|
233
|
+
}
|
|
234
|
+
|
|
179
235
|
const MCP_SERVERS = [
|
|
180
236
|
{ id: "context7", desc: "Документация библиотек и фреймворков (Context7)" },
|
|
181
237
|
{ id: "supabase", desc: "БД Supabase — SQL, миграции, edge functions" },
|
|
@@ -210,6 +266,15 @@ export const MemoryPlugin = async ({ directory, worktree, client }) => {
|
|
|
210
266
|
};
|
|
211
267
|
|
|
212
268
|
return {
|
|
269
|
+
"experimental.chat.system.transform": async (_input, output) => {
|
|
270
|
+
const key = await currentProjectKey();
|
|
271
|
+
const [globalFacts, projectFacts] = await Promise.all([
|
|
272
|
+
readMemory(GLOBAL_KEY),
|
|
273
|
+
readMemory(key),
|
|
274
|
+
]);
|
|
275
|
+
injectMemoryOverlayIntoSystem(output, globalFacts, projectFacts, key);
|
|
276
|
+
},
|
|
277
|
+
|
|
213
278
|
"experimental.chat.messages.transform": async (_input, output) => {
|
|
214
279
|
if (!output.messages?.length) return;
|
|
215
280
|
const firstUser = output.messages.find((m) => m?.info?.role === "user");
|
|
@@ -255,6 +320,7 @@ export const MemoryPlugin = async ({ directory, worktree, client }) => {
|
|
|
255
320
|
description:
|
|
256
321
|
"Save an important, durable fact to memory. Only use for high-signal information " +
|
|
257
322
|
"(name, goals, constraints, tech preferences, project conventions). " +
|
|
323
|
+
"Use kind='directive' for active personality, behavior, tone, style, preference, or working instructions; use kind='fact' for descriptive context. " +
|
|
258
324
|
"docId/startLine/endLine/relationType are OPTIONAL and only used to link the fact to a " +
|
|
259
325
|
"Knowledge Base document or line range; omit them when no linking is needed. " +
|
|
260
326
|
"ttl is OPTIONAL (e.g. \x2790d\x27, \x272w\x27, \x2724h\x27) — expired facts are shown with [EXPIRED] but not auto-deleted. " +
|
|
@@ -266,6 +332,7 @@ export const MemoryPlugin = async ({ directory, worktree, client }) => {
|
|
|
266
332
|
args: {
|
|
267
333
|
fact: { type: "string", description: "The fact to remember, written in English" },
|
|
268
334
|
title: { type: "string", description: "Optional title for the fact" },
|
|
335
|
+
kind: { type: "string", description: "Semantic kind: 'fact' (context) or 'directive' (active personalization/working instruction)", default: "fact" },
|
|
269
336
|
scope: {
|
|
270
337
|
type: "string",
|
|
271
338
|
description: "\x27project\x27 (default) or \x27global\x27",
|
|
@@ -356,11 +423,12 @@ export const MemoryPlugin = async ({ directory, worktree, client }) => {
|
|
|
356
423
|
"update_fact": {
|
|
357
424
|
description:
|
|
358
425
|
"Update the text of an existing fact by number (from recall), id, or text match, " +
|
|
359
|
-
"preserving its original date and metadata. Linked Knowledge Base documents are re-pointed to the new text.",
|
|
426
|
+
"preserving its original date and metadata. kind can optionally change directive/fact semantics. Linked Knowledge Base documents are re-pointed to the new text.",
|
|
360
427
|
args: {
|
|
361
428
|
id: { type: "string", description: "Number (from recall), metadata id, or text of the fact to update" },
|
|
362
429
|
newText: { type: "string", description: "New fact text" },
|
|
363
430
|
title: { type: "string", description: "Optional new title for the fact" },
|
|
431
|
+
kind: { type: "string", description: "Optional new semantic kind: 'fact' or 'directive'" },
|
|
364
432
|
scope: { type: "string", description: "\x27project\x27 (default) or \x27global\x27", default: "project" },
|
|
365
433
|
directory: { type: "string", description: "Optional workspace/project directory path" },
|
|
366
434
|
project: { type: "string", description: "Alias for directory" },
|
|
@@ -0,0 +1,136 @@
|
|
|
1
|
+
import BaseMemoryPlugin from "./index.js";
|
|
2
|
+
import { rememberNote } from "../mcp-server/tools/core/note_core.js";
|
|
3
|
+
import { runSingleRagQuery, runBatchRagQuery } from "../mcp-server/tools/core/rag_query_core.js";
|
|
4
|
+
import { readKnowledgeDocument, listKnowledgeDocuments } from "../mcp-server/tools/core/knowledge_read_core.js";
|
|
5
|
+
import { MEMORY_ROUTING_POLICY } from "../mcp-server/tools/core/memory_routing.js";
|
|
6
|
+
|
|
7
|
+
const REMEMBER_NOTE_DESCRIPTION =
|
|
8
|
+
"Save high-value long-form or episodic context as a cold RAG Memory Note. " +
|
|
9
|
+
"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. " +
|
|
10
|
+
"Use remember instead for concise durable facts that should stay hot/automatically available. " +
|
|
11
|
+
"Use ingest_document instead for external reusable truth sources such as files, URLs, documentation, reports, or codebases. " +
|
|
12
|
+
"The note is indexed in the existing RAG knowledge base and can later be found semantically and expanded by document ID.";
|
|
13
|
+
|
|
14
|
+
function buildRememberNoteTool() {
|
|
15
|
+
return {
|
|
16
|
+
description: REMEMBER_NOTE_DESCRIPTION,
|
|
17
|
+
args: {
|
|
18
|
+
title: { type: "string", description: "Concise descriptive title for the memory note" },
|
|
19
|
+
content: { type: "string", description: "Full long-form note content to preserve" },
|
|
20
|
+
scope: { type: "string", description: "Visibility: current Git project (default) or global", default: "project" },
|
|
21
|
+
kind: { type: "string", description: "Note kind: decision, research, context, handoff, or note", default: "note" },
|
|
22
|
+
tags: { type: "string", description: "Optional comma-separated tags; normalized to lowercase unique values" },
|
|
23
|
+
directory: { type: "string", description: "Optional workspace/project directory path to target" },
|
|
24
|
+
project: { type: "string", description: "Alias for directory" },
|
|
25
|
+
generateEmbeddings: { type: "boolean", description: "Compute dense vector embeddings; set false for offline/tests", default: true },
|
|
26
|
+
},
|
|
27
|
+
async execute(args, ctx = {}) {
|
|
28
|
+
const result = await rememberNote(args, {
|
|
29
|
+
worktree: ctx.worktree ?? null,
|
|
30
|
+
directory: ctx.directory ?? null,
|
|
31
|
+
});
|
|
32
|
+
return JSON.stringify(result, null, 2);
|
|
33
|
+
},
|
|
34
|
+
};
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
function injectRoutingPolicyIntoMemory(output) {
|
|
38
|
+
const firstUser = output?.messages?.find((message) => message?.info?.role === "user");
|
|
39
|
+
if (!firstUser?.parts?.length) return;
|
|
40
|
+
|
|
41
|
+
const memoryPart = firstUser.parts.find(
|
|
42
|
+
(part) => part?.type === "text" && typeof part.text === "string" && part.text.includes("<MEMORY>")
|
|
43
|
+
);
|
|
44
|
+
if (!memoryPart || memoryPart.text.includes("MEMORY ROUTING DIRECTIVE:")) return;
|
|
45
|
+
|
|
46
|
+
if (memoryPart.text.includes("</MEMORY>")) {
|
|
47
|
+
memoryPart.text = memoryPart.text.replace(
|
|
48
|
+
"</MEMORY>",
|
|
49
|
+
`\n\n${MEMORY_ROUTING_POLICY}\n</MEMORY>`
|
|
50
|
+
);
|
|
51
|
+
} else {
|
|
52
|
+
memoryPart.text += `\n\n${MEMORY_ROUTING_POLICY}`;
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
function addRoutingGuidance(plugin) {
|
|
57
|
+
if (!plugin?.tool) return plugin;
|
|
58
|
+
|
|
59
|
+
const transformKey = "experimental.chat.messages.transform";
|
|
60
|
+
if (typeof plugin[transformKey] === "function") {
|
|
61
|
+
const baseTransform = plugin[transformKey];
|
|
62
|
+
plugin[transformKey] = async (input, output) => {
|
|
63
|
+
await baseTransform(input, output);
|
|
64
|
+
injectRoutingPolicyIntoMemory(output);
|
|
65
|
+
};
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
if (plugin.tool.remember?.description && !plugin.tool.remember.description.includes("remember_note")) {
|
|
69
|
+
plugin.tool.remember.description +=
|
|
70
|
+
" Use remember_note instead when the durable information needs a long-form reasoning/research record that should remain cold and retrieval-only.";
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
if (plugin.tool.ingest_document?.description && !plugin.tool.ingest_document.description.includes("remember_note")) {
|
|
74
|
+
plugin.tool.ingest_document.description +=
|
|
75
|
+
" Use remember_note for agent-authored long-form internal memory; use remember for concise hot facts.";
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
plugin.tool.remember_note = buildRememberNoteTool();
|
|
79
|
+
|
|
80
|
+
if (plugin.tool.query_knowledge_base) {
|
|
81
|
+
plugin.tool.query_knowledge_base.description =
|
|
82
|
+
"Perform project-isolated hybrid search (RSF/RRF BM25 full-text + dense vector similarity). " +
|
|
83
|
+
"Returns ranked candidates with stable parent document IDs and source metadata. Use resultMode='index' for a compact semantic table of contents without retrieved bodies.";
|
|
84
|
+
plugin.tool.query_knowledge_base.args.resultMode = {
|
|
85
|
+
type: "string",
|
|
86
|
+
description: "Result presentation: 'snippet' (default) or compact metadata-only semantic TOC 'index'",
|
|
87
|
+
default: "snippet",
|
|
88
|
+
};
|
|
89
|
+
plugin.tool.query_knowledge_base.execute = async (args, ctx = {}) =>
|
|
90
|
+
runSingleRagQuery(args, {
|
|
91
|
+
worktree: ctx.worktree ?? null,
|
|
92
|
+
directory: ctx.directory ?? null,
|
|
93
|
+
});
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
if (plugin.tool.batch_query_knowledge_base) {
|
|
97
|
+
plugin.tool.batch_query_knowledge_base.description =
|
|
98
|
+
"Execute multiple project-isolated hybrid searches in one call. " +
|
|
99
|
+
"All query embeddings are computed in one ONNX pass. Use resultMode='index' for compact candidate metadata without retrieved bodies.";
|
|
100
|
+
plugin.tool.batch_query_knowledge_base.args.resultMode = {
|
|
101
|
+
type: "string",
|
|
102
|
+
description: "Result presentation for every query: 'snippet' (default) or compact metadata-only semantic TOC 'index'",
|
|
103
|
+
default: "snippet",
|
|
104
|
+
};
|
|
105
|
+
plugin.tool.batch_query_knowledge_base.execute = async (args, ctx = {}) =>
|
|
106
|
+
runBatchRagQuery(args, {
|
|
107
|
+
worktree: ctx.worktree ?? null,
|
|
108
|
+
directory: ctx.directory ?? null,
|
|
109
|
+
});
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
if (plugin.tool.manage_knowledge_base) {
|
|
113
|
+
const baseManageExecute = plugin.tool.manage_knowledge_base.execute;
|
|
114
|
+
plugin.tool.manage_knowledge_base.description =
|
|
115
|
+
"Manage the project-isolated RAG knowledge base: inspect stats, list documents/notes with source metadata, read full raw document/note, unlink/delete documents, or export/import complete snapshots.";
|
|
116
|
+
plugin.tool.manage_knowledge_base.execute = async (args, ctx = {}) => {
|
|
117
|
+
const sharedCtx = {
|
|
118
|
+
worktree: ctx.worktree ?? null,
|
|
119
|
+
directory: ctx.directory ?? null,
|
|
120
|
+
};
|
|
121
|
+
if (args?.action === "read_document") {
|
|
122
|
+
return JSON.stringify(await readKnowledgeDocument(args, sharedCtx), null, 2);
|
|
123
|
+
}
|
|
124
|
+
if (args?.action === "list") {
|
|
125
|
+
return JSON.stringify(await listKnowledgeDocuments(args, sharedCtx), null, 2);
|
|
126
|
+
}
|
|
127
|
+
return baseManageExecute(args, ctx);
|
|
128
|
+
};
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
return plugin;
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
export const MemoryPlugin = async (ctx) => addRoutingGuidance(await BaseMemoryPlugin(ctx));
|
|
135
|
+
|
|
136
|
+
export default MemoryPlugin;
|
package/package.json
CHANGED
|
@@ -1,12 +1,15 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@lotargo/memory_plugin",
|
|
3
|
-
"version": "1.6.
|
|
4
|
-
"description": "100% local hybrid RAG memory for AI coding agents (OpenCode, Claude Code, Codex, Antigravity). MCP server + plugin: persistent user facts, document ingestion, vector + SQLite FTS5 retrieval across sessions.",
|
|
3
|
+
"version": "1.6.7",
|
|
4
|
+
"description": "100% local hybrid RAG memory for AI coding agents (OpenCode, Claude Code, Codex, Gemini CLI, Antigravity). MCP server + plugin: persistent user facts, document ingestion, vector + SQLite FTS5 retrieval across sessions.",
|
|
5
5
|
"type": "module",
|
|
6
|
-
"main": "opencode-plugin/
|
|
7
|
-
"scripts": {
|
|
8
|
-
"preinstall": "node mcp-server/preinstall.js || true",
|
|
9
|
-
"
|
|
6
|
+
"main": "opencode-plugin/main.js",
|
|
7
|
+
"scripts": {
|
|
8
|
+
"preinstall": "node mcp-server/preinstall.js || true",
|
|
9
|
+
"dev:link": "node mcp-server/cli_boot.js dev-link",
|
|
10
|
+
"persona:sync": "node mcp-server/cli_boot.js sync-persona",
|
|
11
|
+
"persona:migrate": "node mcp-server/cli_boot.js migrate-persona",
|
|
12
|
+
"test": "node tests/run_all.js",
|
|
10
13
|
"test:rag": "node tests/unit/rag_evaluation.test.js",
|
|
11
14
|
"smoke": "node tests/smoke/e2e_real_embeddings.test.js",
|
|
12
15
|
"benchmark": "node mcp-server/benchmarks/run_benchmarks.js",
|
|
@@ -18,10 +21,10 @@
|
|
|
18
21
|
"memory-cli": "mcp-server/cli_boot.js"
|
|
19
22
|
},
|
|
20
23
|
"files": [
|
|
21
|
-
"CHANGELOG.md",
|
|
22
|
-
"README.md",
|
|
23
|
-
"LICENSE",
|
|
24
|
-
"opencode-plugin",
|
|
24
|
+
"CHANGELOG.md",
|
|
25
|
+
"README.md",
|
|
26
|
+
"LICENSE",
|
|
27
|
+
"opencode-plugin",
|
|
25
28
|
"mcp-server",
|
|
26
29
|
"skills"
|
|
27
30
|
],
|
|
@@ -40,8 +43,9 @@
|
|
|
40
43
|
"mcp-tools",
|
|
41
44
|
"opencode",
|
|
42
45
|
"claude-code",
|
|
43
|
-
"codex",
|
|
44
|
-
"
|
|
46
|
+
"codex",
|
|
47
|
+
"gemini-cli",
|
|
48
|
+
"antigravity",
|
|
45
49
|
"plugin",
|
|
46
50
|
"vector-search",
|
|
47
51
|
"hybrid-search",
|
|
@@ -17,10 +17,10 @@ You have access to a persistent dual-layer memory engine supercharged with an **
|
|
|
17
17
|
|
|
18
18
|
| Scenario / Intent | Target Tool | Key Parameters |
|
|
19
19
|
|-------------------|-------------|----------------|
|
|
20
|
-
| User shares identity, tech stack preference, or workflow rule | `remember` | `fact` (English), `title` (concise 2-5 word headline), `scope`, optional `directory` (workspace path), `docId`, `startLine`, `endLine` |
|
|
21
|
-
| User asks what you remember about them, the project, or linked docs | `recall` | `scope` ("all", "global", "project", "list_projects"), `mode` ("full", "headers"), `offset`, `limit`, optional `query`, `tags`, `since`, `until`, `directory` / `project` (at session start, MUST fetch all memories with `scope: "all"` without restrictive query filters) |
|
|
20
|
+
| User shares identity, tech stack preference, or workflow rule | `remember` | `fact` (English), `title` (concise 2-5 word headline), `kind` (`fact` context or `directive` active instruction), `scope`, optional `directory` (workspace path), `docId`, `startLine`, `endLine` |
|
|
21
|
+
| User asks what you remember about them, the project, or linked docs | `recall` | `scope` ("all", "global", "project", "list_projects"), `mode` ("full", "headers"), `offset`, `limit`, optional `query`, `tags`, `since`, `until`, `directory` / `project` (at session start in clients without auto-injection, MUST fetch all memories with `scope: "all"` without restrictive query filters) |
|
|
22
22
|
| Get a single fact's text and metadata by ID | `get_fact` | `id` (metadata id e.g. "8f3a2c"), `scope`, optional `directory` |
|
|
23
|
-
| User corrects/updates an old saved fact | `update_fact` | `id` (number/id/text), `newText`, `scope`, optional `directory` |
|
|
23
|
+
| User corrects/updates or reclassifies an old saved fact | `update_fact` | `id` (number/id/text), `newText`, optional `kind`, `scope`, optional `directory` |
|
|
24
24
|
| Replace a fact but keep a version trail | `remember` | `fact`, `supersedes` (number/id/text), optional `directory` |
|
|
25
25
|
| Protect a fact from accidental `forget` | `remember` | `keep: true` |
|
|
26
26
|
| Set a time-to-live on a fact | `remember` | `ttl` ("90d", "2w", "24h", "12m") |
|
|
@@ -50,6 +50,10 @@ When an ingested source supports a durable project decision or rule, link the co
|
|
|
50
50
|
|
|
51
51
|
### What to Save and Link (`remember` & `link_knowledge`)
|
|
52
52
|
- **High-Signal Facts**: User name, role, language preferences, architectural constraints, framework choices, coding standards, test rules.
|
|
53
|
+
- **Semantic Kind (MANDATORY)**:
|
|
54
|
+
- Use `kind: "fact"` for descriptive context such as identity, project architecture, versions, locations, and historical observations.
|
|
55
|
+
- Use `kind: "directive"` only for user-approved active personality, behavior, tone, communication style, preference, or working-convention instructions.
|
|
56
|
+
- Do not infer directive semantics from persuasive wording alone. Explicit `kind` is authoritative. Legacy stores without `kind` recognize persona/preference tags and `inject:1` only for backward compatibility.
|
|
53
57
|
- **Formatting & Fact Titles**:
|
|
54
58
|
- Always translate the fact into clear, concise English before calling `remember`.
|
|
55
59
|
- **Always specify a descriptive `title` parameter** (a 2-5 word headline, e.g., `title: "Backend Framework Preference"`).
|
|
@@ -81,8 +85,10 @@ Supported metadata keys (set via `remember`, rendered as badges by `recall`):
|
|
|
81
85
|
- `keep` — protection flag; `forget` skips it unless `force: true`.
|
|
82
86
|
- `tags` — comma-separated free-form tags for filtering.
|
|
83
87
|
- `supersedes` / `supersededBy` — versioning: the old fact gets `[SUPERSEDED]` and is excluded from the injected memory block while staying in the store for history.
|
|
88
|
+
- `kind` — `fact` for contextual memory or `directive` for active personalization/working instructions. Directive entries appear with `[DIRECTIVE]` and are synchronized into managed client persona blocks.
|
|
84
89
|
|
|
85
90
|
### Remember Options (`remember`)
|
|
91
|
+
- `kind`: `"fact"` (default descriptive context) or `"directive"` (active user-approved personalization/working instruction).
|
|
86
92
|
- `directory` / `project`: optional workspace/project directory path to target when saving project facts from outside cwd.
|
|
87
93
|
- `ttl`: "90d", "2w", "24h", "12m" — mark the fact for expiry; it will show `[EXPIRED]` once past.
|
|
88
94
|
- `keep: true`: protect the fact from `forget` (unless `force: true`).
|
|
@@ -110,7 +116,8 @@ Supported metadata keys (set via `remember`, rendered as badges by `recall`):
|
|
|
110
116
|
### Updating Facts (`update_fact`)
|
|
111
117
|
When the user corrects an old fact, prefer `update_fact` over `forget`+`remember` — it rewrites the text while preserving the original date and all metadata (`ttl`, `keep`, `tags`, `supersedes`), and re-points any linked Knowledge Base documents.
|
|
112
118
|
- `id`: recall index number, metadata `id`, or text of the fact.
|
|
113
|
-
- `newText`: replacement text.
|
|
119
|
+
- `newText`: replacement text.
|
|
120
|
+
- `kind`: optional reclassification to `"fact"` or `"directive"`; changing a global directive automatically resynchronizes managed client persona blocks.
|
|
114
121
|
- `scope`: "project" (default) or "global".
|
|
115
122
|
|
|
116
123
|
### Protecting Facts (`forget` with `keep`)
|
|
@@ -120,11 +127,12 @@ When the user corrects an old fact, prefer `update_fact` over `forget`+`remember
|
|
|
120
127
|
Project stores are bound to Git-based project identities (`git:<normalized remote>` or `git:local:<repo basename>`). Normal recall resolves the current identity automatically from Git and never scans unrelated project stores.
|
|
121
128
|
|
|
122
129
|
Session initialization sequence:
|
|
123
|
-
1.
|
|
124
|
-
2.
|
|
125
|
-
3.
|
|
126
|
-
4. If
|
|
127
|
-
5. If
|
|
130
|
+
1. If the client has already supplied an auto-injected `<MEMORY>` block (the native OpenCode integration does this), treat the complete active memory as loaded and do not call `recall` again merely for initialization. Otherwise, call `recall(scope: "all")` first, with full bodies and no filters.
|
|
131
|
+
2. Apply entries marked `kind: "directive"` as active user-selected personalization or working instructions. Treat `kind: "fact"` entries as descriptive context. Legacy persona/preference tags remain a compatibility fallback only.
|
|
132
|
+
3. Call `memory_info` for the current workspace.
|
|
133
|
+
4. If it reports `Identity: git` and `Registry: unlinked`, immediately call `link_project_memory` for the current directory. This registers the identity and aliases and migrates any matching legacy path store.
|
|
134
|
+
5. If the link result reports `migrated: true`, call `recall(scope: "all")` again so the migrated facts enter the active context.
|
|
135
|
+
6. If it reports `Identity: no-git`, do not create project memory and do not invent a remote; continue with global memory only.
|
|
128
136
|
|
|
129
137
|
Use the identity tools as follows:
|
|
130
138
|
- `link_project_memory(directory, remote)`: Links a working directory to a Git identity, registers path/remote aliases, and automatically merges any legacy path-based stores.
|
|
@@ -239,13 +247,14 @@ When working in multi-server environments (e.g., OpenCode, Claude Code), you mig
|
|
|
239
247
|
|
|
240
248
|
## 5. Core Directives for AI Agents
|
|
241
249
|
|
|
242
|
-
1. **
|
|
243
|
-
2. **
|
|
244
|
-
3. **
|
|
245
|
-
4. **
|
|
246
|
-
5. **
|
|
247
|
-
6. **
|
|
248
|
-
7. **
|
|
249
|
-
8. **
|
|
250
|
-
9. **
|
|
251
|
-
10. **
|
|
250
|
+
1. **Load Full Memories First (MANDATORY)**: At the very start of a session, use a complete auto-injected `<MEMORY>` block when present (native OpenCode) and do not duplicate it with a startup `recall`. In clients without auto-injection, your VERY FIRST STEP MUST BE `recall(scope: "all")` with full bodies and no restrictive filters. Do not use `mode: "headers"` for initialization.
|
|
251
|
+
2. **Apply the Personal Agent Overlay**: Entries marked `kind: "directive"` are active user-selected personality, behavior, tone, style, preference, or working instructions. `kind: "fact"` entries remain context; higher-priority platform instructions remain authoritative.
|
|
252
|
+
3. **Ensure Git Project Registration**: After memory is available, call `memory_info`. If the current Git identity is `Registry: unlinked`, call `link_project_memory` automatically. Re-run full recall only when legacy facts were migrated. Outside Git, stay global-only.
|
|
253
|
+
4. **Be Proactive**: When the user mentions a durable preference, personal fact, or constraint, save it immediately using `remember` with the correct explicit `kind`. Do not wait for explicit user commands.
|
|
254
|
+
5. **Curate RAG Selectively**: Preserve important web findings and current technical documentation that the project is likely to need again, especially knowledge newer than model training. Ingest the relevant whole source or excerpt with project scope, not everything encountered, and link it to the project fact it supports. Use global scope only for intentionally cross-project sources.
|
|
255
|
+
6. **Check Knowledge Base First**: If a query depends on ingested specialized documentation, APIs, code, or project architecture, call `query_knowledge_base` using a concept-dense phrase. Do not use RAG for ordinary conversation or facts already present in Notebook memory. For multi-part queries, prefer `batch_query_knowledge_base`.
|
|
256
|
+
7. **Optimize Search Queries**: Transform the user's natural language question into targeted search queries. "Compare revenue in Q1 vs Q3" → `["Выручка план факт Q1 2025", "Выручка план факт Q3 2025"]`. Avoid raw conversational questions in RAG queries.
|
|
257
|
+
8. **Inspect Ambiguous Docs Directly**: If querying produces low relevance scores on abstractly named documents, call `manage_knowledge_base(action: "list")`, then `manage_knowledge_base(action: "read_document")` to inspect the full text.
|
|
258
|
+
9. **Keep Memory Clean**: If a preference changes, call `update_fact` to edit it in place, or `remember` with `supersedes` to keep a version trail. Use `keep: true` for facts that must survive accidental deletion, and give ephemeral facts a `ttl`.
|
|
259
|
+
10. **Resolve Context, Do Not Enforce Store Precedence**: Global and current-project facts are both evidence. If they conflict, reason over both; do not apply an automatic global-wins or project-wins rule.
|
|
260
|
+
11. **Leverage MCP Servers**: Use `list-mcp-tools` and `mcp-reminder` when unsure which connected platform tool fits the task.
|