@optave/codegraph 1.3.0 → 2.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.
- package/README.md +109 -40
- package/package.json +5 -5
- package/src/builder.js +52 -8
- package/src/cli.js +127 -1
- package/src/config.js +45 -3
- package/src/constants.js +0 -2
- package/src/cycles.js +2 -2
- package/src/db.js +13 -0
- package/src/export.js +44 -9
- package/src/index.js +21 -0
- package/src/mcp.js +308 -8
- package/src/parser.js +13 -14
- package/src/queries.js +30 -0
- package/src/registry.js +145 -0
- package/src/resolve.js +1 -1
- package/src/structure.js +491 -0
- package/src/watcher.js +2 -2
package/src/cycles.js
CHANGED
|
@@ -31,8 +31,8 @@ export function findCycles(db, opts = {}) {
|
|
|
31
31
|
FROM edges e
|
|
32
32
|
JOIN nodes n1 ON e.source_id = n1.id
|
|
33
33
|
JOIN nodes n2 ON e.target_id = n2.id
|
|
34
|
-
WHERE n1.kind IN ('function', 'method', 'class')
|
|
35
|
-
AND n2.kind IN ('function', 'method', 'class')
|
|
34
|
+
WHERE n1.kind IN ('function', 'method', 'class', 'interface', 'type', 'struct', 'enum', 'trait', 'record', 'module')
|
|
35
|
+
AND n2.kind IN ('function', 'method', 'class', 'interface', 'type', 'struct', 'enum', 'trait', 'record', 'module')
|
|
36
36
|
AND e.kind = 'calls'
|
|
37
37
|
AND n1.id != n2.id
|
|
38
38
|
`)
|
package/src/db.js
CHANGED
|
@@ -33,6 +33,19 @@ export const MIGRATIONS = [
|
|
|
33
33
|
CREATE INDEX IF NOT EXISTS idx_edges_source ON edges(source_id);
|
|
34
34
|
CREATE INDEX IF NOT EXISTS idx_edges_target ON edges(target_id);
|
|
35
35
|
CREATE INDEX IF NOT EXISTS idx_edges_kind ON edges(kind);
|
|
36
|
+
CREATE TABLE IF NOT EXISTS node_metrics (
|
|
37
|
+
node_id INTEGER PRIMARY KEY,
|
|
38
|
+
line_count INTEGER,
|
|
39
|
+
symbol_count INTEGER,
|
|
40
|
+
import_count INTEGER,
|
|
41
|
+
export_count INTEGER,
|
|
42
|
+
fan_in INTEGER,
|
|
43
|
+
fan_out INTEGER,
|
|
44
|
+
cohesion REAL,
|
|
45
|
+
file_count INTEGER,
|
|
46
|
+
FOREIGN KEY(node_id) REFERENCES nodes(id)
|
|
47
|
+
);
|
|
48
|
+
CREATE INDEX IF NOT EXISTS idx_node_metrics_node ON node_metrics(node_id);
|
|
36
49
|
`,
|
|
37
50
|
},
|
|
38
51
|
{
|
package/src/export.js
CHANGED
|
@@ -24,25 +24,60 @@ export function exportDOT(db, opts = {}) {
|
|
|
24
24
|
`)
|
|
25
25
|
.all();
|
|
26
26
|
|
|
27
|
+
// Try to use directory nodes from DB (built by structure analysis)
|
|
28
|
+
const hasDirectoryNodes =
|
|
29
|
+
db.prepare("SELECT COUNT(*) as c FROM nodes WHERE kind = 'directory'").get().c > 0;
|
|
30
|
+
|
|
27
31
|
const dirs = new Map();
|
|
28
32
|
const allFiles = new Set();
|
|
29
33
|
for (const { source, target } of edges) {
|
|
30
34
|
allFiles.add(source);
|
|
31
35
|
allFiles.add(target);
|
|
32
36
|
}
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
+
|
|
38
|
+
if (hasDirectoryNodes) {
|
|
39
|
+
// Use DB directory structure with cohesion labels
|
|
40
|
+
const dbDirs = db
|
|
41
|
+
.prepare(`
|
|
42
|
+
SELECT n.id, n.name, nm.cohesion
|
|
43
|
+
FROM nodes n
|
|
44
|
+
LEFT JOIN node_metrics nm ON n.id = nm.node_id
|
|
45
|
+
WHERE n.kind = 'directory'
|
|
46
|
+
`)
|
|
47
|
+
.all();
|
|
48
|
+
|
|
49
|
+
for (const d of dbDirs) {
|
|
50
|
+
const containedFiles = db
|
|
51
|
+
.prepare(`
|
|
52
|
+
SELECT n.name FROM edges e
|
|
53
|
+
JOIN nodes n ON e.target_id = n.id
|
|
54
|
+
WHERE e.source_id = ? AND e.kind = 'contains' AND n.kind = 'file'
|
|
55
|
+
`)
|
|
56
|
+
.all(d.id)
|
|
57
|
+
.map((r) => r.name)
|
|
58
|
+
.filter((f) => allFiles.has(f));
|
|
59
|
+
|
|
60
|
+
if (containedFiles.length > 0) {
|
|
61
|
+
dirs.set(d.name, { files: containedFiles, cohesion: d.cohesion });
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
} else {
|
|
65
|
+
// Fallback: reconstruct from path.dirname()
|
|
66
|
+
for (const file of allFiles) {
|
|
67
|
+
const dir = path.dirname(file) || '.';
|
|
68
|
+
if (!dirs.has(dir)) dirs.set(dir, { files: [], cohesion: null });
|
|
69
|
+
dirs.get(dir).files.push(file);
|
|
70
|
+
}
|
|
37
71
|
}
|
|
38
72
|
|
|
39
73
|
let clusterIdx = 0;
|
|
40
|
-
for (const [dir,
|
|
74
|
+
for (const [dir, info] of [...dirs].sort((a, b) => a[0].localeCompare(b[0]))) {
|
|
41
75
|
lines.push(` subgraph cluster_${clusterIdx++} {`);
|
|
42
|
-
|
|
76
|
+
const cohLabel = info.cohesion !== null ? ` (cohesion: ${info.cohesion.toFixed(2)})` : '';
|
|
77
|
+
lines.push(` label="${dir}${cohLabel}";`);
|
|
43
78
|
lines.push(` style=dashed;`);
|
|
44
79
|
lines.push(` color="#999999";`);
|
|
45
|
-
for (const f of files) {
|
|
80
|
+
for (const f of info.files) {
|
|
46
81
|
const label = path.basename(f);
|
|
47
82
|
lines.push(` "${f}" [label="${label}"];`);
|
|
48
83
|
}
|
|
@@ -62,7 +97,7 @@ export function exportDOT(db, opts = {}) {
|
|
|
62
97
|
FROM edges e
|
|
63
98
|
JOIN nodes n1 ON e.source_id = n1.id
|
|
64
99
|
JOIN nodes n2 ON e.target_id = n2.id
|
|
65
|
-
WHERE n1.kind IN ('function', 'method', 'class') AND n2.kind IN ('function', 'method', 'class')
|
|
100
|
+
WHERE n1.kind IN ('function', 'method', 'class', 'interface', 'type', 'struct', 'enum', 'trait', 'record', 'module') AND n2.kind IN ('function', 'method', 'class', 'interface', 'type', 'struct', 'enum', 'trait', 'record', 'module')
|
|
66
101
|
AND e.kind = 'calls'
|
|
67
102
|
`)
|
|
68
103
|
.all();
|
|
@@ -111,7 +146,7 @@ export function exportMermaid(db, opts = {}) {
|
|
|
111
146
|
FROM edges e
|
|
112
147
|
JOIN nodes n1 ON e.source_id = n1.id
|
|
113
148
|
JOIN nodes n2 ON e.target_id = n2.id
|
|
114
|
-
WHERE n1.kind IN ('function', 'method', 'class') AND n2.kind IN ('function', 'method', 'class')
|
|
149
|
+
WHERE n1.kind IN ('function', 'method', 'class', 'interface', 'type', 'struct', 'enum', 'trait', 'record', 'module') AND n2.kind IN ('function', 'method', 'class', 'interface', 'type', 'struct', 'enum', 'trait', 'record', 'module')
|
|
115
150
|
AND e.kind = 'calls'
|
|
116
151
|
`)
|
|
117
152
|
.all();
|
package/src/index.js
CHANGED
|
@@ -46,5 +46,26 @@ export {
|
|
|
46
46
|
moduleMapData,
|
|
47
47
|
queryNameData,
|
|
48
48
|
} from './queries.js';
|
|
49
|
+
// Registry (multi-repo)
|
|
50
|
+
export {
|
|
51
|
+
listRepos,
|
|
52
|
+
loadRegistry,
|
|
53
|
+
pruneRegistry,
|
|
54
|
+
REGISTRY_PATH,
|
|
55
|
+
registerRepo,
|
|
56
|
+
resolveRepoDbPath,
|
|
57
|
+
saveRegistry,
|
|
58
|
+
unregisterRepo,
|
|
59
|
+
} from './registry.js';
|
|
60
|
+
// Structure analysis
|
|
61
|
+
export {
|
|
62
|
+
buildStructure,
|
|
63
|
+
formatHotspots,
|
|
64
|
+
formatModuleBoundaries,
|
|
65
|
+
formatStructure,
|
|
66
|
+
hotspotsData,
|
|
67
|
+
moduleBoundariesData,
|
|
68
|
+
structureData,
|
|
69
|
+
} from './structure.js';
|
|
49
70
|
// Watch mode
|
|
50
71
|
export { watchProject } from './watcher.js';
|
package/src/mcp.js
CHANGED
|
@@ -9,7 +9,14 @@ import { createRequire } from 'node:module';
|
|
|
9
9
|
import { findCycles } from './cycles.js';
|
|
10
10
|
import { findDbPath } from './db.js';
|
|
11
11
|
|
|
12
|
-
const
|
|
12
|
+
const REPO_PROP = {
|
|
13
|
+
repo: {
|
|
14
|
+
type: 'string',
|
|
15
|
+
description: 'Repository name from the registry (omit for local project)',
|
|
16
|
+
},
|
|
17
|
+
};
|
|
18
|
+
|
|
19
|
+
const BASE_TOOLS = [
|
|
13
20
|
{
|
|
14
21
|
name: 'query_function',
|
|
15
22
|
description: 'Find callers and callees of a function by name',
|
|
@@ -66,15 +73,178 @@ const TOOLS = [
|
|
|
66
73
|
},
|
|
67
74
|
},
|
|
68
75
|
},
|
|
76
|
+
{
|
|
77
|
+
name: 'fn_deps',
|
|
78
|
+
description: 'Show function-level dependency chain: what a function calls and what calls it',
|
|
79
|
+
inputSchema: {
|
|
80
|
+
type: 'object',
|
|
81
|
+
properties: {
|
|
82
|
+
name: { type: 'string', description: 'Function/method/class name (partial match)' },
|
|
83
|
+
depth: { type: 'number', description: 'Transitive caller depth', default: 3 },
|
|
84
|
+
no_tests: { type: 'boolean', description: 'Exclude test files', default: false },
|
|
85
|
+
},
|
|
86
|
+
required: ['name'],
|
|
87
|
+
},
|
|
88
|
+
},
|
|
89
|
+
{
|
|
90
|
+
name: 'fn_impact',
|
|
91
|
+
description:
|
|
92
|
+
'Show function-level blast radius: all functions transitively affected by changes to a function',
|
|
93
|
+
inputSchema: {
|
|
94
|
+
type: 'object',
|
|
95
|
+
properties: {
|
|
96
|
+
name: { type: 'string', description: 'Function/method/class name (partial match)' },
|
|
97
|
+
depth: { type: 'number', description: 'Max traversal depth', default: 5 },
|
|
98
|
+
no_tests: { type: 'boolean', description: 'Exclude test files', default: false },
|
|
99
|
+
},
|
|
100
|
+
required: ['name'],
|
|
101
|
+
},
|
|
102
|
+
},
|
|
103
|
+
{
|
|
104
|
+
name: 'diff_impact',
|
|
105
|
+
description: 'Analyze git diff to find which functions changed and their transitive callers',
|
|
106
|
+
inputSchema: {
|
|
107
|
+
type: 'object',
|
|
108
|
+
properties: {
|
|
109
|
+
staged: { type: 'boolean', description: 'Analyze staged changes only', default: false },
|
|
110
|
+
ref: { type: 'string', description: 'Git ref to diff against (default: HEAD)' },
|
|
111
|
+
depth: { type: 'number', description: 'Transitive caller depth', default: 3 },
|
|
112
|
+
no_tests: { type: 'boolean', description: 'Exclude test files', default: false },
|
|
113
|
+
},
|
|
114
|
+
},
|
|
115
|
+
},
|
|
116
|
+
{
|
|
117
|
+
name: 'semantic_search',
|
|
118
|
+
description:
|
|
119
|
+
'Search code symbols by meaning using embeddings (requires prior `codegraph embed`)',
|
|
120
|
+
inputSchema: {
|
|
121
|
+
type: 'object',
|
|
122
|
+
properties: {
|
|
123
|
+
query: { type: 'string', description: 'Natural language search query' },
|
|
124
|
+
limit: { type: 'number', description: 'Max results to return', default: 15 },
|
|
125
|
+
min_score: { type: 'number', description: 'Minimum similarity score (0-1)', default: 0.2 },
|
|
126
|
+
},
|
|
127
|
+
required: ['query'],
|
|
128
|
+
},
|
|
129
|
+
},
|
|
130
|
+
{
|
|
131
|
+
name: 'export_graph',
|
|
132
|
+
description: 'Export the dependency graph in DOT (Graphviz), Mermaid, or JSON format',
|
|
133
|
+
inputSchema: {
|
|
134
|
+
type: 'object',
|
|
135
|
+
properties: {
|
|
136
|
+
format: {
|
|
137
|
+
type: 'string',
|
|
138
|
+
enum: ['dot', 'mermaid', 'json'],
|
|
139
|
+
description: 'Export format',
|
|
140
|
+
},
|
|
141
|
+
file_level: {
|
|
142
|
+
type: 'boolean',
|
|
143
|
+
description: 'File-level graph (true) or function-level (false)',
|
|
144
|
+
default: true,
|
|
145
|
+
},
|
|
146
|
+
},
|
|
147
|
+
required: ['format'],
|
|
148
|
+
},
|
|
149
|
+
},
|
|
150
|
+
{
|
|
151
|
+
name: 'list_functions',
|
|
152
|
+
description:
|
|
153
|
+
'List functions, methods, classes, structs, enums, traits, records, and modules in the codebase, optionally filtered by file or name pattern',
|
|
154
|
+
inputSchema: {
|
|
155
|
+
type: 'object',
|
|
156
|
+
properties: {
|
|
157
|
+
file: { type: 'string', description: 'Filter by file path (partial match)' },
|
|
158
|
+
pattern: { type: 'string', description: 'Filter by function name (partial match)' },
|
|
159
|
+
no_tests: { type: 'boolean', description: 'Exclude test files', default: false },
|
|
160
|
+
},
|
|
161
|
+
},
|
|
162
|
+
},
|
|
163
|
+
{
|
|
164
|
+
name: 'structure',
|
|
165
|
+
description:
|
|
166
|
+
'Show project structure with directory hierarchy, cohesion scores, and per-file metrics',
|
|
167
|
+
inputSchema: {
|
|
168
|
+
type: 'object',
|
|
169
|
+
properties: {
|
|
170
|
+
directory: { type: 'string', description: 'Filter to a specific directory path' },
|
|
171
|
+
depth: { type: 'number', description: 'Max directory depth to show' },
|
|
172
|
+
sort: {
|
|
173
|
+
type: 'string',
|
|
174
|
+
enum: ['cohesion', 'fan-in', 'fan-out', 'density', 'files'],
|
|
175
|
+
description: 'Sort directories by metric',
|
|
176
|
+
},
|
|
177
|
+
},
|
|
178
|
+
},
|
|
179
|
+
},
|
|
180
|
+
{
|
|
181
|
+
name: 'hotspots',
|
|
182
|
+
description:
|
|
183
|
+
'Find structural hotspots: files or directories with extreme fan-in, fan-out, or symbol density',
|
|
184
|
+
inputSchema: {
|
|
185
|
+
type: 'object',
|
|
186
|
+
properties: {
|
|
187
|
+
metric: {
|
|
188
|
+
type: 'string',
|
|
189
|
+
enum: ['fan-in', 'fan-out', 'density', 'coupling'],
|
|
190
|
+
description: 'Metric to rank by',
|
|
191
|
+
},
|
|
192
|
+
level: {
|
|
193
|
+
type: 'string',
|
|
194
|
+
enum: ['file', 'directory'],
|
|
195
|
+
description: 'Rank files or directories',
|
|
196
|
+
},
|
|
197
|
+
limit: { type: 'number', description: 'Number of results to return', default: 10 },
|
|
198
|
+
},
|
|
199
|
+
},
|
|
200
|
+
},
|
|
69
201
|
];
|
|
70
202
|
|
|
71
|
-
|
|
203
|
+
const LIST_REPOS_TOOL = {
|
|
204
|
+
name: 'list_repos',
|
|
205
|
+
description: 'List all repositories registered in the codegraph registry',
|
|
206
|
+
inputSchema: {
|
|
207
|
+
type: 'object',
|
|
208
|
+
properties: {},
|
|
209
|
+
},
|
|
210
|
+
};
|
|
211
|
+
|
|
212
|
+
/**
|
|
213
|
+
* Build the tool list based on multi-repo mode.
|
|
214
|
+
* @param {boolean} multiRepo - If true, inject `repo` prop into each tool and append `list_repos`
|
|
215
|
+
* @returns {object[]}
|
|
216
|
+
*/
|
|
217
|
+
function buildToolList(multiRepo) {
|
|
218
|
+
if (!multiRepo) return BASE_TOOLS;
|
|
219
|
+
return [
|
|
220
|
+
...BASE_TOOLS.map((tool) => ({
|
|
221
|
+
...tool,
|
|
222
|
+
inputSchema: {
|
|
223
|
+
...tool.inputSchema,
|
|
224
|
+
properties: { ...tool.inputSchema.properties, ...REPO_PROP },
|
|
225
|
+
},
|
|
226
|
+
})),
|
|
227
|
+
LIST_REPOS_TOOL,
|
|
228
|
+
];
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
// Backward-compatible export: full multi-repo tool list
|
|
232
|
+
const TOOLS = buildToolList(true);
|
|
233
|
+
|
|
234
|
+
export { TOOLS, buildToolList };
|
|
72
235
|
|
|
73
236
|
/**
|
|
74
237
|
* Start the MCP server.
|
|
75
238
|
* This function requires @modelcontextprotocol/sdk to be installed.
|
|
239
|
+
*
|
|
240
|
+
* @param {string} [customDbPath] - Path to a specific graph.db
|
|
241
|
+
* @param {object} [options]
|
|
242
|
+
* @param {boolean} [options.multiRepo] - Enable multi-repo access (default: false)
|
|
243
|
+
* @param {string[]} [options.allowedRepos] - Restrict access to these repo names only
|
|
76
244
|
*/
|
|
77
|
-
export async function startMCPServer(customDbPath) {
|
|
245
|
+
export async function startMCPServer(customDbPath, options = {}) {
|
|
246
|
+
const { allowedRepos } = options;
|
|
247
|
+
const multiRepo = options.multiRepo || !!allowedRepos;
|
|
78
248
|
let Server, StdioServerTransport;
|
|
79
249
|
try {
|
|
80
250
|
const sdk = await import('@modelcontextprotocol/sdk/server/index.js');
|
|
@@ -90,9 +260,16 @@ export async function startMCPServer(customDbPath) {
|
|
|
90
260
|
}
|
|
91
261
|
|
|
92
262
|
// Lazy import query functions to avoid circular deps at module load
|
|
93
|
-
const {
|
|
94
|
-
|
|
95
|
-
|
|
263
|
+
const {
|
|
264
|
+
queryNameData,
|
|
265
|
+
impactAnalysisData,
|
|
266
|
+
moduleMapData,
|
|
267
|
+
fileDepsData,
|
|
268
|
+
fnDepsData,
|
|
269
|
+
fnImpactData,
|
|
270
|
+
diffImpactData,
|
|
271
|
+
listFunctionsData,
|
|
272
|
+
} = await import('./queries.js');
|
|
96
273
|
|
|
97
274
|
const require = createRequire(import.meta.url);
|
|
98
275
|
const Database = require('better-sqlite3');
|
|
@@ -102,13 +279,37 @@ export async function startMCPServer(customDbPath) {
|
|
|
102
279
|
{ capabilities: { tools: {} } },
|
|
103
280
|
);
|
|
104
281
|
|
|
105
|
-
server.setRequestHandler('tools/list', async () => ({ tools:
|
|
282
|
+
server.setRequestHandler('tools/list', async () => ({ tools: buildToolList(multiRepo) }));
|
|
106
283
|
|
|
107
284
|
server.setRequestHandler('tools/call', async (request) => {
|
|
108
285
|
const { name, arguments: args } = request.params;
|
|
109
|
-
const dbPath = customDbPath || undefined;
|
|
110
286
|
|
|
111
287
|
try {
|
|
288
|
+
if (!multiRepo && args.repo) {
|
|
289
|
+
throw new Error(
|
|
290
|
+
'Multi-repo access is disabled. Restart with `codegraph mcp --multi-repo` to access other repositories.',
|
|
291
|
+
);
|
|
292
|
+
}
|
|
293
|
+
if (!multiRepo && name === 'list_repos') {
|
|
294
|
+
throw new Error(
|
|
295
|
+
'Multi-repo access is disabled. Restart with `codegraph mcp --multi-repo` to list repositories.',
|
|
296
|
+
);
|
|
297
|
+
}
|
|
298
|
+
|
|
299
|
+
let dbPath = customDbPath || undefined;
|
|
300
|
+
if (args.repo) {
|
|
301
|
+
if (allowedRepos && !allowedRepos.includes(args.repo)) {
|
|
302
|
+
throw new Error(`Repository "${args.repo}" is not in the allowed repos list.`);
|
|
303
|
+
}
|
|
304
|
+
const { resolveRepoDbPath } = await import('./registry.js');
|
|
305
|
+
const resolved = resolveRepoDbPath(args.repo);
|
|
306
|
+
if (!resolved)
|
|
307
|
+
throw new Error(
|
|
308
|
+
`Repository "${args.repo}" not found in registry or its database is missing.`,
|
|
309
|
+
);
|
|
310
|
+
dbPath = resolved;
|
|
311
|
+
}
|
|
312
|
+
|
|
112
313
|
let result;
|
|
113
314
|
switch (name) {
|
|
114
315
|
case 'query_function':
|
|
@@ -130,6 +331,105 @@ export async function startMCPServer(customDbPath) {
|
|
|
130
331
|
case 'module_map':
|
|
131
332
|
result = moduleMapData(dbPath, args.limit || 20);
|
|
132
333
|
break;
|
|
334
|
+
case 'fn_deps':
|
|
335
|
+
result = fnDepsData(args.name, dbPath, {
|
|
336
|
+
depth: args.depth,
|
|
337
|
+
noTests: args.no_tests,
|
|
338
|
+
});
|
|
339
|
+
break;
|
|
340
|
+
case 'fn_impact':
|
|
341
|
+
result = fnImpactData(args.name, dbPath, {
|
|
342
|
+
depth: args.depth,
|
|
343
|
+
noTests: args.no_tests,
|
|
344
|
+
});
|
|
345
|
+
break;
|
|
346
|
+
case 'diff_impact':
|
|
347
|
+
result = diffImpactData(dbPath, {
|
|
348
|
+
staged: args.staged,
|
|
349
|
+
ref: args.ref,
|
|
350
|
+
depth: args.depth,
|
|
351
|
+
noTests: args.no_tests,
|
|
352
|
+
});
|
|
353
|
+
break;
|
|
354
|
+
case 'semantic_search': {
|
|
355
|
+
const { searchData } = await import('./embedder.js');
|
|
356
|
+
result = await searchData(args.query, dbPath, {
|
|
357
|
+
limit: args.limit,
|
|
358
|
+
minScore: args.min_score,
|
|
359
|
+
});
|
|
360
|
+
if (result === null) {
|
|
361
|
+
return {
|
|
362
|
+
content: [
|
|
363
|
+
{ type: 'text', text: 'Semantic search unavailable. Run `codegraph embed` first.' },
|
|
364
|
+
],
|
|
365
|
+
isError: true,
|
|
366
|
+
};
|
|
367
|
+
}
|
|
368
|
+
break;
|
|
369
|
+
}
|
|
370
|
+
case 'export_graph': {
|
|
371
|
+
const { exportDOT, exportMermaid, exportJSON } = await import('./export.js');
|
|
372
|
+
const db = new Database(findDbPath(dbPath), { readonly: true });
|
|
373
|
+
const fileLevel = args.file_level !== false;
|
|
374
|
+
switch (args.format) {
|
|
375
|
+
case 'dot':
|
|
376
|
+
result = exportDOT(db, { fileLevel });
|
|
377
|
+
break;
|
|
378
|
+
case 'mermaid':
|
|
379
|
+
result = exportMermaid(db, { fileLevel });
|
|
380
|
+
break;
|
|
381
|
+
case 'json':
|
|
382
|
+
result = exportJSON(db);
|
|
383
|
+
break;
|
|
384
|
+
default:
|
|
385
|
+
db.close();
|
|
386
|
+
return {
|
|
387
|
+
content: [
|
|
388
|
+
{
|
|
389
|
+
type: 'text',
|
|
390
|
+
text: `Unknown format: ${args.format}. Use dot, mermaid, or json.`,
|
|
391
|
+
},
|
|
392
|
+
],
|
|
393
|
+
isError: true,
|
|
394
|
+
};
|
|
395
|
+
}
|
|
396
|
+
db.close();
|
|
397
|
+
break;
|
|
398
|
+
}
|
|
399
|
+
case 'list_functions':
|
|
400
|
+
result = listFunctionsData(dbPath, {
|
|
401
|
+
file: args.file,
|
|
402
|
+
pattern: args.pattern,
|
|
403
|
+
noTests: args.no_tests,
|
|
404
|
+
});
|
|
405
|
+
break;
|
|
406
|
+
case 'structure': {
|
|
407
|
+
const { structureData } = await import('./structure.js');
|
|
408
|
+
result = structureData(dbPath, {
|
|
409
|
+
directory: args.directory,
|
|
410
|
+
depth: args.depth,
|
|
411
|
+
sort: args.sort,
|
|
412
|
+
});
|
|
413
|
+
break;
|
|
414
|
+
}
|
|
415
|
+
case 'hotspots': {
|
|
416
|
+
const { hotspotsData } = await import('./structure.js');
|
|
417
|
+
result = hotspotsData(dbPath, {
|
|
418
|
+
metric: args.metric,
|
|
419
|
+
level: args.level,
|
|
420
|
+
limit: args.limit,
|
|
421
|
+
});
|
|
422
|
+
break;
|
|
423
|
+
}
|
|
424
|
+
case 'list_repos': {
|
|
425
|
+
const { listRepos } = await import('./registry.js');
|
|
426
|
+
let repos = listRepos();
|
|
427
|
+
if (allowedRepos) {
|
|
428
|
+
repos = repos.filter((r) => allowedRepos.includes(r.name));
|
|
429
|
+
}
|
|
430
|
+
result = { repos };
|
|
431
|
+
break;
|
|
432
|
+
}
|
|
133
433
|
default:
|
|
134
434
|
return { content: [{ type: 'text', text: `Unknown tool: ${name}` }], isError: true };
|
|
135
435
|
}
|
package/src/parser.js
CHANGED
|
@@ -2,7 +2,6 @@ import fs from 'node:fs';
|
|
|
2
2
|
import path from 'node:path';
|
|
3
3
|
import { fileURLToPath } from 'node:url';
|
|
4
4
|
import { Language, Parser } from 'web-tree-sitter';
|
|
5
|
-
import { normalizePath } from './constants.js';
|
|
6
5
|
import { warn } from './logger.js';
|
|
7
6
|
import { loadNative } from './native.js';
|
|
8
7
|
|
|
@@ -731,7 +730,7 @@ export function extractGoSymbols(tree, _filePath) {
|
|
|
731
730
|
if (typeNode.type === 'struct_type') {
|
|
732
731
|
definitions.push({
|
|
733
732
|
name: nameNode.text,
|
|
734
|
-
kind: '
|
|
733
|
+
kind: 'struct',
|
|
735
734
|
line: node.startPosition.row + 1,
|
|
736
735
|
endLine: nodeEndLine(node),
|
|
737
736
|
});
|
|
@@ -876,7 +875,7 @@ export function extractRustSymbols(tree, _filePath) {
|
|
|
876
875
|
if (nameNode) {
|
|
877
876
|
definitions.push({
|
|
878
877
|
name: nameNode.text,
|
|
879
|
-
kind: '
|
|
878
|
+
kind: 'struct',
|
|
880
879
|
line: node.startPosition.row + 1,
|
|
881
880
|
endLine: nodeEndLine(node),
|
|
882
881
|
});
|
|
@@ -889,7 +888,7 @@ export function extractRustSymbols(tree, _filePath) {
|
|
|
889
888
|
if (nameNode) {
|
|
890
889
|
definitions.push({
|
|
891
890
|
name: nameNode.text,
|
|
892
|
-
kind: '
|
|
891
|
+
kind: 'enum',
|
|
893
892
|
line: node.startPosition.row + 1,
|
|
894
893
|
endLine: nodeEndLine(node),
|
|
895
894
|
});
|
|
@@ -902,7 +901,7 @@ export function extractRustSymbols(tree, _filePath) {
|
|
|
902
901
|
if (nameNode) {
|
|
903
902
|
definitions.push({
|
|
904
903
|
name: nameNode.text,
|
|
905
|
-
kind: '
|
|
904
|
+
kind: 'trait',
|
|
906
905
|
line: node.startPosition.row + 1,
|
|
907
906
|
endLine: nodeEndLine(node),
|
|
908
907
|
});
|
|
@@ -1186,7 +1185,7 @@ export function extractJavaSymbols(tree, _filePath) {
|
|
|
1186
1185
|
if (nameNode) {
|
|
1187
1186
|
definitions.push({
|
|
1188
1187
|
name: nameNode.text,
|
|
1189
|
-
kind: '
|
|
1188
|
+
kind: 'enum',
|
|
1190
1189
|
line: node.startPosition.row + 1,
|
|
1191
1190
|
endLine: nodeEndLine(node),
|
|
1192
1191
|
});
|
|
@@ -1320,7 +1319,7 @@ export function extractCSharpSymbols(tree, _filePath) {
|
|
|
1320
1319
|
if (nameNode) {
|
|
1321
1320
|
definitions.push({
|
|
1322
1321
|
name: nameNode.text,
|
|
1323
|
-
kind: '
|
|
1322
|
+
kind: 'struct',
|
|
1324
1323
|
line: node.startPosition.row + 1,
|
|
1325
1324
|
endLine: nodeEndLine(node),
|
|
1326
1325
|
});
|
|
@@ -1334,7 +1333,7 @@ export function extractCSharpSymbols(tree, _filePath) {
|
|
|
1334
1333
|
if (nameNode) {
|
|
1335
1334
|
definitions.push({
|
|
1336
1335
|
name: nameNode.text,
|
|
1337
|
-
kind: '
|
|
1336
|
+
kind: 'record',
|
|
1338
1337
|
line: node.startPosition.row + 1,
|
|
1339
1338
|
endLine: nodeEndLine(node),
|
|
1340
1339
|
});
|
|
@@ -1378,7 +1377,7 @@ export function extractCSharpSymbols(tree, _filePath) {
|
|
|
1378
1377
|
if (nameNode) {
|
|
1379
1378
|
definitions.push({
|
|
1380
1379
|
name: nameNode.text,
|
|
1381
|
-
kind: '
|
|
1380
|
+
kind: 'enum',
|
|
1382
1381
|
line: node.startPosition.row + 1,
|
|
1383
1382
|
endLine: nodeEndLine(node),
|
|
1384
1383
|
});
|
|
@@ -1588,7 +1587,7 @@ export function extractRubySymbols(tree, _filePath) {
|
|
|
1588
1587
|
if (nameNode) {
|
|
1589
1588
|
definitions.push({
|
|
1590
1589
|
name: nameNode.text,
|
|
1591
|
-
kind: '
|
|
1590
|
+
kind: 'module',
|
|
1592
1591
|
line: node.startPosition.row + 1,
|
|
1593
1592
|
endLine: nodeEndLine(node),
|
|
1594
1593
|
});
|
|
@@ -1818,7 +1817,7 @@ export function extractPHPSymbols(tree, _filePath) {
|
|
|
1818
1817
|
if (nameNode) {
|
|
1819
1818
|
definitions.push({
|
|
1820
1819
|
name: nameNode.text,
|
|
1821
|
-
kind: '
|
|
1820
|
+
kind: 'trait',
|
|
1822
1821
|
line: node.startPosition.row + 1,
|
|
1823
1822
|
endLine: nodeEndLine(node),
|
|
1824
1823
|
});
|
|
@@ -1831,7 +1830,7 @@ export function extractPHPSymbols(tree, _filePath) {
|
|
|
1831
1830
|
if (nameNode) {
|
|
1832
1831
|
definitions.push({
|
|
1833
1832
|
name: nameNode.text,
|
|
1834
|
-
kind: '
|
|
1833
|
+
kind: 'enum',
|
|
1835
1834
|
line: node.startPosition.row + 1,
|
|
1836
1835
|
endLine: nodeEndLine(node),
|
|
1837
1836
|
});
|
|
@@ -2145,7 +2144,7 @@ export async function parseFilesAuto(filePaths, rootDir, opts = {}) {
|
|
|
2145
2144
|
const nativeResults = native.parseFiles(filePaths, rootDir);
|
|
2146
2145
|
for (const r of nativeResults) {
|
|
2147
2146
|
if (!r) continue;
|
|
2148
|
-
const relPath =
|
|
2147
|
+
const relPath = path.relative(rootDir, r.file).split(path.sep).join('/');
|
|
2149
2148
|
result.set(relPath, normalizeNativeSymbols(r));
|
|
2150
2149
|
}
|
|
2151
2150
|
return result;
|
|
@@ -2163,7 +2162,7 @@ export async function parseFilesAuto(filePaths, rootDir, opts = {}) {
|
|
|
2163
2162
|
}
|
|
2164
2163
|
const symbols = wasmExtractSymbols(parsers, filePath, code);
|
|
2165
2164
|
if (symbols) {
|
|
2166
|
-
const relPath =
|
|
2165
|
+
const relPath = path.relative(rootDir, filePath).split(path.sep).join('/');
|
|
2167
2166
|
result.set(relPath, symbols);
|
|
2168
2167
|
}
|
|
2169
2168
|
}
|
package/src/queries.js
CHANGED
|
@@ -566,6 +566,36 @@ export function diffImpactData(customDbPath, opts = {}) {
|
|
|
566
566
|
};
|
|
567
567
|
}
|
|
568
568
|
|
|
569
|
+
export function listFunctionsData(customDbPath, opts = {}) {
|
|
570
|
+
const db = openReadonlyOrFail(customDbPath);
|
|
571
|
+
const noTests = opts.noTests || false;
|
|
572
|
+
const kinds = ['function', 'method', 'class'];
|
|
573
|
+
const placeholders = kinds.map(() => '?').join(', ');
|
|
574
|
+
|
|
575
|
+
const conditions = [`kind IN (${placeholders})`];
|
|
576
|
+
const params = [...kinds];
|
|
577
|
+
|
|
578
|
+
if (opts.file) {
|
|
579
|
+
conditions.push('file LIKE ?');
|
|
580
|
+
params.push(`%${opts.file}%`);
|
|
581
|
+
}
|
|
582
|
+
if (opts.pattern) {
|
|
583
|
+
conditions.push('name LIKE ?');
|
|
584
|
+
params.push(`%${opts.pattern}%`);
|
|
585
|
+
}
|
|
586
|
+
|
|
587
|
+
let rows = db
|
|
588
|
+
.prepare(
|
|
589
|
+
`SELECT name, kind, file, line FROM nodes WHERE ${conditions.join(' AND ')} ORDER BY file, line`,
|
|
590
|
+
)
|
|
591
|
+
.all(...params);
|
|
592
|
+
|
|
593
|
+
if (noTests) rows = rows.filter((r) => !isTestFile(r.file));
|
|
594
|
+
|
|
595
|
+
db.close();
|
|
596
|
+
return { count: rows.length, functions: rows };
|
|
597
|
+
}
|
|
598
|
+
|
|
569
599
|
// ─── Human-readable output (original formatting) ───────────────────────
|
|
570
600
|
|
|
571
601
|
export function queryName(name, customDbPath, opts = {}) {
|