@mrxkun/mcfast-mcp 4.1.10 → 4.1.12

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.
@@ -26,6 +26,9 @@ import { CuratedMemory } from './layers/curated-memory.js';
26
26
  // Bootstrap
27
27
  import { AgentsMdBootstrap } from './bootstrap/agents-md.js';
28
28
 
29
+ // Project Analysis (AI-powered)
30
+ import { execute as projectAnalyzeExecute } from '../tools/project_analyze.js';
31
+
29
32
  // Embedders
30
33
  import { UltraEnhancedEmbedder } from './utils/ultra-embedder.js';
31
34
  import { SmartRouter } from './utils/smart-router.js';
@@ -38,6 +41,7 @@ import { PatternDetector, SuggestionEngine, StrategySelector } from '../intellig
38
41
  export class MemoryEngine {
39
42
  static instance = null;
40
43
  static instancePromise = null;
44
+ static instancePid = null; // Track which process owns the instance
41
45
 
42
46
  /**
43
47
  * Get singleton instance of MemoryEngine
@@ -45,8 +49,19 @@ export class MemoryEngine {
45
49
  * @returns {MemoryEngine} Singleton instance
46
50
  */
47
51
  static getInstance(options = {}) {
52
+ const currentPid = process.pid;
53
+
54
+ // Check if instance belongs to current process
55
+ if (MemoryEngine.instancePid !== currentPid) {
56
+ // Different process - reset instance
57
+ MemoryEngine.instance = null;
58
+ MemoryEngine.instancePromise = null;
59
+ MemoryEngine.instancePid = null;
60
+ }
61
+
48
62
  if (!MemoryEngine.instance) {
49
63
  MemoryEngine.instance = new MemoryEngine(options);
64
+ MemoryEngine.instancePid = currentPid;
50
65
  }
51
66
  return MemoryEngine.instance;
52
67
  }
@@ -58,6 +73,16 @@ export class MemoryEngine {
58
73
  * @returns {Promise<MemoryEngine>} Initialized singleton instance
59
74
  */
60
75
  static async getOrCreate(projectPath, options = {}) {
76
+ const currentPid = process.pid;
77
+
78
+ // Check if instance belongs to current process
79
+ if (MemoryEngine.instancePid !== currentPid) {
80
+ // Different process - reset instance
81
+ MemoryEngine.instance = null;
82
+ MemoryEngine.instancePromise = null;
83
+ MemoryEngine.instancePid = null;
84
+ }
85
+
61
86
  if (MemoryEngine.instance && MemoryEngine.instance.isInitialized) {
62
87
  return MemoryEngine.instance;
63
88
  }
@@ -68,6 +93,7 @@ export class MemoryEngine {
68
93
 
69
94
  const engine = new MemoryEngine(options);
70
95
  MemoryEngine.instance = engine;
96
+ MemoryEngine.instancePid = currentPid;
71
97
  MemoryEngine.instancePromise = engine.initialize(projectPath).then(() => engine);
72
98
 
73
99
  return MemoryEngine.instancePromise;
@@ -131,6 +157,9 @@ export class MemoryEngine {
131
157
  this.suggestionEngine = null;
132
158
  this.strategySelector = null;
133
159
 
160
+ // Auto-analyze project on first use
161
+ this.autoAnalyze = options.autoAnalyze !== false;
162
+
134
163
  // Search configuration
135
164
  this.searchConfig = {
136
165
  hybrid: {
@@ -196,19 +225,32 @@ export class MemoryEngine {
196
225
  });
197
226
  }
198
227
 
199
- // Initialize file watcher
200
- this.watcher = new FileWatcher(projectPath, this, {
201
- debounceMs: 1500,
202
- ignored: [
203
- '**/node_modules/**',
204
- '**/.git/**',
205
- '**/dist/**',
206
- '**/build/**',
207
- '**/.mcfast/**',
208
- '**/*.log'
209
- ]
210
- });
211
- await this.watcher.start();
228
+ // Initialize file watcher (configurable via MCFAST_FILE_WATCHER env)
229
+ const ENABLE_FILE_WATCHER = process.env.MCFAST_FILE_WATCHER !== 'false';
230
+
231
+ if (ENABLE_FILE_WATCHER) {
232
+ this.watcher = new FileWatcher(projectPath, this, {
233
+ debounceMs: 1500,
234
+ ignored: [
235
+ '**/node_modules/**',
236
+ '**/.git/**',
237
+ '**/dist/**',
238
+ '**/build/**',
239
+ '**/.mcfast/**',
240
+ '**/*.log'
241
+ ]
242
+ });
243
+
244
+ try {
245
+ await this.watcher.start();
246
+ } catch (watcherError) {
247
+ console.error('[MemoryEngine] ⚠️ File watcher failed to start (continuing without file watching):', watcherError.message);
248
+ this.watcher = null;
249
+ }
250
+ } else {
251
+ console.error('[MemoryEngine] File watcher disabled (MCFAST_FILE_WATCHER=false)');
252
+ this.watcher = null;
253
+ }
212
254
 
213
255
  // Perform initial scan
214
256
  await this.performInitialScan();
@@ -223,6 +265,9 @@ export class MemoryEngine {
223
265
  console.error(`[MemoryEngine] Smart Routing: ${this.smartRoutingEnabled ? 'ENABLED' : 'DISABLED'}`);
224
266
  console.error(`[MemoryEngine] Intelligence: ${this.intelligenceEnabled ? 'ENABLED' : 'DISABLED'}`);
225
267
 
268
+ // Auto-analyze project if MCFAST_TOKEN is set
269
+ await this.autoAnalyzeProject();
270
+
226
271
  // Log initialization to daily logs
227
272
  await this.dailyLogs.log('Memory Engine Initialized', `Project: ${projectPath}`, {
228
273
  stats: this.getStats()
@@ -264,6 +309,47 @@ export class MemoryEngine {
264
309
  }
265
310
  }
266
311
 
312
+ /**
313
+ * Auto-analyze project using AI if MCFAST_TOKEN is set
314
+ * Runs in background to not block initialization
315
+ */
316
+ async autoAnalyzeProject() {
317
+ if (!this.autoAnalyze) return;
318
+
319
+ const apiKey = process.env.MCFAST_TOKEN;
320
+ if (!apiKey) {
321
+ console.error('[MemoryEngine] Skipping auto-analysis (no MCFAST_TOKEN)');
322
+ return;
323
+ }
324
+
325
+ // Check if already analyzed (MEMORY.md has Project Context section)
326
+ try {
327
+ const memoryContent = await this.curatedMemory.read();
328
+ if (memoryContent && memoryContent.includes('## Project Context') &&
329
+ !memoryContent.includes('<!-- Add project context here -->')) {
330
+ console.error('[MemoryEngine] Project already analyzed, skipping auto-analysis');
331
+ return;
332
+ }
333
+ } catch (e) {
334
+ // MEMORY.md doesn't exist yet, proceed with analysis
335
+ }
336
+
337
+ // Run analysis in background
338
+ console.error('[MemoryEngine] 🔄 Starting auto-analysis in background...');
339
+
340
+ projectAnalyzeExecute({ force: false, updateMemory: true })
341
+ .then(result => {
342
+ if (result.metadata?.memoryUpdated) {
343
+ console.error('[MemoryEngine] ✅ Auto-analysis complete, MEMORY.md updated');
344
+ } else if (result.isError) {
345
+ console.error('[MemoryEngine] ⚠️ Auto-analysis failed:', result.content?.[0]?.text?.substring(0, 100));
346
+ }
347
+ })
348
+ .catch(err => {
349
+ console.error('[MemoryEngine] ⚠️ Auto-analysis error:', err.message);
350
+ });
351
+ }
352
+
267
353
  async performInitialScan() {
268
354
  if (this.isScanning) return;
269
355
  this.isScanning = true;
@@ -470,24 +556,44 @@ export class MemoryEngine {
470
556
 
471
557
  // ========== Storage Operations ==========
472
558
 
473
- async storeIndexed(indexed) {
474
- this.codebaseDb?.upsertFile?.(indexed.file);
475
- this.codebaseDb?.deleteFactsByFile?.(indexed.file.id);
476
- this.codebaseDb?.deleteChunksByFile?.(indexed.file.id);
559
+ async storeIndexed(indexed, maxRetries = 3) {
560
+ // Retry logic with exponential backoff
561
+ for (let attempt = 1; attempt <= maxRetries; attempt++) {
562
+ try {
563
+ this.codebaseDb?.upsertFile?.(indexed.file);
564
+ this.codebaseDb?.deleteFactsByFile?.(indexed.file.id);
565
+ this.codebaseDb?.deleteChunksByFile?.(indexed.file.id);
477
566
 
478
- for (const fact of indexed.facts) {
479
- this.codebaseDb?.insertFact?.(fact);
480
- }
567
+ for (const fact of indexed.facts) {
568
+ this.codebaseDb?.insertFact?.(fact);
569
+ }
481
570
 
482
- for (const chunk of indexed.chunks) {
483
- this.codebaseDb?.insertChunk?.(chunk);
484
- }
571
+ for (const chunk of indexed.chunks) {
572
+ this.codebaseDb?.insertChunk?.(chunk);
573
+ }
485
574
 
486
- for (const embedding of indexed.embeddings) {
487
- this.codebaseDb?.insertEmbedding?.(embedding);
575
+ for (const embedding of indexed.embeddings) {
576
+ this.codebaseDb?.insertEmbedding?.(embedding);
577
+ }
578
+
579
+ return true; // Success
580
+ } catch (error) {
581
+ if (attempt === maxRetries) {
582
+ console.error(`[MemoryEngine] Failed to store indexed data after ${maxRetries} attempts:`, error.message);
583
+ this.codebaseDb?.recordFailedIndex?.(indexed.file.path, error.message);
584
+ return false;
585
+ }
586
+ // Exponential backoff: 100ms, 200ms, 400ms
587
+ await new Promise(resolve => setTimeout(resolve, 100 * Math.pow(2, attempt - 1)));
588
+ }
488
589
  }
489
590
  }
490
591
 
592
+ // Record failed indexing for tracking
593
+ recordFailedIndex(filePath, errorMessage) {
594
+ console.error(`[MemoryEngine] Indexing failed for ${filePath}: ${errorMessage}`);
595
+ }
596
+
491
597
  // ========== Search Operations ==========
492
598
 
493
599
  async searchFacts(query, limit = 20) {
@@ -573,6 +679,80 @@ export class MemoryEngine {
573
679
  }
574
680
 
575
681
  async searchCodebase(query, limit = 20) {
682
+ const startTime = performance.now();
683
+
684
+ // FIX: Get embeddings from both codebaseDb AND memoryDb
685
+ const [codebaseEmbeddings, memoryEmbeddings] = await Promise.all([
686
+ this.codebaseDb?.getAllEmbeddings?.() || [],
687
+ this.memoryDb?.getAllEmbeddings?.() || []
688
+ ]);
689
+
690
+ // Generate query embedding
691
+ const queryResult = this.embedder.embedCode(query);
692
+ const queryEmbedding = queryResult.vector;
693
+
694
+ // Score and tag results from each source
695
+ const scoreFn = (item) => {
696
+ const buf = item.embedding?.buffer || item.embedding;
697
+ const arr = buf ? new Uint8Array(buf) : new Uint8Array(0);
698
+ return this.embedder.cosineSimilarity(queryEmbedding, Array.from(arr));
699
+ };
700
+
701
+ const codebaseResults = (codebaseEmbeddings || []).map(item => ({
702
+ ...item, similarity: scoreFn(item), source: 'codebase', type: 'code'
703
+ }));
704
+
705
+ const memoryResults = (memoryEmbeddings || []).map(item => ({
706
+ ...item, similarity: scoreFn(item), source: 'memory', type: 'memory'
707
+ }));
708
+
709
+ // Combine results
710
+ let combined = [...codebaseResults, ...memoryResults];
711
+
712
+ // Also get FTS results from both databases
713
+ const [codebaseFts, memoryFts] = await Promise.all([
714
+ this.codebaseDb?.searchFTS?.(query, limit * 2) || { results: [] },
715
+ this.memoryDb?.searchFTS?.(query, limit * 2) || { results: [] }
716
+ ]);
717
+
718
+ // Add FTS results
719
+ for (const r of codebaseFts.results || []) {
720
+ combined.push({ ...r, source: 'fts', type: 'code', score: r.score });
721
+ }
722
+ for (const r of memoryFts.results || []) {
723
+ combined.push({ ...r, source: 'fts', type: 'memory', score: r.score });
724
+ }
725
+
726
+ // Deduplicate and sort
727
+ const seen = new Map();
728
+ for (const r of combined) {
729
+ const key = r.chunk_id || r.id;
730
+ if (!seen.has(key) || (r.similarity || r.score || 0) > (seen.get(key).similarity || seen.get(key).score || 0)) {
731
+ seen.set(key, r);
732
+ }
733
+ }
734
+
735
+ const finalResults = Array.from(seen.values())
736
+ .sort((a, b) => (b.similarity || b.score || 0) - (a.similarity || a.score || 0))
737
+ .slice(0, limit);
738
+
739
+ const duration = performance.now() - startTime;
740
+
741
+ return {
742
+ results: finalResults,
743
+ metadata: {
744
+ vectorCandidates: codebaseResults.length + memoryResults.length,
745
+ ftsCandidates: (codebaseFts.results?.length || 0) + (memoryFts.results?.length || 0),
746
+ totalCandidates: combined.length,
747
+ codebaseResults: codebaseResults.length,
748
+ memoryResults: memoryResults.length,
749
+ duration: duration.toFixed(2) + 'ms'
750
+ }
751
+ };
752
+ }
753
+
754
+ // Legacy search - kept for backward compatibility
755
+ async _searchCodebaseLegacy(query, limit = 20) {
576
756
  // Search codebase using existing methods
577
757
  const vectorResults = await this.searchVector(query, limit * 2);
578
758
  const ftsResults = this.codebaseDb?.searchFTS?.(query, limit * 2);
@@ -781,6 +961,33 @@ export class MemoryEngine {
781
961
  this.isInitialized = false;
782
962
  console.error('[MemoryEngine] Shutdown complete');
783
963
  }
964
+
965
+ async cleanup() {
966
+ // Alias for shutdown for consistency
967
+ return this.shutdown();
968
+ }
969
+ }
970
+
971
+ // Auto-register cleanup handlers for graceful shutdown
972
+ if (typeof process !== 'undefined') {
973
+ const cleanupHandler = async (signal) => {
974
+ console.error(`[MemoryEngine] Received ${signal}, performing cleanup...`);
975
+ if (MemoryEngine.instance) {
976
+ try {
977
+ await MemoryEngine.instance.shutdown();
978
+ } catch (error) {
979
+ console.error(`[MemoryEngine] Error during shutdown: ${error.message}`);
980
+ }
981
+ }
982
+ };
983
+
984
+ // Register handlers only once
985
+ if (!process._memoryEngineCleanupRegistered) {
986
+ process.on('beforeExit', () => cleanupHandler('beforeExit'));
987
+ process.on('SIGINT', () => cleanupHandler('SIGINT'));
988
+ process.on('SIGTERM', () => cleanupHandler('SIGTERM'));
989
+ process._memoryEngineCleanupRegistered = true;
990
+ }
784
991
  }
785
992
 
786
993
  export default MemoryEngine;
@@ -0,0 +1,223 @@
1
+ /**
2
+ * Base Database Class
3
+ * Common methods for MemoryDatabase and CodebaseDatabase
4
+ * Reduces code duplication between the two database classes
5
+ */
6
+
7
+ import Database from 'better-sqlite3';
8
+ import path from 'path';
9
+ import fs from 'fs/promises';
10
+
11
+ export class BaseDatabase {
12
+ constructor(dbPath = null, options = {}) {
13
+ this.dbPath = dbPath;
14
+ this.db = null;
15
+ this.isInitialized = false;
16
+ this.options = options;
17
+
18
+ // Logger (can be replaced with proper logger)
19
+ this.logger = options.logger || console;
20
+ }
21
+
22
+ async initialize() {
23
+ if (this.isInitialized) return;
24
+
25
+ await fs.mkdir(path.dirname(this.dbPath), { recursive: true });
26
+
27
+ this.db = new Database(this.dbPath);
28
+ this.db.pragma('journal_mode = WAL');
29
+
30
+ this.createTables();
31
+ this.isInitialized = true;
32
+
33
+ this._log(`Initialized at: ${this.dbPath}`);
34
+ }
35
+
36
+ /**
37
+ * Override in subclass to create specific tables
38
+ */
39
+ createTables() {
40
+ throw new Error('createTables must be implemented in subclass');
41
+ }
42
+
43
+ /**
44
+ * Common chunk operations
45
+ */
46
+ insertChunk(chunk) {
47
+ throw new Error('insertChunk must be implemented in subclass');
48
+ }
49
+
50
+ deleteChunksByFile(filePath) {
51
+ throw new Error('deleteChunksByFile must be implemented in subclass');
52
+ }
53
+
54
+ getChunksByFile(filePath, limit = 100) {
55
+ throw new Error('getChunksByFile must be implemented in subclass');
56
+ }
57
+
58
+ getRecentChunks(limit = 100) {
59
+ throw new Error('getRecentChunks must be implemented in subclass');
60
+ }
61
+
62
+ /**
63
+ * Common embedding operations
64
+ */
65
+ insertEmbedding(embedding) {
66
+ throw new Error('insertEmbedding must be implemented in subclass');
67
+ }
68
+
69
+ getEmbedding(chunkId) {
70
+ throw new Error('getEmbedding must be implemented in subclass');
71
+ }
72
+
73
+ getAllEmbeddings() {
74
+ throw new Error('getAllEmbeddings must be implemented in subclass');
75
+ }
76
+
77
+ /**
78
+ * Common search operations (FTS5)
79
+ */
80
+ searchFTS(query, limit = 20) {
81
+ const startTime = performance.now();
82
+
83
+ const stmt = this.db.prepare(`
84
+ SELECT
85
+ c.*,
86
+ rank as bm25_score
87
+ FROM chunks_fts fts
88
+ JOIN chunks c ON fts.rowid = c.id
89
+ WHERE chunks_fts MATCH ?
90
+ ORDER BY rank
91
+ LIMIT ?
92
+ `);
93
+
94
+ const results = stmt.all(query, limit);
95
+ const duration = performance.now() - startTime;
96
+
97
+ // Log search
98
+ this.logSearch(query, 'fts', results.length, duration);
99
+
100
+ return {
101
+ results: results.map(r => ({
102
+ ...r,
103
+ score: 1 / (1 + Math.max(0, r.bm25_score))
104
+ })),
105
+ metadata: {
106
+ method: 'fts5',
107
+ duration: duration.toFixed(2) + 'ms',
108
+ candidates: results.length
109
+ }
110
+ };
111
+ }
112
+
113
+ /**
114
+ * Common search history logging
115
+ */
116
+ logSearch(query, method, resultsCount, durationMs) {
117
+ try {
118
+ const stmt = this.db.prepare(`
119
+ INSERT INTO search_history (query, method, results_count, duration_ms, timestamp)
120
+ VALUES (?, ?, ?, ?, ?)
121
+ `);
122
+ stmt.run(query, method, resultsCount, Math.round(durationMs), Date.now());
123
+ } catch (error) {
124
+ // Silent fail - don't break search for logging
125
+ }
126
+ }
127
+
128
+ getSearchStats(days = 7) {
129
+ const since = Date.now() - (days * 24 * 60 * 60 * 1000);
130
+ return this.db.prepare(`
131
+ SELECT
132
+ method,
133
+ COUNT(*) as count,
134
+ AVG(duration_ms) as avg_duration,
135
+ AVG(results_count) as avg_results
136
+ FROM search_history
137
+ WHERE timestamp > ?
138
+ GROUP BY method
139
+ `).all(since);
140
+ }
141
+
142
+ /**
143
+ * Common file tracking operations
144
+ */
145
+ upsertFile(file) {
146
+ throw new Error('upsertFile must be implemented in subclass');
147
+ }
148
+
149
+ getFile(filePath) {
150
+ throw new Error('getFile must be implemented in subclass');
151
+ }
152
+
153
+ isFileIndexed(filePath, contentHash) {
154
+ const file = this.getFile(filePath);
155
+ return file && file.content_hash === contentHash;
156
+ }
157
+
158
+ deleteFile(filePath) {
159
+ throw new Error('deleteFile must be implemented in subclass');
160
+ }
161
+
162
+ /**
163
+ * Common stats
164
+ */
165
+ getStats() {
166
+ throw new Error('getStats must be implemented in subclass');
167
+ }
168
+
169
+ /**
170
+ * Maintenance
171
+ */
172
+ vacuum() {
173
+ this.db.exec('VACUUM');
174
+ this.db.exec('ANALYZE');
175
+ }
176
+
177
+ close() {
178
+ if (this.db) {
179
+ this.db.close();
180
+ this.isInitialized = false;
181
+ }
182
+ }
183
+
184
+ /**
185
+ * Internal logger
186
+ */
187
+ _log(message, level = 'info') {
188
+ const prefix = `[${this.constructor.name}]`;
189
+ if (level === 'error') {
190
+ console.error(`${prefix} ${message}`);
191
+ } else if (level === 'warn') {
192
+ console.warn(`${prefix} ${message}`);
193
+ } else {
194
+ console.error(`${prefix} ${message}`);
195
+ }
196
+ }
197
+
198
+ /**
199
+ * Safe execute with error handling
200
+ */
201
+ safeExecute(fn, errorMessage = 'Operation failed') {
202
+ try {
203
+ return fn();
204
+ } catch (error) {
205
+ this._log(`${errorMessage}: ${error.message}`, 'error');
206
+ return null;
207
+ }
208
+ }
209
+
210
+ /**
211
+ * Safe async execute with error handling
212
+ */
213
+ async safeExecuteAsync(fn, errorMessage = 'Operation failed') {
214
+ try {
215
+ return await fn();
216
+ } catch (error) {
217
+ this._log(`${errorMessage}: ${error.message}`, 'error');
218
+ return null;
219
+ }
220
+ }
221
+ }
222
+
223
+ export default BaseDatabase;
@@ -77,6 +77,7 @@ export class Chunker {
77
77
  id: this.generateChunkId(filePath, startLine),
78
78
  file_id: this.generateFileId(filePath),
79
79
  content: content,
80
+ content_hash: crypto.createHash('md5').update(content).digest('hex'),
80
81
  start_line: startLine,
81
82
  end_line: endLine,
82
83
  token_count: tokens