fraude-code 0.1.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.
Files changed (127) hide show
  1. package/README.md +68 -0
  2. package/dist/index.js +179297 -0
  3. package/package.json +88 -0
  4. package/src/agent/agent.ts +475 -0
  5. package/src/agent/contextManager.ts +141 -0
  6. package/src/agent/index.ts +14 -0
  7. package/src/agent/pendingChanges.ts +270 -0
  8. package/src/agent/prompts/AskPrompt.txt +10 -0
  9. package/src/agent/prompts/FastPrompt.txt +40 -0
  10. package/src/agent/prompts/PlannerPrompt.txt +51 -0
  11. package/src/agent/prompts/ReviewerPrompt.txt +57 -0
  12. package/src/agent/prompts/WorkerPrompt.txt +33 -0
  13. package/src/agent/subagents/askAgent.ts +37 -0
  14. package/src/agent/subagents/extractionAgent.ts +123 -0
  15. package/src/agent/subagents/fastAgent.ts +45 -0
  16. package/src/agent/subagents/managerAgent.ts +36 -0
  17. package/src/agent/subagents/relationAgent.ts +76 -0
  18. package/src/agent/subagents/researchSubAgent.ts +79 -0
  19. package/src/agent/subagents/reviewerSubAgent.ts +42 -0
  20. package/src/agent/subagents/workerSubAgent.ts +42 -0
  21. package/src/agent/tools/bashTool.ts +94 -0
  22. package/src/agent/tools/descriptions/bash.txt +47 -0
  23. package/src/agent/tools/descriptions/edit.txt +7 -0
  24. package/src/agent/tools/descriptions/glob.txt +4 -0
  25. package/src/agent/tools/descriptions/grep.txt +8 -0
  26. package/src/agent/tools/descriptions/lsp.txt +20 -0
  27. package/src/agent/tools/descriptions/plan.txt +3 -0
  28. package/src/agent/tools/descriptions/read.txt +9 -0
  29. package/src/agent/tools/descriptions/todo.txt +12 -0
  30. package/src/agent/tools/descriptions/write.txt +8 -0
  31. package/src/agent/tools/editTool.ts +44 -0
  32. package/src/agent/tools/globTool.ts +59 -0
  33. package/src/agent/tools/grepTool.ts +343 -0
  34. package/src/agent/tools/lspTool.ts +429 -0
  35. package/src/agent/tools/planTool.ts +118 -0
  36. package/src/agent/tools/readTool.ts +78 -0
  37. package/src/agent/tools/rememberTool.ts +91 -0
  38. package/src/agent/tools/testRunnerTool.ts +77 -0
  39. package/src/agent/tools/testTool.ts +44 -0
  40. package/src/agent/tools/todoTool.ts +224 -0
  41. package/src/agent/tools/writeTool.ts +33 -0
  42. package/src/commands/COMMANDS.ts +38 -0
  43. package/src/commands/cerebras/auth.ts +27 -0
  44. package/src/commands/cerebras/index.ts +31 -0
  45. package/src/commands/forget.ts +29 -0
  46. package/src/commands/google/auth.ts +24 -0
  47. package/src/commands/google/index.ts +31 -0
  48. package/src/commands/groq/add_model.ts +60 -0
  49. package/src/commands/groq/auth.ts +24 -0
  50. package/src/commands/groq/index.ts +33 -0
  51. package/src/commands/index.ts +65 -0
  52. package/src/commands/knowledge.ts +92 -0
  53. package/src/commands/log.ts +32 -0
  54. package/src/commands/mistral/auth.ts +27 -0
  55. package/src/commands/mistral/index.ts +31 -0
  56. package/src/commands/model/index.ts +145 -0
  57. package/src/commands/models/index.ts +16 -0
  58. package/src/commands/ollama/index.ts +29 -0
  59. package/src/commands/openrouter/add_model.ts +64 -0
  60. package/src/commands/openrouter/auth.ts +24 -0
  61. package/src/commands/openrouter/index.ts +33 -0
  62. package/src/commands/remember.ts +48 -0
  63. package/src/commands/serve.ts +31 -0
  64. package/src/commands/session/index.ts +21 -0
  65. package/src/commands/usage.ts +15 -0
  66. package/src/commands/visualize.ts +773 -0
  67. package/src/components/App.tsx +55 -0
  68. package/src/components/IntroComponent.tsx +70 -0
  69. package/src/components/LoaderComponent.tsx +68 -0
  70. package/src/components/OutputRenderer.tsx +88 -0
  71. package/src/components/SettingsRenderer.tsx +23 -0
  72. package/src/components/input/CommandSuggestions.tsx +41 -0
  73. package/src/components/input/FileSuggestions.tsx +61 -0
  74. package/src/components/input/InputBox.tsx +371 -0
  75. package/src/components/output/CheckpointView.tsx +13 -0
  76. package/src/components/output/CommandView.tsx +13 -0
  77. package/src/components/output/CommentView.tsx +12 -0
  78. package/src/components/output/ConfirmationView.tsx +179 -0
  79. package/src/components/output/ContextUsage.tsx +62 -0
  80. package/src/components/output/DiffView.tsx +202 -0
  81. package/src/components/output/ErrorView.tsx +14 -0
  82. package/src/components/output/InteractiveServerView.tsx +69 -0
  83. package/src/components/output/KnowledgeView.tsx +220 -0
  84. package/src/components/output/MarkdownView.tsx +15 -0
  85. package/src/components/output/ModelSelectView.tsx +71 -0
  86. package/src/components/output/ReasoningView.tsx +21 -0
  87. package/src/components/output/ToolCallView.tsx +45 -0
  88. package/src/components/settings/ModelList.tsx +250 -0
  89. package/src/components/settings/TokenUsage.tsx +274 -0
  90. package/src/config/schema.ts +19 -0
  91. package/src/config/settings.ts +229 -0
  92. package/src/index.tsx +100 -0
  93. package/src/parsers/tree-sitter-python.wasm +0 -0
  94. package/src/providers/providers.ts +71 -0
  95. package/src/services/PluginLoader.ts +123 -0
  96. package/src/services/cerebras.ts +69 -0
  97. package/src/services/embeddingService.ts +229 -0
  98. package/src/services/google.ts +65 -0
  99. package/src/services/graphSerializer.ts +248 -0
  100. package/src/services/groq.ts +23 -0
  101. package/src/services/knowledgeOrchestrator.ts +286 -0
  102. package/src/services/mistral.ts +79 -0
  103. package/src/services/ollama.ts +109 -0
  104. package/src/services/openrouter.ts +23 -0
  105. package/src/services/symbolExtractor.ts +277 -0
  106. package/src/store/useFraudeStore.ts +123 -0
  107. package/src/store/useSettingsStore.ts +38 -0
  108. package/src/theme.ts +26 -0
  109. package/src/types/Agent.ts +147 -0
  110. package/src/types/CommandDefinition.ts +8 -0
  111. package/src/types/Model.ts +94 -0
  112. package/src/types/OutputItem.ts +24 -0
  113. package/src/types/PluginContext.ts +55 -0
  114. package/src/types/TokenUsage.ts +5 -0
  115. package/src/types/assets.d.ts +4 -0
  116. package/src/utils/agentCognition.ts +1152 -0
  117. package/src/utils/fileSuggestions.ts +111 -0
  118. package/src/utils/index.ts +17 -0
  119. package/src/utils/initFraude.ts +8 -0
  120. package/src/utils/logger.ts +24 -0
  121. package/src/utils/lspClient.ts +1415 -0
  122. package/src/utils/paths.ts +24 -0
  123. package/src/utils/queryHandler.ts +227 -0
  124. package/src/utils/router.ts +278 -0
  125. package/src/utils/streamHandler.ts +132 -0
  126. package/src/utils/treeSitterQueries.ts +125 -0
  127. package/tsconfig.json +33 -0
@@ -0,0 +1,1415 @@
1
+ import * as ts from "typescript";
2
+ import { Parser, Language, Query } from "web-tree-sitter";
3
+ import path from "path";
4
+ import fs from "fs";
5
+ import {
6
+ getQueriesForExtension,
7
+ type LanguageQueries,
8
+ } from "./treeSitterQueries";
9
+
10
+ /**
11
+ * A symbol with extended metadata for knowledge graph enrichment.
12
+ */
13
+ export interface DetailedSymbol {
14
+ name: string;
15
+ kind: string;
16
+ line: number;
17
+ signature?: string; // e.g., "foo(x: number, y: string): boolean"
18
+ docstring?: string; // JSDoc or docstring content
19
+ exported?: boolean; // Is this symbol exported?
20
+ children?: DetailedSymbol[];
21
+ }
22
+
23
+ /**
24
+ * Common interface for all language providers
25
+ */
26
+ export interface LanguageProvider {
27
+ isSupported(extension: string): boolean;
28
+ getDiagnostics(
29
+ filePath: string,
30
+ content: string,
31
+ ): Promise<{ errors: string[]; warnings: string[] }>;
32
+ findDefinition(
33
+ filePath: string,
34
+ content: string,
35
+ line: number,
36
+ character: number,
37
+ ): Promise<{ file: string; line: number; preview?: string } | null>;
38
+ findReferences(
39
+ filePath: string,
40
+ content: string,
41
+ line: number,
42
+ character: number,
43
+ ): Promise<Array<{ file: string; line: number }>>;
44
+ getDocumentSymbols(
45
+ filePath: string,
46
+ content: string,
47
+ ): Promise<
48
+ Array<{ name: string; kind: string; line: number; children?: any[] }>
49
+ >;
50
+
51
+ // Enhanced symbol extraction with signatures and docstrings
52
+ getDocumentSymbolsWithDetails?(
53
+ filePath: string,
54
+ content: string,
55
+ ): Promise<DetailedSymbol[]>;
56
+
57
+ // Added missing methods
58
+ getSymbolInfo(
59
+ filePath: string,
60
+ content: string,
61
+ line: number,
62
+ character: number,
63
+ ): Promise<string | null>;
64
+ findImplementation(
65
+ filePath: string,
66
+ content: string,
67
+ line: number,
68
+ character: number,
69
+ ): Promise<Array<{ file: string; line: number; preview?: string }>>;
70
+
71
+ // Optional advanced features
72
+ prepareCallHierarchy?(
73
+ filePath: string,
74
+ content: string,
75
+ line: number,
76
+ character: number,
77
+ ): Promise<Array<{ name: string; kind: string; file: string; line: number }>>;
78
+ getIncomingCalls?(
79
+ filePath: string,
80
+ content: string,
81
+ line: number,
82
+ character: number,
83
+ ): Promise<Array<{ name: string; file: string; line: number }>>;
84
+ getOutgoingCalls?(
85
+ filePath: string,
86
+ content: string,
87
+ line: number,
88
+ character: number,
89
+ ): Promise<
90
+ Array<{ name: string; file: string; line: number; sourceContext?: string }>
91
+ >;
92
+ searchWorkspaceSymbols?(
93
+ query: string,
94
+ ): Promise<Array<{ name: string; kind: string; file: string; line: number }>>;
95
+
96
+ // Import extraction for knowledge graph
97
+ getImports?(
98
+ filePath: string,
99
+ content: string,
100
+ ): Promise<Array<{ module: string; alias?: string; isRelative: boolean }>>;
101
+
102
+ // Inheritance extraction for knowledge graph
103
+ getInheritance?(
104
+ filePath: string,
105
+ content: string,
106
+ ): Promise<Array<{ className: string; parentName: string; line: number }>>;
107
+ }
108
+
109
+ /**
110
+ * ----------------------------------------------------------------------
111
+ * TIER 1: TypeScript Provider (High Fidelity)
112
+ * Uses the TypeScript Compiler API to provide rich analysis for TS/JS files.
113
+ * ----------------------------------------------------------------------
114
+ */
115
+ class TypeScriptProvider implements LanguageProvider {
116
+ private service: ts.LanguageService;
117
+ private files: Map<string, { version: number; content: string }> = new Map();
118
+
119
+ constructor(private rootPath: string) {
120
+ const registry = ts.createDocumentRegistry();
121
+ const serviceHost: ts.LanguageServiceHost = {
122
+ getScriptFileNames: () => Array.from(this.files.keys()),
123
+ getScriptVersion: (fileName) =>
124
+ this.files.get(fileName)?.version.toString() || "0",
125
+ getScriptSnapshot: (fileName) => {
126
+ const file = this.files.get(fileName);
127
+ if (file) {
128
+ return ts.ScriptSnapshot.fromString(file.content);
129
+ }
130
+ if (fs.existsSync(fileName)) {
131
+ return ts.ScriptSnapshot.fromString(
132
+ fs.readFileSync(fileName, "utf-8"),
133
+ );
134
+ }
135
+ return undefined;
136
+ },
137
+ getCurrentDirectory: () => this.rootPath,
138
+ getCompilationSettings: () => ({
139
+ target: ts.ScriptTarget.ESNext,
140
+ module: ts.ModuleKind.ESNext,
141
+ allowJs: true,
142
+ jsx: ts.JsxEmit.React,
143
+ strict: true,
144
+ moduleResolution: ts.ModuleResolutionKind.NodeJs,
145
+ esModuleInterop: true,
146
+ skipLibCheck: true,
147
+ resolveJsonModule: true,
148
+ }),
149
+ getDefaultLibFileName: (options) => ts.getDefaultLibFilePath(options),
150
+ fileExists: ts.sys.fileExists,
151
+ readFile: ts.sys.readFile,
152
+ readDirectory: ts.sys.readDirectory,
153
+ };
154
+
155
+ this.service = ts.createLanguageService(serviceHost, registry);
156
+ }
157
+
158
+ isSupported(ext: string): boolean {
159
+ return ["ts", "tsx", "js", "jsx"].includes(ext);
160
+ }
161
+
162
+ private updateFile(filePath: string, content: string) {
163
+ const current = this.files.get(filePath);
164
+ if (!current || current.content !== content) {
165
+ this.files.set(filePath, {
166
+ version: (current?.version || 0) + 1,
167
+ content,
168
+ });
169
+ }
170
+ }
171
+
172
+ async getDiagnostics(filePath: string, content: string) {
173
+ this.updateFile(filePath, content);
174
+
175
+ const syntactic = this.service.getSyntacticDiagnostics(filePath);
176
+ const semantic = this.service.getSemanticDiagnostics(filePath);
177
+
178
+ const all = [...syntactic, ...semantic];
179
+ const errors: string[] = [];
180
+ const warnings: string[] = [];
181
+
182
+ for (const d of all) {
183
+ const line = d.file
184
+ ? d.file.getLineAndCharacterOfPosition(d.start!).line + 1
185
+ : 0;
186
+ const msg = ts.flattenDiagnosticMessageText(d.messageText, "\n");
187
+ const fmt = `Line ${line}: ${msg}`;
188
+ if (d.category === ts.DiagnosticCategory.Error) {
189
+ errors.push(fmt);
190
+ } else {
191
+ warnings.push(fmt);
192
+ }
193
+ }
194
+
195
+ return { errors, warnings };
196
+ }
197
+
198
+ async findDefinition(
199
+ filePath: string,
200
+ content: string,
201
+ line: number,
202
+ character: number,
203
+ ) {
204
+ this.updateFile(filePath, content);
205
+ const sourceFile = this.service.getProgram()?.getSourceFile(filePath);
206
+ if (!sourceFile) return null;
207
+
208
+ const pos = sourceFile.getPositionOfLineAndCharacter(
209
+ line - 1,
210
+ character - 1,
211
+ );
212
+ const defs = this.service.getDefinitionAtPosition(filePath, pos);
213
+
214
+ if (!defs || defs.length === 0) return null;
215
+ const def = defs[0];
216
+ if (!def) return null;
217
+
218
+ const defFile = def.fileName;
219
+ const defStart = def.textSpan.start;
220
+
221
+ let fileContent = "";
222
+ if (this.files.has(defFile)) {
223
+ fileContent = this.files.get(defFile)!.content;
224
+ } else if (fs.existsSync(defFile)) {
225
+ fileContent = fs.readFileSync(defFile, "utf-8");
226
+ }
227
+
228
+ if (!fileContent) return null;
229
+
230
+ const tempSource = ts.createSourceFile(
231
+ defFile,
232
+ fileContent,
233
+ ts.ScriptTarget.Latest,
234
+ );
235
+ const linePos = tempSource.getLineAndCharacterOfPosition(defStart);
236
+
237
+ const lines = fileContent.split("\n");
238
+ const preview = lines
239
+ .slice(Math.max(0, linePos.line - 1), linePos.line + 2)
240
+ .join("\n");
241
+
242
+ return {
243
+ file: defFile,
244
+ line: linePos.line + 1,
245
+ preview,
246
+ };
247
+ }
248
+
249
+ async getSymbolInfo(
250
+ filePath: string,
251
+ content: string,
252
+ line: number,
253
+ character: number,
254
+ ) {
255
+ this.updateFile(filePath, content);
256
+ const sourceFile = this.service.getProgram()?.getSourceFile(filePath);
257
+ if (!sourceFile) return null;
258
+
259
+ const pos = sourceFile.getPositionOfLineAndCharacter(
260
+ line - 1,
261
+ character - 1,
262
+ );
263
+ const info = this.service.getQuickInfoAtPosition(filePath, pos);
264
+
265
+ if (!info) return null;
266
+
267
+ const displayParts = ts.displayPartsToString(info.displayParts);
268
+ const doc = ts.displayPartsToString(info.documentation);
269
+
270
+ return `${displayParts}\n${doc}`;
271
+ }
272
+
273
+ async findImplementation(
274
+ filePath: string,
275
+ content: string,
276
+ line: number,
277
+ character: number,
278
+ ) {
279
+ this.updateFile(filePath, content);
280
+ const sourceFile = this.service.getProgram()?.getSourceFile(filePath);
281
+ if (!sourceFile) return [];
282
+
283
+ const pos = sourceFile.getPositionOfLineAndCharacter(
284
+ line - 1,
285
+ character - 1,
286
+ );
287
+ const impls = this.service.getImplementationAtPosition(filePath, pos);
288
+
289
+ if (!impls) return [];
290
+
291
+ return impls.map((impl) => {
292
+ const implSource = this.service
293
+ .getProgram()
294
+ ?.getSourceFile(impl.fileName);
295
+ const line = implSource
296
+ ? implSource.getLineAndCharacterOfPosition(impl.textSpan.start).line + 1
297
+ : 1;
298
+ return { file: impl.fileName, line };
299
+ });
300
+ }
301
+
302
+ async findReferences(
303
+ filePath: string,
304
+ content: string,
305
+ line: number,
306
+ character: number,
307
+ ) {
308
+ this.updateFile(filePath, content);
309
+ const sourceFile = this.service.getProgram()?.getSourceFile(filePath);
310
+ if (!sourceFile) return [];
311
+
312
+ const pos = sourceFile.getPositionOfLineAndCharacter(
313
+ line - 1,
314
+ character - 1,
315
+ );
316
+ const refs = this.service.getReferencesAtPosition(filePath, pos);
317
+
318
+ if (!refs) return [];
319
+
320
+ return refs.map((ref) => {
321
+ const refSource = this.service.getProgram()?.getSourceFile(ref.fileName);
322
+ const line = refSource
323
+ ? refSource.getLineAndCharacterOfPosition(ref.textSpan.start).line + 1
324
+ : 1;
325
+ return { file: ref.fileName, line };
326
+ });
327
+ }
328
+
329
+ async getDocumentSymbols(filePath: string, content: string) {
330
+ this.updateFile(filePath, content);
331
+ const sourceFile = this.service.getProgram()?.getSourceFile(filePath);
332
+ if (!sourceFile) return [];
333
+
334
+ const symbols: any[] = [];
335
+
336
+ const visit = (node: ts.Node) => {
337
+ let name = "";
338
+ let kind = "";
339
+ let children: any[] = [];
340
+
341
+ // Determine node type and name
342
+ if (ts.isFunctionDeclaration(node) && node.name) {
343
+ name = node.name.text;
344
+ kind = "Function";
345
+ // Recurse for local functions/vars if needed, but usually flat is okay for top level
346
+ } else if (ts.isClassDeclaration(node) && node.name) {
347
+ name = node.name.text;
348
+ kind = "Class";
349
+ // Visit children (methods)
350
+ ts.forEachChild(node, (child) => {
351
+ if (ts.isMethodDeclaration(child) && ts.isIdentifier(child.name)) {
352
+ const line =
353
+ sourceFile.getLineAndCharacterOfPosition(child.getStart()).line +
354
+ 1;
355
+ children.push({ name: child.name.text, kind: "Method", line });
356
+ }
357
+ });
358
+ } else if (ts.isInterfaceDeclaration(node) && node.name) {
359
+ name = node.name.text;
360
+ kind = "Interface";
361
+ } else if (ts.isVariableStatement(node)) {
362
+ // Handle exposed const/let
363
+ const list = node.declarationList;
364
+ for (const decl of list.declarations) {
365
+ if (ts.isIdentifier(decl.name)) {
366
+ name = decl.name.text;
367
+ kind = "Variable";
368
+ // Note: handling only the first one if multiple allowed, but structure implies loop
369
+ // We'll just push directly here
370
+ const line =
371
+ sourceFile.getLineAndCharacterOfPosition(decl.getStart()).line +
372
+ 1;
373
+ symbols.push({ name, kind, line });
374
+ name = ""; // reset so we don't push again at end
375
+ }
376
+ }
377
+ }
378
+
379
+ if (name) {
380
+ const line =
381
+ sourceFile.getLineAndCharacterOfPosition(node.getStart()).line + 1;
382
+ symbols.push({
383
+ name,
384
+ kind,
385
+ line,
386
+ children: children.length > 0 ? children : undefined,
387
+ });
388
+ }
389
+ };
390
+
391
+ ts.forEachChild(sourceFile, visit);
392
+ return symbols;
393
+ }
394
+
395
+ async getDocumentSymbolsWithDetails(
396
+ filePath: string,
397
+ content: string,
398
+ ): Promise<import("./lspClient").DetailedSymbol[]> {
399
+ this.updateFile(filePath, content);
400
+ const sourceFile = this.service.getProgram()?.getSourceFile(filePath);
401
+ if (!sourceFile) return [];
402
+
403
+ const typeChecker = this.service.getProgram()?.getTypeChecker();
404
+ const symbols: import("./lspClient").DetailedSymbol[] = [];
405
+
406
+ const getJSDocComment = (node: ts.Node): string | undefined => {
407
+ const jsDocs = ts.getJSDocCommentsAndTags(node);
408
+ if (jsDocs.length === 0) return undefined;
409
+ const comments: string[] = [];
410
+ for (const doc of jsDocs) {
411
+ if (ts.isJSDoc(doc) && doc.comment) {
412
+ comments.push(
413
+ typeof doc.comment === "string"
414
+ ? doc.comment
415
+ : doc.comment.map((c) => c.text).join(""),
416
+ );
417
+ }
418
+ }
419
+ return comments.length > 0 ? comments.join("\n") : undefined;
420
+ };
421
+
422
+ const hasExportModifier = (node: ts.Node): boolean => {
423
+ if (!ts.canHaveModifiers(node)) return false;
424
+ const modifiers = ts.getModifiers(node);
425
+ if (!modifiers) return false;
426
+ return modifiers.some((m) => m.kind === ts.SyntaxKind.ExportKeyword);
427
+ };
428
+
429
+ const getFunctionSignature = (
430
+ node: ts.FunctionDeclaration | ts.MethodDeclaration | ts.ArrowFunction,
431
+ ): string | undefined => {
432
+ if (!typeChecker) return undefined;
433
+ try {
434
+ const signature = typeChecker.getSignatureFromDeclaration(node);
435
+ if (signature) {
436
+ return typeChecker.signatureToString(
437
+ signature,
438
+ node,
439
+ ts.TypeFormatFlags.WriteArrowStyleSignature,
440
+ );
441
+ }
442
+ } catch {
443
+ // Fallback: build manually
444
+ }
445
+ return undefined;
446
+ };
447
+
448
+ const visit = (node: ts.Node) => {
449
+ let sym: import("./lspClient").DetailedSymbol | null = null;
450
+
451
+ if (ts.isFunctionDeclaration(node) && node.name) {
452
+ sym = {
453
+ name: node.name.text,
454
+ kind: "Function",
455
+ line:
456
+ sourceFile.getLineAndCharacterOfPosition(node.getStart()).line + 1,
457
+ signature: getFunctionSignature(node),
458
+ docstring: getJSDocComment(node),
459
+ exported: hasExportModifier(node),
460
+ };
461
+ } else if (ts.isClassDeclaration(node) && node.name) {
462
+ const children: import("./lspClient").DetailedSymbol[] = [];
463
+ ts.forEachChild(node, (child) => {
464
+ if (ts.isMethodDeclaration(child) && ts.isIdentifier(child.name)) {
465
+ children.push({
466
+ name: child.name.text,
467
+ kind: "Method",
468
+ line:
469
+ sourceFile.getLineAndCharacterOfPosition(child.getStart())
470
+ .line + 1,
471
+ signature: getFunctionSignature(child),
472
+ docstring: getJSDocComment(child),
473
+ });
474
+ } else if (
475
+ ts.isPropertyDeclaration(child) &&
476
+ ts.isIdentifier(child.name)
477
+ ) {
478
+ children.push({
479
+ name: child.name.text,
480
+ kind: "Property",
481
+ line:
482
+ sourceFile.getLineAndCharacterOfPosition(child.getStart())
483
+ .line + 1,
484
+ docstring: getJSDocComment(child),
485
+ });
486
+ }
487
+ });
488
+ sym = {
489
+ name: node.name.text,
490
+ kind: "Class",
491
+ line:
492
+ sourceFile.getLineAndCharacterOfPosition(node.getStart()).line + 1,
493
+ docstring: getJSDocComment(node),
494
+ exported: hasExportModifier(node),
495
+ children: children.length > 0 ? children : undefined,
496
+ };
497
+ } else if (ts.isInterfaceDeclaration(node) && node.name) {
498
+ sym = {
499
+ name: node.name.text,
500
+ kind: "Interface",
501
+ line:
502
+ sourceFile.getLineAndCharacterOfPosition(node.getStart()).line + 1,
503
+ docstring: getJSDocComment(node),
504
+ exported: hasExportModifier(node),
505
+ };
506
+ } else if (ts.isVariableStatement(node)) {
507
+ const isExported = hasExportModifier(node);
508
+ for (const decl of node.declarationList.declarations) {
509
+ if (ts.isIdentifier(decl.name)) {
510
+ // Check if it's an arrow function assigned to const
511
+ let signature: string | undefined;
512
+ if (decl.initializer && ts.isArrowFunction(decl.initializer)) {
513
+ signature = getFunctionSignature(decl.initializer);
514
+ }
515
+ symbols.push({
516
+ name: decl.name.text,
517
+ kind: signature ? "Function" : "Variable",
518
+ line:
519
+ sourceFile.getLineAndCharacterOfPosition(decl.getStart()).line +
520
+ 1,
521
+ signature,
522
+ docstring: getJSDocComment(node),
523
+ exported: isExported,
524
+ });
525
+ }
526
+ }
527
+ }
528
+
529
+ if (sym) {
530
+ symbols.push(sym);
531
+ }
532
+ };
533
+
534
+ ts.forEachChild(sourceFile, visit);
535
+ return symbols;
536
+ }
537
+
538
+ async prepareCallHierarchy(
539
+ filePath: string,
540
+ content: string,
541
+ line: number,
542
+ character: number,
543
+ ) {
544
+ this.updateFile(filePath, content);
545
+ const sourceFile = this.service.getProgram()?.getSourceFile(filePath);
546
+ if (!sourceFile) return [];
547
+
548
+ const pos = sourceFile.getPositionOfLineAndCharacter(
549
+ line - 1,
550
+ character - 1,
551
+ );
552
+ // TypeScript service doesn't have a direct 'prepareCallHierarchy' - we simulate it
553
+ // by finding the symbol at position and returning it as a CallHierarchyItem
554
+ const defs = this.service.getDefinitionAtPosition(filePath, pos);
555
+ if (!defs || defs.length === 0) return [];
556
+
557
+ const def = defs[0];
558
+ if (!def) return [];
559
+
560
+ const defSource = this.service.getProgram()?.getSourceFile(def.fileName);
561
+ const defLine = defSource
562
+ ? defSource.getLineAndCharacterOfPosition(def.textSpan.start).line + 1
563
+ : 1;
564
+
565
+ return [
566
+ {
567
+ name: def.name,
568
+ kind: def.kind,
569
+ file: def.fileName,
570
+ line: defLine,
571
+ },
572
+ ];
573
+ }
574
+
575
+ async getOutgoingCalls(
576
+ filePath: string,
577
+ content: string,
578
+ line: number,
579
+ character: number,
580
+ ) {
581
+ this.updateFile(filePath, content);
582
+ // Note: TS LanguageService doesn't expose provideCallHierarchyOutgoingCalls directly
583
+ // widely available. We'll use a simplified AST traversal approach for now
584
+ // to find calls within the function body at the given position.
585
+
586
+ const sourceFile = this.service.getProgram()?.getSourceFile(filePath);
587
+ if (!sourceFile) return [];
588
+
589
+ const pos = sourceFile.getPositionOfLineAndCharacter(
590
+ line - 1,
591
+ character - 1,
592
+ );
593
+
594
+ // Find the function-like node containing the cursor
595
+ let node = this.findContainerNode(sourceFile, pos);
596
+ if (!node) return [];
597
+
598
+ const calls: Array<{ name: string; file: string; line: number }> = [];
599
+
600
+ const visit = (n: ts.Node) => {
601
+ if (ts.isCallExpression(n)) {
602
+ // Resolve the called symbol
603
+ const typeChecker = this.service.getProgram()?.getTypeChecker();
604
+ // Note: Using private access to program/typechecker is tricky,
605
+ // we'll try to find definition of the expression
606
+ const exprStart = n.expression.getStart();
607
+ const defs = this.service.getDefinitionAtPosition(filePath, exprStart);
608
+
609
+ if (defs && defs.length > 0) {
610
+ const def = defs[0];
611
+ if (def) {
612
+ const defSource = this.service
613
+ .getProgram()
614
+ ?.getSourceFile(def.fileName);
615
+ const defLine = defSource
616
+ ? defSource.getLineAndCharacterOfPosition(def.textSpan.start)
617
+ .line + 1
618
+ : 1;
619
+
620
+ calls.push({
621
+ name: def.name,
622
+ file: def.fileName,
623
+ line: defLine,
624
+ });
625
+ }
626
+ }
627
+ }
628
+ ts.forEachChild(n, visit);
629
+ };
630
+
631
+ visit(node);
632
+ return calls;
633
+ }
634
+
635
+ private findContainerNode(
636
+ sourceFile: ts.SourceFile,
637
+ pos: number,
638
+ ): ts.Node | null {
639
+ let fond: ts.Node | null = null;
640
+
641
+ const visit = (node: ts.Node) => {
642
+ if (node.pos <= pos && node.end >= pos) {
643
+ if (
644
+ ts.isFunctionDeclaration(node) ||
645
+ ts.isMethodDeclaration(node) ||
646
+ ts.isArrowFunction(node) ||
647
+ ts.isFunctionExpression(node)
648
+ ) {
649
+ fond = node;
650
+ }
651
+ ts.forEachChild(node, visit);
652
+ }
653
+ };
654
+
655
+ ts.forEachChild(sourceFile, visit);
656
+ return fond;
657
+ }
658
+ }
659
+
660
+ /**
661
+ * ----------------------------------------------------------------------
662
+ * TIER 2: Tree-Sitter Provider (Structure & Symbols)
663
+ * ----------------------------------------------------------------------
664
+ */
665
+ class TreeSitterProvider implements LanguageProvider {
666
+ private parser: any = null;
667
+ private lang: any = null;
668
+ private isReady = false;
669
+
670
+ private initPromise: Promise<void> | null = null;
671
+
672
+ constructor(
673
+ private languageName: string,
674
+ private wasmPath: string,
675
+ ) {
676
+ this.initPromise = this.init();
677
+ }
678
+
679
+ private async init() {
680
+ try {
681
+ if (!fs.existsSync(this.wasmPath)) {
682
+ console.error(`[TreeSitter] WASM file not found: ${this.wasmPath}`);
683
+ return;
684
+ }
685
+ await Parser.init();
686
+ this.lang = await Language.load(this.wasmPath);
687
+ this.parser = new Parser();
688
+ this.parser.setLanguage(this.lang);
689
+ this.isReady = true;
690
+ } catch (e) {
691
+ console.error(`[TreeSitter] Init failed for ${this.languageName}:`, e);
692
+ }
693
+ }
694
+
695
+ /**
696
+ * Ensure initialization is complete before using the parser.
697
+ */
698
+ async ensureReady(): Promise<boolean> {
699
+ if (this.initPromise) {
700
+ await this.initPromise;
701
+ }
702
+ return this.isReady;
703
+ }
704
+
705
+ isSupported(ext: string): boolean {
706
+ return this.isReady;
707
+ }
708
+
709
+ async getDiagnostics() {
710
+ return { errors: [], warnings: [] };
711
+ }
712
+
713
+ async findDefinition(
714
+ filePath: string,
715
+ content: string,
716
+ line: number,
717
+ character: number,
718
+ ) {
719
+ if (!this.parser || !this.lang) return null;
720
+ const tree = this.parser.parse(content);
721
+
722
+ // Simple heuristic: search for definition of symbol at cursor
723
+ // Need exact logic for determining symbol at cursor for TS (row/col)
724
+ // Assume we can get the text
725
+ const lines = content.split("\n");
726
+ const docLine = lines[line - 1] || "";
727
+ // Crude extraction of word
728
+ const match = docLine.slice(0, character).match(/[a-zA-Z0-9_]+$/);
729
+ const suffix = docLine.slice(character).match(/^[a-zA-Z0-9_]+/);
730
+ const word = (match ? match[0] : "") + (suffix ? suffix[0] : "");
731
+ if (!word) return null;
732
+
733
+ const symbols = await this.getDocumentSymbols(filePath, content);
734
+ const found = this.findSymbolRecursive(symbols, word);
735
+ if (found) {
736
+ const preview = lines
737
+ .slice(Math.max(0, found.line - 1), found.line + 2)
738
+ .join("\n");
739
+ return { file: filePath, line: found.line, preview };
740
+ }
741
+ return null;
742
+ }
743
+
744
+ private findSymbolRecursive(symbols: any[], name: string): any {
745
+ for (const s of symbols) {
746
+ if (s.name === name) return s;
747
+ if (s.children) {
748
+ const found = this.findSymbolRecursive(s.children, name);
749
+ if (found) return found;
750
+ }
751
+ }
752
+ return null;
753
+ }
754
+
755
+ async getSymbolInfo(
756
+ filePath: string,
757
+ content: string,
758
+ line: number,
759
+ character: number,
760
+ ) {
761
+ const def = await this.findDefinition(filePath, content, line, character);
762
+ if (def && def.preview) {
763
+ return `Definition:\n${def.preview}`;
764
+ }
765
+ return null;
766
+ }
767
+
768
+ async findImplementation(
769
+ filePath: string,
770
+ content: string,
771
+ line: number,
772
+ character: number,
773
+ ) {
774
+ return []; // Not implemented for TreeSitter yet
775
+ }
776
+
777
+ async findReferences() {
778
+ return [];
779
+ }
780
+
781
+ async getDocumentSymbols(filePath: string, content: string) {
782
+ if (!this.parser || !this.lang) return [];
783
+ const tree = this.parser.parse(content);
784
+
785
+ const ext = filePath.split(".").pop() || "";
786
+ const queries = getQueriesForExtension(ext);
787
+ if (!queries) return [];
788
+
789
+ try {
790
+ const query = new Query(this.lang, queries.definitions);
791
+ const matches = query.matches(tree.rootNode);
792
+
793
+ const symbols = matches.map((m: any) => {
794
+ const nameNode = m.captures.find((c: any) => c.name === "name")?.node;
795
+ const defNode = m.captures.find((c: any) => c.name === "def")?.node;
796
+
797
+ // Determine kind based on node type
798
+ let kind = "Function";
799
+ if (defNode?.type.includes("class")) kind = "Class";
800
+ else if (defNode?.type.includes("struct")) kind = "Struct";
801
+ else if (defNode?.type.includes("enum")) kind = "Enum";
802
+ else if (defNode?.type.includes("trait")) kind = "Trait";
803
+ else if (defNode?.type.includes("interface")) kind = "Interface";
804
+
805
+ return {
806
+ name: nameNode?.text || "anonymous",
807
+ kind,
808
+ line: (defNode?.startPosition.row || 0) + 1,
809
+ endLine: (defNode?.endPosition.row || 0) + 1,
810
+ };
811
+ });
812
+
813
+ return this.buildHierarchy(
814
+ symbols.sort((a: any, b: any) => a.line - b.line),
815
+ );
816
+ } catch {
817
+ return [];
818
+ }
819
+ }
820
+
821
+ /**
822
+ * Build hierarchical symbol tree from flat list based on line ranges.
823
+ */
824
+ private buildHierarchy(flatSymbols: any[]): any[] {
825
+ const root: any[] = [];
826
+ const stack: any[] = [];
827
+
828
+ for (const sym of flatSymbols) {
829
+ // Pop items from stack that don't contain this symbol
830
+ while (
831
+ stack.length > 0 &&
832
+ (stack[stack.length - 1].endLine < sym.line ||
833
+ stack[stack.length - 1].endLine < sym.endLine) // Ensure proper nesting
834
+ ) {
835
+ stack.pop();
836
+ }
837
+
838
+ if (stack.length === 0) {
839
+ root.push(sym);
840
+ } else {
841
+ const parent = stack[stack.length - 1];
842
+ if (!parent.children) parent.children = [];
843
+ parent.children.push(sym);
844
+ }
845
+
846
+ // Push current symbol to stack if it can have children (classes, etc)
847
+ // Function definitions can also have local functions
848
+ stack.push(sym);
849
+ }
850
+
851
+ return root;
852
+ }
853
+
854
+ /**
855
+ * Collect all nodes of specified types from the AST tree.
856
+ * This is the proven approach from analyzer.ts.
857
+ */
858
+ private collectNodes(node: any, wantedTypes: Set<string>): any[] {
859
+ const nodes: any[] = [];
860
+ if (wantedTypes.has(node.type)) {
861
+ nodes.push(node);
862
+ }
863
+ for (const child of node.children || []) {
864
+ if (child !== null) {
865
+ nodes.push(...this.collectNodes(child, wantedTypes));
866
+ }
867
+ }
868
+ return nodes;
869
+ }
870
+
871
+ /**
872
+ * Find the nearest parent node of specified types.
873
+ */
874
+ private findParent(node: any, wantedTypes: Set<string>): any | null {
875
+ let curr = node.parent;
876
+ while (curr) {
877
+ if (wantedTypes.has(curr.type)) {
878
+ return curr;
879
+ }
880
+ curr = curr.parent;
881
+ }
882
+ return null;
883
+ }
884
+
885
+ async getImports(
886
+ filePath: string,
887
+ content: string,
888
+ ): Promise<Array<{ module: string; alias?: string; isRelative: boolean }>> {
889
+ if (!this.parser || !this.lang) return [];
890
+ const tree = this.parser.parse(content);
891
+ if (!tree) return [];
892
+
893
+ const imports: Array<{
894
+ module: string;
895
+ alias?: string;
896
+ isRelative: boolean;
897
+ }> = [];
898
+
899
+ // Python imports - using AST walking like analyzer.ts
900
+ if (this.languageName === "python") {
901
+ const importNodes = this.collectNodes(
902
+ tree.rootNode,
903
+ new Set(["import_statement", "import_from_statement"]),
904
+ );
905
+
906
+ for (const node of importNodes) {
907
+ if (node.type === "import_statement") {
908
+ // import foo, import foo as bar
909
+ const dottedNames = node.descendantsOfType?.("dotted_name") || [];
910
+ for (const n of dottedNames) {
911
+ if (n?.text) {
912
+ imports.push({ module: n.text, isRelative: false });
913
+ }
914
+ }
915
+ const aliased = node.descendantsOfType?.("aliased_import") || [];
916
+ for (const n of aliased) {
917
+ const mod = n.child?.(0)?.text;
918
+ const alias = n.child?.(2)?.text;
919
+ if (mod) {
920
+ imports.push({ module: mod, alias, isRelative: false });
921
+ }
922
+ }
923
+ } else if (node.type === "import_from_statement") {
924
+ // from foo import bar
925
+ const moduleName = node.childForFieldName?.("module_name")?.text;
926
+ if (moduleName) {
927
+ const isRelative = moduleName.startsWith(".");
928
+ imports.push({ module: moduleName, isRelative });
929
+ }
930
+ }
931
+ }
932
+ }
933
+
934
+ return imports;
935
+ }
936
+
937
+ async getOutgoingCalls(
938
+ filePath: string,
939
+ content: string,
940
+ line: number,
941
+ character: number,
942
+ ): Promise<
943
+ Array<{ name: string; file: string; line: number; sourceContext?: string }>
944
+ > {
945
+ if (!this.parser || !this.lang) return [];
946
+ const tree = this.parser.parse(content);
947
+ if (!tree) return [];
948
+
949
+ const calls: Array<{
950
+ name: string;
951
+ file: string;
952
+ line: number;
953
+ sourceContext?: string;
954
+ }> = [];
955
+
956
+ // Python calls - using AST walking like analyzer.ts
957
+ if (this.languageName === "python") {
958
+ const defTypes = new Set(["function_definition", "class_definition"]);
959
+ const callNodes = this.collectNodes(tree.rootNode, new Set(["call"]));
960
+
961
+ for (const callNode of callNodes) {
962
+ // Get function name from the call
963
+ const funcNode = callNode.childForFieldName?.("function");
964
+ if (!funcNode) continue;
965
+
966
+ let funcName = funcNode.text;
967
+ const callLine = callNode.startPosition.row + 1;
968
+
969
+ // Handle attribute access (e.g., obj.method -> keep full path for resolution)
970
+ // For the name, extract just the function/method name
971
+ if (funcNode.type === "attribute") {
972
+ const parts = funcName.split(".");
973
+ funcName = parts[parts.length - 1] || funcName;
974
+ }
975
+
976
+ // Find the containing function/class to set sourceContext
977
+ const parentDef = this.findParent(callNode, defTypes);
978
+ const sourceContext = parentDef?.childForFieldName?.("name")?.text;
979
+
980
+ calls.push({
981
+ name: funcName,
982
+ file: filePath,
983
+ line: callLine,
984
+ sourceContext,
985
+ });
986
+ }
987
+ }
988
+
989
+ return calls;
990
+ }
991
+
992
+ async getInheritance(
993
+ filePath: string,
994
+ content: string,
995
+ ): Promise<Array<{ className: string; parentName: string; line: number }>> {
996
+ if (!this.parser || !this.lang) return [];
997
+ const tree = this.parser.parse(content);
998
+
999
+ const ext = filePath.split(".").pop() || "";
1000
+ const queries = getQueriesForExtension(ext);
1001
+ if (!queries?.inheritance) return [];
1002
+
1003
+ const inheritance: Array<{
1004
+ className: string;
1005
+ parentName: string;
1006
+ line: number;
1007
+ }> = [];
1008
+
1009
+ try {
1010
+ const query = new Query(this.lang, queries.inheritance);
1011
+ const matches = query.matches(tree.rootNode);
1012
+
1013
+ for (const m of matches) {
1014
+ const classNode = m.captures.find(
1015
+ (c: any) => c.name === "class_name",
1016
+ )?.node;
1017
+ const parentNode = m.captures.find(
1018
+ (c: any) => c.name === "parent",
1019
+ )?.node;
1020
+
1021
+ if (classNode && parentNode) {
1022
+ inheritance.push({
1023
+ className: classNode.text,
1024
+ parentName: parentNode.text,
1025
+ line: classNode.startPosition.row + 1,
1026
+ });
1027
+ }
1028
+ }
1029
+ } catch {
1030
+ // Query failed
1031
+ }
1032
+
1033
+ return inheritance;
1034
+ }
1035
+ }
1036
+
1037
+ /**
1038
+ * ----------------------------------------------------------------------
1039
+ * TIER 3: Regex Provider (Fallback)
1040
+ * ----------------------------------------------------------------------
1041
+ */
1042
+ class RegexProvider implements LanguageProvider {
1043
+ private config: Record<
1044
+ string,
1045
+ {
1046
+ defPattern: RegExp;
1047
+ kindMap: (match: RegExpMatchArray) => string;
1048
+ nameIdx: number;
1049
+ extensions: string[];
1050
+ }
1051
+ > = {
1052
+ python: {
1053
+ defPattern: /^\s*(?:async\s+)?(def|class)\s+([a-zA-Z_][a-zA-Z0-9_]*)/gm,
1054
+ kindMap: (m) => (m[1] === "class" ? "Class" : "Function"),
1055
+ nameIdx: 2,
1056
+ extensions: ["py"],
1057
+ },
1058
+ rust: {
1059
+ defPattern:
1060
+ /^\s*(?:pub\s+)?(fn|struct|enum|trait|impl)\s+([a-zA-Z_][a-zA-Z0-9_]*)/gm,
1061
+ kindMap: (m) => {
1062
+ const type = m[1] || "";
1063
+ return type.charAt(0).toUpperCase() + type.slice(1);
1064
+ },
1065
+ nameIdx: 2,
1066
+ extensions: ["rs"],
1067
+ },
1068
+ go: {
1069
+ defPattern: /^\s*func\s+(?:.*?\s+)?([a-zA-Z_][a-zA-Z0-9_]*)\(/gm,
1070
+ kindMap: () => "Function",
1071
+ nameIdx: 1,
1072
+ extensions: ["go"],
1073
+ },
1074
+ default: {
1075
+ defPattern: /^\s*(function|class|interface)\s+([a-zA-Z_][a-zA-Z0-9_]*)/gm,
1076
+ kindMap: (m) => m[1] || "Unknown",
1077
+ nameIdx: 2,
1078
+ extensions: [],
1079
+ },
1080
+ };
1081
+
1082
+ isSupported(ext: string): boolean {
1083
+ return true;
1084
+ }
1085
+
1086
+ private getConfig(ext: string) {
1087
+ for (const key in this.config) {
1088
+ if (this.config[key]!.extensions.includes(ext)) {
1089
+ return this.config[key]!;
1090
+ }
1091
+ }
1092
+ return this.config.default!;
1093
+ }
1094
+
1095
+ async getDiagnostics() {
1096
+ return { errors: [], warnings: [] };
1097
+ }
1098
+
1099
+ async findDefinition(
1100
+ filePath: string,
1101
+ content: string,
1102
+ line: number,
1103
+ character: number,
1104
+ ) {
1105
+ const lines = content.split("\n");
1106
+ const docLine = lines[line - 1];
1107
+ if (!docLine) return null;
1108
+
1109
+ // Extract word at cursor
1110
+ const match = docLine.slice(0, character).match(/[a-zA-Z0-9_]+$/);
1111
+ const suffix = docLine.slice(character).match(/^[a-zA-Z0-9_]+/);
1112
+ const word = (match ? match[0] : "") + (suffix ? suffix[0] : "");
1113
+ if (!word) return null;
1114
+
1115
+ const ext = filePath.split(".").pop() || "";
1116
+ const cfg = this.getConfig(ext);
1117
+
1118
+ for (let i = 0; i < lines.length; i++) {
1119
+ const lineContent = lines[i]!;
1120
+ if (lineContent.includes(word)) {
1121
+ const regex = new RegExp(cfg.defPattern.source, "gm");
1122
+ let match;
1123
+ while ((match = regex.exec(lineContent)) !== null) {
1124
+ if (match[cfg.nameIdx] === word) {
1125
+ const preview = lines.slice(Math.max(0, i - 1), i + 2).join("\n");
1126
+ return { file: filePath, line: i + 1, preview };
1127
+ }
1128
+ }
1129
+ }
1130
+ }
1131
+
1132
+ return null;
1133
+ }
1134
+
1135
+ async getSymbolInfo(
1136
+ filePath: string,
1137
+ content: string,
1138
+ line: number,
1139
+ character: number,
1140
+ ) {
1141
+ const def = await this.findDefinition(filePath, content, line, character);
1142
+ if (def) return `Defined at line ${def.line}`;
1143
+ return null;
1144
+ }
1145
+
1146
+ async findImplementation() {
1147
+ return [];
1148
+ }
1149
+
1150
+ async findReferences(
1151
+ filePath: string,
1152
+ content: string,
1153
+ line: number,
1154
+ character: number,
1155
+ ) {
1156
+ const lines = content.split("\n");
1157
+ const docLine = lines[line - 1];
1158
+ if (!docLine) return [];
1159
+
1160
+ const match = docLine.slice(0, character).match(/[a-zA-Z0-9_]+$/);
1161
+ const suffix = docLine.slice(character).match(/^[a-zA-Z0-9_]+/);
1162
+ const word = (match ? match[0] : "") + (suffix ? suffix[0] : "");
1163
+ if (!word) return [];
1164
+
1165
+ const refs: Array<{ file: string; line: number }> = [];
1166
+ for (let i = 0; i < lines.length; i++) {
1167
+ if (lines[i]!.includes(word)) {
1168
+ refs.push({ file: filePath, line: i + 1 });
1169
+ }
1170
+ }
1171
+ return refs;
1172
+ }
1173
+
1174
+ async getDocumentSymbols(filePath: string, content: string) {
1175
+ const ext = filePath.split(".").pop() || "";
1176
+ const cfg = this.getConfig(ext);
1177
+
1178
+ const symbols: any[] = [];
1179
+ const lines = content.split("\n");
1180
+
1181
+ for (let i = 0; i < lines.length; i++) {
1182
+ const regex = new RegExp(cfg.defPattern.source, "g");
1183
+ let match;
1184
+ while ((match = regex.exec(lines[i]!)) !== null) {
1185
+ // Regex provider can't reliably determine end lines without parsing blocks
1186
+ // So we return flat list for now, or could try indentation heuristics
1187
+ symbols.push({
1188
+ name: match[cfg.nameIdx],
1189
+ kind: cfg.kindMap(match),
1190
+ line: i + 1,
1191
+ // Fallback: assume single line or try to guess?
1192
+ // For now, regex provider remains flat or we can try to guess scope by indentation
1193
+ endLine: i + 1, // Basic assumption
1194
+ });
1195
+ }
1196
+ }
1197
+
1198
+ return symbols;
1199
+ }
1200
+ }
1201
+
1202
+ /**
1203
+ * ----------------------------------------------------------------------
1204
+ * CLIENT: Universal LSP Client
1205
+ * ----------------------------------------------------------------------
1206
+ */
1207
+ export class UniversalLSPClient {
1208
+ private tsProvider: TypeScriptProvider;
1209
+ private pyProvider: TreeSitterProvider;
1210
+ private regexProvider: RegexProvider;
1211
+
1212
+ constructor(private rootPath: string = process.cwd()) {
1213
+ this.tsProvider = new TypeScriptProvider(rootPath);
1214
+ this.regexProvider = new RegexProvider();
1215
+
1216
+ // Use import.meta.dir for Bun to get module-relative path
1217
+ // Fall back to rootPath for Node.js compatibility
1218
+ // Find the project root robustly for both development and bundled production
1219
+ const currentDir = import.meta.dir;
1220
+ let moduleDir = currentDir;
1221
+
1222
+ // If in dist/ or src/utils/, go up until we find the root
1223
+ if (currentDir.endsWith("dist") || currentDir.endsWith("utils")) {
1224
+ moduleDir = path.resolve(currentDir, "..");
1225
+ if (currentDir.endsWith("utils")) {
1226
+ moduleDir = path.resolve(moduleDir, "..");
1227
+ }
1228
+ }
1229
+
1230
+ const pythonWasm = path.resolve(
1231
+ moduleDir,
1232
+ "src/parsers/tree-sitter-python.wasm",
1233
+ );
1234
+ this.pyProvider = new TreeSitterProvider("python", pythonWasm);
1235
+ }
1236
+
1237
+ private async getProvider(filePath: string): Promise<LanguageProvider> {
1238
+ const ext = filePath.split(".").pop() || "";
1239
+
1240
+ if (this.tsProvider.isSupported(ext)) {
1241
+ return this.tsProvider;
1242
+ }
1243
+
1244
+ // For Python, ensure tree-sitter is ready before checking
1245
+ if (ext === "py") {
1246
+ const ready = await this.pyProvider.ensureReady();
1247
+ if (ready) {
1248
+ return this.pyProvider;
1249
+ }
1250
+ }
1251
+
1252
+ return this.regexProvider;
1253
+ }
1254
+
1255
+ async getDiagnostics(filePath: string, content: string) {
1256
+ const provider = await this.getProvider(filePath);
1257
+ return provider.getDiagnostics(filePath, content);
1258
+ }
1259
+
1260
+ async findDefinition(
1261
+ filePath: string,
1262
+ content: string,
1263
+ line: number,
1264
+ character: number,
1265
+ ) {
1266
+ const provider = await this.getProvider(filePath);
1267
+ return provider.findDefinition(filePath, content, line, character);
1268
+ }
1269
+
1270
+ async findReferences(
1271
+ filePath: string,
1272
+ content: string,
1273
+ line: number,
1274
+ character: number,
1275
+ ) {
1276
+ const provider = await this.getProvider(filePath);
1277
+ return provider.findReferences(filePath, content, line, character);
1278
+ }
1279
+
1280
+ async getDocumentSymbols(filePath: string, content: string) {
1281
+ const provider = await this.getProvider(filePath);
1282
+ return provider.getDocumentSymbols(filePath, content);
1283
+ }
1284
+
1285
+ async getSymbolInfo(
1286
+ filePath: string,
1287
+ content: string,
1288
+ line: number,
1289
+ character: number,
1290
+ ) {
1291
+ const provider = await this.getProvider(filePath);
1292
+ return provider.getSymbolInfo(filePath, content, line, character);
1293
+ }
1294
+
1295
+ async findImplementation(
1296
+ filePath: string,
1297
+ content: string,
1298
+ line: number,
1299
+ character: number,
1300
+ ) {
1301
+ const provider = await this.getProvider(filePath);
1302
+ return provider.findImplementation(filePath, content, line, character);
1303
+ }
1304
+
1305
+ async prepareCallHierarchy(
1306
+ filePath: string,
1307
+ content: string,
1308
+ line: number,
1309
+ character: number,
1310
+ ) {
1311
+ const provider = await this.getProvider(filePath);
1312
+ return provider.prepareCallHierarchy
1313
+ ? provider.prepareCallHierarchy(filePath, content, line, character)
1314
+ : [];
1315
+ }
1316
+
1317
+ async getIncomingCalls(
1318
+ filePath: string,
1319
+ content: string,
1320
+ line: number,
1321
+ character: number,
1322
+ ) {
1323
+ const provider = await this.getProvider(filePath);
1324
+ return provider.getIncomingCalls
1325
+ ? provider.getIncomingCalls(filePath, content, line, character)
1326
+ : [];
1327
+ }
1328
+
1329
+ async getOutgoingCalls(
1330
+ filePath: string,
1331
+ content: string,
1332
+ line: number,
1333
+ character: number,
1334
+ ) {
1335
+ const provider = await this.getProvider(filePath);
1336
+ return provider.getOutgoingCalls
1337
+ ? provider.getOutgoingCalls(filePath, content, line, character)
1338
+ : [];
1339
+ }
1340
+
1341
+ async searchWorkspaceSymbols(query: string, filePath: string) {
1342
+ const provider = await this.getProvider(filePath);
1343
+ return provider.searchWorkspaceSymbols
1344
+ ? provider.searchWorkspaceSymbols(query)
1345
+ : [];
1346
+ }
1347
+
1348
+ async getImports(filePath: string, content: string) {
1349
+ const provider = await this.getProvider(filePath);
1350
+ return provider.getImports ? provider.getImports(filePath, content) : [];
1351
+ }
1352
+
1353
+ async getInheritance(filePath: string, content: string) {
1354
+ const provider = await this.getProvider(filePath);
1355
+ return provider.getInheritance
1356
+ ? provider.getInheritance(filePath, content)
1357
+ : [];
1358
+ }
1359
+
1360
+ async getDocumentSymbolsWithDetails(
1361
+ filePath: string,
1362
+ content: string,
1363
+ ): Promise<DetailedSymbol[]> {
1364
+ const provider = await this.getProvider(filePath);
1365
+ return provider.getDocumentSymbolsWithDetails
1366
+ ? provider.getDocumentSymbolsWithDetails(filePath, content)
1367
+ : this.fallbackToBasicSymbols(filePath, content);
1368
+ }
1369
+
1370
+ private async fallbackToBasicSymbols(
1371
+ filePath: string,
1372
+ content: string,
1373
+ ): Promise<DetailedSymbol[]> {
1374
+ // Convert basic symbols to detailed format when provider doesn't support it
1375
+ const provider = await this.getProvider(filePath);
1376
+ const basic = await provider.getDocumentSymbols(filePath, content);
1377
+ return basic.map((s) => ({
1378
+ name: s.name,
1379
+ kind: s.kind,
1380
+ line: s.line,
1381
+ children: s.children?.map((c: any) => ({
1382
+ name: c.name,
1383
+ kind: c.kind,
1384
+ line: c.line,
1385
+ })),
1386
+ }));
1387
+ }
1388
+
1389
+ isSupported(filePath: string): boolean {
1390
+ return true;
1391
+ }
1392
+
1393
+ getSupportedExtensions(): string[] {
1394
+ return ["ts", "js", "py", "rs", "go", "*"];
1395
+ }
1396
+
1397
+ shutdown() {
1398
+ // No-op
1399
+ }
1400
+ }
1401
+
1402
+ let clientInstance: UniversalLSPClient | null = null;
1403
+
1404
+ export function getLSPClient(rootPath?: string): UniversalLSPClient {
1405
+ if (!clientInstance) {
1406
+ clientInstance = new UniversalLSPClient(rootPath);
1407
+ }
1408
+ return clientInstance;
1409
+ }
1410
+
1411
+ export function resetLSPClient(): void {
1412
+ clientInstance = null;
1413
+ }
1414
+
1415
+ export default UniversalLSPClient;