remem-mcp 0.6.3 → 0.6.4

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.
@@ -0,0 +1,300 @@
1
+ import {
2
+ __esm,
3
+ init_esm_shims
4
+ } from "./chunk-XK4Y53J2.js";
5
+
6
+ // src/codegraph/resolver.ts
7
+ function resolveCall(calleeName, callerFilePath, registry, importMap) {
8
+ const parts = calleeName.split(".");
9
+ if (parts.length >= 2) {
10
+ const prefix = parts[0];
11
+ const suffix = parts.slice(1).join(".");
12
+ const modulePath = importMap.lookup(callerFilePath, prefix);
13
+ if (modulePath) {
14
+ const qualifiedName = `${modulePath}/${suffix}`;
15
+ const sym = registry.exactLookup(qualifiedName);
16
+ if (sym) {
17
+ return { calleeId: sym.id, confidence: 0.95, strategy: "import-map" };
18
+ }
19
+ const fullQualified = `${modulePath}/${calleeName}`;
20
+ const symFull = registry.exactLookup(fullQualified);
21
+ if (symFull) {
22
+ return { calleeId: symFull.id, confidence: 0.95, strategy: "import-map" };
23
+ }
24
+ }
25
+ }
26
+ if (parts.length >= 2) {
27
+ const prefix = parts[0];
28
+ const suffix = parts[parts.length - 1];
29
+ const modulePath = importMap.lookup(callerFilePath, prefix);
30
+ if (modulePath) {
31
+ const candidates2 = registry.reverseLookup(suffix);
32
+ for (const c of candidates2) {
33
+ const cModule = registry.getModuleForFile(c.file_path);
34
+ if (cModule === modulePath || cModule.endsWith(`/${modulePath}`)) {
35
+ return { calleeId: c.id, confidence: 0.85, strategy: "import-map-suffix" };
36
+ }
37
+ }
38
+ }
39
+ }
40
+ const callerModule = registry.getModuleForFile(callerFilePath);
41
+ const sameModuleQualified = `${callerModule}/${calleeName}`;
42
+ const symSame = registry.exactLookup(sameModuleQualified);
43
+ if (symSame) {
44
+ return { calleeId: symSame.id, confidence: 0.9, strategy: "same-module" };
45
+ }
46
+ const sameModuleSimple = registry.reverseLookup(calleeName).find(
47
+ (s) => registry.getModuleForFile(s.file_path) === callerModule
48
+ );
49
+ if (sameModuleSimple) {
50
+ return { calleeId: sameModuleSimple.id, confidence: 0.9, strategy: "same-module" };
51
+ }
52
+ const candidates = registry.reverseLookup(calleeName);
53
+ if (candidates.length === 1) {
54
+ return { calleeId: candidates[0].id, confidence: 0.75, strategy: "unique-name" };
55
+ }
56
+ if (candidates.length > 1) {
57
+ const fileImports = importMap.getImportsForFile(callerFilePath);
58
+ if (fileImports) {
59
+ for (const c of candidates) {
60
+ const cModule = registry.getModuleForFile(c.file_path);
61
+ for (const [, impModule] of fileImports) {
62
+ if (cModule === impModule || cModule.startsWith(`${impModule}/`)) {
63
+ return { calleeId: c.id, confidence: 0.55, strategy: "suffix-match" };
64
+ }
65
+ }
66
+ }
67
+ }
68
+ const callerDir = callerFilePath.slice(0, callerFilePath.lastIndexOf("/"));
69
+ let best = candidates[0];
70
+ let bestDist = Infinity;
71
+ for (const c of candidates) {
72
+ const cDir = c.file_path.slice(0, c.file_path.lastIndexOf("/"));
73
+ const dist = pathDistance(callerDir, cDir);
74
+ if (dist < bestDist) {
75
+ bestDist = dist;
76
+ best = c;
77
+ }
78
+ }
79
+ return { calleeId: best.id, confidence: 0.55, strategy: "suffix-match" };
80
+ }
81
+ const fuzzyMatch = fuzzyLookup(calleeName, registry);
82
+ if (fuzzyMatch) {
83
+ return { calleeId: fuzzyMatch.id, confidence: 0.35, strategy: "fuzzy" };
84
+ }
85
+ return { calleeId: null, confidence: 0, strategy: "unresolved" };
86
+ }
87
+ function shouldSkipResolution(calleeName) {
88
+ if (calleeName.length <= 2) return true;
89
+ if (SKIP_NAMES.has(calleeName)) return true;
90
+ const parts = calleeName.split(".");
91
+ if (parts.length >= 2 && parts[0].length <= 1) return true;
92
+ return false;
93
+ }
94
+ function resolveAllCalls(db) {
95
+ try {
96
+ db.exec("CREATE INDEX IF NOT EXISTS idx_calls_callee_name ON calls(callee_name)");
97
+ db.exec("CREATE INDEX IF NOT EXISTS idx_symbols_name ON symbols(name)");
98
+ db.exec("CREATE INDEX IF NOT EXISTS idx_symbols_module ON symbols(module_path)");
99
+ } catch {
100
+ }
101
+ const symbols = db.prepare("SELECT id, name, kind, file_path, module_path, language, parent_id FROM symbols").all();
102
+ const registry = new FunctionRegistry(symbols);
103
+ const imports = db.prepare("SELECT file_path, symbol_name, source_path FROM imports").all();
104
+ const importMap = new ImportMap(imports);
105
+ const calls = db.prepare("SELECT id, caller_id, callee_name, callee_id, line, call_type FROM calls WHERE callee_id IS NULL").all();
106
+ const stats = { total: calls.length, resolved: 0, byStrategy: {} };
107
+ let skipped = 0;
108
+ const callerFiles = /* @__PURE__ */ new Map();
109
+ const callerIds = [...new Set(calls.map((c) => c.caller_id))];
110
+ if (callerIds.length > 0) {
111
+ for (let i = 0; i < callerIds.length; i += 500) {
112
+ const chunk = callerIds.slice(i, i + 500);
113
+ const placeholders = chunk.map(() => "?").join(",");
114
+ const rows = db.prepare(`SELECT id, file_path FROM symbols WHERE id IN (${placeholders})`).all(...chunk);
115
+ for (const r of rows) {
116
+ callerFiles.set(r.id, r.file_path);
117
+ }
118
+ }
119
+ }
120
+ const updateStmt = db.prepare(
121
+ "UPDATE calls SET callee_id = ?, confidence = ? WHERE id = ?"
122
+ );
123
+ const batch = db.transaction(() => {
124
+ for (const call of calls) {
125
+ if (shouldSkipResolution(call.callee_name)) {
126
+ skipped++;
127
+ continue;
128
+ }
129
+ const callerFile = callerFiles.get(call.caller_id) ?? "";
130
+ const result = resolveCall(call.callee_name, callerFile, registry, importMap);
131
+ if (result.calleeId) {
132
+ updateStmt.run(result.calleeId, result.confidence, call.id);
133
+ stats.resolved++;
134
+ stats.byStrategy[result.strategy] = (stats.byStrategy[result.strategy] ?? 0) + 1;
135
+ }
136
+ }
137
+ });
138
+ batch();
139
+ if (skipped > 0) {
140
+ console.error(`[remem-mcp] resolveAllCalls: skipped ${skipped} short/builtin names`);
141
+ }
142
+ return stats;
143
+ }
144
+ function deriveModulePath(filePath) {
145
+ return filePath.replace(/\.(ts|tsx|js|jsx|mjs|cjs|py|go|rs|java|c|cpp|cc|cxx|hpp|cs)$/, "");
146
+ }
147
+ function pathDistance(a, b) {
148
+ if (a === b) return 0;
149
+ const aParts = a.split("/");
150
+ const bParts = b.split("/");
151
+ let common = 0;
152
+ for (let i = 0; i < Math.min(aParts.length, bParts.length); i++) {
153
+ if (aParts[i] === bParts[i]) common++;
154
+ else break;
155
+ }
156
+ return aParts.length + bParts.length - 2 * common;
157
+ }
158
+ function fuzzyLookup(name, registry) {
159
+ let best = null;
160
+ let bestScore = 0;
161
+ for (const [symName, candidates] of registry.reverseLookupEntries()) {
162
+ const score = similarity(name, symName);
163
+ if (score > bestScore && score > 0.6) {
164
+ bestScore = score;
165
+ best = candidates[0];
166
+ }
167
+ }
168
+ return best;
169
+ }
170
+ function similarity(a, b) {
171
+ if (a === b) return 1;
172
+ const aBigrams = bigrams(a);
173
+ const bBigrams = bigrams(b);
174
+ const intersection = aBigrams.filter((bg) => bBigrams.includes(bg)).length;
175
+ const union = aBigrams.length + bBigrams.length - intersection;
176
+ return union > 0 ? intersection / union : 0;
177
+ }
178
+ function bigrams(s) {
179
+ const result = [];
180
+ for (let i = 0; i < s.length - 1; i++) {
181
+ result.push(s.slice(i, i + 2));
182
+ }
183
+ return result;
184
+ }
185
+ var FunctionRegistry, ImportMap, SKIP_NAMES;
186
+ var init_resolver = __esm({
187
+ "src/codegraph/resolver.ts"() {
188
+ init_esm_shims();
189
+ FunctionRegistry = class {
190
+ exactMap = /* @__PURE__ */ new Map();
191
+ reverseIndex = /* @__PURE__ */ new Map();
192
+ fileModules = /* @__PURE__ */ new Map();
193
+ // file_path → module_path
194
+ constructor(symbols) {
195
+ for (const sym of symbols) {
196
+ const modulePath = sym.module_path ?? deriveModulePath(sym.file_path);
197
+ this.fileModules.set(sym.file_path, modulePath);
198
+ const qualifiedName = `${modulePath}/${sym.name}`;
199
+ this.exactMap.set(qualifiedName, sym);
200
+ this.exactMap.set(`${sym.file_path}/${sym.name}`, sym);
201
+ const existing = this.reverseIndex.get(sym.name);
202
+ if (existing) {
203
+ existing.push(sym);
204
+ } else {
205
+ this.reverseIndex.set(sym.name, [sym]);
206
+ }
207
+ }
208
+ }
209
+ /** Look up by qualified name (module_path/name). */
210
+ exactLookup(qualifiedName) {
211
+ return this.exactMap.get(qualifiedName);
212
+ }
213
+ /** Look up by simple name — returns all candidates. */
214
+ reverseLookup(simpleName) {
215
+ return this.reverseIndex.get(simpleName) ?? [];
216
+ }
217
+ /** Get all reverse index entries (for fuzzy lookup). */
218
+ reverseLookupEntries() {
219
+ return this.reverseIndex.entries();
220
+ }
221
+ /** Get module_path for a file. */
222
+ getModuleForFile(filePath) {
223
+ return this.fileModules.get(filePath) ?? deriveModulePath(filePath);
224
+ }
225
+ };
226
+ ImportMap = class {
227
+ fileImports = /* @__PURE__ */ new Map();
228
+ constructor(imports) {
229
+ for (const imp of imports) {
230
+ if (!imp.source_path) continue;
231
+ let fileMap = this.fileImports.get(imp.file_path);
232
+ if (!fileMap) {
233
+ fileMap = /* @__PURE__ */ new Map();
234
+ this.fileImports.set(imp.file_path, fileMap);
235
+ }
236
+ const prefix = imp.symbol_name.split(".")[0] ?? imp.symbol_name;
237
+ const modulePath = deriveModulePath(imp.source_path);
238
+ fileMap.set(prefix, modulePath);
239
+ fileMap.set(imp.symbol_name, modulePath);
240
+ }
241
+ }
242
+ /** Look up module path for an import prefix in a given file. */
243
+ lookup(filePath, prefix) {
244
+ return this.fileImports.get(filePath)?.get(prefix);
245
+ }
246
+ /** Get all imports for a file. */
247
+ getImportsForFile(filePath) {
248
+ return this.fileImports.get(filePath);
249
+ }
250
+ };
251
+ SKIP_NAMES = /* @__PURE__ */ new Set([
252
+ "T",
253
+ "L",
254
+ "R",
255
+ "S",
256
+ "P",
257
+ "C",
258
+ "D",
259
+ "E",
260
+ "F",
261
+ "M",
262
+ "N",
263
+ "V",
264
+ "X",
265
+ "Y",
266
+ "String",
267
+ "Array",
268
+ "Object",
269
+ "Number",
270
+ "Boolean",
271
+ "Map",
272
+ "Set",
273
+ "Date",
274
+ "Error",
275
+ "RegExp",
276
+ "Promise",
277
+ "Symbol",
278
+ "Buffer",
279
+ "Vec",
280
+ "Box",
281
+ "Option",
282
+ "Result",
283
+ "Some",
284
+ "None",
285
+ "Ok",
286
+ "Err",
287
+ "List",
288
+ "Dict",
289
+ "Tuple"
290
+ ]);
291
+ }
292
+ });
293
+ init_resolver();
294
+ export {
295
+ FunctionRegistry,
296
+ ImportMap,
297
+ resolveAllCalls,
298
+ resolveCall
299
+ };
300
+ //# sourceMappingURL=resolver-AVK7XKX7.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../src/codegraph/resolver.ts"],"sourcesContent":["/**\n * Call resolver: 6-strategy cascade for resolving callee names to symbol IDs.\n *\n * Adapted from Codebase-Memory (arXiv:2603.27277, MIT, DeusData 2026).\n * https://github.com/DeusData/codebase-memory-mcp\n *\n * Strategies (in priority order):\n * 1. Import map (0.95): Split callee into prefix.suffix; look up prefix in file's import map\n * 2. Import map suffix (0.85): Suffix-based matching against import-resolved module paths\n * 3. Same module (0.90): Prefix callee with enclosing file's module path\n * 4. Unique name (0.75): Simple name lookup, accept if exactly 1 candidate\n * 5. Suffix match (0.55): Multiple candidates, select by suffix + import-distance\n * 6. Fuzzy (0.30-0.40): String similarity — last resort\n */\n\nimport type { Database } from \"better-sqlite3\";\n\nexport interface SymbolRow {\n id: string;\n name: string;\n kind: string;\n file_path: string;\n module_path: string | null;\n language: string;\n parent_id: string | null;\n}\n\nexport interface CallRow {\n id: number;\n caller_id: string;\n callee_name: string;\n callee_id: string | null;\n line: number;\n call_type: string;\n}\n\nexport interface ImportRow {\n file_path: string;\n symbol_name: string;\n source_path: string | null;\n}\n\nexport interface ResolutionResult {\n calleeId: string | null;\n confidence: number;\n strategy: string;\n}\n\n/**\n * FunctionRegistry indexes all symbol definitions by:\n * - qualified name (exact map): \"module_path/SymbolName\" → SymbolRow\n * - simple name (reverse index): \"SymbolName\" → SymbolRow[]\n */\nexport class FunctionRegistry {\n private exactMap = new Map<string, SymbolRow>();\n private reverseIndex = new Map<string, SymbolRow[]>();\n private fileModules = new Map<string, string>(); // file_path → module_path\n\n constructor(symbols: SymbolRow[]) {\n for (const sym of symbols) {\n // Build module_path from file_path if not set\n const modulePath = sym.module_path ?? deriveModulePath(sym.file_path);\n this.fileModules.set(sym.file_path, modulePath);\n\n // Exact map: module_path/name\n const qualifiedName = `${modulePath}/${sym.name}`;\n this.exactMap.set(qualifiedName, sym);\n // Also index by file_path/name for same-file resolution\n this.exactMap.set(`${sym.file_path}/${sym.name}`, sym);\n\n // Reverse index: simple name → all matching symbols\n const existing = this.reverseIndex.get(sym.name);\n if (existing) {\n existing.push(sym);\n } else {\n this.reverseIndex.set(sym.name, [sym]);\n }\n }\n }\n\n /** Look up by qualified name (module_path/name). */\n exactLookup(qualifiedName: string): SymbolRow | undefined {\n return this.exactMap.get(qualifiedName);\n }\n\n /** Look up by simple name — returns all candidates. */\n reverseLookup(simpleName: string): SymbolRow[] {\n return this.reverseIndex.get(simpleName) ?? [];\n }\n\n /** Get all reverse index entries (for fuzzy lookup). */\n reverseLookupEntries(): IterableIterator<[string, SymbolRow[]]> {\n return this.reverseIndex.entries();\n }\n\n /** Get module_path for a file. */\n getModuleForFile(filePath: string): string {\n return this.fileModules.get(filePath) ?? deriveModulePath(filePath);\n }\n}\n\n/**\n * ImportMap tracks what each file imports: { prefix → module_path }.\n * Built from the imports table.\n */\nexport class ImportMap {\n private fileImports = new Map<string, Map<string, string>>();\n\n constructor(imports: ImportRow[]) {\n for (const imp of imports) {\n if (!imp.source_path) continue;\n let fileMap = this.fileImports.get(imp.file_path);\n if (!fileMap) {\n fileMap = new Map();\n this.fileImports.set(imp.file_path, fileMap);\n }\n // Map: symbol_name (or prefix) → source module_path\n const prefix = imp.symbol_name.split(\".\")[0] ?? imp.symbol_name;\n const modulePath = deriveModulePath(imp.source_path);\n fileMap.set(prefix, modulePath);\n fileMap.set(imp.symbol_name, modulePath);\n }\n }\n\n /** Look up module path for an import prefix in a given file. */\n lookup(filePath: string, prefix: string): string | undefined {\n return this.fileImports.get(filePath)?.get(prefix);\n }\n\n /** Get all imports for a file. */\n getImportsForFile(filePath: string): Map<string, string> | undefined {\n return this.fileImports.get(filePath);\n }\n}\n\n/**\n * Resolve a callee name to a symbol ID using the 6-strategy cascade.\n * Returns { calleeId, confidence, strategy } or { calleeId: null, confidence: 0, strategy: \"unresolved\" }.\n */\nexport function resolveCall(\n calleeName: string,\n callerFilePath: string,\n registry: FunctionRegistry,\n importMap: ImportMap,\n): ResolutionResult {\n // Strategy 1: Import map (0.95)\n // Split callee into prefix.suffix; look up prefix in file's import map\n const parts = calleeName.split(\".\");\n if (parts.length >= 2) {\n const prefix = parts[0];\n const suffix = parts.slice(1).join(\".\");\n const modulePath = importMap.lookup(callerFilePath, prefix);\n if (modulePath) {\n const qualifiedName = `${modulePath}/${suffix}`;\n const sym = registry.exactLookup(qualifiedName);\n if (sym) {\n return { calleeId: sym.id, confidence: 0.95, strategy: \"import-map\" };\n }\n // Also try full callee name in the resolved module\n const fullQualified = `${modulePath}/${calleeName}`;\n const symFull = registry.exactLookup(fullQualified);\n if (symFull) {\n return { calleeId: symFull.id, confidence: 0.95, strategy: \"import-map\" };\n }\n }\n }\n\n // Strategy 2: Import map suffix (0.85)\n // Try suffix-based matching against import-resolved module paths\n if (parts.length >= 2) {\n const prefix = parts[0];\n const suffix = parts[parts.length - 1];\n const modulePath = importMap.lookup(callerFilePath, prefix);\n if (modulePath) {\n const candidates = registry.reverseLookup(suffix);\n for (const c of candidates) {\n const cModule = registry.getModuleForFile(c.file_path);\n if (cModule === modulePath || cModule.endsWith(`/${modulePath}`)) {\n return { calleeId: c.id, confidence: 0.85, strategy: \"import-map-suffix\" };\n }\n }\n }\n }\n\n // Strategy 3: Same module (0.90)\n // Prefix callee with enclosing file's module path\n const callerModule = registry.getModuleForFile(callerFilePath);\n const sameModuleQualified = `${callerModule}/${calleeName}`;\n const symSame = registry.exactLookup(sameModuleQualified);\n if (symSame) {\n return { calleeId: symSame.id, confidence: 0.90, strategy: \"same-module\" };\n }\n // Also try just the simple name in same module\n const sameModuleSimple = registry.reverseLookup(calleeName).find(\n (s) => registry.getModuleForFile(s.file_path) === callerModule,\n );\n if (sameModuleSimple) {\n return { calleeId: sameModuleSimple.id, confidence: 0.90, strategy: \"same-module\" };\n }\n\n // Strategy 4: Unique name (0.75)\n // Simple name lookup, accept if exactly 1 candidate project-wide\n const candidates = registry.reverseLookup(calleeName);\n if (candidates.length === 1) {\n return { calleeId: candidates[0].id, confidence: 0.75, strategy: \"unique-name\" };\n }\n\n // Strategy 5: Suffix match (0.55)\n // Multiple candidates, select by suffix match with import-distance scoring\n if (candidates.length > 1) {\n // Prefer candidates in modules reachable from this file's imports\n const fileImports = importMap.getImportsForFile(callerFilePath);\n if (fileImports) {\n for (const c of candidates) {\n const cModule = registry.getModuleForFile(c.file_path);\n for (const [, impModule] of fileImports) {\n if (cModule === impModule || cModule.startsWith(`${impModule}/`)) {\n return { calleeId: c.id, confidence: 0.55, strategy: \"suffix-match\" };\n }\n }\n }\n }\n // Fallback: pick first candidate (nearest by file path similarity)\n const callerDir = callerFilePath.slice(0, callerFilePath.lastIndexOf(\"/\"));\n let best = candidates[0];\n let bestDist = Infinity;\n for (const c of candidates) {\n const cDir = c.file_path.slice(0, c.file_path.lastIndexOf(\"/\"));\n const dist = pathDistance(callerDir, cDir);\n if (dist < bestDist) {\n bestDist = dist;\n best = c;\n }\n }\n return { calleeId: best.id, confidence: 0.55, strategy: \"suffix-match\" };\n }\n\n // Strategy 6: Fuzzy (0.30-0.40)\n // String similarity — last resort\n const fuzzyMatch = fuzzyLookup(calleeName, registry);\n if (fuzzyMatch) {\n return { calleeId: fuzzyMatch.id, confidence: 0.35, strategy: \"fuzzy\" };\n }\n\n return { calleeId: null, confidence: 0, strategy: \"unresolved\" };\n}\n\n/** Names too short or builtin to bother resolving. */\nconst SKIP_NAMES = new Set([\n \"T\", \"L\", \"R\", \"S\", \"P\", \"C\", \"D\", \"E\", \"F\", \"M\", \"N\", \"V\", \"X\", \"Y\",\n \"String\", \"Array\", \"Object\", \"Number\", \"Boolean\", \"Map\", \"Set\", \"Date\",\n \"Error\", \"RegExp\", \"Promise\", \"Symbol\", \"Buffer\", \"Vec\", \"Box\", \"Option\",\n \"Result\", \"Some\", \"None\", \"Ok\", \"Err\", \"List\", \"Dict\", \"Tuple\",\n]);\n\n/** Check if a callee name should be skipped (too short or builtin type). */\nfunction shouldSkipResolution(calleeName: string): boolean {\n if (calleeName.length <= 2) return true;\n if (SKIP_NAMES.has(calleeName)) return true;\n // Skip single-letter + dot patterns like \"T.foo\"\n const parts = calleeName.split(\".\");\n if (parts.length >= 2 && parts[0].length <= 1) return true;\n return false;\n}\n\n/**\n * Run call resolution on all unresolved calls in the database.\n * Called after indexDirectory() completes.\n */\nexport function resolveAllCalls(db: Database): {\n total: number;\n resolved: number;\n byStrategy: Record<string, number>;\n} {\n // Create index for faster lookups (idempotent)\n try {\n db.exec(\"CREATE INDEX IF NOT EXISTS idx_calls_callee_name ON calls(callee_name)\");\n db.exec(\"CREATE INDEX IF NOT EXISTS idx_symbols_name ON symbols(name)\");\n db.exec(\"CREATE INDEX IF NOT EXISTS idx_symbols_module ON symbols(module_path)\");\n } catch {}\n\n // Load all symbols into FunctionRegistry\n const symbols = db\n .prepare(\"SELECT id, name, kind, file_path, module_path, language, parent_id FROM symbols\")\n .all() as SymbolRow[];\n const registry = new FunctionRegistry(symbols);\n\n // Load all imports into ImportMap\n const imports = db\n .prepare(\"SELECT file_path, symbol_name, source_path FROM imports\")\n .all() as ImportRow[];\n const importMap = new ImportMap(imports);\n\n // Get all unresolved calls — skip short/builtin names to save resolution time\n const calls = db\n .prepare(\"SELECT id, caller_id, callee_name, callee_id, line, call_type FROM calls WHERE callee_id IS NULL\")\n .all() as CallRow[];\n\n const stats = { total: calls.length, resolved: 0, byStrategy: {} as Record<string, number> };\n let skipped = 0;\n\n // Get caller file paths — batch query\n const callerFiles = new Map<string, string>();\n const callerIds = [...new Set(calls.map((c) => c.caller_id))];\n if (callerIds.length > 0) {\n // Batch in chunks of 500 to avoid SQLite param limit\n for (let i = 0; i < callerIds.length; i += 500) {\n const chunk = callerIds.slice(i, i + 500);\n const placeholders = chunk.map(() => \"?\").join(\",\");\n const rows = db\n .prepare(`SELECT id, file_path FROM symbols WHERE id IN (${placeholders})`)\n .all(...chunk) as { id: string; file_path: string }[];\n for (const r of rows) {\n callerFiles.set(r.id, r.file_path);\n }\n }\n }\n\n // Resolve each call — skip short/builtin names\n const updateStmt = db.prepare(\n \"UPDATE calls SET callee_id = ?, confidence = ? WHERE id = ?\",\n );\n const batch = db.transaction(() => {\n for (const call of calls) {\n if (shouldSkipResolution(call.callee_name)) {\n skipped++;\n continue;\n }\n const callerFile = callerFiles.get(call.caller_id) ?? \"\";\n const result = resolveCall(call.callee_name, callerFile, registry, importMap);\n if (result.calleeId) {\n updateStmt.run(result.calleeId, result.confidence, call.id);\n stats.resolved++;\n stats.byStrategy[result.strategy] = (stats.byStrategy[result.strategy] ?? 0) + 1;\n }\n }\n });\n batch();\n\n if (skipped > 0) {\n console.error(`[remem-mcp] resolveAllCalls: skipped ${skipped} short/builtin names`);\n }\n\n return stats;\n}\n\n/** Derive a module path from a file path: \"src/storage/sqlite.ts\" → \"src/storage/sqlite\" */\nfunction deriveModulePath(filePath: string): string {\n return filePath.replace(/\\.(ts|tsx|js|jsx|mjs|cjs|py|go|rs|java|c|cpp|cc|cxx|hpp|cs)$/, \"\");\n}\n\n/** Calculate directory path distance for suffix-match scoring. */\nfunction pathDistance(a: string, b: string): number {\n if (a === b) return 0;\n const aParts = a.split(\"/\");\n const bParts = b.split(\"/\");\n let common = 0;\n for (let i = 0; i < Math.min(aParts.length, bParts.length); i++) {\n if (aParts[i] === bParts[i]) common++;\n else break;\n }\n return aParts.length + bParts.length - 2 * common;\n}\n\n/** Fuzzy lookup: find symbol with most similar name. */\nfunction fuzzyLookup(name: string, registry: FunctionRegistry): SymbolRow | null {\n let best: SymbolRow | null = null;\n let bestScore = 0;\n for (const [symName, candidates] of registry.reverseLookupEntries()) {\n const score = similarity(name, symName);\n if (score > bestScore && score > 0.6) {\n bestScore = score;\n best = candidates[0];\n }\n }\n return best;\n}\n\n/** Simple string similarity (Jaccard on character bigrams). */\nfunction similarity(a: string, b: string): number {\n if (a === b) return 1;\n const aBigrams = bigrams(a);\n const bBigrams = bigrams(b);\n const intersection = aBigrams.filter((bg) => bBigrams.includes(bg)).length;\n const union = aBigrams.length + bBigrams.length - intersection;\n return union > 0 ? intersection / union : 0;\n}\n\nfunction bigrams(s: string): string[] {\n const result: string[] = [];\n for (let i = 0; i < s.length - 1; i++) {\n result.push(s.slice(i, i + 2));\n }\n return result;\n}\n"],"mappings":"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","names":["candidates"]}
package/dist/sdk.js CHANGED
@@ -8,7 +8,7 @@ import {
8
8
  ingestDirectory,
9
9
  ingestFile,
10
10
  searchWiki
11
- } from "./chunk-WBGP7QM3.js";
11
+ } from "./chunk-X4ECGLYH.js";
12
12
  import {
13
13
  SUPPORTED_LANGUAGES,
14
14
  detectLanguage,
@@ -19,7 +19,8 @@ import {
19
19
  indexFile,
20
20
  listSymbols,
21
21
  searchSymbols
22
- } from "./chunk-SHLLMMMU.js";
22
+ } from "./chunk-FA324IBN.js";
23
+ import "./chunk-XK4Y53J2.js";
23
24
  export {
24
25
  LocalEmbedder,
25
26
  Memory,
@@ -53,7 +53,8 @@ CREATE TABLE IF NOT EXISTS messages (
53
53
  created_at INTEGER NOT NULL
54
54
  );
55
55
 
56
- -- L1: Atomic facts (populated by atom-extract pipeline, or CLI extract command)
56
+ -- L1: Atomic facts (phase 2 — populated by atom-extract pipeline or CLI extract command.
57
+ -- Currently written by the extract CLI subcommand; NoopPipeline does not populate this.)
57
58
  CREATE TABLE IF NOT EXISTS atoms (
58
59
  id TEXT PRIMARY KEY,
59
60
  capture_id TEXT NOT NULL REFERENCES captures(id) ON DELETE CASCADE,
@@ -65,7 +66,7 @@ CREATE TABLE IF NOT EXISTS atoms (
65
66
  user_id TEXT
66
67
  );
67
68
 
68
- -- L2: Scenario blocks (populated by scenario pipeline)
69
+ -- L2: Scenario blocks (phase 2 — populated by scenario pipeline, not yet implemented)
69
70
  CREATE TABLE IF NOT EXISTS scenarios (
70
71
  id TEXT PRIMARY KEY,
71
72
  atom_ids TEXT NOT NULL,
@@ -179,6 +180,7 @@ CREATE TABLE IF NOT EXISTS symbols (
179
180
  team_id TEXT,
180
181
  repo_path TEXT, -- root path of the indexed repo
181
182
  content_hash TEXT, -- hash of the symbol body for change detection
183
+ module_path TEXT, -- qualified module path (e.g. src/storage/sqlite) for call resolution
182
184
  created_at INTEGER NOT NULL,
183
185
  updated_at INTEGER NOT NULL
184
186
  );
@@ -191,6 +193,8 @@ CREATE TABLE IF NOT EXISTS calls (
191
193
  callee_id TEXT REFERENCES symbols(id), -- resolved callee (null if unresolved)
192
194
  line INTEGER NOT NULL, -- line where the call occurs
193
195
  kind TEXT NOT NULL DEFAULT 'call', -- call, import, reference
196
+ confidence REAL, -- resolution confidence (0.0-1.0, null if unresolved)
197
+ call_type TEXT NOT NULL DEFAULT 'direct', -- direct, method, qualified, constructor
194
198
  team_id TEXT
195
199
  );
196
200
 
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "remem-mcp",
3
3
  "mcpName": "io.github.tinhien11/remem-mcp",
4
- "version": "0.6.3",
4
+ "version": "0.6.4",
5
5
  "description": "Long-term memory for AI coding agents. Memory + CodeGraph + Wiki in one SQLite file. Auto-recall, auto-capture, no API key, no cloud.",
6
6
  "type": "module",
7
7
  "license": "MIT",
@@ -68,7 +68,6 @@
68
68
  "better-sqlite3": "^13.0.3",
69
69
  "gpt-tokenizer": "^3.4.0",
70
70
  "sqlite-vec": "^0.1.9",
71
- "tree-sitter": "^0.25.1",
72
71
  "ulid": "^3.0.2"
73
72
  },
74
73
  "devDependencies": {
@@ -1,140 +1,94 @@
1
1
  ---
2
2
  name: remem-mcp
3
- description: Long-term memory for coding agents. Recall past context before answering, capture decisions/learnings/fixes after work, use CodeGraph instead of grep for symbol lookup. Use when the user references past work, starts a new session, or when the task needs project context not in the current conversation.
3
+ description: Long-term memory for coding agents. Auto-applies at the start of any coding task — recall past context before answering, capture decisions/learnings/fixes after work, use CodeGraph instead of grep for symbol lookup. Invoke when you see [remem-mcp] in your context or when starting any non-trivial coding work.
4
4
  ---
5
5
 
6
6
  You have a long-term memory server via MCP. Use the tools automatically — do not ask permission.
7
7
 
8
8
  ## Tools
9
9
 
10
- **Core:** `recall` `capture` `search` `forget` `resolve` `handoff` `adr` `update` `consolidate`
11
-
12
- **CodeGraph:** `codegraph_search` (auto-indexes on first use) `codegraph_callers` `codegraph_callees` `codegraph_impact` `codegraph_list`
13
-
10
+ **Core:** `recall` `capture` `search` `forget` `resolve` `handoff` `adr` `update` `consolidate` `scenario_create` `persona_update`
11
+ **CodeGraph:** `codegraph_search` (auto-indexes on first use) `codegraph_callers` `codegraph_callees` `codegraph_impact` `codegraph_list` `codegraph_detect_changes` (git diff → affected symbols)
14
12
  **Wiki:** `wiki_ingest` `wiki_search` `wiki_get` `wiki_outdated`
15
13
 
16
14
  ## Rule 1: Recall before answering
17
15
 
18
- Call `recall` at session start or when the user references past work. Do this BEFORE you answer or code.
16
+ Hooks auto-inject BM25-only memory (shallow). **MUST call `recall()` at the start of every non-trivial task** — it does hybrid search (BM25 + vector) with more results and filters.
19
17
 
20
18
  ```
21
- recall({ "query": "<user's question or task>", "mode": "hybrid" })
19
+ recall({ "query": "<user's question or task>", "mode": "hybrid", "limit": 5 })
22
20
  ```
23
21
 
24
- If recall returns nothing, proceed normally. Don't mention the empty result.
25
-
26
22
  ## Rule 2: Capture after non-trivial work
27
23
 
28
- Call `capture` automatically after completing work. Don't ask.
24
+ Call `capture` automatically after completing work. Don't ask. Include `atoms` — short distilled facts that recall returns instead of raw content (90% fewer tokens).
29
25
 
30
26
  ```
31
- capture({ "content": "Chose SQLite over Postgres for zero-setup MVP.", "type": "decision", "tags": ["arch"] })
27
+ capture({
28
+ "content": "Chose SQLite over Postgres for zero-setup MVP. SQLite has FTS5 + sqlite-vec built in, no server needed.",
29
+ "type": "decision",
30
+ "tags": ["arch"],
31
+ "atoms": ["Use SQLite (not Postgres) for zero-setup MVP", "SQLite has FTS5 + sqlite-vec built in"]
32
+ })
32
33
  ```
33
34
 
34
- **Types:** `decision` (chose X over Y, include why) · `learning` (non-obvious fact) · `task` (completed work) · `error` (bug + root cause + fix) · `conversation` (multi-turn, pass `messages`)
35
+ **Types:** `decision` (chose X over Y, include why) · `learning` (non-obvious fact) · `task` (completed work) · `error` (bug + root cause + fix)
35
36
 
36
- **Good:** specific, useful later. **Bad:** "We talked about the database."
37
+ **Atoms:** 1-3 short self-contained facts. Each useful on its own without the raw content. Write for decisions, learnings, errors. Skip for conversations.
37
38
 
38
- ## Rule 3: Use CodeGraph instead of grep
39
+ ## Rule 3: Consolidate atoms into scenarios (L2)
39
40
 
40
- For function/class/method definitions, call `codegraph_search` — NOT grep. It auto-indexes `src/` on first use, no setup needed.
41
+ When you have 5+ atoms about the same topic, call `scenario_create` to create a high-signal summary. Recall injects scenarios automatically (~100 tokens instead of 5+ atoms).
41
42
 
42
43
  ```
43
- codegraph_search({ "query": "handleCapture" })
44
- codegraph_callers({ "symbol_id": "<id from search>" })
45
- codegraph_impact({ "symbol_id": "<id>" }) // before modifying a function
44
+ scenario_create({
45
+ "atom_ids": ["01...", "01...", "01..."],
46
+ "summary": "Database: SQLite chosen for MVP — FTS5 + sqlite-vec built in, zero setup, no server needed.",
47
+ "persona_tags": ["database", "arch"]
48
+ })
46
49
  ```
47
50
 
48
- Use grep only for: string literals, config values, file names.
49
-
50
- ## Rule 4: Handoff when switching agents
51
-
52
- Call `handoff` at session end or before switching agents. Creates a packet the next agent loads via `recall`.
51
+ **When:** After capturing 5+ decisions/learnings about the same topic (e.g., database, hooks, deployment).
53
52
 
54
- ```
55
- handoff({ "task": "Fix auth bug", "status": "in_progress", "progress": "Found root cause", "next_steps": ["Implement fix"] })
56
- ```
53
+ **Auto-pipeline:** The Stop hook spawns a background worker that auto-extracts L1 atoms from uncaptured L0 entries, auto-consolidates 5+ atoms on the same topic into L2 scenarios, and auto-updates L3 persona from repeated tags. You don't need to call `scenario_create` or `persona_update` manually — the worker does it. Only call them manually if you want a specific summary the worker wouldn't generate.
57
54
 
58
- Skip if task is done or trivial.
55
+ ## Rule 4: Update persona (L3)
59
56
 
60
- ## Rule 5: Forget only on explicit request
61
-
62
- Call `forget` ONLY when the user asks to delete. Always require `confirm: true`.
57
+ When you notice a user preference or pattern (2+ occurrences), call `persona_update`. SessionStart AND UserPromptSubmit inject persona automatically every turn (~50 tokens).
63
58
 
64
59
  ```
65
- forget({ "id": "<id>", "confirm": true, "reject": true, "reason": "Wrong: port is 9090 not 8080." })
60
+ persona_update({ "trait": "language", "value": "Vietnamese" })
61
+ persona_update({ "trait": "output_style", "value": "concise" })
66
62
  ```
67
63
 
68
- `reject: true` tombstones the content hash — blocks re-capture of wrong info.
69
-
70
- ## Trust states
64
+ **When:** User asks for concise output 2+ times, works in a specific language, prefers a framework, uses a specific project.
71
65
 
72
- - `candidate` (default) → `verified` (confirmed correct) → `stale` (replaced, via `resolve` or `supersedes`) → `rejected` (wrong, via `forget`)
73
- - Use `resolve` when two captures conflict. Use `supersedes` on `capture` to replace an old value.
74
-
75
- ## ADR for architectural decisions
76
-
77
- Use `adr` (not `capture`) for decisions with context, alternatives, and consequences.
78
-
79
- ```
80
- adr({ "title": "Use SQLite", "context": "Need zero-setup", "decision": "SQLite + FTS5 + sqlite-vec", "alternatives": ["Postgres+pgvector", "DuckDB"], "consequences": "Single-writer, no remote access" })
81
- ```
66
+ **Auto-pipeline:** The background worker auto-detects tags appearing 2+ times in captures and appends them to persona. Manual `persona_update` is for explicit user preferences the worker can't detect.
82
67
 
83
- ## Search with filters
68
+ ## Rule 5: Use CodeGraph instead of grep
84
69
 
85
- Use `search` (not `recall`) when you need specific filters:
70
+ For function/class/method definitions, call `codegraph_search` — NOT grep. Auto-indexes `src/` on first use.
86
71
 
87
72
  ```
88
- search({ "query": "auth", "filters": { "type": "decision", "tags": ["arch"] } })
73
+ codegraph_search({ "query": "handleCapture" })
74
+ codegraph_callers({ "symbol_id": "<id from search>" })
75
+ codegraph_detect_changes({ "repo_path": "/abs/path" }) // git diff → affected symbols + risk
89
76
  ```
90
77
 
91
- ## Update and consolidate
92
-
93
- - `update` — correct a capture's content/tags. Preserves ID and created_at.
94
- - `consolidate` — find and merge duplicates. `consolidate({ "threshold": 0.75 })` to dry-run, add `"confirm": true` to merge.
78
+ CodeGraph uses 6-strategy call resolution (import-map → same-module → unique-name → suffix → fuzzy) with confidence scoring. JSX components (`<FleetMap/>`) and method calls (`obj.method()`) are captured. Stdlib calls (fmt.Printf, console.log) are filtered out.
95
79
 
96
- ## Hooks (automatic if installed)
97
-
98
- If `npx remem-mcp install-hooks` was run:
99
- - **SessionStart** — recent captures injected automatically
100
- - **PreToolUse** — past errors injected before lint/build/test
101
- - **PostToolUse** — failed commands auto-captured with root cause
102
- - **SessionEnd** — session summary auto-captured
103
-
104
- You can still call tools manually anytime.
80
+ Use grep only for: string literals, config values, file names.
105
81
 
106
- ## Multi-tenant
82
+ ## Other tools
107
83
 
108
- Pass `team_id`, `agent_id`, `user_id`, or `task_id` to isolate memory between teams/projects.
84
+ - `forget({ "id", "confirm": true })` — only when user asks to delete. `reject: true` blocks re-capture of wrong info.
85
+ - `handoff({ "task", "status", "progress", "next_steps" })` — at session end or before switching agents.
86
+ - `adr({ "title", "context", "decision", "alternatives", "consequences" })` — for architectural decisions.
87
+ - `search({ "query", "filters": { "type", "tags" } })` — when you need specific filters.
88
+ - `resolve` — when two captures conflict. `supersedes` on `capture` replaces old values.
89
+ - `update` — correct a capture's content/tags.
90
+ - `consolidate({ "threshold": 0.75 })` — merge duplicate captures. `scenario_create({ "atom_ids", "summary" })` — create L2 scenario. `persona_update({ "trait", "value" })` — update L3 persona.
109
91
 
110
92
  ## Global + project memory
111
93
 
112
- Set `REMEM_GLOBAL_SESSION_KEY=global` in MCP config to enable cross-project memory.
113
-
114
- **How it works:**
115
- - `recall` and `search` automatically search both project and global memory. Project results appear first, then global.
116
- - `session_start` returns recent captures from both project and global sessions.
117
- - 3 slots are reserved for global results so cross-project knowledge isn't buried when project memory is large.
118
-
119
- **When to store global memory:**
120
- - Pass `session_key="global"` to `capture` for: coding conventions, tool preferences, recurring patterns, lessons that apply to any project.
121
- - Or pass `auto_global=true` — server auto-classifies: generic rules/learnings → global, content with file paths/line numbers → project.
122
- - Don't store global: project-specific bugs, file paths, one-off decisions.
123
-
124
- **Example:**
125
- ```
126
- capture(content="Always run tests before committing", type="learning", auto_global=true)
127
- # → auto-classified as global (no file paths, has "always" signal)
128
-
129
- capture(content="Fixed bug in src/server.ts line 1418", type="task", auto_global=true)
130
- # → stays project (has file path)
131
- ```
132
-
133
- ## CLI commands
134
-
135
- ```bash
136
- npx remem-mcp errors # Error learning dashboard
137
- npx remem-mcp extract --limit 50 # L1 atom extraction (needs REMEM_LLM_API_KEY)
138
- npx remem-mcp sync-export # Export memory for team (commit to repo)
139
- npx remem-mcp sync-import # Import teammate's memory
140
- ```
94
+ Set `REMEM_GLOBAL_SESSION_KEY=global` to enable cross-project memory. `recall`/`search` search both. Pass `auto_global=true` to `capture` for auto-classification (generic → global, file paths → project).