@exaudeus/memory-mcp 0.1.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/LICENSE +21 -0
- package/README.md +264 -0
- package/dist/__tests__/clock-and-validators.test.d.ts +1 -0
- package/dist/__tests__/clock-and-validators.test.js +237 -0
- package/dist/__tests__/config-manager.test.d.ts +1 -0
- package/dist/__tests__/config-manager.test.js +142 -0
- package/dist/__tests__/config.test.d.ts +1 -0
- package/dist/__tests__/config.test.js +236 -0
- package/dist/__tests__/crash-journal.test.d.ts +1 -0
- package/dist/__tests__/crash-journal.test.js +203 -0
- package/dist/__tests__/e2e.test.d.ts +1 -0
- package/dist/__tests__/e2e.test.js +788 -0
- package/dist/__tests__/ephemeral-benchmark.test.d.ts +1 -0
- package/dist/__tests__/ephemeral-benchmark.test.js +651 -0
- package/dist/__tests__/ephemeral.test.d.ts +1 -0
- package/dist/__tests__/ephemeral.test.js +435 -0
- package/dist/__tests__/git-service.test.d.ts +1 -0
- package/dist/__tests__/git-service.test.js +43 -0
- package/dist/__tests__/normalize.test.d.ts +1 -0
- package/dist/__tests__/normalize.test.js +161 -0
- package/dist/__tests__/store.test.d.ts +1 -0
- package/dist/__tests__/store.test.js +1153 -0
- package/dist/config-manager.d.ts +49 -0
- package/dist/config-manager.js +126 -0
- package/dist/config.d.ts +32 -0
- package/dist/config.js +162 -0
- package/dist/crash-journal.d.ts +38 -0
- package/dist/crash-journal.js +198 -0
- package/dist/ephemeral-weights.json +1847 -0
- package/dist/ephemeral.d.ts +20 -0
- package/dist/ephemeral.js +516 -0
- package/dist/formatters.d.ts +10 -0
- package/dist/formatters.js +92 -0
- package/dist/git-service.d.ts +5 -0
- package/dist/git-service.js +39 -0
- package/dist/index.d.ts +2 -0
- package/dist/index.js +1197 -0
- package/dist/normalize.d.ts +2 -0
- package/dist/normalize.js +69 -0
- package/dist/store.d.ts +84 -0
- package/dist/store.js +813 -0
- package/dist/text-analyzer.d.ts +32 -0
- package/dist/text-analyzer.js +190 -0
- package/dist/thresholds.d.ts +39 -0
- package/dist/thresholds.js +75 -0
- package/dist/types.d.ts +186 -0
- package/dist/types.js +33 -0
- package/package.json +57 -0
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
/** Parsed filter group: a set of required terms and excluded terms */
|
|
2
|
+
export interface FilterGroup {
|
|
3
|
+
readonly must: Set<string>;
|
|
4
|
+
readonly mustNot: Set<string>;
|
|
5
|
+
}
|
|
6
|
+
/** Naive stem: strip common English suffixes to improve keyword matching.
|
|
7
|
+
* "reducers" -> "reducer", "sealed" stays, "implementations" -> "implement" */
|
|
8
|
+
export declare function stem(word: string): string;
|
|
9
|
+
/** Extract significant keywords from text, with naive stemming.
|
|
10
|
+
* Hyphenated words produce both the compound and its parts:
|
|
11
|
+
* "memory-mcp" -> ["memory-mcp", "memory", "mcp"] */
|
|
12
|
+
export declare function extractKeywords(text: string): Set<string>;
|
|
13
|
+
/** Jaccard similarity: |intersection| / |union| */
|
|
14
|
+
export declare function jaccardSimilarity(a: Set<string>, b: Set<string>): number;
|
|
15
|
+
/** Containment similarity: |intersection| / min(|a|, |b|)
|
|
16
|
+
* Catches when one entry is a subset of a larger one */
|
|
17
|
+
export declare function containmentSimilarity(a: Set<string>, b: Set<string>): number;
|
|
18
|
+
/** Combined similarity: max(jaccard, containment) with title boost.
|
|
19
|
+
* Title keywords get double weight by being included twice. */
|
|
20
|
+
export declare function similarity(titleA: string, contentA: string, titleB: string, contentB: string): number;
|
|
21
|
+
/** Parse a filter string into OR groups of AND/NOT terms.
|
|
22
|
+
* "reducer sealed|MVI -deprecated" -> [
|
|
23
|
+
* { must: ["reducer", "seal"], mustNot: [] },
|
|
24
|
+
* { must: ["mvi"], mustNot: ["deprecat"] }
|
|
25
|
+
* ] */
|
|
26
|
+
export declare function parseFilter(filter: string): FilterGroup[];
|
|
27
|
+
/** Check if a set of keywords matches a filter string using stemmed AND/OR/NOT logic.
|
|
28
|
+
* Entry matches if ANY OR-group is satisfied (all must-terms present, no mustNot-terms present). */
|
|
29
|
+
export declare function matchesFilter(allKeywords: Set<string>, filter: string): boolean;
|
|
30
|
+
/** Compute relevance score for an entry against a filter.
|
|
31
|
+
* Title matches get 2x weight over content-only matches. */
|
|
32
|
+
export declare function computeRelevanceScore(titleKeywords: Set<string>, contentKeywords: Set<string>, confidence: number, filter: string): number;
|
|
@@ -0,0 +1,190 @@
|
|
|
1
|
+
// Pure text analysis: stemming, keyword extraction, similarity, filter parsing.
|
|
2
|
+
// Stateless — all functions are pure. No I/O, no side effects.
|
|
3
|
+
//
|
|
4
|
+
// Design: this module is the seam for future search strategies.
|
|
5
|
+
// v1: keyword matching with naive stemming (this file)
|
|
6
|
+
// v2: spreading activation over a knowledge graph
|
|
7
|
+
// v3: embedding-based cosine similarity
|
|
8
|
+
// Stopwords for keyword extraction — common English words with no semantic value
|
|
9
|
+
const STOPWORDS = new Set([
|
|
10
|
+
'the', 'a', 'an', 'is', 'are', 'was', 'were', 'be', 'been', 'being',
|
|
11
|
+
'have', 'has', 'had', 'do', 'does', 'did', 'will', 'would', 'could',
|
|
12
|
+
'should', 'may', 'might', 'shall', 'can', 'to', 'of', 'in', 'for',
|
|
13
|
+
'on', 'with', 'at', 'by', 'from', 'as', 'into', 'through', 'during',
|
|
14
|
+
'before', 'after', 'and', 'but', 'or', 'nor', 'not', 'so', 'yet',
|
|
15
|
+
'both', 'either', 'neither', 'each', 'every', 'all', 'any', 'few',
|
|
16
|
+
'more', 'most', 'other', 'some', 'such', 'no', 'only', 'own', 'same',
|
|
17
|
+
'than', 'too', 'very', 'just', 'because', 'if', 'when', 'where',
|
|
18
|
+
'how', 'what', 'which', 'who', 'whom', 'this', 'that', 'these',
|
|
19
|
+
'those', 'it', 'its', 'i', 'me', 'my', 'we', 'our', 'you', 'your',
|
|
20
|
+
'he', 'him', 'his', 'she', 'her', 'they', 'them', 'their', 'about',
|
|
21
|
+
'up', 'out', 'then', 'also', 'use', 'used', 'using',
|
|
22
|
+
]);
|
|
23
|
+
/** Naive stem: strip common English suffixes to improve keyword matching.
|
|
24
|
+
* "reducers" -> "reducer", "sealed" stays, "implementations" -> "implement" */
|
|
25
|
+
export function stem(word) {
|
|
26
|
+
if (word.length <= 4)
|
|
27
|
+
return word;
|
|
28
|
+
// Order matters: longest suffixes first
|
|
29
|
+
if (word.endsWith('ations'))
|
|
30
|
+
return word.slice(0, -6);
|
|
31
|
+
if (word.endsWith('tion'))
|
|
32
|
+
return word.slice(0, -4);
|
|
33
|
+
if (word.endsWith('ment'))
|
|
34
|
+
return word.slice(0, -4);
|
|
35
|
+
if (word.endsWith('ness'))
|
|
36
|
+
return word.slice(0, -4);
|
|
37
|
+
if (word.endsWith('ings'))
|
|
38
|
+
return word.slice(0, -4);
|
|
39
|
+
if (word.endsWith('ally'))
|
|
40
|
+
return word.slice(0, -4);
|
|
41
|
+
if (word.endsWith('ing'))
|
|
42
|
+
return word.slice(0, -3);
|
|
43
|
+
if (word.endsWith('ies'))
|
|
44
|
+
return word.slice(0, -3) + 'y';
|
|
45
|
+
if (word.endsWith('ers'))
|
|
46
|
+
return word.slice(0, -1);
|
|
47
|
+
if (word.endsWith('ted'))
|
|
48
|
+
return word.slice(0, -2);
|
|
49
|
+
if (word.endsWith('es') && word.length > 4)
|
|
50
|
+
return word.slice(0, -2);
|
|
51
|
+
if (word.endsWith('ed') && word.length > 4)
|
|
52
|
+
return word.slice(0, -2);
|
|
53
|
+
if (word.endsWith('ly') && word.length > 4)
|
|
54
|
+
return word.slice(0, -2);
|
|
55
|
+
if (word.endsWith('s') && !word.endsWith('ss') && word.length > 4)
|
|
56
|
+
return word.slice(0, -1);
|
|
57
|
+
return word;
|
|
58
|
+
}
|
|
59
|
+
/** Extract significant keywords from text, with naive stemming.
|
|
60
|
+
* Hyphenated words produce both the compound and its parts:
|
|
61
|
+
* "memory-mcp" -> ["memory-mcp", "memory", "mcp"] */
|
|
62
|
+
export function extractKeywords(text) {
|
|
63
|
+
const words = text.toLowerCase()
|
|
64
|
+
.replace(/[^a-z0-9\s_-]/g, ' ') // strip punctuation
|
|
65
|
+
.split(/\s+/)
|
|
66
|
+
.filter(w => w.length > 2) // skip tiny words
|
|
67
|
+
.filter(w => !STOPWORDS.has(w));
|
|
68
|
+
// Expand hyphenated words: keep compound + add individual parts
|
|
69
|
+
const expanded = [];
|
|
70
|
+
for (const w of words) {
|
|
71
|
+
expanded.push(w);
|
|
72
|
+
if (w.includes('-')) {
|
|
73
|
+
for (const part of w.split('-')) {
|
|
74
|
+
if (part.length > 2 && !STOPWORDS.has(part)) {
|
|
75
|
+
expanded.push(part);
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
return new Set(expanded.map(w => stem(w)));
|
|
81
|
+
}
|
|
82
|
+
/** Jaccard similarity: |intersection| / |union| */
|
|
83
|
+
export function jaccardSimilarity(a, b) {
|
|
84
|
+
if (a.size === 0 && b.size === 0)
|
|
85
|
+
return 0;
|
|
86
|
+
let intersection = 0;
|
|
87
|
+
for (const word of a) {
|
|
88
|
+
if (b.has(word))
|
|
89
|
+
intersection++;
|
|
90
|
+
}
|
|
91
|
+
const union = a.size + b.size - intersection;
|
|
92
|
+
return union === 0 ? 0 : intersection / union;
|
|
93
|
+
}
|
|
94
|
+
/** Containment similarity: |intersection| / min(|a|, |b|)
|
|
95
|
+
* Catches when one entry is a subset of a larger one */
|
|
96
|
+
export function containmentSimilarity(a, b) {
|
|
97
|
+
if (a.size === 0 || b.size === 0)
|
|
98
|
+
return 0;
|
|
99
|
+
let intersection = 0;
|
|
100
|
+
for (const word of a) {
|
|
101
|
+
if (b.has(word))
|
|
102
|
+
intersection++;
|
|
103
|
+
}
|
|
104
|
+
return intersection / Math.min(a.size, b.size);
|
|
105
|
+
}
|
|
106
|
+
/** Combined similarity: max(jaccard, containment) with title boost.
|
|
107
|
+
* Title keywords get double weight by being included twice. */
|
|
108
|
+
export function similarity(titleA, contentA, titleB, contentB) {
|
|
109
|
+
// Title keywords counted twice (implicit weight boost)
|
|
110
|
+
const kwA = extractKeywords(`${titleA} ${titleA} ${contentA}`);
|
|
111
|
+
const kwB = extractKeywords(`${titleB} ${titleB} ${contentB}`);
|
|
112
|
+
const jaccard = jaccardSimilarity(kwA, kwB);
|
|
113
|
+
const containment = containmentSimilarity(kwA, kwB);
|
|
114
|
+
return Math.max(jaccard, containment);
|
|
115
|
+
}
|
|
116
|
+
/** Parse a filter string into OR groups of AND/NOT terms.
|
|
117
|
+
* "reducer sealed|MVI -deprecated" -> [
|
|
118
|
+
* { must: ["reducer", "seal"], mustNot: [] },
|
|
119
|
+
* { must: ["mvi"], mustNot: ["deprecat"] }
|
|
120
|
+
* ] */
|
|
121
|
+
export function parseFilter(filter) {
|
|
122
|
+
const orGroups = filter.split('|').map(g => g.trim()).filter(g => g.length > 0);
|
|
123
|
+
if (orGroups.length === 0)
|
|
124
|
+
return [];
|
|
125
|
+
return orGroups.map(group => {
|
|
126
|
+
const terms = group.split(/\s+/).filter(t => t.length > 0);
|
|
127
|
+
const must = new Set();
|
|
128
|
+
const mustNot = new Set();
|
|
129
|
+
for (const term of terms) {
|
|
130
|
+
if (term.startsWith('-') && term.length > 1) {
|
|
131
|
+
// Negation: stem the compound as-is, WITHOUT hyphen expansion.
|
|
132
|
+
// "-memory-mcp" excludes the compound "memory-mcp" only,
|
|
133
|
+
// not standalone "memory" or "mcp".
|
|
134
|
+
const raw = term.slice(1).toLowerCase().replace(/[^a-z0-9_-]/g, '');
|
|
135
|
+
if (raw.length > 2)
|
|
136
|
+
mustNot.add(stem(raw));
|
|
137
|
+
}
|
|
138
|
+
else {
|
|
139
|
+
// Positive terms: full expansion (hyphens split into parts)
|
|
140
|
+
for (const kw of extractKeywords(term)) {
|
|
141
|
+
must.add(kw);
|
|
142
|
+
}
|
|
143
|
+
}
|
|
144
|
+
}
|
|
145
|
+
return { must, mustNot };
|
|
146
|
+
});
|
|
147
|
+
}
|
|
148
|
+
/** Check if a set of keywords matches a filter string using stemmed AND/OR/NOT logic.
|
|
149
|
+
* Entry matches if ANY OR-group is satisfied (all must-terms present, no mustNot-terms present). */
|
|
150
|
+
export function matchesFilter(allKeywords, filter) {
|
|
151
|
+
const groups = parseFilter(filter);
|
|
152
|
+
if (groups.length === 0)
|
|
153
|
+
return true;
|
|
154
|
+
return groups.some(({ must, mustNot }) => {
|
|
155
|
+
for (const term of must) {
|
|
156
|
+
if (!allKeywords.has(term))
|
|
157
|
+
return false;
|
|
158
|
+
}
|
|
159
|
+
for (const term of mustNot) {
|
|
160
|
+
if (allKeywords.has(term))
|
|
161
|
+
return false;
|
|
162
|
+
}
|
|
163
|
+
return must.size > 0 || mustNot.size > 0;
|
|
164
|
+
});
|
|
165
|
+
}
|
|
166
|
+
/** Compute relevance score for an entry against a filter.
|
|
167
|
+
* Title matches get 2x weight over content-only matches. */
|
|
168
|
+
export function computeRelevanceScore(titleKeywords, contentKeywords, confidence, filter) {
|
|
169
|
+
const groups = parseFilter(filter);
|
|
170
|
+
if (groups.length === 0)
|
|
171
|
+
return 0;
|
|
172
|
+
let bestScore = 0;
|
|
173
|
+
for (const { must } of groups) {
|
|
174
|
+
if (must.size === 0)
|
|
175
|
+
continue;
|
|
176
|
+
let score = 0;
|
|
177
|
+
for (const term of must) {
|
|
178
|
+
if (titleKeywords.has(term)) {
|
|
179
|
+
score += 2.0; // title match = 2x weight
|
|
180
|
+
}
|
|
181
|
+
else if (contentKeywords.has(term)) {
|
|
182
|
+
score += 1.0; // content-only match
|
|
183
|
+
}
|
|
184
|
+
}
|
|
185
|
+
const normalized = score / must.size;
|
|
186
|
+
if (normalized > bestScore)
|
|
187
|
+
bestScore = normalized;
|
|
188
|
+
}
|
|
189
|
+
return bestScore * confidence;
|
|
190
|
+
}
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
/** Minimum similarity score for dedup detection at write time (same-topic). */
|
|
2
|
+
export declare const DEDUP_SIMILARITY_THRESHOLD = 0.35;
|
|
3
|
+
/** Minimum similarity score for conflict detection at query time — same topic.
|
|
4
|
+
* Higher threshold since same-topic overlaps are often legitimate variations. */
|
|
5
|
+
export declare const CONFLICT_SIMILARITY_THRESHOLD_SAME_TOPIC = 0.6;
|
|
6
|
+
/** Minimum similarity score for conflict detection at query time — cross topic.
|
|
7
|
+
* Lower threshold: if entries in different topics overlap this much, they're
|
|
8
|
+
* likely talking about the same architectural decision, which is suspicious. */
|
|
9
|
+
export declare const CONFLICT_SIMILARITY_THRESHOLD_CROSS_TOPIC = 0.42;
|
|
10
|
+
/** Minimum similarity score for surfacing relevant preferences (cross-topic). */
|
|
11
|
+
export declare const PREFERENCE_SURFACE_THRESHOLD = 0.2;
|
|
12
|
+
/** Minimum content length (chars) for conflict detection — short entries are too noisy. */
|
|
13
|
+
export declare const CONFLICT_MIN_CONTENT_CHARS = 50;
|
|
14
|
+
/** Opposition keyword pairs for enhanced conflict detection.
|
|
15
|
+
* When entries overlap AND use opposing terms, boost the conflict signal. */
|
|
16
|
+
export declare const OPPOSITION_PAIRS: ReadonlyArray<readonly [string, string]>;
|
|
17
|
+
/** Score multiplier when a reference path basename matches the context keywords. */
|
|
18
|
+
export declare const REFERENCE_BOOST_MULTIPLIER = 1.3;
|
|
19
|
+
/** Per-topic scoring boost factors for contextSearch().
|
|
20
|
+
* Higher = more likely to surface for any given context. */
|
|
21
|
+
export declare const TOPIC_BOOST: Record<string, number>;
|
|
22
|
+
/** Boost for module-scoped topics not in TOPIC_BOOST. */
|
|
23
|
+
export declare const MODULE_TOPIC_BOOST = 1.1;
|
|
24
|
+
/** User entry score when included by default (no keyword match). */
|
|
25
|
+
export declare const USER_ALWAYS_INCLUDE_SCORE_FRACTION = 0.5;
|
|
26
|
+
/** Days since lastAccessed before a standard entry (arch, conv, gotchas, etc.) goes stale. */
|
|
27
|
+
export declare const DEFAULT_STALE_DAYS_STANDARD = 30;
|
|
28
|
+
/** Days since lastAccessed before a preferences entry goes stale.
|
|
29
|
+
* Longer than standard because coding preferences evolve slowly. */
|
|
30
|
+
export declare const DEFAULT_STALE_DAYS_PREFERENCES = 90;
|
|
31
|
+
/** Maximum stale entries surfaced in a single briefing.
|
|
32
|
+
* Keeps the briefing actionable without overwhelming the agent. */
|
|
33
|
+
export declare const DEFAULT_MAX_STALE_IN_BRIEFING = 5;
|
|
34
|
+
/** Maximum dedup suggestions returned when storing a new entry. */
|
|
35
|
+
export declare const DEFAULT_MAX_DEDUP_SUGGESTIONS = 3;
|
|
36
|
+
/** Maximum conflict pairs surfaced per query/context response. */
|
|
37
|
+
export declare const DEFAULT_MAX_CONFLICT_PAIRS = 2;
|
|
38
|
+
/** Maximum related preferences surfaced when storing a non-preference entry. */
|
|
39
|
+
export declare const DEFAULT_MAX_PREFERENCE_SUGGESTIONS = 3;
|
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
// Central threshold definitions for the memory MCP.
|
|
2
|
+
//
|
|
3
|
+
// Split into two categories intentionally:
|
|
4
|
+
//
|
|
5
|
+
// INTERNAL — calibrated against the text-analyzer algorithm (Jaccard + containment hybrid).
|
|
6
|
+
// Changing these without understanding the similarity function produces confusing results.
|
|
7
|
+
// Not exposed to users.
|
|
8
|
+
//
|
|
9
|
+
// USER-FACING — control how the system behaves for the user's workflow.
|
|
10
|
+
// Exposed via memory-config.json under a top-level "behavior" key.
|
|
11
|
+
// All have sensible defaults; the user only needs to set what they want to change.
|
|
12
|
+
// ─── Internal algorithm thresholds ─────────────────────────────────────────
|
|
13
|
+
// These are properties of the similarity function, not user preferences.
|
|
14
|
+
/** Minimum similarity score for dedup detection at write time (same-topic). */
|
|
15
|
+
export const DEDUP_SIMILARITY_THRESHOLD = 0.35;
|
|
16
|
+
/** Minimum similarity score for conflict detection at query time — same topic.
|
|
17
|
+
* Higher threshold since same-topic overlaps are often legitimate variations. */
|
|
18
|
+
export const CONFLICT_SIMILARITY_THRESHOLD_SAME_TOPIC = 0.60;
|
|
19
|
+
/** Minimum similarity score for conflict detection at query time — cross topic.
|
|
20
|
+
* Lower threshold: if entries in different topics overlap this much, they're
|
|
21
|
+
* likely talking about the same architectural decision, which is suspicious. */
|
|
22
|
+
export const CONFLICT_SIMILARITY_THRESHOLD_CROSS_TOPIC = 0.42;
|
|
23
|
+
/** Minimum similarity score for surfacing relevant preferences (cross-topic). */
|
|
24
|
+
export const PREFERENCE_SURFACE_THRESHOLD = 0.20;
|
|
25
|
+
/** Minimum content length (chars) for conflict detection — short entries are too noisy. */
|
|
26
|
+
export const CONFLICT_MIN_CONTENT_CHARS = 50;
|
|
27
|
+
/** Opposition keyword pairs for enhanced conflict detection.
|
|
28
|
+
* When entries overlap AND use opposing terms, boost the conflict signal. */
|
|
29
|
+
export const OPPOSITION_PAIRS = [
|
|
30
|
+
['use', 'avoid'],
|
|
31
|
+
['always', 'never'],
|
|
32
|
+
['prefer', 'avoid'],
|
|
33
|
+
['required', 'forbidden'],
|
|
34
|
+
['mandatory', 'optional'],
|
|
35
|
+
['sync', 'async'],
|
|
36
|
+
['mutable', 'immutable'],
|
|
37
|
+
['mvi', 'mvvm'],
|
|
38
|
+
['sealed class', 'sealed interface'],
|
|
39
|
+
['inheritance', 'composition'],
|
|
40
|
+
['throw', 'return'], // exceptions vs Result types
|
|
41
|
+
['imperative', 'declarative'],
|
|
42
|
+
];
|
|
43
|
+
/** Score multiplier when a reference path basename matches the context keywords. */
|
|
44
|
+
export const REFERENCE_BOOST_MULTIPLIER = 1.30;
|
|
45
|
+
/** Per-topic scoring boost factors for contextSearch().
|
|
46
|
+
* Higher = more likely to surface for any given context. */
|
|
47
|
+
export const TOPIC_BOOST = {
|
|
48
|
+
user: 2.0, // always surface identity
|
|
49
|
+
preferences: 1.8, // almost always relevant
|
|
50
|
+
gotchas: 1.5, // high-value warnings
|
|
51
|
+
conventions: 1.2, // coding patterns
|
|
52
|
+
architecture: 1.0, // baseline
|
|
53
|
+
'recent-work': 0.9, // slightly deprioritized (branch-filtered separately)
|
|
54
|
+
};
|
|
55
|
+
/** Boost for module-scoped topics not in TOPIC_BOOST. */
|
|
56
|
+
export const MODULE_TOPIC_BOOST = 1.1;
|
|
57
|
+
/** User entry score when included by default (no keyword match). */
|
|
58
|
+
export const USER_ALWAYS_INCLUDE_SCORE_FRACTION = 0.5;
|
|
59
|
+
// ─── User-facing behavior defaults ─────────────────────────────────────────
|
|
60
|
+
// These are exposed via memory-config.json "behavior" block.
|
|
61
|
+
// All values below are the defaults used when the user has not configured them.
|
|
62
|
+
/** Days since lastAccessed before a standard entry (arch, conv, gotchas, etc.) goes stale. */
|
|
63
|
+
export const DEFAULT_STALE_DAYS_STANDARD = 30;
|
|
64
|
+
/** Days since lastAccessed before a preferences entry goes stale.
|
|
65
|
+
* Longer than standard because coding preferences evolve slowly. */
|
|
66
|
+
export const DEFAULT_STALE_DAYS_PREFERENCES = 90;
|
|
67
|
+
/** Maximum stale entries surfaced in a single briefing.
|
|
68
|
+
* Keeps the briefing actionable without overwhelming the agent. */
|
|
69
|
+
export const DEFAULT_MAX_STALE_IN_BRIEFING = 5;
|
|
70
|
+
/** Maximum dedup suggestions returned when storing a new entry. */
|
|
71
|
+
export const DEFAULT_MAX_DEDUP_SUGGESTIONS = 3;
|
|
72
|
+
/** Maximum conflict pairs surfaced per query/context response. */
|
|
73
|
+
export const DEFAULT_MAX_CONFLICT_PAIRS = 2;
|
|
74
|
+
/** Maximum related preferences surfaced when storing a non-preference entry. */
|
|
75
|
+
export const DEFAULT_MAX_PREFERENCE_SUGGESTIONS = 3;
|
package/dist/types.d.ts
ADDED
|
@@ -0,0 +1,186 @@
|
|
|
1
|
+
/** Trust levels for knowledge sources, ordered by reliability */
|
|
2
|
+
export type TrustLevel = 'user' | 'agent-confirmed' | 'agent-inferred';
|
|
3
|
+
/** Parse a raw string into a TrustLevel, returning null for invalid input */
|
|
4
|
+
export declare function parseTrustLevel(raw: string): TrustLevel | null;
|
|
5
|
+
/** Predefined topic scopes for organizing knowledge */
|
|
6
|
+
export type TopicScope = 'user' | 'preferences' | 'architecture' | 'conventions' | 'gotchas' | 'recent-work' | `modules/${string}`;
|
|
7
|
+
/** Parse a raw string into a TopicScope, returning null for invalid input */
|
|
8
|
+
export declare function parseTopicScope(raw: string): TopicScope | null;
|
|
9
|
+
/** Injectable clock for deterministic time in tests */
|
|
10
|
+
export interface Clock {
|
|
11
|
+
now(): Date;
|
|
12
|
+
isoNow(): string;
|
|
13
|
+
}
|
|
14
|
+
/** Production clock using real wall time */
|
|
15
|
+
export declare const realClock: Clock;
|
|
16
|
+
/** A single knowledge entry stored in the memory system */
|
|
17
|
+
export interface MemoryEntry {
|
|
18
|
+
readonly id: string;
|
|
19
|
+
readonly topic: TopicScope;
|
|
20
|
+
readonly title: string;
|
|
21
|
+
readonly content: string;
|
|
22
|
+
readonly confidence: number;
|
|
23
|
+
readonly trust: TrustLevel;
|
|
24
|
+
readonly sources: readonly string[];
|
|
25
|
+
readonly references?: readonly string[];
|
|
26
|
+
readonly created: string;
|
|
27
|
+
readonly lastAccessed: string;
|
|
28
|
+
readonly gitSha?: string;
|
|
29
|
+
readonly branch?: string;
|
|
30
|
+
}
|
|
31
|
+
/** In-memory index entry for fast lookups */
|
|
32
|
+
export interface IndexEntry {
|
|
33
|
+
readonly id: string;
|
|
34
|
+
readonly topic: TopicScope;
|
|
35
|
+
readonly title: string;
|
|
36
|
+
readonly confidence: number;
|
|
37
|
+
readonly trust: TrustLevel;
|
|
38
|
+
readonly created: string;
|
|
39
|
+
readonly lastAccessed: string;
|
|
40
|
+
readonly file: string;
|
|
41
|
+
readonly branch?: string;
|
|
42
|
+
}
|
|
43
|
+
/** Detail levels for query responses */
|
|
44
|
+
export type DetailLevel = 'brief' | 'standard' | 'full';
|
|
45
|
+
/** Result of a memory query */
|
|
46
|
+
export interface QueryResult {
|
|
47
|
+
readonly scope: string;
|
|
48
|
+
readonly detail: DetailLevel;
|
|
49
|
+
readonly entries: readonly QueryEntry[];
|
|
50
|
+
readonly totalEntries: number;
|
|
51
|
+
}
|
|
52
|
+
export interface QueryEntry {
|
|
53
|
+
readonly id: string;
|
|
54
|
+
readonly title: string;
|
|
55
|
+
readonly summary: string;
|
|
56
|
+
readonly confidence: number;
|
|
57
|
+
readonly relevanceScore: number;
|
|
58
|
+
readonly fresh: boolean;
|
|
59
|
+
readonly references?: readonly string[];
|
|
60
|
+
readonly content?: string;
|
|
61
|
+
readonly trust?: TrustLevel;
|
|
62
|
+
readonly sources?: readonly string[];
|
|
63
|
+
readonly created?: string;
|
|
64
|
+
readonly lastAccessed?: string;
|
|
65
|
+
readonly gitSha?: string;
|
|
66
|
+
readonly branch?: string;
|
|
67
|
+
}
|
|
68
|
+
/** A related entry surfaced during dedup detection */
|
|
69
|
+
export interface RelatedEntry {
|
|
70
|
+
readonly id: string;
|
|
71
|
+
readonly title: string;
|
|
72
|
+
readonly content: string;
|
|
73
|
+
readonly confidence: number;
|
|
74
|
+
readonly trust: TrustLevel;
|
|
75
|
+
}
|
|
76
|
+
/** Result of a memory store operation — discriminated union eliminates impossible states */
|
|
77
|
+
export type StoreResult = {
|
|
78
|
+
readonly stored: true;
|
|
79
|
+
readonly id: string;
|
|
80
|
+
readonly topic: TopicScope;
|
|
81
|
+
readonly file: string;
|
|
82
|
+
readonly confidence: number;
|
|
83
|
+
readonly warning?: string;
|
|
84
|
+
/** Soft warning when content looks ephemeral — informational, never blocking */
|
|
85
|
+
readonly ephemeralWarning?: string;
|
|
86
|
+
readonly relatedEntries?: readonly RelatedEntry[];
|
|
87
|
+
readonly relevantPreferences?: readonly RelatedEntry[];
|
|
88
|
+
} | {
|
|
89
|
+
readonly stored: false;
|
|
90
|
+
readonly topic: TopicScope;
|
|
91
|
+
readonly warning: string;
|
|
92
|
+
};
|
|
93
|
+
/** Result of a memory correction — discriminated union eliminates impossible states */
|
|
94
|
+
export type CorrectResult = {
|
|
95
|
+
readonly corrected: true;
|
|
96
|
+
readonly id: string;
|
|
97
|
+
readonly action: 'append' | 'replace' | 'delete';
|
|
98
|
+
readonly newConfidence: number;
|
|
99
|
+
readonly trust: TrustLevel;
|
|
100
|
+
} | {
|
|
101
|
+
readonly corrected: false;
|
|
102
|
+
readonly id: string;
|
|
103
|
+
readonly error: string;
|
|
104
|
+
};
|
|
105
|
+
/** Memory health statistics */
|
|
106
|
+
export interface MemoryStats {
|
|
107
|
+
readonly totalEntries: number;
|
|
108
|
+
readonly corruptFiles: number;
|
|
109
|
+
readonly byTopic: Record<string, number>;
|
|
110
|
+
readonly byTrust: Record<TrustLevel, number>;
|
|
111
|
+
readonly byFreshness: {
|
|
112
|
+
fresh: number;
|
|
113
|
+
stale: number;
|
|
114
|
+
unknown: number;
|
|
115
|
+
};
|
|
116
|
+
readonly storageSize: string;
|
|
117
|
+
readonly storageBudgetBytes: number;
|
|
118
|
+
readonly memoryPath: string;
|
|
119
|
+
readonly oldestEntry?: string;
|
|
120
|
+
readonly newestEntry?: string;
|
|
121
|
+
}
|
|
122
|
+
/** A stale entry surfaced during briefing for agent-driven renewal */
|
|
123
|
+
export interface StaleEntry {
|
|
124
|
+
readonly id: string;
|
|
125
|
+
readonly title: string;
|
|
126
|
+
readonly topic: TopicScope;
|
|
127
|
+
readonly daysSinceAccess: number;
|
|
128
|
+
}
|
|
129
|
+
/** A pair of entries with high content overlap — potential conflict */
|
|
130
|
+
export interface ConflictPair {
|
|
131
|
+
readonly a: {
|
|
132
|
+
readonly id: string;
|
|
133
|
+
readonly title: string;
|
|
134
|
+
readonly confidence: number;
|
|
135
|
+
readonly created: string;
|
|
136
|
+
};
|
|
137
|
+
readonly b: {
|
|
138
|
+
readonly id: string;
|
|
139
|
+
readonly title: string;
|
|
140
|
+
readonly confidence: number;
|
|
141
|
+
readonly created: string;
|
|
142
|
+
};
|
|
143
|
+
readonly similarity: number;
|
|
144
|
+
}
|
|
145
|
+
/** Briefing response for session start */
|
|
146
|
+
export interface BriefingResult {
|
|
147
|
+
readonly briefing: string;
|
|
148
|
+
readonly entryCount: number;
|
|
149
|
+
readonly staleEntries: number;
|
|
150
|
+
readonly staleDetails?: readonly StaleEntry[];
|
|
151
|
+
readonly suggestion?: string;
|
|
152
|
+
}
|
|
153
|
+
/** Git operations boundary — injected to keep core logic testable and swappable */
|
|
154
|
+
export interface GitService {
|
|
155
|
+
getCurrentBranch(repoRoot: string): Promise<string>;
|
|
156
|
+
getHeadSha(repoRoot: string): Promise<string | undefined>;
|
|
157
|
+
}
|
|
158
|
+
/** User-configurable behavior thresholds — exposed via memory-config.json "behavior" block.
|
|
159
|
+
* All fields are optional; the system uses the defaults from thresholds.ts when absent. */
|
|
160
|
+
export interface BehaviorConfig {
|
|
161
|
+
/** Days since lastAccessed before a standard entry (arch, conv, gotchas, etc.) goes stale.
|
|
162
|
+
* Lower for fast-moving codebases; higher for stable ones. Default: 30. Range: 1–365. */
|
|
163
|
+
readonly staleDaysStandard?: number;
|
|
164
|
+
/** Days since lastAccessed before a preferences entry goes stale.
|
|
165
|
+
* Preferences evolve slowly — default 90 keeps them fresh longer. Range: 1–730. */
|
|
166
|
+
readonly staleDaysPreferences?: number;
|
|
167
|
+
/** Maximum stale entries shown in a briefing. Default: 5. Range: 1–20. */
|
|
168
|
+
readonly maxStaleInBriefing?: number;
|
|
169
|
+
/** Maximum dedup suggestions when storing a new entry. Default: 3. Range: 1–10. */
|
|
170
|
+
readonly maxDedupSuggestions?: number;
|
|
171
|
+
/** Maximum conflict pairs shown per query/context response. Default: 2. Range: 1–5. */
|
|
172
|
+
readonly maxConflictPairs?: number;
|
|
173
|
+
}
|
|
174
|
+
/** Configuration for the memory MCP */
|
|
175
|
+
export interface MemoryConfig {
|
|
176
|
+
readonly repoRoot: string;
|
|
177
|
+
readonly memoryPath: string;
|
|
178
|
+
readonly storageBudgetBytes: number;
|
|
179
|
+
readonly behavior?: BehaviorConfig;
|
|
180
|
+
readonly clock?: Clock;
|
|
181
|
+
readonly git?: GitService;
|
|
182
|
+
}
|
|
183
|
+
/** Default confidence values by trust level */
|
|
184
|
+
export declare const DEFAULT_CONFIDENCE: Record<TrustLevel, number>;
|
|
185
|
+
/** Storage budget: 2MB */
|
|
186
|
+
export declare const DEFAULT_STORAGE_BUDGET_BYTES: number;
|
package/dist/types.js
ADDED
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
// Core types for the codebase memory MCP
|
|
2
|
+
//
|
|
3
|
+
// Design principles:
|
|
4
|
+
// - Make illegal states unrepresentable: discriminated unions over boolean+optional
|
|
5
|
+
// - Validate at boundaries, trust inside: parse functions at system edges
|
|
6
|
+
// - Explicit domain types over primitives where meaning matters
|
|
7
|
+
const TRUST_LEVELS = ['user', 'agent-confirmed', 'agent-inferred'];
|
|
8
|
+
/** Parse a raw string into a TrustLevel, returning null for invalid input */
|
|
9
|
+
export function parseTrustLevel(raw) {
|
|
10
|
+
return TRUST_LEVELS.includes(raw) ? raw : null;
|
|
11
|
+
}
|
|
12
|
+
const FIXED_TOPICS = ['user', 'preferences', 'architecture', 'conventions', 'gotchas', 'recent-work'];
|
|
13
|
+
/** Parse a raw string into a TopicScope, returning null for invalid input */
|
|
14
|
+
export function parseTopicScope(raw) {
|
|
15
|
+
if (FIXED_TOPICS.includes(raw))
|
|
16
|
+
return raw;
|
|
17
|
+
if (raw.startsWith('modules/') && raw.length > 8)
|
|
18
|
+
return raw;
|
|
19
|
+
return null;
|
|
20
|
+
}
|
|
21
|
+
/** Production clock using real wall time */
|
|
22
|
+
export const realClock = {
|
|
23
|
+
now: () => new Date(),
|
|
24
|
+
isoNow: () => new Date().toISOString(),
|
|
25
|
+
};
|
|
26
|
+
/** Default confidence values by trust level */
|
|
27
|
+
export const DEFAULT_CONFIDENCE = {
|
|
28
|
+
'user': 1.0,
|
|
29
|
+
'agent-confirmed': 0.85,
|
|
30
|
+
'agent-inferred': 0.70,
|
|
31
|
+
};
|
|
32
|
+
/** Storage budget: 2MB */
|
|
33
|
+
export const DEFAULT_STORAGE_BUDGET_BYTES = 2 * 1024 * 1024;
|
package/package.json
ADDED
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@exaudeus/memory-mcp",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"description": "Codebase memory MCP server - persistent, evolving knowledge for AI coding agents",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"main": "dist/index.js",
|
|
7
|
+
"bin": {
|
|
8
|
+
"memory-mcp": "dist/index.js"
|
|
9
|
+
},
|
|
10
|
+
"files": [
|
|
11
|
+
"dist",
|
|
12
|
+
"README.md",
|
|
13
|
+
"LICENSE"
|
|
14
|
+
],
|
|
15
|
+
"scripts": {
|
|
16
|
+
"build": "tsc -p tsconfig.json && cp src/ephemeral-weights.json dist/",
|
|
17
|
+
"typecheck": "tsc --noEmit",
|
|
18
|
+
"dev": "tsx src/index.ts",
|
|
19
|
+
"start": "node dist/index.js",
|
|
20
|
+
"test": "node --import tsx --test src/__tests__/**/*.test.ts",
|
|
21
|
+
"prepublishOnly": "npm run build",
|
|
22
|
+
"semantic-release": "semantic-release"
|
|
23
|
+
},
|
|
24
|
+
"publishConfig": {
|
|
25
|
+
"access": "public"
|
|
26
|
+
},
|
|
27
|
+
"repository": {
|
|
28
|
+
"type": "git",
|
|
29
|
+
"url": "https://github.com/EtienneBBeaulac/memory-mcp.git"
|
|
30
|
+
},
|
|
31
|
+
"keywords": [
|
|
32
|
+
"mcp",
|
|
33
|
+
"memory",
|
|
34
|
+
"codebase-memory",
|
|
35
|
+
"ai-agent",
|
|
36
|
+
"firebender"
|
|
37
|
+
],
|
|
38
|
+
"license": "MIT",
|
|
39
|
+
"dependencies": {
|
|
40
|
+
"@modelcontextprotocol/sdk": "^1.2.0",
|
|
41
|
+
"zod": "^3.23.0"
|
|
42
|
+
},
|
|
43
|
+
"devDependencies": {
|
|
44
|
+
"@semantic-release/changelog": "^6.0.3",
|
|
45
|
+
"@semantic-release/exec": "^7.1.0",
|
|
46
|
+
"@semantic-release/git": "^10.0.1",
|
|
47
|
+
"@semantic-release/github": "^12.0.6",
|
|
48
|
+
"@types/node": "^20.0.0",
|
|
49
|
+
"conventional-changelog-conventionalcommits": "^9.1.0",
|
|
50
|
+
"semantic-release": "^24.2.9",
|
|
51
|
+
"tsx": "^4.0.0",
|
|
52
|
+
"typescript": "^5.0.0"
|
|
53
|
+
},
|
|
54
|
+
"engines": {
|
|
55
|
+
"node": ">=18.0.0"
|
|
56
|
+
}
|
|
57
|
+
}
|