velune-cli 0.9.0__py3-none-any.whl

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 (279) hide show
  1. velune/__init__.py +5 -0
  2. velune/__main__.py +6 -0
  3. velune/cli/__init__.py +5 -0
  4. velune/cli/app.py +208 -0
  5. velune/cli/autocomplete.py +80 -0
  6. velune/cli/banner.py +60 -0
  7. velune/cli/commands/__init__.py +32 -0
  8. velune/cli/commands/ask.py +175 -0
  9. velune/cli/commands/base.py +16 -0
  10. velune/cli/commands/chat.py +228 -0
  11. velune/cli/commands/config.py +224 -0
  12. velune/cli/commands/daemon.py +88 -0
  13. velune/cli/commands/doctor.py +721 -0
  14. velune/cli/commands/init.py +170 -0
  15. velune/cli/commands/mcp.py +82 -0
  16. velune/cli/commands/memory.py +293 -0
  17. velune/cli/commands/models.py +683 -0
  18. velune/cli/commands/preflight.py +95 -0
  19. velune/cli/commands/run.py +270 -0
  20. velune/cli/commands/setup.py +184 -0
  21. velune/cli/commands/workspace.py +249 -0
  22. velune/cli/context.py +36 -0
  23. velune/cli/councilmodel_ui.py +199 -0
  24. velune/cli/display/council_view.py +254 -0
  25. velune/cli/display/memory_view.py +126 -0
  26. velune/cli/display/panels.py +35 -0
  27. velune/cli/display/progress.py +25 -0
  28. velune/cli/display/themes.py +25 -0
  29. velune/cli/main.py +15 -0
  30. velune/cli/model_selector.py +51 -0
  31. velune/cli/modes.py +86 -0
  32. velune/cli/pull_ui.py +123 -0
  33. velune/cli/registry.py +80 -0
  34. velune/cli/rendering/__init__.py +5 -0
  35. velune/cli/rendering/error_panel.py +79 -0
  36. velune/cli/rendering/markdown.py +63 -0
  37. velune/cli/repl.py +1855 -0
  38. velune/cli/session_manager.py +71 -0
  39. velune/cli/slash_commands.py +37 -0
  40. velune/cli/theme.py +8 -0
  41. velune/cognition/__init__.py +23 -0
  42. velune/cognition/agents/__init__.py +7 -0
  43. velune/cognition/agents/coder.py +209 -0
  44. velune/cognition/agents/planner.py +156 -0
  45. velune/cognition/agents/reviewer.py +195 -0
  46. velune/cognition/arbitrator.py +220 -0
  47. velune/cognition/architecture.py +415 -0
  48. velune/cognition/budget.py +65 -0
  49. velune/cognition/council/__init__.py +47 -0
  50. velune/cognition/council/base.py +217 -0
  51. velune/cognition/council/challenger.py +74 -0
  52. velune/cognition/council/coder.py +79 -0
  53. velune/cognition/council/critic_agent.py +43 -0
  54. velune/cognition/council/critic_configs.py +111 -0
  55. velune/cognition/council/critics.py +41 -0
  56. velune/cognition/council/debate.py +46 -0
  57. velune/cognition/council/factory.py +140 -0
  58. velune/cognition/council/messages.py +56 -0
  59. velune/cognition/council/planner.py +124 -0
  60. velune/cognition/council/reviewer.py +74 -0
  61. velune/cognition/council/synthesizer.py +67 -0
  62. velune/cognition/council/tiers.py +188 -0
  63. velune/cognition/council_orchestrator.py +282 -0
  64. velune/cognition/firewall.py +354 -0
  65. velune/cognition/module.py +46 -0
  66. velune/cognition/orchestrator.py +1205 -0
  67. velune/cognition/personality.py +238 -0
  68. velune/cognition/state.py +104 -0
  69. velune/cognition/style_resolver.py +64 -0
  70. velune/cognition/verification.py +205 -0
  71. velune/context/__init__.py +28 -0
  72. velune/context/assembler.py +240 -0
  73. velune/context/budget.py +97 -0
  74. velune/context/extractive.py +95 -0
  75. velune/context/prompt_adaptation.py +480 -0
  76. velune/context/sections.py +99 -0
  77. velune/context/token_counter.py +134 -0
  78. velune/context/utilization.py +33 -0
  79. velune/context/window.py +63 -0
  80. velune/core/__init__.py +89 -0
  81. velune/core/background.py +5 -0
  82. velune/core/config/__init__.py +37 -0
  83. velune/core/errors/__init__.py +90 -0
  84. velune/core/errors/catalog.py +188 -0
  85. velune/core/errors/execution.py +31 -0
  86. velune/core/errors/memory.py +25 -0
  87. velune/core/errors/orchestration.py +31 -0
  88. velune/core/errors/provider.py +37 -0
  89. velune/core/event_loop.py +35 -0
  90. velune/core/logging.py +83 -0
  91. velune/core/paths.py +165 -0
  92. velune/core/runtime.py +113 -0
  93. velune/core/startup_profiler.py +56 -0
  94. velune/core/task_registry.py +117 -0
  95. velune/core/trace.py +83 -0
  96. velune/core/types/__init__.py +48 -0
  97. velune/core/types/agent.py +53 -0
  98. velune/core/types/context.py +42 -0
  99. velune/core/types/inference.py +38 -0
  100. velune/core/types/memory.py +42 -0
  101. velune/core/types/model.py +70 -0
  102. velune/core/types/provider.py +62 -0
  103. velune/core/types/repository.py +38 -0
  104. velune/core/types/task.py +61 -0
  105. velune/core/types/workspace.py +28 -0
  106. velune/daemon/client.py +13 -0
  107. velune/daemon/server.py +127 -0
  108. velune/daemon/transport.py +179 -0
  109. velune/events.py +204 -0
  110. velune/execution/__init__.py +22 -0
  111. velune/execution/benchmarker.py +315 -0
  112. velune/execution/cancellation.py +53 -0
  113. velune/execution/checkpointer.py +130 -0
  114. velune/execution/command_spec.py +165 -0
  115. velune/execution/diff_preview.py +197 -0
  116. velune/execution/executor.py +181 -0
  117. velune/execution/module.py +18 -0
  118. velune/execution/multi_diff.py +67 -0
  119. velune/execution/path_guard.py +74 -0
  120. velune/execution/planner.py +91 -0
  121. velune/execution/rollback.py +89 -0
  122. velune/execution/sandbox.py +268 -0
  123. velune/execution/validator.py +115 -0
  124. velune/hardware/__init__.py +1 -0
  125. velune/hardware/detector.py +192 -0
  126. velune/kernel/__init__.py +55 -0
  127. velune/kernel/bootstrap.py +125 -0
  128. velune/kernel/config.py +426 -0
  129. velune/kernel/entrypoint.py +78 -0
  130. velune/kernel/health.py +54 -0
  131. velune/kernel/lifecycle.py +143 -0
  132. velune/kernel/module.py +17 -0
  133. velune/kernel/modules.py +23 -0
  134. velune/kernel/registry.py +96 -0
  135. velune/kernel/schemas.py +28 -0
  136. velune/main.py +9 -0
  137. velune/mcp/__init__.py +9 -0
  138. velune/mcp/client.py +115 -0
  139. velune/mcp/config.py +19 -0
  140. velune/mcp/server.py +624 -0
  141. velune/memory/__init__.py +32 -0
  142. velune/memory/compaction.py +506 -0
  143. velune/memory/embedding_pipeline.py +241 -0
  144. velune/memory/lifecycle.py +680 -0
  145. velune/memory/module.py +218 -0
  146. velune/memory/prioritizer.py +67 -0
  147. velune/memory/storage/episodic_schema.sql +53 -0
  148. velune/memory/storage/lancedb_store.py +282 -0
  149. velune/memory/storage/sqlite_manager.py +369 -0
  150. velune/memory/storage/sqlite_pool.py +149 -0
  151. velune/memory/tiers/episodic.py +588 -0
  152. velune/memory/tiers/graph.py +378 -0
  153. velune/memory/tiers/lineage.py +416 -0
  154. velune/memory/tiers/semantic.py +475 -0
  155. velune/memory/tiers/working.py +168 -0
  156. velune/memory/vitality.py +132 -0
  157. velune/models/__init__.py +15 -0
  158. velune/models/family.py +76 -0
  159. velune/models/module.py +20 -0
  160. velune/models/probes.py +192 -0
  161. velune/models/profile_cache.py +84 -0
  162. velune/models/profiler.py +108 -0
  163. velune/models/registry.py +251 -0
  164. velune/models/scorer.py +233 -0
  165. velune/models/specializations.py +205 -0
  166. velune/orchestration/__init__.py +19 -0
  167. velune/orchestration/engine.py +239 -0
  168. velune/orchestration/module.py +15 -0
  169. velune/orchestration/role_assignments.py +82 -0
  170. velune/orchestration/schemas.py +98 -0
  171. velune/plugins/__init__.py +20 -0
  172. velune/plugins/hooks.py +50 -0
  173. velune/plugins/loader.py +161 -0
  174. velune/plugins/registry.py +56 -0
  175. velune/plugins/schemas.py +21 -0
  176. velune/providers/__init__.py +23 -0
  177. velune/providers/adapters/anthropic.py +257 -0
  178. velune/providers/adapters/fireworks.py +115 -0
  179. velune/providers/adapters/google.py +234 -0
  180. velune/providers/adapters/groq.py +151 -0
  181. velune/providers/adapters/huggingface.py +210 -0
  182. velune/providers/adapters/llamacpp.py +208 -0
  183. velune/providers/adapters/lmstudio.py +175 -0
  184. velune/providers/adapters/ollama.py +233 -0
  185. velune/providers/adapters/openai.py +213 -0
  186. velune/providers/adapters/openrouter.py +81 -0
  187. velune/providers/adapters/together.py +134 -0
  188. velune/providers/adapters/xai.py +60 -0
  189. velune/providers/base.py +86 -0
  190. velune/providers/benchmarker.py +138 -0
  191. velune/providers/discovery/__init__.py +33 -0
  192. velune/providers/discovery/anthropic.py +79 -0
  193. velune/providers/discovery/benchmarks.py +44 -0
  194. velune/providers/discovery/classifier.py +69 -0
  195. velune/providers/discovery/fireworks.py +95 -0
  196. velune/providers/discovery/gguf.py +88 -0
  197. velune/providers/discovery/google.py +95 -0
  198. velune/providers/discovery/gpu.py +117 -0
  199. velune/providers/discovery/groq.py +21 -0
  200. velune/providers/discovery/huggingface.py +67 -0
  201. velune/providers/discovery/lmstudio.py +80 -0
  202. velune/providers/discovery/ollama.py +162 -0
  203. velune/providers/discovery/openai.py +96 -0
  204. velune/providers/discovery/openrouter.py +113 -0
  205. velune/providers/discovery/scanner.py +115 -0
  206. velune/providers/discovery/together.py +114 -0
  207. velune/providers/discovery/xai.py +57 -0
  208. velune/providers/health.py +67 -0
  209. velune/providers/health_monitor.py +169 -0
  210. velune/providers/keystore.py +142 -0
  211. velune/providers/local_paths.py +49 -0
  212. velune/providers/local_resolver.py +229 -0
  213. velune/providers/module.py +51 -0
  214. velune/providers/ollama_manager.py +193 -0
  215. velune/providers/registry.py +220 -0
  216. velune/providers/router.py +255 -0
  217. velune/providers/task_classifier.py +288 -0
  218. velune/py.typed +0 -0
  219. velune/repository/__init__.py +33 -0
  220. velune/repository/analyzer.py +127 -0
  221. velune/repository/ast_parser.py +822 -0
  222. velune/repository/blast_radius.py +298 -0
  223. velune/repository/boundary_classifier.py +295 -0
  224. velune/repository/cognition.py +316 -0
  225. velune/repository/grapher.py +179 -0
  226. velune/repository/import_graph.py +263 -0
  227. velune/repository/incremental_indexer.py +275 -0
  228. velune/repository/index_state.py +96 -0
  229. velune/repository/indexer.py +243 -0
  230. velune/repository/module.py +17 -0
  231. velune/repository/parser.py +474 -0
  232. velune/repository/project_type.py +300 -0
  233. velune/repository/rename_journal.py +287 -0
  234. velune/repository/scanner.py +193 -0
  235. velune/repository/schemas.py +102 -0
  236. velune/repository/symbol_registry.py +365 -0
  237. velune/repository/tracker.py +252 -0
  238. velune/retrieval/__init__.py +27 -0
  239. velune/retrieval/cache.py +110 -0
  240. velune/retrieval/fast_path.py +391 -0
  241. velune/retrieval/graph.py +124 -0
  242. velune/retrieval/hybrid.py +271 -0
  243. velune/retrieval/keyword.py +131 -0
  244. velune/retrieval/module.py +26 -0
  245. velune/retrieval/pipeline.py +303 -0
  246. velune/retrieval/reranker.py +102 -0
  247. velune/retrieval/schemas.py +59 -0
  248. velune/retrieval/slow_path.py +364 -0
  249. velune/retrieval/vector.py +203 -0
  250. velune/telemetry/__init__.py +59 -0
  251. velune/telemetry/cognition.py +267 -0
  252. velune/telemetry/cost_estimator.py +92 -0
  253. velune/telemetry/debug.py +304 -0
  254. velune/telemetry/doctor.py +244 -0
  255. velune/telemetry/logging.py +286 -0
  256. velune/telemetry/spans.py +277 -0
  257. velune/telemetry/token_tracker.py +140 -0
  258. velune/telemetry/usage_tracker.py +340 -0
  259. velune/tools/__init__.py +41 -0
  260. velune/tools/base/registry.py +87 -0
  261. velune/tools/base/tool.py +63 -0
  262. velune/tools/code/navigate.py +116 -0
  263. velune/tools/code/search.py +123 -0
  264. velune/tools/filesystem/read.py +75 -0
  265. velune/tools/filesystem/search.py +136 -0
  266. velune/tools/filesystem/write.py +163 -0
  267. velune/tools/git/history.py +177 -0
  268. velune/tools/git/operations.py +122 -0
  269. velune/tools/git/state.py +121 -0
  270. velune/tools/module.py +81 -0
  271. velune/tools/terminal/execute.py +72 -0
  272. velune/tools/terminal/history.py +47 -0
  273. velune/tools/web/fetch.py +55 -0
  274. velune/tools/web/validator.py +122 -0
  275. velune_cli-0.9.0.dist-info/METADATA +518 -0
  276. velune_cli-0.9.0.dist-info/RECORD +279 -0
  277. velune_cli-0.9.0.dist-info/WHEEL +4 -0
  278. velune_cli-0.9.0.dist-info/entry_points.txt +2 -0
  279. velune_cli-0.9.0.dist-info/licenses/LICENSE +201 -0
@@ -0,0 +1,822 @@
1
+ """Tree-sitter-based AST parsing for symbol extraction.
2
+
3
+ Tree-sitter ships compiled C extensions (.pyd/.so). Importing them at module
4
+ load time forces Windows to load several DLLs synchronously — each triggering
5
+ a Defender real-time scan — adding seconds to *every* startup, even when no
6
+ parsing is requested. We therefore defer all tree-sitter imports until the
7
+ first actual parse via ``_ensure_tree_sitter()``.
8
+
9
+ ``HAS_TREE_SITTER`` starts ``None`` (unknown) and becomes ``True``/``False``
10
+ after the first lazy load attempt. It remains importable for callers/tests
11
+ that reference it, but no import cost is paid until parsing happens.
12
+ """
13
+
14
+ from __future__ import annotations
15
+
16
+ import asyncio
17
+ import logging
18
+ import uuid
19
+ from dataclasses import dataclass, field
20
+ from enum import StrEnum
21
+ from pathlib import Path
22
+ from typing import TYPE_CHECKING, Any
23
+
24
+ if TYPE_CHECKING:
25
+ from tree_sitter import Tree
26
+
27
+ logger = logging.getLogger("velune.repository.ast_parser")
28
+
29
+ # ---------------------------------------------------------------------------
30
+ # Lazy tree-sitter loader — no DLL cost until first parse
31
+ # ---------------------------------------------------------------------------
32
+
33
+ #: ``None`` = not yet probed; ``True`` = available; ``False`` = unavailable.
34
+ HAS_TREE_SITTER: bool | None = None
35
+ _TS_LANGUAGES: dict[str, Any] = {}
36
+ # We do NOT cache a singleton Parser — each parse invocation creates a fresh
37
+ # Parser (cheap object) and sets its language, which is safe in a thread pool.
38
+ _TS_PARSER_CLS: Any = None # tree_sitter.Parser class, set after lazy load
39
+
40
+
41
+ def _ensure_tree_sitter() -> bool:
42
+ """Lazily import tree-sitter grammars on first use. Returns availability."""
43
+ global HAS_TREE_SITTER, _TS_PARSER_CLS
44
+ if HAS_TREE_SITTER is not None:
45
+ return HAS_TREE_SITTER
46
+ try:
47
+ import tree_sitter_go
48
+ import tree_sitter_python
49
+ import tree_sitter_rust
50
+ import tree_sitter_typescript
51
+ from tree_sitter import Language, Parser
52
+
53
+ _TS_PARSER_CLS = Parser
54
+ for lang_name, factory in (
55
+ ("python", tree_sitter_python.language),
56
+ ("typescript", tree_sitter_typescript.language_typescript),
57
+ ("javascript", tree_sitter_typescript.language_typescript),
58
+ ("go", tree_sitter_go.language),
59
+ ("rust", tree_sitter_rust.language),
60
+ ):
61
+ try:
62
+ lang_obj = Language(factory())
63
+ _TS_LANGUAGES[lang_name] = lang_obj
64
+ except Exception as exc:
65
+ logger.debug("Failed to load %s grammar: %s", lang_name, exc)
66
+ HAS_TREE_SITTER = True
67
+ except ImportError:
68
+ HAS_TREE_SITTER = False
69
+ return HAS_TREE_SITTER
70
+
71
+
72
+ class SymbolKind(StrEnum):
73
+ """Type of code symbol."""
74
+
75
+ FUNCTION = "function"
76
+ CLASS = "class"
77
+ METHOD = "method"
78
+ IMPORT = "import"
79
+ VARIABLE = "variable"
80
+ CONSTANT = "constant"
81
+ INTERFACE = "interface"
82
+ ENUM = "enum"
83
+ ASYNC_FUNCTION = "async_function"
84
+
85
+
86
+ @dataclass
87
+ class Symbol:
88
+ """Extracted code symbol from AST."""
89
+
90
+ id: str # Stable UUID
91
+ name: str
92
+ kind: SymbolKind
93
+ file_path: str # Relative to workspace root
94
+ line_start: int
95
+ line_end: int
96
+ docstring: str | None = None
97
+ parameters: list[str] = field(default_factory=list)
98
+ return_type: str | None = None
99
+ is_exported: bool = False
100
+ source_text: str | None = None # For reconstruction/debugging
101
+
102
+
103
+ @dataclass
104
+ class ParsedFile:
105
+ """Result of parsing a single file."""
106
+
107
+ file_path: Path
108
+ language: str
109
+ symbols: list[Symbol]
110
+ error: str | None = None
111
+
112
+
113
+ class ASTParser:
114
+ """Tree-sitter AST parser for multiple languages."""
115
+
116
+ LANGUAGE_MAP = {
117
+ "python": "tree_sitter_python",
118
+ "javascript": "tree_sitter_typescript",
119
+ "typescript": "tree_sitter_typescript",
120
+ "rust": "tree_sitter_rust",
121
+ "go": "tree_sitter_go",
122
+ }
123
+
124
+ def __init__(self, languages: list[str] | None = None) -> None:
125
+ """Initialize parser with language grammars.
126
+
127
+ Grammars are loaded lazily on first call to :meth:`parse_file` via
128
+ :func:`_ensure_tree_sitter`. This avoids Windows Defender DLL scans
129
+ at startup when no parsing will be performed.
130
+
131
+ Args:
132
+ languages: List of language names (python, javascript, typescript,
133
+ rust, go). If None, defaults to all supported languages.
134
+ """
135
+ self._requested_languages: list[str] = (
136
+ list(self.LANGUAGE_MAP.keys()) if languages is None else languages
137
+ )
138
+ # languages and parser are populated lazily by _ensure_loaded()
139
+ self.languages: dict[str, Any] = {}
140
+ self.parser: Any = None
141
+ self._loaded: bool = False
142
+
143
+ def _ensure_loaded(self) -> bool:
144
+ """Populate ``self.languages`` from the lazy cache.
145
+
146
+ Returns True if at least one grammar is available.
147
+ """
148
+ if self._loaded:
149
+ return bool(self.languages)
150
+ self._loaded = True
151
+ if _ensure_tree_sitter():
152
+ # Restrict to languages the caller requested
153
+ for lang in self._requested_languages:
154
+ lang_lower = lang.lower()
155
+ if lang_lower in _TS_LANGUAGES:
156
+ self.languages[lang_lower] = _TS_LANGUAGES[lang_lower]
157
+ return bool(self.languages)
158
+
159
+ async def parse_file(self, path: Path) -> ParsedFile | None:
160
+ """Parse a file asynchronously without blocking event loop.
161
+
162
+ Grammars are loaded on first call (lazy). Runs the actual parse in a
163
+ thread pool to avoid blocking the event loop.
164
+ """
165
+ try:
166
+ # Detect language from file extension
167
+ language = self._detect_language(path)
168
+ if not language:
169
+ return None
170
+
171
+ # Trigger lazy grammar load on first call
172
+ if not self._ensure_loaded() or language not in self.languages:
173
+ return None
174
+
175
+ # Read file content
176
+ source = path.read_text(encoding="utf-8")
177
+
178
+ # Parse in thread to avoid blocking
179
+ loop = asyncio.get_running_loop()
180
+ tree = await loop.run_in_executor(
181
+ None,
182
+ self._parse_sync,
183
+ source,
184
+ language,
185
+ )
186
+
187
+ if not tree:
188
+ return ParsedFile(path, language, [], error="Parse failed")
189
+
190
+ try:
191
+ rel_path = str(path.relative_to(Path.cwd()))
192
+ except ValueError:
193
+ rel_path = path.name
194
+
195
+ # Extract symbols in thread
196
+ symbols = await loop.run_in_executor(
197
+ None,
198
+ self.extract_symbols,
199
+ tree,
200
+ source,
201
+ language,
202
+ rel_path,
203
+ )
204
+
205
+ return ParsedFile(path, language, symbols)
206
+
207
+ except Exception as e:
208
+ logger.error(f"Error parsing {path}: {e}")
209
+ return None
210
+
211
+ def _parse_sync(self, source: str, language: str) -> Any:
212
+ """Synchronous parse operation (run in executor).
213
+
214
+ Creates a fresh tree_sitter.Parser per call to avoid language-switching
215
+ races when multiple files are parsed concurrently in the thread pool.
216
+ """
217
+ if not _TS_PARSER_CLS or language not in self.languages:
218
+ return None
219
+
220
+ try:
221
+ p = _TS_PARSER_CLS()
222
+ p.language = self.languages[language]
223
+ tree = p.parse(source.encode("utf-8"))
224
+ return tree
225
+ except Exception as e:
226
+ logger.debug(f"Parse error: {e}")
227
+ return None
228
+
229
+ def extract_symbols(
230
+ self,
231
+ tree: Tree,
232
+ source: str,
233
+ language: str,
234
+ file_path: str,
235
+ ) -> list[Symbol]:
236
+ """Extract symbols from AST tree."""
237
+ symbols: list[Symbol] = []
238
+
239
+ if language == "python":
240
+ symbols.extend(self._extract_python_symbols(tree, source, file_path))
241
+ elif language in ("javascript", "typescript"):
242
+ symbols.extend(self._extract_js_symbols(tree, source, file_path, language))
243
+ elif language == "rust":
244
+ symbols.extend(self._extract_rust_symbols(tree, source, file_path))
245
+ elif language == "go":
246
+ symbols.extend(self._extract_go_symbols(tree, source, file_path))
247
+
248
+ return symbols
249
+
250
+ def _extract_python_symbols(self, tree: Tree, source: str, file_path: str) -> list[Symbol]:
251
+ """Extract symbols from Python AST."""
252
+ symbols: list[Symbol] = []
253
+ source_lines = source.split("\n")
254
+
255
+ def visit(node: Any) -> None:
256
+ if node.type == "function_definition":
257
+ sym = self._parse_python_function(node, source_lines, file_path)
258
+ if sym:
259
+ symbols.append(sym)
260
+ elif node.type == "class_definition":
261
+ sym = self._parse_python_class(node, source_lines, file_path)
262
+ if sym:
263
+ symbols.append(sym)
264
+ # Also extract methods from class
265
+ for child in node.children:
266
+ if child.type == "block":
267
+ for subchild in child.children:
268
+ if subchild.type == "function_definition":
269
+ method = self._parse_python_function(
270
+ subchild, source_lines, file_path, is_method=True
271
+ )
272
+ if method:
273
+ symbols.append(method)
274
+ elif node.type in ("import_statement", "import_from_statement"):
275
+ sym = self._parse_python_import(node, source_lines, file_path)
276
+ if sym:
277
+ symbols.append(sym)
278
+
279
+ for child in node.children:
280
+ visit(child)
281
+
282
+ visit(tree.root_node)
283
+ return symbols
284
+
285
+ def _parse_python_function(
286
+ self,
287
+ node: Any,
288
+ source_lines: list[str],
289
+ file_path: str,
290
+ is_method: bool = False,
291
+ ) -> Symbol | None:
292
+ """Parse a Python function node."""
293
+ try:
294
+ name_node = None
295
+ params_node = None
296
+ is_async = False
297
+
298
+ # Check if async
299
+ for child in node.children:
300
+ if child.type == "async":
301
+ is_async = True
302
+
303
+ # Find name and parameters
304
+ for child in node.children:
305
+ if child.type == "identifier":
306
+ name_node = child
307
+ elif child.type == "parameters":
308
+ params_node = child
309
+
310
+ if not name_node:
311
+ return None
312
+
313
+ name = self._get_node_text(name_node, source_lines)
314
+ params = self._extract_parameters(params_node, source_lines) if params_node else []
315
+ docstring = self._extract_python_docstring(node, source_lines)
316
+
317
+ kind = (
318
+ SymbolKind.ASYNC_FUNCTION
319
+ if is_async
320
+ else SymbolKind.METHOD
321
+ if is_method
322
+ else SymbolKind.FUNCTION
323
+ )
324
+
325
+ return Symbol(
326
+ id=str(uuid.uuid4()),
327
+ name=name,
328
+ kind=kind,
329
+ file_path=file_path,
330
+ line_start=node.start_point[0] + 1,
331
+ line_end=node.end_point[0] + 1,
332
+ docstring=docstring,
333
+ parameters=params,
334
+ )
335
+ except Exception as e:
336
+ logger.debug(f"Error parsing Python function: {e}")
337
+ return None
338
+
339
+ def _parse_python_class(
340
+ self, node: Any, source_lines: list[str], file_path: str
341
+ ) -> Symbol | None:
342
+ """Parse a Python class node."""
343
+ try:
344
+ name_node = None
345
+ for child in node.children:
346
+ if child.type == "identifier":
347
+ name_node = child
348
+ break
349
+
350
+ if not name_node:
351
+ return None
352
+
353
+ name = self._get_node_text(name_node, source_lines)
354
+ docstring = self._extract_python_docstring(node, source_lines)
355
+
356
+ return Symbol(
357
+ id=str(uuid.uuid4()),
358
+ name=name,
359
+ kind=SymbolKind.CLASS,
360
+ file_path=file_path,
361
+ line_start=node.start_point[0] + 1,
362
+ line_end=node.end_point[0] + 1,
363
+ docstring=docstring,
364
+ )
365
+ except Exception as e:
366
+ logger.debug(f"Error parsing Python class: {e}")
367
+ return None
368
+
369
+ def _parse_python_import(
370
+ self, node: Any, source_lines: list[str], file_path: str
371
+ ) -> Symbol | None:
372
+ """Parse a Python import statement."""
373
+ try:
374
+ text = self._get_node_text(node, source_lines).strip()
375
+ # Extract imported name
376
+ if "from" in text:
377
+ parts = text.split("import")
378
+ if len(parts) > 1:
379
+ name = parts[-1].strip().split()[0]
380
+ else:
381
+ return None
382
+ else:
383
+ name = text.replace("import", "").strip().split()[0]
384
+
385
+ return Symbol(
386
+ id=str(uuid.uuid4()),
387
+ name=name,
388
+ kind=SymbolKind.IMPORT,
389
+ file_path=file_path,
390
+ line_start=node.start_point[0] + 1,
391
+ line_end=node.end_point[0] + 1,
392
+ source_text=text,
393
+ )
394
+ except Exception as e:
395
+ logger.debug(f"Error parsing Python import: {e}")
396
+ return None
397
+
398
+ def _extract_js_symbols(
399
+ self,
400
+ tree: Tree,
401
+ source: str,
402
+ file_path: str,
403
+ language: str,
404
+ ) -> list[Symbol]:
405
+ """Extract symbols from JavaScript/TypeScript AST."""
406
+ symbols: list[Symbol] = []
407
+ source_lines = source.split("\n")
408
+
409
+ def visit(node: Any) -> None:
410
+ if node.type in ("function_declaration", "generator_function_declaration"):
411
+ sym = self._parse_js_function(node, source_lines, file_path, is_async=False)
412
+ if sym:
413
+ symbols.append(sym)
414
+ elif node.type == "class_declaration":
415
+ sym = self._parse_js_class(node, source_lines, file_path)
416
+ if sym:
417
+ symbols.append(sym)
418
+ # Extract methods
419
+ for child in node.children:
420
+ if child.type == "class_body":
421
+ for method_node in child.children:
422
+ if method_node.type in ("method_definition", "function_definition"):
423
+ method = self._parse_js_function(
424
+ method_node, source_lines, file_path, is_method=True
425
+ )
426
+ if method:
427
+ symbols.append(method)
428
+ elif node.type in ("import_statement", "import_specifier"):
429
+ sym = self._parse_js_import(node, source_lines, file_path)
430
+ if sym:
431
+ symbols.append(sym)
432
+ elif node.type == "export_statement":
433
+ # Extract what's being exported
434
+ for child in node.children:
435
+ if child.type in ("function_declaration", "class_declaration"):
436
+ visit(child)
437
+
438
+ for child in node.children:
439
+ visit(child)
440
+
441
+ visit(tree.root_node)
442
+ return symbols
443
+
444
+ def _parse_js_function(
445
+ self,
446
+ node: Any,
447
+ source_lines: list[str],
448
+ file_path: str,
449
+ is_async: bool = False,
450
+ is_method: bool = False,
451
+ ) -> Symbol | None:
452
+ """Parse a JS/TS function node."""
453
+ try:
454
+ name_node = None
455
+ params_node = None
456
+
457
+ for child in node.children:
458
+ if child.type == "identifier":
459
+ name_node = child
460
+ elif child.type == "formal_parameters":
461
+ params_node = child
462
+
463
+ if not name_node:
464
+ return None
465
+
466
+ name = self._get_node_text(name_node, source_lines)
467
+ params = self._extract_parameters(params_node, source_lines) if params_node else []
468
+
469
+ kind = (
470
+ SymbolKind.ASYNC_FUNCTION
471
+ if is_async
472
+ else SymbolKind.METHOD
473
+ if is_method
474
+ else SymbolKind.FUNCTION
475
+ )
476
+
477
+ return Symbol(
478
+ id=str(uuid.uuid4()),
479
+ name=name,
480
+ kind=kind,
481
+ file_path=file_path,
482
+ line_start=node.start_point[0] + 1,
483
+ line_end=node.end_point[0] + 1,
484
+ parameters=params,
485
+ )
486
+ except Exception as e:
487
+ logger.debug(f"Error parsing JS function: {e}")
488
+ return None
489
+
490
+ def _parse_js_class(self, node: Any, source_lines: list[str], file_path: str) -> Symbol | None:
491
+ """Parse a JS/TS class node."""
492
+ try:
493
+ name_node = None
494
+ for child in node.children:
495
+ if child.type == "identifier":
496
+ name_node = child
497
+ break
498
+
499
+ if not name_node:
500
+ return None
501
+
502
+ name = self._get_node_text(name_node, source_lines)
503
+
504
+ return Symbol(
505
+ id=str(uuid.uuid4()),
506
+ name=name,
507
+ kind=SymbolKind.CLASS,
508
+ file_path=file_path,
509
+ line_start=node.start_point[0] + 1,
510
+ line_end=node.end_point[0] + 1,
511
+ )
512
+ except Exception as e:
513
+ logger.debug(f"Error parsing JS class: {e}")
514
+ return None
515
+
516
+ def _parse_js_import(self, node: Any, source_lines: list[str], file_path: str) -> Symbol | None:
517
+ """Parse a JS/TS import statement."""
518
+ try:
519
+ # Find imported identifiers
520
+ text = self._get_node_text(node, source_lines).strip()
521
+ parts = text.split("from")[0].replace("import", "").strip()
522
+ name = parts.split(",")[0].strip().replace("{", "").replace("}", "").strip()
523
+
524
+ if not name:
525
+ return None
526
+
527
+ return Symbol(
528
+ id=str(uuid.uuid4()),
529
+ name=name,
530
+ kind=SymbolKind.IMPORT,
531
+ file_path=file_path,
532
+ line_start=node.start_point[0] + 1,
533
+ line_end=node.end_point[0] + 1,
534
+ source_text=text,
535
+ )
536
+ except Exception as e:
537
+ logger.debug(f"Error parsing JS import: {e}")
538
+ return None
539
+
540
+ def _extract_rust_symbols(self, tree: Tree, source: str, file_path: str) -> list[Symbol]:
541
+ """Extract symbols from Rust AST."""
542
+ symbols: list[Symbol] = []
543
+ source_lines = source.split("\n")
544
+
545
+ def visit(node: Any) -> None:
546
+ if node.type == "function_item":
547
+ sym = self._parse_rust_function(node, source_lines, file_path)
548
+ if sym:
549
+ symbols.append(sym)
550
+ elif node.type == "struct_item":
551
+ sym = self._parse_rust_struct(node, source_lines, file_path)
552
+ if sym:
553
+ symbols.append(sym)
554
+ elif node.type == "enum_item":
555
+ sym = self._parse_rust_enum(node, source_lines, file_path)
556
+ if sym:
557
+ symbols.append(sym)
558
+ elif node.type == "impl_item":
559
+ # Extract methods from impl block
560
+ for child in node.children:
561
+ if child.type == "function_item":
562
+ method = self._parse_rust_function(
563
+ child, source_lines, file_path, is_method=True
564
+ )
565
+ if method:
566
+ symbols.append(method)
567
+
568
+ for child in node.children:
569
+ visit(child)
570
+
571
+ visit(tree.root_node)
572
+ return symbols
573
+
574
+ def _parse_rust_function(
575
+ self,
576
+ node: Any,
577
+ source_lines: list[str],
578
+ file_path: str,
579
+ is_method: bool = False,
580
+ ) -> Symbol | None:
581
+ """Parse a Rust function node."""
582
+ try:
583
+ name_node = None
584
+ for child in node.children:
585
+ if child.type == "identifier":
586
+ name_node = child
587
+ break
588
+
589
+ if not name_node:
590
+ return None
591
+
592
+ name = self._get_node_text(name_node, source_lines)
593
+
594
+ return Symbol(
595
+ id=str(uuid.uuid4()),
596
+ name=name,
597
+ kind=SymbolKind.METHOD if is_method else SymbolKind.FUNCTION,
598
+ file_path=file_path,
599
+ line_start=node.start_point[0] + 1,
600
+ line_end=node.end_point[0] + 1,
601
+ )
602
+ except Exception as e:
603
+ logger.debug(f"Error parsing Rust function: {e}")
604
+ return None
605
+
606
+ def _parse_rust_struct(
607
+ self, node: Any, source_lines: list[str], file_path: str
608
+ ) -> Symbol | None:
609
+ """Parse a Rust struct node."""
610
+ try:
611
+ name_node = None
612
+ for child in node.children:
613
+ if child.type == "type_identifier":
614
+ name_node = child
615
+ break
616
+
617
+ if not name_node:
618
+ return None
619
+
620
+ name = self._get_node_text(name_node, source_lines)
621
+
622
+ return Symbol(
623
+ id=str(uuid.uuid4()),
624
+ name=name,
625
+ kind=SymbolKind.CLASS,
626
+ file_path=file_path,
627
+ line_start=node.start_point[0] + 1,
628
+ line_end=node.end_point[0] + 1,
629
+ )
630
+ except Exception as e:
631
+ logger.debug(f"Error parsing Rust struct: {e}")
632
+ return None
633
+
634
+ def _parse_rust_enum(self, node: Any, source_lines: list[str], file_path: str) -> Symbol | None:
635
+ """Parse a Rust enum node."""
636
+ try:
637
+ name_node = None
638
+ for child in node.children:
639
+ if child.type == "type_identifier":
640
+ name_node = child
641
+ break
642
+
643
+ if not name_node:
644
+ return None
645
+
646
+ name = self._get_node_text(name_node, source_lines)
647
+
648
+ return Symbol(
649
+ id=str(uuid.uuid4()),
650
+ name=name,
651
+ kind=SymbolKind.ENUM,
652
+ file_path=file_path,
653
+ line_start=node.start_point[0] + 1,
654
+ line_end=node.end_point[0] + 1,
655
+ )
656
+ except Exception as e:
657
+ logger.debug(f"Error parsing Rust enum: {e}")
658
+ return None
659
+
660
+ def _extract_go_symbols(self, tree: Tree, source: str, file_path: str) -> list[Symbol]:
661
+ """Extract symbols from Go AST."""
662
+ symbols: list[Symbol] = []
663
+ source_lines = source.split("\n")
664
+
665
+ def visit(node: Any) -> None:
666
+ if node.type == "function_declaration":
667
+ sym = self._parse_go_function(node, source_lines, file_path)
668
+ if sym:
669
+ symbols.append(sym)
670
+ elif node.type == "method_declaration":
671
+ sym = self._parse_go_function(node, source_lines, file_path, is_method=True)
672
+ if sym:
673
+ symbols.append(sym)
674
+ elif node.type == "type_declaration":
675
+ # Handle struct/interface definitions
676
+ for child in node.children:
677
+ if child.type in ("struct_type", "interface_type"):
678
+ kind = (
679
+ SymbolKind.INTERFACE
680
+ if child.type == "interface_type"
681
+ else SymbolKind.CLASS
682
+ )
683
+ sym = self._parse_go_type(child, source_lines, file_path, kind)
684
+ if sym:
685
+ symbols.append(sym)
686
+
687
+ for child in node.children:
688
+ visit(child)
689
+
690
+ visit(tree.root_node)
691
+ return symbols
692
+
693
+ def _parse_go_function(
694
+ self,
695
+ node: Any,
696
+ source_lines: list[str],
697
+ file_path: str,
698
+ is_method: bool = False,
699
+ ) -> Symbol | None:
700
+ """Parse a Go function node."""
701
+ try:
702
+ name_node = None
703
+ for child in node.children:
704
+ if child.type == "identifier":
705
+ name_node = child
706
+ break
707
+
708
+ if not name_node:
709
+ return None
710
+
711
+ name = self._get_node_text(name_node, source_lines)
712
+
713
+ return Symbol(
714
+ id=str(uuid.uuid4()),
715
+ name=name,
716
+ kind=SymbolKind.METHOD if is_method else SymbolKind.FUNCTION,
717
+ file_path=file_path,
718
+ line_start=node.start_point[0] + 1,
719
+ line_end=node.end_point[0] + 1,
720
+ )
721
+ except Exception as e:
722
+ logger.debug(f"Error parsing Go function: {e}")
723
+ return None
724
+
725
+ def _parse_go_type(
726
+ self,
727
+ node: Any,
728
+ source_lines: list[str],
729
+ file_path: str,
730
+ kind: SymbolKind,
731
+ ) -> Symbol | None:
732
+ """Parse a Go struct/interface type."""
733
+ try:
734
+ # Go types are usually preceded by their name in a type_declaration
735
+ # This is a simplification
736
+ text = self._get_node_text(node, source_lines).strip()
737
+ # Extract name from type definition
738
+ name = text.split("{")[0].split("(")[0].strip()
739
+
740
+ if not name:
741
+ return None
742
+
743
+ return Symbol(
744
+ id=str(uuid.uuid4()),
745
+ name=name,
746
+ kind=kind,
747
+ file_path=file_path,
748
+ line_start=node.start_point[0] + 1,
749
+ line_end=node.end_point[0] + 1,
750
+ )
751
+ except Exception as e:
752
+ logger.debug(f"Error parsing Go type: {e}")
753
+ return None
754
+
755
+ def _get_node_text(self, node: Any, source_lines: list[str]) -> str:
756
+ """Extract text of a node from source."""
757
+ try:
758
+ start_row = node.start_point[0]
759
+ start_col = node.start_point[1]
760
+ end_row = node.end_point[0]
761
+ end_col = node.end_point[1]
762
+
763
+ if start_row == end_row:
764
+ return source_lines[start_row][start_col:end_col]
765
+
766
+ lines = [source_lines[start_row][start_col:]]
767
+ for row in range(start_row + 1, end_row):
768
+ lines.append(source_lines[row])
769
+ lines.append(source_lines[end_row][:end_col])
770
+
771
+ return "\n".join(lines)
772
+ except Exception:
773
+ return ""
774
+
775
+ def _extract_parameters(self, params_node: Any, source_lines: list[str]) -> list[str]:
776
+ """Extract parameter names from function parameters node."""
777
+ try:
778
+ text = self._get_node_text(params_node, source_lines)
779
+ # Simple extraction: split by comma, extract identifiers
780
+ params = []
781
+ for part in text.replace("(", "").replace(")", "").split(","):
782
+ part = part.strip()
783
+ if part:
784
+ # Extract identifier (first word before : or =)
785
+ name = part.split(":")[0].split("=")[0].strip()
786
+ if name:
787
+ params.append(name)
788
+ return params
789
+ except Exception:
790
+ return []
791
+
792
+ def _extract_python_docstring(self, node: Any, source_lines: list[str]) -> str | None:
793
+ """Extract docstring from Python function or class."""
794
+ try:
795
+ # Look for string node after function/class definition
796
+ for child in node.children:
797
+ if child.type == "block":
798
+ for subchild in child.children:
799
+ if subchild.type == "expression_statement":
800
+ for subsubchild in subchild.children:
801
+ if subsubchild.type == "string":
802
+ text = self._get_node_text(subsubchild, source_lines)
803
+ # Remove quotes
804
+ if text.startswith('"""') or text.startswith("'''"):
805
+ return text[3:-3].strip()
806
+ return text.strip("\"'")
807
+ return None
808
+ except Exception:
809
+ return None
810
+
811
+ def _detect_language(self, path: Path) -> str | None:
812
+ """Detect language from file extension."""
813
+ ext_map = {
814
+ ".py": "python",
815
+ ".js": "javascript",
816
+ ".jsx": "javascript",
817
+ ".ts": "typescript",
818
+ ".tsx": "typescript",
819
+ ".rs": "rust",
820
+ ".go": "go",
821
+ }
822
+ return ext_map.get(path.suffix.lower())