@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.
- package/dist/config-manager.d.ts +6 -0
- package/dist/config-manager.js +13 -1
- package/dist/config.js +45 -3
- package/dist/ephemeral.d.ts +7 -3
- package/dist/ephemeral.js +66 -23
- package/dist/formatters.js +22 -12
- package/dist/index.js +256 -248
- package/dist/lobe-resolution.d.ts +34 -0
- package/dist/lobe-resolution.js +89 -0
- package/dist/store.d.ts +3 -0
- package/dist/store.js +11 -2
- package/dist/thresholds.d.ts +3 -8
- package/dist/thresholds.js +4 -8
- package/dist/types.d.ts +6 -0
- package/package.json +1 -1
package/dist/config-manager.d.ts
CHANGED
|
@@ -27,10 +27,14 @@ export declare class ConfigManager {
|
|
|
27
27
|
private stores;
|
|
28
28
|
private lobeHealth;
|
|
29
29
|
private configMtime;
|
|
30
|
+
/** Cached alwaysInclude lobe names — recomputed atomically on reload. */
|
|
31
|
+
private cachedAlwaysIncludeLobes;
|
|
30
32
|
protected statFile(path: string): Promise<{
|
|
31
33
|
mtimeMs: number;
|
|
32
34
|
}>;
|
|
33
35
|
constructor(configPath: string, initial: LoadedConfig, initialStores: Map<string, MarkdownMemoryStore>, initialHealth: Map<string, LobeHealth>);
|
|
36
|
+
/** Derive alwaysInclude lobe names from config — pure, no side effects. */
|
|
37
|
+
private static computeAlwaysIncludeLobes;
|
|
34
38
|
/**
|
|
35
39
|
* Ensure config is fresh. Call at the start of every tool handler.
|
|
36
40
|
* Stats config file, reloads if mtime changed. Graceful on all errors.
|
|
@@ -46,4 +50,6 @@ export declare class ConfigManager {
|
|
|
46
50
|
getLobeHealth(lobe: string): LobeHealth | undefined;
|
|
47
51
|
getConfigOrigin(): ConfigOrigin;
|
|
48
52
|
getLobeConfig(lobe: string): MemoryConfig | undefined;
|
|
53
|
+
/** Returns lobe names where alwaysInclude is true. Cached; rebuilt atomically on hot-reload. */
|
|
54
|
+
getAlwaysIncludeLobes(): readonly string[];
|
|
49
55
|
}
|
package/dist/config-manager.js
CHANGED
|
@@ -28,6 +28,13 @@ export class ConfigManager {
|
|
|
28
28
|
this.stores = initialStores;
|
|
29
29
|
this.lobeHealth = initialHealth;
|
|
30
30
|
this.configMtime = Date.now(); // Initial mtime (will be updated on first stat)
|
|
31
|
+
this.cachedAlwaysIncludeLobes = ConfigManager.computeAlwaysIncludeLobes(this.lobeConfigs);
|
|
32
|
+
}
|
|
33
|
+
/** Derive alwaysInclude lobe names from config — pure, no side effects. */
|
|
34
|
+
static computeAlwaysIncludeLobes(configs) {
|
|
35
|
+
return Array.from(configs.entries())
|
|
36
|
+
.filter(([, config]) => config.alwaysInclude === true)
|
|
37
|
+
.map(([name]) => name);
|
|
31
38
|
}
|
|
32
39
|
/**
|
|
33
40
|
* Ensure config is fresh. Call at the start of every tool handler.
|
|
@@ -90,12 +97,13 @@ export class ConfigManager {
|
|
|
90
97
|
});
|
|
91
98
|
}
|
|
92
99
|
}
|
|
93
|
-
// Atomic swap
|
|
100
|
+
// Atomic swap — all derived state recomputed together
|
|
94
101
|
this.configOrigin = newConfig.origin;
|
|
95
102
|
this.lobeConfigs = newConfig.configs;
|
|
96
103
|
this.stores = newStores;
|
|
97
104
|
this.lobeHealth = newHealth;
|
|
98
105
|
this.configMtime = newMtime;
|
|
106
|
+
this.cachedAlwaysIncludeLobes = ConfigManager.computeAlwaysIncludeLobes(newConfig.configs);
|
|
99
107
|
const lobeCount = newConfig.configs.size;
|
|
100
108
|
const degradedCount = Array.from(newHealth.values()).filter(h => h.status === 'degraded').length;
|
|
101
109
|
const timestamp = new Date().toISOString();
|
|
@@ -123,4 +131,8 @@ export class ConfigManager {
|
|
|
123
131
|
getLobeConfig(lobe) {
|
|
124
132
|
return this.lobeConfigs.get(lobe);
|
|
125
133
|
}
|
|
134
|
+
/** Returns lobe names where alwaysInclude is true. Cached; rebuilt atomically on hot-reload. */
|
|
135
|
+
getAlwaysIncludeLobes() {
|
|
136
|
+
return this.cachedAlwaysIncludeLobes;
|
|
137
|
+
}
|
|
126
138
|
}
|
package/dist/config.js
CHANGED
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
//
|
|
3
3
|
// Priority: memory-config.json → env vars → single-repo default
|
|
4
4
|
// Graceful degradation: each source falls through to the next on failure.
|
|
5
|
-
import { readFileSync } from 'fs';
|
|
5
|
+
import { readFileSync, existsSync, readdirSync } from 'fs';
|
|
6
6
|
import { execFileSync } from 'child_process';
|
|
7
7
|
import path from 'path';
|
|
8
8
|
import os from 'os';
|
|
@@ -81,6 +81,42 @@ function resolveMemoryPath(repoRoot, workspaceName, explicitMemoryDir) {
|
|
|
81
81
|
}
|
|
82
82
|
return path.join(os.homedir(), '.memory-mcp', workspaceName);
|
|
83
83
|
}
|
|
84
|
+
/** If no lobe has alwaysInclude: true AND the legacy global store directory has actual entries,
|
|
85
|
+
* auto-create a "global" lobe pointing to it. Protects existing users who haven't updated their config.
|
|
86
|
+
* Only fires when the dir contains .md files — an empty dir doesn't trigger creation. */
|
|
87
|
+
function ensureAlwaysIncludeLobe(configs, behavior) {
|
|
88
|
+
const hasAlwaysInclude = Array.from(configs.values()).some(c => c.alwaysInclude);
|
|
89
|
+
if (hasAlwaysInclude)
|
|
90
|
+
return;
|
|
91
|
+
// Don't overwrite a user-defined "global" lobe — warn instead.
|
|
92
|
+
// Philosophy: "Make illegal states unrepresentable" — silently replacing config is a hidden state.
|
|
93
|
+
if (configs.has('global')) {
|
|
94
|
+
process.stderr.write(`[memory-mcp] Lobe "global" exists but has no alwaysInclude flag. ` +
|
|
95
|
+
`Add "alwaysInclude": true to your global lobe config to include it in all reads.\n`);
|
|
96
|
+
return;
|
|
97
|
+
}
|
|
98
|
+
const globalPath = path.join(os.homedir(), '.memory-mcp', 'global');
|
|
99
|
+
if (!existsSync(globalPath))
|
|
100
|
+
return;
|
|
101
|
+
// Only auto-create if the dir has actual memory entries (not just an empty directory)
|
|
102
|
+
try {
|
|
103
|
+
const files = readdirSync(globalPath);
|
|
104
|
+
if (!files.some(f => f.endsWith('.md')))
|
|
105
|
+
return;
|
|
106
|
+
}
|
|
107
|
+
catch (err) {
|
|
108
|
+
process.stderr.write(`[memory-mcp] Warning: could not read legacy global store at ${globalPath}: ${err}\n`);
|
|
109
|
+
return;
|
|
110
|
+
}
|
|
111
|
+
configs.set('global', {
|
|
112
|
+
repoRoot: os.homedir(),
|
|
113
|
+
memoryPath: globalPath,
|
|
114
|
+
storageBudgetBytes: DEFAULT_STORAGE_BUDGET_BYTES,
|
|
115
|
+
alwaysInclude: true,
|
|
116
|
+
behavior,
|
|
117
|
+
});
|
|
118
|
+
process.stderr.write(`[memory-mcp] Auto-created "global" lobe (alwaysInclude) from existing ${globalPath}\n`);
|
|
119
|
+
}
|
|
84
120
|
/** Load lobe configs with priority: memory-config.json -> env vars -> single-repo default */
|
|
85
121
|
export function getLobeConfigs() {
|
|
86
122
|
const configs = new Map();
|
|
@@ -105,13 +141,15 @@ export function getLobeConfigs() {
|
|
|
105
141
|
repoRoot,
|
|
106
142
|
memoryPath: resolveMemoryPath(repoRoot, name, config.memoryDir),
|
|
107
143
|
storageBudgetBytes: (config.budgetMB ?? 2) * 1024 * 1024,
|
|
144
|
+
alwaysInclude: config.alwaysInclude ?? false,
|
|
108
145
|
behavior,
|
|
109
146
|
});
|
|
110
147
|
}
|
|
111
148
|
if (configs.size > 0) {
|
|
149
|
+
// Reuse the already-parsed behavior config for the alwaysInclude fallback
|
|
150
|
+
const resolvedBehavior = external.behavior ? behavior : undefined;
|
|
151
|
+
ensureAlwaysIncludeLobe(configs, resolvedBehavior);
|
|
112
152
|
process.stderr.write(`[memory-mcp] Loaded ${configs.size} lobe(s) from memory-config.json\n`);
|
|
113
|
-
// Pass resolved behavior at the top-level so diagnostics can surface active values
|
|
114
|
-
const resolvedBehavior = external.behavior ? parseBehaviorConfig(external.behavior) : undefined;
|
|
115
153
|
return { configs, origin: { source: 'file', path: configPath }, behavior: resolvedBehavior };
|
|
116
154
|
}
|
|
117
155
|
}
|
|
@@ -137,9 +175,11 @@ export function getLobeConfigs() {
|
|
|
137
175
|
repoRoot,
|
|
138
176
|
memoryPath: resolveMemoryPath(repoRoot, name, explicitDir),
|
|
139
177
|
storageBudgetBytes: storageBudget,
|
|
178
|
+
alwaysInclude: false,
|
|
140
179
|
});
|
|
141
180
|
}
|
|
142
181
|
if (configs.size > 0) {
|
|
182
|
+
ensureAlwaysIncludeLobe(configs);
|
|
143
183
|
process.stderr.write(`[memory-mcp] Loaded ${configs.size} lobe(s) from MEMORY_MCP_WORKSPACES env var\n`);
|
|
144
184
|
return { configs, origin: { source: 'env' } };
|
|
145
185
|
}
|
|
@@ -156,7 +196,9 @@ export function getLobeConfigs() {
|
|
|
156
196
|
repoRoot,
|
|
157
197
|
memoryPath: resolveMemoryPath(repoRoot, 'default', explicitDir),
|
|
158
198
|
storageBudgetBytes: storageBudget,
|
|
199
|
+
alwaysInclude: false,
|
|
159
200
|
});
|
|
201
|
+
// No ensureAlwaysIncludeLobe here — single-repo default users have everything in one lobe
|
|
160
202
|
process.stderr.write(`[memory-mcp] Using single-lobe default mode (cwd: ${repoRoot})\n`);
|
|
161
203
|
return { configs, origin: { source: 'default' } };
|
|
162
204
|
}
|
package/dist/ephemeral.d.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import type { TopicScope } from './types.js';
|
|
1
|
+
import type { EphemeralSeverity, TopicScope } from './types.js';
|
|
2
2
|
/** Run the TF-IDF classifier on a text. Returns probability 0-1 that content is ephemeral.
|
|
3
3
|
* Supports both v1 (unigrams only) and v2 (bigrams + engineered features) models. */
|
|
4
4
|
export declare function classifyEphemeral(title: string, content: string, topic?: string): number | null;
|
|
@@ -15,6 +15,10 @@ export interface EphemeralSignal {
|
|
|
15
15
|
* Returns an array of matched signals, empty if content looks durable.
|
|
16
16
|
* Pure function — no side effects, no I/O. */
|
|
17
17
|
export declare function detectEphemeralSignals(title: string, content: string, topic: TopicScope): readonly EphemeralSignal[];
|
|
18
|
-
/**
|
|
18
|
+
/** Derive aggregate severity from a set of detected signals.
|
|
19
|
+
* Single source of truth for the threshold logic — both formatEphemeralWarning
|
|
20
|
+
* and store.ts call this so the thresholds can only diverge in one place. */
|
|
21
|
+
export declare function getEphemeralSeverity(signals: readonly EphemeralSignal[]): EphemeralSeverity | null;
|
|
22
|
+
/** Format ephemeral signals into a visually prominent warning block.
|
|
19
23
|
* Returns undefined if no signals were detected. */
|
|
20
|
-
export declare function formatEphemeralWarning(signals: readonly EphemeralSignal[]): string | undefined;
|
|
24
|
+
export declare function formatEphemeralWarning(signals: readonly EphemeralSignal[], entryId: string): string | undefined;
|
package/dist/ephemeral.js
CHANGED
|
@@ -18,6 +18,7 @@
|
|
|
18
18
|
import { readFileSync } from 'fs';
|
|
19
19
|
import { dirname, join } from 'path';
|
|
20
20
|
import { fileURLToPath } from 'url';
|
|
21
|
+
import { WARN_SEPARATOR } from './thresholds.js';
|
|
21
22
|
let cachedModel = null;
|
|
22
23
|
function loadModel() {
|
|
23
24
|
if (cachedModel)
|
|
@@ -523,6 +524,7 @@ const SIGNALS = [
|
|
|
523
524
|
test: (_title, content) => {
|
|
524
525
|
const patterns = [
|
|
525
526
|
/[,;]\s+also\b/i, // "X works this way, also Y does Z"
|
|
527
|
+
/[.!?]\s+also[,\s]/i, // ". Also, Y does Z" (sentence-start also)
|
|
526
528
|
/[.!?]\s+additionally,/i, // ". Additionally, ..."
|
|
527
529
|
/[.!?]\s+furthermore,/i, // ". Furthermore, ..."
|
|
528
530
|
/\bunrelated:/i, // "Unrelated: ..."
|
|
@@ -589,30 +591,71 @@ export function detectEphemeralSignals(title, content, topic) {
|
|
|
589
591
|
}
|
|
590
592
|
return signals;
|
|
591
593
|
}
|
|
592
|
-
/**
|
|
593
|
-
*
|
|
594
|
-
|
|
594
|
+
/** Derive aggregate severity from a set of detected signals.
|
|
595
|
+
* Single source of truth for the threshold logic — both formatEphemeralWarning
|
|
596
|
+
* and store.ts call this so the thresholds can only diverge in one place. */
|
|
597
|
+
export function getEphemeralSeverity(signals) {
|
|
595
598
|
if (signals.length === 0)
|
|
596
|
-
return
|
|
599
|
+
return null;
|
|
597
600
|
const highCount = signals.filter(s => s.confidence === 'high').length;
|
|
598
|
-
|
|
599
|
-
|
|
600
|
-
|
|
601
|
-
|
|
602
|
-
|
|
603
|
-
|
|
604
|
-
|
|
605
|
-
|
|
606
|
-
|
|
607
|
-
|
|
608
|
-
|
|
609
|
-
|
|
610
|
-
}
|
|
611
|
-
|
|
612
|
-
|
|
613
|
-
|
|
614
|
-
|
|
615
|
-
|
|
601
|
+
if (highCount >= 2)
|
|
602
|
+
return 'high';
|
|
603
|
+
if (highCount === 1)
|
|
604
|
+
return 'medium';
|
|
605
|
+
return 'low';
|
|
606
|
+
}
|
|
607
|
+
/** Format ephemeral signals into a visually prominent warning block.
|
|
608
|
+
* Returns undefined if no signals were detected. */
|
|
609
|
+
export function formatEphemeralWarning(signals, entryId) {
|
|
610
|
+
const severity = getEphemeralSeverity(signals);
|
|
611
|
+
if (severity === null)
|
|
612
|
+
return undefined;
|
|
613
|
+
const signalLines = signals.map(s => ` - ${s.label}: ${s.detail}`).join('\n');
|
|
614
|
+
// Delete command without leading spaces — callsites add their own indentation.
|
|
615
|
+
const deleteCmd = `memory_correct(id: "${entryId}", action: "delete")`;
|
|
616
|
+
// Scale both the header and guidance text with severity.
|
|
617
|
+
// high: near-certain ephemeral → "ACTION REQUIRED" + prominent DELETE label.
|
|
618
|
+
// medium: likely ephemeral → "REVIEW NEEDED" + delete as an option.
|
|
619
|
+
// low: uncertain → "CHECK BELOW" + softest framing.
|
|
620
|
+
switch (severity) {
|
|
621
|
+
case 'high':
|
|
622
|
+
return [
|
|
623
|
+
WARN_SEPARATOR,
|
|
624
|
+
'⚠ EPHEMERAL CONTENT — ACTION REQUIRED ⚠',
|
|
625
|
+
WARN_SEPARATOR,
|
|
626
|
+
signalLines,
|
|
627
|
+
'',
|
|
628
|
+
'Will this still be true in 6 months? Almost certainly not.',
|
|
629
|
+
'',
|
|
630
|
+
`DELETE: ${deleteCmd}`,
|
|
631
|
+
'',
|
|
632
|
+
'If there is a lasting insight here, extract and rephrase',
|
|
633
|
+
'it as a present-tense fact before deleting this entry.',
|
|
634
|
+
WARN_SEPARATOR,
|
|
635
|
+
].join('\n');
|
|
636
|
+
case 'medium':
|
|
637
|
+
return [
|
|
638
|
+
WARN_SEPARATOR,
|
|
639
|
+
'⚠ POSSIBLY EPHEMERAL CONTENT — REVIEW NEEDED ⚠',
|
|
640
|
+
WARN_SEPARATOR,
|
|
641
|
+
signalLines,
|
|
642
|
+
'',
|
|
643
|
+
'Will this still be true in 6 months?',
|
|
644
|
+
` - If not: ${deleteCmd}`,
|
|
645
|
+
` - If so: rephrase as a present-tense fact (what is now true)`,
|
|
646
|
+
WARN_SEPARATOR,
|
|
647
|
+
].join('\n');
|
|
648
|
+
case 'low':
|
|
649
|
+
return [
|
|
650
|
+
WARN_SEPARATOR,
|
|
651
|
+
'⚠ POSSIBLY EPHEMERAL CONTENT — CHECK BELOW ⚠',
|
|
652
|
+
WARN_SEPARATOR,
|
|
653
|
+
signalLines,
|
|
654
|
+
'',
|
|
655
|
+
'Ask: will this still be true in 6 months?',
|
|
656
|
+
` - If not: ${deleteCmd}`,
|
|
657
|
+
` - If so: rephrase as a present-tense fact if possible`,
|
|
658
|
+
WARN_SEPARATOR,
|
|
659
|
+
].join('\n');
|
|
616
660
|
}
|
|
617
|
-
return lines.join('\n');
|
|
618
661
|
}
|
package/dist/formatters.js
CHANGED
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
//
|
|
3
3
|
// Pure functions — no side effects, no state. Each takes structured data
|
|
4
4
|
// and returns a formatted string for the tool response.
|
|
5
|
-
import { DEFAULT_STALE_DAYS_STANDARD, DEFAULT_STALE_DAYS_PREFERENCES, DEFAULT_MAX_STALE_IN_BRIEFING, DEFAULT_MAX_DEDUP_SUGGESTIONS, DEFAULT_MAX_CONFLICT_PAIRS, MAX_FOOTER_TAGS, } from './thresholds.js';
|
|
5
|
+
import { DEFAULT_STALE_DAYS_STANDARD, DEFAULT_STALE_DAYS_PREFERENCES, DEFAULT_MAX_STALE_IN_BRIEFING, DEFAULT_MAX_DEDUP_SUGGESTIONS, DEFAULT_MAX_CONFLICT_PAIRS, MAX_FOOTER_TAGS, WARN_SEPARATOR, } from './thresholds.js';
|
|
6
6
|
import { analyzeFilterGroups } from './text-analyzer.js';
|
|
7
7
|
/** Format the stale entries section for briefing/context responses */
|
|
8
8
|
export function formatStaleSection(staleDetails) {
|
|
@@ -17,23 +17,33 @@ export function formatStaleSection(staleDetails) {
|
|
|
17
17
|
}
|
|
18
18
|
/** Format the conflict detection warning for query/context responses */
|
|
19
19
|
export function formatConflictWarning(conflicts) {
|
|
20
|
-
const lines = [
|
|
20
|
+
const lines = [
|
|
21
|
+
WARN_SEPARATOR,
|
|
22
|
+
'⚠ CONFLICTING ENTRIES DETECTED — ACTION NEEDED ⚠',
|
|
23
|
+
WARN_SEPARATOR,
|
|
24
|
+
];
|
|
21
25
|
for (const c of conflicts) {
|
|
22
|
-
lines.push(`
|
|
23
|
-
lines.push(` vs
|
|
24
|
-
lines.push(`
|
|
25
|
-
|
|
26
|
+
lines.push(` ${c.a.id}: "${c.a.title}" (confidence: ${c.a.confidence}, ${c.a.created.substring(0, 10)})`);
|
|
27
|
+
lines.push(` vs`);
|
|
28
|
+
lines.push(` ${c.b.id}: "${c.b.title}" (confidence: ${c.b.confidence}, ${c.b.created.substring(0, 10)})`);
|
|
29
|
+
lines.push(` Similarity: ${(c.similarity * 100).toFixed(0)}%`);
|
|
30
|
+
lines.push('');
|
|
31
|
+
// Pre-fill which entry to delete so the agent can act immediately.
|
|
26
32
|
if (c.a.confidence !== c.b.confidence) {
|
|
27
|
-
const
|
|
28
|
-
|
|
33
|
+
const keep = c.a.confidence > c.b.confidence ? c.a : c.b;
|
|
34
|
+
const remove = c.a.confidence > c.b.confidence ? c.b : c.a;
|
|
35
|
+
lines.push(` Trust ${keep.id} (higher confidence). Delete the lower-confidence entry:`);
|
|
36
|
+
lines.push(` memory_correct(id: "${remove.id}", action: "delete")`);
|
|
29
37
|
}
|
|
30
38
|
else {
|
|
31
|
-
const
|
|
32
|
-
|
|
39
|
+
const keep = c.a.created > c.b.created ? c.a : c.b;
|
|
40
|
+
const remove = c.a.created > c.b.created ? c.b : c.a;
|
|
41
|
+
lines.push(` ${keep.id} is more recent — may supersede ${remove.id}:`);
|
|
42
|
+
lines.push(` memory_correct(id: "${remove.id}", action: "delete")`);
|
|
33
43
|
}
|
|
44
|
+
lines.push('');
|
|
34
45
|
}
|
|
35
|
-
lines.push(
|
|
36
|
-
lines.push('Consider: memory_correct to consolidate or clarify the difference between these entries.');
|
|
46
|
+
lines.push(WARN_SEPARATOR);
|
|
37
47
|
return lines.join('\n');
|
|
38
48
|
}
|
|
39
49
|
/** Format memory stats for a single lobe or global store */
|