@optave/codegraph 2.1.1-dev.3c12b64 → 2.2.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 +49 -31
- package/package.json +5 -5
- package/src/builder.js +238 -33
- package/src/cli.js +93 -9
- package/src/cycles.js +13 -1
- package/src/db.js +4 -0
- package/src/export.js +20 -7
- package/src/extractors/csharp.js +6 -1
- package/src/extractors/go.js +6 -1
- package/src/extractors/java.js +4 -1
- package/src/extractors/javascript.js +145 -5
- package/src/extractors/php.js +8 -2
- package/src/extractors/python.js +8 -1
- package/src/extractors/ruby.js +4 -1
- package/src/extractors/rust.js +12 -2
- package/src/index.js +6 -0
- package/src/journal.js +109 -0
- package/src/mcp.js +131 -7
- package/src/parser.js +1 -0
- package/src/queries.js +1143 -38
- package/src/structure.js +21 -7
- package/src/watcher.js +25 -0
package/src/extractors/ruby.js
CHANGED
|
@@ -170,7 +170,10 @@ export function extractRubySymbols(tree, _filePath) {
|
|
|
170
170
|
}
|
|
171
171
|
}
|
|
172
172
|
} else {
|
|
173
|
-
|
|
173
|
+
const recv = node.childForFieldName('receiver');
|
|
174
|
+
const call = { name: methodNode.text, line: node.startPosition.row + 1 };
|
|
175
|
+
if (recv) call.receiver = recv.text;
|
|
176
|
+
calls.push(call);
|
|
174
177
|
}
|
|
175
178
|
}
|
|
176
179
|
break;
|
package/src/extractors/rust.js
CHANGED
|
@@ -135,10 +135,20 @@ export function extractRustSymbols(tree, _filePath) {
|
|
|
135
135
|
calls.push({ name: fn.text, line: node.startPosition.row + 1 });
|
|
136
136
|
} else if (fn.type === 'field_expression') {
|
|
137
137
|
const field = fn.childForFieldName('field');
|
|
138
|
-
if (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
|
+
}
|
|
139
144
|
} else if (fn.type === 'scoped_identifier') {
|
|
140
145
|
const name = fn.childForFieldName('name');
|
|
141
|
-
if (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
|
+
}
|
|
142
152
|
}
|
|
143
153
|
}
|
|
144
154
|
break;
|
package/src/index.js
CHANGED
|
@@ -38,7 +38,12 @@ export { isNativeAvailable } from './native.js';
|
|
|
38
38
|
export { getActiveEngine, parseFileAuto, parseFilesAuto } from './parser.js';
|
|
39
39
|
// Query functions (data-returning)
|
|
40
40
|
export {
|
|
41
|
+
ALL_SYMBOL_KINDS,
|
|
42
|
+
contextData,
|
|
41
43
|
diffImpactData,
|
|
44
|
+
explainData,
|
|
45
|
+
FALSE_POSITIVE_CALLER_THRESHOLD,
|
|
46
|
+
FALSE_POSITIVE_NAMES,
|
|
42
47
|
fileDepsData,
|
|
43
48
|
fnDepsData,
|
|
44
49
|
fnImpactData,
|
|
@@ -46,6 +51,7 @@ export {
|
|
|
46
51
|
moduleMapData,
|
|
47
52
|
queryNameData,
|
|
48
53
|
statsData,
|
|
54
|
+
whereData,
|
|
49
55
|
} from './queries.js';
|
|
50
56
|
// Registry (multi-repo)
|
|
51
57
|
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
|
@@ -8,6 +8,7 @@
|
|
|
8
8
|
import { createRequire } from 'node:module';
|
|
9
9
|
import { findCycles } from './cycles.js';
|
|
10
10
|
import { findDbPath } from './db.js';
|
|
11
|
+
import { ALL_SYMBOL_KINDS } from './queries.js';
|
|
11
12
|
|
|
12
13
|
const REPO_PROP = {
|
|
13
14
|
repo: {
|
|
@@ -29,6 +30,7 @@ const BASE_TOOLS = [
|
|
|
29
30
|
description: 'Traversal depth for transitive callers',
|
|
30
31
|
default: 2,
|
|
31
32
|
},
|
|
33
|
+
no_tests: { type: 'boolean', description: 'Exclude test files', default: false },
|
|
32
34
|
},
|
|
33
35
|
required: ['name'],
|
|
34
36
|
},
|
|
@@ -40,6 +42,7 @@ const BASE_TOOLS = [
|
|
|
40
42
|
type: 'object',
|
|
41
43
|
properties: {
|
|
42
44
|
file: { type: 'string', description: 'File path (partial match supported)' },
|
|
45
|
+
no_tests: { type: 'boolean', description: 'Exclude test files', default: false },
|
|
43
46
|
},
|
|
44
47
|
required: ['file'],
|
|
45
48
|
},
|
|
@@ -51,6 +54,7 @@ const BASE_TOOLS = [
|
|
|
51
54
|
type: 'object',
|
|
52
55
|
properties: {
|
|
53
56
|
file: { type: 'string', description: 'File path to analyze' },
|
|
57
|
+
no_tests: { type: 'boolean', description: 'Exclude test files', default: false },
|
|
54
58
|
},
|
|
55
59
|
required: ['file'],
|
|
56
60
|
},
|
|
@@ -70,6 +74,7 @@ const BASE_TOOLS = [
|
|
|
70
74
|
type: 'object',
|
|
71
75
|
properties: {
|
|
72
76
|
limit: { type: 'number', description: 'Number of top files to show', default: 20 },
|
|
77
|
+
no_tests: { type: 'boolean', description: 'Exclude test files', default: false },
|
|
73
78
|
},
|
|
74
79
|
},
|
|
75
80
|
},
|
|
@@ -81,6 +86,15 @@ const BASE_TOOLS = [
|
|
|
81
86
|
properties: {
|
|
82
87
|
name: { type: 'string', description: 'Function/method/class name (partial match)' },
|
|
83
88
|
depth: { type: 'number', description: 'Transitive caller depth', default: 3 },
|
|
89
|
+
file: {
|
|
90
|
+
type: 'string',
|
|
91
|
+
description: 'Scope search to functions in this file (partial match)',
|
|
92
|
+
},
|
|
93
|
+
kind: {
|
|
94
|
+
type: 'string',
|
|
95
|
+
enum: ALL_SYMBOL_KINDS,
|
|
96
|
+
description: 'Filter to a specific symbol kind',
|
|
97
|
+
},
|
|
84
98
|
no_tests: { type: 'boolean', description: 'Exclude test files', default: false },
|
|
85
99
|
},
|
|
86
100
|
required: ['name'],
|
|
@@ -95,11 +109,88 @@ const BASE_TOOLS = [
|
|
|
95
109
|
properties: {
|
|
96
110
|
name: { type: 'string', description: 'Function/method/class name (partial match)' },
|
|
97
111
|
depth: { type: 'number', description: 'Max traversal depth', default: 5 },
|
|
112
|
+
file: {
|
|
113
|
+
type: 'string',
|
|
114
|
+
description: 'Scope search to functions in this file (partial match)',
|
|
115
|
+
},
|
|
116
|
+
kind: {
|
|
117
|
+
type: 'string',
|
|
118
|
+
enum: ALL_SYMBOL_KINDS,
|
|
119
|
+
description: 'Filter to a specific symbol kind',
|
|
120
|
+
},
|
|
121
|
+
no_tests: { type: 'boolean', description: 'Exclude test files', default: false },
|
|
122
|
+
},
|
|
123
|
+
required: ['name'],
|
|
124
|
+
},
|
|
125
|
+
},
|
|
126
|
+
{
|
|
127
|
+
name: 'context',
|
|
128
|
+
description:
|
|
129
|
+
'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',
|
|
130
|
+
inputSchema: {
|
|
131
|
+
type: 'object',
|
|
132
|
+
properties: {
|
|
133
|
+
name: { type: 'string', description: 'Function/method/class name (partial match)' },
|
|
134
|
+
depth: {
|
|
135
|
+
type: 'number',
|
|
136
|
+
description: 'Include callee source up to N levels deep (0=no source, 1=direct)',
|
|
137
|
+
default: 0,
|
|
138
|
+
},
|
|
139
|
+
file: {
|
|
140
|
+
type: 'string',
|
|
141
|
+
description: 'Scope search to functions in this file (partial match)',
|
|
142
|
+
},
|
|
143
|
+
kind: {
|
|
144
|
+
type: 'string',
|
|
145
|
+
enum: ALL_SYMBOL_KINDS,
|
|
146
|
+
description: 'Filter to a specific symbol kind',
|
|
147
|
+
},
|
|
148
|
+
no_source: {
|
|
149
|
+
type: 'boolean',
|
|
150
|
+
description: 'Skip source extraction (metadata only)',
|
|
151
|
+
default: false,
|
|
152
|
+
},
|
|
98
153
|
no_tests: { type: 'boolean', description: 'Exclude test files', default: false },
|
|
154
|
+
include_tests: {
|
|
155
|
+
type: 'boolean',
|
|
156
|
+
description: 'Include test file source code',
|
|
157
|
+
default: false,
|
|
158
|
+
},
|
|
99
159
|
},
|
|
100
160
|
required: ['name'],
|
|
101
161
|
},
|
|
102
162
|
},
|
|
163
|
+
{
|
|
164
|
+
name: 'explain',
|
|
165
|
+
description:
|
|
166
|
+
'Structural summary of a file or function: public/internal API, data flow, dependencies. No LLM needed.',
|
|
167
|
+
inputSchema: {
|
|
168
|
+
type: 'object',
|
|
169
|
+
properties: {
|
|
170
|
+
target: { type: 'string', description: 'File path or function name' },
|
|
171
|
+
no_tests: { type: 'boolean', description: 'Exclude test files', default: false },
|
|
172
|
+
},
|
|
173
|
+
required: ['target'],
|
|
174
|
+
},
|
|
175
|
+
},
|
|
176
|
+
{
|
|
177
|
+
name: 'where',
|
|
178
|
+
description:
|
|
179
|
+
'Find where a symbol is defined and used, or list symbols/imports/exports for a file. Minimal, fast lookup.',
|
|
180
|
+
inputSchema: {
|
|
181
|
+
type: 'object',
|
|
182
|
+
properties: {
|
|
183
|
+
target: { type: 'string', description: 'Symbol name or file path' },
|
|
184
|
+
file_mode: {
|
|
185
|
+
type: 'boolean',
|
|
186
|
+
description: 'Treat target as file path (list symbols/imports/exports)',
|
|
187
|
+
default: false,
|
|
188
|
+
},
|
|
189
|
+
no_tests: { type: 'boolean', description: 'Exclude test files', default: false },
|
|
190
|
+
},
|
|
191
|
+
required: ['target'],
|
|
192
|
+
},
|
|
193
|
+
},
|
|
103
194
|
{
|
|
104
195
|
name: 'diff_impact',
|
|
105
196
|
description: 'Analyze git diff to find which functions changed and their transitive callers',
|
|
@@ -195,6 +286,7 @@ const BASE_TOOLS = [
|
|
|
195
286
|
description: 'Rank files or directories',
|
|
196
287
|
},
|
|
197
288
|
limit: { type: 'number', description: 'Number of results to return', default: 10 },
|
|
289
|
+
no_tests: { type: 'boolean', description: 'Exclude test files', default: false },
|
|
198
290
|
},
|
|
199
291
|
},
|
|
200
292
|
},
|
|
@@ -245,12 +337,15 @@ export { TOOLS, buildToolList };
|
|
|
245
337
|
export async function startMCPServer(customDbPath, options = {}) {
|
|
246
338
|
const { allowedRepos } = options;
|
|
247
339
|
const multiRepo = options.multiRepo || !!allowedRepos;
|
|
248
|
-
let Server, StdioServerTransport;
|
|
340
|
+
let Server, StdioServerTransport, ListToolsRequestSchema, CallToolRequestSchema;
|
|
249
341
|
try {
|
|
250
342
|
const sdk = await import('@modelcontextprotocol/sdk/server/index.js');
|
|
251
343
|
Server = sdk.Server;
|
|
252
344
|
const transport = await import('@modelcontextprotocol/sdk/server/stdio.js');
|
|
253
345
|
StdioServerTransport = transport.StdioServerTransport;
|
|
346
|
+
const types = await import('@modelcontextprotocol/sdk/types.js');
|
|
347
|
+
ListToolsRequestSchema = types.ListToolsRequestSchema;
|
|
348
|
+
CallToolRequestSchema = types.CallToolRequestSchema;
|
|
254
349
|
} catch {
|
|
255
350
|
console.error(
|
|
256
351
|
'MCP server requires @modelcontextprotocol/sdk.\n' +
|
|
@@ -267,6 +362,9 @@ export async function startMCPServer(customDbPath, options = {}) {
|
|
|
267
362
|
fileDepsData,
|
|
268
363
|
fnDepsData,
|
|
269
364
|
fnImpactData,
|
|
365
|
+
contextData,
|
|
366
|
+
explainData,
|
|
367
|
+
whereData,
|
|
270
368
|
diffImpactData,
|
|
271
369
|
listFunctionsData,
|
|
272
370
|
} = await import('./queries.js');
|
|
@@ -279,9 +377,11 @@ export async function startMCPServer(customDbPath, options = {}) {
|
|
|
279
377
|
{ capabilities: { tools: {} } },
|
|
280
378
|
);
|
|
281
379
|
|
|
282
|
-
server.setRequestHandler(
|
|
380
|
+
server.setRequestHandler(ListToolsRequestSchema, async () => ({
|
|
381
|
+
tools: buildToolList(multiRepo),
|
|
382
|
+
}));
|
|
283
383
|
|
|
284
|
-
server.setRequestHandler(
|
|
384
|
+
server.setRequestHandler(CallToolRequestSchema, async (request) => {
|
|
285
385
|
const { name, arguments: args } = request.params;
|
|
286
386
|
|
|
287
387
|
try {
|
|
@@ -313,13 +413,13 @@ export async function startMCPServer(customDbPath, options = {}) {
|
|
|
313
413
|
let result;
|
|
314
414
|
switch (name) {
|
|
315
415
|
case 'query_function':
|
|
316
|
-
result = queryNameData(args.name, dbPath);
|
|
416
|
+
result = queryNameData(args.name, dbPath, { noTests: args.no_tests });
|
|
317
417
|
break;
|
|
318
418
|
case 'file_deps':
|
|
319
|
-
result = fileDepsData(args.file, dbPath);
|
|
419
|
+
result = fileDepsData(args.file, dbPath, { noTests: args.no_tests });
|
|
320
420
|
break;
|
|
321
421
|
case 'impact_analysis':
|
|
322
|
-
result = impactAnalysisData(args.file, dbPath);
|
|
422
|
+
result = impactAnalysisData(args.file, dbPath, { noTests: args.no_tests });
|
|
323
423
|
break;
|
|
324
424
|
case 'find_cycles': {
|
|
325
425
|
const db = new Database(findDbPath(dbPath), { readonly: true });
|
|
@@ -329,17 +429,40 @@ export async function startMCPServer(customDbPath, options = {}) {
|
|
|
329
429
|
break;
|
|
330
430
|
}
|
|
331
431
|
case 'module_map':
|
|
332
|
-
result = moduleMapData(dbPath, args.limit || 20);
|
|
432
|
+
result = moduleMapData(dbPath, args.limit || 20, { noTests: args.no_tests });
|
|
333
433
|
break;
|
|
334
434
|
case 'fn_deps':
|
|
335
435
|
result = fnDepsData(args.name, dbPath, {
|
|
336
436
|
depth: args.depth,
|
|
437
|
+
file: args.file,
|
|
438
|
+
kind: args.kind,
|
|
337
439
|
noTests: args.no_tests,
|
|
338
440
|
});
|
|
339
441
|
break;
|
|
340
442
|
case 'fn_impact':
|
|
341
443
|
result = fnImpactData(args.name, dbPath, {
|
|
342
444
|
depth: args.depth,
|
|
445
|
+
file: args.file,
|
|
446
|
+
kind: args.kind,
|
|
447
|
+
noTests: args.no_tests,
|
|
448
|
+
});
|
|
449
|
+
break;
|
|
450
|
+
case 'context':
|
|
451
|
+
result = contextData(args.name, dbPath, {
|
|
452
|
+
depth: args.depth,
|
|
453
|
+
file: args.file,
|
|
454
|
+
kind: args.kind,
|
|
455
|
+
noSource: args.no_source,
|
|
456
|
+
noTests: args.no_tests,
|
|
457
|
+
includeTests: args.include_tests,
|
|
458
|
+
});
|
|
459
|
+
break;
|
|
460
|
+
case 'explain':
|
|
461
|
+
result = explainData(args.target, dbPath, { noTests: args.no_tests });
|
|
462
|
+
break;
|
|
463
|
+
case 'where':
|
|
464
|
+
result = whereData(args.target, dbPath, {
|
|
465
|
+
file: args.file_mode,
|
|
343
466
|
noTests: args.no_tests,
|
|
344
467
|
});
|
|
345
468
|
break;
|
|
@@ -418,6 +541,7 @@ export async function startMCPServer(customDbPath, options = {}) {
|
|
|
418
541
|
metric: args.metric,
|
|
419
542
|
level: args.level,
|
|
420
543
|
limit: args.limit,
|
|
544
|
+
noTests: args.no_tests,
|
|
421
545
|
});
|
|
422
546
|
break;
|
|
423
547
|
}
|