@mrxkun/mcfast-mcp 4.1.1 → 4.1.3

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/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@mrxkun/mcfast-mcp",
3
- "version": "4.1.1",
4
- "description": "Ultra-fast code editing with WASM acceleration, fuzzy patching, multi-layer caching, and 8 unified tools. Optimized for AI code assistants with 80-98% latency reduction. Phase 5: Memory v4.1.0 - Markdown source, Hybrid Search, Two-tier memory.",
3
+ "version": "4.1.3",
4
+ "description": "Ultra-fast code editing with WASM acceleration, fuzzy patching, multi-layer caching, and 8 unified tools. v4.1.3: Fixed MCP stream issues (console.log->stderr), Singleton pattern, Duplicate log prevention.",
5
5
  "type": "module",
6
6
  "bin": {
7
7
  "mcfast-mcp": "src/index.js"
@@ -11,6 +11,15 @@ export class DailyLogs {
11
11
  constructor(options = {}) {
12
12
  this.memoryPath = options.memoryPath || '.mcfast';
13
13
  this.logsDir = path.join(this.memoryPath, 'memory');
14
+
15
+ // Track recently logged entries to avoid duplicates
16
+ this.recentLogs = new Map();
17
+ this.dedupWindowMs = 5000; // 5 seconds
18
+
19
+ // Create directory immediately on construction
20
+ this.ensureDirectory().catch(() => {
21
+ // Silent fail - will retry when logging
22
+ });
14
23
  }
15
24
 
16
25
  /**
@@ -52,6 +61,24 @@ export class DailyLogs {
52
61
  * @param {Object} metadata - Optional metadata
53
62
  */
54
63
  async log(title, content, metadata = {}) {
64
+ // Check for duplicate within dedup window
65
+ const logKey = `${title}:${content.substring(0, 50)}`;
66
+ const now = Date.now();
67
+ const lastLog = this.recentLogs.get(logKey);
68
+
69
+ if (lastLog && (now - lastLog) < this.dedupWindowMs) {
70
+ console.error(`[DailyLogs] Skipping duplicate log: "${title}"`);
71
+ return null;
72
+ }
73
+
74
+ // Track this log
75
+ this.recentLogs.set(logKey, now);
76
+
77
+ // Cleanup old entries periodically
78
+ if (this.recentLogs.size > 100) {
79
+ this.cleanupRecentLogs();
80
+ }
81
+
55
82
  await this.ensureDirectory();
56
83
 
57
84
  const logPath = this.getTodayLogPath();
@@ -85,7 +112,7 @@ export class DailyLogs {
85
112
  // Append entry
86
113
  await fs.writeFile(logPath, fileContent + entry);
87
114
 
88
- console.log(`[DailyLogs] Logged: "${title}" to ${logPath}`);
115
+ console.error(`[DailyLogs] Logged: "${title}" to ${logPath}`);
89
116
  return logPath;
90
117
  }
91
118
 
@@ -231,6 +258,18 @@ export class DailyLogs {
231
258
  return false;
232
259
  }
233
260
  }
261
+
262
+ /**
263
+ * Cleanup old recent log entries
264
+ */
265
+ cleanupRecentLogs() {
266
+ const now = Date.now();
267
+ for (const [key, timestamp] of this.recentLogs.entries()) {
268
+ if (now - timestamp > this.dedupWindowMs) {
269
+ this.recentLogs.delete(key);
270
+ }
271
+ }
272
+ }
234
273
  }
235
274
 
236
275
  export default DailyLogs;
@@ -36,6 +36,43 @@ import { SyncEngine } from './utils/sync-engine.js';
36
36
  import { PatternDetector, SuggestionEngine, StrategySelector } from '../intelligence/index.js';
37
37
 
38
38
  export class MemoryEngine {
39
+ static instance = null;
40
+ static instancePromise = null;
41
+
42
+ /**
43
+ * Get singleton instance of MemoryEngine
44
+ * @param {Object} options - Configuration options
45
+ * @returns {MemoryEngine} Singleton instance
46
+ */
47
+ static getInstance(options = {}) {
48
+ if (!MemoryEngine.instance) {
49
+ MemoryEngine.instance = new MemoryEngine(options);
50
+ }
51
+ return MemoryEngine.instance;
52
+ }
53
+
54
+ /**
55
+ * Get or create singleton instance with async initialization
56
+ * @param {string} projectPath - Project path
57
+ * @param {Object} options - Configuration options
58
+ * @returns {Promise<MemoryEngine>} Initialized singleton instance
59
+ */
60
+ static async getOrCreate(projectPath, options = {}) {
61
+ if (MemoryEngine.instance && MemoryEngine.instance.isInitialized) {
62
+ return MemoryEngine.instance;
63
+ }
64
+
65
+ if (MemoryEngine.instancePromise) {
66
+ return MemoryEngine.instancePromise;
67
+ }
68
+
69
+ const engine = new MemoryEngine(options);
70
+ MemoryEngine.instance = engine;
71
+ MemoryEngine.instancePromise = engine.initialize(projectPath).then(() => engine);
72
+
73
+ return MemoryEngine.instancePromise;
74
+ }
75
+
39
76
  constructor(options = {}) {
40
77
  this.projectPath = null;
41
78
  this.isInitialized = false;
@@ -125,9 +162,9 @@ export class MemoryEngine {
125
162
  await fs.mkdir(this.memoryPath, { recursive: true });
126
163
  await fs.mkdir(this.indexPath, { recursive: true });
127
164
 
128
- console.log(`[MemoryEngine] Initializing...`);
129
- console.log(`[MemoryEngine] Project: ${projectPath}`);
130
- console.log(`[MemoryEngine] Memory path: ${this.memoryPath}`);
165
+ console.error(`[MemoryEngine] Initializing...`);
166
+ console.error(`[MemoryEngine] Project: ${projectPath}`);
167
+ console.error(`[MemoryEngine] Memory path: ${this.memoryPath}`);
131
168
 
132
169
  // Initialize bootstrap (AGENTS.md)
133
170
  this.agentsBootstrap = new AgentsMdBootstrap({ projectPath });
@@ -181,10 +218,10 @@ export class MemoryEngine {
181
218
 
182
219
  this.isInitialized = true;
183
220
 
184
- console.log(`[MemoryEngine] ✅ Initialized successfully`);
185
- console.log(`[MemoryEngine] Hybrid Search: ${this.searchConfig.hybrid.enabled ? 'ENABLED' : 'DISABLED'}`);
186
- console.log(`[MemoryEngine] Smart Routing: ${this.smartRoutingEnabled ? 'ENABLED' : 'DISABLED'}`);
187
- console.log(`[MemoryEngine] Intelligence: ${this.intelligenceEnabled ? 'ENABLED' : 'DISABLED'}`);
221
+ console.error(`[MemoryEngine] ✅ Initialized successfully`);
222
+ console.error(`[MemoryEngine] Hybrid Search: ${this.searchConfig.hybrid.enabled ? 'ENABLED' : 'DISABLED'}`);
223
+ console.error(`[MemoryEngine] Smart Routing: ${this.smartRoutingEnabled ? 'ENABLED' : 'DISABLED'}`);
224
+ console.error(`[MemoryEngine] Intelligence: ${this.intelligenceEnabled ? 'ENABLED' : 'DISABLED'}`);
188
225
 
189
226
  // Log initialization to daily logs
190
227
  await this.dailyLogs.log('Memory Engine Initialized', `Project: ${projectPath}`, {
@@ -200,7 +237,7 @@ export class MemoryEngine {
200
237
  this.suggestionEngine = new SuggestionEngine({ memoryEngine: this });
201
238
  this.strategySelector = new StrategySelector({ memoryEngine: this });
202
239
 
203
- console.log('[MemoryEngine] Intelligence engines initialized');
240
+ console.error('[MemoryEngine] Intelligence engines initialized');
204
241
 
205
242
  // Load models in background
206
243
  (async () => {
@@ -216,7 +253,7 @@ export class MemoryEngine {
216
253
  );
217
254
 
218
255
  await Promise.race([Promise.all(loadPromises), timeoutPromise]);
219
- console.log('[MemoryEngine] ✅ Intelligence models loaded');
256
+ console.error('[MemoryEngine] ✅ Intelligence models loaded');
220
257
  } catch (error) {
221
258
  console.warn('[MemoryEngine] Model loading failed or timed out:', error.message);
222
259
  }
@@ -231,7 +268,7 @@ export class MemoryEngine {
231
268
  if (this.isScanning) return;
232
269
  this.isScanning = true;
233
270
 
234
- console.log('[MemoryEngine] 🔍 Performing initial codebase scan...');
271
+ console.error('[MemoryEngine] 🔍 Performing initial codebase scan...');
235
272
 
236
273
  try {
237
274
  // Scan memory files first
@@ -243,8 +280,8 @@ export class MemoryEngine {
243
280
  // Update indexing progress
244
281
  this.codebaseDb?.completeIndexing?.();
245
282
 
246
- console.log('[MemoryEngine] ✅ Initial scan complete');
247
- console.log(`[MemoryEngine] Stats:`, this.getStats());
283
+ console.error('[MemoryEngine] ✅ Initial scan complete');
284
+ console.error(`[MemoryEngine] Stats:`, this.getStats());
248
285
 
249
286
  } catch (error) {
250
287
  console.error('[MemoryEngine] Error during initial scan:', error);
@@ -254,7 +291,7 @@ export class MemoryEngine {
254
291
  }
255
292
 
256
293
  async scanMemoryFiles() {
257
- console.log('[MemoryEngine] Scanning memory files...');
294
+ console.error('[MemoryEngine] Scanning memory files...');
258
295
 
259
296
  try {
260
297
  // Index MEMORY.md
@@ -272,24 +309,24 @@ export class MemoryEngine {
272
309
  }
273
310
  }
274
311
 
275
- console.log('[MemoryEngine] Memory files indexed');
312
+ console.error('[MemoryEngine] Memory files indexed');
276
313
  } catch (error) {
277
314
  console.error('[MemoryEngine] Error scanning memory files:', error);
278
315
  }
279
316
  }
280
317
 
281
318
  async scanCodebase() {
282
- console.log('[MemoryEngine] Scanning codebase...');
319
+ console.error('[MemoryEngine] Scanning codebase...');
283
320
 
284
321
  const extensions = ['.js', '.jsx', '.ts', '.tsx', '.py', '.java', '.go', '.rs', '.cpp', '.c', '.h'];
285
322
  const files = await this.findFiles(this.projectPath, extensions);
286
323
 
287
324
  if (files.length === 0) {
288
- console.log('[MemoryEngine] No code files found to index');
325
+ console.error('[MemoryEngine] No code files found to index');
289
326
  return;
290
327
  }
291
328
 
292
- console.log(`[MemoryEngine] Found ${files.length} files to index`);
329
+ console.error(`[MemoryEngine] Found ${files.length} files to index`);
293
330
 
294
331
  // Start indexing progress
295
332
  this.codebaseDb?.startIndexing?.(files.length);
@@ -313,7 +350,7 @@ export class MemoryEngine {
313
350
 
314
351
  indexed++;
315
352
  if (indexed % 10 === 0) {
316
- console.log(`[MemoryEngine] Indexed ${indexed}/${files.length} files...`);
353
+ console.error(`[MemoryEngine] Indexed ${indexed}/${files.length} files...`);
317
354
  }
318
355
  } catch (error) {
319
356
  console.warn(`[MemoryEngine] Failed to index ${filePath}:`, error.message);
@@ -321,7 +358,7 @@ export class MemoryEngine {
321
358
  }
322
359
  }
323
360
 
324
- console.log(`[MemoryEngine] Codebase scan complete: ${indexed} indexed, ${failed} failed`);
361
+ console.error(`[MemoryEngine] Codebase scan complete: ${indexed} indexed, ${failed} failed`);
325
362
  }
326
363
 
327
364
  async indexMarkdownFile(filePath) {
@@ -498,7 +535,7 @@ export class MemoryEngine {
498
535
  const startTime = performance.now();
499
536
  const limit = options.limit || 10;
500
537
 
501
- console.log(`[MemoryEngine] 🔍 Hybrid search: "${query}"`);
538
+ console.error(`[MemoryEngine] 🔍 Hybrid search: "${query}"`);
502
539
 
503
540
  // Get vector results first
504
541
  const vectorResult = await this.searchVector(query, limit * 4);
@@ -710,7 +747,7 @@ export class MemoryEngine {
710
747
  // ========== Lifecycle ==========
711
748
 
712
749
  async shutdown() {
713
- console.log('[MemoryEngine] Shutting down...');
750
+ console.error('[MemoryEngine] Shutting down...');
714
751
 
715
752
  if (this.intelligenceEnabled) {
716
753
  await this.strategySelector?.saveModel?.();
@@ -723,7 +760,7 @@ export class MemoryEngine {
723
760
  this.codebaseDb?.close?.();
724
761
 
725
762
  this.isInitialized = false;
726
- console.log('[MemoryEngine] Shutdown complete');
763
+ console.error('[MemoryEngine] Shutdown complete');
727
764
  }
728
765
  }
729
766
 
@@ -0,0 +1,494 @@
1
+ /**
2
+ * Context Analyzer
3
+ * Phân tích changes và tự động cập nhật MEMORY.md sử dụng mcfast API (Mercury qua API)
4
+ *
5
+ * Flow:
6
+ * 1. Detect file changes
7
+ * 2. Send to mcfast API /analyze endpoint
8
+ * 3. Get structured summary
9
+ * 4. Auto-update MEMORY.md if important
10
+ * 5. Return results immediately
11
+ */
12
+
13
+ import fs from 'fs/promises';
14
+ import path from 'path';
15
+
16
+ const API_URL = process.env.MCFAST_API_URL || "https://mcfast.vercel.app/api/v1";
17
+
18
+ export class ContextAnalyzer {
19
+ constructor(options = {}) {
20
+ this.apiKey = options.apiKey || process.env.MCFAST_TOKEN;
21
+ this.projectPath = options.projectPath || process.cwd();
22
+ this.memoryPath = options.memoryPath || path.join(this.projectPath, '.mcfast');
23
+
24
+ // Analysis cache to avoid re-analyzing same changes
25
+ this.analysisCache = new Map();
26
+ this.cacheMaxSize = 100;
27
+
28
+ // Session tracking
29
+ this.currentSession = {
30
+ startTime: Date.now(),
31
+ changes: [],
32
+ filesAffected: new Set(),
33
+ patterns: []
34
+ };
35
+
36
+ console.log('[ContextAnalyzer] Initialized with mcfast API');
37
+ }
38
+
39
+ /**
40
+ * Call mcfast API for analysis
41
+ * Sử dụng endpoint /apply để phân tích changes
42
+ */
43
+ async callMcfastAPI(instruction, files = {}) {
44
+ try {
45
+ const response = await fetch(`${API_URL}/apply`, {
46
+ method: "POST",
47
+ headers: {
48
+ "Content-Type": "application/json",
49
+ "Authorization": `Bearer ${this.apiKey}`,
50
+ },
51
+ body: JSON.stringify({
52
+ instruction,
53
+ files,
54
+ toolName: 'context_analyze',
55
+ options: {
56
+ mode: 'analyze_only',
57
+ return_analysis: true
58
+ }
59
+ }),
60
+ });
61
+
62
+ if (!response.ok) {
63
+ const errorText = await response.text();
64
+ throw new Error(`API Error (${response.status}): ${errorText}`);
65
+ }
66
+
67
+ return await response.json();
68
+ } catch (error) {
69
+ console.error('[ContextAnalyzer] API call failed:', error.message);
70
+ throw error;
71
+ }
72
+ }
73
+
74
+ /**
75
+ * Phân tích changes và trả về kết quả nhanh
76
+ * @param {Object} changeInfo - Thông tin về changes
77
+ * @returns {Promise<Object>} Analysis result
78
+ */
79
+ async analyze(changeInfo) {
80
+ const startTime = performance.now();
81
+
82
+ try {
83
+ // Build analysis prompt
84
+ const prompt = this.buildAnalysisPrompt(changeInfo);
85
+
86
+ // Call mcfast API (sử dụng Mercury thông qua API)
87
+ const result = await this.callMcfastAPI(prompt, {
88
+ 'analysis.txt': changeInfo.diff || ''
89
+ });
90
+
91
+ // Parse API response
92
+ const analysis = this.parseAnalysis(result.content || result.analysis || '');
93
+
94
+ // Update session tracking
95
+ this.trackSessionChange(changeInfo, analysis);
96
+
97
+ // Auto-update MEMORY.md if important
98
+ if (analysis.importance >= 7) {
99
+ await this.autoUpdateMemory(analysis);
100
+ }
101
+
102
+ const duration = performance.now() - startTime;
103
+
104
+ return {
105
+ success: true,
106
+ analysis: analysis,
107
+ duration: Math.round(duration),
108
+ session: this.getSessionSummary(),
109
+ metadata: {
110
+ importance: analysis.importance,
111
+ shouldUpdateMemory: analysis.importance >= 7,
112
+ filesAffected: Array.from(this.currentSession.filesAffected),
113
+ apiLatency: result.latency_ms || 0
114
+ }
115
+ };
116
+
117
+ } catch (error) {
118
+ console.error('[ContextAnalyzer] Analysis failed:', error.message);
119
+ return {
120
+ success: false,
121
+ error: error.message,
122
+ duration: Math.round(performance.now() - startTime)
123
+ };
124
+ }
125
+ }
126
+
127
+ /**
128
+ * Build analysis prompt for mcfast API
129
+ */
130
+ buildAnalysisPrompt(changeInfo) {
131
+ const {
132
+ filePath,
133
+ changeType, // 'add', 'modify', 'delete'
134
+ oldContent = '',
135
+ newContent = '',
136
+ diff = '',
137
+ instruction = '' // Original user instruction if available
138
+ } = changeInfo;
139
+
140
+ return `Analyze this code change and provide a structured summary.
141
+
142
+ **File:** ${filePath}
143
+ **Change Type:** ${changeType}
144
+ ${instruction ? `**User Instruction:** ${instruction}` : ''}
145
+
146
+ **Diff/Changes:**
147
+ ${diff || `Old: ${oldContent.substring(0, 500)}...\nNew: ${newContent.substring(0, 500)}...`}
148
+
149
+ **Analyze and return JSON format:**
150
+ {
151
+ "summary": "Brief summary of what was changed (1-2 sentences)",
152
+ "type": "Type of change: feature|bugfix|refactor|docs|test|config",
153
+ "importance": "Number 1-10: How important is this change?",
154
+ "impact": "Impact level: low|medium|high",
155
+ "keyChanges": ["List of specific changes made"],
156
+ "decisions": ["Any architectural or design decisions implied"],
157
+ "patterns": ["Code patterns or conventions used"],
158
+ "risks": ["Potential risks or concerns"],
159
+ "suggestions": ["Suggestions for improvement"],
160
+ "shouldRemember": "Boolean: Should this be remembered in long-term memory?",
161
+ "memorySection": "Which MEMORY.md section to update: User Preferences|Important Decisions|Key Contacts|Project Context|Lessons Learned"
162
+ }
163
+
164
+ Return ONLY valid JSON, no markdown formatting.`;
165
+ }
166
+
167
+ /**
168
+ * Parse mcfast API response
169
+ */
170
+ parseAnalysis(content) {
171
+ try {
172
+ // Try to extract JSON from response
173
+ const jsonMatch = content.match(/\{[\s\S]*\}/);
174
+ if (jsonMatch) {
175
+ const analysis = JSON.parse(jsonMatch[0]);
176
+
177
+ // Ensure all fields exist with defaults
178
+ return {
179
+ summary: analysis.summary || 'Change analyzed',
180
+ type: analysis.type || 'unknown',
181
+ importance: parseInt(analysis.importance) || 5,
182
+ impact: analysis.impact || 'medium',
183
+ keyChanges: analysis.keyChanges || [],
184
+ decisions: analysis.decisions || [],
185
+ patterns: analysis.patterns || [],
186
+ risks: analysis.risks || [],
187
+ suggestions: analysis.suggestions || [],
188
+ shouldRemember: analysis.shouldRemember || false,
189
+ memorySection: analysis.memorySection || 'Project Context'
190
+ };
191
+ }
192
+ } catch (error) {
193
+ console.warn('[ContextAnalyzer] Failed to parse JSON, using fallback');
194
+ }
195
+
196
+ // Fallback: treat entire content as summary
197
+ return {
198
+ summary: content.substring(0, 200),
199
+ type: 'unknown',
200
+ importance: 5,
201
+ impact: 'medium',
202
+ keyChanges: [],
203
+ decisions: [],
204
+ patterns: [],
205
+ risks: [],
206
+ suggestions: [],
207
+ shouldRemember: false,
208
+ memorySection: 'Project Context'
209
+ };
210
+ }
211
+
212
+ /**
213
+ * Track changes in current session
214
+ */
215
+ trackSessionChange(changeInfo, analysis) {
216
+ this.currentSession.changes.push({
217
+ timestamp: Date.now(),
218
+ file: changeInfo.filePath,
219
+ type: changeInfo.changeType,
220
+ analysis: analysis
221
+ });
222
+
223
+ this.currentSession.filesAffected.add(changeInfo.filePath);
224
+
225
+ // Track patterns
226
+ if (analysis.patterns) {
227
+ analysis.patterns.forEach(pattern => {
228
+ if (!this.currentSession.patterns.includes(pattern)) {
229
+ this.currentSession.patterns.push(pattern);
230
+ }
231
+ });
232
+ }
233
+ }
234
+
235
+ /**
236
+ * Get current session summary
237
+ */
238
+ getSessionSummary() {
239
+ const duration = Date.now() - this.currentSession.startTime;
240
+ const changeCount = this.currentSession.changes.length;
241
+
242
+ // Calculate importance distribution
243
+ const importanceSum = this.currentSession.changes.reduce(
244
+ (sum, c) => sum + (c.analysis.importance || 5),
245
+ 0
246
+ );
247
+ const avgImportance = changeCount > 0 ? (importanceSum / changeCount).toFixed(1) : 0;
248
+
249
+ return {
250
+ duration: Math.round(duration / 1000), // seconds
251
+ changeCount,
252
+ filesAffected: Array.from(this.currentSession.filesAffected),
253
+ patterns: this.currentSession.patterns,
254
+ avgImportance,
255
+ highImpactChanges: this.currentSession.changes.filter(
256
+ c => c.analysis.importance >= 8
257
+ ).length
258
+ };
259
+ }
260
+
261
+ /**
262
+ * Auto-update MEMORY.md with important information
263
+ */
264
+ async autoUpdateMemory(analysis) {
265
+ try {
266
+ const memoryPath = path.join(this.memoryPath, 'MEMORY.md');
267
+
268
+ // Check if file exists
269
+ let content = '';
270
+ try {
271
+ content = await fs.readFile(memoryPath, 'utf-8');
272
+ } catch (error) {
273
+ // File doesn't exist, will be created
274
+ content = this.getDefaultMemoryTemplate();
275
+ }
276
+
277
+ // Determine what to add based on analysis
278
+ let update = '';
279
+ const timestamp = new Date().toISOString().split('T')[0];
280
+
281
+ if (analysis.decisions && analysis.decisions.length > 0) {
282
+ // Add to Important Decisions
283
+ update = `\n- **${timestamp}:** ${analysis.summary}`;
284
+ if (analysis.decisions[0]) {
285
+ update += `\n - Decision: ${analysis.decisions[0]}`;
286
+ }
287
+ content = this.addToSection(content, 'Important Decisions', update);
288
+ }
289
+
290
+ if (analysis.patterns && analysis.patterns.length > 0 && analysis.importance >= 8) {
291
+ // Add to Project Context if high importance
292
+ update = `\n- **${timestamp}:** Using ${analysis.patterns.join(', ')}`;
293
+ content = this.addToSection(content, 'Project Context', update);
294
+ }
295
+
296
+ if (analysis.type === 'bugfix' && analysis.importance >= 7) {
297
+ // Add to Lessons Learned
298
+ update = `\n- **${timestamp}:** Fixed issue - ${analysis.summary}`;
299
+ if (analysis.risks[0]) {
300
+ update += `\n - Lesson: ${analysis.risks[0]}`;
301
+ }
302
+ content = this.addToSection(content, 'Lessons Learned', update);
303
+ }
304
+
305
+ // Update timestamp
306
+ content = content.replace(
307
+ /\*Last updated:.*\*/,
308
+ `*Last updated: ${new Date().toISOString()}*`
309
+ );
310
+
311
+ await fs.writeFile(memoryPath, content);
312
+ console.log(`[ContextAnalyzer] Updated MEMORY.md: ${analysis.memorySection}`);
313
+
314
+ } catch (error) {
315
+ console.error('[ContextAnalyzer] Failed to update MEMORY.md:', error.message);
316
+ }
317
+ }
318
+
319
+ /**
320
+ * Add content to a specific section in MEMORY.md
321
+ */
322
+ addToSection(content, sectionName, newContent) {
323
+ const sectionRegex = new RegExp(`(## ${sectionName}.*?)(?=\n## |$)`, 's');
324
+ const match = content.match(sectionRegex);
325
+
326
+ if (match) {
327
+ // Insert after section header
328
+ const section = match[1];
329
+ const updatedSection = section + newContent;
330
+ return content.replace(section, updatedSection);
331
+ }
332
+
333
+ return content;
334
+ }
335
+
336
+ /**
337
+ * Get default MEMORY.md template
338
+ */
339
+ getDefaultMemoryTemplate() {
340
+ return `# Long-term Memory
341
+
342
+ This file contains curated, persistent knowledge.
343
+
344
+ ## User Preferences
345
+ <!-- Add user preferences here -->
346
+
347
+ ## Important Decisions
348
+ <!-- Add important decisions here -->
349
+
350
+ ## Key Contacts
351
+ <!-- Add key contacts here -->
352
+
353
+ ## Project Context
354
+ <!-- Add project context here -->
355
+
356
+ ## Lessons Learned
357
+ <!-- Add lessons learned here -->
358
+
359
+ ---
360
+ *Last updated: ${new Date().toISOString()}*
361
+ `;
362
+ }
363
+
364
+ /**
365
+ * Generate session summary at end of work
366
+ */
367
+ async generateSessionSummary() {
368
+ const session = this.getSessionSummary();
369
+
370
+ if (session.changeCount === 0) {
371
+ return { summary: 'No changes made in this session' };
372
+ }
373
+
374
+ // Build summary for Mercury
375
+ const changesList = this.currentSession.changes
376
+ .map(c => `- ${c.file}: ${c.analysis.summary} (importance: ${c.analysis.importance})`)
377
+ .join('\n');
378
+
379
+ const prompt = `Summarize this coding session in 2-3 sentences.
380
+
381
+ **Session Stats:**
382
+ - Duration: ${Math.round(session.duration / 60)} minutes
383
+ - Files changed: ${session.filesAffected.length}
384
+ - Total changes: ${session.changeCount}
385
+ - High impact changes: ${session.highImpactChanges}
386
+
387
+ **Changes made:**
388
+ ${changesList}
389
+
390
+ **Summary:**`;
391
+
392
+ try {
393
+ const result = await this.callMcfastAPI(prompt, {});
394
+
395
+ return {
396
+ summary: result.content.trim(),
397
+ session: session,
398
+ timestamp: Date.now()
399
+ };
400
+ } catch (error) {
401
+ return {
402
+ summary: `Session with ${session.changeCount} changes across ${session.filesAffected.length} files`,
403
+ session: session,
404
+ timestamp: Date.now()
405
+ };
406
+ }
407
+ }
408
+
409
+ /**
410
+ * End current session and generate summary
411
+ */
412
+ async endSession() {
413
+ const summary = await this.generateSessionSummary();
414
+
415
+ // Log to daily logs
416
+ const dailyLogs = this.memory.dailyLogs;
417
+ if (dailyLogs) {
418
+ await dailyLogs.log('Session Summary', summary.summary, {
419
+ filesChanged: summary.session.filesAffected.length,
420
+ duration: summary.session.duration,
421
+ avgImportance: summary.session.avgImportance
422
+ });
423
+ }
424
+
425
+ // Reset session
426
+ this.currentSession = {
427
+ startTime: Date.now(),
428
+ changes: [],
429
+ filesAffected: new Set(),
430
+ patterns: []
431
+ };
432
+
433
+ return summary;
434
+ }
435
+
436
+ /**
437
+ * Quick analysis - returns immediately with cached result if available
438
+ */
439
+ async quickAnalyze(filePath, changeType, diff = '') {
440
+ const cacheKey = this.getCacheKey(filePath, diff);
441
+
442
+ // Check cache
443
+ if (this.analysisCache.has(cacheKey)) {
444
+ return {
445
+ success: true,
446
+ cached: true,
447
+ analysis: this.analysisCache.get(cacheKey),
448
+ duration: 0
449
+ };
450
+ }
451
+
452
+ // Perform analysis
453
+ const result = await this.analyze({
454
+ filePath,
455
+ changeType,
456
+ diff
457
+ });
458
+
459
+ // Cache result
460
+ if (result.success) {
461
+ this.cacheResult(cacheKey, result.analysis);
462
+ }
463
+
464
+ return result;
465
+ }
466
+
467
+ getCacheKey(filePath, diff) {
468
+ return `${filePath}:${diff.substring(0, 100)}`;
469
+ }
470
+
471
+ cacheResult(key, analysis) {
472
+ if (this.analysisCache.size >= this.cacheMaxSize) {
473
+ // Remove oldest entry
474
+ const firstKey = this.analysisCache.keys().next().value;
475
+ this.analysisCache.delete(firstKey);
476
+ }
477
+ this.analysisCache.set(key, analysis);
478
+ }
479
+
480
+ /**
481
+ * Get analysis statistics
482
+ */
483
+ getStats() {
484
+ return {
485
+ sessionDuration: Math.round((Date.now() - this.currentSession.startTime) / 1000),
486
+ changesInSession: this.currentSession.changes.length,
487
+ filesAffected: this.currentSession.filesAffected.size,
488
+ cacheSize: this.analysisCache.size,
489
+ patternsDetected: this.currentSession.patterns.length
490
+ };
491
+ }
492
+ }
493
+
494
+ export default ContextAnalyzer;
@@ -51,8 +51,8 @@ export class FileWatcher {
51
51
  }
52
52
 
53
53
  async start() {
54
- console.log(`[FileWatcher] Starting watcher for: ${this.projectPath}`);
55
- console.log(`[FileWatcher] Debounce: ${this.debounceMs}ms`);
54
+ console.error(`[FileWatcher] Starting watcher for: ${this.projectPath}`);
55
+ console.error(`[FileWatcher] Debounce: ${this.debounceMs}ms`);
56
56
 
57
57
  this.watcher = chokidar.watch(this.projectPath, {
58
58
  ignored: this.ignored,
@@ -79,11 +79,11 @@ export class FileWatcher {
79
79
  this.watcher.once('error', reject);
80
80
  });
81
81
 
82
- console.log(`[FileWatcher] Ready and watching`);
82
+ console.error(`[FileWatcher] Ready and watching`);
83
83
  }
84
84
 
85
85
  handleAdd(filePath) {
86
- console.log(`[FileWatcher] File added: ${filePath}`);
86
+ console.error(`[FileWatcher] File added: ${filePath}`);
87
87
  this.pendingUpdates.set(filePath, {
88
88
  path: filePath,
89
89
  type: 'add',
@@ -93,7 +93,7 @@ export class FileWatcher {
93
93
  }
94
94
 
95
95
  handleChange(filePath) {
96
- console.log(`[FileWatcher] File changed: ${filePath}`);
96
+ console.error(`[FileWatcher] File changed: ${filePath}`);
97
97
  this.pendingUpdates.set(filePath, {
98
98
  path: filePath,
99
99
  type: 'change',
@@ -103,7 +103,7 @@ export class FileWatcher {
103
103
  }
104
104
 
105
105
  handleDelete(filePath) {
106
- console.log(`[FileWatcher] File deleted: ${filePath}`);
106
+ console.error(`[FileWatcher] File deleted: ${filePath}`);
107
107
  this.pendingUpdates.set(filePath, {
108
108
  path: filePath,
109
109
  type: 'delete',
@@ -131,7 +131,7 @@ export class FileWatcher {
131
131
 
132
132
  if (updates.length === 0) return;
133
133
 
134
- console.log(`[FileWatcher] Processing ${updates.length} updates...`);
134
+ console.error(`[FileWatcher] Processing ${updates.length} updates...`);
135
135
 
136
136
  // Group by type for efficiency
137
137
  const adds = updates.filter(u => u.type === 'add' || u.type === 'change');
@@ -147,7 +147,7 @@ export class FileWatcher {
147
147
  await this.processFile(update);
148
148
  }
149
149
 
150
- console.log(`[FileWatcher] Processed ${updates.length} updates`);
150
+ console.error(`[FileWatcher] Processed ${updates.length} updates`);
151
151
 
152
152
  } catch (error) {
153
153
  console.error(`[FileWatcher] Error processing queue:`, error);
@@ -167,7 +167,7 @@ export class FileWatcher {
167
167
 
168
168
  // Skip large files (> 1MB)
169
169
  if (stats.size > 1024 * 1024) {
170
- console.log(`[FileWatcher] Skipping large file: ${filePath} (${Math.round(stats.size / 1024)}KB)`);
170
+ console.error(`[FileWatcher] Skipping large file: ${filePath} (${Math.round(stats.size / 1024)}KB)`);
171
171
  return;
172
172
  }
173
173
 
@@ -177,7 +177,7 @@ export class FileWatcher {
177
177
 
178
178
  // Check if already indexed with same hash
179
179
  if (this.memory.codebaseDb?.isFileIndexed?.(filePath, contentHash)) {
180
- console.log(`[FileWatcher] File unchanged: ${filePath}`);
180
+ console.error(`[FileWatcher] File unchanged: ${filePath}`);
181
181
  return;
182
182
  }
183
183
 
@@ -208,7 +208,7 @@ export class FileWatcher {
208
208
  const file = this.memory.codebaseDb?.getFileByPath?.(filePath);
209
209
  if (file) {
210
210
  this.memory.codebaseDb.deleteFile(file.id);
211
- console.log(`[FileWatcher] Deleted from index: ${filePath}`);
211
+ console.error(`[FileWatcher] Deleted from index: ${filePath}`);
212
212
  }
213
213
 
214
214
  this.stats.filesDeleted++;
@@ -220,7 +220,7 @@ export class FileWatcher {
220
220
  }
221
221
 
222
222
  async indexMarkdownFile(filePath, content, contentHash, stats) {
223
- console.log(`[FileWatcher] Indexing Markdown: ${filePath}`);
223
+ console.error(`[FileWatcher] Indexing Markdown: ${filePath}`);
224
224
 
225
225
  // Delete old chunks if updating
226
226
  const relativePath = path.relative(this.projectPath, filePath);
@@ -279,18 +279,18 @@ export class FileWatcher {
279
279
  }
280
280
  }
281
281
 
282
- console.log(`[FileWatcher] Indexed ${chunks.length} chunks from ${filePath}`);
282
+ console.error(`[FileWatcher] Indexed ${chunks.length} chunks from ${filePath}`);
283
283
  }
284
284
 
285
285
  async indexCodeFile(filePath, content, contentHash, stats) {
286
- console.log(`[FileWatcher] Indexing code: ${filePath}`);
286
+ console.error(`[FileWatcher] Indexing code: ${filePath}`);
287
287
 
288
288
  // Use the existing indexer
289
289
  if (this.memory.indexer) {
290
290
  try {
291
291
  const indexed = await this.memory.indexer.indexFile(filePath, content);
292
292
  await this.memory.storeIndexed(indexed);
293
- console.log(`[FileWatcher] Indexed ${indexed.facts.length} facts, ${indexed.chunks.length} chunks from ${filePath}`);
293
+ console.error(`[FileWatcher] Indexed ${indexed.facts.length} facts, ${indexed.chunks.length} chunks from ${filePath}`);
294
294
  } catch (error) {
295
295
  console.error(`[FileWatcher] Failed to index ${filePath}:`, error.message);
296
296
  }
@@ -319,7 +319,7 @@ export class FileWatcher {
319
319
 
320
320
  await this.watcher.close();
321
321
  this.watcher = null;
322
- console.log(`[FileWatcher] Stopped`);
322
+ console.error(`[FileWatcher] Stopped`);
323
323
  }
324
324
  }
325
325
  }
@@ -8,24 +8,15 @@ import fs from 'fs/promises';
8
8
  import path from 'path';
9
9
  import { MemoryEngine } from '../memory/index.js';
10
10
 
11
- // Memory engine instance (initialized lazily)
12
- let memoryEngine = null;
13
- let enginePromise = null;
14
-
11
+ /**
12
+ * Get MemoryEngine singleton instance
13
+ * Uses static getOrCreate to ensure single instance
14
+ */
15
15
  async function getMemoryEngine() {
16
- if (memoryEngine) return memoryEngine;
17
- if (enginePromise) return enginePromise;
18
-
19
- enginePromise = (async () => {
20
- memoryEngine = new MemoryEngine({
21
- apiKey: process.env.MCFAST_TOKEN,
22
- enableSync: true
23
- });
24
- await memoryEngine.initialize(process.cwd());
25
- return memoryEngine;
26
- })();
27
-
28
- return enginePromise;
16
+ return MemoryEngine.getOrCreate(process.cwd(), {
17
+ apiKey: process.env.MCFAST_TOKEN,
18
+ enableSync: true
19
+ });
29
20
  }
30
21
 
31
22
  /**
@@ -6,24 +6,15 @@
6
6
 
7
7
  import { MemoryEngine } from '../memory/index.js';
8
8
 
9
- // Memory engine instance (initialized lazily)
10
- let memoryEngine = null;
11
- let enginePromise = null;
12
-
9
+ /**
10
+ * Get MemoryEngine singleton instance
11
+ * Uses static getOrCreate to ensure single instance
12
+ */
13
13
  async function getMemoryEngine() {
14
- if (memoryEngine) return memoryEngine;
15
- if (enginePromise) return enginePromise;
16
-
17
- enginePromise = (async () => {
18
- memoryEngine = new MemoryEngine({
19
- apiKey: process.env.MCFAST_TOKEN,
20
- enableSync: true
21
- });
22
- await memoryEngine.initialize(process.cwd());
23
- return memoryEngine;
24
- })();
25
-
26
- return enginePromise;
14
+ return MemoryEngine.getOrCreate(process.cwd(), {
15
+ apiKey: process.env.MCFAST_TOKEN,
16
+ enableSync: true
17
+ });
27
18
  }
28
19
 
29
20
  /**