@mrxkun/mcfast-mcp 4.1.14 → 4.2.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +13 -1
- package/package.json +1 -1
- package/src/index.js +75 -70
- package/src/memory/bootstrap/project-scanner.js +328 -0
- package/src/memory/memory-engine.js +123 -2
- package/src/memory/utils/chunker.js +5 -4
- package/src/memory/utils/compaction.js +281 -0
- package/src/memory/watchers/file-watcher.js +59 -44
- package/src/tools/memory_get.js +113 -11
package/README.md
CHANGED
|
@@ -29,7 +29,18 @@
|
|
|
29
29
|
|
|
30
30
|
---
|
|
31
31
|
|
|
32
|
-
## 📦 Current Version: v4.
|
|
32
|
+
## 📦 Current Version: v4.2.0
|
|
33
|
+
|
|
34
|
+
### What's New in v4.2.0 🎉
|
|
35
|
+
- **Minor Version Release**: UI & system update for Project Bootstrapping.
|
|
36
|
+
- **Configurable Bootstrap Mode**: Choose between `Local`, `Cloud`, and `Hybrid` analysis modes for new projects via Dashboard or Environment.
|
|
37
|
+
- **Schema Update**: Added `bootstrap_mode` column to `user_settings`.
|
|
38
|
+
|
|
39
|
+
### What's New in v4.1.18 🚀
|
|
40
|
+
- **Local Project Scanner**: Zero-config bootstrap! Automatically understands project name, type, tech stack, and API routes without internet/API.
|
|
41
|
+
- **Memory Compaction**: Automatic self-maintenance. Compacts old logs into `MEMORY.md` and trims archives when storage exceeds limits.
|
|
42
|
+
- **100% MCP Compliance**: Fully audited stdio communication and lifecycle management (no more JSON-RPC corruption or EOF errors).
|
|
43
|
+
- **Tool Enhancements**: `memory_get` now supports `type='intelligence'` and `type='compact'`.
|
|
33
44
|
|
|
34
45
|
### What's New in v4.1.10 🐛
|
|
35
46
|
- **Bug Fixes**: Fixed `getCuratedMemories()` and `getIntelligenceStats()` missing methods
|
|
@@ -47,6 +58,7 @@
|
|
|
47
58
|
- **Two-Tier Memory**: Daily logs + Curated memory
|
|
48
59
|
- **Hybrid Search**: Vector 70% + BM25 30% for 90%+ accuracy
|
|
49
60
|
- **File Watcher**: Auto-indexes files with debounced 1.5s delay
|
|
61
|
+
- **Local Bootstrapping**: Zero-config initial project scan logic.
|
|
50
62
|
|
|
51
63
|
---
|
|
52
64
|
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@mrxkun/mcfast-mcp",
|
|
3
|
-
"version": "4.
|
|
3
|
+
"version": "4.2.0",
|
|
4
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": {
|
package/src/index.js
CHANGED
|
@@ -8,7 +8,10 @@
|
|
|
8
8
|
// CRITICAL: Suppress ALL console output in MCP mode to prevent JSON parsing errors
|
|
9
9
|
// MCP protocol requires stdout to contain ONLY JSON-RPC messages
|
|
10
10
|
|
|
11
|
-
|
|
11
|
+
import os from 'os';
|
|
12
|
+
import crypto from 'crypto';
|
|
13
|
+
|
|
14
|
+
// ANSI Color Codes for Terminal Output
|
|
12
15
|
const colors = {
|
|
13
16
|
reset: '\x1b[0m',
|
|
14
17
|
bold: '\x1b[1m',
|
|
@@ -23,17 +26,11 @@ const colors = {
|
|
|
23
26
|
bgBlack: '\x1b[40m',
|
|
24
27
|
};
|
|
25
28
|
|
|
26
|
-
//
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
// Keep console.error enabled for debugging and MCP logging
|
|
33
|
-
// const originalConsoleError = console.error;
|
|
34
|
-
// console.error = function(...args) {
|
|
35
|
-
// // Suppressed in MCP mode
|
|
36
|
-
// };
|
|
29
|
+
// MCP SPEC: stdout must contain ONLY JSON-RPC messages.
|
|
30
|
+
// Redirect console.log to stderr so native module warnings (prebuild-install, etc.)
|
|
31
|
+
// don't corrupt the JSON-RPC stream.
|
|
32
|
+
const _origConsoleLog = console.log;
|
|
33
|
+
console.log = (...args) => process.stderr.write(args.join(' ') + '\n');
|
|
37
34
|
|
|
38
35
|
import { Server } from "@modelcontextprotocol/sdk/server/index.js";
|
|
39
36
|
import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
|
|
@@ -81,8 +78,11 @@ const execAsync = promisify(exec);
|
|
|
81
78
|
|
|
82
79
|
// ============================================================================
|
|
83
80
|
// LOCK FILE MECHANISM - Prevent multiple instances running simultaneously
|
|
81
|
+
// Use home directory to avoid permission issues in project root or systems where CWD is /
|
|
84
82
|
// ============================================================================
|
|
85
|
-
const
|
|
83
|
+
const LOCK_DIR = path.join(os.homedir(), '.mcfast', 'locks');
|
|
84
|
+
const projectHash = crypto.createHash('md5').update(process.cwd()).digest('hex');
|
|
85
|
+
const LOCK_FILE_PATH = path.join(LOCK_DIR, `${projectHash}.lock`);
|
|
86
86
|
let lockFileHandle = null;
|
|
87
87
|
|
|
88
88
|
async function acquireLock() {
|
|
@@ -568,13 +568,13 @@ function prettyLogResult(toolName, success, latencyMs, summary) {
|
|
|
568
568
|
|
|
569
569
|
// Token validation moved to request handlers for better UX (prevents server crash)
|
|
570
570
|
if (!TOKEN) {
|
|
571
|
-
console.error("⚠️
|
|
571
|
+
console.error("⚠️ Warning: MCFAST_TOKEN is missing. Tools that call the cloud API will fail.");
|
|
572
572
|
}
|
|
573
573
|
|
|
574
574
|
const server = new Server(
|
|
575
575
|
{
|
|
576
576
|
name: "mcfast",
|
|
577
|
-
version: "1.
|
|
577
|
+
version: "4.1.15",
|
|
578
578
|
},
|
|
579
579
|
{
|
|
580
580
|
capabilities: {
|
|
@@ -3448,86 +3448,91 @@ async function handleHealthCheck() {
|
|
|
3448
3448
|
}
|
|
3449
3449
|
|
|
3450
3450
|
/**
|
|
3451
|
-
*
|
|
3451
|
+
* MCP Server Startup
|
|
3452
|
+
* Following MCP spec: https://spec.modelcontextprotocol.io/specification/
|
|
3453
|
+
*
|
|
3454
|
+
* Lifecycle order per MCP spec:
|
|
3455
|
+
* 1. Acquire lock (prevent duplicate instances)
|
|
3456
|
+
* 2. Create transport
|
|
3457
|
+
* 3. Register signal/error handlers
|
|
3458
|
+
* 4. Start background tasks (memory engine)
|
|
3459
|
+
* 5. Call server.connect(transport) — this MUST be last
|
|
3460
|
+
* 6. Process remains alive via stdio loop managed by SDK
|
|
3452
3461
|
*/
|
|
3453
3462
|
|
|
3454
|
-
//
|
|
3455
|
-
const lockAcquired = await acquireLock();
|
|
3456
|
-
if (!lockAcquired) {
|
|
3457
|
-
console.error(`${colors.red}[ERROR]${colors.reset} Failed to acquire lock. Another mcfast-mcp instance may be running.`);
|
|
3458
|
-
console.error(`${colors.yellow}[HINT]${colors.reset} Delete ${LOCK_FILE_PATH} if you're sure no other instance is running.`);
|
|
3459
|
-
process.exit(1);
|
|
3460
|
-
}
|
|
3461
|
-
|
|
3462
|
-
const transport = new StdioServerTransport();
|
|
3463
|
-
|
|
3464
|
-
// NOTE: Do NOT add process.stdin.on('end') handler here.
|
|
3465
|
-
// VSCode and some MCP clients send ephemeral EOF / half-close signals during
|
|
3466
|
-
// the initialize handshake, which would cause premature gracefulShutdown
|
|
3467
|
-
// before the server has a chance to respond, resulting in the
|
|
3468
|
-
// "Error: calling 'initialize': EOF" error.
|
|
3469
|
-
//
|
|
3470
|
-
// The MCP SDK's StdioServerTransport already manages transport lifecycle.
|
|
3471
|
-
// Only handle unrecoverable stdout errors.
|
|
3472
|
-
process.stdout.on('error', (err) => {
|
|
3473
|
-
if (err.code === 'EPIPE' || err.code === 'ERR_STREAM_DESTROYED') {
|
|
3474
|
-
console.error('[MCP] stdout closed, shutting down:', err.code);
|
|
3475
|
-
gracefulShutdown('stdout.error');
|
|
3476
|
-
}
|
|
3477
|
-
});
|
|
3478
|
-
|
|
3479
|
-
// Pre-initialize memory engine in background
|
|
3480
|
-
backgroundInitializeMemoryEngine().catch(err => {
|
|
3481
|
-
console.error(`${colors.yellow}[Memory]${colors.reset} Background init error: ${err.message}`);
|
|
3482
|
-
// Don't exit - continue without memory
|
|
3483
|
-
});
|
|
3484
|
-
|
|
3485
|
-
// Graceful shutdown handler
|
|
3463
|
+
// Graceful shutdown - defined BEFORE any signal handlers reference it
|
|
3486
3464
|
async function gracefulShutdown(signal) {
|
|
3487
|
-
|
|
3465
|
+
// Guard: avoid re-entrant shutdown calls
|
|
3466
|
+
if (gracefulShutdown._running) return;
|
|
3467
|
+
gracefulShutdown._running = true;
|
|
3488
3468
|
|
|
3489
|
-
|
|
3469
|
+
console.error(`[mcfast] Shutting down (${signal})...`);
|
|
3470
|
+
|
|
3471
|
+
const SHUTDOWN_TIMEOUT_MS = 5000; // MCP recommendation
|
|
3490
3472
|
|
|
3491
3473
|
try {
|
|
3492
3474
|
await Promise.race([
|
|
3493
3475
|
(async () => {
|
|
3494
|
-
// Stop memory engine
|
|
3495
3476
|
if (memoryEngine && memoryEngineReady) {
|
|
3496
|
-
await memoryEngine.cleanup();
|
|
3477
|
+
await memoryEngine.cleanup().catch(() => { });
|
|
3497
3478
|
}
|
|
3498
|
-
|
|
3499
|
-
// Flush audit queue
|
|
3500
3479
|
const auditQ = getAuditQueue();
|
|
3501
|
-
await auditQ.destroy();
|
|
3502
|
-
|
|
3503
|
-
// Release lock
|
|
3480
|
+
await auditQ.destroy().catch(() => { });
|
|
3504
3481
|
await releaseLock();
|
|
3505
3482
|
})(),
|
|
3506
|
-
new Promise(resolve => setTimeout(resolve,
|
|
3483
|
+
new Promise(resolve => setTimeout(resolve, SHUTDOWN_TIMEOUT_MS))
|
|
3507
3484
|
]);
|
|
3508
|
-
|
|
3509
|
-
|
|
3510
|
-
|
|
3511
|
-
console.error(`${colors.red}[Shutdown]${colors.reset} Error during cleanup: ${error.message}`);
|
|
3485
|
+
console.error('[mcfast] Shutdown complete.');
|
|
3486
|
+
} catch (err) {
|
|
3487
|
+
console.error('[mcfast] Error during shutdown:', err.message);
|
|
3512
3488
|
}
|
|
3513
3489
|
|
|
3514
|
-
// Always exit (even if cleanup fails) to prevent zombie processes
|
|
3515
3490
|
process.exit(0);
|
|
3516
3491
|
}
|
|
3492
|
+
gracefulShutdown._running = false;
|
|
3517
3493
|
|
|
3494
|
+
// Signal handlers (registered before server.connect)
|
|
3518
3495
|
process.on('SIGINT', () => gracefulShutdown('SIGINT'));
|
|
3519
3496
|
process.on('SIGTERM', () => gracefulShutdown('SIGTERM'));
|
|
3520
3497
|
|
|
3521
|
-
//
|
|
3498
|
+
// MCP SPEC: stdout must never receive non-JSON-RPC data.
|
|
3499
|
+
// Only shut down on actual unrecoverable stdout errors, NOT on stdin 'end'.
|
|
3500
|
+
// stdin 'end' can be a transient event during handshake in some MCP clients.
|
|
3501
|
+
process.stdout.on('error', (err) => {
|
|
3502
|
+
if (err.code === 'EPIPE' || err.code === 'ERR_STREAM_DESTROYED') {
|
|
3503
|
+
gracefulShutdown('stdout.EPIPE');
|
|
3504
|
+
}
|
|
3505
|
+
});
|
|
3506
|
+
|
|
3507
|
+
// Catch unhandled errors - log but DO NOT exit on every unhandled rejection
|
|
3508
|
+
// (memory scan errors etc. shouldn't kill the whole server)
|
|
3522
3509
|
process.on('uncaughtException', (err) => {
|
|
3523
|
-
console.error('[
|
|
3524
|
-
|
|
3510
|
+
console.error('[mcfast] Uncaught exception:', err.message);
|
|
3511
|
+
// Only exit for truly fatal errors
|
|
3512
|
+
if (err.code === 'ERR_USE_AFTER_CLOSE' || err.code === 'ERR_SERVER_DESTROYED') {
|
|
3513
|
+
gracefulShutdown('uncaughtException');
|
|
3514
|
+
}
|
|
3525
3515
|
});
|
|
3526
3516
|
|
|
3527
|
-
process.on('unhandledRejection', (reason
|
|
3528
|
-
|
|
3529
|
-
|
|
3517
|
+
process.on('unhandledRejection', (reason) => {
|
|
3518
|
+
// Log but don't exit — background tasks (memory, scan) may reject harmlessly
|
|
3519
|
+
console.error('[mcfast] Unhandled rejection:', reason?.message || reason);
|
|
3530
3520
|
});
|
|
3531
3521
|
|
|
3522
|
+
// Acquire instance lock
|
|
3523
|
+
const lockAcquired = await acquireLock();
|
|
3524
|
+
if (!lockAcquired) {
|
|
3525
|
+
console.error(`[mcfast] FATAL: Could not acquire lock. Delete ${LOCK_FILE_PATH} if no other instance is running.`);
|
|
3526
|
+
process.exit(1);
|
|
3527
|
+
}
|
|
3528
|
+
|
|
3529
|
+
// Start background memory initialization (non-blocking)
|
|
3530
|
+
backgroundInitializeMemoryEngine().catch(err => {
|
|
3531
|
+
console.error('[mcfast] Memory init error (non-fatal):', err.message);
|
|
3532
|
+
});
|
|
3533
|
+
|
|
3534
|
+
// Connect to MCP transport — MUST be the last startup step per MCP spec
|
|
3535
|
+
const transport = new StdioServerTransport();
|
|
3532
3536
|
await server.connect(transport);
|
|
3533
|
-
|
|
3537
|
+
|
|
3538
|
+
console.error(`[mcfast] MCP server v4.1.15 ready (pid=${process.pid})`);
|
|
@@ -0,0 +1,328 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Local Project Scanner (Bootstrap)
|
|
3
|
+
* Tự động scan dự án mới không cần API/internet.
|
|
4
|
+
* Chạy lần đầu khi chưa có "Project Context" trong MEMORY.md
|
|
5
|
+
*
|
|
6
|
+
* Phân tích:
|
|
7
|
+
* - package.json / pyproject.toml / Cargo.toml / go.mod / pom.xml
|
|
8
|
+
* - README.md (nếu có)
|
|
9
|
+
* - Cấu trúc thư mục
|
|
10
|
+
* - Tech stack detection từ dependencies
|
|
11
|
+
* - Entry points
|
|
12
|
+
* - API routes (Next.js / Express / FastAPI patterns)
|
|
13
|
+
*/
|
|
14
|
+
|
|
15
|
+
import fs from 'fs/promises';
|
|
16
|
+
import path from 'path';
|
|
17
|
+
|
|
18
|
+
const IGNORED_DIRS = new Set([
|
|
19
|
+
'node_modules', '.git', 'dist', 'build', '.next', '.nuxt',
|
|
20
|
+
'.turbo', 'coverage', '.mcfast', '__pycache__', 'target',
|
|
21
|
+
'.gradle', 'vendor', '.venv', 'venv', '.cache'
|
|
22
|
+
]);
|
|
23
|
+
|
|
24
|
+
export class ProjectScanner {
|
|
25
|
+
constructor(projectPath) {
|
|
26
|
+
this.projectPath = projectPath;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
/**
|
|
30
|
+
* Full scan — returns a structured project overview object
|
|
31
|
+
*/
|
|
32
|
+
async scan() {
|
|
33
|
+
const [
|
|
34
|
+
packageInfo,
|
|
35
|
+
readme,
|
|
36
|
+
structure,
|
|
37
|
+
entryPoints,
|
|
38
|
+
routes,
|
|
39
|
+
techStack
|
|
40
|
+
] = await Promise.all([
|
|
41
|
+
this.readPackageFile(),
|
|
42
|
+
this.readReadme(),
|
|
43
|
+
this.buildStructure(3),
|
|
44
|
+
this.detectEntryPoints(),
|
|
45
|
+
this.detectRoutes(),
|
|
46
|
+
null // will compute after packageInfo
|
|
47
|
+
]);
|
|
48
|
+
|
|
49
|
+
const stack = this.detectTechStack(packageInfo, structure);
|
|
50
|
+
|
|
51
|
+
return {
|
|
52
|
+
name: packageInfo?.name || path.basename(this.projectPath),
|
|
53
|
+
description: packageInfo?.description || null,
|
|
54
|
+
version: packageInfo?.version || null,
|
|
55
|
+
license: packageInfo?.license || null,
|
|
56
|
+
type: this.detectProjectType(packageInfo, structure),
|
|
57
|
+
technologies: stack,
|
|
58
|
+
entryPoints,
|
|
59
|
+
apiRoutes: routes,
|
|
60
|
+
structure,
|
|
61
|
+
readme: readme ? readme.substring(0, 1500) : null, // cap size
|
|
62
|
+
generatedAt: new Date().toISOString()
|
|
63
|
+
};
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
/**
|
|
67
|
+
* Read main package descriptor (package.json, pyproject.toml, etc.)
|
|
68
|
+
*/
|
|
69
|
+
async readPackageFile() {
|
|
70
|
+
// Node.js
|
|
71
|
+
try {
|
|
72
|
+
const content = await fs.readFile(
|
|
73
|
+
path.join(this.projectPath, 'package.json'), 'utf-8'
|
|
74
|
+
);
|
|
75
|
+
return JSON.parse(content);
|
|
76
|
+
} catch { }
|
|
77
|
+
|
|
78
|
+
// Try workspace root if monorepo
|
|
79
|
+
try {
|
|
80
|
+
const entries = await fs.readdir(this.projectPath, { withFileTypes: true });
|
|
81
|
+
const pkgDirs = entries.filter(e => e.isDirectory() && !IGNORED_DIRS.has(e.name));
|
|
82
|
+
for (const dir of pkgDirs.slice(0, 5)) {
|
|
83
|
+
try {
|
|
84
|
+
const content = await fs.readFile(
|
|
85
|
+
path.join(this.projectPath, dir.name, 'package.json'), 'utf-8'
|
|
86
|
+
);
|
|
87
|
+
return JSON.parse(content); // return first found
|
|
88
|
+
} catch { }
|
|
89
|
+
}
|
|
90
|
+
} catch { }
|
|
91
|
+
|
|
92
|
+
return null;
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
/**
|
|
96
|
+
* Read README file
|
|
97
|
+
*/
|
|
98
|
+
async readReadme() {
|
|
99
|
+
const candidates = ['README.md', 'Readme.md', 'readme.md', 'README.txt'];
|
|
100
|
+
for (const name of candidates) {
|
|
101
|
+
try {
|
|
102
|
+
return await fs.readFile(path.join(this.projectPath, name), 'utf-8');
|
|
103
|
+
} catch { }
|
|
104
|
+
}
|
|
105
|
+
return null;
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
/**
|
|
109
|
+
* Build directory structure tree
|
|
110
|
+
*/
|
|
111
|
+
async buildStructure(maxDepth = 3, dir = this.projectPath, depth = 0, prefix = '') {
|
|
112
|
+
if (depth >= maxDepth) return '';
|
|
113
|
+
const lines = [];
|
|
114
|
+
try {
|
|
115
|
+
const entries = await fs.readdir(dir, { withFileTypes: true });
|
|
116
|
+
const filtered = entries
|
|
117
|
+
.filter(e => !IGNORED_DIRS.has(e.name) && !e.name.startsWith('.'))
|
|
118
|
+
.sort((a, b) => {
|
|
119
|
+
if (a.isDirectory() && !b.isDirectory()) return -1;
|
|
120
|
+
if (!a.isDirectory() && b.isDirectory()) return 1;
|
|
121
|
+
return a.name.localeCompare(b.name);
|
|
122
|
+
})
|
|
123
|
+
.slice(0, 20); // limit entries per dir
|
|
124
|
+
|
|
125
|
+
for (let i = 0; i < filtered.length; i++) {
|
|
126
|
+
const e = filtered[i];
|
|
127
|
+
const isLast = i === filtered.length - 1;
|
|
128
|
+
lines.push(prefix + (isLast ? '└── ' : '├── ') + e.name + (e.isDirectory() ? '/' : ''));
|
|
129
|
+
if (e.isDirectory()) {
|
|
130
|
+
const sub = await this.buildStructure(
|
|
131
|
+
maxDepth,
|
|
132
|
+
path.join(dir, e.name),
|
|
133
|
+
depth + 1,
|
|
134
|
+
prefix + (isLast ? ' ' : '│ ')
|
|
135
|
+
);
|
|
136
|
+
if (sub) lines.push(sub);
|
|
137
|
+
}
|
|
138
|
+
}
|
|
139
|
+
} catch { }
|
|
140
|
+
return lines.join('\n');
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
/**
|
|
144
|
+
* Detect entry points
|
|
145
|
+
*/
|
|
146
|
+
async detectEntryPoints() {
|
|
147
|
+
const candidates = [
|
|
148
|
+
'src/index.ts', 'src/index.js', 'src/main.ts', 'src/main.js',
|
|
149
|
+
'src/app.ts', 'src/app.js', 'index.ts', 'index.js',
|
|
150
|
+
'server.ts', 'server.js', 'main.py', 'app.py', 'main.go',
|
|
151
|
+
'app/layout.tsx', 'app/page.tsx', 'pages/index.tsx', 'pages/index.js'
|
|
152
|
+
];
|
|
153
|
+
const found = [];
|
|
154
|
+
for (const f of candidates) {
|
|
155
|
+
try {
|
|
156
|
+
await fs.access(path.join(this.projectPath, f));
|
|
157
|
+
found.push(f);
|
|
158
|
+
} catch { }
|
|
159
|
+
}
|
|
160
|
+
return found;
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
/**
|
|
164
|
+
* Detect API routes (Next.js App Router, Pages Router, Express)
|
|
165
|
+
*/
|
|
166
|
+
async detectRoutes() {
|
|
167
|
+
const routes = [];
|
|
168
|
+
|
|
169
|
+
// Next.js App Router: app/api/**/route.ts
|
|
170
|
+
const appApiDir = path.join(this.projectPath, 'app', 'api');
|
|
171
|
+
const appRoutes = await this.scanRouteDir(appApiDir, 'app/api');
|
|
172
|
+
routes.push(...appRoutes);
|
|
173
|
+
|
|
174
|
+
// Next.js Pages Router: pages/api/**
|
|
175
|
+
const pagesApiDir = path.join(this.projectPath, 'pages', 'api');
|
|
176
|
+
const pagesRoutes = await this.scanRouteDir(pagesApiDir, 'pages/api');
|
|
177
|
+
routes.push(...pagesRoutes);
|
|
178
|
+
|
|
179
|
+
// src/routes (Express-style)
|
|
180
|
+
const srcRoutesDir = path.join(this.projectPath, 'src', 'routes');
|
|
181
|
+
const srcRoutes = await this.scanRouteDir(srcRoutesDir, 'src/routes');
|
|
182
|
+
routes.push(...srcRoutes);
|
|
183
|
+
|
|
184
|
+
return routes.slice(0, 30);
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
async scanRouteDir(dir, prefix) {
|
|
188
|
+
const routes = [];
|
|
189
|
+
try {
|
|
190
|
+
const walk = async (d, rel) => {
|
|
191
|
+
const entries = await fs.readdir(d, { withFileTypes: true }).catch(() => []);
|
|
192
|
+
for (const e of entries) {
|
|
193
|
+
if (e.isDirectory()) {
|
|
194
|
+
await walk(path.join(d, e.name), `${rel}/${e.name}`);
|
|
195
|
+
} else if (/\.(ts|js)$/.test(e.name)) {
|
|
196
|
+
routes.push(`${rel}/${e.name}`);
|
|
197
|
+
}
|
|
198
|
+
}
|
|
199
|
+
};
|
|
200
|
+
await walk(dir, prefix);
|
|
201
|
+
} catch { }
|
|
202
|
+
return routes;
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
/**
|
|
206
|
+
* Detect tech stack from package.json dependencies
|
|
207
|
+
*/
|
|
208
|
+
detectTechStack(packageJson, structure) {
|
|
209
|
+
const stack = {
|
|
210
|
+
languages: [],
|
|
211
|
+
frameworks: [],
|
|
212
|
+
databases: [],
|
|
213
|
+
testing: [],
|
|
214
|
+
tools: []
|
|
215
|
+
};
|
|
216
|
+
|
|
217
|
+
const deps = {
|
|
218
|
+
...(packageJson?.dependencies || {}),
|
|
219
|
+
...(packageJson?.devDependencies || {})
|
|
220
|
+
};
|
|
221
|
+
const depNames = Object.keys(deps);
|
|
222
|
+
|
|
223
|
+
// Languages
|
|
224
|
+
if (depNames.some(d => d === 'typescript' || d === 'ts-node')) stack.languages.push('TypeScript');
|
|
225
|
+
if (packageJson) stack.languages.push('JavaScript');
|
|
226
|
+
if (structure?.includes('main.py') || structure?.includes('app.py')) stack.languages.push('Python');
|
|
227
|
+
if (structure?.includes('main.go')) stack.languages.push('Go');
|
|
228
|
+
if (structure?.includes('Cargo.toml')) stack.languages.push('Rust');
|
|
229
|
+
|
|
230
|
+
// Frameworks
|
|
231
|
+
if (depNames.includes('next')) stack.frameworks.push('Next.js');
|
|
232
|
+
if (depNames.includes('react')) stack.frameworks.push('React');
|
|
233
|
+
if (depNames.includes('vue')) stack.frameworks.push('Vue.js');
|
|
234
|
+
if (depNames.includes('express')) stack.frameworks.push('Express');
|
|
235
|
+
if (depNames.includes('fastify')) stack.frameworks.push('Fastify');
|
|
236
|
+
if (depNames.includes('hono')) stack.frameworks.push('Hono');
|
|
237
|
+
if (depNames.includes('nuxt')) stack.frameworks.push('Nuxt');
|
|
238
|
+
if (depNames.includes('svelte')) stack.frameworks.push('Svelte');
|
|
239
|
+
if (depNames.includes('electron')) stack.frameworks.push('Electron');
|
|
240
|
+
|
|
241
|
+
// Databases
|
|
242
|
+
if (depNames.some(d => /pg|postgres|postgresql/.test(d))) stack.databases.push('PostgreSQL');
|
|
243
|
+
if (depNames.some(d => /mysql|mysql2/.test(d))) stack.databases.push('MySQL');
|
|
244
|
+
if (depNames.includes('better-sqlite3') || depNames.includes('sqlite3')) stack.databases.push('SQLite');
|
|
245
|
+
if (depNames.some(d => /mongoose|mongodb/.test(d))) stack.databases.push('MongoDB');
|
|
246
|
+
if (depNames.some(d => /redis|ioredis/.test(d))) stack.databases.push('Redis');
|
|
247
|
+
if (depNames.includes('prisma') || depNames.includes('@prisma/client')) stack.databases.push('Prisma ORM');
|
|
248
|
+
if (depNames.includes('drizzle-orm')) stack.databases.push('Drizzle ORM');
|
|
249
|
+
|
|
250
|
+
// Testing
|
|
251
|
+
if (depNames.includes('jest') || depNames.includes('@jest/core')) stack.testing.push('Jest');
|
|
252
|
+
if (depNames.includes('vitest')) stack.testing.push('Vitest');
|
|
253
|
+
if (depNames.includes('playwright') || depNames.includes('@playwright/test')) stack.testing.push('Playwright');
|
|
254
|
+
if (depNames.includes('cypress')) stack.testing.push('Cypress');
|
|
255
|
+
|
|
256
|
+
// Tools
|
|
257
|
+
if (depNames.includes('turbo') || structure?.includes('turbo.json')) stack.tools.push('Turborepo');
|
|
258
|
+
if (depNames.includes('vite')) stack.tools.push('Vite');
|
|
259
|
+
if (depNames.includes('webpack')) stack.tools.push('Webpack');
|
|
260
|
+
if (structure?.includes('docker-compose')) stack.tools.push('Docker');
|
|
261
|
+
|
|
262
|
+
return stack;
|
|
263
|
+
}
|
|
264
|
+
|
|
265
|
+
/**
|
|
266
|
+
* Detect project type
|
|
267
|
+
*/
|
|
268
|
+
detectProjectType(packageJson, structure) {
|
|
269
|
+
if (structure?.includes('app/page.tsx') || structure?.includes('app/layout.tsx')) return 'Next.js App';
|
|
270
|
+
if (packageJson?.dependencies?.next) return 'Next.js';
|
|
271
|
+
if (packageJson?.dependencies?.electron) return 'Electron App';
|
|
272
|
+
if (packageJson?.dependencies?.react) return 'React App';
|
|
273
|
+
if (packageJson?.dependencies?.express || packageJson?.dependencies?.fastify) return 'Node.js API';
|
|
274
|
+
if (structure?.includes('main.py')) return 'Python App';
|
|
275
|
+
if (structure?.includes('Cargo.toml')) return 'Rust Project';
|
|
276
|
+
if (structure?.includes('go.mod')) return 'Go Project';
|
|
277
|
+
const name = packageJson?.name || '';
|
|
278
|
+
if (name.includes('mcp') || name.includes('mcfast')) return 'MCP Server';
|
|
279
|
+
return 'Node.js Project';
|
|
280
|
+
}
|
|
281
|
+
|
|
282
|
+
/**
|
|
283
|
+
* Generate MEMORY.md Project Context section from scan result
|
|
284
|
+
*/
|
|
285
|
+
buildMemorySection(scan) {
|
|
286
|
+
const date = new Date().toISOString().split('T')[0];
|
|
287
|
+
const lines = [
|
|
288
|
+
`<!-- Auto-generated by mcfast local scanner on ${date} -->`,
|
|
289
|
+
`- **${date}** Project: **${scan.name}**`,
|
|
290
|
+
` - Type: ${scan.type}`,
|
|
291
|
+
];
|
|
292
|
+
|
|
293
|
+
if (scan.description) lines.push(` - Description: ${scan.description}`);
|
|
294
|
+
if (scan.version) lines.push(` - Version: ${scan.version}`);
|
|
295
|
+
|
|
296
|
+
const { languages, frameworks, databases, testing, tools } = scan.technologies;
|
|
297
|
+
if (languages.length) lines.push(` - Languages: ${[...new Set(languages)].join(', ')}`);
|
|
298
|
+
if (frameworks.length) lines.push(` - Frameworks: ${frameworks.join(', ')}`);
|
|
299
|
+
if (databases.length) lines.push(` - Databases: ${databases.join(', ')}`);
|
|
300
|
+
if (testing.length) lines.push(` - Testing: ${testing.join(', ')}`);
|
|
301
|
+
if (tools.length) lines.push(` - Tools: ${tools.join(', ')}`);
|
|
302
|
+
|
|
303
|
+
if (scan.entryPoints.length) {
|
|
304
|
+
lines.push(` - Entry Points: ${scan.entryPoints.join(', ')}`);
|
|
305
|
+
}
|
|
306
|
+
|
|
307
|
+
if (scan.apiRoutes.length) {
|
|
308
|
+
lines.push(` - API Routes (${scan.apiRoutes.length}): ${scan.apiRoutes.slice(0, 5).join(', ')}${scan.apiRoutes.length > 5 ? ` ...+${scan.apiRoutes.length - 5} more` : ''}`);
|
|
309
|
+
}
|
|
310
|
+
|
|
311
|
+
if (scan.readme) {
|
|
312
|
+
// Extract first paragraph of README as summary
|
|
313
|
+
const firstPara = scan.readme
|
|
314
|
+
.replace(/^#+.*\n/gm, '')
|
|
315
|
+
.replace(/!\[.*?\]\(.*?\)/g, '')
|
|
316
|
+
.replace(/\[([^\]]+)\]\([^)]+\)/g, '$1')
|
|
317
|
+
.split('\n\n')[0]
|
|
318
|
+
.trim();
|
|
319
|
+
if (firstPara && firstPara.length > 20) {
|
|
320
|
+
lines.push(` - README summary: ${firstPara.substring(0, 300)}`);
|
|
321
|
+
}
|
|
322
|
+
}
|
|
323
|
+
|
|
324
|
+
return lines.join('\n');
|
|
325
|
+
}
|
|
326
|
+
}
|
|
327
|
+
|
|
328
|
+
export default ProjectScanner;
|