@lotargo/memory_plugin 1.6.6 → 1.6.8

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (48) hide show
  1. package/CHANGELOG.md +148 -101
  2. package/README.md +436 -304
  3. package/mcp-server/cli/direct_commands.js +39 -0
  4. package/mcp-server/cli.js +16 -5
  5. package/mcp-server/cli_boot.js +4 -1
  6. package/mcp-server/client_cli.js +73 -0
  7. package/mcp-server/client_paths.js +44 -0
  8. package/mcp-server/client_registration.js +38 -0
  9. package/mcp-server/codex_config.js +86 -8
  10. package/mcp-server/db/database.js +14 -21
  11. package/mcp-server/db/migrations.js +66 -77
  12. package/mcp-server/db/rag_blob_transport.js +143 -0
  13. package/mcp-server/db/rag_sync.js +284 -0
  14. package/mcp-server/db/sync_queue.js +219 -307
  15. package/mcp-server/dev_link.js +142 -0
  16. package/mcp-server/fact_format.js +44 -12
  17. package/mcp-server/index.js +17 -7
  18. package/mcp-server/ingest/exporter.js +44 -38
  19. package/mcp-server/ingest/normalizer.js +1 -1
  20. package/mcp-server/ingest/pipeline.js +260 -248
  21. package/mcp-server/persona_migration.js +39 -0
  22. package/mcp-server/prompt_manager.js +162 -55
  23. package/mcp-server/retrieval/retriever.js +99 -64
  24. package/mcp-server/setup.js +150 -100
  25. package/mcp-server/storage/blob_store.js +53 -1
  26. package/mcp-server/tools/core/knowledge_read_core.js +163 -0
  27. package/mcp-server/tools/core/memory_core.js +24 -4
  28. package/mcp-server/tools/core/memory_routing.js +10 -0
  29. package/mcp-server/tools/core/note_core.js +53 -0
  30. package/mcp-server/tools/core/rag_query_core.js +169 -0
  31. package/mcp-server/tools/index.js +11 -9
  32. package/mcp-server/tools/memory_tools.js +4 -1
  33. package/mcp-server/tools/note_tools.js +35 -0
  34. package/mcp-server/tools/rag_tools.js +211 -364
  35. package/mcp-server/uninstall.js +627 -0
  36. package/opencode-plugin/index.js +80 -12
  37. package/opencode-plugin/main.js +136 -0
  38. package/package.json +25 -5
  39. package/skills/using-memory/SKILL.md +28 -19
  40. package/mcp-server/benchmarks/fetch_real_corpus.js +0 -351
  41. package/mcp-server/benchmarks/gpu_profile_benchmark.js +0 -170
  42. package/mcp-server/benchmarks/policy_dominance_test.js +0 -221
  43. package/mcp-server/benchmarks/quality_evaluator.js +0 -598
  44. package/mcp-server/benchmarks/raw_corpus_data.js +0 -613
  45. package/mcp-server/benchmarks/run_benchmarks.js +0 -366
  46. package/mcp-server/benchmarks/stress_ingestion.js +0 -195
  47. package/mcp-server/benchmarks/table_code_retrieval.js +0 -453
  48. package/mcp-server/benchmarks/test_dual_layer.js +0 -141
@@ -0,0 +1,142 @@
1
+ import { copyFile, cp, mkdir, readFile, readdir, rename, writeFile } from "node:fs/promises";
2
+ import { existsSync } from "node:fs";
3
+ import { spawnSync } from "node:child_process";
4
+ import { dirname, join } from "node:path";
5
+ import { fileURLToPath, pathToFileURL } from "node:url";
6
+ import { resolveClientPaths } from "./client_paths.js";
7
+
8
+ export const PACKAGE_NAME = "@lotargo/memory_plugin";
9
+ export const REPO_ROOT = dirname(dirname(fileURLToPath(import.meta.url)));
10
+ export const DEV_PLUGIN_FILE = join(REPO_ROOT, "opencode-plugin", "main.js");
11
+
12
+ const LEGACY_PACKAGE_NAMES = new Set([
13
+ PACKAGE_NAME,
14
+ "opencode-memory-plugin",
15
+ "memory_plugin",
16
+ "memory-plugin",
17
+ ]);
18
+
19
+ function pluginSpec(entry) {
20
+ return Array.isArray(entry) ? entry[0] : entry;
21
+ }
22
+
23
+ export function isMemoryPluginSpec(spec, devPluginUrl = pathToFileURL(DEV_PLUGIN_FILE).href) {
24
+ if (typeof spec !== "string") return false;
25
+ if (LEGACY_PACKAGE_NAMES.has(spec)) return true;
26
+ if (/^@lotargo\/memory_plugin(?:@[^/]+)?$/i.test(spec)) return true;
27
+ if (/^(?:opencode-memory-plugin|memory_plugin|memory-plugin)(?:@[^/]+)?$/i.test(spec)) return true;
28
+ if (spec === devPluginUrl) return true;
29
+ return false;
30
+ }
31
+
32
+ export function rewriteOpenCodePluginList(pluginList, devPluginUrl) {
33
+ const input = Array.isArray(pluginList) ? pluginList : [];
34
+ const output = [];
35
+ let inserted = false;
36
+
37
+ for (const entry of input) {
38
+ const spec = pluginSpec(entry);
39
+ if (!isMemoryPluginSpec(spec, devPluginUrl)) {
40
+ output.push(entry);
41
+ continue;
42
+ }
43
+ if (inserted) continue;
44
+ output.push(Array.isArray(entry) ? [devPluginUrl, entry[1]] : devPluginUrl);
45
+ inserted = true;
46
+ }
47
+
48
+ if (!inserted) output.push(devPluginUrl);
49
+ return output;
50
+ }
51
+
52
+ export async function linkOpenCodeToRepository({
53
+ configDir = resolveClientPaths().opencodeDir,
54
+ pluginFile = DEV_PLUGIN_FILE,
55
+ } = {}) {
56
+ if (!existsSync(pluginFile)) throw new Error(`OpenCode plugin entry not found: ${pluginFile}`);
57
+ const configPath = join(configDir, "opencode.json");
58
+ const backupPath = `${configPath}.memory-dev-backup`;
59
+ await mkdir(configDir, { recursive: true });
60
+
61
+ let config = {};
62
+ if (existsSync(configPath)) {
63
+ const raw = await readFile(configPath, "utf-8");
64
+ config = raw.trim() ? JSON.parse(raw) : {};
65
+ if (!existsSync(backupPath)) await copyFile(configPath, backupPath);
66
+ }
67
+
68
+ const devPluginUrl = pathToFileURL(pluginFile).href;
69
+ config.plugin = rewriteOpenCodePluginList(config.plugin, devPluginUrl);
70
+ const content = `${JSON.stringify(config, null, 2)}\n`;
71
+ const tmpPath = `${configPath}.memory-dev-tmp-${process.pid}`;
72
+ await writeFile(tmpPath, content, "utf-8");
73
+ await rename(tmpPath, configPath);
74
+
75
+ return { configPath, backupPath, devPluginUrl };
76
+ }
77
+
78
+ export function linkGlobalNpmPackage({ repoRoot = REPO_ROOT } = {}) {
79
+ const npmExecPath = process.env.npm_execpath;
80
+ const command = npmExecPath && existsSync(npmExecPath) ? process.execPath : "npm";
81
+ const args = npmExecPath && existsSync(npmExecPath) ? [npmExecPath, "link"] : ["link"];
82
+ const result = spawnSync(command, args, {
83
+ cwd: repoRoot,
84
+ stdio: "inherit",
85
+ // A direct npm-cli.js invocation is preferred. The shell fallback is only
86
+ // needed when dev-link is launched outside npm and no npm_execpath exists;
87
+ // the command and arguments are fixed, not user-provided.
88
+ shell: process.platform === "win32" && !npmExecPath,
89
+ });
90
+ if (result.error) throw result.error;
91
+ if (result.status !== 0) throw new Error(`npm link failed with exit code ${result.status}`);
92
+ }
93
+
94
+ export async function syncDevelopmentSkills({
95
+ skillsSource = join(REPO_ROOT, "skills"),
96
+ targets = null,
97
+ } = {}) {
98
+ if (!existsSync(skillsSource)) return [];
99
+ const { home, opencodeDir } = resolveClientPaths();
100
+ const destinations = targets || [
101
+ join(opencodeDir, "skills"),
102
+ join(home, ".codex", "skills"),
103
+ join(home, ".agents", "skills"),
104
+ join(home, ".claude", "skills"),
105
+ join(home, ".gemini", "config", "skills"),
106
+ ];
107
+ const entries = await readdir(skillsSource, { withFileTypes: true });
108
+ for (const destination of destinations) {
109
+ await mkdir(destination, { recursive: true });
110
+ for (const entry of entries) {
111
+ if (!entry.isDirectory()) continue;
112
+ await cp(join(skillsSource, entry.name), join(destination, entry.name), {
113
+ recursive: true,
114
+ force: true,
115
+ });
116
+ }
117
+ }
118
+ return destinations;
119
+ }
120
+
121
+ export async function runDevLink({ skipNpmLink = false } = {}) {
122
+ console.log(`\nLinking ${PACKAGE_NAME} to the working repository...\n`);
123
+ if (!skipNpmLink) {
124
+ linkGlobalNpmPackage();
125
+ console.log(` [OK] System CLI linked to ${REPO_ROOT}`);
126
+ }
127
+ const linked = await linkOpenCodeToRepository();
128
+ console.log(` [OK] OpenCode plugin source: ${linked.devPluginUrl}`);
129
+ console.log(` [OK] OpenCode config: ${linked.configPath}`);
130
+ console.log(` [OK] Original config backup: ${linked.backupPath}`);
131
+ const { enableGlobalPrompt } = await import("./prompt_manager.js");
132
+ const promptResults = await enableGlobalPrompt();
133
+ const promptFailures = promptResults.filter((result) => result.status === "failed");
134
+ if (promptFailures.length) {
135
+ throw new Error(`Failed to synchronize client prompts: ${promptFailures.map((item) => item.name).join(", ")}`);
136
+ }
137
+ console.log(" [OK] Codex, Claude Code, Gemini CLI, and Antigravity prompts synchronized");
138
+ const skillTargets = await syncDevelopmentSkills();
139
+ console.log(` [OK] Development skills synchronized to ${skillTargets.length} client location(s)`);
140
+ console.log("\nDevelopment link is active. After source edits, restart OpenCode; publishing and reinstalling are not required.\n");
141
+ return linked;
142
+ }
@@ -12,9 +12,25 @@
12
12
  // keep "1" = protected fact: forget refuses to delete without force
13
13
  // supersedes id (or number/text) this fact replaces
14
14
  // supersededBy id of the fact that replaced this one
15
- // tags comma-separated free-form tags for recall filtering
15
+ // tags comma-separated free-form tags for recall filtering
16
+ // kind "fact" = descriptive context, "directive" = active personalization/working instruction
16
17
 
17
- const META_KEYS = ["id", "ttl", "keep", "supersededBy", "supersedes", "tags", "inject"];
18
+ const META_KEYS = ["id", "ttl", "keep", "supersededBy", "supersedes", "tags", "inject", "kind"];
19
+
20
+ const LEGACY_DIRECTIVE_TAGS = new Set([
21
+ "persona",
22
+ "behavior",
23
+ "behaviour",
24
+ "speech",
25
+ "style",
26
+ "tone",
27
+ "preference",
28
+ "preferences",
29
+ "pref",
30
+ "instruction",
31
+ "instructions",
32
+ "directive",
33
+ ]);
18
34
 
19
35
  const ENTRY_RE = /^- \[(\d{4}-\d{2}-\d{2})\s+(\d{2}:\d{2})\]\s+(.*)$/;
20
36
 
@@ -125,9 +141,24 @@ export function isKeepFact(line) {
125
141
  }
126
142
 
127
143
  // True if the fact has been superseded by another fact.
128
- export function isSuperseded(line) {
129
- return Boolean(factMeta(line).supersededBy);
130
- }
144
+ export function isSuperseded(line) {
145
+ return Boolean(factMeta(line).supersededBy);
146
+ }
147
+
148
+ // Explicit kind metadata is authoritative. Legacy stores predate `kind`, so
149
+ // well-known personalization tags and inject:1 remain a compatibility bridge.
150
+ // kind:fact can explicitly opt a tagged item out of directive semantics.
151
+ export function isDirectiveFact(line) {
152
+ const meta = factMeta(line);
153
+ if (meta.kind === "directive") return true;
154
+ if (meta.kind === "fact") return false;
155
+ if (meta.inject === "1") return true;
156
+ const tags = String(meta.tags || "")
157
+ .split(",")
158
+ .map((tag) => tag.trim().toLowerCase())
159
+ .filter(Boolean);
160
+ return tags.some((tag) => LEGACY_DIRECTIVE_TAGS.has(tag));
161
+ }
131
162
 
132
163
  // Keyword match: space-separated terms, all must be present (case-insensitive).
133
164
  // Also matches against the fact's id and tags.
@@ -161,13 +192,14 @@ export function inDateRange(factLine, since, until) {
161
192
  }
162
193
 
163
194
  // Human-readable badges for a fact line, e.g. ["EXPIRED", "KEEP", "SUPERSEDED"].
164
- export function metaBadges(factLine, now = Date.now()) {
165
- const badges = [];
166
- if (isExpiredLine(factLine, now)) badges.push("EXPIRED");
167
- if (isKeepFact(factLine)) badges.push("KEEP");
168
- if (isSuperseded(factLine)) badges.push("SUPERSEDED");
169
- return badges;
170
- }
195
+ export function metaBadges(factLine, now = Date.now()) {
196
+ const badges = [];
197
+ if (isExpiredLine(factLine, now)) badges.push("EXPIRED");
198
+ if (isKeepFact(factLine)) badges.push("KEEP");
199
+ if (isSuperseded(factLine)) badges.push("SUPERSEDED");
200
+ if (isDirectiveFact(factLine)) badges.push("DIRECTIVE");
201
+ return badges;
202
+ }
171
203
 
172
204
  // Display text of a fact line with badges appended, e.g.
173
205
  // "user prefers TS [EXPIRED] [KEEP]"
@@ -45,15 +45,17 @@ function printUsage() {
45
45
 
46
46
  Usage:
47
47
  memory_plugin Start the MCP server on stdio (default)
48
- memory_plugin setup [--opencode|--claude|--codex|--antigravity] [--mode <MODE>]
48
+ memory_plugin setup [--opencode|--claude|--codex|--gemini|--antigravity] [--mode <MODE>]
49
+ memory_plugin setup --uninstall [--purge] [--purge-cache] [--dry-run] [--yes]
50
+ memory_plugin uninstall [--purge] [--purge-cache] [--dry-run] [--yes] [--opencode|--claude|--codex|--gemini|--antigravity]
49
51
  memory_plugin cli Interactive terminal UI
50
52
  memory_plugin login [--from-env|--api-token|--db-url <URL>]
51
53
  memory_plugin logout [--api-key]
52
54
  memory_plugin auth-status
53
55
  memory_plugin link|unlink|relink|identity [--dir <path>] [--remote <url>]
54
56
  memory_plugin migrate_titles [--key <key>]
55
- memory_plugin enable-prompt | disable-prompt
56
- memory_plugin doctor --codex
57
+ memory_plugin enable-prompt | disable-prompt
58
+ memory_plugin doctor --codex
57
59
 
58
60
  Options:
59
61
  -h, --help Show this help text
@@ -62,9 +64,11 @@ Options:
62
64
  Secrets: prefer TURSO_API_TOKEN / TURSO_DB_URL / TURSO_DB_TOKEN environment
63
65
  variables over command-line flags — argv is visible to other local processes.
64
66
  Data directory: ${MEMORY_DIR}`);
65
- }
67
+ }
66
68
 
67
- if (cliArgs.includes("--help") || cliArgs.includes("-h") || cliArgs[0] === "help") {
69
+ const isUninstallCommand = cliArgs.includes("uninstall") || cliArgs.includes("--uninstall");
70
+
71
+ if (!isUninstallCommand && (cliArgs.includes("--help") || cliArgs.includes("-h") || cliArgs[0] === "help")) {
68
72
  printUsage();
69
73
  process.exit(0);
70
74
  }
@@ -74,8 +78,14 @@ if (cliArgs.includes("--version") || cliArgs.includes("-v")) {
74
78
  process.exit(0);
75
79
  }
76
80
 
77
- if (cliArgs.includes("setup") || cliArgs.includes("install") || cliArgs.includes("--setup") || cliArgs.includes("-s")) {
78
- const { runSetup } = await import("./setup.js");
81
+ if (isUninstallCommand) {
82
+ const { runUninstall } = await import("./uninstall.js");
83
+ await runUninstall();
84
+ process.exit(process.exitCode || 0);
85
+ }
86
+
87
+ if (cliArgs.includes("setup") || cliArgs.includes("install") || cliArgs.includes("--setup") || cliArgs.includes("-s")) {
88
+ const { runSetup } = await import("./setup.js");
79
89
  await runSetup();
80
90
  process.exit(0);
81
91
  }
@@ -1,8 +1,8 @@
1
1
  import { getDatabase } from "../db/database.js";
2
2
  import { writeFileSync, mkdirSync, existsSync } from "node:fs";
3
3
  import { join } from "node:path";
4
- import { MEMORY_DIR } from "../memory.js";
5
- import { toVectorBytes } from "../retrieval/retriever.js";
4
+ import { MEMORY_DIR } from "../memory.js";
5
+ import { toVectorBytes } from "../retrieval/retriever.js";
6
6
 
7
7
  export const EXPORTS_DIR = join(MEMORY_DIR, "exports");
8
8
 
@@ -36,32 +36,38 @@ export async function exportDocumentData(docIdOrPath, customDb = null) {
36
36
 
37
37
  const sections = await db.prepare("SELECT id, heading, breadcrumbs, content, token_count FROM sections WHERE doc_id = ?").all(doc.id);
38
38
  const mediumChunks = await db.prepare("SELECT id, section_id, content, block_type, token_count, created_at FROM medium_chunks WHERE doc_id = ?").all(doc.id);
39
- const rawMicroChunks = await db.prepare("SELECT id, medium_id, section_id, content, vector, token_count, retrieval_policy, policy_source_id FROM micro_chunks WHERE doc_id = ?").all(doc.id);
40
- const microChunks = rawMicroChunks.map((chunk) => {
41
- const bytes = toVectorBytes(chunk.vector);
42
- return {
43
- ...chunk,
44
- vector: bytes && bytes.byteLength
45
- ? Buffer.from(bytes.buffer, bytes.byteOffset, bytes.byteLength).toString("base64")
46
- : "",
47
- };
48
- });
49
- const ownedRows = await db.prepare(`
50
- SELECT id FROM sections WHERE doc_id = ?
51
- UNION SELECT id FROM medium_chunks WHERE doc_id = ?
52
- UNION SELECT id FROM micro_chunks WHERE doc_id = ?;
53
- `).all(doc.id, doc.id, doc.id);
54
- const ownedIds = [doc.id, ...ownedRows.map((row) => row.id)];
55
- const placeholders = ownedIds.map(() => "?").join(",");
56
- const graphEdges = await db.prepare(`
57
- SELECT source_id, target_id, relation_type, metadata_json, created_at
58
- FROM graph_edges
59
- WHERE source_id IN (${placeholders})
60
- OR target_id IN (${placeholders})
61
- OR target_id GLOB ?;
62
- `).all(...ownedIds, ...ownedIds, `${doc.id}:L*`);
63
- const knowledgeLinks = await db.prepare("SELECT * FROM knowledge_links WHERE doc_id = ?").all(doc.id);
64
- const documentScopes = await db.prepare("SELECT scope_key, created_at FROM document_scopes WHERE doc_id = ?").all(doc.id);
39
+ const rawMicroChunks = await db.prepare(`
40
+ SELECT m.id, m.medium_id, m.section_id, m.content, m.vector, m.token_count,
41
+ m.retrieval_policy, m.policy_source_id, s.breadcrumbs
42
+ FROM micro_chunks m
43
+ LEFT JOIN sections s ON s.id = m.section_id
44
+ WHERE m.doc_id = ?;
45
+ `).all(doc.id);
46
+ const microChunks = rawMicroChunks.map((chunk) => {
47
+ const bytes = toVectorBytes(chunk.vector);
48
+ return {
49
+ ...chunk,
50
+ vector: bytes && bytes.byteLength
51
+ ? Buffer.from(bytes.buffer, bytes.byteOffset, bytes.byteLength).toString("base64")
52
+ : "",
53
+ };
54
+ });
55
+ const ownedRows = await db.prepare(`
56
+ SELECT id FROM sections WHERE doc_id = ?
57
+ UNION SELECT id FROM medium_chunks WHERE doc_id = ?
58
+ UNION SELECT id FROM micro_chunks WHERE doc_id = ?;
59
+ `).all(doc.id, doc.id, doc.id);
60
+ const ownedIds = [doc.id, ...ownedRows.map((row) => row.id)];
61
+ const placeholders = ownedIds.map(() => "?").join(",");
62
+ const graphEdges = await db.prepare(`
63
+ SELECT source_id, target_id, relation_type, metadata_json, created_at
64
+ FROM graph_edges
65
+ WHERE source_id IN (${placeholders})
66
+ OR target_id IN (${placeholders})
67
+ OR target_id GLOB ?;
68
+ `).all(...ownedIds, ...ownedIds, `${doc.id}:L*`);
69
+ const knowledgeLinks = await db.prepare("SELECT * FROM knowledge_links WHERE doc_id = ?").all(doc.id);
70
+ const documentScopes = await db.prepare("SELECT scope_key, created_at FROM document_scopes WHERE doc_id = ?").all(doc.id);
65
71
 
66
72
  return {
67
73
  document: {
@@ -69,11 +75,11 @@ export async function exportDocumentData(docIdOrPath, customDb = null) {
69
75
  path: doc.path,
70
76
  title: doc.title,
71
77
  blob_hash: doc.blob_hash,
72
- checksum: doc.checksum,
73
- toc_json: doc.toc_json,
74
- metadata_json: doc.metadata_json,
75
- toc,
76
- metadata,
78
+ checksum: doc.checksum,
79
+ toc_json: doc.toc_json,
80
+ metadata_json: doc.metadata_json,
81
+ toc,
82
+ metadata,
77
83
  created_at: doc.created_at,
78
84
  updated_at: doc.updated_at,
79
85
  },
@@ -86,11 +92,11 @@ export async function exportDocumentData(docIdOrPath, customDb = null) {
86
92
  sections,
87
93
  medium_chunks: mediumChunks,
88
94
  micro_chunks: microChunks,
89
- graph_edges: graphEdges,
90
- knowledge_links: knowledgeLinks,
91
- document_scopes: documentScopes,
92
- };
93
- }
95
+ graph_edges: graphEdges,
96
+ knowledge_links: knowledgeLinks,
97
+ document_scopes: documentScopes,
98
+ };
99
+ }
94
100
 
95
101
  export async function exportDocumentToJsonString(docIdOrPath, customDb = null) {
96
102
  const data = await exportDocumentData(docIdOrPath, customDb);
@@ -3,7 +3,7 @@ import { isIP } from "node:net";
3
3
  import { lookup } from "node:dns/promises";
4
4
  import { PDFParse } from "pdf-parse";
5
5
  import mammoth from "mammoth";
6
- import xlsx from "xlsx";
6
+ import * as xlsx from "xlsx";
7
7
 
8
8
  export function cleanHtml(html) {
9
9
  if (!html) return "";