@misterhuydo/cairn-mcp 1.0.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.
@@ -0,0 +1,103 @@
1
+ import fs from 'fs/promises';
2
+ import path from 'path';
3
+ import { walkRepo, BUILD_FILES } from '../indexer/fileWalker.js';
4
+ import { parseFile } from '../indexer/parserFactory.js';
5
+ import { scanFile } from '../indexer/securityScanner.js';
6
+ import { parseMaven } from '../indexer/buildParsers/mavenParser.js';
7
+ import { parseNpm } from '../indexer/buildParsers/npmParser.js';
8
+ import { parseGradle } from '../indexer/buildParsers/gradleParser.js';
9
+ import { upsertFile, upsertSymbol, clearFileData } from '../graph/nodes.js';
10
+ import { insertDependency, insertBuildDep, insertSecurityFinding } from '../graph/edges.js';
11
+
12
+ function inferFileType(filePath) {
13
+ const fp = filePath.toLowerCase();
14
+ if (fp.includes('test') || fp.includes('spec')) return 'test';
15
+ if (fp.endsWith('pom.xml') || fp.endsWith('package.json') || fp.endsWith('build.gradle')) return 'build';
16
+ if (fp.endsWith('.yml') || fp.endsWith('.yaml') || fp.endsWith('.properties') || fp.endsWith('.env')) return 'config';
17
+ if (fp.endsWith('.md') || fp.endsWith('.html')) return 'doc';
18
+ return 'source';
19
+ }
20
+
21
+ export async function maintain(db, { roots, languages }) {
22
+ const startTime = Date.now();
23
+ const stats = {
24
+ repos_indexed: 0,
25
+ files_by_language: {},
26
+ symbols_total: 0,
27
+ dependencies_mapped: 0,
28
+ security_findings: 0,
29
+ };
30
+
31
+ for (const root of roots) {
32
+ const repoName = path.basename(root);
33
+ stats.repos_indexed++;
34
+
35
+ // Index source files
36
+ const files = await walkRepo(root);
37
+ for (const filePath of files) {
38
+ try {
39
+ const content = await fs.readFile(filePath, 'utf-8');
40
+ const result = await parseFile(filePath, content, repoName);
41
+ if (!result) continue;
42
+
43
+ const { language } = result;
44
+ if (languages && !languages.includes(language)) continue;
45
+
46
+ stats.files_by_language[language] = (stats.files_by_language[language] || 0) + 1;
47
+
48
+ const fileId = upsertFile(db, {
49
+ repo: repoName,
50
+ path: filePath,
51
+ language,
52
+ file_type: inferFileType(filePath),
53
+ });
54
+
55
+ clearFileData(db, fileId);
56
+
57
+ for (const sym of (result.symbols || [])) {
58
+ upsertSymbol(db, { ...sym, file_id: fileId, language });
59
+ stats.symbols_total++;
60
+ }
61
+
62
+ for (const imp of (result.imports || [])) {
63
+ insertDependency(db, { from_file_id: fileId, to_fqn: imp, dep_type: 'imports' });
64
+ stats.dependencies_mapped++;
65
+ }
66
+
67
+ const findings = scanFile(filePath, content, language);
68
+ for (const f of findings) {
69
+ insertSecurityFinding(db, { file_id: fileId, ...f });
70
+ stats.security_findings++;
71
+ }
72
+ } catch {
73
+ // Skip unreadable/unparseable files
74
+ }
75
+ }
76
+
77
+ // Index build files
78
+ for (const buildFile of BUILD_FILES) {
79
+ const buildPath = path.join(root, buildFile);
80
+ try {
81
+ const content = await fs.readFile(buildPath, 'utf-8');
82
+ let deps = [];
83
+ if (buildFile === 'pom.xml') deps = parseMaven(buildPath, content, repoName);
84
+ else if (buildFile === 'package.json') deps = parseNpm(buildPath, content, repoName);
85
+ else if (buildFile === 'build.gradle') deps = parseGradle(buildPath, content, repoName);
86
+ for (const dep of deps) insertBuildDep(db, { repo: repoName, ...dep });
87
+ } catch {
88
+ // Build file not present or unreadable
89
+ }
90
+ }
91
+ }
92
+
93
+ // Rebuild FTS5 index
94
+ db.exec(`
95
+ DELETE FROM fts_index;
96
+ INSERT INTO fts_index(fqn, name, description, path, repo, language, kind)
97
+ SELECT s.fqn, s.name, COALESCE(s.description,''), f.path, f.repo, s.language, s.kind
98
+ FROM symbols s JOIN files f ON s.file_id = f.id;
99
+ `);
100
+
101
+ stats.duration_ms = Date.now() - startTime;
102
+ return { content: [{ type: 'text', text: JSON.stringify(stats, null, 2) }] };
103
+ }
@@ -0,0 +1,27 @@
1
+ export function search(db, { query, limit = 10, language, kind }) {
2
+ if (!query?.trim()) {
3
+ return { content: [{ type: 'text', text: '[]' }] };
4
+ }
5
+
6
+ // Build the SQL — FTS5 columns are: fqn, name, description, path, repo, language, kind
7
+ let sql = `
8
+ SELECT fqn, name, kind, language, description, path, repo,
9
+ bm25(fts_index) as score
10
+ FROM fts_index
11
+ WHERE fts_index MATCH ?
12
+ `;
13
+ const params = [query];
14
+
15
+ if (language) { sql += ' AND language = ?'; params.push(language); }
16
+ if (kind) { sql += ' AND kind = ?'; params.push(kind); }
17
+
18
+ sql += ' ORDER BY bm25(fts_index) LIMIT ?';
19
+ params.push(limit);
20
+
21
+ try {
22
+ const results = db.prepare(sql).all(...params);
23
+ return { content: [{ type: 'text', text: JSON.stringify(results, null, 2) }] };
24
+ } catch (err) {
25
+ return { content: [{ type: 'text', text: JSON.stringify({ error: err.message }) }] };
26
+ }
27
+ }
@@ -0,0 +1,53 @@
1
+ export function security(db, { paths, severity = 'HIGH', language }) {
2
+ let sql = `
3
+ SELECT sf.severity, sf.cwe, sf.rule_name as rule, sf.line,
4
+ sf.code_snippet as snippet, sf.description as fix,
5
+ f.path as file, f.language as lang
6
+ FROM security_findings sf
7
+ JOIN files f ON sf.file_id = f.id
8
+ WHERE 1=1
9
+ `;
10
+ const params = [];
11
+
12
+ if (severity === 'HIGH') {
13
+ sql += ` AND sf.severity = 'HIGH'`;
14
+ } else if (severity === 'MEDIUM') {
15
+ sql += ` AND sf.severity IN ('HIGH','MEDIUM')`;
16
+ } else if (severity !== 'ALL') {
17
+ sql += ' AND sf.severity = ?';
18
+ params.push(severity);
19
+ }
20
+
21
+ if (language) {
22
+ sql += ' AND f.language = ?';
23
+ params.push(language);
24
+ }
25
+
26
+ if (paths && paths.length > 0) {
27
+ sql += ` AND (${paths.map(() => 'f.path LIKE ?').join(' OR ')})`;
28
+ params.push(...paths.map(p => `${p}%`));
29
+ }
30
+
31
+ sql += ' ORDER BY sf.severity, f.path';
32
+
33
+ const findings = db.prepare(sql).all(...params);
34
+
35
+ const byLanguage = {};
36
+ const bySeverity = {};
37
+ for (const f of findings) {
38
+ byLanguage[f.lang] = (byLanguage[f.lang] || 0) + 1;
39
+ bySeverity[f.severity] = (bySeverity[f.severity] || 0) + 1;
40
+ }
41
+
42
+ return {
43
+ content: [{
44
+ type: 'text',
45
+ text: JSON.stringify({
46
+ total_findings: findings.length,
47
+ by_language: byLanguage,
48
+ by_severity: bySeverity,
49
+ findings: findings.slice(0, 100),
50
+ }, null, 2),
51
+ }],
52
+ };
53
+ }