@exaudeus/memory-mcp 0.1.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 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
- `1. Edit ${configOrigin.path}\n` +
110
- `2. Add: "${lobe}": { "root": "$HOME/git/.../repo", "memoryDir": ".memory", "budgetMB": 2 }\n` +
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
- // Shared lobe property for tool schemas
156
- const isSingleLobe = lobeNames.length === 1;
157
- const lobeProperty = {
158
- type: 'string',
159
- description: isSingleLobe
160
- ? `Memory lobe name (defaults to "${lobeNames[0]}" if omitted)`
161
- : `Memory lobe name. Optional for reads (query/context/briefing/stats search all lobes when omitted). Required for writes (store/correct/bootstrap). Available: ${lobeNames.join(', ')}`,
162
- enum: lobeNames.length > 1 ? lobeNames : undefined,
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
- server.setRequestHandler(ListToolsRequestSchema, async () => ({
171
- tools: [
172
- // memory_list_lobes is hidden — lobe info is surfaced in memory_context() hints
173
- // and memory_stats. The handler still works if called directly.
174
- {
175
- name: 'memory_store',
176
- 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")',
177
- inputSchema: {
178
- type: 'object',
179
- properties: {
180
- lobe: lobeProperty,
181
- topic: {
182
- type: 'string',
183
- description: 'user | preferences | architecture | conventions | gotchas | recent-work | modules/<name>',
184
- enum: ['user', 'preferences', 'architecture', 'conventions', 'gotchas', 'recent-work'],
185
- },
186
- title: {
187
- type: 'string',
188
- description: 'Short title for this entry',
189
- },
190
- content: {
191
- type: 'string',
192
- description: 'The knowledge to store',
193
- },
194
- sources: {
195
- type: 'array',
196
- items: { type: 'string' },
197
- description: 'File paths that informed this (provenance, for freshness tracking)',
198
- default: [],
199
- },
200
- references: {
201
- type: 'array',
202
- items: { type: 'string' },
203
- description: 'Files, classes, or symbols this knowledge is about (semantic pointers). Example: ["features/messaging/impl/MessagingReducer.kt"]',
204
- default: [],
205
- },
206
- trust: {
207
- type: 'string',
208
- enum: ['user', 'agent-confirmed', 'agent-inferred'],
209
- description: 'user (from human) > agent-confirmed > agent-inferred',
210
- default: 'agent-inferred',
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
- name: 'memory_query',
218
- 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.',
219
- inputSchema: {
220
- type: 'object',
221
- properties: {
222
- lobe: lobeProperty,
223
- scope: {
224
- type: 'string',
225
- description: 'Optional. Defaults to "*" (all topics). Options: * | user | preferences | architecture | conventions | gotchas | recent-work | modules/<name>',
226
- },
227
- detail: {
228
- type: 'string',
229
- enum: ['brief', 'standard', 'full'],
230
- description: 'brief = titles only, standard = summaries, full = complete content + metadata',
231
- default: 'brief',
232
- },
233
- filter: {
234
- type: 'string',
235
- description: 'Search terms. "A B" = AND, "A|B" = OR, "-A" = NOT. Example: "reducer sealed -deprecated"',
236
- },
237
- branch: {
238
- type: 'string',
239
- description: 'Branch for recent-work. Omit = current branch, "*" = all branches.',
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
- name: 'memory_correct',
247
- description: 'Fix or delete an entry. Example: memory_correct(id: "arch-3f7a", action: "replace", correction: "updated content")',
248
- inputSchema: {
249
- type: 'object',
250
- properties: {
251
- lobe: lobeProperty,
252
- id: {
253
- type: 'string',
254
- description: 'Entry ID (e.g. arch-3f7a, pref-5c9b)',
255
- },
256
- correction: {
257
- type: 'string',
258
- description: 'New text (for append/replace). Not needed for delete.',
259
- },
260
- action: {
261
- type: 'string',
262
- enum: ['append', 'replace', 'delete'],
263
- description: 'append | replace | delete',
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
- name: 'memory_context',
271
- 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")',
272
- inputSchema: {
273
- type: 'object',
274
- properties: {
275
- lobe: lobeProperty,
276
- context: {
277
- type: 'string',
278
- description: 'Optional. What you are about to do, in natural language. Omit for session-start briefing (user + preferences + stale entries).',
279
- },
280
- maxResults: {
281
- type: 'number',
282
- description: 'Max results (default: 10)',
283
- default: 10,
284
- },
285
- minMatch: {
286
- type: 'number',
287
- description: 'Min keyword match ratio 0-1 (default: 0.2). Higher = stricter.',
288
- default: 0.2,
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
- // memory_stats is hidden agents rarely need it proactively. Mentioned in
295
- // hints when storage is running low. The handler still works if called directly.
296
- {
297
- name: 'memory_bootstrap',
298
- description: 'First-time setup: scan repo structure, README, and build system to seed initial knowledge. Run once per new codebase.',
299
- inputSchema: {
300
- type: 'object',
301
- properties: {
302
- lobe: lobeProperty,
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
- // memory_diagnose is intentionally hidden from the tool list it clutters
308
- // agent tool discovery and should only be called when directed by error messages
309
- // or crash reports. The handler still works if called directly.
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. For targeted results use lobe: "${allQueryLobeNames[0]}" (available: ${allQueryLobeNames.join(', ')}).`);
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, remind the agent to specify one for targeted results
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
- ctxHints.push(`Searched all lobes. For faster, targeted results use lobe: "${allCtxLobeNames[0]}" (available: ${allCtxLobeNames.join(', ')}).`);
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: lobe, topic (architecture|conventions|gotchas|recent-work), title, content';
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) ---
@@ -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>;
@@ -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": "0.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",
@@ -17,7 +17,7 @@
17
17
  "typecheck": "tsc --noEmit",
18
18
  "dev": "tsx src/index.ts",
19
19
  "start": "node dist/index.js",
20
- "test": "node --import tsx --test src/__tests__/**/*.test.ts",
20
+ "test": "sh -c 'node --import tsx --test src/__tests__/*.test.ts'",
21
21
  "prepublishOnly": "npm run build",
22
22
  "semantic-release": "semantic-release"
23
23
  },
@@ -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": "^24.2.9",
50
+ "semantic-release": "^25.0.3",
51
51
  "tsx": "^4.0.0",
52
52
  "typescript": "^5.0.0"
53
53
  },
@@ -1 +0,0 @@
1
- export {};