@exaudeus/memory-mcp 1.1.0 → 1.2.0

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/ephemeral.js CHANGED
@@ -228,11 +228,66 @@ const SIGNALS = [
228
228
  /\b(doesn'?t|don'?t) (handle|support|implement).*\byet\b/,
229
229
  /\bis (underway|not finished|not complete)\b/,
230
230
  /\bblocked on\b/,
231
+ // Work-remaining section markers (colon required to avoid matching prose)
232
+ /\bremaining\s*:/, /\bopen items?\s*:/, /\bstill to do\s*:/,
233
+ /\bpending (tasks?|items?|work)\s*:/,
231
234
  ];
232
235
  const m = firstMatch(content, patterns);
233
236
  return m ? `contains "${m[0]}" — task tracking doesn't belong in long-term memory` : undefined;
234
237
  },
235
238
  },
239
+ // ── Completed task / work summary ──────────────────────────────────────
240
+ // Catches session work summaries stored as permanent knowledge.
241
+ // "Documentation updates complete", "Completed the migration", "All X validated".
242
+ {
243
+ id: 'completed-task',
244
+ label: 'Completed task announcement',
245
+ confidence: 'high',
246
+ test: (title, content) => {
247
+ const titlePatterns = [
248
+ // "Documentation updates complete", "migration done", "task finished"
249
+ /\b(update|migration|refactor|implementation|task|feature|docs?|documentation|sync|work|changes?|setup)\s+(complete[d]?|done|finished?)\b/,
250
+ // "Complete: X" or "Done - X" as a status prefix in the title
251
+ /^(complete[d]?|done|finished?)\s*[-–—:]/,
252
+ ];
253
+ const contentPatterns = [
254
+ // Content that opens with a completion verb — strongest signal
255
+ /^completed?\s+\w/,
256
+ // "Successfully completed/deployed/merged/migrated..."
257
+ /\bsuccessfully\s+(completed?|deployed?|merged?|migrated?|updated?|implemented?|refactored?)\b/,
258
+ // "All cross-references validated", "All tests done"
259
+ // [\w-] (no \s) restricts to single-word or hyphenated nouns, preventing
260
+ // "Every code fix is validated" (a durable policy) from matching
261
+ /\b(all|every)\s+\S[\w-]{0,25}\s+(validated?|complete[d]?|verified?|done)\b/,
262
+ // "has been completed/finished/merged/deployed"
263
+ /\b(has|have)\s+been\s+(completed?|finished?|merged?|deployed?)\b/,
264
+ ];
265
+ const tm = firstMatch(title, titlePatterns);
266
+ if (tm)
267
+ return `title contains "${tm[0]}" — task completion announcements aren't durable knowledge`;
268
+ const cm = firstMatch(content, contentPatterns);
269
+ return cm ? `contains "${cm[0]}" — task completion announcements aren't durable knowledge` : undefined;
270
+ },
271
+ },
272
+ // ── Diff stats / work metrics ──────────────────────────────────────────
273
+ // Quantitative work summaries ("14 docs modified", "508 additions, 173 deletions")
274
+ // never appear in durable knowledge — they're always session activity reports.
275
+ {
276
+ id: 'diff-stats',
277
+ label: 'Diff stats or work metrics',
278
+ confidence: 'high',
279
+ test: (_title, _content, raw) => {
280
+ const content = raw.content.toLowerCase();
281
+ const patterns = [
282
+ // Git-style: "(508 additions, 173 deletions)" or "508 additions, 173 deletions"
283
+ /\b\d+\s+additions?,\s*\d+\s+deletions?\b/,
284
+ // Work quantity: "14 docs modified", "5 files changed"
285
+ /\b\d+\s+(docs?|files?|tests?|endpoints?|functions?|modules?|classes?|pages?)\s+(modified|changed|updated|added|created|deleted)\b/,
286
+ ];
287
+ const m = firstMatch(content, patterns);
288
+ return m ? `"${m[0]}" — quantitative work metrics are session activity, not lasting knowledge` : undefined;
289
+ },
290
+ },
236
291
  // ── Stack traces / debug logs ──────────────────────────────────────────
237
292
  {
238
293
  id: 'stack-trace',
@@ -386,6 +441,27 @@ const SIGNALS = [
386
441
  return m ? `"${m[0]}" — store the decision or fact, not the meeting reference` : undefined;
387
442
  },
388
443
  },
444
+ // ── Shipped / released / merged ────────────────────────────────────────
445
+ // Positive deployment/release language not already caught by temporal.
446
+ // ("just deployed" is caught by temporal; this catches statements without "just".)
447
+ {
448
+ id: 'shipped',
449
+ label: 'Deployed, released, or merged',
450
+ confidence: 'medium',
451
+ test: (_title, content) => {
452
+ const patterns = [
453
+ // "was/got/has been deployed to production/staging/main" — past-tense anchor prevents
454
+ // firing on policy conventions like "is deployed to production before release"
455
+ /\b(?:was|were|got|has been)\s+deployed?\s+(?:to|into)\s+(production|prod|staging|main|master)\b/,
456
+ // "was/got/has been merged into main/master/develop/trunk"
457
+ /\b(?:was|were|got|has been)\s+merged?\s+(?:to|into)\s+(main|master|develop|trunk)\b/,
458
+ // "released to production" or "released as v1.2" or "released as version 1.2"
459
+ /\breleased?\s+(?:to\s+(?:production|prod)|(?:as\s+)?(?:version\s+)?v?\d+\.\d+)\b/,
460
+ ];
461
+ const m = firstMatch(content, patterns);
462
+ return m ? `"${m[0]}" — deployment/release events are ephemeral; store the resulting behavior instead` : undefined;
463
+ },
464
+ },
389
465
  // ── Pending decision / under evaluation ─────────────────────────────────
390
466
  {
391
467
  id: 'pending-decision',
@@ -435,6 +511,30 @@ const SIGNALS = [
435
511
  return m ? `"${m[0]}" — metric changes are often transient observations` : undefined;
436
512
  },
437
513
  },
514
+ // ── Bundling conjunctions ──────────────────────────────────────────────
515
+ // Catches agents that put multiple unrelated facts in a single entry using
516
+ // explicit linking language. Only fires on sentence-boundary connectors
517
+ // (comma or start of new sentence) to avoid false positives on prose like
518
+ // "X and Y work together" or "also useful for Z".
519
+ {
520
+ id: 'bundling-conjunction',
521
+ label: 'Multiple facts bundled',
522
+ confidence: 'low',
523
+ test: (_title, content) => {
524
+ const patterns = [
525
+ /[,;]\s+also\b/i, // "X works this way, also Y does Z"
526
+ /[.!?]\s+additionally,/i, // ". Additionally, ..."
527
+ /[.!?]\s+furthermore,/i, // ". Furthermore, ..."
528
+ /\bunrelated:/i, // "Unrelated: ..."
529
+ /\bseparately:/i, // "Separately: ..."
530
+ /[.!?]\s+on a (separate|different|unrelated) note[,:]?/i,
531
+ ];
532
+ const m = firstMatch(content, patterns);
533
+ return m
534
+ ? `"${m[0]}" — consider splitting into separate entries (one insight per entry)`
535
+ : undefined;
536
+ },
537
+ },
438
538
  // ── Very short content ─────────────────────────────────────────────────
439
539
  {
440
540
  id: 'too-short',
@@ -502,15 +602,17 @@ export function formatEphemeralWarning(signals) {
502
602
  '',
503
603
  ];
504
604
  // Scale the guidance with confidence — high-confidence gets direct advice,
505
- // low-confidence gets a softer suggestion to let the agent decide
605
+ // low-confidence gets a softer suggestion to let the agent decide.
606
+ // Always include the positive redirect: store state, not events.
506
607
  if (highCount >= 2) {
507
608
  lines.push('This is almost certainly session-specific. Consider deleting after your session.');
609
+ lines.push('If there is a lasting insight here, rephrase it as a present-tense fact: what is now true about the codebase?');
508
610
  }
509
611
  else if (highCount === 1) {
510
- lines.push('If this is a lasting insight, keep it. If session-specific, consider deleting after your session.');
612
+ lines.push('If this is a lasting insight, rephrase it as a present-tense fact (what is now true) rather than an action report (what you did). If session-specific, consider deleting after your session.');
511
613
  }
512
614
  else {
513
- lines.push('This might still be valid long-term knowledge use your judgment.');
615
+ lines.push('If this is durable knowledge, rephrase as a present-tense fact: what is now true? If it describes what you did rather than what is, consider deleting after your session.');
514
616
  }
515
617
  return lines.join('\n');
516
618
  }
package/dist/index.js CHANGED
@@ -183,7 +183,10 @@ server.setRequestHandler(ListToolsRequestSchema, async () => {
183
183
  // and memory_stats. The handler still works if called directly.
184
184
  {
185
185
  name: 'memory_store',
186
- description: 'Store knowledge. "user" and "preferences" are global (no lobe needed). Use tags for exact-match categorization. Add a shared tag (e.g., "test-entry") for bulk operations. Example: memory_store(topic: "gotchas", title: "Build cache", content: "Must clean build after Tuist changes", tags: ["build", "ios"])',
186
+ // Example comes first agents form their call shape from the first concrete pattern they see.
187
+ // "entries" (not "content") signals a collection; fighting the "content = string" prior
188
+ // is an architectural fix rather than patching the description after the fact.
189
+ description: 'memory_store(topic: "gotchas", entries: [{title: "Build cache", fact: "Must clean build after Tuist changes"}, {title: "Tuist version", fact: "Project requires Tuist 4.x"}], tags: ["build"]). Stores enduring knowledge — (1) Codebase facts (architecture, conventions, gotchas, modules): what IS true now, not past actions. Wrong: "Completed migration to StateFlow." Right: "All ViewModels use StateFlow." (2) User knowledge (user, preferences): who the person is, how they work. "user" and "preferences" are global. One insight per object; use multiple objects instead of bundling.',
187
190
  inputSchema: {
188
191
  type: 'object',
189
192
  properties: {
@@ -196,13 +199,25 @@ server.setRequestHandler(ListToolsRequestSchema, async () => {
196
199
  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
200
  enum: ['user', 'preferences', 'architecture', 'conventions', 'gotchas', 'recent-work'],
198
201
  },
199
- title: {
200
- type: 'string',
201
- description: 'Short title for this entry',
202
- },
203
- content: {
204
- type: 'string',
205
- description: 'The knowledge to store',
202
+ entries: {
203
+ type: 'array',
204
+ // Type annotation first agents trained on code read type signatures before prose.
205
+ description: 'Array<{title: string, fact: string}> — not a string. One object per insight. title: short label (2-5 words). fact: codebase topics → present-tense state ("X uses Y", not "Completed X"); user/preferences → what the person expressed. Wrong: one object bundling two facts. Right: two objects.',
206
+ items: {
207
+ type: 'object',
208
+ properties: {
209
+ title: {
210
+ type: 'string',
211
+ description: 'Short label for this insight (2-5 words)',
212
+ },
213
+ fact: {
214
+ type: 'string',
215
+ description: 'The insight itself — one focused fact or observation',
216
+ },
217
+ },
218
+ required: ['title', 'fact'],
219
+ },
220
+ minItems: 1,
206
221
  },
207
222
  sources: {
208
223
  type: 'array',
@@ -229,7 +244,7 @@ server.setRequestHandler(ListToolsRequestSchema, async () => {
229
244
  default: [],
230
245
  },
231
246
  },
232
- required: ['topic', 'title', 'content'],
247
+ required: ['topic', 'entries'],
233
248
  },
234
249
  },
235
250
  {
@@ -393,11 +408,16 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
393
408
  };
394
409
  }
395
410
  case 'memory_store': {
396
- const { lobe: rawLobe, topic: rawTopic, title, content, sources, references, trust: rawTrust, tags: rawTags } = z.object({
411
+ const { lobe: rawLobe, topic: rawTopic, entries: rawEntries, sources, references, trust: rawTrust, tags: rawTags } = z.object({
397
412
  lobe: z.string().optional(),
398
413
  topic: z.string(),
399
- title: z.string().min(1),
400
- content: z.string().min(1),
414
+ // Accept a bare {title, fact} object in addition to the canonical array form.
415
+ // Only objects are auto-wrapped — strings and other primitives still fail with
416
+ // a type error, preserving the "validate at boundaries" invariant.
417
+ entries: z.preprocess((val) => (val !== null && !Array.isArray(val) && typeof val === 'object' ? [val] : val), z.array(z.object({
418
+ title: z.string().min(1),
419
+ fact: z.string().min(1),
420
+ })).min(1)),
401
421
  sources: z.array(z.string()).default([]),
402
422
  references: z.array(z.string()).default([]),
403
423
  trust: z.enum(['user', 'agent-confirmed', 'agent-inferred']).default('agent-inferred'),
@@ -424,54 +444,71 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
424
444
  const ctx = resolveToolContext(effectiveLobe, { isGlobal });
425
445
  if (!ctx.ok)
426
446
  return contextError(ctx);
427
- const result = await ctx.store.store(topic, title, content, sources,
428
- // User/preferences default to 'user' trust unless explicitly set otherwise
429
- isGlobal && trust === 'agent-inferred' ? 'user' : trust, references, rawTags);
430
- if (!result.stored) {
431
- return {
432
- content: [{ type: 'text', text: `[${ctx.label}] Failed to store: ${result.warning}` }],
433
- isError: true,
434
- };
447
+ const effectiveTrust = isGlobal && trust === 'agent-inferred' ? 'user' : trust;
448
+ const storedResults = [];
449
+ for (const { title, fact } of rawEntries) {
450
+ const result = await ctx.store.store(topic, title, fact, sources, effectiveTrust, references, rawTags);
451
+ if (!result.stored) {
452
+ return {
453
+ content: [{ type: 'text', text: `[${ctx.label}] Failed to store "${title}": ${result.warning}` }],
454
+ isError: true,
455
+ };
456
+ }
457
+ storedResults.push({ title, result });
458
+ }
459
+ // Build response header
460
+ const lines = [];
461
+ if (storedResults.length === 1) {
462
+ const { result } = storedResults[0];
463
+ lines.push(`[${ctx.label}] Stored entry ${result.id} in ${result.topic} (confidence: ${result.confidence})`);
464
+ if (result.warning)
465
+ lines.push(`Note: ${result.warning}`);
466
+ }
467
+ else {
468
+ const { result: first } = storedResults[0];
469
+ lines.push(`[${ctx.label}] Stored ${storedResults.length} entries in ${first.topic} (confidence: ${first.confidence}):`);
470
+ for (const { title, result } of storedResults) {
471
+ lines.push(` - ${result.id}: "${title}"`);
472
+ }
435
473
  }
436
- const lines = [
437
- `[${ctx.label}] Stored entry ${result.id} in ${result.topic} (confidence: ${result.confidence})`,
438
- ];
439
- if (result.warning)
440
- lines.push(`Note: ${result.warning}`);
441
474
  // Limit to at most 2 hint sections per response to prevent hint fatigue.
442
475
  // Priority: dedup > ephemeral > preferences (dedup is actionable and high-signal,
443
476
  // ephemeral warnings affect entry quality, preferences are informational).
477
+ // For multi-entry batches, hints reference the first triggering entry.
444
478
  let hintCount = 0;
445
- // Dedup: surface related entries in the same topic
446
- if (result.relatedEntries && result.relatedEntries.length > 0 && hintCount < 2) {
447
- hintCount++;
448
- lines.push('');
449
- lines.push('⚠ Similar entries found in the same topic:');
450
- for (const r of result.relatedEntries) {
451
- lines.push(` - ${r.id}: "${r.title}" (confidence: ${r.confidence})`);
452
- lines.push(` Content: ${r.content.length > 120 ? r.content.substring(0, 120) + '...' : r.content}`);
479
+ for (const { title, result } of storedResults) {
480
+ const entryPrefix = storedResults.length > 1 ? `"${title}": ` : '';
481
+ // Dedup: surface related entries in the same topic
482
+ if (result.relatedEntries && result.relatedEntries.length > 0 && hintCount < 2) {
483
+ hintCount++;
484
+ lines.push('');
485
+ lines.push(`⚠ ${entryPrefix}Similar entries found in the same topic:`);
486
+ for (const r of result.relatedEntries) {
487
+ lines.push(` - ${r.id}: "${r.title}" (confidence: ${r.confidence})`);
488
+ lines.push(` Content: ${r.content.length > 120 ? r.content.substring(0, 120) + '...' : r.content}`);
489
+ }
490
+ lines.push('');
491
+ lines.push('To consolidate: memory_correct(id: "<old-id>", action: "replace", correction: "<merged content>") then memory_correct(id: "<new-id>", action: "delete")');
453
492
  }
454
- lines.push('');
455
- lines.push('To consolidate: memory_correct(id: "<old-id>", action: "replace", correction: "<merged content>") then memory_correct(id: "<new-id>", action: "delete")');
456
- }
457
- // Ephemeral content warning — soft nudge, never blocking
458
- if (result.ephemeralWarning && hintCount < 2) {
459
- hintCount++;
460
- lines.push('');
461
- lines.push(`⏳ ${result.ephemeralWarning}`);
462
- }
463
- // Preference surfacing: show relevant preferences for non-preference entries
464
- if (result.relevantPreferences && result.relevantPreferences.length > 0 && hintCount < 2) {
465
- hintCount++;
466
- lines.push('');
467
- lines.push('📌 Relevant preferences:');
468
- for (const p of result.relevantPreferences) {
469
- lines.push(` - [pref] ${p.title}: ${p.content.length > 120 ? p.content.substring(0, 120) + '...' : p.content}`);
493
+ // Ephemeral content warning — soft nudge, never blocking
494
+ if (result.ephemeralWarning && hintCount < 2) {
495
+ hintCount++;
496
+ lines.push('');
497
+ lines.push(`⏳ ${entryPrefix}${result.ephemeralWarning}`);
498
+ }
499
+ // Preference surfacing: show relevant preferences for non-preference entries
500
+ if (result.relevantPreferences && result.relevantPreferences.length > 0 && hintCount < 2) {
501
+ hintCount++;
502
+ lines.push('');
503
+ lines.push(`📌 ${entryPrefix}Relevant preferences:`);
504
+ for (const p of result.relevantPreferences) {
505
+ lines.push(` - [pref] ${p.title}: ${p.content.length > 120 ? p.content.substring(0, 120) + '...' : p.content}`);
506
+ }
507
+ lines.push('');
508
+ lines.push('Review the stored entry against these preferences for potential conflicts.');
470
509
  }
471
- lines.push('');
472
- lines.push('Review the stored entry against these preferences for potential conflicts.');
473
510
  }
474
- // Vocabulary echo: show existing tags to drive convergence
511
+ // Vocabulary echo: show existing tags to drive convergence (once per response)
475
512
  if (hintCount < 2) {
476
513
  const tagFreq = ctx.store.getTagFrequency();
477
514
  if (tagFreq.size > 0) {
@@ -1032,8 +1069,8 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
1032
1069
  const lobeNames = configManager.getLobeNames();
1033
1070
  hint = `\n\nHint: lobe is required. Use memory_list_lobes to see available lobes. Available: ${lobeNames.join(', ')}`;
1034
1071
  }
1035
- else if (message.includes('"topic"') || message.includes('"title"') || message.includes('"content"')) {
1036
- 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).';
1072
+ else if (message.includes('"topic"') || message.includes('"entries"')) {
1073
+ hint = '\n\nHint: memory_store requires: topic (architecture|conventions|gotchas|recent-work|modules/<name>) and entries (Array<{title, fact}>). Example: entries: [{title: "Build cache", fact: "Must clean build after Tuist changes"}]. Use modules/<name> for custom namespaces.';
1037
1074
  }
1038
1075
  else if (message.includes('"scope"')) {
1039
1076
  hint = '\n\nHint: memory_query requires: lobe, scope (architecture|conventions|gotchas|recent-work|modules/<name>|* for all)';
package/dist/normalize.js CHANGED
@@ -6,11 +6,6 @@
6
6
  /** Canonical param name aliases — maps guessed names to their correct form */
7
7
  const PARAM_ALIASES = {
8
8
  // memory_store aliases
9
- key: 'title',
10
- name: 'title',
11
- value: 'content',
12
- body: 'content',
13
- text: 'content',
14
9
  refs: 'references',
15
10
  // memory_query aliases
16
11
  query: 'filter',
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@exaudeus/memory-mcp",
3
- "version": "1.1.0",
3
+ "version": "1.2.0",
4
4
  "description": "Codebase memory MCP server - persistent, evolving knowledge for AI coding agents",
5
5
  "type": "module",
6
6
  "main": "dist/index.js",