@mrxkun/mcfast-mcp 4.1.15 → 4.2.1
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 +67 -68
- package/src/memory/bootstrap/project-scanner.js +328 -0
- package/src/memory/memory-engine.js +125 -4
- package/src/memory/stores/base-database.js +9 -9
- package/src/memory/stores/codebase-database.js +33 -18
- package/src/memory/stores/memory-database.js +41 -31
- package/src/memory/utils/chunker.js +5 -4
- package/src/memory/utils/compaction.js +281 -0
- package/src/memory/utils/dashboard-client.js +7 -6
- package/src/memory/watchers/file-watcher.js +59 -44
- package/src/strategies/tree-sitter/queries.js +2 -2
- 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.1
|
|
3
|
+
"version": "4.2.1",
|
|
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
|
@@ -26,17 +26,11 @@ const colors = {
|
|
|
26
26
|
bgBlack: '\x1b[40m',
|
|
27
27
|
};
|
|
28
28
|
|
|
29
|
-
//
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
// Keep console.error enabled for debugging and MCP logging
|
|
36
|
-
// const originalConsoleError = console.error;
|
|
37
|
-
// console.error = function(...args) {
|
|
38
|
-
// // Suppressed in MCP mode
|
|
39
|
-
// };
|
|
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');
|
|
40
34
|
|
|
41
35
|
import { Server } from "@modelcontextprotocol/sdk/server/index.js";
|
|
42
36
|
import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
|
|
@@ -574,13 +568,13 @@ function prettyLogResult(toolName, success, latencyMs, summary) {
|
|
|
574
568
|
|
|
575
569
|
// Token validation moved to request handlers for better UX (prevents server crash)
|
|
576
570
|
if (!TOKEN) {
|
|
577
|
-
console.error("⚠️
|
|
571
|
+
console.error("⚠️ Warning: MCFAST_TOKEN is missing. Tools that call the cloud API will fail.");
|
|
578
572
|
}
|
|
579
573
|
|
|
580
574
|
const server = new Server(
|
|
581
575
|
{
|
|
582
576
|
name: "mcfast",
|
|
583
|
-
version: "1.
|
|
577
|
+
version: "4.1.15",
|
|
584
578
|
},
|
|
585
579
|
{
|
|
586
580
|
capabilities: {
|
|
@@ -3454,86 +3448,91 @@ async function handleHealthCheck() {
|
|
|
3454
3448
|
}
|
|
3455
3449
|
|
|
3456
3450
|
/**
|
|
3457
|
-
*
|
|
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
|
|
3458
3461
|
*/
|
|
3459
3462
|
|
|
3460
|
-
//
|
|
3461
|
-
const lockAcquired = await acquireLock();
|
|
3462
|
-
if (!lockAcquired) {
|
|
3463
|
-
console.error(`${colors.red}[ERROR]${colors.reset} Failed to acquire lock. Another mcfast-mcp instance may be running.`);
|
|
3464
|
-
console.error(`${colors.yellow}[HINT]${colors.reset} Delete ${LOCK_FILE_PATH} if you're sure no other instance is running.`);
|
|
3465
|
-
process.exit(1);
|
|
3466
|
-
}
|
|
3467
|
-
|
|
3468
|
-
const transport = new StdioServerTransport();
|
|
3469
|
-
|
|
3470
|
-
// NOTE: Do NOT add process.stdin.on('end') handler here.
|
|
3471
|
-
// VSCode and some MCP clients send ephemeral EOF / half-close signals during
|
|
3472
|
-
// the initialize handshake, which would cause premature gracefulShutdown
|
|
3473
|
-
// before the server has a chance to respond, resulting in the
|
|
3474
|
-
// "Error: calling 'initialize': EOF" error.
|
|
3475
|
-
//
|
|
3476
|
-
// The MCP SDK's StdioServerTransport already manages transport lifecycle.
|
|
3477
|
-
// Only handle unrecoverable stdout errors.
|
|
3478
|
-
process.stdout.on('error', (err) => {
|
|
3479
|
-
if (err.code === 'EPIPE' || err.code === 'ERR_STREAM_DESTROYED') {
|
|
3480
|
-
console.error('[MCP] stdout closed, shutting down:', err.code);
|
|
3481
|
-
gracefulShutdown('stdout.error');
|
|
3482
|
-
}
|
|
3483
|
-
});
|
|
3484
|
-
|
|
3485
|
-
// Pre-initialize memory engine in background
|
|
3486
|
-
backgroundInitializeMemoryEngine().catch(err => {
|
|
3487
|
-
console.error(`${colors.yellow}[Memory]${colors.reset} Background init error: ${err.message}`);
|
|
3488
|
-
// Don't exit - continue without memory
|
|
3489
|
-
});
|
|
3490
|
-
|
|
3491
|
-
// Graceful shutdown handler
|
|
3463
|
+
// Graceful shutdown - defined BEFORE any signal handlers reference it
|
|
3492
3464
|
async function gracefulShutdown(signal) {
|
|
3493
|
-
|
|
3465
|
+
// Guard: avoid re-entrant shutdown calls
|
|
3466
|
+
if (gracefulShutdown._running) return;
|
|
3467
|
+
gracefulShutdown._running = true;
|
|
3494
3468
|
|
|
3495
|
-
|
|
3469
|
+
console.error(`[mcfast] Shutting down (${signal})...`);
|
|
3470
|
+
|
|
3471
|
+
const SHUTDOWN_TIMEOUT_MS = 5000; // MCP recommendation
|
|
3496
3472
|
|
|
3497
3473
|
try {
|
|
3498
3474
|
await Promise.race([
|
|
3499
3475
|
(async () => {
|
|
3500
|
-
// Stop memory engine
|
|
3501
3476
|
if (memoryEngine && memoryEngineReady) {
|
|
3502
|
-
await memoryEngine.cleanup();
|
|
3477
|
+
await memoryEngine.cleanup().catch(() => { });
|
|
3503
3478
|
}
|
|
3504
|
-
|
|
3505
|
-
// Flush audit queue
|
|
3506
3479
|
const auditQ = getAuditQueue();
|
|
3507
|
-
await auditQ.destroy();
|
|
3508
|
-
|
|
3509
|
-
// Release lock
|
|
3480
|
+
await auditQ.destroy().catch(() => { });
|
|
3510
3481
|
await releaseLock();
|
|
3511
3482
|
})(),
|
|
3512
|
-
new Promise(resolve => setTimeout(resolve,
|
|
3483
|
+
new Promise(resolve => setTimeout(resolve, SHUTDOWN_TIMEOUT_MS))
|
|
3513
3484
|
]);
|
|
3514
|
-
|
|
3515
|
-
|
|
3516
|
-
|
|
3517
|
-
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);
|
|
3518
3488
|
}
|
|
3519
3489
|
|
|
3520
|
-
// Always exit (even if cleanup fails) to prevent zombie processes
|
|
3521
3490
|
process.exit(0);
|
|
3522
3491
|
}
|
|
3492
|
+
gracefulShutdown._running = false;
|
|
3523
3493
|
|
|
3494
|
+
// Signal handlers (registered before server.connect)
|
|
3524
3495
|
process.on('SIGINT', () => gracefulShutdown('SIGINT'));
|
|
3525
3496
|
process.on('SIGTERM', () => gracefulShutdown('SIGTERM'));
|
|
3526
3497
|
|
|
3527
|
-
//
|
|
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)
|
|
3528
3509
|
process.on('uncaughtException', (err) => {
|
|
3529
|
-
console.error('[
|
|
3530
|
-
|
|
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
|
+
}
|
|
3531
3515
|
});
|
|
3532
3516
|
|
|
3533
|
-
process.on('unhandledRejection', (reason
|
|
3534
|
-
|
|
3535
|
-
|
|
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);
|
|
3536
3520
|
});
|
|
3537
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();
|
|
3538
3536
|
await server.connect(transport);
|
|
3539
|
-
|
|
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;
|