@exaudeus/memory-mcp 1.0.0 → 1.0.1
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/dist/index.js +226 -146
- package/dist/store.js +17 -0
- package/dist/thresholds.d.ts +8 -0
- package/dist/thresholds.js +8 -0
- package/package.json +2 -2
package/dist/index.js
CHANGED
|
@@ -9,6 +9,7 @@ import { z } from 'zod';
|
|
|
9
9
|
import path from 'path';
|
|
10
10
|
import os from 'os';
|
|
11
11
|
import { existsSync, writeFileSync } from 'fs';
|
|
12
|
+
import { readFile, writeFile } from 'fs/promises';
|
|
12
13
|
import { MarkdownMemoryStore } from './store.js';
|
|
13
14
|
import { DEFAULT_STORAGE_BUDGET_BYTES, parseTopicScope, parseTrustLevel } from './types.js';
|
|
14
15
|
import { getLobeConfigs } from './config.js';
|
|
@@ -16,6 +17,8 @@ import { ConfigManager } from './config-manager.js';
|
|
|
16
17
|
import { normalizeArgs } from './normalize.js';
|
|
17
18
|
import { buildCrashReport, writeCrashReport, writeCrashReportSync, readLatestCrash, readCrashHistory, clearLatestCrash, formatCrashReport, formatCrashSummary, markServerStarted, } from './crash-journal.js';
|
|
18
19
|
import { formatStaleSection, formatConflictWarning, formatStats, formatBehaviorConfigSection } from './formatters.js';
|
|
20
|
+
import { extractKeywords } from './text-analyzer.js';
|
|
21
|
+
import { CROSS_LOBE_WEAK_SCORE_PENALTY, CROSS_LOBE_MIN_MATCH_RATIO } from './thresholds.js';
|
|
19
22
|
let serverMode = { kind: 'running' };
|
|
20
23
|
const lobeHealth = new Map();
|
|
21
24
|
const serverStartTime = Date.now();
|
|
@@ -105,10 +108,9 @@ function resolveToolContext(rawLobe, opts) {
|
|
|
105
108
|
const configOrigin = configManager.getConfigOrigin();
|
|
106
109
|
let hint = '';
|
|
107
110
|
if (configOrigin.source === 'file') {
|
|
108
|
-
hint = `\n\nTo add lobe "${lobe}":\n` +
|
|
109
|
-
`
|
|
110
|
-
`
|
|
111
|
-
`3. Restart the memory MCP server`;
|
|
111
|
+
hint = `\n\nTo add lobe "${lobe}", either:\n` +
|
|
112
|
+
`A) Call memory_bootstrap(lobe: "${lobe}", root: "/absolute/path/to/repo") — auto-adds it in one step.\n` +
|
|
113
|
+
`B) Edit ${configOrigin.path}, add: "${lobe}": { "root": "/absolute/path/to/repo", "budgetMB": 2 }, then retry (no restart needed — the server hot-reloads automatically).`;
|
|
112
114
|
}
|
|
113
115
|
else if (configOrigin.source === 'env') {
|
|
114
116
|
hint = `\n\nTo add lobe "${lobe}", update MEMORY_MCP_WORKSPACES env var or create memory-config.json`;
|
|
@@ -152,163 +154,186 @@ function inferLobeFromPaths(paths) {
|
|
|
152
154
|
return matchedLobes.size === 1 ? matchedLobes.values().next().value : undefined;
|
|
153
155
|
}
|
|
154
156
|
const server = new Server({ name: 'memory-mcp', version: '0.1.0' }, { capabilities: { tools: {} } });
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
:
|
|
162
|
-
|
|
163
|
-
}
|
|
157
|
+
/** Build the shared lobe property for tool schemas — called on each ListTools request
|
|
158
|
+
* so the description and enum stay in sync after a hot-reload adds or removes lobes. */
|
|
159
|
+
function buildLobeProperty(currentLobeNames) {
|
|
160
|
+
const isSingle = currentLobeNames.length === 1;
|
|
161
|
+
return {
|
|
162
|
+
type: 'string',
|
|
163
|
+
description: isSingle
|
|
164
|
+
? `Memory lobe name (defaults to "${currentLobeNames[0]}" if omitted)`
|
|
165
|
+
: `Memory lobe name. Optional for reads (query/context/briefing/stats search all lobes when omitted). Required for writes (store/correct/bootstrap). Available: ${currentLobeNames.join(', ')}`,
|
|
166
|
+
enum: currentLobeNames.length > 1 ? [...currentLobeNames] : undefined,
|
|
167
|
+
};
|
|
168
|
+
}
|
|
164
169
|
/** Helper to format config file path for display */
|
|
165
170
|
function configFileDisplay() {
|
|
166
171
|
const origin = configManager.getConfigOrigin();
|
|
167
172
|
return origin.source === 'file' ? origin.path : '(not using config file)';
|
|
168
173
|
}
|
|
169
174
|
// --- Tool definitions ---
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
175
|
+
// Handler is async so it can call configManager.ensureFresh() and return a fresh
|
|
176
|
+
// lobe list. This ensures the enum and descriptions stay correct after hot-reload.
|
|
177
|
+
server.setRequestHandler(ListToolsRequestSchema, async () => {
|
|
178
|
+
await configManager.ensureFresh();
|
|
179
|
+
const currentLobeNames = configManager.getLobeNames();
|
|
180
|
+
const lobeProperty = buildLobeProperty(currentLobeNames);
|
|
181
|
+
return { tools: [
|
|
182
|
+
// memory_list_lobes is hidden — lobe info is surfaced in memory_context() hints
|
|
183
|
+
// and memory_stats. The handler still works if called directly.
|
|
184
|
+
{
|
|
185
|
+
name: 'memory_store',
|
|
186
|
+
description: 'Store knowledge. "user" and "preferences" are global (no lobe needed). Example: memory_store(topic: "gotchas", title: "Build cache", content: "Must clean build after Tuist changes")',
|
|
187
|
+
inputSchema: {
|
|
188
|
+
type: 'object',
|
|
189
|
+
properties: {
|
|
190
|
+
lobe: lobeProperty,
|
|
191
|
+
topic: {
|
|
192
|
+
type: 'string',
|
|
193
|
+
// modules/<name> is intentionally excluded from the enum so the MCP schema
|
|
194
|
+
// doesn't restrict it — agents can pass any "modules/foo" value and it works.
|
|
195
|
+
// The description makes this explicit.
|
|
196
|
+
description: 'Predefined: user | preferences | architecture | conventions | gotchas | recent-work. Custom namespace: modules/<name> (e.g. modules/brainstorm, modules/game-design, modules/api-notes). Use modules/<name> for any domain that doesn\'t fit the built-in topics.',
|
|
197
|
+
enum: ['user', 'preferences', 'architecture', 'conventions', 'gotchas', 'recent-work'],
|
|
198
|
+
},
|
|
199
|
+
title: {
|
|
200
|
+
type: 'string',
|
|
201
|
+
description: 'Short title for this entry',
|
|
202
|
+
},
|
|
203
|
+
content: {
|
|
204
|
+
type: 'string',
|
|
205
|
+
description: 'The knowledge to store',
|
|
206
|
+
},
|
|
207
|
+
sources: {
|
|
208
|
+
type: 'array',
|
|
209
|
+
items: { type: 'string' },
|
|
210
|
+
description: 'File paths that informed this (provenance, for freshness tracking)',
|
|
211
|
+
default: [],
|
|
212
|
+
},
|
|
213
|
+
references: {
|
|
214
|
+
type: 'array',
|
|
215
|
+
items: { type: 'string' },
|
|
216
|
+
description: 'Files, classes, or symbols this knowledge is about (semantic pointers). Example: ["features/messaging/impl/MessagingReducer.kt"]',
|
|
217
|
+
default: [],
|
|
218
|
+
},
|
|
219
|
+
trust: {
|
|
220
|
+
type: 'string',
|
|
221
|
+
enum: ['user', 'agent-confirmed', 'agent-inferred'],
|
|
222
|
+
description: 'user (from human) > agent-confirmed > agent-inferred',
|
|
223
|
+
default: 'agent-inferred',
|
|
224
|
+
},
|
|
211
225
|
},
|
|
226
|
+
required: ['topic', 'title', 'content'],
|
|
212
227
|
},
|
|
213
|
-
required: ['topic', 'title', 'content'],
|
|
214
228
|
},
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
229
|
+
{
|
|
230
|
+
name: 'memory_query',
|
|
231
|
+
description: 'Search stored knowledge. Searches all lobes when lobe is omitted. Example: memory_query(scope: "*", filter: "reducer sealed", detail: "full"). Use scope "*" to search everything. Use detail "full" for complete content.',
|
|
232
|
+
inputSchema: {
|
|
233
|
+
type: 'object',
|
|
234
|
+
properties: {
|
|
235
|
+
lobe: lobeProperty,
|
|
236
|
+
scope: {
|
|
237
|
+
type: 'string',
|
|
238
|
+
description: 'Optional. Defaults to "*" (all topics). Options: * | user | preferences | architecture | conventions | gotchas | recent-work | modules/<name>',
|
|
239
|
+
},
|
|
240
|
+
detail: {
|
|
241
|
+
type: 'string',
|
|
242
|
+
enum: ['brief', 'standard', 'full'],
|
|
243
|
+
description: 'brief = titles only, standard = summaries, full = complete content + metadata',
|
|
244
|
+
default: 'brief',
|
|
245
|
+
},
|
|
246
|
+
filter: {
|
|
247
|
+
type: 'string',
|
|
248
|
+
description: 'Search terms. "A B" = AND, "A|B" = OR, "-A" = NOT. Example: "reducer sealed -deprecated"',
|
|
249
|
+
},
|
|
250
|
+
branch: {
|
|
251
|
+
type: 'string',
|
|
252
|
+
description: 'Branch for recent-work. Omit = current branch, "*" = all branches.',
|
|
253
|
+
},
|
|
240
254
|
},
|
|
255
|
+
required: [],
|
|
241
256
|
},
|
|
242
|
-
required: [],
|
|
243
257
|
},
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
258
|
+
{
|
|
259
|
+
name: 'memory_correct',
|
|
260
|
+
description: 'Fix or delete an entry. Example: memory_correct(id: "arch-3f7a", action: "replace", correction: "updated content")',
|
|
261
|
+
inputSchema: {
|
|
262
|
+
type: 'object',
|
|
263
|
+
properties: {
|
|
264
|
+
lobe: lobeProperty,
|
|
265
|
+
id: {
|
|
266
|
+
type: 'string',
|
|
267
|
+
description: 'Entry ID (e.g. arch-3f7a, pref-5c9b)',
|
|
268
|
+
},
|
|
269
|
+
correction: {
|
|
270
|
+
type: 'string',
|
|
271
|
+
description: 'New text (for append/replace). Not needed for delete.',
|
|
272
|
+
},
|
|
273
|
+
action: {
|
|
274
|
+
type: 'string',
|
|
275
|
+
enum: ['append', 'replace', 'delete'],
|
|
276
|
+
description: 'append | replace | delete',
|
|
277
|
+
},
|
|
264
278
|
},
|
|
279
|
+
required: ['id', 'action'],
|
|
265
280
|
},
|
|
266
|
-
required: ['id', 'action'],
|
|
267
281
|
},
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
282
|
+
{
|
|
283
|
+
name: 'memory_context',
|
|
284
|
+
description: 'Session start AND pre-task lookup. Call with no args at session start to get user identity, preferences, and stale entries. Call with context to get task-specific knowledge. Searches all lobes when lobe is omitted. Example: memory_context() or memory_context(context: "writing a Kotlin reducer")',
|
|
285
|
+
inputSchema: {
|
|
286
|
+
type: 'object',
|
|
287
|
+
properties: {
|
|
288
|
+
lobe: lobeProperty,
|
|
289
|
+
context: {
|
|
290
|
+
type: 'string',
|
|
291
|
+
description: 'Optional. What you are about to do, in natural language. Omit for session-start briefing (user + preferences + stale entries).',
|
|
292
|
+
},
|
|
293
|
+
maxResults: {
|
|
294
|
+
type: 'number',
|
|
295
|
+
description: 'Max results (default: 10)',
|
|
296
|
+
default: 10,
|
|
297
|
+
},
|
|
298
|
+
minMatch: {
|
|
299
|
+
type: 'number',
|
|
300
|
+
description: 'Min keyword match ratio 0-1 (default: 0.2). Higher = stricter.',
|
|
301
|
+
default: 0.2,
|
|
302
|
+
},
|
|
289
303
|
},
|
|
304
|
+
required: [],
|
|
290
305
|
},
|
|
291
|
-
required: [],
|
|
292
306
|
},
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
307
|
+
// memory_stats is hidden — agents rarely need it proactively. Mentioned in
|
|
308
|
+
// hints when storage is running low. The handler still works if called directly.
|
|
309
|
+
{
|
|
310
|
+
name: 'memory_bootstrap',
|
|
311
|
+
description: 'First-time setup: scan repo structure, README, and build system to seed initial knowledge. Run once per new codebase. If the lobe does not exist yet, provide "root" to auto-add it to memory-config.json and proceed without a manual restart.',
|
|
312
|
+
inputSchema: {
|
|
313
|
+
type: 'object',
|
|
314
|
+
properties: {
|
|
315
|
+
lobe: {
|
|
316
|
+
type: 'string',
|
|
317
|
+
// No enum restriction: agents pass new lobe names not yet in the config
|
|
318
|
+
description: `Memory lobe name. If the lobe doesn't exist yet, also pass "root" to auto-create it. Available lobes: ${currentLobeNames.join(', ')}`,
|
|
319
|
+
},
|
|
320
|
+
root: {
|
|
321
|
+
type: 'string',
|
|
322
|
+
description: 'Absolute path to the repo root. Required only when the lobe does not exist yet — the server will add it to memory-config.json automatically.',
|
|
323
|
+
},
|
|
324
|
+
budgetMB: {
|
|
325
|
+
type: 'number',
|
|
326
|
+
description: 'Storage budget in MB for the new lobe (default: 2). Only used when auto-creating a lobe via "root".',
|
|
327
|
+
},
|
|
328
|
+
},
|
|
329
|
+
required: [],
|
|
303
330
|
},
|
|
304
|
-
required: [],
|
|
305
331
|
},
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
}));
|
|
332
|
+
// memory_diagnose is intentionally hidden from the tool list — it clutters
|
|
333
|
+
// agent tool discovery and should only be called when directed by error messages
|
|
334
|
+
// or crash reports. The handler still works if called directly.
|
|
335
|
+
] };
|
|
336
|
+
});
|
|
312
337
|
// --- Tool handlers ---
|
|
313
338
|
server.setRequestHandler(CallToolRequestSchema, async (request) => {
|
|
314
339
|
const { name, arguments: rawArgs } = request.params;
|
|
@@ -564,7 +589,7 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
|
|
|
564
589
|
// Nudge: when searching all lobes, remind the agent to specify one for targeted results
|
|
565
590
|
const allQueryLobeNames = configManager.getLobeNames();
|
|
566
591
|
if (!rawLobe && !isGlobalQuery && allQueryLobeNames.length > 1) {
|
|
567
|
-
hints.push(`Searched all lobes
|
|
592
|
+
hints.push(`Searched all lobes (${allQueryLobeNames.join(', ')}). Specify lobe: "<name>" for targeted results.`);
|
|
568
593
|
}
|
|
569
594
|
if (detail !== 'full') {
|
|
570
595
|
hints.push('Use detail: "full" to see complete entry content.');
|
|
@@ -742,6 +767,21 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
|
|
|
742
767
|
allLobeResults.push(...lobeResults);
|
|
743
768
|
}
|
|
744
769
|
}
|
|
770
|
+
// Cross-lobe weak-match penalty: demote results from other repos that only matched
|
|
771
|
+
// on generic software terms (e.g. "codebase", "structure"). Without this, a high-
|
|
772
|
+
// confidence entry from an unrelated repo can outrank genuinely relevant knowledge
|
|
773
|
+
// simply because popular terms appear in it.
|
|
774
|
+
// Applied only in multi-lobe mode; single-lobe and global results are never penalized.
|
|
775
|
+
if (isCtxMultiLobe) {
|
|
776
|
+
const contextKwCount = extractKeywords(context).size;
|
|
777
|
+
// Minimum keyword matches required to avoid the penalty (at least 40% of context, min 2)
|
|
778
|
+
const minMatchCount = Math.max(2, Math.ceil(contextKwCount * CROSS_LOBE_MIN_MATCH_RATIO));
|
|
779
|
+
for (let i = 0; i < allLobeResults.length; i++) {
|
|
780
|
+
if (allLobeResults[i].matchedKeywords.length < minMatchCount) {
|
|
781
|
+
allLobeResults[i] = { ...allLobeResults[i], score: allLobeResults[i].score * CROSS_LOBE_WEAK_SCORE_PENALTY };
|
|
782
|
+
}
|
|
783
|
+
}
|
|
784
|
+
}
|
|
745
785
|
// Always include global store (user + preferences)
|
|
746
786
|
const globalResults = await globalStore.contextSearch(context, max, undefined, threshold);
|
|
747
787
|
for (const r of globalResults)
|
|
@@ -812,10 +852,14 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
|
|
|
812
852
|
}
|
|
813
853
|
// Hints
|
|
814
854
|
const ctxHints = [];
|
|
815
|
-
// Nudge: when searching all lobes,
|
|
855
|
+
// Nudge: when searching all lobes, infer the most relevant lobe from context keywords
|
|
856
|
+
// matching lobe names (e.g. "minion-miner" in context → suggest lobe "minion-miner"),
|
|
857
|
+
// falling back to the first lobe when no name overlap is found.
|
|
816
858
|
const allCtxLobeNames = configManager.getLobeNames();
|
|
817
859
|
if (!rawLobe && allCtxLobeNames.length > 1) {
|
|
818
|
-
|
|
860
|
+
const contextKws = extractKeywords(context);
|
|
861
|
+
const inferredLobe = allCtxLobeNames.find(name => [...extractKeywords(name)].some(kw => contextKws.has(kw))) ?? allCtxLobeNames[0];
|
|
862
|
+
ctxHints.push(`Searched all lobes. For faster, targeted results use lobe: "${inferredLobe}" (available: ${allCtxLobeNames.join(', ')}).`);
|
|
819
863
|
}
|
|
820
864
|
if (results.length >= max) {
|
|
821
865
|
ctxHints.push(`Showing top ${max} results. Increase maxResults or raise minMatch to refine.`);
|
|
@@ -861,9 +905,45 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
|
|
|
861
905
|
return { content: [{ type: 'text', text: sections.join('\n\n---\n\n') }] };
|
|
862
906
|
}
|
|
863
907
|
case 'memory_bootstrap': {
|
|
864
|
-
const { lobe: rawLobe } = z.object({
|
|
908
|
+
const { lobe: rawLobe, root, budgetMB } = z.object({
|
|
865
909
|
lobe: z.string().optional(),
|
|
910
|
+
root: z.string().optional(),
|
|
911
|
+
budgetMB: z.number().positive().optional(),
|
|
866
912
|
}).parse(args);
|
|
913
|
+
// Auto-create lobe: if the lobe is unknown AND root is provided AND config is
|
|
914
|
+
// file-based, write the new lobe entry into memory-config.json and hot-reload.
|
|
915
|
+
// This lets the agent bootstrap a brand-new repo in a single tool call.
|
|
916
|
+
if (rawLobe && root && !configManager.getStore(rawLobe)) {
|
|
917
|
+
const origin = configManager.getConfigOrigin();
|
|
918
|
+
if (origin.source !== 'file') {
|
|
919
|
+
return {
|
|
920
|
+
content: [{
|
|
921
|
+
type: 'text',
|
|
922
|
+
text: `Cannot auto-add lobe "${rawLobe}": config is not file-based (source: ${origin.source}).\n\n` +
|
|
923
|
+
`Create memory-config.json next to the memory MCP server with a "lobes" block, then retry.`,
|
|
924
|
+
}],
|
|
925
|
+
isError: true,
|
|
926
|
+
};
|
|
927
|
+
}
|
|
928
|
+
try {
|
|
929
|
+
const raw = await readFile(origin.path, 'utf-8');
|
|
930
|
+
const config = JSON.parse(raw);
|
|
931
|
+
if (!config.lobes || typeof config.lobes !== 'object')
|
|
932
|
+
config.lobes = {};
|
|
933
|
+
config.lobes[rawLobe] = { root, budgetMB: budgetMB ?? 2 };
|
|
934
|
+
await writeFile(origin.path, JSON.stringify(config, null, 2) + '\n', 'utf-8');
|
|
935
|
+
process.stderr.write(`[memory-mcp] Auto-added lobe "${rawLobe}" (root: ${root}) to memory-config.json\n`);
|
|
936
|
+
}
|
|
937
|
+
catch (err) {
|
|
938
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
939
|
+
return {
|
|
940
|
+
content: [{ type: 'text', text: `Failed to auto-add lobe "${rawLobe}" to memory-config.json: ${message}` }],
|
|
941
|
+
isError: true,
|
|
942
|
+
};
|
|
943
|
+
}
|
|
944
|
+
// Reload config to pick up the new lobe (hot-reload detects the updated mtime)
|
|
945
|
+
await configManager.ensureFresh();
|
|
946
|
+
}
|
|
867
947
|
// Resolve store — after this point, rawLobe is never used again
|
|
868
948
|
const ctx = resolveToolContext(rawLobe);
|
|
869
949
|
if (!ctx.ok)
|
|
@@ -907,7 +987,7 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
|
|
|
907
987
|
hint = `\n\nHint: lobe is required. Use memory_list_lobes to see available lobes. Available: ${lobeNames.join(', ')}`;
|
|
908
988
|
}
|
|
909
989
|
else if (message.includes('"topic"') || message.includes('"title"') || message.includes('"content"')) {
|
|
910
|
-
hint = '\n\nHint: memory_store requires:
|
|
990
|
+
hint = '\n\nHint: memory_store requires: topic (architecture|conventions|gotchas|recent-work|modules/<name>), title, content. Use modules/<name> for custom namespaces (e.g. modules/brainstorm, modules/game-design).';
|
|
911
991
|
}
|
|
912
992
|
else if (message.includes('"scope"')) {
|
|
913
993
|
hint = '\n\nHint: memory_query requires: lobe, scope (architecture|conventions|gotchas|recent-work|modules/<name>|* for all)';
|
package/dist/store.js
CHANGED
|
@@ -344,6 +344,7 @@ export class MarkdownMemoryStore {
|
|
|
344
344
|
{ file: 'go.mod', meaning: 'Go module' },
|
|
345
345
|
{ file: 'pyproject.toml', meaning: 'Python project' },
|
|
346
346
|
{ file: 'Tuist.swift', meaning: 'iOS project managed by Tuist' },
|
|
347
|
+
{ file: 'project.godot', meaning: 'Godot Engine project (GDScript/C#)' },
|
|
347
348
|
];
|
|
348
349
|
const detected = [];
|
|
349
350
|
for (const { file, meaning } of buildFiles) {
|
|
@@ -362,6 +363,22 @@ export class MarkdownMemoryStore {
|
|
|
362
363
|
results.push(await this.store('recent-work', 'Recent Git History', `Last 5 commits:\n${stdout.trim()}`, [], 'agent-inferred'));
|
|
363
364
|
}
|
|
364
365
|
catch { /* not a git repo or git not available */ }
|
|
366
|
+
// 5. Fallback: if nothing was detected, store a minimal overview so the lobe
|
|
367
|
+
// is never left completely empty after bootstrap (makes memory_context useful immediately).
|
|
368
|
+
const storedCount = results.filter(r => r.stored).length;
|
|
369
|
+
if (storedCount === 0) {
|
|
370
|
+
try {
|
|
371
|
+
const topLevel = await fs.readdir(repoRoot, { withFileTypes: true });
|
|
372
|
+
const items = topLevel
|
|
373
|
+
.filter(d => !d.name.startsWith('.') && d.name !== 'node_modules')
|
|
374
|
+
.map(d => `${d.isDirectory() ? '[dir]' : '[file]'} ${d.name}`)
|
|
375
|
+
.join(', ');
|
|
376
|
+
results.push(await this.store('architecture', 'Repo Overview', items.length > 0
|
|
377
|
+
? `Minimal repository. Top-level contents: ${items}.`
|
|
378
|
+
: `Empty or newly initialized repository at ${repoRoot}.`, [], 'agent-inferred'));
|
|
379
|
+
}
|
|
380
|
+
catch { /* ignore */ }
|
|
381
|
+
}
|
|
365
382
|
return results;
|
|
366
383
|
}
|
|
367
384
|
// --- Contextual search (memory_context) ---
|
package/dist/thresholds.d.ts
CHANGED
|
@@ -16,6 +16,14 @@ export declare const CONFLICT_MIN_CONTENT_CHARS = 50;
|
|
|
16
16
|
export declare const OPPOSITION_PAIRS: ReadonlyArray<readonly [string, string]>;
|
|
17
17
|
/** Score multiplier when a reference path basename matches the context keywords. */
|
|
18
18
|
export declare const REFERENCE_BOOST_MULTIPLIER = 1.3;
|
|
19
|
+
/** Score multiplier applied to weak cross-lobe results in multi-lobe context search.
|
|
20
|
+
* Prevents generic software terms (e.g. "codebase", "structure") from surfacing
|
|
21
|
+
* entries from unrelated repos with high confidence/topic-boost scores. */
|
|
22
|
+
export declare const CROSS_LOBE_WEAK_SCORE_PENALTY = 0.5;
|
|
23
|
+
/** Fraction of context keywords an entry must match to avoid the cross-lobe penalty.
|
|
24
|
+
* E.g. 0.40 means an entry must match at least 40% of the context keywords (minimum 2)
|
|
25
|
+
* to be treated as a strong cross-lobe match. */
|
|
26
|
+
export declare const CROSS_LOBE_MIN_MATCH_RATIO = 0.4;
|
|
19
27
|
/** Per-topic scoring boost factors for contextSearch().
|
|
20
28
|
* Higher = more likely to surface for any given context. */
|
|
21
29
|
export declare const TOPIC_BOOST: Record<string, number>;
|
package/dist/thresholds.js
CHANGED
|
@@ -42,6 +42,14 @@ export const OPPOSITION_PAIRS = [
|
|
|
42
42
|
];
|
|
43
43
|
/** Score multiplier when a reference path basename matches the context keywords. */
|
|
44
44
|
export const REFERENCE_BOOST_MULTIPLIER = 1.30;
|
|
45
|
+
/** Score multiplier applied to weak cross-lobe results in multi-lobe context search.
|
|
46
|
+
* Prevents generic software terms (e.g. "codebase", "structure") from surfacing
|
|
47
|
+
* entries from unrelated repos with high confidence/topic-boost scores. */
|
|
48
|
+
export const CROSS_LOBE_WEAK_SCORE_PENALTY = 0.50;
|
|
49
|
+
/** Fraction of context keywords an entry must match to avoid the cross-lobe penalty.
|
|
50
|
+
* E.g. 0.40 means an entry must match at least 40% of the context keywords (minimum 2)
|
|
51
|
+
* to be treated as a strong cross-lobe match. */
|
|
52
|
+
export const CROSS_LOBE_MIN_MATCH_RATIO = 0.40;
|
|
45
53
|
/** Per-topic scoring boost factors for contextSearch().
|
|
46
54
|
* Higher = more likely to surface for any given context. */
|
|
47
55
|
export const TOPIC_BOOST = {
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@exaudeus/memory-mcp",
|
|
3
|
-
"version": "1.0.
|
|
3
|
+
"version": "1.0.1",
|
|
4
4
|
"description": "Codebase memory MCP server - persistent, evolving knowledge for AI coding agents",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "dist/index.js",
|
|
@@ -47,7 +47,7 @@
|
|
|
47
47
|
"@semantic-release/github": "^12.0.6",
|
|
48
48
|
"@types/node": "^20.0.0",
|
|
49
49
|
"conventional-changelog-conventionalcommits": "^9.1.0",
|
|
50
|
-
"semantic-release": "^
|
|
50
|
+
"semantic-release": "^25.0.3",
|
|
51
51
|
"tsx": "^4.0.0",
|
|
52
52
|
"typescript": "^5.0.0"
|
|
53
53
|
},
|