@optave/codegraph 2.0.0 → 2.1.1-dev.00f091c
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 +74 -37
- package/package.json +10 -10
- package/src/builder.js +252 -38
- package/src/cli.js +44 -8
- package/src/config.js +1 -1
- package/src/db.js +4 -0
- package/src/embedder.js +3 -3
- package/src/extractors/csharp.js +248 -0
- package/src/extractors/go.js +172 -0
- package/src/extractors/hcl.js +73 -0
- package/src/extractors/helpers.js +10 -0
- package/src/extractors/index.js +9 -0
- package/src/extractors/java.js +230 -0
- package/src/extractors/javascript.js +414 -0
- package/src/extractors/php.js +243 -0
- package/src/extractors/python.js +150 -0
- package/src/extractors/ruby.js +188 -0
- package/src/extractors/rust.js +225 -0
- package/src/index.js +2 -0
- package/src/journal.js +109 -0
- package/src/mcp.js +47 -4
- package/src/parser.js +28 -1890
- package/src/queries.js +586 -4
- package/src/registry.js +24 -7
- package/src/resolve.js +4 -3
- package/src/watcher.js +25 -0
|
@@ -0,0 +1,225 @@
|
|
|
1
|
+
import { nodeEndLine } from './helpers.js';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Extract symbols from Rust files.
|
|
5
|
+
*/
|
|
6
|
+
export function extractRustSymbols(tree, _filePath) {
|
|
7
|
+
const definitions = [];
|
|
8
|
+
const calls = [];
|
|
9
|
+
const imports = [];
|
|
10
|
+
const classes = [];
|
|
11
|
+
const exports = [];
|
|
12
|
+
|
|
13
|
+
function findCurrentImpl(node) {
|
|
14
|
+
let current = node.parent;
|
|
15
|
+
while (current) {
|
|
16
|
+
if (current.type === 'impl_item') {
|
|
17
|
+
const typeNode = current.childForFieldName('type');
|
|
18
|
+
return typeNode ? typeNode.text : null;
|
|
19
|
+
}
|
|
20
|
+
current = current.parent;
|
|
21
|
+
}
|
|
22
|
+
return null;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
function walkRustNode(node) {
|
|
26
|
+
switch (node.type) {
|
|
27
|
+
case 'function_item': {
|
|
28
|
+
const nameNode = node.childForFieldName('name');
|
|
29
|
+
if (nameNode) {
|
|
30
|
+
const implType = findCurrentImpl(node);
|
|
31
|
+
const fullName = implType ? `${implType}.${nameNode.text}` : nameNode.text;
|
|
32
|
+
const kind = implType ? 'method' : 'function';
|
|
33
|
+
definitions.push({
|
|
34
|
+
name: fullName,
|
|
35
|
+
kind,
|
|
36
|
+
line: node.startPosition.row + 1,
|
|
37
|
+
endLine: nodeEndLine(node),
|
|
38
|
+
});
|
|
39
|
+
}
|
|
40
|
+
break;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
case 'struct_item': {
|
|
44
|
+
const nameNode = node.childForFieldName('name');
|
|
45
|
+
if (nameNode) {
|
|
46
|
+
definitions.push({
|
|
47
|
+
name: nameNode.text,
|
|
48
|
+
kind: 'struct',
|
|
49
|
+
line: node.startPosition.row + 1,
|
|
50
|
+
endLine: nodeEndLine(node),
|
|
51
|
+
});
|
|
52
|
+
}
|
|
53
|
+
break;
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
case 'enum_item': {
|
|
57
|
+
const nameNode = node.childForFieldName('name');
|
|
58
|
+
if (nameNode) {
|
|
59
|
+
definitions.push({
|
|
60
|
+
name: nameNode.text,
|
|
61
|
+
kind: 'enum',
|
|
62
|
+
line: node.startPosition.row + 1,
|
|
63
|
+
endLine: nodeEndLine(node),
|
|
64
|
+
});
|
|
65
|
+
}
|
|
66
|
+
break;
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
case 'trait_item': {
|
|
70
|
+
const nameNode = node.childForFieldName('name');
|
|
71
|
+
if (nameNode) {
|
|
72
|
+
definitions.push({
|
|
73
|
+
name: nameNode.text,
|
|
74
|
+
kind: 'trait',
|
|
75
|
+
line: node.startPosition.row + 1,
|
|
76
|
+
endLine: nodeEndLine(node),
|
|
77
|
+
});
|
|
78
|
+
const body = node.childForFieldName('body');
|
|
79
|
+
if (body) {
|
|
80
|
+
for (let i = 0; i < body.childCount; i++) {
|
|
81
|
+
const child = body.child(i);
|
|
82
|
+
if (
|
|
83
|
+
child &&
|
|
84
|
+
(child.type === 'function_signature_item' || child.type === 'function_item')
|
|
85
|
+
) {
|
|
86
|
+
const methName = child.childForFieldName('name');
|
|
87
|
+
if (methName) {
|
|
88
|
+
definitions.push({
|
|
89
|
+
name: `${nameNode.text}.${methName.text}`,
|
|
90
|
+
kind: 'method',
|
|
91
|
+
line: child.startPosition.row + 1,
|
|
92
|
+
endLine: child.endPosition.row + 1,
|
|
93
|
+
});
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
break;
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
case 'impl_item': {
|
|
103
|
+
const typeNode = node.childForFieldName('type');
|
|
104
|
+
const traitNode = node.childForFieldName('trait');
|
|
105
|
+
if (typeNode && traitNode) {
|
|
106
|
+
classes.push({
|
|
107
|
+
name: typeNode.text,
|
|
108
|
+
implements: traitNode.text,
|
|
109
|
+
line: node.startPosition.row + 1,
|
|
110
|
+
});
|
|
111
|
+
}
|
|
112
|
+
break;
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
case 'use_declaration': {
|
|
116
|
+
const argNode = node.child(1);
|
|
117
|
+
if (argNode) {
|
|
118
|
+
const usePaths = extractRustUsePath(argNode);
|
|
119
|
+
for (const imp of usePaths) {
|
|
120
|
+
imports.push({
|
|
121
|
+
source: imp.source,
|
|
122
|
+
names: imp.names,
|
|
123
|
+
line: node.startPosition.row + 1,
|
|
124
|
+
rustUse: true,
|
|
125
|
+
});
|
|
126
|
+
}
|
|
127
|
+
}
|
|
128
|
+
break;
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
case 'call_expression': {
|
|
132
|
+
const fn = node.childForFieldName('function');
|
|
133
|
+
if (fn) {
|
|
134
|
+
if (fn.type === 'identifier') {
|
|
135
|
+
calls.push({ name: fn.text, line: node.startPosition.row + 1 });
|
|
136
|
+
} else if (fn.type === 'field_expression') {
|
|
137
|
+
const field = fn.childForFieldName('field');
|
|
138
|
+
if (field) {
|
|
139
|
+
const value = fn.childForFieldName('value');
|
|
140
|
+
const call = { name: field.text, line: node.startPosition.row + 1 };
|
|
141
|
+
if (value) call.receiver = value.text;
|
|
142
|
+
calls.push(call);
|
|
143
|
+
}
|
|
144
|
+
} else if (fn.type === 'scoped_identifier') {
|
|
145
|
+
const name = fn.childForFieldName('name');
|
|
146
|
+
if (name) {
|
|
147
|
+
const path = fn.childForFieldName('path');
|
|
148
|
+
const call = { name: name.text, line: node.startPosition.row + 1 };
|
|
149
|
+
if (path) call.receiver = path.text;
|
|
150
|
+
calls.push(call);
|
|
151
|
+
}
|
|
152
|
+
}
|
|
153
|
+
}
|
|
154
|
+
break;
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
case 'macro_invocation': {
|
|
158
|
+
const macroNode = node.child(0);
|
|
159
|
+
if (macroNode) {
|
|
160
|
+
calls.push({ name: `${macroNode.text}!`, line: node.startPosition.row + 1 });
|
|
161
|
+
}
|
|
162
|
+
break;
|
|
163
|
+
}
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
for (let i = 0; i < node.childCount; i++) walkRustNode(node.child(i));
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
walkRustNode(tree.rootNode);
|
|
170
|
+
return { definitions, calls, imports, classes, exports };
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
function extractRustUsePath(node) {
|
|
174
|
+
if (!node) return [];
|
|
175
|
+
|
|
176
|
+
if (node.type === 'use_list') {
|
|
177
|
+
const results = [];
|
|
178
|
+
for (let i = 0; i < node.childCount; i++) {
|
|
179
|
+
results.push(...extractRustUsePath(node.child(i)));
|
|
180
|
+
}
|
|
181
|
+
return results;
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
if (node.type === 'scoped_use_list') {
|
|
185
|
+
const pathNode = node.childForFieldName('path');
|
|
186
|
+
const listNode = node.childForFieldName('list');
|
|
187
|
+
const prefix = pathNode ? pathNode.text : '';
|
|
188
|
+
if (listNode) {
|
|
189
|
+
const names = [];
|
|
190
|
+
for (let i = 0; i < listNode.childCount; i++) {
|
|
191
|
+
const child = listNode.child(i);
|
|
192
|
+
if (
|
|
193
|
+
child &&
|
|
194
|
+
(child.type === 'identifier' || child.type === 'use_as_clause' || child.type === 'self')
|
|
195
|
+
) {
|
|
196
|
+
const name =
|
|
197
|
+
child.type === 'use_as_clause'
|
|
198
|
+
? (child.childForFieldName('alias') || child.childForFieldName('name'))?.text
|
|
199
|
+
: child.text;
|
|
200
|
+
if (name) names.push(name);
|
|
201
|
+
}
|
|
202
|
+
}
|
|
203
|
+
return [{ source: prefix, names }];
|
|
204
|
+
}
|
|
205
|
+
return [{ source: prefix, names: [] }];
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
if (node.type === 'use_as_clause') {
|
|
209
|
+
const name = node.childForFieldName('alias') || node.childForFieldName('name');
|
|
210
|
+
return [{ source: node.text, names: name ? [name.text] : [] }];
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
if (node.type === 'use_wildcard') {
|
|
214
|
+
const pathNode = node.childForFieldName('path');
|
|
215
|
+
return [{ source: pathNode ? pathNode.text : '*', names: ['*'] }];
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
if (node.type === 'scoped_identifier' || node.type === 'identifier') {
|
|
219
|
+
const text = node.text;
|
|
220
|
+
const lastName = text.split('::').pop();
|
|
221
|
+
return [{ source: text, names: [lastName] }];
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
return [];
|
|
225
|
+
}
|
package/src/index.js
CHANGED
|
@@ -38,6 +38,7 @@ export { isNativeAvailable } from './native.js';
|
|
|
38
38
|
export { getActiveEngine, parseFileAuto, parseFilesAuto } from './parser.js';
|
|
39
39
|
// Query functions (data-returning)
|
|
40
40
|
export {
|
|
41
|
+
contextData,
|
|
41
42
|
diffImpactData,
|
|
42
43
|
fileDepsData,
|
|
43
44
|
fnDepsData,
|
|
@@ -45,6 +46,7 @@ export {
|
|
|
45
46
|
impactAnalysisData,
|
|
46
47
|
moduleMapData,
|
|
47
48
|
queryNameData,
|
|
49
|
+
statsData,
|
|
48
50
|
} from './queries.js';
|
|
49
51
|
// Registry (multi-repo)
|
|
50
52
|
export {
|
package/src/journal.js
ADDED
|
@@ -0,0 +1,109 @@
|
|
|
1
|
+
import fs from 'node:fs';
|
|
2
|
+
import path from 'node:path';
|
|
3
|
+
import { debug, warn } from './logger.js';
|
|
4
|
+
|
|
5
|
+
export const JOURNAL_FILENAME = 'changes.journal';
|
|
6
|
+
const HEADER_PREFIX = '# codegraph-journal v1 ';
|
|
7
|
+
|
|
8
|
+
/**
|
|
9
|
+
* Read and validate the change journal.
|
|
10
|
+
* Returns { valid, timestamp, changed[], removed[] } or { valid: false }.
|
|
11
|
+
*/
|
|
12
|
+
export function readJournal(rootDir) {
|
|
13
|
+
const journalPath = path.join(rootDir, '.codegraph', JOURNAL_FILENAME);
|
|
14
|
+
let content;
|
|
15
|
+
try {
|
|
16
|
+
content = fs.readFileSync(journalPath, 'utf-8');
|
|
17
|
+
} catch {
|
|
18
|
+
return { valid: false };
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
const lines = content.split('\n');
|
|
22
|
+
if (lines.length === 0 || !lines[0].startsWith(HEADER_PREFIX)) {
|
|
23
|
+
debug('Journal has malformed or missing header');
|
|
24
|
+
return { valid: false };
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
const timestamp = Number(lines[0].slice(HEADER_PREFIX.length).trim());
|
|
28
|
+
if (!Number.isFinite(timestamp) || timestamp <= 0) {
|
|
29
|
+
debug('Journal has invalid timestamp');
|
|
30
|
+
return { valid: false };
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
const changed = [];
|
|
34
|
+
const removed = [];
|
|
35
|
+
const seenChanged = new Set();
|
|
36
|
+
const seenRemoved = new Set();
|
|
37
|
+
|
|
38
|
+
for (let i = 1; i < lines.length; i++) {
|
|
39
|
+
const line = lines[i].trim();
|
|
40
|
+
if (!line || line.startsWith('#')) continue;
|
|
41
|
+
|
|
42
|
+
if (line.startsWith('DELETED ')) {
|
|
43
|
+
const filePath = line.slice(8);
|
|
44
|
+
if (filePath && !seenRemoved.has(filePath)) {
|
|
45
|
+
seenRemoved.add(filePath);
|
|
46
|
+
removed.push(filePath);
|
|
47
|
+
}
|
|
48
|
+
} else {
|
|
49
|
+
if (!seenChanged.has(line)) {
|
|
50
|
+
seenChanged.add(line);
|
|
51
|
+
changed.push(line);
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
return { valid: true, timestamp, changed, removed };
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
/**
|
|
60
|
+
* Append changed/deleted paths to the journal.
|
|
61
|
+
* Creates the journal with a header if it doesn't exist.
|
|
62
|
+
*/
|
|
63
|
+
export function appendJournalEntries(rootDir, entries) {
|
|
64
|
+
const dir = path.join(rootDir, '.codegraph');
|
|
65
|
+
const journalPath = path.join(dir, JOURNAL_FILENAME);
|
|
66
|
+
|
|
67
|
+
if (!fs.existsSync(dir)) {
|
|
68
|
+
fs.mkdirSync(dir, { recursive: true });
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
// If journal doesn't exist, create with a placeholder header
|
|
72
|
+
if (!fs.existsSync(journalPath)) {
|
|
73
|
+
fs.writeFileSync(journalPath, `${HEADER_PREFIX}0\n`);
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
const lines = entries.map((e) => {
|
|
77
|
+
if (e.deleted) return `DELETED ${e.file}`;
|
|
78
|
+
return e.file;
|
|
79
|
+
});
|
|
80
|
+
|
|
81
|
+
fs.appendFileSync(journalPath, `${lines.join('\n')}\n`);
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
/**
|
|
85
|
+
* Write a fresh journal header after a successful build.
|
|
86
|
+
* Atomic: write to temp file then rename.
|
|
87
|
+
*/
|
|
88
|
+
export function writeJournalHeader(rootDir, timestamp) {
|
|
89
|
+
const dir = path.join(rootDir, '.codegraph');
|
|
90
|
+
const journalPath = path.join(dir, JOURNAL_FILENAME);
|
|
91
|
+
const tmpPath = `${journalPath}.tmp`;
|
|
92
|
+
|
|
93
|
+
if (!fs.existsSync(dir)) {
|
|
94
|
+
fs.mkdirSync(dir, { recursive: true });
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
try {
|
|
98
|
+
fs.writeFileSync(tmpPath, `${HEADER_PREFIX}${timestamp}\n`);
|
|
99
|
+
fs.renameSync(tmpPath, journalPath);
|
|
100
|
+
} catch (err) {
|
|
101
|
+
warn(`Failed to write journal header: ${err.message}`);
|
|
102
|
+
// Clean up temp file if rename failed
|
|
103
|
+
try {
|
|
104
|
+
fs.unlinkSync(tmpPath);
|
|
105
|
+
} catch {
|
|
106
|
+
/* ignore */
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
}
|
package/src/mcp.js
CHANGED
|
@@ -100,6 +100,34 @@ const BASE_TOOLS = [
|
|
|
100
100
|
required: ['name'],
|
|
101
101
|
},
|
|
102
102
|
},
|
|
103
|
+
{
|
|
104
|
+
name: 'context',
|
|
105
|
+
description:
|
|
106
|
+
'Full context for a function: source code, dependencies with summaries, callers, signature, and related tests — everything needed to understand or modify a function in one call',
|
|
107
|
+
inputSchema: {
|
|
108
|
+
type: 'object',
|
|
109
|
+
properties: {
|
|
110
|
+
name: { type: 'string', description: 'Function/method/class name (partial match)' },
|
|
111
|
+
depth: {
|
|
112
|
+
type: 'number',
|
|
113
|
+
description: 'Include callee source up to N levels deep (0=no source, 1=direct)',
|
|
114
|
+
default: 0,
|
|
115
|
+
},
|
|
116
|
+
no_source: {
|
|
117
|
+
type: 'boolean',
|
|
118
|
+
description: 'Skip source extraction (metadata only)',
|
|
119
|
+
default: false,
|
|
120
|
+
},
|
|
121
|
+
no_tests: { type: 'boolean', description: 'Exclude test files', default: false },
|
|
122
|
+
include_tests: {
|
|
123
|
+
type: 'boolean',
|
|
124
|
+
description: 'Include test file source code',
|
|
125
|
+
default: false,
|
|
126
|
+
},
|
|
127
|
+
},
|
|
128
|
+
required: ['name'],
|
|
129
|
+
},
|
|
130
|
+
},
|
|
103
131
|
{
|
|
104
132
|
name: 'diff_impact',
|
|
105
133
|
description: 'Analyze git diff to find which functions changed and their transitive callers',
|
|
@@ -245,12 +273,15 @@ export { TOOLS, buildToolList };
|
|
|
245
273
|
export async function startMCPServer(customDbPath, options = {}) {
|
|
246
274
|
const { allowedRepos } = options;
|
|
247
275
|
const multiRepo = options.multiRepo || !!allowedRepos;
|
|
248
|
-
let Server, StdioServerTransport;
|
|
276
|
+
let Server, StdioServerTransport, ListToolsRequestSchema, CallToolRequestSchema;
|
|
249
277
|
try {
|
|
250
278
|
const sdk = await import('@modelcontextprotocol/sdk/server/index.js');
|
|
251
279
|
Server = sdk.Server;
|
|
252
280
|
const transport = await import('@modelcontextprotocol/sdk/server/stdio.js');
|
|
253
281
|
StdioServerTransport = transport.StdioServerTransport;
|
|
282
|
+
const types = await import('@modelcontextprotocol/sdk/types.js');
|
|
283
|
+
ListToolsRequestSchema = types.ListToolsRequestSchema;
|
|
284
|
+
CallToolRequestSchema = types.CallToolRequestSchema;
|
|
254
285
|
} catch {
|
|
255
286
|
console.error(
|
|
256
287
|
'MCP server requires @modelcontextprotocol/sdk.\n' +
|
|
@@ -267,6 +298,7 @@ export async function startMCPServer(customDbPath, options = {}) {
|
|
|
267
298
|
fileDepsData,
|
|
268
299
|
fnDepsData,
|
|
269
300
|
fnImpactData,
|
|
301
|
+
contextData,
|
|
270
302
|
diffImpactData,
|
|
271
303
|
listFunctionsData,
|
|
272
304
|
} = await import('./queries.js');
|
|
@@ -279,9 +311,11 @@ export async function startMCPServer(customDbPath, options = {}) {
|
|
|
279
311
|
{ capabilities: { tools: {} } },
|
|
280
312
|
);
|
|
281
313
|
|
|
282
|
-
server.setRequestHandler(
|
|
314
|
+
server.setRequestHandler(ListToolsRequestSchema, async () => ({
|
|
315
|
+
tools: buildToolList(multiRepo),
|
|
316
|
+
}));
|
|
283
317
|
|
|
284
|
-
server.setRequestHandler(
|
|
318
|
+
server.setRequestHandler(CallToolRequestSchema, async (request) => {
|
|
285
319
|
const { name, arguments: args } = request.params;
|
|
286
320
|
|
|
287
321
|
try {
|
|
@@ -343,6 +377,14 @@ export async function startMCPServer(customDbPath, options = {}) {
|
|
|
343
377
|
noTests: args.no_tests,
|
|
344
378
|
});
|
|
345
379
|
break;
|
|
380
|
+
case 'context':
|
|
381
|
+
result = contextData(args.name, dbPath, {
|
|
382
|
+
depth: args.depth,
|
|
383
|
+
noSource: args.no_source,
|
|
384
|
+
noTests: args.no_tests,
|
|
385
|
+
includeTests: args.include_tests,
|
|
386
|
+
});
|
|
387
|
+
break;
|
|
346
388
|
case 'diff_impact':
|
|
347
389
|
result = diffImpactData(dbPath, {
|
|
348
390
|
staged: args.staged,
|
|
@@ -422,7 +464,8 @@ export async function startMCPServer(customDbPath, options = {}) {
|
|
|
422
464
|
break;
|
|
423
465
|
}
|
|
424
466
|
case 'list_repos': {
|
|
425
|
-
const { listRepos } = await import('./registry.js');
|
|
467
|
+
const { listRepos, pruneRegistry } = await import('./registry.js');
|
|
468
|
+
pruneRegistry();
|
|
426
469
|
let repos = listRepos();
|
|
427
470
|
if (allowedRepos) {
|
|
428
471
|
repos = repos.filter((r) => allowedRepos.includes(r.name));
|