@rpcajr/smart-graph-indexer 1.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/.agents/rules/antigravity-rtk-rules.md +32 -0
- package/README.md +116 -0
- package/dist/graph.d.ts +32 -0
- package/dist/graph.js +86 -0
- package/dist/index.d.ts +2 -0
- package/dist/index.js +227 -0
- package/dist/mcp_server.d.ts +48 -0
- package/dist/mcp_server.js +433 -0
- package/dist/parser.d.ts +42 -0
- package/dist/parser.js +529 -0
- package/dist/stats.d.ts +43 -0
- package/dist/stats.js +146 -0
- package/dist/ui.d.ts +2 -0
- package/dist/ui.js +1003 -0
- package/dist/vector.d.ts +66 -0
- package/dist/vector.js +144 -0
- package/dist/wasm/tree-sitter-c.wasm +0 -0
- package/dist/wasm/tree-sitter-c_sharp.wasm +0 -0
- package/dist/wasm/tree-sitter-cpp.wasm +0 -0
- package/dist/wasm/tree-sitter-go.wasm +0 -0
- package/dist/wasm/tree-sitter-java.wasm +0 -0
- package/dist/wasm/tree-sitter-python.wasm +0 -0
- package/dist/wasm/tree-sitter-ruby.wasm +0 -0
- package/dist/wasm/tree-sitter-rust.wasm +0 -0
- package/dist/wasm/tree-sitter-typescript.wasm +0 -0
- package/dist/wasm/tree-sitter.wasm +0 -0
- package/dist/watcher.d.ts +36 -0
- package/dist/watcher.js +166 -0
- package/mcp-config-example.json +12 -0
- package/mcp_smart_indexer_implementation_spec.md +366 -0
- package/package.json +35 -0
- package/src/graph.ts +93 -0
- package/src/index.ts +216 -0
- package/src/mcp_server.ts +454 -0
- package/src/parser.ts +484 -0
- package/src/stats.ts +156 -0
- package/src/ui.ts +956 -0
- package/src/vector.ts +166 -0
- package/src/watcher.ts +144 -0
- package/test_project/App.java +16 -0
- package/test_project/BillingService.cs +31 -0
- package/test_project/auth.ts +18 -0
- package/test_project/config.ts +11 -0
- package/test_project/database.ts +21 -0
- package/test_project/index.ts +13 -0
- package/test_project/main.go +11 -0
- package/test_project/main.py +21 -0
- package/test_project/processor.py +12 -0
- package/test_project/utils.py +13 -0
- package/tsconfig.json +16 -0
- package/wasm/tree-sitter-c.wasm +0 -0
- package/wasm/tree-sitter-c_sharp.wasm +0 -0
- package/wasm/tree-sitter-cpp.wasm +0 -0
- package/wasm/tree-sitter-go.wasm +0 -0
- package/wasm/tree-sitter-java.wasm +0 -0
- package/wasm/tree-sitter-python.wasm +0 -0
- package/wasm/tree-sitter-ruby.wasm +0 -0
- package/wasm/tree-sitter-rust.wasm +0 -0
- package/wasm/tree-sitter-typescript.wasm +0 -0
- package/wasm/tree-sitter.wasm +0 -0
package/src/parser.ts
ADDED
|
@@ -0,0 +1,484 @@
|
|
|
1
|
+
import * as fs from 'fs';
|
|
2
|
+
import * as path from 'path';
|
|
3
|
+
import * as crypto from 'crypto';
|
|
4
|
+
import Parser from 'web-tree-sitter';
|
|
5
|
+
|
|
6
|
+
export interface SymbolSkeleton {
|
|
7
|
+
name: string;
|
|
8
|
+
kind: 'Class' | 'Struct' | 'Method' | 'Property' | 'Interface' | 'Function' | 'Enum' | 'Trait';
|
|
9
|
+
signature: string;
|
|
10
|
+
docstring?: string;
|
|
11
|
+
start_line: number; // 1-indexed
|
|
12
|
+
end_line: number; // 1-indexed
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
export interface FileSkeleton {
|
|
16
|
+
file_path: string;
|
|
17
|
+
file_hash: string;
|
|
18
|
+
symbols: SymbolSkeleton[];
|
|
19
|
+
dependencies: string[];
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
export class ASTParser {
|
|
23
|
+
private isInitialized = false;
|
|
24
|
+
private languages: Record<string, Parser.Language> = {};
|
|
25
|
+
|
|
26
|
+
constructor() {}
|
|
27
|
+
|
|
28
|
+
/**
|
|
29
|
+
* Initializes the web-tree-sitter parser and loads language WASM files.
|
|
30
|
+
*/
|
|
31
|
+
public async init(wasmDir: string) {
|
|
32
|
+
if (this.isInitialized) return;
|
|
33
|
+
|
|
34
|
+
await Parser.init({
|
|
35
|
+
locateFile: (filepath: string) => {
|
|
36
|
+
if (filepath === 'tree-sitter.wasm') {
|
|
37
|
+
return path.join(wasmDir, 'tree-sitter.wasm');
|
|
38
|
+
}
|
|
39
|
+
return filepath;
|
|
40
|
+
}
|
|
41
|
+
});
|
|
42
|
+
|
|
43
|
+
const langFiles = {
|
|
44
|
+
csharp: 'tree-sitter-c_sharp.wasm',
|
|
45
|
+
typescript: 'tree-sitter-typescript.wasm',
|
|
46
|
+
python: 'tree-sitter-python.wasm',
|
|
47
|
+
java: 'tree-sitter-java.wasm',
|
|
48
|
+
go: 'tree-sitter-go.wasm',
|
|
49
|
+
rust: 'tree-sitter-rust.wasm',
|
|
50
|
+
ruby: 'tree-sitter-ruby.wasm',
|
|
51
|
+
cpp: 'tree-sitter-cpp.wasm',
|
|
52
|
+
c: 'tree-sitter-c.wasm',
|
|
53
|
+
};
|
|
54
|
+
|
|
55
|
+
for (const [key, file] of Object.entries(langFiles)) {
|
|
56
|
+
const fullPath = path.join(wasmDir, file);
|
|
57
|
+
if (fs.existsSync(fullPath)) {
|
|
58
|
+
try {
|
|
59
|
+
this.languages[key] = await Parser.Language.load(fullPath);
|
|
60
|
+
} catch (err) {
|
|
61
|
+
console.error(`Failed to load grammar: ${file}`, err);
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
this.isInitialized = true;
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
/**
|
|
69
|
+
* Calculates MD5 hash of file content.
|
|
70
|
+
*/
|
|
71
|
+
public getHash(content: string): string {
|
|
72
|
+
return crypto.createHash('md5').update(content).digest('hex');
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
/**
|
|
76
|
+
* Parsers a file and extracts its skeleton.
|
|
77
|
+
*/
|
|
78
|
+
public parseFile(filePath: string, content: string): FileSkeleton {
|
|
79
|
+
const ext = path.extname(filePath).toLowerCase();
|
|
80
|
+
const file_hash = this.getHash(content);
|
|
81
|
+
const dependencies = this.extractDependencies(content, ext);
|
|
82
|
+
|
|
83
|
+
let symbols: SymbolSkeleton[] = [];
|
|
84
|
+
|
|
85
|
+
if (!this.isInitialized) {
|
|
86
|
+
return { file_path: filePath, file_hash, symbols, dependencies };
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
const parser = new Parser();
|
|
90
|
+
|
|
91
|
+
try {
|
|
92
|
+
if (ext === '.cs' && this.languages.csharp) {
|
|
93
|
+
parser.setLanguage(this.languages.csharp);
|
|
94
|
+
const tree = parser.parse(content);
|
|
95
|
+
symbols = this.parseCSharp(tree.rootNode, content);
|
|
96
|
+
} else if (['.ts', '.tsx', '.js', '.jsx'].includes(ext) && this.languages.typescript) {
|
|
97
|
+
parser.setLanguage(this.languages.typescript);
|
|
98
|
+
const tree = parser.parse(content);
|
|
99
|
+
symbols = this.parseTypeScript(tree.rootNode, content);
|
|
100
|
+
} else if (ext === '.py' && this.languages.python) {
|
|
101
|
+
parser.setLanguage(this.languages.python);
|
|
102
|
+
const tree = parser.parse(content);
|
|
103
|
+
symbols = this.parsePython(tree.rootNode, content);
|
|
104
|
+
} else if (ext === '.java' && this.languages.java) {
|
|
105
|
+
parser.setLanguage(this.languages.java);
|
|
106
|
+
const tree = parser.parse(content);
|
|
107
|
+
symbols = this.parseJava(tree.rootNode, content);
|
|
108
|
+
} else if (ext === '.go' && this.languages.go) {
|
|
109
|
+
parser.setLanguage(this.languages.go);
|
|
110
|
+
const tree = parser.parse(content);
|
|
111
|
+
symbols = this.parseGo(tree.rootNode, content);
|
|
112
|
+
} else if (ext === '.rs' && this.languages.rust) {
|
|
113
|
+
parser.setLanguage(this.languages.rust);
|
|
114
|
+
const tree = parser.parse(content);
|
|
115
|
+
symbols = this.parseRust(tree.rootNode, content);
|
|
116
|
+
} else if (ext === '.rb' && this.languages.ruby) {
|
|
117
|
+
parser.setLanguage(this.languages.ruby);
|
|
118
|
+
const tree = parser.parse(content);
|
|
119
|
+
symbols = this.parseRuby(tree.rootNode, content);
|
|
120
|
+
} else if (['.cpp', '.hpp', '.cc', '.cxx', '.h', '.c'].includes(ext)) {
|
|
121
|
+
const lang = ['.c', '.h'].includes(ext) ? this.languages.c : this.languages.cpp;
|
|
122
|
+
if (lang) {
|
|
123
|
+
parser.setLanguage(lang);
|
|
124
|
+
const tree = parser.parse(content);
|
|
125
|
+
symbols = this.parseCpp(tree.rootNode, content);
|
|
126
|
+
}
|
|
127
|
+
}
|
|
128
|
+
} catch (err) {
|
|
129
|
+
console.error(`Error parsing AST for ${filePath}:`, err);
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
return {
|
|
133
|
+
file_path: filePath,
|
|
134
|
+
file_hash,
|
|
135
|
+
symbols,
|
|
136
|
+
dependencies,
|
|
137
|
+
};
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
private extractDependencies(content: string, ext: string): string[] {
|
|
141
|
+
const deps = new Set<string>();
|
|
142
|
+
if (ext === '.cs') {
|
|
143
|
+
const match = content.matchAll(/^\s*using\s+([^;=]+);/gm);
|
|
144
|
+
for (const m of match) deps.add(m[1].trim());
|
|
145
|
+
} else if (['.ts', '.tsx', '.js', '.jsx'].includes(ext)) {
|
|
146
|
+
const importMatch = content.matchAll(/import\s+(?:[^'"]+\s+from\s+)?['"]([^'"]+)['"]/g);
|
|
147
|
+
for (const m of importMatch) deps.add(m[1]);
|
|
148
|
+
const requireMatch = content.matchAll(/(?:const|let|var)\s+.*\s*=\s*require\(['"]([^'"]+)['"]\)/g);
|
|
149
|
+
for (const m of requireMatch) deps.add(m[1]);
|
|
150
|
+
} else if (ext === '.py') {
|
|
151
|
+
const importMatch = content.matchAll(/^\s*import\s+([^\s\n#]+)/gm);
|
|
152
|
+
for (const m of importMatch) deps.add(m[1].trim());
|
|
153
|
+
const fromMatch = content.matchAll(/^\s*from\s+([^\s\n#]+)\s+import/gm);
|
|
154
|
+
for (const m of fromMatch) deps.add(m[1].trim());
|
|
155
|
+
} else if (ext === '.java') {
|
|
156
|
+
const match = content.matchAll(/^\s*import\s+([^;]+);/gm);
|
|
157
|
+
for (const m of match) deps.add(m[1].trim());
|
|
158
|
+
} else if (ext === '.go') {
|
|
159
|
+
const match = content.matchAll(/^\s*import\s+(?:\([^)]+\)|"[^"]+")/g);
|
|
160
|
+
for (const m of match) {
|
|
161
|
+
const inner = m[0].match(/"([^"]+)"/g);
|
|
162
|
+
if (inner) {
|
|
163
|
+
for (const item of inner) deps.add(item.replace(/"/g, ''));
|
|
164
|
+
}
|
|
165
|
+
}
|
|
166
|
+
} else if (ext === '.rs') {
|
|
167
|
+
const match = content.matchAll(/^\s*use\s+([^;]+);/gm);
|
|
168
|
+
for (const m of match) deps.add(m[1].trim());
|
|
169
|
+
} else if (ext === '.rb') {
|
|
170
|
+
const match = content.matchAll(/^\s*(?:require|require_relative)\s+['"]([^'"]+)['"]/gm);
|
|
171
|
+
for (const m of match) deps.add(m[1].trim());
|
|
172
|
+
} else if (['.cpp', '.hpp', '.cc', '.cxx', '.h', '.c'].includes(ext)) {
|
|
173
|
+
const match = content.matchAll(/^\s*#\s*include\s+([<"][^>"]+[>"])/gm);
|
|
174
|
+
for (const m of match) deps.add(m[1].trim());
|
|
175
|
+
}
|
|
176
|
+
return Array.from(deps);
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
private getPrecedingDocstring(node: Parser.SyntaxNode, content: string): string | undefined {
|
|
180
|
+
const startLine = node.startPosition.row;
|
|
181
|
+
const lines = content.split('\n');
|
|
182
|
+
let lineIdx = startLine - 1;
|
|
183
|
+
const comments: string[] = [];
|
|
184
|
+
|
|
185
|
+
while (lineIdx >= 0) {
|
|
186
|
+
const currentLine = lines[lineIdx].trim();
|
|
187
|
+
if (currentLine.startsWith('///') || currentLine.startsWith('//') || currentLine.startsWith('/*') || currentLine.startsWith('*') || currentLine.endsWith('*/') || currentLine.startsWith('#')) {
|
|
188
|
+
comments.unshift(lines[lineIdx].trim());
|
|
189
|
+
lineIdx--;
|
|
190
|
+
} else if (currentLine === '') {
|
|
191
|
+
lineIdx--;
|
|
192
|
+
} else {
|
|
193
|
+
break;
|
|
194
|
+
}
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
if (comments.length > 0) {
|
|
198
|
+
return comments.join('\n');
|
|
199
|
+
}
|
|
200
|
+
return undefined;
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
private parseCSharp(root: Parser.SyntaxNode, content: string): SymbolSkeleton[] {
|
|
204
|
+
const symbols: SymbolSkeleton[] = [];
|
|
205
|
+
const traverse = (node: Parser.SyntaxNode) => {
|
|
206
|
+
const type = node.type;
|
|
207
|
+
if (type === 'class_declaration' || type === 'interface_declaration' || type === 'struct_declaration') {
|
|
208
|
+
const nameNode = node.childForFieldName('name');
|
|
209
|
+
const name = nameNode ? nameNode.text : 'Unknown';
|
|
210
|
+
const kind = type === 'class_declaration' ? 'Class' : type === 'interface_declaration' ? 'Interface' : 'Struct';
|
|
211
|
+
const bodyNode = node.childForFieldName('body') || node.lastChild;
|
|
212
|
+
const sigEnd = bodyNode && bodyNode.type === 'declaration_list' ? bodyNode.startIndex : node.endIndex;
|
|
213
|
+
symbols.push({
|
|
214
|
+
name, kind,
|
|
215
|
+
signature: content.substring(node.startIndex, sigEnd).trim(),
|
|
216
|
+
docstring: this.getPrecedingDocstring(node, content),
|
|
217
|
+
start_line: node.startPosition.row + 1,
|
|
218
|
+
end_line: node.endPosition.row + 1,
|
|
219
|
+
});
|
|
220
|
+
} else if (type === 'method_declaration') {
|
|
221
|
+
const nameNode = node.childForFieldName('name');
|
|
222
|
+
const name = nameNode ? nameNode.text : 'Unknown';
|
|
223
|
+
const bodyNode = node.childForFieldName('body');
|
|
224
|
+
const sigEnd = bodyNode ? bodyNode.startIndex : node.endIndex;
|
|
225
|
+
symbols.push({
|
|
226
|
+
name, kind: 'Method',
|
|
227
|
+
signature: content.substring(node.startIndex, sigEnd).trim(),
|
|
228
|
+
docstring: this.getPrecedingDocstring(node, content),
|
|
229
|
+
start_line: node.startPosition.row + 1,
|
|
230
|
+
end_line: node.endPosition.row + 1,
|
|
231
|
+
});
|
|
232
|
+
}
|
|
233
|
+
for (let i = 0; i < node.childCount; i++) traverse(node.child(i)!);
|
|
234
|
+
};
|
|
235
|
+
traverse(root);
|
|
236
|
+
return symbols;
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
private parseTypeScript(root: Parser.SyntaxNode, content: string): SymbolSkeleton[] {
|
|
240
|
+
const symbols: SymbolSkeleton[] = [];
|
|
241
|
+
const traverse = (node: Parser.SyntaxNode) => {
|
|
242
|
+
const type = node.type;
|
|
243
|
+
if (type === 'class_declaration' || type === 'interface_declaration') {
|
|
244
|
+
const nameNode = node.childForFieldName('name');
|
|
245
|
+
const name = nameNode ? nameNode.text : 'Unknown';
|
|
246
|
+
const kind = type === 'class_declaration' ? 'Class' : 'Interface';
|
|
247
|
+
const bodyNode = node.childForFieldName('body') || node.lastChild;
|
|
248
|
+
const sigEnd = bodyNode && (bodyNode.type === 'class_body' || bodyNode.type === 'object_type') ? bodyNode.startIndex : node.endIndex;
|
|
249
|
+
symbols.push({
|
|
250
|
+
name, kind,
|
|
251
|
+
signature: content.substring(node.startIndex, sigEnd).trim(),
|
|
252
|
+
docstring: this.getPrecedingDocstring(node, content),
|
|
253
|
+
start_line: node.startPosition.row + 1,
|
|
254
|
+
end_line: node.endPosition.row + 1,
|
|
255
|
+
});
|
|
256
|
+
} else if (type === 'method_definition' || type === 'function_declaration') {
|
|
257
|
+
const nameNode = node.childForFieldName('name');
|
|
258
|
+
const name = nameNode ? nameNode.text : 'Unknown';
|
|
259
|
+
const bodyNode = node.childForFieldName('body');
|
|
260
|
+
const sigEnd = bodyNode ? bodyNode.startIndex : node.endIndex;
|
|
261
|
+
symbols.push({
|
|
262
|
+
name, kind: type === 'method_definition' ? 'Method' : 'Function',
|
|
263
|
+
signature: content.substring(node.startIndex, sigEnd).trim(),
|
|
264
|
+
docstring: this.getPrecedingDocstring(node, content),
|
|
265
|
+
start_line: node.startPosition.row + 1,
|
|
266
|
+
end_line: node.endPosition.row + 1,
|
|
267
|
+
});
|
|
268
|
+
}
|
|
269
|
+
for (let i = 0; i < node.childCount; i++) traverse(node.child(i)!);
|
|
270
|
+
};
|
|
271
|
+
traverse(root);
|
|
272
|
+
return symbols;
|
|
273
|
+
}
|
|
274
|
+
|
|
275
|
+
private parsePython(root: Parser.SyntaxNode, content: string): SymbolSkeleton[] {
|
|
276
|
+
const symbols: SymbolSkeleton[] = [];
|
|
277
|
+
const traverse = (node: Parser.SyntaxNode) => {
|
|
278
|
+
const type = node.type;
|
|
279
|
+
if (type === 'class_definition' || type === 'function_definition') {
|
|
280
|
+
const nameNode = node.childForFieldName('name');
|
|
281
|
+
const name = nameNode ? nameNode.text : 'Unknown';
|
|
282
|
+
const bodyNode = node.childForFieldName('body');
|
|
283
|
+
const sigEnd = bodyNode ? bodyNode.startIndex : node.endIndex;
|
|
284
|
+
symbols.push({
|
|
285
|
+
name, kind: type === 'class_definition' ? 'Class' : 'Function',
|
|
286
|
+
signature: content.substring(node.startIndex, sigEnd).trim(),
|
|
287
|
+
docstring: this.getPythonDocstring(bodyNode, content),
|
|
288
|
+
start_line: node.startPosition.row + 1,
|
|
289
|
+
end_line: node.endPosition.row + 1,
|
|
290
|
+
});
|
|
291
|
+
}
|
|
292
|
+
for (let i = 0; i < node.childCount; i++) traverse(node.child(i)!);
|
|
293
|
+
};
|
|
294
|
+
traverse(root);
|
|
295
|
+
return symbols;
|
|
296
|
+
}
|
|
297
|
+
|
|
298
|
+
private getPythonDocstring(bodyNode: Parser.SyntaxNode | null, content: string): string | undefined {
|
|
299
|
+
if (!bodyNode || bodyNode.childCount === 0) return undefined;
|
|
300
|
+
const firstStmt = bodyNode.child(0);
|
|
301
|
+
if (firstStmt && firstStmt.type === 'expression_statement') {
|
|
302
|
+
const stringNode = firstStmt.child(0);
|
|
303
|
+
if (stringNode && stringNode.type === 'string') {
|
|
304
|
+
let text = stringNode.text.trim();
|
|
305
|
+
if (text.startsWith('"""') && text.endsWith('"""')) return text.substring(3, text.length - 3).trim();
|
|
306
|
+
if (text.startsWith("'''") && text.endsWith("'''")) return text.substring(3, text.length - 3).trim();
|
|
307
|
+
return text;
|
|
308
|
+
}
|
|
309
|
+
}
|
|
310
|
+
return undefined;
|
|
311
|
+
}
|
|
312
|
+
|
|
313
|
+
private parseJava(root: Parser.SyntaxNode, content: string): SymbolSkeleton[] {
|
|
314
|
+
const symbols: SymbolSkeleton[] = [];
|
|
315
|
+
const traverse = (node: Parser.SyntaxNode) => {
|
|
316
|
+
const type = node.type;
|
|
317
|
+
if (type === 'class_declaration' || type === 'interface_declaration') {
|
|
318
|
+
const nameNode = node.childForFieldName('name');
|
|
319
|
+
const name = nameNode ? nameNode.text : 'Unknown';
|
|
320
|
+
const bodyNode = node.childForFieldName('body');
|
|
321
|
+
const sigEnd = bodyNode ? bodyNode.startIndex : node.endIndex;
|
|
322
|
+
symbols.push({
|
|
323
|
+
name, kind: type === 'class_declaration' ? 'Class' : 'Interface',
|
|
324
|
+
signature: content.substring(node.startIndex, sigEnd).trim(),
|
|
325
|
+
docstring: this.getPrecedingDocstring(node, content),
|
|
326
|
+
start_line: node.startPosition.row + 1,
|
|
327
|
+
end_line: node.endPosition.row + 1,
|
|
328
|
+
});
|
|
329
|
+
} else if (type === 'method_declaration') {
|
|
330
|
+
const nameNode = node.childForFieldName('name');
|
|
331
|
+
const name = nameNode ? nameNode.text : 'Unknown';
|
|
332
|
+
const bodyNode = node.childForFieldName('body');
|
|
333
|
+
const sigEnd = bodyNode ? bodyNode.startIndex : node.endIndex;
|
|
334
|
+
symbols.push({
|
|
335
|
+
name, kind: 'Method',
|
|
336
|
+
signature: content.substring(node.startIndex, sigEnd).trim(),
|
|
337
|
+
docstring: this.getPrecedingDocstring(node, content),
|
|
338
|
+
start_line: node.startPosition.row + 1,
|
|
339
|
+
end_line: node.endPosition.row + 1,
|
|
340
|
+
});
|
|
341
|
+
}
|
|
342
|
+
for (let i = 0; i < node.childCount; i++) traverse(node.child(i)!);
|
|
343
|
+
};
|
|
344
|
+
traverse(root);
|
|
345
|
+
return symbols;
|
|
346
|
+
}
|
|
347
|
+
|
|
348
|
+
private parseGo(root: Parser.SyntaxNode, content: string): SymbolSkeleton[] {
|
|
349
|
+
const symbols: SymbolSkeleton[] = [];
|
|
350
|
+
const traverse = (node: Parser.SyntaxNode) => {
|
|
351
|
+
const type = node.type;
|
|
352
|
+
if (type === 'type_spec') {
|
|
353
|
+
const nameNode = node.childForFieldName('name');
|
|
354
|
+
const name = nameNode ? nameNode.text : 'Unknown';
|
|
355
|
+
const typeNode = node.childForFieldName('type');
|
|
356
|
+
const kind = typeNode && typeNode.type === 'interface_type' ? 'Interface' : 'Struct';
|
|
357
|
+
symbols.push({
|
|
358
|
+
name, kind,
|
|
359
|
+
signature: content.substring(node.startIndex, node.endIndex).trim(),
|
|
360
|
+
docstring: this.getPrecedingDocstring(node, content),
|
|
361
|
+
start_line: node.startPosition.row + 1,
|
|
362
|
+
end_line: node.endPosition.row + 1,
|
|
363
|
+
});
|
|
364
|
+
} else if (type === 'function_declaration' || type === 'method_declaration') {
|
|
365
|
+
const nameNode = node.childForFieldName('name');
|
|
366
|
+
const name = nameNode ? nameNode.text : 'Unknown';
|
|
367
|
+
const bodyNode = node.childForFieldName('body');
|
|
368
|
+
const sigEnd = bodyNode ? bodyNode.startIndex : node.endIndex;
|
|
369
|
+
symbols.push({
|
|
370
|
+
name, kind: type === 'method_declaration' ? 'Method' : 'Function',
|
|
371
|
+
signature: content.substring(node.startIndex, sigEnd).trim(),
|
|
372
|
+
docstring: this.getPrecedingDocstring(node, content),
|
|
373
|
+
start_line: node.startPosition.row + 1,
|
|
374
|
+
end_line: node.endPosition.row + 1,
|
|
375
|
+
});
|
|
376
|
+
}
|
|
377
|
+
for (let i = 0; i < node.childCount; i++) traverse(node.child(i)!);
|
|
378
|
+
};
|
|
379
|
+
traverse(root);
|
|
380
|
+
return symbols;
|
|
381
|
+
}
|
|
382
|
+
|
|
383
|
+
private parseRust(root: Parser.SyntaxNode, content: string): SymbolSkeleton[] {
|
|
384
|
+
const symbols: SymbolSkeleton[] = [];
|
|
385
|
+
const traverse = (node: Parser.SyntaxNode) => {
|
|
386
|
+
const type = node.type;
|
|
387
|
+
if (type === 'struct_item' || type === 'enum_item' || type === 'trait_item') {
|
|
388
|
+
const nameNode = node.childForFieldName('name');
|
|
389
|
+
const name = nameNode ? nameNode.text : 'Unknown';
|
|
390
|
+
const kind = type === 'struct_item' ? 'Struct' : type === 'enum_item' ? 'Enum' : 'Trait';
|
|
391
|
+
const bodyNode = node.childForFieldName('body') || node.lastChild;
|
|
392
|
+
const sigEnd = bodyNode ? bodyNode.startIndex : node.endIndex;
|
|
393
|
+
symbols.push({
|
|
394
|
+
name, kind,
|
|
395
|
+
signature: content.substring(node.startIndex, sigEnd).trim(),
|
|
396
|
+
docstring: this.getPrecedingDocstring(node, content),
|
|
397
|
+
start_line: node.startPosition.row + 1,
|
|
398
|
+
end_line: node.endPosition.row + 1,
|
|
399
|
+
});
|
|
400
|
+
} else if (type === 'function_item') {
|
|
401
|
+
const nameNode = node.childForFieldName('name');
|
|
402
|
+
const name = nameNode ? nameNode.text : 'Unknown';
|
|
403
|
+
const bodyNode = node.childForFieldName('body');
|
|
404
|
+
const sigEnd = bodyNode ? bodyNode.startIndex : node.endIndex;
|
|
405
|
+
symbols.push({
|
|
406
|
+
name, kind: 'Function',
|
|
407
|
+
signature: content.substring(node.startIndex, sigEnd).trim(),
|
|
408
|
+
docstring: this.getPrecedingDocstring(node, content),
|
|
409
|
+
start_line: node.startPosition.row + 1,
|
|
410
|
+
end_line: node.endPosition.row + 1,
|
|
411
|
+
});
|
|
412
|
+
}
|
|
413
|
+
for (let i = 0; i < node.childCount; i++) traverse(node.child(i)!);
|
|
414
|
+
};
|
|
415
|
+
traverse(root);
|
|
416
|
+
return symbols;
|
|
417
|
+
}
|
|
418
|
+
|
|
419
|
+
private parseRuby(root: Parser.SyntaxNode, content: string): SymbolSkeleton[] {
|
|
420
|
+
const symbols: SymbolSkeleton[] = [];
|
|
421
|
+
const traverse = (node: Parser.SyntaxNode) => {
|
|
422
|
+
const type = node.type;
|
|
423
|
+
if (type === 'class' || type === 'module') {
|
|
424
|
+
const nameNode = node.childForFieldName('name');
|
|
425
|
+
const name = nameNode ? nameNode.text : 'Unknown';
|
|
426
|
+
symbols.push({
|
|
427
|
+
name, kind: type === 'class' ? 'Class' : 'Interface',
|
|
428
|
+
signature: `${type} ${name}`,
|
|
429
|
+
docstring: this.getPrecedingDocstring(node, content),
|
|
430
|
+
start_line: node.startPosition.row + 1,
|
|
431
|
+
end_line: node.endPosition.row + 1,
|
|
432
|
+
});
|
|
433
|
+
} else if (type === 'method') {
|
|
434
|
+
const nameNode = node.childForFieldName('name');
|
|
435
|
+
const name = nameNode ? nameNode.text : 'Unknown';
|
|
436
|
+
symbols.push({
|
|
437
|
+
name, kind: 'Method',
|
|
438
|
+
signature: `def ${name}`,
|
|
439
|
+
docstring: this.getPrecedingDocstring(node, content),
|
|
440
|
+
start_line: node.startPosition.row + 1,
|
|
441
|
+
end_line: node.endPosition.row + 1,
|
|
442
|
+
});
|
|
443
|
+
}
|
|
444
|
+
for (let i = 0; i < node.childCount; i++) traverse(node.child(i)!);
|
|
445
|
+
};
|
|
446
|
+
traverse(root);
|
|
447
|
+
return symbols;
|
|
448
|
+
}
|
|
449
|
+
|
|
450
|
+
private parseCpp(root: Parser.SyntaxNode, content: string): SymbolSkeleton[] {
|
|
451
|
+
const symbols: SymbolSkeleton[] = [];
|
|
452
|
+
const traverse = (node: Parser.SyntaxNode) => {
|
|
453
|
+
const type = node.type;
|
|
454
|
+
if (type === 'class_specifier' || type === 'struct_specifier') {
|
|
455
|
+
const nameNode = node.childForFieldName('name');
|
|
456
|
+
const name = nameNode ? nameNode.text : 'Unknown';
|
|
457
|
+
const bodyNode = node.childForFieldName('body');
|
|
458
|
+
const sigEnd = bodyNode ? bodyNode.startIndex : node.endIndex;
|
|
459
|
+
symbols.push({
|
|
460
|
+
name, kind: type === 'class_specifier' ? 'Class' : 'Struct',
|
|
461
|
+
signature: content.substring(node.startIndex, sigEnd).trim(),
|
|
462
|
+
docstring: this.getPrecedingDocstring(node, content),
|
|
463
|
+
start_line: node.startPosition.row + 1,
|
|
464
|
+
end_line: node.endPosition.row + 1,
|
|
465
|
+
});
|
|
466
|
+
} else if (type === 'function_definition') {
|
|
467
|
+
const declarator = node.childForFieldName('declarator');
|
|
468
|
+
const name = declarator ? declarator.text : 'Unknown';
|
|
469
|
+
const bodyNode = node.childForFieldName('body');
|
|
470
|
+
const sigEnd = bodyNode ? bodyNode.startIndex : node.endIndex;
|
|
471
|
+
symbols.push({
|
|
472
|
+
name, kind: 'Function',
|
|
473
|
+
signature: content.substring(node.startIndex, sigEnd).trim(),
|
|
474
|
+
docstring: this.getPrecedingDocstring(node, content),
|
|
475
|
+
start_line: node.startPosition.row + 1,
|
|
476
|
+
end_line: node.endPosition.row + 1,
|
|
477
|
+
});
|
|
478
|
+
}
|
|
479
|
+
for (let i = 0; i < node.childCount; i++) traverse(node.child(i)!);
|
|
480
|
+
};
|
|
481
|
+
traverse(root);
|
|
482
|
+
return symbols;
|
|
483
|
+
}
|
|
484
|
+
}
|
package/src/stats.ts
ADDED
|
@@ -0,0 +1,156 @@
|
|
|
1
|
+
import * as fs from 'fs';
|
|
2
|
+
import * as path from 'path';
|
|
3
|
+
|
|
4
|
+
export interface ToolCallStats {
|
|
5
|
+
calls: number;
|
|
6
|
+
saved: number;
|
|
7
|
+
actual: number;
|
|
8
|
+
alternative: number;
|
|
9
|
+
totalTimeMs: number;
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
export interface HistoryEntry {
|
|
13
|
+
timestamp: string; // ISO string
|
|
14
|
+
sessionId: string; // timestamp of session start
|
|
15
|
+
toolName: string;
|
|
16
|
+
saved: number;
|
|
17
|
+
actual: number;
|
|
18
|
+
alternative: number;
|
|
19
|
+
target?: string; // file path or search query target
|
|
20
|
+
execTimeMs: number; // execution time in milliseconds
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
export interface TokenStats {
|
|
24
|
+
totalCalls: number;
|
|
25
|
+
totalTokensSaved: number;
|
|
26
|
+
totalTokensActual: number;
|
|
27
|
+
totalTokensAlternative: number;
|
|
28
|
+
efficiencyPercentage: number;
|
|
29
|
+
callsByType: Record<string, ToolCallStats>;
|
|
30
|
+
history: HistoryEntry[];
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
export class StatsManager {
|
|
34
|
+
private statsPath: string;
|
|
35
|
+
private stats: TokenStats;
|
|
36
|
+
private currentSessionId: string;
|
|
37
|
+
|
|
38
|
+
constructor(projectPath: string) {
|
|
39
|
+
const mcpDir = path.join(path.resolve(projectPath), '.graph-indexer');
|
|
40
|
+
this.statsPath = path.join(mcpDir, 'stats.json');
|
|
41
|
+
this.currentSessionId = new Date().toISOString();
|
|
42
|
+
this.stats = this.loadStats();
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
private loadStats(): TokenStats {
|
|
46
|
+
if (fs.existsSync(this.statsPath)) {
|
|
47
|
+
try {
|
|
48
|
+
const raw = fs.readFileSync(this.statsPath, 'utf-8');
|
|
49
|
+
const parsed = JSON.parse(raw);
|
|
50
|
+
if (parsed && parsed.totalCalls !== undefined) {
|
|
51
|
+
if (!parsed.history) {
|
|
52
|
+
parsed.history = [];
|
|
53
|
+
}
|
|
54
|
+
parsed.efficiencyPercentage = parsed.totalTokensAlternative > 0
|
|
55
|
+
? Math.round((parsed.totalTokensSaved / parsed.totalTokensAlternative) * 100)
|
|
56
|
+
: 0;
|
|
57
|
+
return parsed;
|
|
58
|
+
}
|
|
59
|
+
} catch (err) {
|
|
60
|
+
console.error('Failed to load stats, resetting...', err);
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
return {
|
|
64
|
+
totalCalls: 0,
|
|
65
|
+
totalTokensSaved: 0,
|
|
66
|
+
totalTokensActual: 0,
|
|
67
|
+
totalTokensAlternative: 0,
|
|
68
|
+
efficiencyPercentage: 0,
|
|
69
|
+
callsByType: {
|
|
70
|
+
search_symbols: { calls: 0, saved: 0, actual: 0, alternative: 0, totalTimeMs: 0 },
|
|
71
|
+
get_file_skeleton: { calls: 0, saved: 0, actual: 0, alternative: 0, totalTimeMs: 0 },
|
|
72
|
+
get_dependencies: { calls: 0, saved: 0, actual: 0, alternative: 0, totalTimeMs: 0 },
|
|
73
|
+
read_symbol_body: { calls: 0, saved: 0, actual: 0, alternative: 0, totalTimeMs: 0 }
|
|
74
|
+
},
|
|
75
|
+
history: []
|
|
76
|
+
};
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
private saveStats() {
|
|
80
|
+
try {
|
|
81
|
+
const mcpDir = path.dirname(this.statsPath);
|
|
82
|
+
if (!fs.existsSync(mcpDir)) {
|
|
83
|
+
fs.mkdirSync(mcpDir, { recursive: true });
|
|
84
|
+
}
|
|
85
|
+
fs.writeFileSync(this.statsPath, JSON.stringify(this.stats, null, 2), 'utf-8');
|
|
86
|
+
} catch (err) {
|
|
87
|
+
console.error('Failed to save stats:', err);
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
/**
|
|
92
|
+
* Estimates tokens based on character count (1 token approx 4 characters).
|
|
93
|
+
*/
|
|
94
|
+
public estimateTokens(text: string): number {
|
|
95
|
+
return Math.ceil(text.length / 4);
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
/**
|
|
99
|
+
* Records a tool call and computes the token savings.
|
|
100
|
+
*/
|
|
101
|
+
public recordCall(
|
|
102
|
+
toolName: string,
|
|
103
|
+
actualPayload: string,
|
|
104
|
+
alternativeText: string,
|
|
105
|
+
target?: string,
|
|
106
|
+
execTimeMs: number = 0
|
|
107
|
+
) {
|
|
108
|
+
const actualTokens = this.estimateTokens(actualPayload);
|
|
109
|
+
const alternativeTokens = this.estimateTokens(alternativeText);
|
|
110
|
+
const savedTokens = Math.max(0, alternativeTokens - actualTokens);
|
|
111
|
+
|
|
112
|
+
this.stats.totalCalls += 1;
|
|
113
|
+
this.stats.totalTokensActual += actualTokens;
|
|
114
|
+
this.stats.totalTokensAlternative += alternativeTokens;
|
|
115
|
+
this.stats.totalTokensSaved += savedTokens;
|
|
116
|
+
|
|
117
|
+
// Calculate overall efficiency percentage
|
|
118
|
+
this.stats.efficiencyPercentage = this.stats.totalTokensAlternative > 0
|
|
119
|
+
? Math.round((this.stats.totalTokensSaved / this.stats.totalTokensAlternative) * 100)
|
|
120
|
+
: 0;
|
|
121
|
+
|
|
122
|
+
if (!this.stats.callsByType[toolName]) {
|
|
123
|
+
this.stats.callsByType[toolName] = { calls: 0, saved: 0, actual: 0, alternative: 0, totalTimeMs: 0 };
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
const toolStats = this.stats.callsByType[toolName];
|
|
127
|
+
toolStats.calls += 1;
|
|
128
|
+
toolStats.saved += savedTokens;
|
|
129
|
+
toolStats.actual += actualTokens;
|
|
130
|
+
toolStats.alternative += alternativeTokens;
|
|
131
|
+
toolStats.totalTimeMs += execTimeMs;
|
|
132
|
+
|
|
133
|
+
// Add entry to history
|
|
134
|
+
this.stats.history.unshift({
|
|
135
|
+
timestamp: new Date().toISOString(),
|
|
136
|
+
sessionId: this.currentSessionId,
|
|
137
|
+
toolName,
|
|
138
|
+
saved: savedTokens,
|
|
139
|
+
actual: actualTokens,
|
|
140
|
+
alternative: alternativeTokens,
|
|
141
|
+
target,
|
|
142
|
+
execTimeMs
|
|
143
|
+
});
|
|
144
|
+
|
|
145
|
+
// Cap history to 500 entries to avoid file bloat
|
|
146
|
+
if (this.stats.history.length > 500) {
|
|
147
|
+
this.stats.history = this.stats.history.slice(0, 500);
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
this.saveStats();
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
public getStats(): TokenStats {
|
|
154
|
+
return this.stats;
|
|
155
|
+
}
|
|
156
|
+
}
|