@exaudeus/memory-mcp 1.7.0 → 1.9.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/store.d.ts CHANGED
@@ -1,4 +1,4 @@
1
- import type { MemoryEntry, TopicScope, TrustLevel, DetailLevel, DurabilityDecision, QueryResult, StoreResult, CorrectResult, MemoryStats, BriefingResult, ConflictPair, MemoryConfig } from './types.js';
1
+ import type { MemoryEntry, TopicScope, TrustLevel, DetailLevel, DurabilityDecision, QueryResult, StoreResult, CorrectResult, MemoryStats, ReEmbedResult, BriefingResult, ConflictPair, MemoryConfig } from './types.js';
2
2
  import type { ScoredEntry } from './ranking.js';
3
3
  export declare class MarkdownMemoryStore {
4
4
  private readonly config;
@@ -13,6 +13,14 @@ export declare class MarkdownMemoryStore {
13
13
  /** Resolved behavior thresholds — user config merged over defaults.
14
14
  * Centralizes threshold resolution so every caller gets the same value. */
15
15
  private get behavior();
16
+ /** Whether an embedder is configured — for mode indicator display. Read-only. */
17
+ get hasEmbedder(): boolean;
18
+ /** Count of vectorized entries — lightweight, no disk reload.
19
+ * For mode indicator display. Use stats() for full diagnostics. */
20
+ get vectorCount(): number;
21
+ /** Count of total entries — lightweight, no disk reload.
22
+ * For mode indicator display. Use stats() for full diagnostics. */
23
+ get entryCount(): number;
16
24
  /** Initialize the store: create memory dir and load existing entries */
17
25
  init(): Promise<void>;
18
26
  /** Store a new knowledge entry */
@@ -26,6 +34,11 @@ export declare class MarkdownMemoryStore {
26
34
  hasEntry(id: string): Promise<boolean>;
27
35
  /** Correct an existing entry */
28
36
  correct(id: string, correction: string, action: 'append' | 'replace' | 'delete'): Promise<CorrectResult>;
37
+ /** Re-embed all entries that don't have vectors.
38
+ * Idempotent: entries already in the vectors map are skipped.
39
+ * Early-exit: if the first embed fails, returns immediately (avoids burning through
40
+ * all entries just to discover the embedder is unavailable). */
41
+ reEmbed(): Promise<ReEmbedResult>;
29
42
  /** Get memory health statistics */
30
43
  stats(): Promise<MemoryStats>;
31
44
  /** Bootstrap: scan repo structure and seed initial knowledge */
package/dist/store.js CHANGED
@@ -7,7 +7,7 @@ import crypto from 'crypto';
7
7
  import { execFile } from 'child_process';
8
8
  import { promisify } from 'util';
9
9
  import { DEFAULT_CONFIDENCE, realClock, parseTopicScope, parseTrustLevel, parseTags, asEmbeddingVector } from './types.js';
10
- import { DEDUP_SIMILARITY_THRESHOLD, DEDUP_SEMANTIC_THRESHOLD, SEMANTIC_MIN_SIMILARITY, CONFLICT_SIMILARITY_THRESHOLD_SAME_TOPIC, CONFLICT_SIMILARITY_THRESHOLD_CROSS_TOPIC, CONFLICT_MIN_CONTENT_CHARS, OPPOSITION_PAIRS, PREFERENCE_SURFACE_THRESHOLD, 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, } from './thresholds.js';
10
+ import { DEDUP_SIMILARITY_THRESHOLD, DEDUP_SEMANTIC_THRESHOLD, SEMANTIC_MIN_SIMILARITY, CONFLICT_SIMILARITY_THRESHOLD_SAME_TOPIC, CONFLICT_SIMILARITY_THRESHOLD_CROSS_TOPIC, CONFLICT_MIN_CONTENT_CHARS, OPPOSITION_PAIRS, PREFERENCE_SURFACE_THRESHOLD, TOPIC_BOOST, MODULE_TOPIC_BOOST, USER_ALWAYS_INCLUDE_SCORE_FRACTION, QUERY_EMBED_TIMEOUT_MS, 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, } from './thresholds.js';
11
11
  import { realGitService } from './git-service.js';
12
12
  import { extractKeywords, similarity, cosineSimilarity, matchesFilter, computeRelevanceScore, } from './text-analyzer.js';
13
13
  import { keywordRank, semanticRank, mergeRankings } from './ranking.js';
@@ -38,6 +38,20 @@ export class MarkdownMemoryStore {
38
38
  maxConflictPairs: b.maxConflictPairs ?? DEFAULT_MAX_CONFLICT_PAIRS,
39
39
  };
40
40
  }
41
+ /** Whether an embedder is configured — for mode indicator display. Read-only. */
42
+ get hasEmbedder() {
43
+ return this.embedder !== null;
44
+ }
45
+ /** Count of vectorized entries — lightweight, no disk reload.
46
+ * For mode indicator display. Use stats() for full diagnostics. */
47
+ get vectorCount() {
48
+ return this.vectors.size;
49
+ }
50
+ /** Count of total entries — lightweight, no disk reload.
51
+ * For mode indicator display. Use stats() for full diagnostics. */
52
+ get entryCount() {
53
+ return this.entries.size;
54
+ }
41
55
  /** Initialize the store: create memory dir and load existing entries */
42
56
  async init() {
43
57
  await fs.mkdir(this.memoryPath, { recursive: true });
@@ -336,6 +350,44 @@ export class MarkdownMemoryStore {
336
350
  }
337
351
  return { corrected: true, id, action, newConfidence: 1.0, trust: 'user' };
338
352
  }
353
+ /** Re-embed all entries that don't have vectors.
354
+ * Idempotent: entries already in the vectors map are skipped.
355
+ * Early-exit: if the first embed fails, returns immediately (avoids burning through
356
+ * all entries just to discover the embedder is unavailable). */
357
+ async reEmbed() {
358
+ if (!this.embedder) {
359
+ return { embedded: 0, skipped: 0, failed: 0, error: 'No embedder configured' };
360
+ }
361
+ await this.reloadFromDisk();
362
+ // Probe: try embedding a short text to check availability before iterating
363
+ const probe = await this.embedder.embed('probe');
364
+ if (!probe.ok) {
365
+ return { embedded: 0, skipped: 0, failed: 0, error: `Embedder unavailable: ${probe.failure.kind}` };
366
+ }
367
+ let embedded = 0;
368
+ let skipped = 0;
369
+ let failed = 0;
370
+ for (const entry of this.entries.values()) {
371
+ // Entry already has a vector with correct dimensions — skip
372
+ if (this.vectors.has(entry.id)) {
373
+ skipped++;
374
+ continue;
375
+ }
376
+ // Embed the entry
377
+ const embedText = `${entry.title}\n\n${entry.content}`;
378
+ const result = await this.embedder.embed(embedText);
379
+ if (result.ok) {
380
+ const file = this.entryToRelativePath(entry);
381
+ await this.persistVector(file, result.vector);
382
+ this.vectors.set(entry.id, result.vector);
383
+ embedded++;
384
+ }
385
+ else {
386
+ failed++;
387
+ }
388
+ }
389
+ return { embedded, skipped, failed };
390
+ }
339
391
  /** Get memory health statistics */
340
392
  async stats() {
341
393
  await this.reloadFromDisk();
@@ -368,6 +420,7 @@ export class MarkdownMemoryStore {
368
420
  return {
369
421
  totalEntries: allEntries.length,
370
422
  corruptFiles: this.corruptFileCount,
423
+ vectorCount: this.vectors.size,
371
424
  byTopic, byTrust, byFreshness, byTag,
372
425
  storageSize: this.formatBytes(storageSize ?? 0),
373
426
  storageBudgetBytes: this.config.storageBudgetBytes,
@@ -483,7 +536,8 @@ export class MarkdownMemoryStore {
483
536
  const debug = process.env.MEMORY_MCP_DEBUG === '1';
484
537
  let semanticResults = [];
485
538
  if (this.embedder) {
486
- const queryResult = await this.embedder.embed(context);
539
+ const querySignal = AbortSignal.timeout(QUERY_EMBED_TIMEOUT_MS);
540
+ const queryResult = await this.embedder.embed(context, querySignal);
487
541
  if (queryResult.ok) {
488
542
  // In debug mode, get ALL scores (threshold=0) for calibration logging
489
543
  const rawSemanticResults = semanticRank(allEntries, this.vectors, queryResult.vector, debug ? 0 : SEMANTIC_MIN_SIMILARITY, ctx);
@@ -60,3 +60,15 @@ export declare function matchesFilter(allKeywords: Set<string>, filter: string,
60
60
  * Title matches get 2x weight over content-only matches.
61
61
  * Tag and exact matches count as full-weight hits (same as title). */
62
62
  export declare function computeRelevanceScore(titleKeywords: Set<string>, contentKeywords: Set<string>, confidence: number, filter: string, tags?: readonly string[]): number;
63
+ /** Extract a title and content from a single observation string.
64
+ * Title: first sentence (terminated by . ! ? or newline), capped at MAX_TITLE_LENGTH.
65
+ * Content: the full observation text (title is a derived label, not subtracted).
66
+ *
67
+ * Abbreviation-safe: requires 2+ word characters before the period to avoid
68
+ * splitting on "e.g.", "U.S.", "i.e.", etc.
69
+ *
70
+ * Pure function — no I/O, no side effects. */
71
+ export declare function extractTitle(observation: string): {
72
+ readonly title: string;
73
+ readonly content: string;
74
+ };
@@ -275,3 +275,31 @@ export function computeRelevanceScore(titleKeywords, contentKeywords, confidence
275
275
  }
276
276
  return bestScore * confidence;
277
277
  }
278
+ /** Maximum title length before truncation */
279
+ const MAX_TITLE_LENGTH = 80;
280
+ /** Extract a title and content from a single observation string.
281
+ * Title: first sentence (terminated by . ! ? or newline), capped at MAX_TITLE_LENGTH.
282
+ * Content: the full observation text (title is a derived label, not subtracted).
283
+ *
284
+ * Abbreviation-safe: requires 2+ word characters before the period to avoid
285
+ * splitting on "e.g.", "U.S.", "i.e.", etc.
286
+ *
287
+ * Pure function — no I/O, no side effects. */
288
+ export function extractTitle(observation) {
289
+ const trimmed = observation.trim();
290
+ if (trimmed.length === 0)
291
+ return { title: '', content: '' };
292
+ // Find first sentence boundary.
293
+ // The lookbehind (?<=\w{2}) ensures we don't split on abbreviations like "e.g." or "U.S."
294
+ // where a period follows a single character. Newlines always end a sentence.
295
+ const sentenceMatch = trimmed.match(/(?<=\w{2})[.!?](?:\s|$)|\n/);
296
+ const sentenceEnd = sentenceMatch?.index ?? -1;
297
+ const firstSentence = sentenceEnd >= 0
298
+ ? trimmed.slice(0, sentenceEnd + 1).trim()
299
+ : trimmed;
300
+ // Truncate if too long
301
+ const title = firstSentence.length <= MAX_TITLE_LENGTH
302
+ ? firstSentence
303
+ : firstSentence.slice(0, MAX_TITLE_LENGTH - 3).trimEnd() + '...';
304
+ return { title, content: trimmed };
305
+ }
@@ -30,6 +30,10 @@ export declare const SEMANTIC_MIN_SIMILARITY = 0.45;
30
30
  * disruptive than missing real ones. Two entries must be quite similar to be
31
31
  * flagged as potential duplicates. */
32
32
  export declare const DEDUP_SEMANTIC_THRESHOLD = 0.8;
33
+ /** Query-time embed timeout — tighter than store-time (5s) for responsiveness.
34
+ * Model-warm latency is ~10ms; 2s covers machine-under-load with margin.
35
+ * Cold starts handled by LazyEmbedder's probe (which uses the full 5s). */
36
+ export declare const QUERY_EMBED_TIMEOUT_MS = 2000;
33
37
  /** Score multiplier when a reference path basename matches the context keywords. */
34
38
  export declare const REFERENCE_BOOST_MULTIPLIER = 1.3;
35
39
  /** Per-topic scoring boost factors for contextSearch().
@@ -56,6 +56,10 @@ export const SEMANTIC_MIN_SIMILARITY = 0.45;
56
56
  * disruptive than missing real ones. Two entries must be quite similar to be
57
57
  * flagged as potential duplicates. */
58
58
  export const DEDUP_SEMANTIC_THRESHOLD = 0.80;
59
+ /** Query-time embed timeout — tighter than store-time (5s) for responsiveness.
60
+ * Model-warm latency is ~10ms; 2s covers machine-under-load with margin.
61
+ * Cold starts handled by LazyEmbedder's probe (which uses the full 5s). */
62
+ export const QUERY_EMBED_TIMEOUT_MS = 2000;
59
63
  /** Score multiplier when a reference path basename matches the context keywords. */
60
64
  export const REFERENCE_BOOST_MULTIPLIER = 1.30;
61
65
  /** Per-topic scoring boost factors for contextSearch().
package/dist/types.d.ts CHANGED
@@ -7,7 +7,7 @@ export type EphemeralSeverity = 'high' | 'medium' | 'low';
7
7
  /** Parse a raw string into a TrustLevel, returning null for invalid input */
8
8
  export declare function parseTrustLevel(raw: string): TrustLevel | null;
9
9
  /** Predefined topic scopes for organizing knowledge */
10
- export type TopicScope = 'user' | 'preferences' | 'architecture' | 'conventions' | 'gotchas' | 'recent-work' | `modules/${string}`;
10
+ export type TopicScope = 'user' | 'preferences' | 'architecture' | 'conventions' | 'gotchas' | 'general' | 'recent-work' | `modules/${string}`;
11
11
  /** Validated tag: lowercase alphanumeric slug (letters, digits, hyphens).
12
12
  * Branded type prevents accidentally passing raw strings where validated tags are expected. */
13
13
  export type Tag = string & {
@@ -156,6 +156,7 @@ export type CorrectResult = {
156
156
  export interface MemoryStats {
157
157
  readonly totalEntries: number;
158
158
  readonly corruptFiles: number;
159
+ readonly vectorCount: number;
159
160
  readonly byTopic: Record<string, number>;
160
161
  readonly byTrust: Record<TrustLevel, number>;
161
162
  readonly byFreshness: {
@@ -222,6 +223,24 @@ export interface BehaviorConfig {
222
223
  /** Maximum conflict pairs shown per query/context response. Default: 2. Range: 1–5. */
223
224
  readonly maxConflictPairs?: number;
224
225
  }
226
+ /** Supported embedding providers — closed union for exhaustive handling. */
227
+ export type EmbedderProvider = 'ollama' | 'none';
228
+ /** Embedding configuration from memory-config.json "embedder" block.
229
+ * All fields optional except provider — defaults are sensible for nomic-embed-text on localhost. */
230
+ export interface EmbedderConfig {
231
+ readonly provider: EmbedderProvider;
232
+ readonly model?: string;
233
+ readonly baseUrl?: string;
234
+ readonly timeoutMs?: number;
235
+ readonly dimensions?: number;
236
+ }
237
+ /** Result of a re-embed operation */
238
+ export interface ReEmbedResult {
239
+ readonly embedded: number;
240
+ readonly skipped: number;
241
+ readonly failed: number;
242
+ readonly error?: string;
243
+ }
225
244
  /** Configuration for the memory MCP */
226
245
  export interface MemoryConfig {
227
246
  readonly repoRoot: string;
package/dist/types.js CHANGED
@@ -9,7 +9,7 @@ const TRUST_LEVELS = ['user', 'agent-confirmed', 'agent-inferred'];
9
9
  export function parseTrustLevel(raw) {
10
10
  return TRUST_LEVELS.includes(raw) ? raw : null;
11
11
  }
12
- const FIXED_TOPICS = ['user', 'preferences', 'architecture', 'conventions', 'gotchas', 'recent-work'];
12
+ const FIXED_TOPICS = ['user', 'preferences', 'architecture', 'conventions', 'gotchas', 'general', 'recent-work'];
13
13
  /** Construct an EmbeddingVector from a Float32Array. Boundary validation only —
14
14
  * callers (OllamaEmbedder, FakeEmbedder, vector deserialization) validate dimensions. */
15
15
  export function asEmbeddingVector(raw) {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@exaudeus/memory-mcp",
3
- "version": "1.7.0",
3
+ "version": "1.9.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",