@mrxkun/mcfast-mcp 4.1.15 → 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 CHANGED
@@ -29,7 +29,18 @@
29
29
 
30
30
  ---
31
31
 
32
- ## 📦 Current Version: v4.1.10
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.15",
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
@@ -26,17 +26,11 @@ const colors = {
26
26
  bgBlack: '\x1b[40m',
27
27
  };
28
28
 
29
- // Override console.log - suppress in MCP mode
30
- const originalConsoleLog = console.log;
31
- console.log = function (...args) {
32
- // Suppressed in MCP mode
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("⚠️ Warning: MCFAST_TOKEN is missing. Tools will fail until configured.");
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.3.0",
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
- * Start Server
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
- // Acquire lock before starting - prevents multiple instances
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
- console.error(`${colors.yellow}[Shutdown]${colors.reset} Received ${signal}, cleaning up...`);
3465
+ // Guard: avoid re-entrant shutdown calls
3466
+ if (gracefulShutdown._running) return;
3467
+ gracefulShutdown._running = true;
3494
3468
 
3495
- const SHUTDOWN_TIMEOUT = 5000; // 5 seconds per MCP best practices
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, SHUTDOWN_TIMEOUT))
3483
+ new Promise(resolve => setTimeout(resolve, SHUTDOWN_TIMEOUT_MS))
3513
3484
  ]);
3514
-
3515
- console.error(`${colors.green}[Shutdown]${colors.reset} Cleanup complete`);
3516
- } catch (error) {
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
- // Protect against zombie processes - handle unexpected errors
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('[MCP] Uncaught exception:', err);
3530
- gracefulShutdown('uncaughtException');
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, promise) => {
3534
- console.error('[MCP] Unhandled rejection:', reason);
3535
- gracefulShutdown('unhandledRejection');
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
- console.error("mcfast MCP v1.0.0 running on stdio");
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;