codegraphcontext-rust 0.5.5__cp311-cp311-win_amd64.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 (101) hide show
  1. codegraphcontext/__init__.py +2 -0
  2. codegraphcontext/__main__.py +5 -0
  3. codegraphcontext/_cgc_rust.cp311-win_amd64.pyd +0 -0
  4. codegraphcontext/cli/__init__.py +1 -0
  5. codegraphcontext/cli/cli_helpers.py +774 -0
  6. codegraphcontext/cli/config_manager.py +1051 -0
  7. codegraphcontext/cli/main.py +2386 -0
  8. codegraphcontext/cli/registry_commands.py +404 -0
  9. codegraphcontext/cli/setup_macos.py +93 -0
  10. codegraphcontext/cli/setup_wizard.py +991 -0
  11. codegraphcontext/cli/visualizer.py +50 -0
  12. codegraphcontext/core/__init__.py +165 -0
  13. codegraphcontext/core/bundle_registry.py +181 -0
  14. codegraphcontext/core/cgc_bundle.py +858 -0
  15. codegraphcontext/core/cgcignore.py +118 -0
  16. codegraphcontext/core/database.py +273 -0
  17. codegraphcontext/core/database_falkordb.py +480 -0
  18. codegraphcontext/core/database_falkordb_remote.py +199 -0
  19. codegraphcontext/core/database_kuzu.py +636 -0
  20. codegraphcontext/core/falkor_worker.py +132 -0
  21. codegraphcontext/core/jobs.py +132 -0
  22. codegraphcontext/core/watcher.py +260 -0
  23. codegraphcontext/prompts.py +125 -0
  24. codegraphcontext/server.py +471 -0
  25. codegraphcontext/tool_definitions.py +220 -0
  26. codegraphcontext/tools/__init__.py +0 -0
  27. codegraphcontext/tools/advanced_language_query_tool.py +103 -0
  28. codegraphcontext/tools/code_finder.py +1160 -0
  29. codegraphcontext/tools/graph_builder.py +337 -0
  30. codegraphcontext/tools/handlers/analysis_handlers.py +114 -0
  31. codegraphcontext/tools/handlers/indexing_handlers.py +116 -0
  32. codegraphcontext/tools/handlers/management_handlers.py +339 -0
  33. codegraphcontext/tools/handlers/query_handlers.py +83 -0
  34. codegraphcontext/tools/handlers/watcher_handlers.py +83 -0
  35. codegraphcontext/tools/indexing/__init__.py +1 -0
  36. codegraphcontext/tools/indexing/constants.py +25 -0
  37. codegraphcontext/tools/indexing/discovery.py +64 -0
  38. codegraphcontext/tools/indexing/engine.py +109 -0
  39. codegraphcontext/tools/indexing/persistence/__init__.py +3 -0
  40. codegraphcontext/tools/indexing/persistence/parquet_loader.py +643 -0
  41. codegraphcontext/tools/indexing/persistence/writer.py +1114 -0
  42. codegraphcontext/tools/indexing/pipeline.py +171 -0
  43. codegraphcontext/tools/indexing/pre_scan.py +105 -0
  44. codegraphcontext/tools/indexing/resolution/__init__.py +8 -0
  45. codegraphcontext/tools/indexing/resolution/calls.py +253 -0
  46. codegraphcontext/tools/indexing/resolution/inheritance.py +91 -0
  47. codegraphcontext/tools/indexing/sanitize.py +41 -0
  48. codegraphcontext/tools/indexing/schema.py +79 -0
  49. codegraphcontext/tools/indexing/schema_contract.py +44 -0
  50. codegraphcontext/tools/indexing/scip_pipeline.py +140 -0
  51. codegraphcontext/tools/languages/c.py +563 -0
  52. codegraphcontext/tools/languages/cpp.py +616 -0
  53. codegraphcontext/tools/languages/csharp.py +551 -0
  54. codegraphcontext/tools/languages/dart.py +378 -0
  55. codegraphcontext/tools/languages/elixir.py +460 -0
  56. codegraphcontext/tools/languages/go.py +499 -0
  57. codegraphcontext/tools/languages/haskell.py +426 -0
  58. codegraphcontext/tools/languages/java.py +500 -0
  59. codegraphcontext/tools/languages/javascript.py +589 -0
  60. codegraphcontext/tools/languages/kotlin.py +640 -0
  61. codegraphcontext/tools/languages/perl.py +260 -0
  62. codegraphcontext/tools/languages/php.py +519 -0
  63. codegraphcontext/tools/languages/python.py +576 -0
  64. codegraphcontext/tools/languages/ruby.py +537 -0
  65. codegraphcontext/tools/languages/rust.py +295 -0
  66. codegraphcontext/tools/languages/scala.py +519 -0
  67. codegraphcontext/tools/languages/swift.py +491 -0
  68. codegraphcontext/tools/languages/typescript.py +575 -0
  69. codegraphcontext/tools/languages/typescriptjsx.py +151 -0
  70. codegraphcontext/tools/package_resolver.py +472 -0
  71. codegraphcontext/tools/query_tool_languages/c_toolkit.py +5 -0
  72. codegraphcontext/tools/query_tool_languages/cpp_toolkit.py +87 -0
  73. codegraphcontext/tools/query_tool_languages/csharp_toolkit.py +5 -0
  74. codegraphcontext/tools/query_tool_languages/dart_toolkit.py +5 -0
  75. codegraphcontext/tools/query_tool_languages/go_toolkit.py +5 -0
  76. codegraphcontext/tools/query_tool_languages/haskell_toolkit.py +5 -0
  77. codegraphcontext/tools/query_tool_languages/java_toolkit.py +5 -0
  78. codegraphcontext/tools/query_tool_languages/javascript_toolkit.py +5 -0
  79. codegraphcontext/tools/query_tool_languages/perl_toolkit.py +5 -0
  80. codegraphcontext/tools/query_tool_languages/python_toolkit.py +5 -0
  81. codegraphcontext/tools/query_tool_languages/ruby_toolkit.py +5 -0
  82. codegraphcontext/tools/query_tool_languages/rust_toolkit.py +5 -0
  83. codegraphcontext/tools/query_tool_languages/scala_toolkit.py +5 -0
  84. codegraphcontext/tools/query_tool_languages/swift_toolkit.py +5 -0
  85. codegraphcontext/tools/query_tool_languages/typescript_toolkit.py +5 -0
  86. codegraphcontext/tools/scip_indexer.py +467 -0
  87. codegraphcontext/tools/scip_pb2.py +2455 -0
  88. codegraphcontext/tools/system.py +133 -0
  89. codegraphcontext/tools/tree_sitter_parser.py +103 -0
  90. codegraphcontext/utils/debug_log.py +90 -0
  91. codegraphcontext/utils/path_ignore.py +55 -0
  92. codegraphcontext/utils/repo_path.py +27 -0
  93. codegraphcontext/utils/tree_sitter_manager.py +285 -0
  94. codegraphcontext/utils/visualize_graph.py +0 -0
  95. codegraphcontext/viz/server.py +282 -0
  96. codegraphcontext_rust-0.5.5.dist-info/METADATA +537 -0
  97. codegraphcontext_rust-0.5.5.dist-info/RECORD +101 -0
  98. codegraphcontext_rust-0.5.5.dist-info/WHEEL +4 -0
  99. codegraphcontext_rust-0.5.5.dist-info/entry_points.txt +3 -0
  100. codegraphcontext_rust-0.5.5.dist-info/licenses/LICENSE +21 -0
  101. codegraphcontext_rust-0.5.5.dist-info/sboms/cgc-python.cyclonedx.json +2333 -0
@@ -0,0 +1,337 @@
1
+
2
+ # src/codegraphcontext/tools/graph_builder.py
3
+ """Facade for graph indexing; implementation lives in indexing/."""
4
+
5
+ import asyncio
6
+ from datetime import datetime
7
+ from pathlib import Path
8
+ from typing import Any, Dict, Optional, Tuple
9
+
10
+ from ..cli.config_manager import get_config_value
11
+ from ..core.database import DatabaseManager
12
+ from ..core.jobs import JobManager, JobStatus
13
+ from ..utils.debug_log import debug_log, error_logger, info_logger, warning_logger
14
+ from .indexing.constants import DEFAULT_IGNORE_PATTERNS
15
+ from .indexing.persistence.writer import GraphWriter
16
+ from .indexing.pipeline import run_tree_sitter_index_async
17
+ from .indexing.pre_scan import pre_scan_for_imports
18
+ from .indexing.resolution.calls import build_function_call_groups, resolve_function_call
19
+ from .indexing.resolution.inheritance import build_inheritance_and_csharp_files
20
+ from .indexing.sanitize import MAX_STR_LEN, sanitize_props
21
+ from .indexing.schema import create_graph_schema
22
+ from .indexing.scip_pipeline import name_from_symbol, run_scip_index_async
23
+ from .tree_sitter_parser import TreeSitterParser
24
+
25
+
26
+ class GraphBuilder:
27
+ """Module for building and managing the code graph (Neo4j / Falkor / Kùzu)."""
28
+
29
+ def __init__(self, db_manager: DatabaseManager, job_manager: JobManager, loop: asyncio.AbstractEventLoop):
30
+ self.db_manager = db_manager
31
+ self.job_manager = job_manager
32
+ self.loop = loop
33
+ self.driver = self.db_manager.get_driver()
34
+ self._writer = GraphWriter(self.driver)
35
+ self.parsers = {
36
+ ".py": "python",
37
+ ".ipynb": "python",
38
+ ".js": "javascript",
39
+ ".jsx": "javascript",
40
+ ".mjs": "javascript",
41
+ ".cjs": "javascript",
42
+ ".go": "go",
43
+ ".ts": "typescript",
44
+ ".tsx": "tsx",
45
+ ".cpp": "cpp",
46
+ ".h": "cpp",
47
+ ".hpp": "cpp",
48
+ ".hh": "cpp",
49
+ ".rs": "rust",
50
+ ".c": "c",
51
+ ".java": "java",
52
+ ".rb": "ruby",
53
+ ".cs": "c_sharp",
54
+ ".php": "php",
55
+ ".kt": "kotlin",
56
+ ".scala": "scala",
57
+ ".sc": "scala",
58
+ ".swift": "swift",
59
+ ".hs": "haskell",
60
+ ".dart": "dart",
61
+ ".pl": "perl",
62
+ ".pm": "perl",
63
+ ".ex": "elixir",
64
+ ".exs": "elixir",
65
+ }
66
+ self._parsed_cache = {}
67
+ self.create_schema()
68
+
69
+ def get_parser(self, extension: str) -> Optional[TreeSitterParser]:
70
+ """Gets or creates a TreeSitterParser for the given extension."""
71
+ lang_name = self.parsers.get(extension)
72
+ if not lang_name:
73
+ return None
74
+
75
+ if lang_name not in self._parsed_cache:
76
+ try:
77
+ self._parsed_cache[lang_name] = TreeSitterParser(lang_name)
78
+ except Exception as e:
79
+ warning_logger(f"Failed to initialize parser for {lang_name}: {e}")
80
+ return None
81
+ return self._parsed_cache[lang_name]
82
+
83
+ def create_schema(self) -> None:
84
+ create_graph_schema(self.driver, self.db_manager)
85
+
86
+ _MAX_STR_LEN = MAX_STR_LEN
87
+
88
+ @staticmethod
89
+ def _sanitize_props(props: Dict) -> Dict:
90
+ return sanitize_props(props)
91
+
92
+ def _resolve_function_call(
93
+ self,
94
+ call: Dict,
95
+ caller_file_path: str,
96
+ local_names: set,
97
+ local_imports: dict,
98
+ imports_map: dict,
99
+ skip_external: bool,
100
+ ):
101
+ return resolve_function_call(
102
+ call, caller_file_path, local_names, local_imports, imports_map, skip_external
103
+ )
104
+
105
+ def pre_scan_imports(self, files: list[Path]) -> dict:
106
+ """Build global imports_map from language pre-scans (public API for watchers/pipeline)."""
107
+ return pre_scan_for_imports(files, self.parsers.keys(), self.get_parser)
108
+
109
+ def _pre_scan_for_imports(self, files: list[Path]) -> dict:
110
+ return self.pre_scan_imports(files)
111
+
112
+ def add_repository_to_graph(self, repo_path: Path, is_dependency: bool = False) -> None:
113
+ self._writer.add_repository_to_graph(repo_path, is_dependency)
114
+
115
+ def add_file_to_graph(
116
+ self, file_data: Dict, repo_name: str, imports_map: dict, repo_path_str: str = None
117
+ ) -> None:
118
+ self._writer.add_file_to_graph(file_data, repo_name, imports_map, repo_path_str=repo_path_str)
119
+
120
+ def link_function_calls(
121
+ self,
122
+ all_file_data: list[Dict],
123
+ imports_map: dict,
124
+ file_class_lookup: Optional[Dict[str, set]] = None,
125
+ ) -> None:
126
+ """Resolve and persist CALLS relationships (public API)."""
127
+ groups = build_function_call_groups(all_file_data, imports_map, file_class_lookup)
128
+ self._writer.write_function_call_groups(*groups)
129
+
130
+ def _create_all_function_calls(
131
+ self, all_file_data: list[Dict], imports_map: dict, file_class_lookup: Optional[Dict[str, set]] = None
132
+ ) -> None:
133
+ self.link_function_calls(all_file_data, imports_map, file_class_lookup)
134
+
135
+ def link_inheritance(self, all_file_data: list[Dict], imports_map: dict) -> None:
136
+ """Resolve and persist INHERITS / C# IMPLEMENTS (public API)."""
137
+ info_logger(f"[INHERITS] Resolving inheritance links across {len(all_file_data)} files...")
138
+ inheritance_batch, csharp_files = build_inheritance_and_csharp_files(all_file_data, imports_map)
139
+ self._writer.write_inheritance_links(inheritance_batch, csharp_files, imports_map)
140
+
141
+ def _create_all_inheritance_links(self, all_file_data: list[Dict], imports_map: dict) -> None:
142
+ self.link_inheritance(all_file_data, imports_map)
143
+
144
+ def delete_file_from_graph(self, path: str) -> None:
145
+ self._writer.delete_file_from_graph(path)
146
+
147
+ def delete_repository_from_graph(self, repo_path: str) -> bool:
148
+ return self._writer.delete_repository_from_graph(repo_path)
149
+
150
+ def get_caller_file_paths(self, file_path_str: str) -> set:
151
+ return self._writer.get_caller_file_paths(file_path_str)
152
+
153
+ def get_inheritance_neighbor_paths(self, file_path_str: str) -> set:
154
+ return self._writer.get_inheritance_neighbor_paths(file_path_str)
155
+
156
+ def delete_outgoing_calls_from_files(self, file_paths: list) -> None:
157
+ self._writer.delete_outgoing_calls_from_files(file_paths)
158
+
159
+ def delete_inherits_for_files(self, file_paths: list) -> None:
160
+ self._writer.delete_inherits_for_files(file_paths)
161
+
162
+ def get_repo_class_lookup(self, repo_path: Path) -> dict:
163
+ return self._writer.get_repo_class_lookup(repo_path)
164
+
165
+ def delete_relationship_links(self, repo_path: Path) -> None:
166
+ self._writer.delete_relationship_links(repo_path)
167
+
168
+ def update_file_in_graph(self, path: Path, repo_path: Path, imports_map: dict):
169
+ file_path_str = str(path.resolve())
170
+ repo_name = repo_path.name
171
+
172
+ self.delete_file_from_graph(file_path_str)
173
+
174
+ if path.exists():
175
+ file_data = self.parse_file(repo_path, path)
176
+
177
+ if "error" not in file_data:
178
+ self.add_file_to_graph(file_data, repo_name, imports_map)
179
+ return file_data
180
+ error_logger(f"Skipping graph add for {file_path_str} due to parsing error: {file_data['error']}")
181
+ return None
182
+ return {"deleted": True, "path": file_path_str}
183
+
184
+ def parse_file(self, repo_path: Path, path: Path, is_dependency: bool = False) -> Dict:
185
+ lang_name = self.parsers.get(path.suffix)
186
+ if not lang_name:
187
+ warning_logger(f"No parser found for file extension {path.suffix}. Skipping {path}")
188
+ return {"path": str(path), "error": f"No parser for {path.suffix}", "unsupported": True}
189
+
190
+ # Try Rust engine first (faster, no Python overhead)
191
+ try:
192
+ from .indexing.engine import RUST_AVAILABLE, _RUST_SUPPORTED_LANGS, _rust_parse_file
193
+ if RUST_AVAILABLE and lang_name in _RUST_SUPPORTED_LANGS and path.suffix != ".ipynb":
194
+ index_source = (get_config_value("INDEX_SOURCE") or "false").lower() == "true"
195
+ file_data = _rust_parse_file(str(path), lang_name, is_dependency, index_source)
196
+ if "error" not in file_data:
197
+ file_data["repo_path"] = str(repo_path)
198
+ return file_data
199
+ except Exception:
200
+ pass # Fall through to Python parser
201
+
202
+ # Python fallback (for .ipynb, unsupported langs, or Rust errors)
203
+ parser = self.get_parser(path.suffix)
204
+ if not parser:
205
+ return {"path": str(path), "error": f"No parser for {path.suffix}", "unsupported": True}
206
+
207
+ debug_log(f"[parse_file] Starting parsing for: {path} with {parser.language_name} parser")
208
+ try:
209
+ index_source = (get_config_value("INDEX_SOURCE") or "false").lower() == "true"
210
+ if parser.language_name == "python":
211
+ is_notebook = path.suffix == ".ipynb"
212
+ file_data = parser.parse(
213
+ path,
214
+ is_dependency,
215
+ is_notebook=is_notebook,
216
+ index_source=index_source,
217
+ )
218
+ else:
219
+ file_data = parser.parse(path, is_dependency, index_source=index_source)
220
+ file_data["repo_path"] = str(repo_path)
221
+ return file_data
222
+ except Exception as e:
223
+ error_logger(f"Error parsing {path} with {parser.language_name} parser: {e}")
224
+ debug_log(f"[parse_file] Error parsing {path}: {e}")
225
+ return {"path": str(path), "error": str(e)}
226
+
227
+ def estimate_processing_time(self, path: Path) -> Optional[Tuple[int, float]]:
228
+ try:
229
+ supported_extensions = self.parsers.keys()
230
+ if path.is_file():
231
+ if path.suffix in supported_extensions:
232
+ files = [path]
233
+ else:
234
+ return 0, 0.0
235
+ else:
236
+ all_files = path.rglob("*")
237
+ files = [f for f in all_files if f.is_file() and f.suffix in supported_extensions]
238
+
239
+ ignore_dirs_str = get_config_value("IGNORE_DIRS") or ""
240
+ if ignore_dirs_str:
241
+ ignore_dirs = {d.strip().lower() for d in ignore_dirs_str.split(",") if d.strip()}
242
+ if ignore_dirs:
243
+ kept_files = []
244
+ for f in files:
245
+ try:
246
+ parts = set(p.lower() for p in f.relative_to(path).parent.parts)
247
+ if not parts.intersection(ignore_dirs):
248
+ kept_files.append(f)
249
+ except ValueError:
250
+ kept_files.append(f)
251
+ files = kept_files
252
+
253
+ total_files = len(files)
254
+ estimated_time = total_files * 0.05
255
+ return total_files, estimated_time
256
+ except Exception as e:
257
+ error_logger(f"Could not estimate processing time for {path}: {e}")
258
+ return None
259
+
260
+ async def _build_graph_from_scip(
261
+ self, path: Path, is_dependency: bool, job_id: Optional[str], lang: str
262
+ ):
263
+ from . import scip_indexer
264
+
265
+ await run_scip_index_async(
266
+ path,
267
+ is_dependency,
268
+ job_id,
269
+ lang,
270
+ self._writer,
271
+ self.job_manager,
272
+ self.parsers.keys(),
273
+ self.get_parser,
274
+ scip_indexer,
275
+ )
276
+
277
+ def _name_from_symbol(self, symbol: str) -> str:
278
+ return name_from_symbol(symbol)
279
+
280
+ async def build_graph_from_path_async(
281
+ self, path: Path, is_dependency: bool = False, job_id: str = None, cgcignore_path: str = None
282
+ ):
283
+ try:
284
+ scip_enabled = (get_config_value("SCIP_INDEXER") or "false").lower() == "true"
285
+ if scip_enabled:
286
+ from .scip_indexer import detect_project_lang, is_scip_available
287
+
288
+ scip_langs_str = get_config_value("SCIP_LANGUAGES") or "python,typescript,go,rust,java"
289
+ scip_languages = [l.strip() for l in scip_langs_str.split(",") if l.strip()]
290
+ detected_lang = detect_project_lang(path, scip_languages)
291
+
292
+ if detected_lang and is_scip_available(detected_lang):
293
+ info_logger(f"SCIP_INDEXER=true — using SCIP for language: {detected_lang}")
294
+ await self._build_graph_from_scip(path, is_dependency, job_id, detected_lang)
295
+ return
296
+ if detected_lang:
297
+ warning_logger(
298
+ f"SCIP_INDEXER=true but scip-{detected_lang} binary not found. "
299
+ f"Falling back to Tree-sitter. Install it first."
300
+ )
301
+ else:
302
+ info_logger(
303
+ "SCIP_INDEXER=true but no SCIP-supported language detected. "
304
+ "Falling back to Tree-sitter."
305
+ )
306
+
307
+ await run_tree_sitter_index_async(
308
+ path,
309
+ is_dependency,
310
+ job_id,
311
+ cgcignore_path,
312
+ self._writer,
313
+ self.job_manager,
314
+ self.parsers,
315
+ self.get_parser,
316
+ self.parse_file,
317
+ self.add_minimal_file_node,
318
+ )
319
+ except Exception as e:
320
+ error_message = str(e)
321
+ error_logger(f"Failed to build graph for path {path}: {error_message}")
322
+ if job_id:
323
+ if (
324
+ "no such file found" in error_message
325
+ or "deleted" in error_message
326
+ or "not found" in error_message
327
+ ):
328
+ status = JobStatus.CANCELLED
329
+ else:
330
+ status = JobStatus.FAILED
331
+
332
+ self.job_manager.update_job(
333
+ job_id, status=status, end_time=datetime.now(), errors=[str(e)]
334
+ )
335
+
336
+ def add_minimal_file_node(self, file_path: Path, repo_path: Path, is_dependency: bool = False) -> None:
337
+ self._writer.add_minimal_file_node(file_path, repo_path, is_dependency)
@@ -0,0 +1,114 @@
1
+ from typing import Any, Dict
2
+ from ..code_finder import CodeFinder
3
+ from ...utils.debug_log import debug_log
4
+
5
+ def find_dead_code(code_finder: CodeFinder, **args) -> Dict[str, Any]:
6
+ """Tool to find potentially dead code across the entire project."""
7
+ exclude_decorated_with = args.get("exclude_decorated_with", [])
8
+ repo_path = args.get("repo_path")
9
+ try:
10
+ debug_log(f"Finding dead code. repo_path={repo_path}")
11
+ results = code_finder.find_dead_code(exclude_decorated_with=exclude_decorated_with, repo_path=repo_path)
12
+
13
+ return {
14
+ "success": True,
15
+ "query_type": "dead_code",
16
+ "results": results
17
+ }
18
+ except Exception as e:
19
+ debug_log(f"Error finding dead code: {str(e)}")
20
+ return {"error": f"Failed to find dead code: {str(e)}"}
21
+
22
+ def calculate_cyclomatic_complexity(code_finder: CodeFinder, **args) -> Dict[str, Any]:
23
+ """Tool to calculate cyclomatic complexity for a given function."""
24
+ function_name = args.get("function_name")
25
+ path = args.get("path")
26
+ repo_path = args.get("repo_path")
27
+
28
+ try:
29
+ debug_log(f"Calculating cyclomatic complexity for function: {function_name}, repo_path={repo_path}")
30
+ results = code_finder.get_cyclomatic_complexity(function_name, path, repo_path=repo_path)
31
+
32
+ response = {
33
+ "success": True,
34
+ "function_name": function_name,
35
+ "results": results
36
+ }
37
+ if path:
38
+ response["path"] = path
39
+
40
+ return response
41
+ except Exception as e:
42
+ debug_log(f"Error calculating cyclomatic complexity: {str(e)}")
43
+ return {"error": f"Failed to calculate cyclomatic complexity: {str(e)}"}
44
+
45
+ def find_most_complex_functions(code_finder: CodeFinder, **args) -> Dict[str, Any]:
46
+ """Tool to find the most complex functions."""
47
+ limit = args.get("limit", 10)
48
+ repo_path = args.get("repo_path")
49
+ try:
50
+ debug_log(f"Finding the top {limit} most complex functions. repo_path={repo_path}")
51
+ results = code_finder.find_most_complex_functions(limit, repo_path=repo_path)
52
+ return {
53
+ "success": True,
54
+ "limit": limit,
55
+ "results": results
56
+ }
57
+ except Exception as e:
58
+ debug_log(f"Error finding most complex functions: {str(e)}")
59
+ return {"error": f"Failed to find most complex functions: {str(e)}"}
60
+
61
+ def analyze_code_relationships(code_finder: CodeFinder, **args) -> Dict[str, Any]:
62
+ """Tool to analyze code relationships"""
63
+ query_type = args.get("query_type")
64
+ target = args.get("target")
65
+ context = args.get("context")
66
+ repo_path = args.get("repo_path")
67
+
68
+ if not query_type or not target:
69
+ return {
70
+ "error": "Both 'query_type' and 'target' are required",
71
+ "supported_query_types": [
72
+ "find_callers", "find_callees", "find_all_callers", "find_all_callees", "find_importers", "who_modifies",
73
+ "class_hierarchy", "overrides", "dead_code", "call_chain",
74
+ "module_deps", "variable_scope", "find_complexity", "find_functions_by_argument", "find_functions_by_decorator"
75
+ ]
76
+ }
77
+
78
+ try:
79
+ debug_log(f"Analyzing relationships: {query_type} for {target}, repo_path={repo_path}")
80
+ results = code_finder.analyze_code_relationships(query_type, target, context, repo_path=repo_path)
81
+
82
+ return {
83
+ "success": True, "query_type": query_type, "target": target,
84
+ "context": context, "results": results
85
+ }
86
+
87
+ except Exception as e:
88
+ debug_log(f"Error analyzing relationships: {str(e)}")
89
+ return {"error": f"Failed to analyze relationships: {str(e)}"}
90
+
91
+ def find_code(code_finder: CodeFinder, **args) -> Dict[str, Any]:
92
+ """Tool to find relevant code snippets"""
93
+ query = args.get("query")
94
+ DEFAULT_EDIT_DISTANCE = 2
95
+ DEFAULT_FUZZY_SEARCH = False
96
+
97
+ fuzzy_search = args.get("fuzzy_search", DEFAULT_FUZZY_SEARCH)
98
+ edit_distance = args.get("edit_distance", DEFAULT_EDIT_DISTANCE)
99
+ repo_path = args.get("repo_path")
100
+
101
+ if fuzzy_search:
102
+ # Preserve case for Lucene / Levenshtein name matching; lowercasing breaks
103
+ # camelCase fuzzy hits (see GH #758).
104
+ query = query.replace("_", " ").strip()
105
+
106
+ try:
107
+ debug_log(f"Finding code for query: {query} with fuzzy_search={fuzzy_search}, edit_distance={edit_distance}, repo_path={repo_path}")
108
+ results = code_finder.find_related_code(query, fuzzy_search, edit_distance, repo_path=repo_path)
109
+
110
+ return {"success": True, "query": query, "results": results}
111
+
112
+ except Exception as e:
113
+ debug_log(f"Error finding code: {str(e)}")
114
+ return {"error": f"Failed to find code: {str(e)}"}
@@ -0,0 +1,116 @@
1
+ from typing import Any, Dict
2
+ from pathlib import Path
3
+ import asyncio
4
+ import os
5
+ from ...utils.debug_log import debug_log
6
+ from ...utils.repo_path import repo_record_matches_path
7
+ from ..package_resolver import get_local_package_path
8
+
9
+ def add_code_to_graph(graph_builder, job_manager, loop, list_repos_func, **args) -> Dict[str, Any]:
10
+ """
11
+ Tool implementation to index a directory of code.
12
+ Runs indexing asynchronously via a background job.
13
+ """
14
+ path = args.get("path")
15
+ is_dependency = args.get("is_dependency", False)
16
+
17
+ try:
18
+ path_obj = Path(path).resolve()
19
+
20
+ if not path_obj.exists():
21
+ return {
22
+ "success": True,
23
+ "status": "path_not_found",
24
+ "message": f"Path '{path}' does not exist."
25
+ }
26
+
27
+ # Prevent re-indexing the same repository.
28
+ indexed_repos = list_repos_func().get("repositories", [])
29
+ for repo in indexed_repos:
30
+ if repo_record_matches_path(repo, path_obj):
31
+ return {
32
+ "success": False,
33
+ "message": f"Repository '{path}' is already indexed."
34
+ }
35
+
36
+ # Estimate time and create a job for the user to track.
37
+ total_files, estimated_time = graph_builder.estimate_processing_time(path_obj)
38
+ job_id = job_manager.create_job(str(path_obj), is_dependency)
39
+ job_manager.update_job(job_id, total_files=total_files, estimated_duration=estimated_time)
40
+
41
+ # Create the coroutine for the background task and schedule it on the main event loop.
42
+ coro = graph_builder.build_graph_from_path_async(
43
+ path_obj, is_dependency, job_id
44
+ )
45
+ asyncio.run_coroutine_threadsafe(coro, loop)
46
+
47
+ debug_log(f"Started background job {job_id} for path: {str(path_obj)}, is_dependency: {is_dependency}")
48
+
49
+ return {
50
+ "success": True, "job_id": job_id,
51
+ "message": f"Background processing started for {str(path_obj)}",
52
+ "estimated_files": total_files,
53
+ "estimated_duration_seconds": round(estimated_time, 2),
54
+ "estimated_duration_human": f"{int(estimated_time // 60)}m {int(estimated_time % 60)}s" if estimated_time >= 60 else f"{int(estimated_time)}s",
55
+ "instructions": f"Use 'check_job_status' with job_id '{job_id}' to monitor progress"
56
+ }
57
+
58
+ except Exception as e:
59
+ debug_log(f"Error creating background job: {str(e)}")
60
+ return {"error": f"Failed to start background processing: {str(e)}"}
61
+
62
+ def add_package_to_graph(graph_builder, job_manager, loop, list_repos_func, **args) -> Dict[str, Any]:
63
+ """Tool to add a package to the graph by auto-discovering its location"""
64
+ package_name = args.get("package_name")
65
+ language = args.get("language")
66
+ is_dependency = args.get("is_dependency", True)
67
+
68
+ if not language:
69
+ return {"error": "The 'language' parameter is required."}
70
+
71
+ try:
72
+ # Check if the package is already indexed
73
+ indexed_repos = list_repos_func().get("repositories", [])
74
+ for repo in indexed_repos:
75
+ if repo.get("is_dependency") and (repo.get("name") == package_name or repo.get("name") == f"{package_name}.py"):
76
+ return {
77
+ "success": False,
78
+ "message": f"Package '{package_name}' is already indexed."
79
+ }
80
+
81
+ package_path = get_local_package_path(package_name, language)
82
+
83
+ if not package_path:
84
+ return {"error": f"Could not find package '{package_name}' for language '{language}'. Make sure it's installed."}
85
+
86
+ if not os.path.exists(package_path):
87
+ return {"error": f"Package path '{package_path}' does not exist"}
88
+
89
+ path_obj = Path(package_path)
90
+
91
+ total_files, estimated_time = graph_builder.estimate_processing_time(path_obj)
92
+
93
+ job_id = job_manager.create_job(package_path, is_dependency)
94
+
95
+ job_manager.update_job(job_id, total_files=total_files, estimated_duration=estimated_time)
96
+
97
+ coro = graph_builder.build_graph_from_path_async(
98
+ path_obj, is_dependency, job_id
99
+ )
100
+ asyncio.run_coroutine_threadsafe(coro, loop)
101
+
102
+ debug_log(f"Started background job {job_id} for package: {package_name} at {package_path}, is_dependency: {is_dependency}")
103
+
104
+ return {
105
+ "success": True, "job_id": job_id, "package_name": package_name,
106
+ "discovered_path": package_path,
107
+ "message": f"Background processing started for package '{package_name}'",
108
+ "estimated_files": total_files,
109
+ "estimated_duration_seconds": round(estimated_time, 2),
110
+ "estimated_duration_human": f"{int(estimated_time // 60)}m {int(estimated_time % 60)}s" if estimated_time >= 60 else f"{int(estimated_time)}s",
111
+ "instructions": f"Use 'check_job_status' with job_id '{job_id}' to monitor progress"
112
+ }
113
+
114
+ except Exception as e:
115
+ debug_log(f"Error creating background job for package {package_name}: {str(e)}")
116
+ return {"error": f"Failed to start background processing for package '{package_name}': {str(e)}"}