project-graph-mcp 1.0.1 → 1.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 +10 -2
- package/package.json +7 -3
- package/src/.project-graph-cache.json +1 -0
- package/src/filters.js +1 -0
- package/src/graph-builder.js +1 -1
- package/src/lang-go.js +285 -0
- package/src/lang-python.js +197 -0
- package/src/lang-typescript.js +190 -0
- package/src/lang-utils.js +124 -0
- package/src/mcp-server.js +67 -2
- package/src/parser.js +42 -3
- package/src/server.js +0 -0
- package/src/tool-defs.js +33 -0
- package/src/tools.js +237 -7
|
@@ -0,0 +1,190 @@
|
|
|
1
|
+
import { stripStringsAndComments } from './lang-utils.js';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* TypeScript/TSX regex-based parser.
|
|
5
|
+
* Extracts structural information (classes, functions, imports, exports, calls)
|
|
6
|
+
* directly from TypeScript code without relying on Acorn.
|
|
7
|
+
*
|
|
8
|
+
* Strategy: Instead of stripping TS syntax to feed Acorn (which causes
|
|
9
|
+
* catastrophic backtracking in regex), parse structural elements directly
|
|
10
|
+
* — same approach as lang-python.js and lang-go.js.
|
|
11
|
+
*
|
|
12
|
+
* @param {string} code - TypeScript source code
|
|
13
|
+
* @param {string} filename - File path for the result
|
|
14
|
+
* @returns {ParseResult}
|
|
15
|
+
*/
|
|
16
|
+
export function parseTypeScript(code, filename) {
|
|
17
|
+
const result = {
|
|
18
|
+
file: filename,
|
|
19
|
+
classes: [],
|
|
20
|
+
functions: [],
|
|
21
|
+
imports: [],
|
|
22
|
+
exports: [],
|
|
23
|
+
};
|
|
24
|
+
|
|
25
|
+
// Strip strings, template literals, and comments to avoid false matches
|
|
26
|
+
const cleaned = stripStringsAndComments(code);
|
|
27
|
+
const lines = cleaned.split('\n');
|
|
28
|
+
|
|
29
|
+
let currentClass = null;
|
|
30
|
+
let currentFunc = null;
|
|
31
|
+
|
|
32
|
+
for (let i = 0; i < lines.length; i++) {
|
|
33
|
+
const line = lines[i];
|
|
34
|
+
const lineNum = i + 1;
|
|
35
|
+
|
|
36
|
+
// --- Imports ---
|
|
37
|
+
// import { A, B } from 'module'
|
|
38
|
+
const importFromMatch = line.match(/^\s*import\s+(?:type\s+)?(?:\{([^}]+)\}|(\w+))\s+from\s/);
|
|
39
|
+
if (importFromMatch) {
|
|
40
|
+
if (importFromMatch[1]) {
|
|
41
|
+
importFromMatch[1].split(',').forEach(s => {
|
|
42
|
+
const name = s.trim().replace(/\s+as\s+\w+/, '').replace(/^type\s+/, '');
|
|
43
|
+
if (name) result.imports.push(name);
|
|
44
|
+
});
|
|
45
|
+
} else if (importFromMatch[2]) {
|
|
46
|
+
result.imports.push(importFromMatch[2]);
|
|
47
|
+
}
|
|
48
|
+
continue;
|
|
49
|
+
}
|
|
50
|
+
// import * as name from 'module'
|
|
51
|
+
const importStarMatch = line.match(/^\s*import\s+\*\s+as\s+(\w+)\s+from\s/);
|
|
52
|
+
if (importStarMatch) {
|
|
53
|
+
result.imports.push(importStarMatch[1]);
|
|
54
|
+
continue;
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
// --- Exports ---
|
|
58
|
+
const exportMatch = line.match(/^\s*export\s+(?:default\s+)?(?:class|function|const|let|var|type|interface|enum|abstract)\s+(\w+)/);
|
|
59
|
+
if (exportMatch) {
|
|
60
|
+
result.exports.push(exportMatch[1]);
|
|
61
|
+
}
|
|
62
|
+
// export { A, B }
|
|
63
|
+
const exportBraceMatch = line.match(/^\s*export\s+\{([^}]+)\}/);
|
|
64
|
+
if (exportBraceMatch) {
|
|
65
|
+
exportBraceMatch[1].split(',').forEach(s => {
|
|
66
|
+
const name = s.trim().replace(/\s+as\s+\w+/, '');
|
|
67
|
+
if (name) result.exports.push(name);
|
|
68
|
+
});
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
// Skip type-only declarations (no runtime code)
|
|
72
|
+
if (/^\s*(type|interface)\s+\w+/.test(line)) {
|
|
73
|
+
continue;
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
// --- Classes ---
|
|
77
|
+
const classMatch = line.match(/^\s*(?:export\s+)?(?:default\s+)?(?:abstract\s+)?class\s+(\w+)(?:\s+extends\s+(\w+))?/);
|
|
78
|
+
if (classMatch) {
|
|
79
|
+
currentClass = {
|
|
80
|
+
name: classMatch[1],
|
|
81
|
+
extends: classMatch[2] || null,
|
|
82
|
+
methods: [],
|
|
83
|
+
properties: [],
|
|
84
|
+
calls: [],
|
|
85
|
+
file: filename,
|
|
86
|
+
line: lineNum,
|
|
87
|
+
};
|
|
88
|
+
result.classes.push(currentClass);
|
|
89
|
+
currentFunc = null;
|
|
90
|
+
continue;
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
// Detect end of class or function (closing brace at col 0)
|
|
94
|
+
if (/^}/.test(line)) {
|
|
95
|
+
currentClass = null;
|
|
96
|
+
currentFunc = null;
|
|
97
|
+
continue;
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
// --- Methods (inside class) ---
|
|
101
|
+
if (currentClass) {
|
|
102
|
+
// public/private/protected/static/async methodName(
|
|
103
|
+
const methodMatch = line.match(/^\s+(?:(?:public|private|protected|static|readonly|abstract|override|async)\s+)*(\w+)\s*(?:<[^>]*>)?\s*\(/);
|
|
104
|
+
if (methodMatch && methodMatch[1] !== 'if' && methodMatch[1] !== 'for' &&
|
|
105
|
+
methodMatch[1] !== 'while' && methodMatch[1] !== 'switch' &&
|
|
106
|
+
methodMatch[1] !== 'catch' && methodMatch[1] !== 'return' &&
|
|
107
|
+
methodMatch[1] !== 'new' && methodMatch[1] !== 'constructor' &&
|
|
108
|
+
methodMatch[1] !== 'super') {
|
|
109
|
+
currentClass.methods.push(methodMatch[1]);
|
|
110
|
+
}
|
|
111
|
+
// constructor
|
|
112
|
+
if (/^\s+constructor\s*\(/.test(line)) {
|
|
113
|
+
currentClass.methods.push('constructor');
|
|
114
|
+
}
|
|
115
|
+
// Property: name: Type or name = value
|
|
116
|
+
const propMatch = line.match(/^\s+(?:(?:public|private|protected|static|readonly|declare|override|abstract)\s+)*(\w+)\s*[?!]?\s*[:=]/);
|
|
117
|
+
if (propMatch && !methodMatch && propMatch[1] !== 'if' && propMatch[1] !== 'const' &&
|
|
118
|
+
propMatch[1] !== 'let' && propMatch[1] !== 'var' && propMatch[1] !== 'return') {
|
|
119
|
+
currentClass.properties.push(propMatch[1]);
|
|
120
|
+
}
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
// --- Functions (top-level) ---
|
|
124
|
+
if (!currentClass) {
|
|
125
|
+
// function name(, async function name(, export function, export default function
|
|
126
|
+
const fnMatch = line.match(/^\s*(?:export\s+)?(?:default\s+)?(?:async\s+)?function\s+(\w+)/);
|
|
127
|
+
if (fnMatch) {
|
|
128
|
+
currentFunc = {
|
|
129
|
+
name: fnMatch[1],
|
|
130
|
+
exported: /^\s*export\s+/.test(line),
|
|
131
|
+
calls: [],
|
|
132
|
+
params: extractParams(line),
|
|
133
|
+
file: filename,
|
|
134
|
+
line: lineNum,
|
|
135
|
+
};
|
|
136
|
+
result.functions.push(currentFunc);
|
|
137
|
+
continue;
|
|
138
|
+
}
|
|
139
|
+
// Arrow functions: const name = (...) => or export const name = (
|
|
140
|
+
const arrowMatch = line.match(/^\s*(?:export\s+)?(?:const|let|var)\s+(\w+)\s*=\s*(?:async\s+)?(?:\([^)]*\)|[a-zA-Z_]\w*)\s*(?::\s*\w+(?:<[^>]*>)?)?\s*=>/);
|
|
141
|
+
if (arrowMatch) {
|
|
142
|
+
currentFunc = {
|
|
143
|
+
name: arrowMatch[1],
|
|
144
|
+
exported: /^\s*export\s+/.test(line),
|
|
145
|
+
calls: [],
|
|
146
|
+
params: extractParams(line),
|
|
147
|
+
file: filename,
|
|
148
|
+
line: lineNum,
|
|
149
|
+
};
|
|
150
|
+
result.functions.push(currentFunc);
|
|
151
|
+
continue;
|
|
152
|
+
}
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
// --- Calls ---
|
|
156
|
+
const callRegex = /\b([a-zA-Z_$]\w*)\s*(?:<[^>]*>)?\s*\(/g;
|
|
157
|
+
let callMatch;
|
|
158
|
+
while ((callMatch = callRegex.exec(line)) !== null) {
|
|
159
|
+
const name = callMatch[1];
|
|
160
|
+
// Skip keywords and common built-ins
|
|
161
|
+
if (['if', 'for', 'while', 'switch', 'catch', 'return', 'new', 'throw',
|
|
162
|
+
'typeof', 'delete', 'void', 'import', 'export', 'class', 'function',
|
|
163
|
+
'const', 'let', 'var', 'async', 'await', 'super', 'this',
|
|
164
|
+
'interface', 'type', 'enum', 'declare', 'abstract'].includes(name)) {
|
|
165
|
+
continue;
|
|
166
|
+
}
|
|
167
|
+
if (currentClass) {
|
|
168
|
+
currentClass.calls.push(name);
|
|
169
|
+
} else if (currentFunc) {
|
|
170
|
+
currentFunc.calls.push(name);
|
|
171
|
+
}
|
|
172
|
+
}
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
return result;
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
/**
|
|
179
|
+
* Extract parameter names from a function signature line.
|
|
180
|
+
* @param {string} line
|
|
181
|
+
* @returns {string[]}
|
|
182
|
+
*/
|
|
183
|
+
function extractParams(line) {
|
|
184
|
+
const match = line.match(/\(([^)]*)\)/);
|
|
185
|
+
if (!match) return [];
|
|
186
|
+
return match[1]
|
|
187
|
+
.split(',')
|
|
188
|
+
.map(p => p.trim().replace(/[?!]?\s*:.*$/, '').replace(/\s*=.*$/, '').trim())
|
|
189
|
+
.filter(p => p && !p.startsWith('...'));
|
|
190
|
+
}
|
|
@@ -0,0 +1,124 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Strip strings, template literals, and comments from source code.
|
|
3
|
+
* Preserves line structure (newlines are kept) and character positions.
|
|
4
|
+
* @param {string} code
|
|
5
|
+
* @param {Object} [options]
|
|
6
|
+
* @param {boolean} [options.singleQuote=true] - Handle single-quoted strings
|
|
7
|
+
* @param {boolean} [options.backtick=true] - Handle backtick strings/templates
|
|
8
|
+
* @param {boolean} [options.hashComment=false] - Handle # comments (Python)
|
|
9
|
+
* @param {boolean} [options.tripleQuote=false] - Handle ''' and """ (Python)
|
|
10
|
+
* @param {boolean} [options.templateInterpolation=true] - Handle ${} in backticks
|
|
11
|
+
* @returns {string}
|
|
12
|
+
*/
|
|
13
|
+
export function stripStringsAndComments(code, options = {}) {
|
|
14
|
+
const {
|
|
15
|
+
singleQuote = true,
|
|
16
|
+
backtick = true,
|
|
17
|
+
hashComment = false,
|
|
18
|
+
tripleQuote = false,
|
|
19
|
+
templateInterpolation = true
|
|
20
|
+
} = options;
|
|
21
|
+
|
|
22
|
+
let result = '';
|
|
23
|
+
let i = 0;
|
|
24
|
+
|
|
25
|
+
while (i < code.length) {
|
|
26
|
+
// Hash comment
|
|
27
|
+
if (hashComment && code[i] === '#') {
|
|
28
|
+
while (i < code.length && code[i] !== '\n') {
|
|
29
|
+
result += ' ';
|
|
30
|
+
i++;
|
|
31
|
+
}
|
|
32
|
+
continue;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
// Triple quotes
|
|
36
|
+
if (tripleQuote && (
|
|
37
|
+
(code[i] === "'" && code[i+1] === "'" && code[i+2] === "'") ||
|
|
38
|
+
(code[i] === '"' && code[i+1] === '"' && code[i+2] === '"')
|
|
39
|
+
)) {
|
|
40
|
+
const quote = code[i];
|
|
41
|
+
result += ' ';
|
|
42
|
+
i += 3;
|
|
43
|
+
while (i < code.length) {
|
|
44
|
+
if (code[i] === '\\') {
|
|
45
|
+
result += ' ';
|
|
46
|
+
i += 2;
|
|
47
|
+
continue;
|
|
48
|
+
}
|
|
49
|
+
if (code[i] === quote && code[i+1] === quote && code[i+2] === quote) {
|
|
50
|
+
result += ' ';
|
|
51
|
+
i += 3;
|
|
52
|
+
break;
|
|
53
|
+
}
|
|
54
|
+
result += code[i] === '\n' ? '\n' : ' ';
|
|
55
|
+
i++;
|
|
56
|
+
}
|
|
57
|
+
continue;
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
// Single-line comment //
|
|
61
|
+
if (!hashComment && code[i] === '/' && code[i + 1] === '/') {
|
|
62
|
+
while (i < code.length && code[i] !== '\n') {
|
|
63
|
+
result += ' ';
|
|
64
|
+
i++;
|
|
65
|
+
}
|
|
66
|
+
continue;
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
// Multi-line comment /* ... */
|
|
70
|
+
if (!hashComment && code[i] === '/' && code[i + 1] === '*') {
|
|
71
|
+
i += 2;
|
|
72
|
+
result += ' ';
|
|
73
|
+
while (i < code.length && !(code[i] === '*' && code[i + 1] === '/')) {
|
|
74
|
+
result += code[i] === '\n' ? '\n' : ' ';
|
|
75
|
+
i++;
|
|
76
|
+
}
|
|
77
|
+
if (i < code.length) { result += ' '; i += 2; }
|
|
78
|
+
continue;
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
// String literals
|
|
82
|
+
if (code[i] === '"' || (singleQuote && code[i] === "'") || (backtick && code[i] === '`')) {
|
|
83
|
+
const quote = code[i];
|
|
84
|
+
result += ' ';
|
|
85
|
+
i++;
|
|
86
|
+
while (i < code.length) {
|
|
87
|
+
if (code[i] === '\\') {
|
|
88
|
+
result += ' ';
|
|
89
|
+
i += 2;
|
|
90
|
+
continue;
|
|
91
|
+
}
|
|
92
|
+
if (code[i] === quote) {
|
|
93
|
+
result += ' ';
|
|
94
|
+
i++;
|
|
95
|
+
break;
|
|
96
|
+
}
|
|
97
|
+
// Template literal: ${...} — keep the expression
|
|
98
|
+
if (templateInterpolation && quote === '`' && code[i] === '$' && code[i + 1] === '{') {
|
|
99
|
+
result += '${';
|
|
100
|
+
i += 2;
|
|
101
|
+
let depth = 1;
|
|
102
|
+
while (i < code.length && depth > 0) {
|
|
103
|
+
if (code[i] === '{') depth++;
|
|
104
|
+
if (code[i] === '}') depth--;
|
|
105
|
+
if (depth > 0) {
|
|
106
|
+
result += code[i] === '\n' ? '\n' : code[i];
|
|
107
|
+
} else {
|
|
108
|
+
result += '}';
|
|
109
|
+
}
|
|
110
|
+
i++;
|
|
111
|
+
}
|
|
112
|
+
continue;
|
|
113
|
+
}
|
|
114
|
+
result += code[i] === '\n' ? '\n' : ' ';
|
|
115
|
+
i++;
|
|
116
|
+
}
|
|
117
|
+
continue;
|
|
118
|
+
}
|
|
119
|
+
result += code[i];
|
|
120
|
+
i++;
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
return result;
|
|
124
|
+
}
|
package/src/mcp-server.js
CHANGED
|
@@ -6,8 +6,11 @@
|
|
|
6
6
|
* - Sends server→client requests (roots/list) to get workspace info
|
|
7
7
|
*/
|
|
8
8
|
|
|
9
|
+
import fs from 'fs';
|
|
10
|
+
import path from 'path';
|
|
11
|
+
import { fileURLToPath } from 'url';
|
|
9
12
|
import { TOOLS } from './tool-defs.js';
|
|
10
|
-
import { getSkeleton, getFocusZone, expand, deps, usages, invalidateCache } from './tools.js';
|
|
13
|
+
import { getSkeleton, getFocusZone, expand, deps, usages, invalidateCache, getCallChain } from './tools.js';
|
|
11
14
|
import { getPendingTests, markTestPassed, markTestFailed, getTestSummary, resetTestState } from './test-annotations.js';
|
|
12
15
|
import { getFilters, setFilters, addExcludes, removeExcludes, resetFilters } from './filters.js';
|
|
13
16
|
import { getInstructions } from './instructions.js';
|
|
@@ -23,6 +26,8 @@ import { getCustomRules, setCustomRule, checkCustomRules } from './custom-rules.
|
|
|
23
26
|
import { getFrameworkReference } from './framework-references.js';
|
|
24
27
|
import { setRoots, resolvePath } from './workspace.js';
|
|
25
28
|
|
|
29
|
+
const __dirname = path.dirname(fileURLToPath(import.meta.url));
|
|
30
|
+
|
|
26
31
|
/**
|
|
27
32
|
* Tool handlers registry
|
|
28
33
|
* Maps tool names to their handler functions
|
|
@@ -34,6 +39,7 @@ const TOOL_HANDLERS = {
|
|
|
34
39
|
expand: (args) => expand(args.symbol),
|
|
35
40
|
deps: (args) => deps(args.symbol),
|
|
36
41
|
usages: (args) => usages(args.symbol),
|
|
42
|
+
get_call_chain: (args) => getCallChain({ from: args.from, to: args.to, path: args.path ? resolvePath(args.path) : undefined }),
|
|
37
43
|
invalidate_cache: () => { invalidateCache(); return { success: true }; },
|
|
38
44
|
|
|
39
45
|
// Test Checklist Tools
|
|
@@ -51,6 +57,22 @@ const TOOL_HANDLERS = {
|
|
|
51
57
|
reset_filters: () => resetFilters(),
|
|
52
58
|
|
|
53
59
|
// Guidelines
|
|
60
|
+
get_usage_guide: (args) => {
|
|
61
|
+
try {
|
|
62
|
+
const guidePath = path.join(__dirname, '..', 'GUIDE.md');
|
|
63
|
+
const content = fs.readFileSync(guidePath, 'utf8');
|
|
64
|
+
if (!args.topic) return content;
|
|
65
|
+
const regex = new RegExp(`## ${args.topic}`, 'i');
|
|
66
|
+
const match = content.match(regex);
|
|
67
|
+
if (!match) return `Topic '${args.topic}' not found in guide.`;
|
|
68
|
+
const start = match.index;
|
|
69
|
+
let end = content.indexOf('\n## ', start + 1);
|
|
70
|
+
if (end === -1) end = content.length;
|
|
71
|
+
return content.substring(start, end).trim();
|
|
72
|
+
} catch (e) {
|
|
73
|
+
return `Failed to read usage guide: ${e.message}`;
|
|
74
|
+
}
|
|
75
|
+
},
|
|
54
76
|
get_agent_instructions: () => getInstructions(),
|
|
55
77
|
|
|
56
78
|
// Documentation
|
|
@@ -115,6 +137,13 @@ const RESPONSE_HINTS = {
|
|
|
115
137
|
'💡 Use usages() for cross-project reference search.',
|
|
116
138
|
],
|
|
117
139
|
|
|
140
|
+
get_call_chain: (result) => {
|
|
141
|
+
if (result.error) return [];
|
|
142
|
+
return [
|
|
143
|
+
'💡 Use expand() on intermediate steps to understand how data is passed along the chain.',
|
|
144
|
+
];
|
|
145
|
+
},
|
|
146
|
+
|
|
118
147
|
invalidate_cache: () => [
|
|
119
148
|
'✅ Cache cleared. Run get_skeleton() to rebuild the project graph.',
|
|
120
149
|
],
|
|
@@ -212,11 +241,47 @@ export function createServer(sendToClient) {
|
|
|
212
241
|
id,
|
|
213
242
|
result: {
|
|
214
243
|
protocolVersion: '2024-11-05',
|
|
215
|
-
capabilities: { tools: {} },
|
|
244
|
+
capabilities: { tools: {}, resources: {} },
|
|
216
245
|
serverInfo: { name: 'project-graph', version: '1.1.0' },
|
|
217
246
|
},
|
|
218
247
|
};
|
|
219
248
|
|
|
249
|
+
case 'resources/list':
|
|
250
|
+
return {
|
|
251
|
+
jsonrpc: '2.0',
|
|
252
|
+
id,
|
|
253
|
+
result: {
|
|
254
|
+
resources: [
|
|
255
|
+
{
|
|
256
|
+
uri: 'project-graph://guide',
|
|
257
|
+
name: 'Project Graph Usage Guide',
|
|
258
|
+
description: 'Comprehensive guide with workflows and examples',
|
|
259
|
+
mimeType: 'text/markdown',
|
|
260
|
+
},
|
|
261
|
+
],
|
|
262
|
+
},
|
|
263
|
+
};
|
|
264
|
+
|
|
265
|
+
case 'resources/read': {
|
|
266
|
+
if (params.uri !== 'project-graph://guide') {
|
|
267
|
+
return { jsonrpc: '2.0', id, error: { code: -32602, message: `Resource not found: ${params.uri}` } };
|
|
268
|
+
}
|
|
269
|
+
const content = fs.readFileSync(path.join(__dirname, '..', 'GUIDE.md'), 'utf8');
|
|
270
|
+
return {
|
|
271
|
+
jsonrpc: '2.0',
|
|
272
|
+
id,
|
|
273
|
+
result: {
|
|
274
|
+
contents: [
|
|
275
|
+
{
|
|
276
|
+
uri: 'project-graph://guide',
|
|
277
|
+
mimeType: 'text/markdown',
|
|
278
|
+
text: content,
|
|
279
|
+
},
|
|
280
|
+
],
|
|
281
|
+
},
|
|
282
|
+
};
|
|
283
|
+
}
|
|
284
|
+
|
|
220
285
|
case 'tools/list':
|
|
221
286
|
return { jsonrpc: '2.0', id, result: { tools: TOOLS } };
|
|
222
287
|
|
package/src/parser.js
CHANGED
|
@@ -8,6 +8,12 @@ import { join, relative, resolve } from 'path';
|
|
|
8
8
|
import { parse } from '../vendor/acorn.mjs';
|
|
9
9
|
import * as walk from '../vendor/walk.mjs';
|
|
10
10
|
import { shouldExcludeDir, shouldExcludeFile, parseGitignore } from './filters.js';
|
|
11
|
+
import { parseTypeScript } from './lang-typescript.js';
|
|
12
|
+
import { parsePython } from './lang-python.js';
|
|
13
|
+
import { parseGo } from './lang-go.js';
|
|
14
|
+
|
|
15
|
+
/** Supported source file extensions */
|
|
16
|
+
const SOURCE_EXTENSIONS = ['.js', '.ts', '.tsx', '.py', '.go'];
|
|
11
17
|
|
|
12
18
|
/**
|
|
13
19
|
* @typedef {Object} ClassInfo
|
|
@@ -239,7 +245,7 @@ export async function parseProject(dir) {
|
|
|
239
245
|
for (const file of files) {
|
|
240
246
|
const content = readFileSync(file, 'utf-8');
|
|
241
247
|
const relPath = relative(resolvedDir, file);
|
|
242
|
-
const parsed = await
|
|
248
|
+
const parsed = await parseFileByExtension(content, relPath);
|
|
243
249
|
|
|
244
250
|
result.files.push(relPath);
|
|
245
251
|
result.classes.push(...parsed.classes);
|
|
@@ -255,13 +261,46 @@ export async function parseProject(dir) {
|
|
|
255
261
|
return result;
|
|
256
262
|
}
|
|
257
263
|
|
|
264
|
+
/**
|
|
265
|
+
* Route file to appropriate parser based on extension.
|
|
266
|
+
* @param {string} code
|
|
267
|
+
* @param {string} filename
|
|
268
|
+
* @returns {Promise<ParseResult>}
|
|
269
|
+
*/
|
|
270
|
+
async function parseFileByExtension(code, filename) {
|
|
271
|
+
if (filename.endsWith('.py')) {
|
|
272
|
+
return parsePython(code, filename);
|
|
273
|
+
}
|
|
274
|
+
if (filename.endsWith('.go')) {
|
|
275
|
+
return parseGo(code, filename);
|
|
276
|
+
}
|
|
277
|
+
if (filename.endsWith('.ts') || filename.endsWith('.tsx')) {
|
|
278
|
+
return parseTypeScript(code, filename);
|
|
279
|
+
}
|
|
280
|
+
// Default: JS via Acorn
|
|
281
|
+
return parseFile(code, filename);
|
|
282
|
+
}
|
|
283
|
+
|
|
284
|
+
/**
|
|
285
|
+
* Check if file is a supported source file.
|
|
286
|
+
* @param {string} filename
|
|
287
|
+
* @returns {boolean}
|
|
288
|
+
*/
|
|
289
|
+
function isSourceFile(filename) {
|
|
290
|
+
// Exclude Symbiote.js presentation files
|
|
291
|
+
if (filename.endsWith('.css.js') || filename.endsWith('.tpl.js')) {
|
|
292
|
+
return false;
|
|
293
|
+
}
|
|
294
|
+
return SOURCE_EXTENSIONS.some(ext => filename.endsWith(ext));
|
|
295
|
+
}
|
|
296
|
+
|
|
258
297
|
/**
|
|
259
298
|
* Find all JS files recursively (uses filter configuration)
|
|
260
299
|
* @param {string} dir
|
|
261
300
|
* @param {string} [rootDir] - Root directory for relative path calculation
|
|
262
301
|
* @returns {string[]}
|
|
263
302
|
*/
|
|
264
|
-
function findJSFiles(dir, rootDir = dir) {
|
|
303
|
+
export function findJSFiles(dir, rootDir = dir) {
|
|
265
304
|
// Parse gitignore on first call
|
|
266
305
|
if (dir === rootDir) {
|
|
267
306
|
parseGitignore(rootDir);
|
|
@@ -279,7 +318,7 @@ function findJSFiles(dir, rootDir = dir) {
|
|
|
279
318
|
if (!shouldExcludeDir(entry, relativePath)) {
|
|
280
319
|
files.push(...findJSFiles(fullPath, rootDir));
|
|
281
320
|
}
|
|
282
|
-
} else if (
|
|
321
|
+
} else if (isSourceFile(entry)) {
|
|
283
322
|
if (!shouldExcludeFile(entry, relativePath)) {
|
|
284
323
|
files.push(fullPath);
|
|
285
324
|
}
|
package/src/server.js
CHANGED
|
File without changes
|
package/src/tool-defs.js
CHANGED
|
@@ -73,6 +73,19 @@ export const TOOLS = [
|
|
|
73
73
|
required: ['symbol'],
|
|
74
74
|
},
|
|
75
75
|
},
|
|
76
|
+
{
|
|
77
|
+
name: 'get_call_chain',
|
|
78
|
+
description: 'Find the shortest call chain from one function/class to another through the dependency graph.',
|
|
79
|
+
inputSchema: {
|
|
80
|
+
type: 'object',
|
|
81
|
+
properties: {
|
|
82
|
+
from: { type: 'string', description: 'Starting symbol (e.g., "authMiddleware")' },
|
|
83
|
+
to: { type: 'string', description: 'Target symbol (e.g., "renderDashboard")' },
|
|
84
|
+
path: { type: 'string', description: 'Path to scan (optional)' }
|
|
85
|
+
},
|
|
86
|
+
required: ['from', 'to'],
|
|
87
|
+
},
|
|
88
|
+
},
|
|
76
89
|
{
|
|
77
90
|
name: 'invalidate_cache',
|
|
78
91
|
description: 'Invalidate the cached graph. Use after making code changes.',
|
|
@@ -213,6 +226,26 @@ export const TOOLS = [
|
|
|
213
226
|
},
|
|
214
227
|
|
|
215
228
|
// Guidelines
|
|
229
|
+
{
|
|
230
|
+
name: 'get_usage_guide',
|
|
231
|
+
description: [
|
|
232
|
+
'Get the comprehensive usage guide for project-graph with examples and best practices.',
|
|
233
|
+
'Call this FIRST when planning how to analyze, navigate, or audit a codebase.',
|
|
234
|
+
'Returns practical examples and recommended workflow for each feature area.',
|
|
235
|
+
'',
|
|
236
|
+
'Available topics: navigation, analysis, testing, documentation, rules, workflow.',
|
|
237
|
+
'Omit topic to get the full guide.',
|
|
238
|
+
].join('\n'),
|
|
239
|
+
inputSchema: {
|
|
240
|
+
type: 'object',
|
|
241
|
+
properties: {
|
|
242
|
+
topic: {
|
|
243
|
+
type: 'string',
|
|
244
|
+
description: 'Optional topic filter: navigation, analysis, testing, documentation, rules, workflow',
|
|
245
|
+
},
|
|
246
|
+
},
|
|
247
|
+
},
|
|
248
|
+
},
|
|
216
249
|
{
|
|
217
250
|
name: 'get_agent_instructions',
|
|
218
251
|
description: 'Get coding guidelines, architectural standards, and JSDoc rules for this project.',
|