@misterhuydo/cairn-mcp 1.6.21 → 1.7.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 +4 -4
- package/package.json +1 -1
- package/src/bundler/outliner.js +12 -9
- package/src/graph/db.js +5 -2
- package/src/graph/nodes.js +5 -5
- package/src/indexer/parsers/javaParser.js +11 -7
- package/src/indexer/parsers/pythonParser.js +7 -3
- package/src/indexer/parsers/tsParser.js +9 -5
- package/src/tools/maintain.js +15 -1
- package/src/tools/outline.js +7 -4
- package/src/tools/search.js +16 -1
package/README.md
CHANGED
|
@@ -44,7 +44,7 @@ No need to re-run `cairn install` — hooks and MCP config carry over between ve
|
|
|
44
44
|
|
|
45
45
|
| Hook | Trigger | Effect |
|
|
46
46
|
|---|---|---|
|
|
47
|
-
| `PreToolUse[Read]` | Every file read | Source files compressed ~68% before Claude sees them; large files show structural outline to save tokens |
|
|
47
|
+
| `PreToolUse[Read]` | Every file read | Source files compressed ~68% before Claude sees them; large files show structural outline with **line numbers** to save tokens |
|
|
48
48
|
| `PreToolUse[Edit]` | Every file edit | Blocks Edit if Claude only saw compressed content — requires a full re-read first |
|
|
49
49
|
| `Stop` | End of every response | Session auto-saved to `.cairn/session.json` |
|
|
50
50
|
| `UserPromptSubmit` | First message of a new session | Fresh project: Claude prompted to run `cairn_maintain`. Returning session: Claude prompted to run `cairn_resume` |
|
|
@@ -66,11 +66,11 @@ No manual steps. The index lives in `.cairn/index.db` inside your project — li
|
|
|
66
66
|
|
|
67
67
|
| Tool | What it does |
|
|
68
68
|
|---|---|
|
|
69
|
-
| `cairn_maintain` | Full index of the current project |
|
|
69
|
+
| `cairn_maintain` | Full index of the current project. Suggests adding `.cairn/` to `.gitignore` if not already present |
|
|
70
70
|
| `cairn_resume` | Restore last session + re-index only changed files |
|
|
71
|
-
| `cairn_search` | Find classes, functions, components by name or concept |
|
|
71
|
+
| `cairn_search` | Find classes, functions, components by name or concept — results include file path and **line number** |
|
|
72
72
|
| `cairn_describe` | Summarize what a folder or module does |
|
|
73
|
-
| `cairn_outline` | Structural outline + heuristic issue detection. On large federated projects returns a per-service summary; use `repo` param to drill into one service |
|
|
73
|
+
| `cairn_outline` | Structural outline with **line numbers** per symbol + heuristic issue detection. On large federated projects returns a per-service summary; use `repo` param to drill into one service |
|
|
74
74
|
| `cairn_code_graph` | Dependency health — instability, cycles, load-bearing modules |
|
|
75
75
|
| `cairn_security` | Scan for XSS, SQLi, hardcoded secrets, weak crypto, and more |
|
|
76
76
|
| `cairn_todos` | Scan codebase for TODO/FIXME/HACK comments, add manual items, resolve and list them |
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@misterhuydo/cairn-mcp",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.7.0",
|
|
4
4
|
"description": "MCP server that gives Claude Code persistent memory across sessions. Index your codebase once, search symbols, bundle source, scan for vulnerabilities, and checkpoint/resume work — across Java, TypeScript, Vue, Python, SQL and more.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "index.js",
|
package/src/bundler/outliner.js
CHANGED
|
@@ -24,8 +24,8 @@ function outlineCStyle(content, { annotations = false } = {}) {
|
|
|
24
24
|
const out = [];
|
|
25
25
|
let depth = 0;
|
|
26
26
|
|
|
27
|
-
for (
|
|
28
|
-
const line =
|
|
27
|
+
for (let i = 0; i < lines.length; i++) {
|
|
28
|
+
const line = lines[i].trim();
|
|
29
29
|
if (!line) continue;
|
|
30
30
|
|
|
31
31
|
const { opens, closes } = countBraces(line);
|
|
@@ -33,11 +33,11 @@ function outlineCStyle(content, { annotations = false } = {}) {
|
|
|
33
33
|
|
|
34
34
|
if (depth === 0) {
|
|
35
35
|
const sig = topLevelSig(line);
|
|
36
|
-
if (sig !== null) out.push(sig);
|
|
36
|
+
if (sig !== null) out.push(`${i + 1}: ${sig}`);
|
|
37
37
|
else if (line === '}') out.push('}'); // close class/block
|
|
38
38
|
} else if (depth === 1) {
|
|
39
39
|
const sig = memberSig(line, annotations);
|
|
40
|
-
if (sig !== null) out.push(INDENT + sig);
|
|
40
|
+
if (sig !== null) out.push(`${INDENT}${i + 1}: ${sig}`);
|
|
41
41
|
}
|
|
42
42
|
// depth > 1: skip (inside method bodies)
|
|
43
43
|
|
|
@@ -138,14 +138,16 @@ function countBraces(line) {
|
|
|
138
138
|
|
|
139
139
|
function outlinePython(content) {
|
|
140
140
|
const out = [];
|
|
141
|
-
|
|
141
|
+
const lines = content.split('\n');
|
|
142
|
+
for (let i = 0; i < lines.length; i++) {
|
|
143
|
+
const line = lines[i];
|
|
142
144
|
if (!line.trim()) continue;
|
|
143
145
|
const trimmed = line.trimStart();
|
|
144
146
|
if (trimmed.startsWith('import ') || trimmed.startsWith('from ')) {
|
|
145
147
|
out.push(line);
|
|
146
148
|
} else if (trimmed.startsWith('class ') || trimmed.startsWith('def ') ||
|
|
147
149
|
trimmed.startsWith('async def ')) {
|
|
148
|
-
out.push(line.replace(/:(\s*)$/, ':'));
|
|
150
|
+
out.push(`${i + 1}: ${line.trim().replace(/:(\s*)$/, ':')}`);
|
|
149
151
|
}
|
|
150
152
|
}
|
|
151
153
|
return out.join('\n');
|
|
@@ -175,10 +177,11 @@ function outlineVue(content) {
|
|
|
175
177
|
|
|
176
178
|
function outlineSQL(content) {
|
|
177
179
|
const out = [];
|
|
178
|
-
|
|
179
|
-
|
|
180
|
+
const lines = content.split('\n');
|
|
181
|
+
for (let i = 0; i < lines.length; i++) {
|
|
182
|
+
const t = lines[i].trim();
|
|
180
183
|
if (/^(CREATE|ALTER|DROP|INSERT|UPDATE|DELETE|SELECT|WITH|GRANT|REVOKE|BEGIN|COMMIT)\b/i.test(t)) {
|
|
181
|
-
out.push(t.slice(0, 120)
|
|
184
|
+
out.push(`${i + 1}: ${t.slice(0, 120)}${t.length > 120 ? '...' : ''}`);
|
|
182
185
|
}
|
|
183
186
|
}
|
|
184
187
|
return out.join('\n');
|
package/src/graph/db.js
CHANGED
|
@@ -21,7 +21,8 @@ const SCHEMA = `
|
|
|
21
21
|
kind TEXT,
|
|
22
22
|
language TEXT,
|
|
23
23
|
exported INTEGER DEFAULT 0,
|
|
24
|
-
description TEXT
|
|
24
|
+
description TEXT,
|
|
25
|
+
line INTEGER
|
|
25
26
|
);
|
|
26
27
|
|
|
27
28
|
CREATE TABLE IF NOT EXISTS dependencies (
|
|
@@ -147,7 +148,7 @@ function tableSelect(schema, table, offset) {
|
|
|
147
148
|
case 'files':
|
|
148
149
|
return `SELECT id+${offset} AS id, repo, path, language, file_type, last_indexed FROM ${q}.files`;
|
|
149
150
|
case 'symbols':
|
|
150
|
-
return `SELECT id+${offset} AS id, file_id+${offset} AS file_id, fqn, name, kind, language, exported, description FROM ${q}.symbols`;
|
|
151
|
+
return `SELECT id+${offset} AS id, file_id+${offset} AS file_id, fqn, name, kind, language, exported, description, line FROM ${q}.symbols`;
|
|
151
152
|
case 'dependencies':
|
|
152
153
|
return `SELECT from_file_id+${offset} AS from_file_id, to_fqn, dep_type FROM ${q}.dependencies`;
|
|
153
154
|
case 'build_deps':
|
|
@@ -187,6 +188,8 @@ export function openDB() {
|
|
|
187
188
|
db.exec('PRAGMA journal_mode = WAL');
|
|
188
189
|
db.exec('PRAGMA synchronous = NORMAL');
|
|
189
190
|
db.exec(SCHEMA);
|
|
191
|
+
// Migration: add line column to existing DBs that predate v1.7.0
|
|
192
|
+
try { db.exec('ALTER TABLE main.symbols ADD COLUMN line INTEGER'); } catch {}
|
|
190
193
|
mountParentSubIndexes(db);
|
|
191
194
|
refreshFederatedViews(db);
|
|
192
195
|
return db;
|
package/src/graph/nodes.js
CHANGED
|
@@ -14,16 +14,16 @@ export function upsertFile(db, { repo, path, language, file_type }) {
|
|
|
14
14
|
return result.lastInsertRowid;
|
|
15
15
|
}
|
|
16
16
|
|
|
17
|
-
export function upsertSymbol(db, { file_id, fqn, name, kind, language, exported, description }) {
|
|
17
|
+
export function upsertSymbol(db, { file_id, fqn, name, kind, language, exported, description, line }) {
|
|
18
18
|
const existing = db.prepare('SELECT id FROM main.symbols WHERE fqn = ?').get(fqn);
|
|
19
19
|
if (existing) {
|
|
20
20
|
db.prepare(
|
|
21
|
-
'UPDATE main.symbols SET file_id=?, name=?, kind=?, language=?, exported=?, description=? WHERE id=?'
|
|
22
|
-
).run(file_id, name, kind, language, exported ? 1 : 0, description || '', existing.id);
|
|
21
|
+
'UPDATE main.symbols SET file_id=?, name=?, kind=?, language=?, exported=?, description=?, line=? WHERE id=?'
|
|
22
|
+
).run(file_id, name, kind, language, exported ? 1 : 0, description || '', line ?? null, existing.id);
|
|
23
23
|
} else {
|
|
24
24
|
db.prepare(
|
|
25
|
-
'INSERT INTO main.symbols (file_id, fqn, name, kind, language, exported, description) VALUES (?, ?, ?, ?, ?, ?, ?)'
|
|
26
|
-
).run(file_id, fqn, name, kind, language, exported ? 1 : 0, description || '');
|
|
25
|
+
'INSERT INTO main.symbols (file_id, fqn, name, kind, language, exported, description, line) VALUES (?, ?, ?, ?, ?, ?, ?, ?)'
|
|
26
|
+
).run(file_id, fqn, name, kind, language, exported ? 1 : 0, description || '', line ?? null);
|
|
27
27
|
}
|
|
28
28
|
}
|
|
29
29
|
|
|
@@ -1,17 +1,21 @@
|
|
|
1
|
+
function lineOf(content, index) {
|
|
2
|
+
return content.substring(0, index).split('\n').length;
|
|
3
|
+
}
|
|
4
|
+
|
|
1
5
|
export function parseJava(filePath, content, repoName) {
|
|
2
6
|
const pkg = content.match(/^package\s+([\w.]+);/m)?.[1] || '';
|
|
3
7
|
const imports = [...content.matchAll(/^import\s+([\w.]+);/gm)].map(m => m[1]);
|
|
4
8
|
|
|
5
|
-
const
|
|
6
|
-
|
|
7
|
-
);
|
|
9
|
+
const classRe = /(?:public|protected)?\s+(?:abstract\s+)?(?:class|interface|enum|record)\s+(\w+)/;
|
|
10
|
+
const classMatch = classRe.exec(content);
|
|
8
11
|
const name = classMatch?.[1] || '';
|
|
9
12
|
const kind = classMatch?.[0]?.match(/class|interface|enum|record/)?.[0] || 'class';
|
|
10
13
|
const fqn = pkg ? `${pkg}.${name}` : name;
|
|
14
|
+
const classLine = classMatch ? lineOf(content, classMatch.index) : null;
|
|
11
15
|
|
|
12
|
-
const
|
|
16
|
+
const methodMatches = [...content.matchAll(
|
|
13
17
|
/(?:public|protected|private)[^{;]*\s+(\w+)\s*\([^)]*\)\s*(?:throws[^{]*)?\{/gm
|
|
14
|
-
)]
|
|
18
|
+
)];
|
|
15
19
|
|
|
16
20
|
const extendsMatch = content.match(/extends\s+([\w.]+)/)?.[1];
|
|
17
21
|
const implementsMatch = content.match(/implements\s+([\w.,\s]+)/)?.[1]
|
|
@@ -21,12 +25,12 @@ export function parseJava(filePath, content, repoName) {
|
|
|
21
25
|
?.replace(/\s*\*\s?/g, ' ').trim();
|
|
22
26
|
|
|
23
27
|
const methodSymbols = fqn
|
|
24
|
-
?
|
|
28
|
+
? methodMatches.map(m => ({ name: m[1], fqn: `${fqn}.${m[1]}`, kind: 'method', exported: false, description: '', line: lineOf(content, m.index) }))
|
|
25
29
|
: [];
|
|
26
30
|
|
|
27
31
|
return {
|
|
28
32
|
language: 'java',
|
|
29
|
-
symbols: [{ name, fqn, kind, exported: true, description: javadoc || '' }, ...methodSymbols],
|
|
33
|
+
symbols: [{ name, fqn, kind, exported: true, description: javadoc || '', line: classLine }, ...methodSymbols],
|
|
30
34
|
imports,
|
|
31
35
|
extends: extendsMatch ? [extendsMatch] : [],
|
|
32
36
|
implements: implementsMatch || [],
|
|
@@ -1,3 +1,7 @@
|
|
|
1
|
+
function lineOf(content, index) {
|
|
2
|
+
return content.substring(0, index).split('\n').length;
|
|
3
|
+
}
|
|
4
|
+
|
|
1
5
|
export function parsePython(filePath, content, repoName) {
|
|
2
6
|
const symbols = [];
|
|
3
7
|
const imports = [];
|
|
@@ -9,17 +13,17 @@ export function parsePython(filePath, content, repoName) {
|
|
|
9
13
|
|
|
10
14
|
for (const m of content.matchAll(/^class\s+(\w+)(?:\(([^)]*)\))?:/gm)) {
|
|
11
15
|
const docstring = content.slice(m.index).match(/:\s*\n\s+"""([\s\S]*?)"""/)?.[1]?.trim();
|
|
12
|
-
symbols.push({ name: m[1], fqn: `${filePath}::${m[1]}`, kind: 'class', exported: true, description: docstring || '' });
|
|
16
|
+
symbols.push({ name: m[1], fqn: `${filePath}::${m[1]}`, kind: 'class', exported: true, description: docstring || '', line: lineOf(content, m.index) });
|
|
13
17
|
}
|
|
14
18
|
|
|
15
19
|
for (const m of content.matchAll(/^(?:async\s+)?def\s+(\w+)\s*\(/gm)) {
|
|
16
20
|
if (m[1].startsWith('_')) continue;
|
|
17
21
|
const docstring = content.slice(m.index).match(/\).*?:\s*\n\s+"""([\s\S]*?)"""/)?.[1]?.trim();
|
|
18
|
-
symbols.push({ name: m[1], fqn: `${filePath}::${m[1]}`, kind: 'function', exported: true, description: docstring || '' });
|
|
22
|
+
symbols.push({ name: m[1], fqn: `${filePath}::${m[1]}`, kind: 'function', exported: true, description: docstring || '', line: lineOf(content, m.index) });
|
|
19
23
|
}
|
|
20
24
|
|
|
21
25
|
for (const m of content.matchAll(/^@([\w.]+)(?:\(([^)]*)\))?/gm))
|
|
22
|
-
symbols.push({ name: m[1], fqn: `${filePath}::@${m[1]}`, kind: 'decorator', exported: false, description: '' });
|
|
26
|
+
symbols.push({ name: m[1], fqn: `${filePath}::@${m[1]}`, kind: 'decorator', exported: false, description: '', line: lineOf(content, m.index) });
|
|
23
27
|
|
|
24
28
|
return { language: 'python', symbols, imports };
|
|
25
29
|
}
|
|
@@ -1,3 +1,7 @@
|
|
|
1
|
+
function lineOf(content, index) {
|
|
2
|
+
return content.substring(0, index).split('\n').length;
|
|
3
|
+
}
|
|
4
|
+
|
|
1
5
|
export function parseTS(filePath, content, repoName, language = 'typescript') {
|
|
2
6
|
const symbols = [];
|
|
3
7
|
const imports = [];
|
|
@@ -14,26 +18,26 @@ export function parseTS(filePath, content, repoName, language = 'typescript') {
|
|
|
14
18
|
for (const m of content.matchAll(/(?:export\s+)?(?:abstract\s+)?class\s+(\w+)/g)) {
|
|
15
19
|
const exported = m[0].startsWith('export');
|
|
16
20
|
const jsdoc = extractJSDoc(content, m.index);
|
|
17
|
-
symbols.push({ name: m[1], fqn: `${filePath}::${m[1]}`, kind: 'class', exported, description: jsdoc });
|
|
21
|
+
symbols.push({ name: m[1], fqn: `${filePath}::${m[1]}`, kind: 'class', exported, description: jsdoc, line: lineOf(content, m.index) });
|
|
18
22
|
}
|
|
19
23
|
|
|
20
24
|
// Interfaces & Types
|
|
21
25
|
for (const m of content.matchAll(/export\s+(?:interface|type)\s+(\w+)/g))
|
|
22
|
-
symbols.push({ name: m[1], fqn: `${filePath}::${m[1]}`, kind: 'interface', exported: true, description: '' });
|
|
26
|
+
symbols.push({ name: m[1], fqn: `${filePath}::${m[1]}`, kind: 'interface', exported: true, description: '', line: lineOf(content, m.index) });
|
|
23
27
|
|
|
24
28
|
// Exported named functions
|
|
25
29
|
for (const m of content.matchAll(/export\s+(?:async\s+)?function\s+(\w+)/g)) {
|
|
26
30
|
const jsdoc = extractJSDoc(content, m.index);
|
|
27
|
-
symbols.push({ name: m[1], fqn: `${filePath}::${m[1]}`, kind: 'function', exported: true, description: jsdoc });
|
|
31
|
+
symbols.push({ name: m[1], fqn: `${filePath}::${m[1]}`, kind: 'function', exported: true, description: jsdoc, line: lineOf(content, m.index) });
|
|
28
32
|
}
|
|
29
33
|
|
|
30
34
|
// Exported arrow functions: export const foo = (...) =>
|
|
31
35
|
for (const m of content.matchAll(/export\s+const\s+(\w+)\s*=\s*(?:async\s+)?\(/g))
|
|
32
|
-
symbols.push({ name: m[1], fqn: `${filePath}::${m[1]}`, kind: 'function', exported: true, description: '' });
|
|
36
|
+
symbols.push({ name: m[1], fqn: `${filePath}::${m[1]}`, kind: 'function', exported: true, description: '', line: lineOf(content, m.index) });
|
|
33
37
|
|
|
34
38
|
// Enums
|
|
35
39
|
for (const m of content.matchAll(/export\s+(?:const\s+)?enum\s+(\w+)/g))
|
|
36
|
-
symbols.push({ name: m[1], fqn: `${filePath}::${m[1]}`, kind: 'enum', exported: true, description: '' });
|
|
40
|
+
symbols.push({ name: m[1], fqn: `${filePath}::${m[1]}`, kind: 'enum', exported: true, description: '', line: lineOf(content, m.index) });
|
|
37
41
|
|
|
38
42
|
return { language, symbols, imports };
|
|
39
43
|
}
|
package/src/tools/maintain.js
CHANGED
|
@@ -132,8 +132,22 @@ export async function maintain(db, { languages } = {}) {
|
|
|
132
132
|
refreshFederatedViews(db);
|
|
133
133
|
}
|
|
134
134
|
|
|
135
|
+
// Check if .cairn is gitignored; suggest adding it if not
|
|
136
|
+
let gitignoreHint = null;
|
|
137
|
+
try {
|
|
138
|
+
const gitignorePath = path.join(root, '.gitignore');
|
|
139
|
+
const gitignoreContent = await fs.readFile(gitignorePath, 'utf-8').catch(() => '');
|
|
140
|
+
const lines = gitignoreContent.split('\n').map(l => l.trim());
|
|
141
|
+
const covered = lines.some(l => l === '.cairn' || l === '.cairn/' || l === '/.cairn' || l === '/.cairn/');
|
|
142
|
+
if (!covered) {
|
|
143
|
+
gitignoreHint = 'Add .cairn/ to your .gitignore to avoid committing the index and session cache.';
|
|
144
|
+
}
|
|
145
|
+
} catch {}
|
|
146
|
+
|
|
135
147
|
stats.duration_ms = Date.now() - startTime;
|
|
136
|
-
|
|
148
|
+
const result = { ...stats };
|
|
149
|
+
if (gitignoreHint) result.gitignore_hint = gitignoreHint;
|
|
150
|
+
return { content: [{ type: 'text', text: JSON.stringify(result, null, 2) }] };
|
|
137
151
|
}
|
|
138
152
|
|
|
139
153
|
// Incremental re-index: only process a specific list of changed file paths
|
package/src/tools/outline.js
CHANGED
|
@@ -105,7 +105,7 @@ export function outlineProject(db, { paths: scopePaths, repo, checks } = {}) {
|
|
|
105
105
|
}
|
|
106
106
|
|
|
107
107
|
const allSymbols = db.prepare(
|
|
108
|
-
`SELECT file_id, fqn, name, kind, exported FROM symbols ORDER BY file_id, kind, name`
|
|
108
|
+
`SELECT file_id, fqn, name, kind, exported, line FROM symbols ORDER BY file_id, kind, name`
|
|
109
109
|
).all();
|
|
110
110
|
|
|
111
111
|
for (const sym of allSymbols) {
|
|
@@ -141,16 +141,19 @@ export function outlineProject(db, { paths: scopePaths, repo, checks } = {}) {
|
|
|
141
141
|
|
|
142
142
|
for (const [className, { meta, members }] of classes) {
|
|
143
143
|
const exported = meta.exported ? '+' : '-';
|
|
144
|
-
|
|
144
|
+
const lineTag = meta.line ? `:${meta.line}` : '';
|
|
145
|
+
outlineLines.push(` ${exported} ${meta.kind} ${className}${lineTag}`);
|
|
145
146
|
for (const m of members) {
|
|
146
147
|
const vis = m.exported ? '+' : '-';
|
|
147
|
-
|
|
148
|
+
const mLineTag = m.line ? `:${m.line}` : '';
|
|
149
|
+
outlineLines.push(` ${vis} ${m.name}${mLineTag} (${m.kind})`);
|
|
148
150
|
}
|
|
149
151
|
}
|
|
150
152
|
for (const sym of topLevel) {
|
|
151
153
|
if (sym.kind === 'class') continue; // already handled above
|
|
152
154
|
const vis = sym.exported ? '+' : '-';
|
|
153
|
-
|
|
155
|
+
const lineTag = sym.line ? `:${sym.line}` : '';
|
|
156
|
+
outlineLines.push(` ${vis} ${sym.name}${lineTag} (${sym.kind})`);
|
|
154
157
|
}
|
|
155
158
|
}
|
|
156
159
|
|
package/src/tools/search.js
CHANGED
|
@@ -44,5 +44,20 @@ export function search(db, { query, limit = 10, language, kind }) {
|
|
|
44
44
|
return true;
|
|
45
45
|
});
|
|
46
46
|
|
|
47
|
-
|
|
47
|
+
const top = deduped.slice(0, limit);
|
|
48
|
+
|
|
49
|
+
// Look up line numbers from symbols table (not stored in FTS index)
|
|
50
|
+
const fqnToLine = new Map();
|
|
51
|
+
for (const schema of schemas) {
|
|
52
|
+
const missing = top.filter(r => !fqnToLine.has(r.fqn)).map(r => r.fqn);
|
|
53
|
+
if (missing.length === 0) break;
|
|
54
|
+
try {
|
|
55
|
+
const phs = missing.map(() => '?').join(',');
|
|
56
|
+
const rows = db.prepare(`SELECT fqn, line FROM ${schema}.symbols WHERE fqn IN (${phs})`).all(...missing);
|
|
57
|
+
for (const r of rows) if (r.line != null) fqnToLine.set(r.fqn, r.line);
|
|
58
|
+
} catch {}
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
const results = top.map(r => ({ ...r, line: fqnToLine.get(r.fqn) ?? null }));
|
|
62
|
+
return { content: [{ type: 'text', text: JSON.stringify(results, null, 2) }] };
|
|
48
63
|
}
|