milens 0.6.2 → 0.6.4
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/.agents/skills/adapters/SKILL.md +31 -0
- package/.agents/skills/analyzer/SKILL.md +55 -0
- package/.agents/skills/apps/SKILL.md +42 -0
- package/.agents/skills/docs/SKILL.md +46 -0
- package/.agents/skills/milens/SKILL.md +168 -0
- package/.agents/skills/milens-code-review/SKILL.md +186 -0
- package/.agents/skills/milens-eval/SKILL.md +221 -0
- package/.agents/skills/milens-plan/SKILL.md +227 -0
- package/.agents/skills/milens-refactor-clean/SKILL.md +209 -0
- package/.agents/skills/milens-security-review/SKILL.md +224 -0
- package/.agents/skills/milens-tdd/SKILL.md +156 -0
- package/.agents/skills/parser/SKILL.md +60 -0
- package/.agents/skills/root/SKILL.md +64 -0
- package/.agents/skills/scripts/SKILL.md +27 -0
- package/.agents/skills/security/SKILL.md +44 -0
- package/.agents/skills/server/SKILL.md +46 -0
- package/.agents/skills/store/SKILL.md +53 -0
- package/.agents/skills/test/SKILL.md +73 -0
- package/LICENSE +75 -75
- package/README.md +524 -305
- package/adapters/README.md +107 -0
- package/adapters/claude-code/.claude/mcp.json +9 -0
- package/adapters/claude-code/CLAUDE.md +58 -0
- package/adapters/codex/.codex/codex.md +52 -0
- package/adapters/copilot/.github/copilot-instructions.md +62 -0
- package/adapters/cursor/.cursorrules +9 -0
- package/adapters/gemini/.gemini/context.md +58 -0
- package/adapters/opencode/.opencode/config.json +9 -0
- package/adapters/opencode/AGENTS.md +58 -0
- package/adapters/zed/.zed/settings.json +8 -0
- package/dist/agents-md.d.ts +3 -0
- package/dist/agents-md.d.ts.map +1 -0
- package/dist/agents-md.js +112 -0
- package/dist/agents-md.js.map +1 -0
- package/dist/analyzer/engine.d.ts +1 -0
- package/dist/analyzer/engine.d.ts.map +1 -1
- package/dist/analyzer/engine.js +27 -8
- package/dist/analyzer/engine.js.map +1 -1
- package/dist/analyzer/review.d.ts +23 -0
- package/dist/analyzer/review.d.ts.map +1 -0
- package/dist/analyzer/review.js +143 -0
- package/dist/analyzer/review.js.map +1 -0
- package/dist/analyzer/testplan.d.ts +59 -0
- package/dist/analyzer/testplan.d.ts.map +1 -0
- package/dist/analyzer/testplan.js +218 -0
- package/dist/analyzer/testplan.js.map +1 -0
- package/dist/cli.js +1192 -401
- package/dist/cli.js.map +1 -1
- package/dist/metrics.d.ts +51 -0
- package/dist/metrics.d.ts.map +1 -0
- package/dist/metrics.js +64 -0
- package/dist/metrics.js.map +1 -0
- package/dist/parser/extract.d.ts +1 -0
- package/dist/parser/extract.d.ts.map +1 -1
- package/dist/parser/extract.js +8 -0
- package/dist/parser/extract.js.map +1 -1
- package/dist/parser/lang-go.d.ts.map +1 -1
- package/dist/parser/lang-go.js +75 -39
- package/dist/parser/lang-go.js.map +1 -1
- package/dist/parser/lang-java.d.ts.map +1 -1
- package/dist/parser/lang-java.js +30 -29
- package/dist/parser/lang-java.js.map +1 -1
- package/dist/parser/lang-js.js +105 -105
- package/dist/parser/lang-php.js +38 -38
- package/dist/parser/lang-py.d.ts.map +1 -1
- package/dist/parser/lang-py.js +53 -31
- package/dist/parser/lang-py.js.map +1 -1
- package/dist/parser/lang-ruby.d.ts.map +1 -1
- package/dist/parser/lang-ruby.js +15 -14
- package/dist/parser/lang-ruby.js.map +1 -1
- package/dist/parser/lang-rust.js +30 -30
- package/dist/parser/lang-ts.js +191 -191
- package/dist/security/deps.d.ts +38 -0
- package/dist/security/deps.d.ts.map +1 -0
- package/dist/security/deps.js +685 -0
- package/dist/security/deps.js.map +1 -0
- package/dist/security/rules.d.ts +42 -0
- package/dist/security/rules.d.ts.map +1 -0
- package/dist/security/rules.js +940 -0
- package/dist/security/rules.js.map +1 -0
- package/dist/server/hooks.d.ts +26 -0
- package/dist/server/hooks.d.ts.map +1 -0
- package/dist/server/hooks.js +253 -0
- package/dist/server/hooks.js.map +1 -0
- package/dist/server/mcp-prompts.d.ts +277 -0
- package/dist/server/mcp-prompts.d.ts.map +1 -0
- package/dist/server/mcp-prompts.js +627 -0
- package/dist/server/mcp-prompts.js.map +1 -0
- package/dist/server/mcp.d.ts.map +1 -1
- package/dist/server/mcp.js +520 -36
- package/dist/server/mcp.js.map +1 -1
- package/dist/server/test-plan.d.ts +20 -0
- package/dist/server/test-plan.d.ts.map +1 -0
- package/dist/server/test-plan.js +100 -0
- package/dist/server/test-plan.js.map +1 -0
- package/dist/skills.js +152 -120
- package/dist/skills.js.map +1 -1
- package/dist/store/annotations.d.ts +41 -0
- package/dist/store/annotations.d.ts.map +1 -0
- package/dist/store/annotations.js +192 -0
- package/dist/store/annotations.js.map +1 -0
- package/dist/store/confidence.d.ts +18 -0
- package/dist/store/confidence.d.ts.map +1 -0
- package/dist/store/confidence.js +82 -0
- package/dist/store/confidence.js.map +1 -0
- package/dist/store/db.d.ts +68 -1
- package/dist/store/db.d.ts.map +1 -1
- package/dist/store/db.js +349 -139
- package/dist/store/db.js.map +1 -1
- package/dist/store/schema.sql +128 -83
- package/dist/store/vectors.d.ts +65 -0
- package/dist/store/vectors.d.ts.map +1 -0
- package/dist/store/vectors.js +212 -0
- package/dist/store/vectors.js.map +1 -0
- package/dist/types.d.ts +101 -0
- package/dist/types.d.ts.map +1 -1
- package/dist/utils.d.ts +3 -0
- package/dist/utils.d.ts.map +1 -0
- package/dist/utils.js +9 -0
- package/dist/utils.js.map +1 -0
- package/docs/README.md +24 -0
- package/docs/diagram2.svg +1 -1
- package/package.json +80 -65
package/dist/server/mcp.js
CHANGED
|
@@ -14,6 +14,10 @@ import { RepoRegistry } from '../store/registry.js';
|
|
|
14
14
|
import { getParser, loadLanguage } from '../parser/loader.js';
|
|
15
15
|
import { ALL_LANGS } from '../parser/languages.js';
|
|
16
16
|
import { fileURLToPath } from 'node:url';
|
|
17
|
+
import { generateTestPlan } from './test-plan.js';
|
|
18
|
+
import { AnnotationStore } from '../store/annotations.js';
|
|
19
|
+
import { registerAllPrompts } from './mcp-prompts.js';
|
|
20
|
+
import { loadRules } from '../security/rules.js';
|
|
17
21
|
const __dirname = dirname(fileURLToPath(import.meta.url));
|
|
18
22
|
const PKG_VERSION = process.env.MILENS_VERSION ?? JSON.parse(readFileSync(join(__dirname, '..', '..', 'package.json'), 'utf-8')).version;
|
|
19
23
|
// ── Lazy DB connection with idle eviction ──
|
|
@@ -310,42 +314,42 @@ function loadGrepIgnoreRules(rootPath) {
|
|
|
310
314
|
return ig;
|
|
311
315
|
}
|
|
312
316
|
// ── Server instructions (sent to client via MCP protocol on initialize) ──
|
|
313
|
-
const MILENS_INSTRUCTIONS = `milens — code intelligence engine. Indexes codebases into symbol graphs.
|
|
314
|
-
|
|
315
|
-
## Tool selection
|
|
316
|
-
- \`query\` — find symbol definitions (code identifiers only)
|
|
317
|
-
- \`grep\` — text search ALL files. Use \`scope\` param: all (default), code (source only), imports, definitions
|
|
318
|
-
- \`context\` — 360° view: incoming + outgoing for a symbol
|
|
319
|
-
- \`impact\` — blast radius: what breaks if symbol changes
|
|
320
|
-
- \`overview\` — combined context + impact + grep in one call (preferred for editing workflows)
|
|
321
|
-
- \`edit_check\` — pre-edit safety: callers + export status + re-export chains + test coverage + ⚠ warnings (fastest for edits)
|
|
322
|
-
- \`trace\` — execution flow: call chains from entrypoints to a symbol (or downstream from it)
|
|
323
|
-
- \`routes\` — detect framework routes/endpoints (Express, FastAPI, NestJS, Flask, Go, PHP, Rails)
|
|
324
|
-
- \`smart_context\` — intent-aware context: understand/edit/debug/test (returns only what matters for intent)
|
|
325
|
-
- \`domains\` — show domain clusters: groups of files forming logical modules based on dependency graph
|
|
326
|
-
- \`repos\` — list all indexed repositories with summary stats (multi-repo support)
|
|
327
|
-
- \`detect_changes\` — git diff → affected symbols
|
|
328
|
-
- \`explain_relationship\` — shortest path between two symbols
|
|
329
|
-
- \`find_dead_code\` — unused exports
|
|
330
|
-
- \`get_file_symbols\` — all symbols in a file
|
|
331
|
-
- \`get_type_hierarchy\` — inheritance tree
|
|
332
|
-
|
|
333
|
-
## Rules
|
|
334
|
-
- Before editing a symbol: run \`edit_check\` or \`smart_context\` with intent=edit
|
|
335
|
-
- For debugging: run \`smart_context\` with intent=debug or \`trace\` to=symbol
|
|
336
|
-
- For writing tests: run \`smart_context\` with intent=test — shows deps to mock + callers to cover
|
|
337
|
-
- \`impact\` only tracks code deps — always pair with \`grep\` for templates/configs
|
|
338
|
-
- Use \`query\` for camelCase/PascalCase identifiers, \`grep\` for display text or multi-word strings
|
|
339
|
-
- impact depth: 1=WILL BREAK, 2=LIKELY AFFECTED, 3=MAY NEED TESTING
|
|
340
|
-
- ⚠ markers indicate unresolved INTERNAL references — external package imports/calls are tracked separately
|
|
341
|
-
- ✓ test coverage shown on edit_check — symbols with no test coverage get a warning
|
|
342
|
-
- ⏳ staleness: files not re-analyzed in 24h are flagged — consider re-running \`milens analyze\`
|
|
343
|
-
|
|
344
|
-
## Resources (MCP Resources protocol)
|
|
345
|
-
- \`milens://overview\` — index overview (stats, domains, coverage, staleness)
|
|
346
|
-
- \`milens://symbol/{name}\` — symbol context by name
|
|
347
|
-
- \`milens://file/{path}\` — all symbols in a file
|
|
348
|
-
- \`milens://domain/{name}\` — domain cluster details
|
|
317
|
+
const MILENS_INSTRUCTIONS = `milens — code intelligence engine. Indexes codebases into symbol graphs.
|
|
318
|
+
|
|
319
|
+
## Tool selection
|
|
320
|
+
- \`query\` — find symbol definitions (code identifiers only)
|
|
321
|
+
- \`grep\` — text search ALL files. Use \`scope\` param: all (default), code (source only), imports, definitions
|
|
322
|
+
- \`context\` — 360° view: incoming + outgoing for a symbol
|
|
323
|
+
- \`impact\` — blast radius: what breaks if symbol changes
|
|
324
|
+
- \`overview\` — combined context + impact + grep in one call (preferred for editing workflows)
|
|
325
|
+
- \`edit_check\` — pre-edit safety: callers + export status + re-export chains + test coverage + ⚠ warnings (fastest for edits)
|
|
326
|
+
- \`trace\` — execution flow: call chains from entrypoints to a symbol (or downstream from it)
|
|
327
|
+
- \`routes\` — detect framework routes/endpoints (Express, FastAPI, NestJS, Flask, Go, PHP, Rails)
|
|
328
|
+
- \`smart_context\` — intent-aware context: understand/edit/debug/test (returns only what matters for intent)
|
|
329
|
+
- \`domains\` — show domain clusters: groups of files forming logical modules based on dependency graph
|
|
330
|
+
- \`repos\` — list all indexed repositories with summary stats (multi-repo support)
|
|
331
|
+
- \`detect_changes\` — git diff → affected symbols
|
|
332
|
+
- \`explain_relationship\` — shortest path between two symbols
|
|
333
|
+
- \`find_dead_code\` — unused exports
|
|
334
|
+
- \`get_file_symbols\` — all symbols in a file
|
|
335
|
+
- \`get_type_hierarchy\` — inheritance tree
|
|
336
|
+
|
|
337
|
+
## Rules
|
|
338
|
+
- Before editing a symbol: run \`edit_check\` or \`smart_context\` with intent=edit
|
|
339
|
+
- For debugging: run \`smart_context\` with intent=debug or \`trace\` to=symbol
|
|
340
|
+
- For writing tests: run \`smart_context\` with intent=test — shows deps to mock + callers to cover
|
|
341
|
+
- \`impact\` only tracks code deps — always pair with \`grep\` for templates/configs
|
|
342
|
+
- Use \`query\` for camelCase/PascalCase identifiers, \`grep\` for display text or multi-word strings
|
|
343
|
+
- impact depth: 1=WILL BREAK, 2=LIKELY AFFECTED, 3=MAY NEED TESTING
|
|
344
|
+
- ⚠ markers indicate unresolved INTERNAL references — external package imports/calls are tracked separately
|
|
345
|
+
- ✓ test coverage shown on edit_check — symbols with no test coverage get a warning
|
|
346
|
+
- ⏳ staleness: files not re-analyzed in 24h are flagged — consider re-running \`milens analyze\`
|
|
347
|
+
|
|
348
|
+
## Resources (MCP Resources protocol)
|
|
349
|
+
- \`milens://overview\` — index overview (stats, domains, coverage, staleness)
|
|
350
|
+
- \`milens://symbol/{name}\` — symbol context by name
|
|
351
|
+
- \`milens://file/{path}\` — all symbols in a file
|
|
352
|
+
- \`milens://domain/{name}\` — domain cluster details
|
|
349
353
|
`;
|
|
350
354
|
// ── Server setup ──
|
|
351
355
|
export function createMcpServer(rootPath) {
|
|
@@ -410,6 +414,29 @@ export function createMcpServer(rootPath) {
|
|
|
410
414
|
}
|
|
411
415
|
return origTool(...args);
|
|
412
416
|
});
|
|
417
|
+
// ── Selective tool profiles (W4) ──
|
|
418
|
+
const profile = process.env.MILENS_PROFILE || undefined;
|
|
419
|
+
if (profile && profile !== 'full') {
|
|
420
|
+
const minimal = new Set(['query', 'grep', 'context', 'impact', 'status', 'codebase_summary', 'edit_check', 'detect_changes', 'get_file_symbols', 'overview']);
|
|
421
|
+
const standard = new Set([...minimal, 'domains', 'repos', 'explain_relationship', 'find_dead_code', 'get_type_hierarchy', 'trace', 'routes', 'smart_context', 'review_pr', 'review_symbol', 'test_coverage_gaps', 'test_plan', 'test_impact', 'session_start', 'recall']);
|
|
422
|
+
const allowed = profile === 'minimal' ? minimal : standard;
|
|
423
|
+
// Wrap server.tool again to gate by profile
|
|
424
|
+
const profileWrappedTool = server.tool.bind(server);
|
|
425
|
+
server.tool = ((...args) => {
|
|
426
|
+
const toolName = args[0];
|
|
427
|
+
if (!allowed.has(toolName)) {
|
|
428
|
+
// Return a no-op tool that explains it's disabled
|
|
429
|
+
const origLength = args.length;
|
|
430
|
+
const handler = args[origLength - 1];
|
|
431
|
+
if (typeof handler === 'function') {
|
|
432
|
+
args[origLength - 1] = async () => ({
|
|
433
|
+
content: [{ type: 'text', text: `Tool "${toolName}" disabled by profile "${profile}". Use --profile full to enable.` }],
|
|
434
|
+
});
|
|
435
|
+
}
|
|
436
|
+
}
|
|
437
|
+
return profileWrappedTool(...args);
|
|
438
|
+
});
|
|
439
|
+
}
|
|
413
440
|
// ── Tool: query ──
|
|
414
441
|
server.tool('query', 'Search indexed symbol definitions by name/kind. For text in templates/configs/docs, use `grep`.', {
|
|
415
442
|
query: z.string().describe('Symbol name, kind, or keyword to search'),
|
|
@@ -1308,6 +1335,284 @@ export function createMcpServer(rootPath) {
|
|
|
1308
1335
|
return { content: [{ type: 'text', text: `Query error: ${err.message}` }] };
|
|
1309
1336
|
}
|
|
1310
1337
|
});
|
|
1338
|
+
// ═══ codebase_summary ═══
|
|
1339
|
+
server.tool('codebase_summary', 'Compact ~500 token codebase overview: domains, top hubs, test coverage, annotations count. Use at the start of every session.', { repo: z.string().optional() }, async ({ repo }) => {
|
|
1340
|
+
const { db } = getDb(repo);
|
|
1341
|
+
const summary = db.getCodebaseSummary();
|
|
1342
|
+
const lines = [
|
|
1343
|
+
'Milens Codebase Summary:',
|
|
1344
|
+
` Symbols: ${summary.symbols} | Links: ${summary.links} | Files: ${summary.files}`,
|
|
1345
|
+
];
|
|
1346
|
+
if (summary.exportedSymbols > 0) {
|
|
1347
|
+
lines.push(` Test coverage: ${summary.coveragePct}% (${summary.testedSymbols}/${summary.exportedSymbols} exported symbols tested)`);
|
|
1348
|
+
}
|
|
1349
|
+
if (summary.domains.length > 0) {
|
|
1350
|
+
const domainStr = summary.domains.map(d => `${d.domain}(${d.symbols}s)`).join(', ');
|
|
1351
|
+
lines.push(` Domains: ${domainStr}`);
|
|
1352
|
+
}
|
|
1353
|
+
if (summary.topHubs.length > 0) {
|
|
1354
|
+
const hubStr = summary.topHubs.map(h => `${h.name}(${h.kind},heat:${h.heat})`).join(', ');
|
|
1355
|
+
lines.push(` Top hubs: ${hubStr}`);
|
|
1356
|
+
}
|
|
1357
|
+
try {
|
|
1358
|
+
const annCount = db.getAnnotationCount();
|
|
1359
|
+
lines.push(` Total annotations: ${annCount}`);
|
|
1360
|
+
}
|
|
1361
|
+
catch {
|
|
1362
|
+
lines.push(' Total annotations: 0');
|
|
1363
|
+
}
|
|
1364
|
+
return { content: [{ type: 'text', text: lines.join('\n') }] };
|
|
1365
|
+
});
|
|
1366
|
+
// ═══ review_pr ═══
|
|
1367
|
+
server.tool('review_pr', 'PR risk assessment: git diff -> affected symbols with risk scores (LOW/MEDIUM/HIGH/CRITICAL).', { ref: z.string().optional().default('HEAD'), repo: z.string().optional() }, async ({ ref, repo }) => {
|
|
1368
|
+
const { db, root } = getDb(repo);
|
|
1369
|
+
let changedFiles = [];
|
|
1370
|
+
try {
|
|
1371
|
+
const { execSync } = await import('node:child_process');
|
|
1372
|
+
const diff = execSync(`git diff --name-only ${ref}`, { cwd: root, encoding: 'utf-8' }).trim();
|
|
1373
|
+
changedFiles = diff ? diff.split('\n').filter(Boolean) : [];
|
|
1374
|
+
}
|
|
1375
|
+
catch { }
|
|
1376
|
+
if (changedFiles.length === 0) {
|
|
1377
|
+
return { content: [{ type: 'text', text: 'No changed files detected.' }] };
|
|
1378
|
+
}
|
|
1379
|
+
const allAffected = [];
|
|
1380
|
+
for (const file of changedFiles) {
|
|
1381
|
+
const syms = db.getSymbolsByFile(file);
|
|
1382
|
+
if (syms.length === 0)
|
|
1383
|
+
continue;
|
|
1384
|
+
for (const sym of syms) {
|
|
1385
|
+
const incoming = db.getIncomingLinks(sym.id).filter(l => l.type !== 'contains');
|
|
1386
|
+
const depsCount = incoming.length;
|
|
1387
|
+
const heat = sym.heat ?? 0;
|
|
1388
|
+
const hasTest = db.getSymbolTestCoverage(sym.id);
|
|
1389
|
+
const score = Math.round((heat / 100) * 40 + Math.min(depsCount / 10, 1) * 35 + (hasTest ? 0 : 25));
|
|
1390
|
+
let level = 'LOW';
|
|
1391
|
+
if (score > 75)
|
|
1392
|
+
level = 'CRITICAL';
|
|
1393
|
+
else if (score > 50)
|
|
1394
|
+
level = 'HIGH';
|
|
1395
|
+
else if (score > 25)
|
|
1396
|
+
level = 'MEDIUM';
|
|
1397
|
+
allAffected.push({ symbol: sym.name, kind: sym.kind, file: sym.filePath, heat, dependents: depsCount, hasTest, riskScore: score, riskLevel: level });
|
|
1398
|
+
}
|
|
1399
|
+
}
|
|
1400
|
+
allAffected.sort((a, b) => b.riskScore - a.riskScore);
|
|
1401
|
+
const summary = { CRITICAL: 0, HIGH: 0, MEDIUM: 0, LOW: 0 };
|
|
1402
|
+
for (const a of allAffected)
|
|
1403
|
+
summary[a.riskLevel]++;
|
|
1404
|
+
const lines = [`PR Risk Assessment (vs ${ref}):\n`];
|
|
1405
|
+
lines.push(`${changedFiles.length} changed files, ${allAffected.length} affected symbols\n`);
|
|
1406
|
+
for (const a of allAffected.slice(0, 30)) {
|
|
1407
|
+
lines.push(` ${a.symbol} [${a.kind}] ${a.file} — heat:${a.heat} deps:${a.dependents} test:${a.hasTest ? 'yes' : 'no'} → ${a.riskLevel}(${a.riskScore})`);
|
|
1408
|
+
}
|
|
1409
|
+
lines.push(`\nSummary: CRITICAL=${summary.CRITICAL} HIGH=${summary.HIGH} MEDIUM=${summary.MEDIUM} LOW=${summary.LOW}`);
|
|
1410
|
+
return { content: [{ type: 'text', text: lines.join('\n') }] };
|
|
1411
|
+
});
|
|
1412
|
+
// ═══ review_symbol ═══
|
|
1413
|
+
server.tool('review_symbol', 'Deep-dive single symbol risk: role, heat, dependents, test status, risk level.', { name: z.string(), repo: z.string().optional() }, async ({ name, repo }) => {
|
|
1414
|
+
const { db, root } = getDb(repo);
|
|
1415
|
+
const syms = db.findSymbolByName(name);
|
|
1416
|
+
if (syms.length === 0)
|
|
1417
|
+
return { content: [{ type: 'text', text: `"${name}" not found.` }] };
|
|
1418
|
+
const lines = [];
|
|
1419
|
+
for (const sym of syms) {
|
|
1420
|
+
const incoming = db.getIncomingLinks(sym.id).filter(l => l.type !== 'contains');
|
|
1421
|
+
const outgoing = db.getOutgoingLinks(sym.id).filter(l => l.type !== 'contains');
|
|
1422
|
+
const depsCount = incoming.length;
|
|
1423
|
+
const depsTop = incoming.slice(0, 5).map(l => { const s = db.findSymbolById(l.fromId); return s?.name ?? l.fromId; });
|
|
1424
|
+
const outCount = outgoing.length;
|
|
1425
|
+
const outTop = outgoing.slice(0, 5).map(l => { const s = db.findSymbolById(l.toId); return s?.name ?? l.toId; });
|
|
1426
|
+
const hasTest = sym.exported ? db.getSymbolTestCoverage(sym.id) : false;
|
|
1427
|
+
const heat = sym.heat ?? 0;
|
|
1428
|
+
const score = Math.round((heat / 100) * 40 + Math.min(depsCount / 10, 1) * 35 + (hasTest ? 0 : 25));
|
|
1429
|
+
let risk = 'LOW';
|
|
1430
|
+
if (score > 75)
|
|
1431
|
+
risk = 'CRITICAL';
|
|
1432
|
+
else if (score > 50)
|
|
1433
|
+
risk = 'HIGH';
|
|
1434
|
+
else if (score > 25)
|
|
1435
|
+
risk = 'MEDIUM';
|
|
1436
|
+
lines.push(`${sym.name} [${sym.kind}] ${sym.filePath}:${sym.startLine}`);
|
|
1437
|
+
lines.push(` role: ${sym.role ?? 'unknown'} | heat: ${heat} | exported: ${sym.exported}`);
|
|
1438
|
+
lines.push(` dependents: ${depsCount} ${depsTop.length ? '(' + depsTop.join(', ') + ')' : ''}`);
|
|
1439
|
+
lines.push(` dependencies: ${outCount} ${outTop.length ? '(' + outTop.join(', ') + ')' : ''}`);
|
|
1440
|
+
lines.push(` test coverage: ${hasTest ? 'yes' : 'no'}`);
|
|
1441
|
+
lines.push(` risk: ${risk} (score: ${score})`);
|
|
1442
|
+
if (risk === 'CRITICAL')
|
|
1443
|
+
lines.push(` recommendation: High risk — has ${depsCount} dependents with no test coverage. Write tests before modifying.`);
|
|
1444
|
+
else if (risk === 'HIGH')
|
|
1445
|
+
lines.push(` recommendation: Review dependents carefully before modifying.`);
|
|
1446
|
+
lines.push('');
|
|
1447
|
+
}
|
|
1448
|
+
return { content: [{ type: 'text', text: lines.join('\n') }] };
|
|
1449
|
+
});
|
|
1450
|
+
// ═══ test_coverage_gaps ═══
|
|
1451
|
+
server.tool('test_coverage_gaps', 'Untested exported symbols sorted by risk. Prioritize writing tests for these.', { limit: z.number().optional().default(20), repo: z.string().optional() }, async ({ limit, repo }) => {
|
|
1452
|
+
const { db } = getDb(repo);
|
|
1453
|
+
const coverage = db.getTestCoverage();
|
|
1454
|
+
const gaps = db.getTestCoverageGaps(limit);
|
|
1455
|
+
const lines = [`Test Coverage: ${coverage.testedSymbols}/${coverage.exportedProductionSymbols} (${coverage.exportedProductionSymbols > 0 ? Math.round(coverage.testedSymbols / coverage.exportedProductionSymbols * 100) : 0}%) from ${coverage.testFiles} test files\n`];
|
|
1456
|
+
if (gaps.length === 0) {
|
|
1457
|
+
lines.push('All exported symbols have test coverage!');
|
|
1458
|
+
}
|
|
1459
|
+
else {
|
|
1460
|
+
lines.push(`Top ${gaps.length} untested symbols:\n`);
|
|
1461
|
+
for (const g of gaps) {
|
|
1462
|
+
const incoming = db.getIncomingLinks(g.id).filter(l => l.type !== 'contains');
|
|
1463
|
+
const risk = (g.heat ?? 0) > 80 ? 'CRITICAL' : (g.heat ?? 0) > 50 ? 'HIGH' : (g.heat ?? 0) > 30 ? 'MEDIUM' : 'LOW';
|
|
1464
|
+
lines.push(` ${g.name} [${g.kind}] ${g.filePath}:${g.startLine} — heat:${g.heat ?? 0} deps:${incoming.length} risk:${risk}`);
|
|
1465
|
+
}
|
|
1466
|
+
}
|
|
1467
|
+
return { content: [{ type: 'text', text: lines.join('\n') }] };
|
|
1468
|
+
});
|
|
1469
|
+
// ═══ test_impact ═══
|
|
1470
|
+
server.tool('test_impact', 'Map changed code -> which test files to run. Use after making changes.', { ref: z.string().optional().default('HEAD'), repo: z.string().optional() }, async ({ ref, repo }) => {
|
|
1471
|
+
const { db, root } = getDb(repo);
|
|
1472
|
+
let changedFiles = [];
|
|
1473
|
+
try {
|
|
1474
|
+
const { execSync } = await import('node:child_process');
|
|
1475
|
+
const diff = execSync(`git diff --name-only ${ref}`, { cwd: root, encoding: 'utf-8' }).trim();
|
|
1476
|
+
changedFiles = diff ? diff.split('\n').filter(Boolean) : [];
|
|
1477
|
+
}
|
|
1478
|
+
catch { }
|
|
1479
|
+
if (changedFiles.length === 0)
|
|
1480
|
+
return { content: [{ type: 'text', text: 'No changed files.' }] };
|
|
1481
|
+
const changedIds = [];
|
|
1482
|
+
const changedNames = [];
|
|
1483
|
+
for (const file of changedFiles) {
|
|
1484
|
+
for (const sym of db.getSymbolsByFile(file)) {
|
|
1485
|
+
changedIds.push(sym.id);
|
|
1486
|
+
changedNames.push(sym.name);
|
|
1487
|
+
}
|
|
1488
|
+
}
|
|
1489
|
+
if (changedIds.length === 0)
|
|
1490
|
+
return { content: [{ type: 'text', text: 'No symbols in changed files.' }] };
|
|
1491
|
+
const impact = db.getTestImpact(changedIds);
|
|
1492
|
+
const lines = [`Changed symbols (${changedNames.length}): ${changedNames.join(', ')}`];
|
|
1493
|
+
lines.push(`\nAffected test files (${impact.testFiles.length}):`);
|
|
1494
|
+
for (const f of impact.testFiles)
|
|
1495
|
+
lines.push(` ${f}`);
|
|
1496
|
+
if (impact.testFiles.length > 0) {
|
|
1497
|
+
lines.push(`\nSuggested command: npx vitest run ${impact.testFiles.join(' ')}`);
|
|
1498
|
+
}
|
|
1499
|
+
return { content: [{ type: 'text', text: lines.join('\n') }] };
|
|
1500
|
+
});
|
|
1501
|
+
// ═══ test_plan ═══
|
|
1502
|
+
server.tool('test_plan', 'Generate a test strategy for a symbol: mock plan + >=3 test scenarios.', { name: z.string(), repo: z.string().optional() }, async ({ name, repo }) => {
|
|
1503
|
+
const { db } = getDb(repo);
|
|
1504
|
+
const plan = generateTestPlan(db, name);
|
|
1505
|
+
if (!plan)
|
|
1506
|
+
return { content: [{ type: 'text', text: `"${name}" not found.` }] };
|
|
1507
|
+
return { content: [{ type: 'text', text: plan.planText }] };
|
|
1508
|
+
});
|
|
1509
|
+
// ═══ annotate ═══
|
|
1510
|
+
server.tool('annotate', 'Record a note about a symbol for future sessions. Use after discovering bugs, patterns, or important caveats.', {
|
|
1511
|
+
symbol: z.string(),
|
|
1512
|
+
key: z.enum(['note', 'bug', 'security', 'architecture', 'workflow', 'test', 'dependency', 'refactor']),
|
|
1513
|
+
value: z.string(),
|
|
1514
|
+
agent: z.string().optional(),
|
|
1515
|
+
session_id: z.string().optional(),
|
|
1516
|
+
confidence: z.number().optional().default(0.5),
|
|
1517
|
+
}, async ({ symbol, key, value, agent, session_id, confidence }) => {
|
|
1518
|
+
const { db } = getDb();
|
|
1519
|
+
const store = new AnnotationStore(db.connection);
|
|
1520
|
+
const ann = store.annotate(symbol, key, value, { agent, sessionId: session_id });
|
|
1521
|
+
return { content: [{ type: 'text', text: `Annotation saved: ${ann.id}\n symbol: ${ann.symbol}\n key: ${ann.key}\n confidence: ${ann.confidence}` }] };
|
|
1522
|
+
});
|
|
1523
|
+
// ═══ recall ═══
|
|
1524
|
+
server.tool('recall', 'Retrieve annotations saved in previous sessions. Filter by symbol, key, or agent.', {
|
|
1525
|
+
symbol: z.string().optional(), key: z.enum(['note', 'bug', 'security', 'architecture', 'workflow', 'test', 'dependency', 'refactor']).optional(),
|
|
1526
|
+
agent: z.string().optional(), limit: z.number().optional().default(50),
|
|
1527
|
+
}, async ({ symbol, key, agent, limit }) => {
|
|
1528
|
+
const { db } = getDb();
|
|
1529
|
+
const store = new AnnotationStore(db.connection);
|
|
1530
|
+
const results = store.recall({ symbol, key, agent, limit });
|
|
1531
|
+
if (results.length === 0)
|
|
1532
|
+
return { content: [{ type: 'text', text: 'No annotations found.' }] };
|
|
1533
|
+
const lines = [`${results.length} annotation(s):\n`];
|
|
1534
|
+
for (const a of results) {
|
|
1535
|
+
lines.push(`[${a.key}] ${a.symbol} — ${a.value.slice(0, 120)}`);
|
|
1536
|
+
lines.push(` confidence: ${a.confidence.toFixed(1)} | agent: ${a.agent ?? '?'} | ${a.updatedAt}\n`);
|
|
1537
|
+
}
|
|
1538
|
+
return { content: [{ type: 'text', text: lines.join('\n') }] };
|
|
1539
|
+
});
|
|
1540
|
+
// ═══ session_start ═══
|
|
1541
|
+
server.tool('session_start', 'Start a new session. Returns a session ID to use with annotate, session_end, and handoff.', { agent: z.string().describe('Agent name (e.g. vibe-coder, reviewer)') }, async ({ agent }) => {
|
|
1542
|
+
const { db } = getDb();
|
|
1543
|
+
const store = new AnnotationStore(db.connection);
|
|
1544
|
+
const sessionId = store.sessionStart(agent);
|
|
1545
|
+
return { content: [{ type: 'text', text: `Session started: ${sessionId}\nAgent: ${agent}\nUse this ID with annotate() and session_end().` }] };
|
|
1546
|
+
});
|
|
1547
|
+
// ═══ session_context ═══
|
|
1548
|
+
server.tool('session_context', 'Get metadata about a session: annotations, tool calls, duration.', { session_id: z.string() }, async ({ session_id }) => {
|
|
1549
|
+
const { db } = getDb();
|
|
1550
|
+
const store = new AnnotationStore(db.connection);
|
|
1551
|
+
const ctx = store.sessionContext(session_id);
|
|
1552
|
+
if (!ctx.session)
|
|
1553
|
+
return { content: [{ type: 'text', text: `Session "${session_id}" not found.` }] };
|
|
1554
|
+
const s = ctx.session;
|
|
1555
|
+
const lines = [
|
|
1556
|
+
`Session: ${s.id}`,
|
|
1557
|
+
`Agent: ${s.agent} | Status: ${s.status}`,
|
|
1558
|
+
`Started: ${s.startedAt} | Ended: ${s.endedAt ?? 'in progress'}`,
|
|
1559
|
+
`Tool calls: ${s.toolCallsCount} | Annotations: ${s.annotationsCount}`,
|
|
1560
|
+
];
|
|
1561
|
+
if (s.context)
|
|
1562
|
+
lines.push(`Context: ${s.context}`);
|
|
1563
|
+
if (ctx.annotations.length > 0) {
|
|
1564
|
+
lines.push(`\nAnnotations (${ctx.annotations.length}):`);
|
|
1565
|
+
for (const a of ctx.annotations) {
|
|
1566
|
+
lines.push(` [${a.key}] ${a.symbol}: ${a.value.slice(0, 80)}`);
|
|
1567
|
+
}
|
|
1568
|
+
}
|
|
1569
|
+
return { content: [{ type: 'text', text: lines.join('\n') }] };
|
|
1570
|
+
});
|
|
1571
|
+
// ═══ session_end ═══
|
|
1572
|
+
server.tool('session_end', 'End a session and record its stats. Use at the end of every session.', { session_id: z.string(), status: z.enum(['completed', 'failed']).optional().default('completed') }, async ({ session_id, status }) => {
|
|
1573
|
+
const { db } = getDb();
|
|
1574
|
+
const store = new AnnotationStore(db.connection);
|
|
1575
|
+
const summary = store.sessionEnd(session_id, status);
|
|
1576
|
+
return { content: [{ type: 'text', text: `Session ended: ${session_id}\nStatus: ${status}\nAnnotations: ${summary.annotationCount}` }] };
|
|
1577
|
+
});
|
|
1578
|
+
// ═══ handoff ═══
|
|
1579
|
+
server.tool('handoff', 'Transfer context from one agent session to another. Ends the source session and creates a new one for the target agent.', {
|
|
1580
|
+
from_session: z.string(), to_agent: z.string(),
|
|
1581
|
+
context: z.string().describe('Summary of what was done, key decisions, and caveats for the next agent'),
|
|
1582
|
+
}, async ({ from_session, to_agent, context }) => {
|
|
1583
|
+
const { db } = getDb();
|
|
1584
|
+
const store = new AnnotationStore(db.connection);
|
|
1585
|
+
const result = store.handoff(from_session, to_agent, context);
|
|
1586
|
+
return { content: [{ type: 'text', text: `Handoff complete.\nNew session: ${result.newSessionId}\nAgent: ${to_agent}\nAnnotations copied: ${result.annotationsCopied}` }] };
|
|
1587
|
+
});
|
|
1588
|
+
// ═══ semantic_search ═══
|
|
1589
|
+
server.tool('semantic_search', 'Search symbols by semantic meaning (falls back to FTS5 keyword search when embeddings unavailable).', { query: z.string(), limit: z.number().optional().default(10), repo: z.string().optional() }, async ({ query, limit, repo }) => {
|
|
1590
|
+
const { db } = getDb(repo);
|
|
1591
|
+
if (db.searchSymbols(query, limit).length > 0) {
|
|
1592
|
+
const results = db.searchSymbols(query, limit);
|
|
1593
|
+
const lines = [`Semantic search (FTS5 fallback — embeddings not available):\n`];
|
|
1594
|
+
for (const s of results) {
|
|
1595
|
+
lines.push(`${s.name} [${s.kind}] ${s.filePath}:${s.startLine}${s.exported ? ' (exported)' : ''}`);
|
|
1596
|
+
}
|
|
1597
|
+
return { content: [{ type: 'text', text: lines.join('\n') }] };
|
|
1598
|
+
}
|
|
1599
|
+
return { content: [{ type: 'text', text: `No results for "${query}". Embeddings not available. Run \`milens analyze --embeddings\` for semantic search.` }] };
|
|
1600
|
+
});
|
|
1601
|
+
// ═══ find_similar ═══
|
|
1602
|
+
server.tool('find_similar', 'Find symbols topologically similar to a given symbol (shared callers/callees). Useful for finding patterns to copy or refactor together.', { name: z.string(), limit: z.number().optional().default(10), repo: z.string().optional() }, async ({ name, limit, repo }) => {
|
|
1603
|
+
const { db } = getDb(repo);
|
|
1604
|
+
const syms = db.findSymbolByName(name);
|
|
1605
|
+
if (syms.length === 0)
|
|
1606
|
+
return { content: [{ type: 'text', text: `"${name}" not found.` }] };
|
|
1607
|
+
const results = db.findTopologicallySimilar(syms[0].id, limit);
|
|
1608
|
+
if (results.length === 0)
|
|
1609
|
+
return { content: [{ type: 'text', text: `No similar symbols found for "${name}".` }] };
|
|
1610
|
+
const lines = [`Symbols similar to "${name}":\n`];
|
|
1611
|
+
for (const r of results) {
|
|
1612
|
+
lines.push(` ${r.symbol.name} [${r.symbol.kind}] ${r.symbol.filePath}:${r.symbol.startLine} — similarity: ${r.similarity.toFixed(2)}`);
|
|
1613
|
+
}
|
|
1614
|
+
return { content: [{ type: 'text', text: lines.join('\n') }] };
|
|
1615
|
+
});
|
|
1311
1616
|
// ══════════════════════════════════════════════
|
|
1312
1617
|
// ── MCP Resources ──
|
|
1313
1618
|
// ══════════════════════════════════════════════
|
|
@@ -1462,6 +1767,185 @@ export function createMcpServer(rootPath) {
|
|
|
1462
1767
|
},
|
|
1463
1768
|
}],
|
|
1464
1769
|
}));
|
|
1770
|
+
// ── Prompt: vibe-code-planner ──
|
|
1771
|
+
server.prompt('vibe-code-planner', 'ECC-style Planner Agent workflow: analyze codebase, create implementation plan with blast radius awareness', { feature: z.string().describe('Feature or task name to plan') }, ({ feature }) => ({
|
|
1772
|
+
messages: [{
|
|
1773
|
+
role: 'user',
|
|
1774
|
+
content: {
|
|
1775
|
+
type: 'text',
|
|
1776
|
+
text: `I am the Planner Agent. I need to create an implementation plan for "${feature}".\n\n` +
|
|
1777
|
+
`Follow this ECC Planner workflow:\n\n` +
|
|
1778
|
+
`PHASE 1 — CODEBASE INTELLIGENCE:\n` +
|
|
1779
|
+
`1. Run \`codebase_summary()\` to understand the project structure\n` +
|
|
1780
|
+
`2. Run \`domains()\` to see module clusters\n` +
|
|
1781
|
+
`3. Run \`routes()\` to find relevant API endpoints\n\n` +
|
|
1782
|
+
`PHASE 2 — TARGET ANALYSIS:\n` +
|
|
1783
|
+
`4. Run \`smart_context({name: "keySymbol", intent: "edit"})\` for each affected symbol\n` +
|
|
1784
|
+
`5. Run \`edit_check({name: "keySymbol"})\` for safety\n` +
|
|
1785
|
+
`6. Run \`trace({to: "keySymbol"})\` to understand execution flow\n\n` +
|
|
1786
|
+
`PHASE 3 — IMPACT PREDICTION:\n` +
|
|
1787
|
+
`7. Run \`impact({target: "keySymbol", depth: 3})\` to see blast radius\n` +
|
|
1788
|
+
`8. Run \`explain_relationship({from: "A", to: "B"})\` for distant dependencies\n\n` +
|
|
1789
|
+
`PHASE 4 — TEST STRATEGY:\n` +
|
|
1790
|
+
`9. Run \`test_plan({name: "keySymbol"})\` for mock strategy\n` +
|
|
1791
|
+
`10. Run \`test_coverage_gaps()\` to check existing coverage\n\n` +
|
|
1792
|
+
`PHASE 5 — FINAL PLAN:\n` +
|
|
1793
|
+
`Output a plan.md with: Overview, Architecture Changes, Implementation Steps (file+action+why+deps+risk), Testing Strategy, Risks & Mitigations, Success Criteria.\n\n` +
|
|
1794
|
+
`Use the ECC plan format with specific file paths, dependencies, and risk levels (LOW/MEDIUM/HIGH).`,
|
|
1795
|
+
},
|
|
1796
|
+
}],
|
|
1797
|
+
}));
|
|
1798
|
+
// ── Prompt: vibe-code-reviewer ──
|
|
1799
|
+
server.prompt('vibe-code-reviewer', 'ECC-style Reviewer Agent workflow: PR risk assessment, dead code detection, security scan', { session_id: z.string().optional().describe('Optional session ID for annotation context') }, ({ session_id }) => ({
|
|
1800
|
+
messages: [{
|
|
1801
|
+
role: 'user',
|
|
1802
|
+
content: {
|
|
1803
|
+
type: 'text',
|
|
1804
|
+
text: `I am the Reviewer Agent. Review the current changes thoroughly.${session_id ? ` Session: ${session_id}` : ''}\n\n` +
|
|
1805
|
+
`Follow this ECC Reviewer workflow:\n\n` +
|
|
1806
|
+
`1. Run \`review_pr()\` to get risk scores for all changed symbols\n` +
|
|
1807
|
+
`2. For each CRITICAL/HIGH symbol:\n` +
|
|
1808
|
+
` a. Run \`review_symbol({name})\` for deep dive\n` +
|
|
1809
|
+
` b. Run \`context({name})\` to see relationships\n` +
|
|
1810
|
+
` c. Run \`grep({pattern: "symbolName"})\` for text references\n` +
|
|
1811
|
+
`3. Run \`find_dead_code()\` to detect orphaned symbols\n` +
|
|
1812
|
+
`4. Run \`grep({pattern: "password|secret|api_key|token", scope: "code"})\` for secrets\n` +
|
|
1813
|
+
`5. Run \`grep({pattern: "TODO|FIXME|HACK|console\\\\.log", scope: "code"})\` for tech debt\n` +
|
|
1814
|
+
`6. Run \`detect_changes()\` to verify expected files only\n` +
|
|
1815
|
+
`7. Create a review report: symbols OK to merge vs symbols needing fixes\n` +
|
|
1816
|
+
`8. Run \`annotate({symbol, key: "bug"|"security", value})\` for any critical findings`,
|
|
1817
|
+
},
|
|
1818
|
+
}],
|
|
1819
|
+
}));
|
|
1820
|
+
// ── Prompt: closed-loop-session ──
|
|
1821
|
+
server.prompt('closed-loop-session', 'Complete 6-phase closed-loop session: Analyze → Plan → Code → Verify → Learn → Improve', { task: z.string().describe('Task description'), agent: z.string().optional().default('vibe-coder') }, ({ task, agent }) => ({
|
|
1822
|
+
messages: [{
|
|
1823
|
+
role: 'user',
|
|
1824
|
+
content: {
|
|
1825
|
+
type: 'text',
|
|
1826
|
+
text: `Run a complete closed-loop development session for: "${task}"\n\n` +
|
|
1827
|
+
`PHASE 1 — ANALYZE (bootstrap):\n` +
|
|
1828
|
+
` session_start({agent: "${agent}"}) → codebase_summary() → domains() → recall()\n\n` +
|
|
1829
|
+
`PHASE 2 — PLAN:\n` +
|
|
1830
|
+
` smart_context({intent: "edit"}) → edit_check() → impact({depth: 3}) → test_plan()\n\n` +
|
|
1831
|
+
`PHASE 3 — CODE:\n` +
|
|
1832
|
+
` Implement changes with guard: edit_check() before each edit, impact() mid-edit, context() for reference\n\n` +
|
|
1833
|
+
`PHASE 4 — VERIFY:\n` +
|
|
1834
|
+
` detect_changes() → test_impact() → review_pr() → test_coverage_gaps() → grep(secrets)\n\n` +
|
|
1835
|
+
`PHASE 5 — LEARN:\n` +
|
|
1836
|
+
` annotate() key observations → session_context() → handoff() if needed\n\n` +
|
|
1837
|
+
`PHASE 6 — IMPROVE:\n` +
|
|
1838
|
+
` milens evolve (if patterns ready) → milens metrics (check health)\n\n` +
|
|
1839
|
+
`At the end: session_end({session_id}) to record stats.`,
|
|
1840
|
+
},
|
|
1841
|
+
}],
|
|
1842
|
+
}));
|
|
1843
|
+
// ── Register MCP Prompts (W1) ──
|
|
1844
|
+
registerAllPrompts(server);
|
|
1845
|
+
// ── Tool: security_scan (S2) ──
|
|
1846
|
+
server.tool('security_scan', 'Scan codebase for security vulnerabilities using 50+ built-in rules. Replaces multiple manual grep() calls. Categories: secrets, injection, unicode, dangerous, config, data-leak, crypto, auth, file-access.', {
|
|
1847
|
+
scope: z.enum(['all', 'secrets', 'injection', 'unicode', 'dangerous', 'config', 'data-leak', 'crypto', 'auth', 'file-access']).optional().default('all').describe('Scan scope'),
|
|
1848
|
+
repo: z.string().optional().describe('Repository root path'),
|
|
1849
|
+
severity: z.enum(['CRITICAL', 'HIGH', 'MEDIUM', 'LOW']).optional().describe('Minimum severity filter'),
|
|
1850
|
+
limit: z.number().optional().default(50).describe('Max findings'),
|
|
1851
|
+
}, async ({ scope, repo, severity, limit }) => {
|
|
1852
|
+
const { db, root } = getDb(repo);
|
|
1853
|
+
const rules = loadRules();
|
|
1854
|
+
// Filter rules by scope and severity
|
|
1855
|
+
const filtered = rules.filter(r => {
|
|
1856
|
+
if (scope !== 'all' && r.category !== scope)
|
|
1857
|
+
return false;
|
|
1858
|
+
if (severity) {
|
|
1859
|
+
const sevOrder = { CRITICAL: 4, HIGH: 3, MEDIUM: 2, LOW: 1 };
|
|
1860
|
+
if ((sevOrder[r.severity] || 0) < (sevOrder[severity] || 0))
|
|
1861
|
+
return false;
|
|
1862
|
+
}
|
|
1863
|
+
return r.enabled;
|
|
1864
|
+
});
|
|
1865
|
+
// Get all source files from the DB
|
|
1866
|
+
const symbols = db.getAllSymbols();
|
|
1867
|
+
const fileSet = new Set();
|
|
1868
|
+
for (const s of symbols) {
|
|
1869
|
+
if (s.filePath && !s.filePath.includes('node_modules') && !s.filePath.includes('.git')) {
|
|
1870
|
+
fileSet.add(s.filePath);
|
|
1871
|
+
}
|
|
1872
|
+
}
|
|
1873
|
+
const files = [...fileSet].slice(0, 1000); // cap at 1000 files
|
|
1874
|
+
const { readFileSync: rfs, existsSync: es } = await import('node:fs');
|
|
1875
|
+
const { resolve: resolvePath } = await import('node:path');
|
|
1876
|
+
const findings = [];
|
|
1877
|
+
const byCategory = {};
|
|
1878
|
+
const bySeverity = { CRITICAL: 0, HIGH: 0, MEDIUM: 0, LOW: 0 };
|
|
1879
|
+
for (const file of files) {
|
|
1880
|
+
const fullPath = resolvePath(root, file);
|
|
1881
|
+
if (!es(fullPath))
|
|
1882
|
+
continue;
|
|
1883
|
+
// Skip files that don't match rule fileGlobs (simple check)
|
|
1884
|
+
const applicableRules = filtered.filter(r => {
|
|
1885
|
+
if (!r.fileGlob)
|
|
1886
|
+
return true;
|
|
1887
|
+
// Simple glob: just check extension
|
|
1888
|
+
const ext = r.fileGlob.replace('**/*.', '').replace('**/*', '');
|
|
1889
|
+
return file.endsWith(ext) || r.fileGlob === '**/*';
|
|
1890
|
+
});
|
|
1891
|
+
if (applicableRules.length === 0)
|
|
1892
|
+
continue;
|
|
1893
|
+
try {
|
|
1894
|
+
const content = rfs(fullPath, 'utf-8');
|
|
1895
|
+
const lines = content.split('\n');
|
|
1896
|
+
for (const rule of applicableRules) {
|
|
1897
|
+
for (const pattern of rule.patterns) {
|
|
1898
|
+
let match;
|
|
1899
|
+
// Reset regex lastIndex for global patterns
|
|
1900
|
+
pattern.lastIndex = 0;
|
|
1901
|
+
while ((match = pattern.exec(content)) !== null) {
|
|
1902
|
+
const lineNum = content.substring(0, match.index).split('\n').length;
|
|
1903
|
+
const ctxStart = Math.max(0, lineNum - 3);
|
|
1904
|
+
const ctxEnd = Math.min(lines.length, lineNum + 2);
|
|
1905
|
+
const context = lines.slice(ctxStart, ctxEnd).join('\n');
|
|
1906
|
+
findings.push({
|
|
1907
|
+
ruleId: rule.id,
|
|
1908
|
+
category: rule.category,
|
|
1909
|
+
severity: rule.severity,
|
|
1910
|
+
owasp: rule.owasp,
|
|
1911
|
+
file,
|
|
1912
|
+
line: lineNum,
|
|
1913
|
+
match: match[0].length > 100 ? match[0].slice(0, 97) + '...' : match[0],
|
|
1914
|
+
context,
|
|
1915
|
+
fix: rule.fix,
|
|
1916
|
+
});
|
|
1917
|
+
byCategory[rule.category] = (byCategory[rule.category] || 0) + 1;
|
|
1918
|
+
bySeverity[rule.severity] = (bySeverity[rule.severity] || 0) + 1;
|
|
1919
|
+
}
|
|
1920
|
+
}
|
|
1921
|
+
}
|
|
1922
|
+
}
|
|
1923
|
+
catch {
|
|
1924
|
+
// Skip unreadable files
|
|
1925
|
+
}
|
|
1926
|
+
}
|
|
1927
|
+
// Calculate security score (100 - deductions)
|
|
1928
|
+
const deduction = findings.filter((f) => f.severity === 'CRITICAL').length * 5 +
|
|
1929
|
+
findings.filter((f) => f.severity === 'HIGH').length * 2 +
|
|
1930
|
+
findings.filter((f) => f.severity === 'MEDIUM').length * 0.5;
|
|
1931
|
+
const score = Math.max(0, Math.round(100 - deduction));
|
|
1932
|
+
const limited = findings.slice(0, limit);
|
|
1933
|
+
return {
|
|
1934
|
+
content: [{
|
|
1935
|
+
type: 'text',
|
|
1936
|
+
text: JSON.stringify({
|
|
1937
|
+
summary: {
|
|
1938
|
+
totalScanned: files.length,
|
|
1939
|
+
findings: findings.length,
|
|
1940
|
+
byCategory,
|
|
1941
|
+
bySeverity,
|
|
1942
|
+
score,
|
|
1943
|
+
},
|
|
1944
|
+
findings: limited,
|
|
1945
|
+
}, null, 2),
|
|
1946
|
+
}],
|
|
1947
|
+
};
|
|
1948
|
+
});
|
|
1465
1949
|
return server;
|
|
1466
1950
|
}
|
|
1467
1951
|
// ── Transport: stdio ──
|