@mrxkun/mcfast-mcp 4.1.11 → 4.1.13
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/index.js +318 -201
- package/src/memory/memory-engine.js +24 -17
- package/src/memory/watchers/file-watcher.js +8 -6
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. v4.1.
|
|
3
|
+
"version": "4.1.13",
|
|
4
|
+
"description": "Ultra-fast code editing with WASM acceleration, fuzzy patching, multi-layer caching, and 8 unified tools. v4.1.12: Implement proper MCP stdio transport lifecycle and cleanup to prevent zombie processes.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"bin": {
|
|
7
7
|
"mcfast-mcp": "src/index.js"
|
package/src/index.js
CHANGED
|
@@ -25,13 +25,13 @@ const colors = {
|
|
|
25
25
|
|
|
26
26
|
// Override console.log - suppress in MCP mode
|
|
27
27
|
const originalConsoleLog = console.log;
|
|
28
|
-
console.log = function(...args) {
|
|
28
|
+
console.log = function (...args) {
|
|
29
29
|
// Suppressed in MCP mode
|
|
30
30
|
};
|
|
31
31
|
|
|
32
32
|
// Override console.error - suppress in MCP mode
|
|
33
33
|
const originalConsoleError = console.error;
|
|
34
|
-
console.error = function(...args) {
|
|
34
|
+
console.error = function (...args) {
|
|
35
35
|
// Suppressed in MCP mode
|
|
36
36
|
};
|
|
37
37
|
|
|
@@ -86,70 +86,70 @@ const LOCK_FILE_PATH = path.join(process.cwd(), '.mcfast', '.mcp-instance.lock')
|
|
|
86
86
|
let lockFileHandle = null;
|
|
87
87
|
|
|
88
88
|
async function acquireLock() {
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
89
|
+
try {
|
|
90
|
+
const lockDir = path.dirname(LOCK_FILE_PATH);
|
|
91
|
+
await fs.mkdir(lockDir, { recursive: true });
|
|
92
|
+
|
|
93
|
+
lockFileHandle = await fs.open(LOCK_FILE_PATH, 'wx');
|
|
94
|
+
|
|
95
|
+
// Write current PID
|
|
96
|
+
await lockFileHandle.write(`${process.pid}\n${Date.now()}\n`, 0);
|
|
97
|
+
await lockFileHandle.sync();
|
|
98
|
+
|
|
99
|
+
// Cleanup on exit
|
|
100
|
+
process.on('beforeExit', releaseLock);
|
|
101
|
+
process.on('SIGINT', releaseLock);
|
|
102
|
+
process.on('SIGTERM', releaseLock);
|
|
103
|
+
process.on('uncaughtException', releaseLock);
|
|
104
|
+
|
|
105
|
+
console.error(`${colors.cyan}[Lock]${colors.reset} Acquired lock file: ${LOCK_FILE_PATH}`);
|
|
106
|
+
return true;
|
|
107
|
+
} catch (error) {
|
|
108
|
+
if (error.code === 'EEXIST') {
|
|
109
|
+
// Lock file exists, check if it's stale
|
|
110
|
+
try {
|
|
111
|
+
const lockContent = await fs.readFile(LOCK_FILE_PATH, 'utf-8');
|
|
112
|
+
const [pid, timestamp] = lockContent.trim().split('\n');
|
|
113
|
+
const lockAge = Date.now() - parseInt(timestamp);
|
|
114
|
+
|
|
115
|
+
// Check if process is still running (stale lock if > 5 minutes)
|
|
116
|
+
if (lockAge > 5 * 60 * 1000) {
|
|
117
|
+
console.error(`${colors.yellow}[Lock]${colors.reset} Stale lock detected (age: ${Math.round(lockAge / 1000)}s), removing...`);
|
|
118
|
+
await fs.unlink(LOCK_FILE_PATH);
|
|
119
|
+
return acquireLock(); // Retry
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
console.error(`${colors.red}[Lock]${colors.reset} Another instance is already running (PID: ${pid})`);
|
|
123
|
+
console.error(`${colors.yellow}[Lock]${colors.reset} To force start, delete: ${LOCK_FILE_PATH}`);
|
|
124
|
+
return false;
|
|
125
|
+
} catch (readError) {
|
|
126
|
+
console.error(`${colors.red}[Lock]${colors.reset} Failed to read lock file: ${readError.message}`);
|
|
127
|
+
return false;
|
|
128
|
+
}
|
|
129
|
+
}
|
|
130
|
+
console.error(`${colors.red}[Lock]${colors.reset} Failed to acquire lock: ${error.message}`);
|
|
127
131
|
return false;
|
|
128
|
-
}
|
|
129
132
|
}
|
|
130
|
-
console.error(`${colors.red}[Lock]${colors.reset} Failed to acquire lock: ${error.message}`);
|
|
131
|
-
return false;
|
|
132
|
-
}
|
|
133
133
|
}
|
|
134
134
|
|
|
135
135
|
async function releaseLock() {
|
|
136
|
-
|
|
136
|
+
if (lockFileHandle) {
|
|
137
|
+
try {
|
|
138
|
+
await lockFileHandle.close();
|
|
139
|
+
lockFileHandle = null;
|
|
140
|
+
} catch (e) {
|
|
141
|
+
// Ignore close errors
|
|
142
|
+
}
|
|
143
|
+
}
|
|
144
|
+
|
|
137
145
|
try {
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
} catch (
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
try {
|
|
146
|
-
await fs.unlink(LOCK_FILE_PATH);
|
|
147
|
-
console.error(`${colors.cyan}[Lock]${colors.reset} Released lock file`);
|
|
148
|
-
} catch (error) {
|
|
149
|
-
if (error.code !== 'ENOENT') {
|
|
150
|
-
console.error(`${colors.yellow}[Lock]${colors.reset} Failed to release lock: ${error.message}`);
|
|
151
|
-
}
|
|
152
|
-
}
|
|
146
|
+
await fs.unlink(LOCK_FILE_PATH);
|
|
147
|
+
console.error(`${colors.cyan}[Lock]${colors.reset} Released lock file`);
|
|
148
|
+
} catch (error) {
|
|
149
|
+
if (error.code !== 'ENOENT') {
|
|
150
|
+
console.error(`${colors.yellow}[Lock]${colors.reset} Failed to release lock: ${error.message}`);
|
|
151
|
+
}
|
|
152
|
+
}
|
|
153
153
|
}
|
|
154
154
|
|
|
155
155
|
// ============================================================================
|
|
@@ -171,36 +171,36 @@ let memoryEngineReady = false;
|
|
|
171
171
|
*/
|
|
172
172
|
async function backgroundInitializeMemoryEngine() {
|
|
173
173
|
if (memoryEngineInitPromise) return memoryEngineInitPromise;
|
|
174
|
-
|
|
174
|
+
|
|
175
175
|
memoryEngineInitPromise = (async () => {
|
|
176
176
|
const maxRetries = 2;
|
|
177
177
|
let lastError = null;
|
|
178
|
-
|
|
178
|
+
|
|
179
179
|
for (let attempt = 1; attempt <= maxRetries; attempt++) {
|
|
180
180
|
try {
|
|
181
181
|
memoryEngine = new MemoryEngine({
|
|
182
182
|
apiKey: TOKEN,
|
|
183
183
|
enableSync: true
|
|
184
184
|
});
|
|
185
|
-
|
|
185
|
+
|
|
186
186
|
// Use timeout for initialization
|
|
187
187
|
const initTimeout = new Promise((_, reject) => {
|
|
188
188
|
setTimeout(() => reject(new Error('Memory engine init timeout')), 25000);
|
|
189
189
|
});
|
|
190
|
-
|
|
190
|
+
|
|
191
191
|
await Promise.race([
|
|
192
192
|
memoryEngine.initialize(process.cwd()),
|
|
193
193
|
initTimeout
|
|
194
194
|
]);
|
|
195
|
-
|
|
195
|
+
|
|
196
196
|
memoryEngineReady = true;
|
|
197
197
|
console.error(`${colors.cyan}[Memory]${colors.reset} Engine initialized successfully`);
|
|
198
198
|
return memoryEngine;
|
|
199
|
-
|
|
199
|
+
|
|
200
200
|
} catch (error) {
|
|
201
201
|
lastError = error;
|
|
202
202
|
console.error(`${colors.yellow}[Memory]${colors.reset} Initialization attempt ${attempt}/${maxRetries} failed: ${error.message}`);
|
|
203
|
-
|
|
203
|
+
|
|
204
204
|
if (attempt < maxRetries) {
|
|
205
205
|
// Exponential backoff before retry
|
|
206
206
|
const backoffMs = 1000 * Math.pow(2, attempt - 1);
|
|
@@ -209,7 +209,7 @@ async function backgroundInitializeMemoryEngine() {
|
|
|
209
209
|
}
|
|
210
210
|
}
|
|
211
211
|
}
|
|
212
|
-
|
|
212
|
+
|
|
213
213
|
// All retries failed - graceful degradation
|
|
214
214
|
console.error(`${colors.yellow}[Memory]${colors.reset} All initialization attempts failed. Running in degraded mode.`);
|
|
215
215
|
console.error(`${colors.yellow}[Memory]${colors.reset} Error: ${lastError?.message || 'Unknown error'}`);
|
|
@@ -217,7 +217,7 @@ async function backgroundInitializeMemoryEngine() {
|
|
|
217
217
|
memoryEngineReady = false;
|
|
218
218
|
return null;
|
|
219
219
|
})();
|
|
220
|
-
|
|
220
|
+
|
|
221
221
|
return memoryEngineInitPromise;
|
|
222
222
|
}
|
|
223
223
|
|
|
@@ -228,7 +228,7 @@ async function getMemoryEngine() {
|
|
|
228
228
|
if (memoryEngine && memoryEngineReady) {
|
|
229
229
|
return memoryEngine;
|
|
230
230
|
}
|
|
231
|
-
|
|
231
|
+
|
|
232
232
|
// Wait for initialization with timeout
|
|
233
233
|
if (memoryEngineInitPromise) {
|
|
234
234
|
try {
|
|
@@ -240,12 +240,12 @@ async function getMemoryEngine() {
|
|
|
240
240
|
console.error(`${colors.yellow}[Memory]${colors.reset} Waiting for engine: ${error.message}`);
|
|
241
241
|
}
|
|
242
242
|
}
|
|
243
|
-
|
|
243
|
+
|
|
244
244
|
// If still not ready, try to initialize now
|
|
245
245
|
if (!memoryEngine) {
|
|
246
246
|
await backgroundInitializeMemoryEngine();
|
|
247
247
|
}
|
|
248
|
-
|
|
248
|
+
|
|
249
249
|
return memoryEngine;
|
|
250
250
|
}
|
|
251
251
|
|
|
@@ -255,18 +255,18 @@ async function getMemoryEngine() {
|
|
|
255
255
|
async function searchMemoryContext(instruction, maxResults = 5) {
|
|
256
256
|
try {
|
|
257
257
|
const engine = await getMemoryEngine();
|
|
258
|
-
|
|
258
|
+
|
|
259
259
|
// Use intelligent search for best context
|
|
260
|
-
const searchResult = await engine.intelligentSearch(instruction, {
|
|
261
|
-
limit: maxResults
|
|
260
|
+
const searchResult = await engine.intelligentSearch(instruction, {
|
|
261
|
+
limit: maxResults
|
|
262
262
|
});
|
|
263
|
-
|
|
263
|
+
|
|
264
264
|
if (!searchResult.results || searchResult.results.length === 0) {
|
|
265
265
|
return '';
|
|
266
266
|
}
|
|
267
267
|
|
|
268
268
|
let context = '';
|
|
269
|
-
|
|
269
|
+
|
|
270
270
|
// Add code context from search results
|
|
271
271
|
context += '\n\n--- MEMORY CONTEXT (Relevant Code) ---\n';
|
|
272
272
|
searchResult.results.forEach((result, idx) => {
|
|
@@ -277,7 +277,7 @@ async function searchMemoryContext(instruction, maxResults = 5) {
|
|
|
277
277
|
context += `${result.code.substring(0, 300)}${result.code.length > 300 ? '...' : ''}\n`;
|
|
278
278
|
}
|
|
279
279
|
});
|
|
280
|
-
|
|
280
|
+
|
|
281
281
|
// Add facts if available
|
|
282
282
|
const facts = await engine.searchFacts(instruction, 5);
|
|
283
283
|
if (facts && facts.length > 0) {
|
|
@@ -286,7 +286,7 @@ async function searchMemoryContext(instruction, maxResults = 5) {
|
|
|
286
286
|
context += `• ${fact.type}: ${fact.name} (${fact.file_id})\n`;
|
|
287
287
|
});
|
|
288
288
|
}
|
|
289
|
-
|
|
289
|
+
|
|
290
290
|
return context;
|
|
291
291
|
} catch (error) {
|
|
292
292
|
console.error(`${colors.yellow}[Memory]${colors.reset} Context search failed: ${error.message}`);
|
|
@@ -304,44 +304,44 @@ async function autoTriggerIntelligence(filePath, instruction) {
|
|
|
304
304
|
if (process.env.MCFAST_INTELLIGENCE_AUTO !== 'true') {
|
|
305
305
|
return;
|
|
306
306
|
}
|
|
307
|
-
|
|
307
|
+
|
|
308
308
|
const engine = await getMemoryEngine();
|
|
309
309
|
const cache = getIntelligenceCache();
|
|
310
|
-
|
|
310
|
+
|
|
311
311
|
// 1. Detect patterns (cached for 5 minutes)
|
|
312
312
|
const patternCacheKey = { file: filePath, type: 'patterns' };
|
|
313
313
|
let patterns = cache.get('patterns', patternCacheKey);
|
|
314
|
-
|
|
314
|
+
|
|
315
315
|
if (!patterns) {
|
|
316
316
|
patterns = await engine.detectPatterns();
|
|
317
317
|
if (patterns && patterns.length > 0) {
|
|
318
318
|
cache.set('patterns', patternCacheKey, patterns);
|
|
319
319
|
console.error(`${colors.cyan}[Intelligence]${colors.reset} 💡 Detected ${patterns.length} patterns`);
|
|
320
|
-
|
|
320
|
+
|
|
321
321
|
// Log high-confidence patterns
|
|
322
322
|
patterns.filter(p => p.confidence > 0.8).forEach(p => {
|
|
323
323
|
console.error(`${colors.yellow}[Pattern]${colors.reset} ${p.type}: ${p.message}`);
|
|
324
324
|
});
|
|
325
325
|
}
|
|
326
326
|
}
|
|
327
|
-
|
|
327
|
+
|
|
328
328
|
// 2. Get suggestions (cached for 5 minutes)
|
|
329
329
|
const suggestionCacheKey = { file: filePath, type: 'suggestions' };
|
|
330
330
|
let suggestions = cache.get('suggestions', suggestionCacheKey);
|
|
331
|
-
|
|
331
|
+
|
|
332
332
|
if (!suggestions) {
|
|
333
333
|
suggestions = await engine.getSuggestions({ currentFile: filePath });
|
|
334
334
|
if (suggestions && suggestions.length > 0) {
|
|
335
335
|
cache.set('suggestions', suggestionCacheKey, suggestions);
|
|
336
336
|
console.error(`${colors.cyan}[Intelligence]${colors.reset} 💡 ${suggestions.length} suggestions available`);
|
|
337
|
-
|
|
337
|
+
|
|
338
338
|
// Show top suggestions
|
|
339
339
|
suggestions.slice(0, 3).forEach(s => {
|
|
340
340
|
console.error(`${colors.yellow}[Suggestion]${colors.reset} ${s.type}: ${s.message}`);
|
|
341
341
|
});
|
|
342
342
|
}
|
|
343
343
|
}
|
|
344
|
-
|
|
344
|
+
|
|
345
345
|
} catch (error) {
|
|
346
346
|
// Silent fail - intelligence should not break the tool
|
|
347
347
|
if (VERBOSE) {
|
|
@@ -357,7 +357,7 @@ async function logEditToMemory({ instruction, files, strategy, success, errorMes
|
|
|
357
357
|
try {
|
|
358
358
|
const engine = await getMemoryEngine();
|
|
359
359
|
const diffSize = Object.keys(files).reduce((total, fp) => total + (files[fp]?.length || 0), 0);
|
|
360
|
-
|
|
360
|
+
|
|
361
361
|
engine.logEditToMemory({
|
|
362
362
|
instruction: instruction.substring(0, 500),
|
|
363
363
|
files: Object.keys(files),
|
|
@@ -367,7 +367,7 @@ async function logEditToMemory({ instruction, files, strategy, success, errorMes
|
|
|
367
367
|
durationMs,
|
|
368
368
|
errorMessage
|
|
369
369
|
});
|
|
370
|
-
|
|
370
|
+
|
|
371
371
|
console.error(`${colors.cyan}[Memory]${colors.reset} Edit logged to history`);
|
|
372
372
|
} catch (error) {
|
|
373
373
|
console.error(`${colors.yellow}[Memory]${colors.reset} Log failed: ${error.message}`);
|
|
@@ -380,13 +380,13 @@ async function logEditToMemory({ instruction, files, strategy, success, errorMes
|
|
|
380
380
|
async function analyzeRenameImpact(symbolName, currentFile = null) {
|
|
381
381
|
try {
|
|
382
382
|
const engine = await getMemoryEngine();
|
|
383
|
-
|
|
383
|
+
|
|
384
384
|
// Search for symbol references across codebase
|
|
385
|
-
const searchResult = await engine.intelligentSearch(symbolName, {
|
|
385
|
+
const searchResult = await engine.intelligentSearch(symbolName, {
|
|
386
386
|
limit: 20,
|
|
387
387
|
currentFile
|
|
388
388
|
});
|
|
389
|
-
|
|
389
|
+
|
|
390
390
|
const impactedFiles = new Set();
|
|
391
391
|
const references = [];
|
|
392
392
|
|
|
@@ -757,6 +757,16 @@ server.setRequestHandler(ListToolsRequestSchema, async () => {
|
|
|
757
757
|
},
|
|
758
758
|
required: []
|
|
759
759
|
}
|
|
760
|
+
},
|
|
761
|
+
// HEALTH CHECK TOOL
|
|
762
|
+
{
|
|
763
|
+
name: "health_check",
|
|
764
|
+
description: "💚 Check MCP server health status and resource usage. Returns: process status, memory engine state, uptime, and system metrics.",
|
|
765
|
+
inputSchema: {
|
|
766
|
+
type: "object",
|
|
767
|
+
properties: {},
|
|
768
|
+
required: []
|
|
769
|
+
}
|
|
760
770
|
}
|
|
761
771
|
],
|
|
762
772
|
};
|
|
@@ -964,6 +974,11 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
|
|
|
964
974
|
result = await projectAnalyzeExecute(args);
|
|
965
975
|
summary = 'Project analyzed';
|
|
966
976
|
}
|
|
977
|
+
// HEALTH CHECK TOOL
|
|
978
|
+
else if (name === "health_check") {
|
|
979
|
+
result = await handleHealthCheck();
|
|
980
|
+
summary = 'Health check completed';
|
|
981
|
+
}
|
|
967
982
|
else {
|
|
968
983
|
throw new Error(`Tool not found: ${name}`);
|
|
969
984
|
}
|
|
@@ -1041,9 +1056,9 @@ async function reportAudit(data) {
|
|
|
1041
1056
|
flushInterval: 5000,
|
|
1042
1057
|
verbose: VERBOSE
|
|
1043
1058
|
});
|
|
1044
|
-
|
|
1059
|
+
|
|
1045
1060
|
queue.add(auditData);
|
|
1046
|
-
|
|
1061
|
+
|
|
1047
1062
|
if (VERBOSE) {
|
|
1048
1063
|
const stats = queue.getStats();
|
|
1049
1064
|
console.error(`${colors.dim}[Audit] Queued for batching. Queue size: ${stats.queued}${colors.reset}`);
|
|
@@ -1082,9 +1097,9 @@ async function handleRegexSearch({ query, path: searchPath, caseSensitive = fals
|
|
|
1082
1097
|
if (!validation.valid) {
|
|
1083
1098
|
const latency = Date.now() - start;
|
|
1084
1099
|
return {
|
|
1085
|
-
content: [{
|
|
1086
|
-
type: "text",
|
|
1087
|
-
text: `⚠️ Invalid regex pattern: ${validation.error}\n\nQuery: "${query}"\n\n💡 Tips:\n• Basic regex works: \\bword\\b, [a-z]+, (foo|bar)\n• Avoid lookbehind (?<=...) and lookahead (?=...) - not supported by grep\n• Use word boundaries \\b instead of lookarounds\n• Escape special chars: \\\\, \\+, \\*`
|
|
1100
|
+
content: [{
|
|
1101
|
+
type: "text",
|
|
1102
|
+
text: `⚠️ Invalid regex pattern: ${validation.error}\n\nQuery: "${query}"\n\n💡 Tips:\n• Basic regex works: \\bword\\b, [a-z]+, (foo|bar)\n• Avoid lookbehind (?<=...) and lookahead (?=...) - not supported by grep\n• Use word boundaries \\b instead of lookarounds\n• Escape special chars: \\\\, \\+, \\*`
|
|
1088
1103
|
}],
|
|
1089
1104
|
isError: true
|
|
1090
1105
|
};
|
|
@@ -1120,12 +1135,12 @@ async function handleRegexSearch({ query, path: searchPath, caseSensitive = fals
|
|
|
1120
1135
|
|
|
1121
1136
|
try {
|
|
1122
1137
|
const { stdout, stderr } = await execAsync(command, { maxBuffer: 10 * 1024 * 1024 }); // 10MB buffer
|
|
1123
|
-
|
|
1138
|
+
|
|
1124
1139
|
// Check for grep regex errors in stderr
|
|
1125
1140
|
if (stderr && stderr.includes('Invalid regular expression')) {
|
|
1126
1141
|
throw new Error(`grep: ${stderr}`);
|
|
1127
1142
|
}
|
|
1128
|
-
|
|
1143
|
+
|
|
1129
1144
|
const results = stdout.trim().split('\n').filter(Boolean);
|
|
1130
1145
|
|
|
1131
1146
|
const latency = Date.now() - start;
|
|
@@ -1190,12 +1205,12 @@ async function handleRegexSearch({ query, path: searchPath, caseSensitive = fals
|
|
|
1190
1205
|
content: [{ type: "text", text: `🔍 Regex Search: No matches found for "${query}" in ${searchPath} (${latency}ms)` }]
|
|
1191
1206
|
};
|
|
1192
1207
|
}
|
|
1193
|
-
|
|
1208
|
+
|
|
1194
1209
|
// Handle regex syntax errors
|
|
1195
1210
|
if (execErr.stderr && execErr.stderr.includes('Invalid')) {
|
|
1196
1211
|
throw new Error(`Invalid regex syntax: ${execErr.stderr}. Try simplifying your pattern.`);
|
|
1197
1212
|
}
|
|
1198
|
-
|
|
1213
|
+
|
|
1199
1214
|
throw execErr;
|
|
1200
1215
|
}
|
|
1201
1216
|
} catch (error) {
|
|
@@ -1222,14 +1237,14 @@ async function handleRegexSearch({ query, path: searchPath, caseSensitive = fals
|
|
|
1222
1237
|
*/
|
|
1223
1238
|
function validateRegex(pattern) {
|
|
1224
1239
|
const warnings = [];
|
|
1225
|
-
|
|
1240
|
+
|
|
1226
1241
|
try {
|
|
1227
1242
|
// Test if it's valid JavaScript regex
|
|
1228
1243
|
new RegExp(pattern);
|
|
1229
1244
|
} catch (e) {
|
|
1230
1245
|
return { valid: false, error: e.message, warnings };
|
|
1231
1246
|
}
|
|
1232
|
-
|
|
1247
|
+
|
|
1233
1248
|
// Check for patterns not supported by basic grep (PCRE-only features)
|
|
1234
1249
|
const unsupportedPatterns = [
|
|
1235
1250
|
{ regex: /\(\?<[=!]/, name: 'Lookbehind (?<=...) or negative lookbehind (?<!...)' },
|
|
@@ -1241,18 +1256,18 @@ function validateRegex(pattern) {
|
|
|
1241
1256
|
{ regex: /\(\?\d*\)/, name: 'Subroutine calls (?1)' },
|
|
1242
1257
|
{ regex: /\(\?&\w+\)/, name: 'Subroutine calls (?&name)' }
|
|
1243
1258
|
];
|
|
1244
|
-
|
|
1259
|
+
|
|
1245
1260
|
for (const { regex, name } of unsupportedPatterns) {
|
|
1246
1261
|
if (regex.test(pattern)) {
|
|
1247
1262
|
warnings.push(`Pattern uses ${name} which may not be supported by grep. Results may be incomplete.`);
|
|
1248
1263
|
}
|
|
1249
1264
|
}
|
|
1250
|
-
|
|
1265
|
+
|
|
1251
1266
|
// Check for potentially dangerous patterns (catastrophic backtracking)
|
|
1252
1267
|
if (/\(\w+\+\+?\)|\(\w+\*\*?\)\*\*?/.test(pattern)) {
|
|
1253
1268
|
warnings.push('Pattern may cause performance issues (nested quantifiers)');
|
|
1254
1269
|
}
|
|
1255
|
-
|
|
1270
|
+
|
|
1256
1271
|
return { valid: true, warnings };
|
|
1257
1272
|
}
|
|
1258
1273
|
|
|
@@ -1282,7 +1297,7 @@ async function handleSearchFilesystem({ query, path: searchPath, isRegex = false
|
|
|
1282
1297
|
// Convert literal query to glob pattern for file search
|
|
1283
1298
|
pattern = `**/*${query}*`;
|
|
1284
1299
|
}
|
|
1285
|
-
|
|
1300
|
+
|
|
1286
1301
|
// Use fast-glob to find files
|
|
1287
1302
|
const files = await fg([pattern], {
|
|
1288
1303
|
cwd,
|
|
@@ -1294,7 +1309,7 @@ async function handleSearchFilesystem({ query, path: searchPath, isRegex = false
|
|
|
1294
1309
|
|
|
1295
1310
|
// Also search file contents if it's not a file pattern
|
|
1296
1311
|
const contentMatches = [];
|
|
1297
|
-
|
|
1312
|
+
|
|
1298
1313
|
// For simple queries, also search in common code files
|
|
1299
1314
|
if (!isRegex && query.length > 1) {
|
|
1300
1315
|
const codeFiles = await fg(['**/*.{js,ts,jsx,tsx,py,go,rs,java,rb,php}'], {
|
|
@@ -1302,7 +1317,7 @@ async function handleSearchFilesystem({ query, path: searchPath, isRegex = false
|
|
|
1302
1317
|
ignore: ['node_modules/**', '.git/**'],
|
|
1303
1318
|
absolute: false
|
|
1304
1319
|
});
|
|
1305
|
-
|
|
1320
|
+
|
|
1306
1321
|
// Search in first 50 files to avoid performance issues
|
|
1307
1322
|
for (const file of codeFiles.slice(0, 50)) {
|
|
1308
1323
|
try {
|
|
@@ -1310,7 +1325,7 @@ async function handleSearchFilesystem({ query, path: searchPath, isRegex = false
|
|
|
1310
1325
|
if (content.includes(query)) {
|
|
1311
1326
|
const lines = content.split('\n');
|
|
1312
1327
|
const matches = [];
|
|
1313
|
-
|
|
1328
|
+
|
|
1314
1329
|
lines.forEach((line, idx) => {
|
|
1315
1330
|
if (line.includes(query)) {
|
|
1316
1331
|
matches.push({
|
|
@@ -1320,7 +1335,7 @@ async function handleSearchFilesystem({ query, path: searchPath, isRegex = false
|
|
|
1320
1335
|
});
|
|
1321
1336
|
}
|
|
1322
1337
|
});
|
|
1323
|
-
|
|
1338
|
+
|
|
1324
1339
|
if (matches.length > 0) {
|
|
1325
1340
|
contentMatches.push({
|
|
1326
1341
|
file,
|
|
@@ -1335,7 +1350,7 @@ async function handleSearchFilesystem({ query, path: searchPath, isRegex = false
|
|
|
1335
1350
|
}
|
|
1336
1351
|
|
|
1337
1352
|
const latency = Date.now() - start;
|
|
1338
|
-
|
|
1353
|
+
|
|
1339
1354
|
// Format output
|
|
1340
1355
|
let output = `${colors.bold}Filesystem Search Results${colors.reset}\n\n`;
|
|
1341
1356
|
output += `Query: "${query}"\n`;
|
|
@@ -1381,7 +1396,7 @@ async function handleSearchFilesystem({ query, path: searchPath, isRegex = false
|
|
|
1381
1396
|
};
|
|
1382
1397
|
} catch (error) {
|
|
1383
1398
|
const latency = Date.now() - start;
|
|
1384
|
-
|
|
1399
|
+
|
|
1385
1400
|
reportAudit({
|
|
1386
1401
|
tool: 'search_filesystem',
|
|
1387
1402
|
instruction: `Search: ${query}`,
|
|
@@ -1441,7 +1456,7 @@ async function handleReapply({ instruction, files, errorContext = "", attempt =
|
|
|
1441
1456
|
*/
|
|
1442
1457
|
async function handleEdit({ instruction, files, code_edit, dryRun = false }) {
|
|
1443
1458
|
const editStartTime = Date.now();
|
|
1444
|
-
|
|
1459
|
+
|
|
1445
1460
|
// Validate required parameters
|
|
1446
1461
|
if (!instruction) {
|
|
1447
1462
|
return {
|
|
@@ -1449,16 +1464,16 @@ async function handleEdit({ instruction, files, code_edit, dryRun = false }) {
|
|
|
1449
1464
|
isError: true
|
|
1450
1465
|
};
|
|
1451
1466
|
}
|
|
1452
|
-
|
|
1467
|
+
|
|
1453
1468
|
if (!files || Object.keys(files).length === 0) {
|
|
1454
1469
|
return {
|
|
1455
1470
|
content: [{ type: "text", text: "❌ Error: 'files' parameter is required and must contain at least one file" }],
|
|
1456
1471
|
isError: true
|
|
1457
1472
|
};
|
|
1458
1473
|
}
|
|
1459
|
-
|
|
1474
|
+
|
|
1460
1475
|
const firstFile = Object.keys(files)[0];
|
|
1461
|
-
|
|
1476
|
+
|
|
1462
1477
|
// 1. Retrieve memory context for enhanced understanding
|
|
1463
1478
|
let memoryContext = '';
|
|
1464
1479
|
try {
|
|
@@ -1469,7 +1484,7 @@ async function handleEdit({ instruction, files, code_edit, dryRun = false }) {
|
|
|
1469
1484
|
} catch (error) {
|
|
1470
1485
|
console.error(`${colors.yellow}[Memory]${colors.reset} Context retrieval failed: ${error.message}`);
|
|
1471
1486
|
}
|
|
1472
|
-
|
|
1487
|
+
|
|
1473
1488
|
// 2. Analyze impact for rename operations
|
|
1474
1489
|
let impactAnalysis = null;
|
|
1475
1490
|
const renamePattern = instruction.match(/(?:rename|change)\s+(?:function|class|const|let|var|method)\s+(\w+)\s+to\s+(\w+)/i);
|
|
@@ -1484,7 +1499,7 @@ async function handleEdit({ instruction, files, code_edit, dryRun = false }) {
|
|
|
1484
1499
|
console.error(`${colors.yellow}[IMPACT]${colors.reset} Analysis failed: ${error.message}`);
|
|
1485
1500
|
}
|
|
1486
1501
|
}
|
|
1487
|
-
|
|
1502
|
+
|
|
1488
1503
|
// Check for multi-file edits first
|
|
1489
1504
|
const multiFileEdit = detectCrossFileEdit(instruction, files);
|
|
1490
1505
|
|
|
@@ -1518,7 +1533,7 @@ async function handleEdit({ instruction, files, code_edit, dryRun = false }) {
|
|
|
1518
1533
|
errorMessage: result.message || result.error,
|
|
1519
1534
|
durationMs: Date.now() - editStartTime
|
|
1520
1535
|
});
|
|
1521
|
-
|
|
1536
|
+
|
|
1522
1537
|
return {
|
|
1523
1538
|
content: [{
|
|
1524
1539
|
type: "text",
|
|
@@ -1556,7 +1571,7 @@ async function handleEdit({ instruction, files, code_edit, dryRun = false }) {
|
|
|
1556
1571
|
errorMessage: error.message,
|
|
1557
1572
|
durationMs: Date.now() - editStartTime
|
|
1558
1573
|
});
|
|
1559
|
-
|
|
1574
|
+
|
|
1560
1575
|
return {
|
|
1561
1576
|
content: [{
|
|
1562
1577
|
type: "text",
|
|
@@ -1661,7 +1676,7 @@ async function handleEdit({ instruction, files, code_edit, dryRun = false }) {
|
|
|
1661
1676
|
errorMessage: error.message,
|
|
1662
1677
|
durationMs: Date.now() - editStartTime
|
|
1663
1678
|
});
|
|
1664
|
-
|
|
1679
|
+
|
|
1665
1680
|
return {
|
|
1666
1681
|
content: [{
|
|
1667
1682
|
type: "text",
|
|
@@ -1792,7 +1807,7 @@ async function handleEdit({ instruction, files, code_edit, dryRun = false }) {
|
|
|
1792
1807
|
|
|
1793
1808
|
// Include impact analysis in result if available
|
|
1794
1809
|
let resultText = `✅ AST Refactor Applied Successfully\n\nOperation: ${pattern.type}\nChanges: ${transformResult.count || transformResult.replacements || 0} locations\n\nBackup: ${editResult.backupPath}`;
|
|
1795
|
-
|
|
1810
|
+
|
|
1796
1811
|
if (impactAnalysis && impactAnalysis.suggestion) {
|
|
1797
1812
|
resultText += `\n\n${impactAnalysis.suggestion}`;
|
|
1798
1813
|
if (impactAnalysis.impactedFiles.length > 0) {
|
|
@@ -1823,7 +1838,7 @@ async function handleEdit({ instruction, files, code_edit, dryRun = false }) {
|
|
|
1823
1838
|
errorMessage: error.message,
|
|
1824
1839
|
durationMs: Date.now() - editStartTime
|
|
1825
1840
|
});
|
|
1826
|
-
|
|
1841
|
+
|
|
1827
1842
|
return {
|
|
1828
1843
|
content: [{
|
|
1829
1844
|
type: "text",
|
|
@@ -1846,7 +1861,7 @@ async function handleEdit({ instruction, files, code_edit, dryRun = false }) {
|
|
|
1846
1861
|
dryRun,
|
|
1847
1862
|
toolName: 'edit'
|
|
1848
1863
|
});
|
|
1849
|
-
|
|
1864
|
+
|
|
1850
1865
|
// Log to memory
|
|
1851
1866
|
await logEditToMemory({
|
|
1852
1867
|
instruction,
|
|
@@ -1856,7 +1871,7 @@ async function handleEdit({ instruction, files, code_edit, dryRun = false }) {
|
|
|
1856
1871
|
errorMessage: result.isError ? result.content?.[0]?.text : null,
|
|
1857
1872
|
durationMs: Date.now() - editStartTime
|
|
1858
1873
|
});
|
|
1859
|
-
|
|
1874
|
+
|
|
1860
1875
|
return result;
|
|
1861
1876
|
}
|
|
1862
1877
|
}
|
|
@@ -1869,7 +1884,7 @@ async function handleEdit({ instruction, files, code_edit, dryRun = false }) {
|
|
|
1869
1884
|
dryRun,
|
|
1870
1885
|
toolName: 'edit'
|
|
1871
1886
|
});
|
|
1872
|
-
|
|
1887
|
+
|
|
1873
1888
|
// Log to memory
|
|
1874
1889
|
await logEditToMemory({
|
|
1875
1890
|
instruction,
|
|
@@ -1879,7 +1894,7 @@ async function handleEdit({ instruction, files, code_edit, dryRun = false }) {
|
|
|
1879
1894
|
errorMessage: result.isError ? result.content?.[0]?.text : null,
|
|
1880
1895
|
durationMs: Date.now() - editStartTime
|
|
1881
1896
|
});
|
|
1882
|
-
|
|
1897
|
+
|
|
1883
1898
|
return result;
|
|
1884
1899
|
}
|
|
1885
1900
|
|
|
@@ -1889,14 +1904,14 @@ async function handleEdit({ instruction, files, code_edit, dryRun = false }) {
|
|
|
1889
1904
|
if (memoryContext) {
|
|
1890
1905
|
enhancedInstruction = `${instruction}\n\n--- CONTEXT FROM CODEBASE ---${memoryContext}`;
|
|
1891
1906
|
}
|
|
1892
|
-
|
|
1907
|
+
|
|
1893
1908
|
const result = await handleApplyFast({
|
|
1894
1909
|
instruction: enhancedInstruction,
|
|
1895
1910
|
files,
|
|
1896
1911
|
dryRun,
|
|
1897
1912
|
toolName: 'edit'
|
|
1898
1913
|
});
|
|
1899
|
-
|
|
1914
|
+
|
|
1900
1915
|
// Log to memory
|
|
1901
1916
|
await logEditToMemory({
|
|
1902
1917
|
instruction,
|
|
@@ -1906,13 +1921,13 @@ async function handleEdit({ instruction, files, code_edit, dryRun = false }) {
|
|
|
1906
1921
|
errorMessage: result.isError ? result.content?.[0]?.text : null,
|
|
1907
1922
|
durationMs: Date.now() - editStartTime
|
|
1908
1923
|
});
|
|
1909
|
-
|
|
1924
|
+
|
|
1910
1925
|
// Auto-trigger intelligence tools in background (non-blocking)
|
|
1911
1926
|
if (!result.isError) {
|
|
1912
1927
|
const firstFile = Object.keys(files)[0];
|
|
1913
|
-
autoTriggerIntelligence(firstFile, instruction).catch(() => {});
|
|
1928
|
+
autoTriggerIntelligence(firstFile, instruction).catch(() => { });
|
|
1914
1929
|
}
|
|
1915
|
-
|
|
1930
|
+
|
|
1916
1931
|
return result;
|
|
1917
1932
|
}
|
|
1918
1933
|
|
|
@@ -1924,7 +1939,7 @@ async function handleEdit({ instruction, files, code_edit, dryRun = false }) {
|
|
|
1924
1939
|
*/
|
|
1925
1940
|
async function handleSearch({ query, files, path, mode = 'auto', regex = false, caseSensitive = false, contextLines = 2, maxResults = 10 }) {
|
|
1926
1941
|
const searchStartTime = Date.now();
|
|
1927
|
-
|
|
1942
|
+
|
|
1928
1943
|
// For regex mode without files, we need to do content-based regex search
|
|
1929
1944
|
// since fast-glob doesn't support full regex patterns
|
|
1930
1945
|
if (regex && !files && mode === 'auto') {
|
|
@@ -1939,9 +1954,9 @@ async function handleSearch({ query, files, path, mode = 'auto', regex = false,
|
|
|
1939
1954
|
if ((detectedMode === 'auto' || detectedMode === 'local') && !regex) {
|
|
1940
1955
|
try {
|
|
1941
1956
|
const engine = await getMemoryEngine();
|
|
1942
|
-
|
|
1957
|
+
|
|
1943
1958
|
// Use intelligent search (UltraHybrid + Smart Routing)
|
|
1944
|
-
const searchResult = await engine.intelligentSearch(query, {
|
|
1959
|
+
const searchResult = await engine.intelligentSearch(query, {
|
|
1945
1960
|
limit: maxResults,
|
|
1946
1961
|
currentFile: files ? Object.keys(files)[0] : null
|
|
1947
1962
|
});
|
|
@@ -2017,7 +2032,7 @@ async function handleRead({ filePath, filePaths, start_line, end_line, max_lines
|
|
|
2017
2032
|
if (filePaths && Array.isArray(filePaths) && filePaths.length > 0) {
|
|
2018
2033
|
return await handleBatchRead(filePaths, start_line, end_line, max_lines_per_file);
|
|
2019
2034
|
}
|
|
2020
|
-
|
|
2035
|
+
|
|
2021
2036
|
// Single file mode (backward compatible)
|
|
2022
2037
|
return await handleReadFile({ path: filePath, start_line, end_line });
|
|
2023
2038
|
}
|
|
@@ -2028,50 +2043,50 @@ async function handleRead({ filePath, filePaths, start_line, end_line, max_lines
|
|
|
2028
2043
|
async function handleBatchRead(filePaths, start_line, end_line, max_lines_per_file) {
|
|
2029
2044
|
const start = Date.now();
|
|
2030
2045
|
const MAX_BATCH_SIZE = 50;
|
|
2031
|
-
|
|
2046
|
+
|
|
2032
2047
|
if (filePaths.length > MAX_BATCH_SIZE) {
|
|
2033
2048
|
return {
|
|
2034
2049
|
content: [{ type: "text", text: `❌ Batch read limit exceeded: ${filePaths.length} files (max ${MAX_BATCH_SIZE})` }],
|
|
2035
2050
|
isError: true
|
|
2036
2051
|
};
|
|
2037
2052
|
}
|
|
2038
|
-
|
|
2053
|
+
|
|
2039
2054
|
console.error(`${colors.cyan}[BATCH READ]${colors.reset} ${filePaths.length} files`);
|
|
2040
|
-
|
|
2055
|
+
|
|
2041
2056
|
// Read all files in parallel with Promise.all
|
|
2042
2057
|
const results = await Promise.all(
|
|
2043
2058
|
filePaths.map(async (filePath) => {
|
|
2044
2059
|
try {
|
|
2045
|
-
const result = await handleReadFileInternal({
|
|
2046
|
-
path: filePath,
|
|
2047
|
-
start_line,
|
|
2060
|
+
const result = await handleReadFileInternal({
|
|
2061
|
+
path: filePath,
|
|
2062
|
+
start_line,
|
|
2048
2063
|
end_line,
|
|
2049
|
-
max_lines: max_lines_per_file
|
|
2064
|
+
max_lines: max_lines_per_file
|
|
2050
2065
|
});
|
|
2051
2066
|
return { path: filePath, ...result, success: true };
|
|
2052
2067
|
} catch (error) {
|
|
2053
|
-
return {
|
|
2054
|
-
path: filePath,
|
|
2055
|
-
success: false,
|
|
2068
|
+
return {
|
|
2069
|
+
path: filePath,
|
|
2070
|
+
success: false,
|
|
2056
2071
|
error: error.message,
|
|
2057
2072
|
content: `❌ Error reading ${filePath}: ${error.message}`
|
|
2058
2073
|
};
|
|
2059
2074
|
}
|
|
2060
2075
|
})
|
|
2061
2076
|
);
|
|
2062
|
-
|
|
2077
|
+
|
|
2063
2078
|
// Calculate stats
|
|
2064
2079
|
const successful = results.filter(r => r.success).length;
|
|
2065
2080
|
const failed = results.filter(r => !r.success).length;
|
|
2066
2081
|
const totalLines = results.reduce((sum, r) => sum + (r.lines || 0), 0);
|
|
2067
2082
|
const totalSize = results.reduce((sum, r) => sum + (r.size || 0), 0);
|
|
2068
|
-
|
|
2083
|
+
|
|
2069
2084
|
// Format output
|
|
2070
2085
|
let output = `📚 Batch Read Complete (${filePaths.length} files)\n`;
|
|
2071
2086
|
output += `✅ Successful: ${successful} ❌ Failed: ${failed}\n`;
|
|
2072
2087
|
output += `📊 Total: ${totalLines} lines, ${(totalSize / 1024).toFixed(1)} KB\n`;
|
|
2073
2088
|
output += `⏱️ Latency: ${Date.now() - start}ms\n\n`;
|
|
2074
|
-
|
|
2089
|
+
|
|
2075
2090
|
// Add each file's content
|
|
2076
2091
|
results.forEach((result, index) => {
|
|
2077
2092
|
output += `[${index + 1}/${filePaths.length}] `;
|
|
@@ -2090,7 +2105,7 @@ async function handleBatchRead(filePaths, start_line, end_line, max_lines_per_fi
|
|
|
2090
2105
|
}
|
|
2091
2106
|
output += `\n\n`;
|
|
2092
2107
|
});
|
|
2093
|
-
|
|
2108
|
+
|
|
2094
2109
|
// Report audit for batch operation
|
|
2095
2110
|
reportAudit({
|
|
2096
2111
|
tool: 'read_file',
|
|
@@ -2101,7 +2116,7 @@ async function handleBatchRead(filePaths, start_line, end_line, max_lines_per_fi
|
|
|
2101
2116
|
input_tokens: Math.ceil(filePaths.join(',').length / 4),
|
|
2102
2117
|
output_tokens: Math.ceil(output.length / 4)
|
|
2103
2118
|
});
|
|
2104
|
-
|
|
2119
|
+
|
|
2105
2120
|
return {
|
|
2106
2121
|
content: [{ type: "text", text: output }]
|
|
2107
2122
|
};
|
|
@@ -2119,7 +2134,7 @@ async function handleReadFileInternal({ path: filePath, start_line, end_line, ma
|
|
|
2119
2134
|
}
|
|
2120
2135
|
|
|
2121
2136
|
const STREAM_THRESHOLD = 1024 * 1024; // 1MB
|
|
2122
|
-
|
|
2137
|
+
|
|
2123
2138
|
let startLine = start_line ? parseInt(start_line) : 1;
|
|
2124
2139
|
let endLine = end_line ? parseInt(end_line) : -1;
|
|
2125
2140
|
let outputContent;
|
|
@@ -2247,12 +2262,12 @@ async function handleWarpgrep({ query, include = ".", isRegex = false, caseSensi
|
|
|
2247
2262
|
});
|
|
2248
2263
|
return { content: [{ type: "text", text: msg }] };
|
|
2249
2264
|
}
|
|
2250
|
-
|
|
2265
|
+
|
|
2251
2266
|
// Handle regex syntax errors
|
|
2252
2267
|
if (execErr.stderr && execErr.stderr.includes('Invalid')) {
|
|
2253
2268
|
throw new Error(`Invalid regex syntax: ${execErr.stderr}. Try simplifying your pattern.`);
|
|
2254
2269
|
}
|
|
2255
|
-
|
|
2270
|
+
|
|
2256
2271
|
throw execErr;
|
|
2257
2272
|
}
|
|
2258
2273
|
} catch (error) {
|
|
@@ -3078,18 +3093,18 @@ async function handleMemorySearch({ query, type = 'all', maxResults = 6, minScor
|
|
|
3078
3093
|
*/
|
|
3079
3094
|
async function handleMemoryGet({ type }) {
|
|
3080
3095
|
const start = Date.now();
|
|
3081
|
-
|
|
3096
|
+
|
|
3082
3097
|
// Early return if memory engine not ready
|
|
3083
3098
|
if (!memoryEngineReady) {
|
|
3084
3099
|
return {
|
|
3085
|
-
content: [{
|
|
3086
|
-
type: "text",
|
|
3087
|
-
text: `⏳ Memory engine is still initializing. Please try again in a few seconds.`
|
|
3100
|
+
content: [{
|
|
3101
|
+
type: "text",
|
|
3102
|
+
text: `⏳ Memory engine is still initializing. Please try again in a few seconds.`
|
|
3088
3103
|
}],
|
|
3089
3104
|
isError: true
|
|
3090
3105
|
};
|
|
3091
3106
|
}
|
|
3092
|
-
|
|
3107
|
+
|
|
3093
3108
|
try {
|
|
3094
3109
|
const engine = await getMemoryEngine();
|
|
3095
3110
|
let output = '';
|
|
@@ -3123,12 +3138,12 @@ async function handleMemoryGet({ type }) {
|
|
|
3123
3138
|
} else if (type === 'reindex') {
|
|
3124
3139
|
output = `🔄 Re-indexing Codebase\n\n`;
|
|
3125
3140
|
output += `Starting re-index in background...\n`;
|
|
3126
|
-
|
|
3141
|
+
|
|
3127
3142
|
// Trigger re-index in background
|
|
3128
3143
|
engine.performInitialScan().catch(err => {
|
|
3129
3144
|
console.error('[Reindex] Error:', err.message);
|
|
3130
3145
|
});
|
|
3131
|
-
|
|
3146
|
+
|
|
3132
3147
|
output += `✅ Re-index started. Check stats in a few seconds.\n`;
|
|
3133
3148
|
output += `Use 'memory_get stats' to check progress.`;
|
|
3134
3149
|
} else if (type === 'intelligence') {
|
|
@@ -3176,18 +3191,18 @@ async function handleMemoryGet({ type }) {
|
|
|
3176
3191
|
*/
|
|
3177
3192
|
async function handleDetectPatterns() {
|
|
3178
3193
|
const start = Date.now();
|
|
3179
|
-
|
|
3194
|
+
|
|
3180
3195
|
// Early return if memory engine not ready
|
|
3181
3196
|
if (!memoryEngineReady) {
|
|
3182
3197
|
return {
|
|
3183
|
-
content: [{
|
|
3184
|
-
type: "text",
|
|
3185
|
-
text: `⏳ Memory engine is still initializing. Please try again in a few seconds.`
|
|
3198
|
+
content: [{
|
|
3199
|
+
type: "text",
|
|
3200
|
+
text: `⏳ Memory engine is still initializing. Please try again in a few seconds.`
|
|
3186
3201
|
}],
|
|
3187
3202
|
isError: true
|
|
3188
3203
|
};
|
|
3189
3204
|
}
|
|
3190
|
-
|
|
3205
|
+
|
|
3191
3206
|
try {
|
|
3192
3207
|
const engine = await getMemoryEngine();
|
|
3193
3208
|
const patterns = await engine.detectPatterns();
|
|
@@ -3235,18 +3250,18 @@ async function handleDetectPatterns() {
|
|
|
3235
3250
|
*/
|
|
3236
3251
|
async function handleGetSuggestions({ currentFile, currentLine }) {
|
|
3237
3252
|
const start = Date.now();
|
|
3238
|
-
|
|
3253
|
+
|
|
3239
3254
|
// Early return if memory engine not ready
|
|
3240
3255
|
if (!memoryEngineReady) {
|
|
3241
3256
|
return {
|
|
3242
|
-
content: [{
|
|
3243
|
-
type: "text",
|
|
3244
|
-
text: `⏳ Memory engine is still initializing. Please try again in a few seconds.`
|
|
3257
|
+
content: [{
|
|
3258
|
+
type: "text",
|
|
3259
|
+
text: `⏳ Memory engine is still initializing. Please try again in a few seconds.`
|
|
3245
3260
|
}],
|
|
3246
3261
|
isError: true
|
|
3247
3262
|
};
|
|
3248
3263
|
}
|
|
3249
|
-
|
|
3264
|
+
|
|
3250
3265
|
try {
|
|
3251
3266
|
const engine = await getMemoryEngine();
|
|
3252
3267
|
const suggestions = await engine.getSuggestions({
|
|
@@ -3300,18 +3315,18 @@ async function handleGetSuggestions({ currentFile, currentLine }) {
|
|
|
3300
3315
|
*/
|
|
3301
3316
|
async function handleSelectStrategy({ instruction, files = [] }) {
|
|
3302
3317
|
const start = Date.now();
|
|
3303
|
-
|
|
3318
|
+
|
|
3304
3319
|
// Early return if memory engine not ready
|
|
3305
3320
|
if (!memoryEngineReady) {
|
|
3306
3321
|
return {
|
|
3307
|
-
content: [{
|
|
3308
|
-
type: "text",
|
|
3309
|
-
text: `⏳ Memory engine is still initializing. Please try again in a few seconds.`
|
|
3322
|
+
content: [{
|
|
3323
|
+
type: "text",
|
|
3324
|
+
text: `⏳ Memory engine is still initializing. Please try again in a few seconds.`
|
|
3310
3325
|
}],
|
|
3311
3326
|
isError: true
|
|
3312
3327
|
};
|
|
3313
3328
|
}
|
|
3314
|
-
|
|
3329
|
+
|
|
3315
3330
|
try {
|
|
3316
3331
|
const engine = await getMemoryEngine();
|
|
3317
3332
|
const result = await engine.selectStrategy(instruction, { files });
|
|
@@ -3352,6 +3367,74 @@ async function handleSelectStrategy({ instruction, files = [] }) {
|
|
|
3352
3367
|
}
|
|
3353
3368
|
}
|
|
3354
3369
|
|
|
3370
|
+
/**
|
|
3371
|
+
* HEALTH CHECK HANDLER
|
|
3372
|
+
*/
|
|
3373
|
+
async function handleHealthCheck() {
|
|
3374
|
+
const start = Date.now();
|
|
3375
|
+
|
|
3376
|
+
try {
|
|
3377
|
+
const memoryStats = memoryEngine ? await memoryEngine.getStats() : null;
|
|
3378
|
+
const watcherStats = memoryEngine?.watcher ? memoryEngine.watcher.getStats() : null;
|
|
3379
|
+
|
|
3380
|
+
const health = {
|
|
3381
|
+
status: 'healthy',
|
|
3382
|
+
timestamp: Date.now(),
|
|
3383
|
+
pid: process.pid,
|
|
3384
|
+
uptime: process.uptime(),
|
|
3385
|
+
memoryUsage: {
|
|
3386
|
+
rss: Math.round(process.memoryUsage().rss / 1024 / 1024) + ' MB',
|
|
3387
|
+
heapTotal: Math.round(process.memoryUsage().heapTotal / 1024 / 1024) + ' MB',
|
|
3388
|
+
heapUsed: Math.round(process.memoryUsage().heapUsed / 1024 / 1024) + ' MB',
|
|
3389
|
+
external: Math.round(process.memoryUsage().external / 1024 / 1024) + ' MB'
|
|
3390
|
+
},
|
|
3391
|
+
memoryEngine: {
|
|
3392
|
+
ready: memoryEngineReady,
|
|
3393
|
+
initialized: memoryEngine?.isInitialized || false,
|
|
3394
|
+
stats: memoryStats
|
|
3395
|
+
},
|
|
3396
|
+
fileWatcher: watcherStats || {
|
|
3397
|
+
enabled: process.env.MCFAST_FILE_WATCHER !== 'false',
|
|
3398
|
+
status: 'not_running'
|
|
3399
|
+
},
|
|
3400
|
+
latencyMs: Date.now() - start
|
|
3401
|
+
};
|
|
3402
|
+
|
|
3403
|
+
reportAudit({
|
|
3404
|
+
tool: 'health_check',
|
|
3405
|
+
status: 'success',
|
|
3406
|
+
latency_ms: health.latencyMs
|
|
3407
|
+
});
|
|
3408
|
+
|
|
3409
|
+
return {
|
|
3410
|
+
content: [{
|
|
3411
|
+
type: 'text',
|
|
3412
|
+
text: JSON.stringify(health, null, 2)
|
|
3413
|
+
}]
|
|
3414
|
+
};
|
|
3415
|
+
|
|
3416
|
+
} catch (error) {
|
|
3417
|
+
reportAudit({
|
|
3418
|
+
tool: 'health_check',
|
|
3419
|
+
status: 'error',
|
|
3420
|
+
error_message: error.message,
|
|
3421
|
+
latency_ms: Date.now() - start
|
|
3422
|
+
});
|
|
3423
|
+
|
|
3424
|
+
return {
|
|
3425
|
+
content: [{
|
|
3426
|
+
type: 'text',
|
|
3427
|
+
text: JSON.stringify({
|
|
3428
|
+
status: 'error',
|
|
3429
|
+
error: error.message,
|
|
3430
|
+
timestamp: Date.now()
|
|
3431
|
+
})
|
|
3432
|
+
}],
|
|
3433
|
+
isError: true
|
|
3434
|
+
};
|
|
3435
|
+
}
|
|
3436
|
+
}
|
|
3437
|
+
|
|
3355
3438
|
/**
|
|
3356
3439
|
* Start Server
|
|
3357
3440
|
*/
|
|
@@ -3359,13 +3442,28 @@ async function handleSelectStrategy({ instruction, files = [] }) {
|
|
|
3359
3442
|
// Acquire lock before starting - prevents multiple instances
|
|
3360
3443
|
const lockAcquired = await acquireLock();
|
|
3361
3444
|
if (!lockAcquired) {
|
|
3362
|
-
|
|
3363
|
-
|
|
3364
|
-
|
|
3445
|
+
console.error(`${colors.red}[ERROR]${colors.reset} Failed to acquire lock. Another mcfast-mcp instance may be running.`);
|
|
3446
|
+
console.error(`${colors.yellow}[HINT]${colors.reset} Delete ${LOCK_FILE_PATH} if you're sure no other instance is running.`);
|
|
3447
|
+
process.exit(1);
|
|
3365
3448
|
}
|
|
3366
3449
|
|
|
3367
3450
|
const transport = new StdioServerTransport();
|
|
3368
3451
|
|
|
3452
|
+
// NOTE: Do NOT add process.stdin.on('end') handler here.
|
|
3453
|
+
// VSCode and some MCP clients send ephemeral EOF / half-close signals during
|
|
3454
|
+
// the initialize handshake, which would cause premature gracefulShutdown
|
|
3455
|
+
// before the server has a chance to respond, resulting in the
|
|
3456
|
+
// "Error: calling 'initialize': EOF" error.
|
|
3457
|
+
//
|
|
3458
|
+
// The MCP SDK's StdioServerTransport already manages transport lifecycle.
|
|
3459
|
+
// Only handle unrecoverable stdout errors.
|
|
3460
|
+
process.stdout.on('error', (err) => {
|
|
3461
|
+
if (err.code === 'EPIPE' || err.code === 'ERR_STREAM_DESTROYED') {
|
|
3462
|
+
console.error('[MCP] stdout closed, shutting down:', err.code);
|
|
3463
|
+
gracefulShutdown('stdout.error');
|
|
3464
|
+
}
|
|
3465
|
+
});
|
|
3466
|
+
|
|
3369
3467
|
// Pre-initialize memory engine in background
|
|
3370
3468
|
backgroundInitializeMemoryEngine().catch(err => {
|
|
3371
3469
|
console.error(`${colors.yellow}[Memory]${colors.reset} Background init error: ${err.message}`);
|
|
@@ -3374,31 +3472,50 @@ backgroundInitializeMemoryEngine().catch(err => {
|
|
|
3374
3472
|
|
|
3375
3473
|
// Graceful shutdown handler
|
|
3376
3474
|
async function gracefulShutdown(signal) {
|
|
3377
|
-
|
|
3378
|
-
|
|
3379
|
-
|
|
3380
|
-
|
|
3381
|
-
|
|
3382
|
-
|
|
3383
|
-
|
|
3384
|
-
|
|
3385
|
-
|
|
3386
|
-
|
|
3387
|
-
|
|
3388
|
-
|
|
3389
|
-
|
|
3390
|
-
|
|
3391
|
-
|
|
3392
|
-
|
|
3393
|
-
|
|
3394
|
-
|
|
3395
|
-
|
|
3396
|
-
|
|
3397
|
-
|
|
3475
|
+
console.error(`${colors.yellow}[Shutdown]${colors.reset} Received ${signal}, cleaning up...`);
|
|
3476
|
+
|
|
3477
|
+
const SHUTDOWN_TIMEOUT = 5000; // 5 seconds per MCP best practices
|
|
3478
|
+
|
|
3479
|
+
try {
|
|
3480
|
+
await Promise.race([
|
|
3481
|
+
(async () => {
|
|
3482
|
+
// Stop memory engine
|
|
3483
|
+
if (memoryEngine && memoryEngineReady) {
|
|
3484
|
+
await memoryEngine.cleanup();
|
|
3485
|
+
}
|
|
3486
|
+
|
|
3487
|
+
// Flush audit queue
|
|
3488
|
+
const auditQ = getAuditQueue();
|
|
3489
|
+
await auditQ.destroy();
|
|
3490
|
+
|
|
3491
|
+
// Release lock
|
|
3492
|
+
await releaseLock();
|
|
3493
|
+
})(),
|
|
3494
|
+
new Promise(resolve => setTimeout(resolve, SHUTDOWN_TIMEOUT))
|
|
3495
|
+
]);
|
|
3496
|
+
|
|
3497
|
+
console.error(`${colors.green}[Shutdown]${colors.reset} Cleanup complete`);
|
|
3498
|
+
} catch (error) {
|
|
3499
|
+
console.error(`${colors.red}[Shutdown]${colors.reset} Error during cleanup: ${error.message}`);
|
|
3500
|
+
}
|
|
3501
|
+
|
|
3502
|
+
// Always exit (even if cleanup fails) to prevent zombie processes
|
|
3503
|
+
process.exit(0);
|
|
3398
3504
|
}
|
|
3399
3505
|
|
|
3400
3506
|
process.on('SIGINT', () => gracefulShutdown('SIGINT'));
|
|
3401
3507
|
process.on('SIGTERM', () => gracefulShutdown('SIGTERM'));
|
|
3402
3508
|
|
|
3509
|
+
// Protect against zombie processes - handle unexpected errors
|
|
3510
|
+
process.on('uncaughtException', (err) => {
|
|
3511
|
+
console.error('[MCP] Uncaught exception:', err);
|
|
3512
|
+
gracefulShutdown('uncaughtException');
|
|
3513
|
+
});
|
|
3514
|
+
|
|
3515
|
+
process.on('unhandledRejection', (reason, promise) => {
|
|
3516
|
+
console.error('[MCP] Unhandled rejection:', reason);
|
|
3517
|
+
gracefulShutdown('unhandledRejection');
|
|
3518
|
+
});
|
|
3519
|
+
|
|
3403
3520
|
await server.connect(transport);
|
|
3404
3521
|
console.error("mcfast MCP v1.0.0 running on stdio");
|
|
@@ -225,23 +225,30 @@ export class MemoryEngine {
|
|
|
225
225
|
});
|
|
226
226
|
}
|
|
227
227
|
|
|
228
|
-
// Initialize file watcher
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
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)');
|
|
245
252
|
this.watcher = null;
|
|
246
253
|
}
|
|
247
254
|
|
|
@@ -453,19 +453,21 @@ export class FileWatcher {
|
|
|
453
453
|
|
|
454
454
|
console.error('[FileWatcher] Stopping...');
|
|
455
455
|
|
|
456
|
-
// Stop cleanup interval
|
|
456
|
+
// 1. Stop cleanup interval
|
|
457
457
|
if (this.cleanupInterval) {
|
|
458
458
|
clearInterval(this.cleanupInterval);
|
|
459
459
|
this.cleanupInterval = null;
|
|
460
460
|
}
|
|
461
461
|
|
|
462
|
-
//
|
|
463
|
-
|
|
464
|
-
|
|
465
|
-
|
|
462
|
+
// 2. Clear pending updates map (free memory)
|
|
463
|
+
this.pendingUpdates.clear();
|
|
464
|
+
|
|
465
|
+
// 3. Cancel debounce timeout if exists
|
|
466
|
+
if (this.flushQueue && this.flushQueue.cancel) {
|
|
467
|
+
this.flushQueue.cancel();
|
|
466
468
|
}
|
|
467
469
|
|
|
468
|
-
// Close watcher
|
|
470
|
+
// 4. Close chokidar watcher (critical - prevents persistent file watching)
|
|
469
471
|
if (this.watcher) {
|
|
470
472
|
await this.watcher.close();
|
|
471
473
|
this.watcher = null;
|