@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 +2 -2
- package/src/memory/layers/daily-logs.js +40 -1
- package/src/memory/memory-engine.js +59 -22
- package/src/memory/utils/context-analyzer.js +494 -0
- package/src/memory/watchers/file-watcher.js +16 -16
- package/src/tools/memory_get.js +8 -17
- package/src/tools/memory_search.js +8 -17
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@mrxkun/mcfast-mcp",
|
|
3
|
-
"version": "4.1.
|
|
4
|
-
"description": "Ultra-fast code editing with WASM acceleration, fuzzy patching, multi-layer caching, and 8 unified tools.
|
|
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.
|
|
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.
|
|
129
|
-
console.
|
|
130
|
-
console.
|
|
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.
|
|
185
|
-
console.
|
|
186
|
-
console.
|
|
187
|
-
console.
|
|
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.
|
|
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.
|
|
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.
|
|
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.
|
|
247
|
-
console.
|
|
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.
|
|
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.
|
|
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.
|
|
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.
|
|
325
|
+
console.error('[MemoryEngine] No code files found to index');
|
|
289
326
|
return;
|
|
290
327
|
}
|
|
291
328
|
|
|
292
|
-
console.
|
|
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.
|
|
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.
|
|
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.
|
|
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.
|
|
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.
|
|
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.
|
|
55
|
-
console.
|
|
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.
|
|
82
|
+
console.error(`[FileWatcher] Ready and watching`);
|
|
83
83
|
}
|
|
84
84
|
|
|
85
85
|
handleAdd(filePath) {
|
|
86
|
-
console.
|
|
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.
|
|
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.
|
|
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.
|
|
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.
|
|
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.
|
|
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.
|
|
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.
|
|
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.
|
|
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.
|
|
282
|
+
console.error(`[FileWatcher] Indexed ${chunks.length} chunks from ${filePath}`);
|
|
283
283
|
}
|
|
284
284
|
|
|
285
285
|
async indexCodeFile(filePath, content, contentHash, stats) {
|
|
286
|
-
console.
|
|
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.
|
|
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.
|
|
322
|
+
console.error(`[FileWatcher] Stopped`);
|
|
323
323
|
}
|
|
324
324
|
}
|
|
325
325
|
}
|
package/src/tools/memory_get.js
CHANGED
|
@@ -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
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
11
|
+
/**
|
|
12
|
+
* Get MemoryEngine singleton instance
|
|
13
|
+
* Uses static getOrCreate to ensure single instance
|
|
14
|
+
*/
|
|
15
15
|
async function getMemoryEngine() {
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
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
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
9
|
+
/**
|
|
10
|
+
* Get MemoryEngine singleton instance
|
|
11
|
+
* Uses static getOrCreate to ensure single instance
|
|
12
|
+
*/
|
|
13
13
|
async function getMemoryEngine() {
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
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
|
/**
|