@exaudeus/memory-mcp 1.2.0 → 1.4.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.
@@ -0,0 +1,34 @@
1
+ /** Outcome of resolving which lobes to search when the agent didn't specify one. */
2
+ export type LobeResolution = {
3
+ readonly kind: 'resolved';
4
+ readonly lobes: readonly string[];
5
+ readonly label: string;
6
+ } | {
7
+ readonly kind: 'global-only';
8
+ readonly hint: string;
9
+ };
10
+ /** A root URI from the MCP client (e.g. "file:///Users/me/projects/zillow"). */
11
+ export interface ClientRoot {
12
+ readonly uri: string;
13
+ }
14
+ /** Minimal lobe config needed for matching — just the repo root path. */
15
+ export interface LobeRootConfig {
16
+ readonly name: string;
17
+ readonly repoRoot: string;
18
+ }
19
+ /** Match MCP client workspace root URIs against known lobe repo roots.
20
+ * Returns matched lobe names, or empty array if none match.
21
+ *
22
+ * Matching rules:
23
+ * - file:// URIs are stripped to filesystem paths
24
+ * - Both paths are normalized via path.resolve
25
+ * - A match occurs when either path is equal to or nested inside the other,
26
+ * checked at path-separator boundaries (no partial-name false positives) */
27
+ export declare function matchRootsToLobeNames(clientRoots: readonly ClientRoot[], lobeConfigs: readonly LobeRootConfig[]): readonly string[];
28
+ /** Build a LobeResolution from the available lobe names and matched lobes.
29
+ * Encodes the degradation ladder as a pure function.
30
+ *
31
+ * When isFirstMemoryToolCall is true (default), alwaysIncludeLobes are appended
32
+ * to the resolved set (deduped). When false, they are excluded — the agent has
33
+ * already loaded global knowledge in this conversation. */
34
+ export declare function buildLobeResolution(allLobeNames: readonly string[], matchedLobes: readonly string[], alwaysIncludeLobes?: readonly string[], isFirstMemoryToolCall?: boolean): LobeResolution;
@@ -0,0 +1,89 @@
1
+ // Pure lobe resolution logic — extracted for testability.
2
+ //
3
+ // When the agent doesn't specify a lobe, we determine which lobe(s) to search
4
+ // via a degradation ladder:
5
+ // 1. Single lobe configured → use it (unambiguous)
6
+ // 2. Multiple lobes → match client workspace roots against lobe repo roots
7
+ // 3. Fallback → global-only with a hint to specify the lobe
8
+ //
9
+ // This prevents cross-lobe leakage (e.g. game design lore surfacing in an Android MR review).
10
+ import path from 'path';
11
+ /** Check if `child` is equal to or nested under `parent` with path-boundary awareness.
12
+ * Prevents false matches like "/projects/zillow-tools" matching "/projects/zillow". */
13
+ function isPathPrefixOf(parent, child) {
14
+ if (child === parent)
15
+ return true;
16
+ // Ensure the prefix ends at a path separator boundary
17
+ const withSep = parent.endsWith(path.sep) ? parent : parent + path.sep;
18
+ return child.startsWith(withSep);
19
+ }
20
+ /** Match MCP client workspace root URIs against known lobe repo roots.
21
+ * Returns matched lobe names, or empty array if none match.
22
+ *
23
+ * Matching rules:
24
+ * - file:// URIs are stripped to filesystem paths
25
+ * - Both paths are normalized via path.resolve
26
+ * - A match occurs when either path is equal to or nested inside the other,
27
+ * checked at path-separator boundaries (no partial-name false positives) */
28
+ export function matchRootsToLobeNames(clientRoots, lobeConfigs) {
29
+ if (clientRoots.length === 0 || lobeConfigs.length === 0)
30
+ return [];
31
+ const matchedLobes = new Set();
32
+ for (const root of clientRoots) {
33
+ // MCP roots use file:// URIs — strip the scheme to get the filesystem path
34
+ const rootPath = root.uri.startsWith('file://') ? root.uri.slice(7) : root.uri;
35
+ const normalizedRoot = path.resolve(rootPath);
36
+ for (const lobe of lobeConfigs) {
37
+ const normalizedLobe = path.resolve(lobe.repoRoot);
38
+ // Match if one path is equal to or nested inside the other
39
+ if (isPathPrefixOf(normalizedLobe, normalizedRoot) || isPathPrefixOf(normalizedRoot, normalizedLobe)) {
40
+ matchedLobes.add(lobe.name);
41
+ }
42
+ }
43
+ }
44
+ return Array.from(matchedLobes);
45
+ }
46
+ /** Build a LobeResolution from the available lobe names and matched lobes.
47
+ * Encodes the degradation ladder as a pure function.
48
+ *
49
+ * When isFirstMemoryToolCall is true (default), alwaysIncludeLobes are appended
50
+ * to the resolved set (deduped). When false, they are excluded — the agent has
51
+ * already loaded global knowledge in this conversation. */
52
+ export function buildLobeResolution(allLobeNames, matchedLobes, alwaysIncludeLobes = [], isFirstMemoryToolCall = true) {
53
+ // Single lobe — always resolved, regardless of root matching
54
+ if (allLobeNames.length === 1 && alwaysIncludeLobes.length === 0) {
55
+ return { kind: 'resolved', lobes: allLobeNames, label: allLobeNames[0] };
56
+ }
57
+ // Build the base resolved set
58
+ let baseLobes;
59
+ if (allLobeNames.length === 1) {
60
+ baseLobes = allLobeNames;
61
+ }
62
+ else if (matchedLobes.length > 0) {
63
+ baseLobes = matchedLobes;
64
+ }
65
+ else {
66
+ baseLobes = [];
67
+ }
68
+ // Append alwaysInclude lobes when isFirstMemoryToolCall is true (deduped)
69
+ const resolvedSet = new Set(baseLobes);
70
+ if (isFirstMemoryToolCall) {
71
+ for (const lobe of alwaysIncludeLobes) {
72
+ resolvedSet.add(lobe);
73
+ }
74
+ }
75
+ if (resolvedSet.size > 0) {
76
+ const lobes = Array.from(resolvedSet);
77
+ return {
78
+ kind: 'resolved',
79
+ lobes,
80
+ label: lobes.length === 1 ? lobes[0] : lobes.join('+'),
81
+ };
82
+ }
83
+ // Fallback — no lobes could be determined
84
+ return {
85
+ kind: 'global-only',
86
+ hint: `Multiple lobes available (${allLobeNames.join(', ')}) but none could be inferred from client workspace roots. ` +
87
+ `Specify lobe parameter for lobe-specific results.`,
88
+ };
89
+ }
package/dist/store.d.ts CHANGED
@@ -18,6 +18,9 @@ export declare class MarkdownMemoryStore {
18
18
  query(scope: string, detail?: DetailLevel, filter?: string, branchFilter?: string): Promise<QueryResult>;
19
19
  /** Generate a session-start briefing */
20
20
  briefing(maxTokens?: number): Promise<BriefingResult>;
21
+ /** Check if an entry exists by ID — read-only, no side effects beyond disk reload.
22
+ * Use this to probe for entry ownership before calling correct(). */
23
+ hasEntry(id: string): Promise<boolean>;
21
24
  /** Correct an existing entry */
22
25
  correct(id: string, correction: string, action: 'append' | 'replace' | 'delete'): Promise<CorrectResult>;
23
26
  /** Get memory health statistics */
package/dist/store.js CHANGED
@@ -10,7 +10,7 @@ import { DEFAULT_CONFIDENCE, realClock, parseTopicScope, parseTrustLevel, parseT
10
10
  import { DEDUP_SIMILARITY_THRESHOLD, CONFLICT_SIMILARITY_THRESHOLD_SAME_TOPIC, CONFLICT_SIMILARITY_THRESHOLD_CROSS_TOPIC, CONFLICT_MIN_CONTENT_CHARS, OPPOSITION_PAIRS, PREFERENCE_SURFACE_THRESHOLD, REFERENCE_BOOST_MULTIPLIER, TOPIC_BOOST, MODULE_TOPIC_BOOST, USER_ALWAYS_INCLUDE_SCORE_FRACTION, DEFAULT_STALE_DAYS_STANDARD, DEFAULT_STALE_DAYS_PREFERENCES, DEFAULT_MAX_STALE_IN_BRIEFING, DEFAULT_MAX_DEDUP_SUGGESTIONS, DEFAULT_MAX_CONFLICT_PAIRS, DEFAULT_MAX_PREFERENCE_SUGGESTIONS, TAG_MATCH_BOOST, } from './thresholds.js';
11
11
  import { realGitService } from './git-service.js';
12
12
  import { extractKeywords, stem, similarity, matchesFilter, computeRelevanceScore, } from './text-analyzer.js';
13
- import { detectEphemeralSignals, formatEphemeralWarning } from './ephemeral.js';
13
+ import { detectEphemeralSignals, formatEphemeralWarning, getEphemeralSeverity } from './ephemeral.js';
14
14
  // Used only by bootstrap() for git log — not part of the GitService boundary
15
15
  // because bootstrap is a one-shot utility, not a recurring operation
16
16
  const execFileAsync = promisify(execFile);
@@ -89,9 +89,12 @@ export class MarkdownMemoryStore {
89
89
  const ephemeralSignals = topic !== 'recent-work'
90
90
  ? detectEphemeralSignals(title, content, topic)
91
91
  : [];
92
- const ephemeralWarning = formatEphemeralWarning(ephemeralSignals);
92
+ // getEphemeralSeverity is the single source of threshold logic shared with formatEphemeralWarning.
93
+ const ephemeralSeverity = getEphemeralSeverity(ephemeralSignals);
94
+ const ephemeralWarning = formatEphemeralWarning(ephemeralSignals, id);
93
95
  return {
94
96
  stored: true, id, topic, file, confidence, warning, ephemeralWarning,
97
+ ephemeralSeverity: ephemeralSeverity ?? undefined,
95
98
  relatedEntries: relatedEntries.length > 0 ? relatedEntries : undefined,
96
99
  relevantPreferences: relevantPreferences && relevantPreferences.length > 0 ? relevantPreferences : undefined,
97
100
  };
@@ -253,6 +256,12 @@ export class MarkdownMemoryStore {
253
256
  suggestion,
254
257
  };
255
258
  }
259
+ /** Check if an entry exists by ID — read-only, no side effects beyond disk reload.
260
+ * Use this to probe for entry ownership before calling correct(). */
261
+ async hasEntry(id) {
262
+ await this.reloadFromDisk();
263
+ return this.entries.has(id);
264
+ }
256
265
  /** Correct an existing entry */
257
266
  async correct(id, correction, action) {
258
267
  // Reload to ensure we have the latest
@@ -16,14 +16,6 @@ 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;
27
19
  /** Per-topic scoring boost factors for contextSearch().
28
20
  * Higher = more likely to surface for any given context. */
29
21
  export declare const TOPIC_BOOST: Record<string, number>;
@@ -51,3 +43,6 @@ export declare const TAG_MATCH_BOOST = 1.5;
51
43
  export declare const VOCABULARY_ECHO_LIMIT = 8;
52
44
  /** Maximum tags shown in query/context footer. */
53
45
  export declare const MAX_FOOTER_TAGS = 12;
46
+ /** Visual separator for warning blocks in tool responses.
47
+ * Width chosen to stand out as a block boundary in any terminal or chat rendering. */
48
+ export declare const WARN_SEPARATOR: string;
@@ -42,14 +42,6 @@ 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;
53
45
  /** Per-topic scoring boost factors for contextSearch().
54
46
  * Higher = more likely to surface for any given context. */
55
47
  export const TOPIC_BOOST = {
@@ -87,3 +79,7 @@ export const TAG_MATCH_BOOST = 1.5;
87
79
  export const VOCABULARY_ECHO_LIMIT = 8;
88
80
  /** Maximum tags shown in query/context footer. */
89
81
  export const MAX_FOOTER_TAGS = 12;
82
+ // ─── Display formatting constants ───────────────────────────────────────────
83
+ /** Visual separator for warning blocks in tool responses.
84
+ * Width chosen to stand out as a block boundary in any terminal or chat rendering. */
85
+ export const WARN_SEPARATOR = '='.repeat(52);
package/dist/types.d.ts CHANGED
@@ -1,5 +1,8 @@
1
1
  /** Trust levels for knowledge sources, ordered by reliability */
2
2
  export type TrustLevel = 'user' | 'agent-confirmed' | 'agent-inferred';
3
+ /** Ephemeral detection severity — three distinct levels so consumers can branch exhaustively.
4
+ * Separated from EphemeralSignal.confidence to represent the aggregate outcome of all signals. */
5
+ export type EphemeralSeverity = 'high' | 'medium' | 'low';
3
6
  /** Parse a raw string into a TrustLevel, returning null for invalid input */
4
7
  export declare function parseTrustLevel(raw: string): TrustLevel | null;
5
8
  /** Predefined topic scopes for organizing knowledge */
@@ -96,6 +99,8 @@ export type StoreResult = {
96
99
  readonly warning?: string;
97
100
  /** Soft warning when content looks ephemeral — informational, never blocking */
98
101
  readonly ephemeralWarning?: string;
102
+ /** Aggregate severity of all ephemeral signals that fired — absent when none fired */
103
+ readonly ephemeralSeverity?: EphemeralSeverity;
99
104
  readonly relatedEntries?: readonly RelatedEntry[];
100
105
  readonly relevantPreferences?: readonly RelatedEntry[];
101
106
  } | {
@@ -190,6 +195,7 @@ export interface MemoryConfig {
190
195
  readonly repoRoot: string;
191
196
  readonly memoryPath: string;
192
197
  readonly storageBudgetBytes: number;
198
+ readonly alwaysInclude: boolean;
193
199
  readonly behavior?: BehaviorConfig;
194
200
  readonly clock?: Clock;
195
201
  readonly git?: GitService;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@exaudeus/memory-mcp",
3
- "version": "1.2.0",
3
+ "version": "1.4.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",