@mr-jones123/toji 0.1.1

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.
Files changed (42) hide show
  1. package/README.md +158 -0
  2. package/package.json +47 -0
  3. package/packages/toji-comms/README.md +71 -0
  4. package/packages/toji-comms/src/cli/agents.ts +121 -0
  5. package/packages/toji-comms/src/cli/mmx.ts +65 -0
  6. package/packages/toji-comms/src/cli/subprocess.ts +47 -0
  7. package/packages/toji-comms/src/comms/orchestrator.ts +92 -0
  8. package/packages/toji-comms/src/comms/prompt.ts +84 -0
  9. package/packages/toji-comms/src/comms/store.ts +145 -0
  10. package/packages/toji-comms/src/comms/types.ts +94 -0
  11. package/packages/toji-comms/src/db/connection.ts +58 -0
  12. package/packages/toji-comms/src/db/migrations.ts +69 -0
  13. package/packages/toji-comms/src/index.ts +368 -0
  14. package/packages/toji-comms/src/mcp/client.ts +71 -0
  15. package/packages/toji-comms/src/mcp/server.ts +81 -0
  16. package/packages/toji-mem/README.md +52 -0
  17. package/packages/toji-mem/grammars/manifest.json +9 -0
  18. package/packages/toji-mem/grammars/tree-sitter-cpp.wasm +0 -0
  19. package/packages/toji-mem/grammars/tree-sitter-dart.wasm +0 -0
  20. package/packages/toji-mem/grammars/tree-sitter-java.wasm +0 -0
  21. package/packages/toji-mem/grammars/tree-sitter-javascript.wasm +0 -0
  22. package/packages/toji-mem/grammars/tree-sitter-python.wasm +0 -0
  23. package/packages/toji-mem/grammars/tree-sitter-tsx.wasm +0 -0
  24. package/packages/toji-mem/grammars/tree-sitter-typescript.wasm +0 -0
  25. package/packages/toji-mem/src/db/connection.ts +58 -0
  26. package/packages/toji-mem/src/db/migrations.ts +181 -0
  27. package/packages/toji-mem/src/index.ts +326 -0
  28. package/packages/toji-mem/src/indexer/file-walker.ts +45 -0
  29. package/packages/toji-mem/src/indexer/index-project.ts +277 -0
  30. package/packages/toji-mem/src/indexer/parsers/cpp.ts +81 -0
  31. package/packages/toji-mem/src/indexer/parsers/dart.ts +91 -0
  32. package/packages/toji-mem/src/indexer/parsers/java.ts +83 -0
  33. package/packages/toji-mem/src/indexer/parsers/python.ts +84 -0
  34. package/packages/toji-mem/src/indexer/parsers/registry.ts +28 -0
  35. package/packages/toji-mem/src/indexer/parsers/tree-sitter-loader.ts +39 -0
  36. package/packages/toji-mem/src/indexer/parsers/types.ts +48 -0
  37. package/packages/toji-mem/src/indexer/parsers/typescript.ts +105 -0
  38. package/packages/toji-mem/src/standards/store.ts +52 -0
  39. package/packages/toji-mem/src/tools/blast-radius.ts +98 -0
  40. package/packages/toji-mem/src/tools/graph-explore.ts +186 -0
  41. package/packages/toji-mem/src/tools/project-overview.ts +102 -0
  42. package/packages/toji-mem/src/tools/query-memory.ts +105 -0
@@ -0,0 +1,277 @@
1
+ import type { TojiDatabase } from "../db/connection";
2
+ import { createHash } from "node:crypto";
3
+ import { readFile } from "node:fs/promises";
4
+ import { basename, dirname, join, normalize, parse, resolve } from "node:path";
5
+
6
+ import { walkProjectFiles } from "./file-walker";
7
+ import { getParserForPath, getSupportedExtensions } from "./parsers/registry";
8
+
9
+ export interface IndexProjectResult {
10
+ projectId: number;
11
+ rootPath: string;
12
+ indexedFiles: number;
13
+ symbols: number;
14
+ imports: number;
15
+ calls: number;
16
+ }
17
+
18
+ export async function indexProject(database: TojiDatabase, rootPath: string): Promise<IndexProjectResult> {
19
+ const normalizedRoot = resolve(rootPath);
20
+ const project = database
21
+ .query<{ id: number }, [string, string]>(
22
+ `INSERT INTO projects (root_path, name, indexed_at)
23
+ VALUES (?, ?, CURRENT_TIMESTAMP)
24
+ ON CONFLICT(root_path) DO UPDATE SET indexed_at = CURRENT_TIMESTAMP
25
+ RETURNING id`,
26
+ )
27
+ .get(normalizedRoot, basename(normalizedRoot));
28
+
29
+ if (!project) throw new Error("Failed to create or update project");
30
+
31
+ const files = await walkProjectFiles(normalizedRoot, getSupportedExtensions());
32
+ const freshHashes = await hashFiles(normalizedRoot, files);
33
+ const previousHashes = new Map(
34
+ database
35
+ .query<{ path: string; hash: string }, [number]>(`SELECT path, hash FROM files WHERE project_id = ?`)
36
+ .all(project.id)
37
+ .map((row) => [row.path, row.hash]),
38
+ );
39
+
40
+ if (hashesMatch(freshHashes, previousHashes)) {
41
+ const counts = database
42
+ .query<{ symbols: number; imports: number; calls: number }, [number, number, number]>(
43
+ `SELECT
44
+ (SELECT COUNT(*) FROM symbols WHERE project_id = ?) AS symbols,
45
+ (SELECT COUNT(*) FROM imports WHERE project_id = ?) AS imports,
46
+ (SELECT COUNT(*) FROM calls WHERE project_id = ?) AS calls`,
47
+ )
48
+ .get(project.id, project.id, project.id);
49
+ return {
50
+ projectId: project.id,
51
+ rootPath: normalizedRoot,
52
+ indexedFiles: files.length,
53
+ symbols: counts?.symbols ?? 0,
54
+ imports: counts?.imports ?? 0,
55
+ calls: counts?.calls ?? 0,
56
+ };
57
+ }
58
+
59
+ database.query("DELETE FROM files WHERE project_id = ?").run(project.id);
60
+ database.query("DELETE FROM edges WHERE project_id = ?").run(project.id);
61
+ database.query("DELETE FROM symbol_fts WHERE project_id = ?").run(project.id);
62
+ database.query("DELETE FROM file_fts WHERE project_id = ?").run(project.id);
63
+
64
+ let symbolCount = 0;
65
+ let importCount = 0;
66
+ let callCount = 0;
67
+
68
+ const insertFile = database.query<{ id: number }, [number, string, string, string, string]>(
69
+ `INSERT INTO files (project_id, path, name, language, hash)
70
+ VALUES (?, ?, ?, ?, ?)
71
+ RETURNING id`,
72
+ );
73
+ const insertSymbol = database.query<{ id: number }, [number, number, string, string, string, string, number, number, string | null, number]>(
74
+ `INSERT INTO symbols (project_id, file_id, name, canon_name, kind, language, start_line, end_line, signature, exported)
75
+ VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
76
+ RETURNING id`,
77
+ );
78
+ const insertImport = database.query<unknown, [number, number, string | null, string, number]>(
79
+ `INSERT INTO imports (project_id, file_id, imported_name, source, start_line)
80
+ VALUES (?, ?, ?, ?, ?)`,
81
+ );
82
+ const insertCall = database.query<unknown, [number, number, string | null, string, number]>(
83
+ `INSERT INTO calls (project_id, file_id, caller_name, callee_name, start_line)
84
+ VALUES (?, ?, ?, ?, ?)`,
85
+ );
86
+ const insertSymbolFts = database.query<unknown, [number, number, number, string, string, string, string, string, string, string]>(
87
+ `INSERT INTO symbol_fts (symbol_id, project_id, file_id, name, canon_name, kind, language, path, signature, docstring)
88
+ VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`,
89
+ );
90
+ const insertFileFts = database.query<unknown, [number, number, string, string, string, string]>(
91
+ `INSERT INTO file_fts (file_id, project_id, name, path, language, docstring) VALUES (?, ?, ?, ?, ?, ?)`,
92
+ );
93
+
94
+ const fileIds = new Map<string, number>();
95
+ const symbolsByFile = new Map<number, Array<{ id: number; name: string; startLine: number; endLine: number; exported: boolean }>>();
96
+ const importsByFile = new Map<number, Array<{ source: string; startLine: number }>>();
97
+ const callsByFile = new Map<number, Array<{ callerId?: number; calleeName: string; startLine: number }>>();
98
+ const relationsByFile = new Map<number, Array<{ fromName: string; toName: string; edgeType: "symbol_extends_symbol" | "symbol_implements_symbol" }>>();
99
+
100
+ for (const relativePath of files) {
101
+ const parser = getParserForPath(relativePath);
102
+ if (!parser) continue;
103
+
104
+ const absolutePath = join(normalizedRoot, relativePath);
105
+ const content = await readFile(absolutePath, "utf8");
106
+ const hash = freshHashes.get(relativePath) ?? createHash("sha256").update(content).digest("hex");
107
+ const fileName = basename(relativePath);
108
+ const file = insertFile.get(project.id, relativePath, fileName, parser.language, hash);
109
+ if (!file) continue;
110
+
111
+ insertFileFts.run(file.id, project.id, fileName, relativePath, parser.language, "");
112
+ fileIds.set(relativePath, file.id);
113
+
114
+ const parsed = await parser.parseFile({ projectId: project.id, filePath: relativePath, content });
115
+ const fileSymbols: Array<{ id: number; name: string; startLine: number; endLine: number; exported: boolean }> = [];
116
+
117
+ for (const symbol of parsed.symbols) {
118
+ const canon_name = symbolQname(relativePath, symbol.name);
119
+ const insertedSymbol = insertSymbol.get(
120
+ project.id,
121
+ file.id,
122
+ symbol.name,
123
+ canon_name,
124
+ symbol.kind,
125
+ parser.language,
126
+ symbol.startLine,
127
+ symbol.endLine,
128
+ symbol.signature ?? null,
129
+ symbol.exported ? 1 : 0,
130
+ );
131
+ if (!insertedSymbol) continue;
132
+
133
+ fileSymbols.push({
134
+ id: insertedSymbol.id,
135
+ name: symbol.name,
136
+ startLine: symbol.startLine,
137
+ endLine: symbol.endLine,
138
+ exported: symbol.exported,
139
+ });
140
+ insertSymbolFts.run(insertedSymbol.id, project.id, file.id, symbol.name, canon_name, symbol.kind, parser.language, relativePath, symbol.signature ?? "", "");
141
+ symbolCount += 1;
142
+ }
143
+
144
+ symbolsByFile.set(file.id, fileSymbols);
145
+
146
+ const fileImports: Array<{ source: string; startLine: number }> = [];
147
+ for (const parsedImport of parsed.imports) {
148
+ insertImport.run(project.id, file.id, parsedImport.importedName ?? null, parsedImport.source, parsedImport.startLine);
149
+ fileImports.push({ source: parsedImport.source, startLine: parsedImport.startLine });
150
+ importCount += 1;
151
+ }
152
+ importsByFile.set(file.id, fileImports);
153
+
154
+ const fileCalls: Array<{ callerId?: number; calleeName: string; startLine: number }> = [];
155
+ for (const call of parsed.calls) {
156
+ const caller = findContainingSymbol(fileSymbols, call.startLine);
157
+ insertCall.run(project.id, file.id, caller?.name ?? call.callerName ?? null, call.calleeName, call.startLine);
158
+ fileCalls.push({ callerId: caller?.id, calleeName: call.calleeName, startLine: call.startLine });
159
+ callCount += 1;
160
+ }
161
+ callsByFile.set(file.id, fileCalls);
162
+ relationsByFile.set(file.id, parsed.relations.map((relation) => ({
163
+ fromName: relation.fromName,
164
+ toName: relation.toName,
165
+ edgeType: relation.edgeType,
166
+ })));
167
+ }
168
+
169
+ createGraphEdges(database, project.id, files, fileIds, symbolsByFile, importsByFile, callsByFile, relationsByFile);
170
+
171
+ return { projectId: project.id, rootPath: normalizedRoot, indexedFiles: files.length, symbols: symbolCount, imports: importCount, calls: callCount };
172
+ }
173
+
174
+ async function hashFiles(rootPath: string, files: string[]): Promise<Map<string, string>> {
175
+ const hashes = new Map<string, string>();
176
+ for (const relativePath of files) {
177
+ const content = await readFile(join(rootPath, relativePath), "utf8");
178
+ hashes.set(relativePath, createHash("sha256").update(content).digest("hex"));
179
+ }
180
+ return hashes;
181
+ }
182
+
183
+ function hashesMatch(fresh: Map<string, string>, previous: Map<string, string>): boolean {
184
+ if (fresh.size === 0 || fresh.size !== previous.size) return false;
185
+ for (const [path, hash] of fresh) {
186
+ if (previous.get(path) !== hash) return false;
187
+ }
188
+ return true;
189
+ }
190
+
191
+ function symbolQname(filePath: string, symbolName: string): string {
192
+ const parsed = parse(filePath);
193
+ const moduleName = join(parsed.dir, parsed.name).replaceAll("/", ".").replaceAll("\\\\", ".");
194
+ return moduleName ? `${moduleName}.${symbolName}` : symbolName;
195
+ }
196
+
197
+ function findContainingSymbol(
198
+ symbols: Array<{ id: number; name: string; startLine: number; endLine: number }>,
199
+ line: number,
200
+ ): { id: number; name: string; startLine: number; endLine: number } | undefined {
201
+ return symbols
202
+ .filter((symbol) => symbol.startLine <= line && symbol.endLine >= line)
203
+ .sort((left, right) => left.endLine - left.startLine - (right.endLine - right.startLine))[0];
204
+ }
205
+
206
+ function createGraphEdges(
207
+ database: TojiDatabase,
208
+ projectId: number,
209
+ files: string[],
210
+ fileIds: Map<string, number>,
211
+ symbolsByFile: Map<number, Array<{ id: number; name: string; startLine: number; endLine: number; exported: boolean }>>,
212
+ importsByFile: Map<number, Array<{ source: string; startLine: number }>>,
213
+ callsByFile: Map<number, Array<{ callerId?: number; calleeName: string; startLine: number }>>,
214
+ relationsByFile: Map<number, Array<{ fromName: string; toName: string; edgeType: "symbol_extends_symbol" | "symbol_implements_symbol" }>>,
215
+ ): void {
216
+ const insertEdge = database.query<unknown, [number, string, number, string, number | null, string | null, string, number]>(
217
+ `INSERT INTO edges (project_id, from_type, from_id, to_type, to_id, to_name, edge_type, confidence)
218
+ VALUES (?, ?, ?, ?, ?, ?, ?, ?)`,
219
+ );
220
+
221
+ const symbolsByName = new Map<string, Array<{ id: number; fileId: number; exported: boolean }>>();
222
+ for (const [fileId, symbols] of symbolsByFile) {
223
+ for (const symbol of symbols) {
224
+ insertEdge.run(projectId, "file", fileId, "symbol", symbol.id, symbol.name, "file_contains_symbol", 1);
225
+ if (symbol.exported) insertEdge.run(projectId, "file", fileId, "symbol", symbol.id, symbol.name, "file_exports_symbol", 1);
226
+
227
+ const namedSymbols = symbolsByName.get(symbol.name) ?? [];
228
+ namedSymbols.push({ id: symbol.id, fileId, exported: symbol.exported });
229
+ symbolsByName.set(symbol.name, namedSymbols);
230
+ }
231
+ }
232
+
233
+ for (const [relativePath, fileId] of fileIds) {
234
+ for (const parsedImport of importsByFile.get(fileId) ?? []) {
235
+ const importedFile = resolveImportPath(relativePath, parsedImport.source, files);
236
+ if (importedFile) {
237
+ insertEdge.run(projectId, "file", fileId, "file", fileIds.get(importedFile) ?? null, importedFile, "file_imports_file", 0.9);
238
+ } else {
239
+ insertEdge.run(projectId, "file", fileId, "package", null, parsedImport.source, "file_imports_package", 0.8);
240
+ }
241
+ }
242
+
243
+ for (const relation of relationsByFile.get(fileId) ?? []) {
244
+ const fromSymbol = (symbolsByFile.get(fileId) ?? []).find((symbol) => symbol.name === relation.fromName);
245
+ if (!fromSymbol) continue;
246
+
247
+ const target = symbolsByName.get(relation.toName)?.find((symbol) => symbol.exported) ?? symbolsByName.get(relation.toName)?.[0];
248
+ insertEdge.run(projectId, "symbol", fromSymbol.id, "symbol", target?.id ?? null, relation.toName, relation.edgeType, target ? 0.8 : 0.4);
249
+ }
250
+
251
+ for (const call of callsByFile.get(fileId) ?? []) {
252
+ if (!call.callerId) continue;
253
+ const sameFileTarget = (symbolsByFile.get(fileId) ?? []).find((symbol) => symbol.name === call.calleeName);
254
+ const globalTarget = symbolsByName.get(call.calleeName)?.find((symbol) => symbol.exported) ?? symbolsByName.get(call.calleeName)?.[0];
255
+ const target = sameFileTarget ? { id: sameFileTarget.id, confidence: 0.9 } : globalTarget ? { id: globalTarget.id, confidence: 0.55 } : undefined;
256
+ insertEdge.run(
257
+ projectId,
258
+ "symbol",
259
+ call.callerId,
260
+ "symbol",
261
+ target?.id ?? null,
262
+ call.calleeName,
263
+ "symbol_calls_symbol",
264
+ target?.confidence ?? 0.3,
265
+ );
266
+ }
267
+ }
268
+ }
269
+
270
+ function resolveImportPath(fromPath: string, source: string, files: string[]): string | undefined {
271
+ if (!source.startsWith(".")) return undefined;
272
+
273
+ const basePath = normalize(join(dirname(fromPath), source));
274
+ const candidates = [basePath, `${basePath}.ts`, `${basePath}.tsx`, `${basePath}.js`, `${basePath}.jsx`, `${basePath}.py`, join(basePath, "index.ts")];
275
+ return candidates.find((candidate) => files.includes(candidate));
276
+ }
277
+
@@ -0,0 +1,81 @@
1
+ import { Query } from "web-tree-sitter";
2
+
3
+ import { loadTreeSitterLanguage, parseWithGrammar } from "./tree-sitter-loader";
4
+ import type { LanguageParser, ParsedCall, ParsedFile, ParsedImport, ParsedRelation, ParsedSymbol } from "./types";
5
+
6
+ const cppQuerySource = `
7
+ (function_definition declarator: (function_declarator declarator: (identifier) @function.name)) @function.definition
8
+ (function_definition declarator: (function_declarator declarator: (field_identifier) @method.name)) @function.definition
9
+ (class_specifier name: (type_identifier) @class.name) @class.definition
10
+ (struct_specifier name: (type_identifier) @class.name) @class.definition
11
+ (preproc_include path: [(string_literal) (system_lib_string)] @import.source) @import.statement
12
+ (call_expression function: (identifier) @call.name) @call.expression
13
+ (call_expression function: (field_expression field: (field_identifier) @call.member_name)) @call.expression
14
+ `;
15
+
16
+ export const cppParser: LanguageParser = {
17
+ language: "cpp",
18
+ extensions: [".cpp", ".cc", ".cxx", ".c++", ".hpp", ".hh", ".hxx", ".h++", ".h"],
19
+ async parseFile(input): Promise<ParsedFile> {
20
+ const tree = await parseWithGrammar("tree-sitter-cpp.wasm", input.content);
21
+ const grammar = await loadTreeSitterLanguage("tree-sitter-cpp.wasm");
22
+ const query = new Query(grammar, cppQuerySource);
23
+ const captures = query.captures(tree.rootNode);
24
+
25
+ const symbols: ParsedSymbol[] = [];
26
+ const imports: ParsedImport[] = [];
27
+ const calls: ParsedCall[] = [];
28
+ const relations: ParsedRelation[] = [];
29
+
30
+ for (const capture of captures) {
31
+ if (capture.name === "function.name" || capture.name === "method.name") {
32
+ const definition = nearest(capture.node, "function_definition");
33
+ symbols.push({
34
+ name: capture.node.text,
35
+ kind: capture.name === "method.name" ? "method" : "function",
36
+ startLine: definition?.startPosition.row !== undefined ? definition.startPosition.row + 1 : capture.node.startPosition.row + 1,
37
+ endLine: definition?.endPosition.row !== undefined ? definition.endPosition.row + 1 : capture.node.endPosition.row + 1,
38
+ exported: true,
39
+ signature: definition?.text.split("\n")[0]?.trim(),
40
+ });
41
+ continue;
42
+ }
43
+
44
+ if (capture.name === "class.name") {
45
+ const definition = capture.node.parent;
46
+ symbols.push({
47
+ name: capture.node.text,
48
+ kind: "class",
49
+ startLine: definition?.startPosition.row !== undefined ? definition.startPosition.row + 1 : capture.node.startPosition.row + 1,
50
+ endLine: definition?.endPosition.row !== undefined ? definition.endPosition.row + 1 : capture.node.endPosition.row + 1,
51
+ exported: true,
52
+ signature: definition?.text.split("\n")[0]?.trim(),
53
+ });
54
+ continue;
55
+ }
56
+
57
+ if (capture.name === "import.source") {
58
+ imports.push({
59
+ source: capture.node.text.replace(/[<>"]/g, ""),
60
+ startLine: capture.node.startPosition.row + 1,
61
+ });
62
+ continue;
63
+ }
64
+
65
+ if (capture.name === "call.name" || capture.name === "call.member_name") {
66
+ calls.push({ calleeName: capture.node.text, startLine: capture.node.startPosition.row + 1 });
67
+ }
68
+ }
69
+
70
+ return { symbols, imports, calls, relations };
71
+ },
72
+ };
73
+
74
+ function nearest(node: { parent: unknown; type: string } | null, type: string): { type: string; text: string; startPosition: { row: number }; endPosition: { row: number } } | undefined {
75
+ let current = node;
76
+ while (current) {
77
+ if (current.type === type) return current as unknown as { type: string; text: string; startPosition: { row: number }; endPosition: { row: number } };
78
+ current = current.parent as typeof current;
79
+ }
80
+ return undefined;
81
+ }
@@ -0,0 +1,91 @@
1
+ import { Query } from "web-tree-sitter";
2
+
3
+ import { loadTreeSitterLanguage, parseWithGrammar } from "./tree-sitter-loader";
4
+ import type { LanguageParser, ParsedCall, ParsedFile, ParsedImport, ParsedRelation, ParsedSymbol, SymbolKind } from "./types";
5
+
6
+ const querySource = `
7
+ (class_definition name: (identifier) @class.name) @class.definition
8
+ (class_definition name: (identifier) @heritage.from superclass: (superclass (type_identifier) @extends.to))
9
+ (class_definition name: (identifier) @heritage.from interfaces: (interfaces (type_identifier) @implements.to))
10
+ (function_signature name: (identifier) @function.name) @function.definition
11
+ (method_signature (function_signature name: (identifier) @method.name)) @method.definition
12
+ (library_import (import_specification (configurable_uri (uri (string_literal) @import.source)))) @import.statement
13
+ (expression_statement (identifier) @call.name (selector (argument_part))) @call.expression
14
+ `;
15
+
16
+ const symbolKinds = new Map<string, SymbolKind>([
17
+ ["class.name", "class"],
18
+ ["function.name", "function"],
19
+ ["method.name", "method"],
20
+ ]);
21
+
22
+ export const dartParser: LanguageParser = {
23
+ language: "dart",
24
+ extensions: [".dart"],
25
+ async parseFile(input): Promise<ParsedFile> {
26
+ const grammarFile = "tree-sitter-dart.wasm";
27
+ const tree = await parseWithGrammar(grammarFile, input.content);
28
+ const grammar = await loadTreeSitterLanguage(grammarFile);
29
+ const query = new Query(grammar, querySource);
30
+ const captures = query.captures(tree.rootNode);
31
+
32
+ const symbols: ParsedSymbol[] = [];
33
+ const imports: ParsedImport[] = [];
34
+ const calls: ParsedCall[] = [];
35
+ const relations: ParsedRelation[] = [];
36
+ let heritageFrom: string | undefined;
37
+
38
+ for (const capture of captures) {
39
+ const kind = symbolKinds.get(capture.name);
40
+ if (kind) {
41
+ const parent = capture.node.parent;
42
+ if (kind === "function" && parent?.parent?.type === "method_signature") continue;
43
+ symbols.push({
44
+ name: capture.node.text,
45
+ kind,
46
+ startLine: capture.node.startPosition.row + 1,
47
+ endLine: parent ? parent.endPosition.row + 1 : capture.node.endPosition.row + 1,
48
+ exported: !capture.node.text.startsWith("_"),
49
+ signature: parent?.text.split("\n")[0]?.trim(),
50
+ });
51
+ continue;
52
+ }
53
+
54
+ if (capture.name === "import.source") {
55
+ imports.push({ source: capture.node.text.replace(/^['\"]|['\"]$/g, ""), startLine: capture.node.startPosition.row + 1 });
56
+ continue;
57
+ }
58
+
59
+ if (capture.name === "call.name") {
60
+ calls.push({ calleeName: capture.node.text, startLine: capture.node.startPosition.row + 1 });
61
+ continue;
62
+ }
63
+
64
+ if (capture.name === "heritage.from") {
65
+ heritageFrom = capture.node.text;
66
+ continue;
67
+ }
68
+
69
+ if (capture.name === "extends.to" && heritageFrom) {
70
+ relations.push({
71
+ fromName: heritageFrom,
72
+ toName: capture.node.text,
73
+ edgeType: "symbol_extends_symbol",
74
+ startLine: capture.node.startPosition.row + 1,
75
+ });
76
+ continue;
77
+ }
78
+
79
+ if (capture.name === "implements.to" && heritageFrom) {
80
+ relations.push({
81
+ fromName: heritageFrom,
82
+ toName: capture.node.text,
83
+ edgeType: "symbol_implements_symbol",
84
+ startLine: capture.node.startPosition.row + 1,
85
+ });
86
+ }
87
+ }
88
+
89
+ return { symbols, imports, calls, relations };
90
+ },
91
+ };
@@ -0,0 +1,83 @@
1
+ import { Query } from "web-tree-sitter";
2
+
3
+ import { loadTreeSitterLanguage, parseWithGrammar } from "./tree-sitter-loader";
4
+ import type { LanguageParser, ParsedCall, ParsedFile, ParsedImport, ParsedRelation, ParsedSymbol, SymbolKind } from "./types";
5
+
6
+ const querySource = `
7
+ (class_declaration name: (identifier) @class.name) @class.definition
8
+ (interface_declaration name: (identifier) @interface.name) @interface.definition
9
+ (method_declaration name: (identifier) @method.name) @method.definition
10
+ (constructor_declaration name: (identifier) @method.name) @method.definition
11
+ (import_declaration (scoped_identifier) @import.source) @import.statement
12
+ (import_declaration (identifier) @import.source) @import.statement
13
+ (method_invocation name: (identifier) @call.name) @call.expression
14
+ (class_declaration name: (identifier) @heritage.from superclass: (superclass (type_identifier) @extends.to))
15
+ (class_declaration name: (identifier) @heritage.from interfaces: (super_interfaces (type_list (type_identifier) @implements.to)))
16
+ (interface_declaration name: (identifier) @heritage.from (extends_interfaces (type_list (type_identifier) @extends.to)))
17
+ `;
18
+
19
+ const symbolKinds = new Map<string, SymbolKind>([
20
+ ["class.name", "class"],
21
+ ["interface.name", "interface"],
22
+ ["method.name", "method"],
23
+ ]);
24
+
25
+ export const javaParser: LanguageParser = {
26
+ language: "java",
27
+ extensions: [".java"],
28
+ async parseFile(input): Promise<ParsedFile> {
29
+ const grammarFile = "tree-sitter-java.wasm";
30
+ const tree = await parseWithGrammar(grammarFile, input.content);
31
+ const grammar = await loadTreeSitterLanguage(grammarFile);
32
+ const query = new Query(grammar, querySource);
33
+ const captures = query.captures(tree.rootNode);
34
+
35
+ const symbols: ParsedSymbol[] = [];
36
+ const imports: ParsedImport[] = [];
37
+ const calls: ParsedCall[] = [];
38
+ const relations: ParsedRelation[] = [];
39
+ let heritageFrom: string | undefined;
40
+
41
+ for (const capture of captures) {
42
+ const kind = symbolKinds.get(capture.name);
43
+ if (kind) {
44
+ const parent = capture.node.parent;
45
+ symbols.push({
46
+ name: capture.node.text,
47
+ kind,
48
+ startLine: capture.node.startPosition.row + 1,
49
+ endLine: parent ? parent.endPosition.row + 1 : capture.node.endPosition.row + 1,
50
+ exported: parent?.text.startsWith("public ") ?? false,
51
+ signature: parent?.text.split("\n")[0]?.trim(),
52
+ });
53
+ continue;
54
+ }
55
+
56
+ if (capture.name === "import.source") {
57
+ imports.push({ source: capture.node.text, startLine: capture.node.startPosition.row + 1 });
58
+ continue;
59
+ }
60
+
61
+ if (capture.name === "call.name") {
62
+ calls.push({ calleeName: capture.node.text, startLine: capture.node.startPosition.row + 1 });
63
+ continue;
64
+ }
65
+
66
+ if (capture.name === "heritage.from") {
67
+ heritageFrom = capture.node.text;
68
+ continue;
69
+ }
70
+
71
+ if ((capture.name === "extends.to" || capture.name === "implements.to") && heritageFrom) {
72
+ relations.push({
73
+ fromName: heritageFrom,
74
+ toName: capture.node.text,
75
+ edgeType: capture.name === "extends.to" ? "symbol_extends_symbol" : "symbol_implements_symbol",
76
+ startLine: capture.node.startPosition.row + 1,
77
+ });
78
+ }
79
+ }
80
+
81
+ return { symbols, imports, calls, relations };
82
+ },
83
+ };
@@ -0,0 +1,84 @@
1
+ import { Query } from "web-tree-sitter";
2
+
3
+ import { loadTreeSitterLanguage, parseWithGrammar } from "./tree-sitter-loader";
4
+ import type { LanguageParser, ParsedCall, ParsedFile, ParsedImport, ParsedRelation, ParsedSymbol, SymbolKind } from "./types";
5
+
6
+ const querySource = `
7
+ (function_definition name: (identifier) @function.name) @function.definition
8
+ (class_definition name: (identifier) @class.name) @class.definition
9
+ (class_definition name: (identifier) @heritage.from superclasses: (argument_list (identifier) @extends.to))
10
+ (import_statement) @import.statement
11
+ (import_from_statement module_name: (dotted_name) @import.source) @import.statement
12
+ (call function: (identifier) @call.name) @call.expression
13
+ (call function: (attribute attribute: (identifier) @call.member_name)) @call.expression
14
+ `;
15
+
16
+ const symbolKinds = new Map<string, SymbolKind>([
17
+ ["function.name", "function"],
18
+ ["class.name", "class"],
19
+ ]);
20
+
21
+ export const pythonParser: LanguageParser = {
22
+ language: "python",
23
+ extensions: [".py"],
24
+ async parseFile(input): Promise<ParsedFile> {
25
+ const grammarFile = "tree-sitter-python.wasm";
26
+ const tree = await parseWithGrammar(grammarFile, input.content);
27
+ const grammar = await loadTreeSitterLanguage(grammarFile);
28
+ const query = new Query(grammar, querySource);
29
+ const captures = query.captures(tree.rootNode);
30
+
31
+ const symbols: ParsedSymbol[] = [];
32
+ const imports: ParsedImport[] = [];
33
+ const calls: ParsedCall[] = [];
34
+ const relations: ParsedRelation[] = [];
35
+ let heritageFrom: string | undefined;
36
+
37
+ for (const capture of captures) {
38
+ const kind = symbolKinds.get(capture.name);
39
+ if (kind) {
40
+ const parent = capture.node.parent;
41
+ symbols.push({
42
+ name: capture.node.text,
43
+ kind,
44
+ startLine: capture.node.startPosition.row + 1,
45
+ endLine: parent ? parent.endPosition.row + 1 : capture.node.endPosition.row + 1,
46
+ exported: !capture.node.text.startsWith("_"),
47
+ signature: parent?.text.split("\n")[0]?.trim(),
48
+ });
49
+ continue;
50
+ }
51
+
52
+ if (capture.name === "import.source") {
53
+ imports.push({ source: capture.node.text, startLine: capture.node.startPosition.row + 1 });
54
+ continue;
55
+ }
56
+
57
+ if (capture.name === "import.statement") {
58
+ imports.push({ source: capture.node.text, startLine: capture.node.startPosition.row + 1 });
59
+ continue;
60
+ }
61
+
62
+ if (capture.name === "call.name" || capture.name === "call.member_name") {
63
+ calls.push({ calleeName: capture.node.text, startLine: capture.node.startPosition.row + 1 });
64
+ continue;
65
+ }
66
+
67
+ if (capture.name === "heritage.from") {
68
+ heritageFrom = capture.node.text;
69
+ continue;
70
+ }
71
+
72
+ if (capture.name === "extends.to" && heritageFrom) {
73
+ relations.push({
74
+ fromName: heritageFrom,
75
+ toName: capture.node.text,
76
+ edgeType: "symbol_extends_symbol",
77
+ startLine: capture.node.startPosition.row + 1,
78
+ });
79
+ }
80
+ }
81
+
82
+ return { symbols, imports, calls, relations };
83
+ },
84
+ };
@@ -0,0 +1,28 @@
1
+ import { extname } from "node:path";
2
+
3
+ import { cppParser } from "./cpp";
4
+ import { dartParser } from "./dart";
5
+ import { javaParser } from "./java";
6
+ import { pythonParser } from "./python";
7
+ import { createTypeScriptParser } from "./typescript";
8
+ import type { LanguageParser } from "./types";
9
+
10
+ const parsers: LanguageParser[] = [
11
+ createTypeScriptParser("typescript", "tree-sitter-typescript.wasm", [".ts", ".mts", ".cts"]),
12
+ createTypeScriptParser("tsx", "tree-sitter-tsx.wasm", [".tsx"]),
13
+ createTypeScriptParser("javascript", "tree-sitter-javascript.wasm", [".js", ".mjs", ".cjs"]),
14
+ createTypeScriptParser("jsx", "tree-sitter-javascript.wasm", [".jsx"]),
15
+ cppParser,
16
+ dartParser,
17
+ javaParser,
18
+ pythonParser,
19
+ ];
20
+
21
+ export function getParserForPath(filePath: string): LanguageParser | undefined {
22
+ const extension = extname(filePath);
23
+ return parsers.find((parser) => parser.extensions.includes(extension));
24
+ }
25
+
26
+ export function getSupportedExtensions(): string[] {
27
+ return parsers.flatMap((parser) => parser.extensions);
28
+ }