codegraphcontext 0.1.35__tar.gz → 0.1.36__tar.gz

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 (86) hide show
  1. {codegraphcontext-0.1.35/src/codegraphcontext.egg-info → codegraphcontext-0.1.36}/PKG-INFO +2 -2
  2. {codegraphcontext-0.1.35 → codegraphcontext-0.1.36}/README.md +1 -1
  3. {codegraphcontext-0.1.35 → codegraphcontext-0.1.36}/pyproject.toml +1 -1
  4. {codegraphcontext-0.1.35 → codegraphcontext-0.1.36}/src/codegraphcontext/cli/registry_commands.py +74 -0
  5. {codegraphcontext-0.1.35 → codegraphcontext-0.1.36}/src/codegraphcontext/server.py +14 -1
  6. {codegraphcontext-0.1.35 → codegraphcontext-0.1.36}/src/codegraphcontext/tool_definitions.py +33 -0
  7. {codegraphcontext-0.1.35 → codegraphcontext-0.1.36}/src/codegraphcontext/tools/graph_builder.py +4 -0
  8. codegraphcontext-0.1.36/src/codegraphcontext/tools/handlers/management_handlers.py +304 -0
  9. codegraphcontext-0.1.36/src/codegraphcontext/tools/languages/haskell.py +533 -0
  10. codegraphcontext-0.1.36/src/codegraphcontext/tools/query_tool_languages/haskell_toolkit.py +5 -0
  11. {codegraphcontext-0.1.35 → codegraphcontext-0.1.36}/src/codegraphcontext/utils/tree_sitter_manager.py +1 -0
  12. {codegraphcontext-0.1.35 → codegraphcontext-0.1.36/src/codegraphcontext.egg-info}/PKG-INFO +2 -2
  13. {codegraphcontext-0.1.35 → codegraphcontext-0.1.36}/src/codegraphcontext.egg-info/SOURCES.txt +2 -0
  14. codegraphcontext-0.1.35/src/codegraphcontext/tools/handlers/management_handlers.py +0 -113
  15. {codegraphcontext-0.1.35 → codegraphcontext-0.1.36}/LICENSE +0 -0
  16. {codegraphcontext-0.1.35 → codegraphcontext-0.1.36}/MANIFEST.in +0 -0
  17. {codegraphcontext-0.1.35 → codegraphcontext-0.1.36}/setup.cfg +0 -0
  18. {codegraphcontext-0.1.35 → codegraphcontext-0.1.36}/src/codegraphcontext/__init__.py +0 -0
  19. {codegraphcontext-0.1.35 → codegraphcontext-0.1.36}/src/codegraphcontext/__main__.py +0 -0
  20. {codegraphcontext-0.1.35 → codegraphcontext-0.1.36}/src/codegraphcontext/cli/__init__.py +0 -0
  21. {codegraphcontext-0.1.35 → codegraphcontext-0.1.36}/src/codegraphcontext/cli/cli_helpers.py +0 -0
  22. {codegraphcontext-0.1.35 → codegraphcontext-0.1.36}/src/codegraphcontext/cli/config_manager.py +0 -0
  23. {codegraphcontext-0.1.35 → codegraphcontext-0.1.36}/src/codegraphcontext/cli/main.py +0 -0
  24. {codegraphcontext-0.1.35 → codegraphcontext-0.1.36}/src/codegraphcontext/cli/setup_macos.py +0 -0
  25. {codegraphcontext-0.1.35 → codegraphcontext-0.1.36}/src/codegraphcontext/cli/setup_wizard.py +0 -0
  26. {codegraphcontext-0.1.35 → codegraphcontext-0.1.36}/src/codegraphcontext/cli/visualizer.py +0 -0
  27. {codegraphcontext-0.1.35 → codegraphcontext-0.1.36}/src/codegraphcontext/core/__init__.py +0 -0
  28. {codegraphcontext-0.1.35 → codegraphcontext-0.1.36}/src/codegraphcontext/core/cgc_bundle.py +0 -0
  29. {codegraphcontext-0.1.35 → codegraphcontext-0.1.36}/src/codegraphcontext/core/database.py +0 -0
  30. {codegraphcontext-0.1.35 → codegraphcontext-0.1.36}/src/codegraphcontext/core/database_falkordb.py +0 -0
  31. {codegraphcontext-0.1.35 → codegraphcontext-0.1.36}/src/codegraphcontext/core/falkor_worker.py +0 -0
  32. {codegraphcontext-0.1.35 → codegraphcontext-0.1.36}/src/codegraphcontext/core/jobs.py +0 -0
  33. {codegraphcontext-0.1.35 → codegraphcontext-0.1.36}/src/codegraphcontext/core/watcher.py +0 -0
  34. {codegraphcontext-0.1.35 → codegraphcontext-0.1.36}/src/codegraphcontext/prompts.py +0 -0
  35. {codegraphcontext-0.1.35 → codegraphcontext-0.1.36}/src/codegraphcontext/tools/__init__.py +0 -0
  36. {codegraphcontext-0.1.35 → codegraphcontext-0.1.36}/src/codegraphcontext/tools/advanced_language_query_tool.py +0 -0
  37. {codegraphcontext-0.1.35 → codegraphcontext-0.1.36}/src/codegraphcontext/tools/code_finder.py +0 -0
  38. {codegraphcontext-0.1.35 → codegraphcontext-0.1.36}/src/codegraphcontext/tools/handlers/analysis_handlers.py +0 -0
  39. {codegraphcontext-0.1.35 → codegraphcontext-0.1.36}/src/codegraphcontext/tools/handlers/indexing_handlers.py +0 -0
  40. {codegraphcontext-0.1.35 → codegraphcontext-0.1.36}/src/codegraphcontext/tools/handlers/query_handlers.py +0 -0
  41. {codegraphcontext-0.1.35 → codegraphcontext-0.1.36}/src/codegraphcontext/tools/handlers/watcher_handlers.py +0 -0
  42. {codegraphcontext-0.1.35 → codegraphcontext-0.1.36}/src/codegraphcontext/tools/languages/c.py +0 -0
  43. {codegraphcontext-0.1.35 → codegraphcontext-0.1.36}/src/codegraphcontext/tools/languages/cpp.py +0 -0
  44. {codegraphcontext-0.1.35 → codegraphcontext-0.1.36}/src/codegraphcontext/tools/languages/csharp.py +0 -0
  45. {codegraphcontext-0.1.35 → codegraphcontext-0.1.36}/src/codegraphcontext/tools/languages/go.py +0 -0
  46. {codegraphcontext-0.1.35 → codegraphcontext-0.1.36}/src/codegraphcontext/tools/languages/java.py +0 -0
  47. {codegraphcontext-0.1.35 → codegraphcontext-0.1.36}/src/codegraphcontext/tools/languages/javascript.py +0 -0
  48. {codegraphcontext-0.1.35 → codegraphcontext-0.1.36}/src/codegraphcontext/tools/languages/kotlin.py +0 -0
  49. {codegraphcontext-0.1.35 → codegraphcontext-0.1.36}/src/codegraphcontext/tools/languages/php.py +0 -0
  50. {codegraphcontext-0.1.35 → codegraphcontext-0.1.36}/src/codegraphcontext/tools/languages/python.py +0 -0
  51. {codegraphcontext-0.1.35 → codegraphcontext-0.1.36}/src/codegraphcontext/tools/languages/ruby.py +0 -0
  52. {codegraphcontext-0.1.35 → codegraphcontext-0.1.36}/src/codegraphcontext/tools/languages/rust.py +0 -0
  53. {codegraphcontext-0.1.35 → codegraphcontext-0.1.36}/src/codegraphcontext/tools/languages/scala.py +0 -0
  54. {codegraphcontext-0.1.35 → codegraphcontext-0.1.36}/src/codegraphcontext/tools/languages/swift.py +0 -0
  55. {codegraphcontext-0.1.35 → codegraphcontext-0.1.36}/src/codegraphcontext/tools/languages/typescript.py +0 -0
  56. {codegraphcontext-0.1.35 → codegraphcontext-0.1.36}/src/codegraphcontext/tools/languages/typescriptjsx.py +0 -0
  57. {codegraphcontext-0.1.35 → codegraphcontext-0.1.36}/src/codegraphcontext/tools/package_resolver.py +0 -0
  58. {codegraphcontext-0.1.35 → codegraphcontext-0.1.36}/src/codegraphcontext/tools/query_tool_languages/c_toolkit.py +0 -0
  59. {codegraphcontext-0.1.35 → codegraphcontext-0.1.36}/src/codegraphcontext/tools/query_tool_languages/cpp_toolkit.py +0 -0
  60. {codegraphcontext-0.1.35 → codegraphcontext-0.1.36}/src/codegraphcontext/tools/query_tool_languages/csharp_toolkit.py +0 -0
  61. {codegraphcontext-0.1.35 → codegraphcontext-0.1.36}/src/codegraphcontext/tools/query_tool_languages/go_toolkit.py +0 -0
  62. {codegraphcontext-0.1.35 → codegraphcontext-0.1.36}/src/codegraphcontext/tools/query_tool_languages/java_toolkit.py +0 -0
  63. {codegraphcontext-0.1.35 → codegraphcontext-0.1.36}/src/codegraphcontext/tools/query_tool_languages/javascript_toolkit.py +0 -0
  64. {codegraphcontext-0.1.35 → codegraphcontext-0.1.36}/src/codegraphcontext/tools/query_tool_languages/python_toolkit.py +0 -0
  65. {codegraphcontext-0.1.35 → codegraphcontext-0.1.36}/src/codegraphcontext/tools/query_tool_languages/ruby_toolkit.py +0 -0
  66. {codegraphcontext-0.1.35 → codegraphcontext-0.1.36}/src/codegraphcontext/tools/query_tool_languages/rust_toolkit.py +0 -0
  67. {codegraphcontext-0.1.35 → codegraphcontext-0.1.36}/src/codegraphcontext/tools/query_tool_languages/scala_toolkit.py +0 -0
  68. {codegraphcontext-0.1.35 → codegraphcontext-0.1.36}/src/codegraphcontext/tools/query_tool_languages/swift_toolkit.py +0 -0
  69. {codegraphcontext-0.1.35 → codegraphcontext-0.1.36}/src/codegraphcontext/tools/query_tool_languages/typescript_toolkit.py +0 -0
  70. {codegraphcontext-0.1.35 → codegraphcontext-0.1.36}/src/codegraphcontext/tools/system.py +0 -0
  71. {codegraphcontext-0.1.35 → codegraphcontext-0.1.36}/src/codegraphcontext/utils/debug_log.py +0 -0
  72. {codegraphcontext-0.1.35 → codegraphcontext-0.1.36}/src/codegraphcontext/utils/visualize_graph.py +0 -0
  73. {codegraphcontext-0.1.35 → codegraphcontext-0.1.36}/src/codegraphcontext.egg-info/dependency_links.txt +0 -0
  74. {codegraphcontext-0.1.35 → codegraphcontext-0.1.36}/src/codegraphcontext.egg-info/entry_points.txt +0 -0
  75. {codegraphcontext-0.1.35 → codegraphcontext-0.1.36}/src/codegraphcontext.egg-info/requires.txt +0 -0
  76. {codegraphcontext-0.1.35 → codegraphcontext-0.1.36}/src/codegraphcontext.egg-info/top_level.txt +0 -0
  77. {codegraphcontext-0.1.35 → codegraphcontext-0.1.36}/tests/test_cpp_parser.py +0 -0
  78. {codegraphcontext-0.1.35 → codegraphcontext-0.1.36}/tests/test_database_validation.py +0 -0
  79. {codegraphcontext-0.1.35 → codegraphcontext-0.1.36}/tests/test_end_to_end.py +0 -0
  80. {codegraphcontext-0.1.35 → codegraphcontext-0.1.36}/tests/test_graph_indexing.py +0 -0
  81. {codegraphcontext-0.1.35 → codegraphcontext-0.1.36}/tests/test_graph_indexing_js.py +0 -0
  82. {codegraphcontext-0.1.35 → codegraphcontext-0.1.36}/tests/test_kotlin_parser.py +0 -0
  83. {codegraphcontext-0.1.35 → codegraphcontext-0.1.36}/tests/test_swift_parser.py +0 -0
  84. {codegraphcontext-0.1.35 → codegraphcontext-0.1.36}/tests/test_tree_sitter_manager.py +0 -0
  85. {codegraphcontext-0.1.35 → codegraphcontext-0.1.36}/tests/test_typescript_parser.py +0 -0
  86. {codegraphcontext-0.1.35 → codegraphcontext-0.1.36}/tests/test_visualization.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: codegraphcontext
3
- Version: 0.1.35
3
+ Version: 0.1.36
4
4
  Summary: An MCP server that indexes local code into a graph database to provide context to AI assistants.
5
5
  Author-email: Shashank Shekhar Singh <shashankshekharsingh1205@gmail.com>
6
6
  License: MIT License
@@ -91,7 +91,7 @@ A powerful **MCP server** and **CLI toolkit** that indexes local code into a gra
91
91
  ![Using the MCP server](https://github.com/CodeGraphContext/CodeGraphContext/blob/main/images/Usecase.gif)
92
92
 
93
93
  ## Project Details
94
- - **Version:** 0.1.35
94
+ - **Version:** 0.1.36
95
95
  - **Authors:** Shashank Shekhar Singh <shashankshekharsingh1205@gmail.com>
96
96
  - **License:** MIT License (See [LICENSE](LICENSE) for details)
97
97
  - **Website:** [CodeGraphContext](http://codegraphcontext.vercel.app/)
@@ -29,7 +29,7 @@ A powerful **MCP server** and **CLI toolkit** that indexes local code into a gra
29
29
  ![Using the MCP server](https://github.com/CodeGraphContext/CodeGraphContext/blob/main/images/Usecase.gif)
30
30
 
31
31
  ## Project Details
32
- - **Version:** 0.1.35
32
+ - **Version:** 0.1.36
33
33
  - **Authors:** Shashank Shekhar Singh <shashankshekharsingh1205@gmail.com>
34
34
  - **License:** MIT License (See [LICENSE](LICENSE) for details)
35
35
  - **Website:** [CodeGraphContext](http://codegraphcontext.vercel.app/)
@@ -1,6 +1,6 @@
1
1
  [project]
2
2
  name = "codegraphcontext"
3
- version = "0.1.35"
3
+ version = "0.1.36"
4
4
  description = "An MCP server that indexes local code into a graph database to provide context to AI assistants."
5
5
  authors = [{ name = "Shashank Shekhar Singh", email = "shashankshekharsingh1205@gmail.com" }]
6
6
  readme = "README.md"
@@ -408,3 +408,77 @@ def request_bundle(repo_url: str, wait: bool = False):
408
408
  if wait:
409
409
  console.print("\n[yellow]Note: Automatic waiting not yet implemented.[/yellow]")
410
410
  console.print("[dim]Please check back in 5-10 minutes and use 'cgc registry download <name>'[/dim]")
411
+
412
+
413
+ def load_bundle_command(bundle_name: str, clear_existing: bool = False):
414
+ """
415
+ Load a bundle (for MCP tool integration).
416
+
417
+ This is a wrapper around download_bundle that returns structured data
418
+ instead of using console output and typer.Exit.
419
+
420
+ Args:
421
+ bundle_name: Name of the bundle to load
422
+ clear_existing: Whether to clear existing data before loading
423
+
424
+ Returns:
425
+ Tuple of (success: bool, message: str, stats: dict)
426
+ """
427
+ from pathlib import Path
428
+ from .cli_helpers import _initialize_services
429
+ from ..core.cgc_bundle import CGCBundle
430
+
431
+ try:
432
+ # Initialize services
433
+ services = _initialize_services()
434
+ if not all(services):
435
+ return (False, "Failed to initialize database services", {})
436
+
437
+ db_manager, _, _ = services
438
+
439
+ # Check if bundle exists locally
440
+ bundle_path = Path(bundle_name)
441
+ if not bundle_path.exists():
442
+ # Try to download from registry
443
+ try:
444
+ download_bundle(bundle_name, output_dir=None, auto_load=False)
445
+ # After download, the file should exist
446
+ if not bundle_path.exists():
447
+ # Try with .cgc extension
448
+ bundle_path = Path(f"{bundle_name}.cgc")
449
+ if not bundle_path.exists():
450
+ return (False, f"Bundle not found: {bundle_name}", {})
451
+ except Exception as e:
452
+ return (False, f"Failed to download bundle: {str(e)}", {})
453
+
454
+ # Load the bundle
455
+ bundle = CGCBundle(db_manager)
456
+ success, message = bundle.import_from_bundle(
457
+ bundle_path=bundle_path,
458
+ clear_existing=clear_existing
459
+ )
460
+
461
+ if success:
462
+ # Extract stats from message if available
463
+ stats = {}
464
+ if "Nodes:" in message and "Edges:" in message:
465
+ try:
466
+ parts = message.split("|")
467
+ for part in parts:
468
+ if "Nodes:" in part:
469
+ stats["nodes"] = int(part.split(":")[1].strip().replace(",", ""))
470
+ elif "Edges:" in part:
471
+ stats["edges"] = int(part.split(":")[1].strip().replace(",", ""))
472
+ except:
473
+ pass
474
+
475
+ return (True, message, stats)
476
+ else:
477
+ return (False, message, {})
478
+
479
+ except Exception as e:
480
+ return (False, f"Error loading bundle: {str(e)}", {})
481
+ finally:
482
+ if 'db_manager' in locals():
483
+ db_manager.close_driver()
484
+
@@ -164,6 +164,16 @@ class MCPServer:
164
164
  **args
165
165
  )
166
166
 
167
+ def load_bundle_tool(self, **args) -> Dict[str, Any]:
168
+ return management_handlers.load_bundle(self.code_finder, **args)
169
+
170
+ def search_registry_bundles_tool(self, **args) -> Dict[str, Any]:
171
+ return management_handlers.search_registry_bundles(self.code_finder, **args)
172
+
173
+ def get_repository_stats_tool(self, **args) -> Dict[str, Any]:
174
+ return management_handlers.get_repository_stats(self.code_finder, **args)
175
+
176
+
167
177
  async def handle_tool_call(self, tool_name: str, args: Dict[str, Any]) -> Dict[str, Any]:
168
178
  """
169
179
  Routes a tool call from the AI assistant to the appropriate handler function.
@@ -184,7 +194,10 @@ class MCPServer:
184
194
  "delete_repository": self.delete_repository_tool,
185
195
  "visualize_graph_query": self.visualize_graph_query_tool,
186
196
  "list_watched_paths": self.list_watched_paths_tool,
187
- "unwatch_directory": self.unwatch_directory_tool
197
+ "unwatch_directory": self.unwatch_directory_tool,
198
+ "load_bundle": self.load_bundle_tool,
199
+ "search_registry_bundles": self.search_registry_bundles_tool,
200
+ "get_repository_stats": self.get_repository_stats_tool
188
201
  }
189
202
  handler = tool_map.get(tool_name)
190
203
  if handler:
@@ -156,5 +156,38 @@ TOOLS = {
156
156
  },
157
157
  "required": ["path"]
158
158
  }
159
+ },
160
+ "load_bundle": {
161
+ "name": "load_bundle",
162
+ "description": "Load a pre-indexed .cgc bundle into the database. Can load from local file or automatically download from registry if not found locally. Bundles are portable snapshots of indexed code that load instantly without re-indexing.",
163
+ "inputSchema": {
164
+ "type": "object",
165
+ "properties": {
166
+ "bundle_name": {"type": "string", "description": "Name of the bundle to load (e.g., 'flask', 'pandas', 'flask-main-2579ce9.cgc'). Can be a full filename or just the package name."},
167
+ "clear_existing": {"type": "boolean", "description": "Whether to clear existing data before loading. Use with caution.", "default": False}
168
+ },
169
+ "required": ["bundle_name"]
170
+ }
171
+ },
172
+ "search_registry_bundles": {
173
+ "name": "search_registry_bundles",
174
+ "description": "Search for available pre-indexed bundles in the registry. Returns bundles matching the search query with details like repository, version, size, and download information.",
175
+ "inputSchema": {
176
+ "type": "object",
177
+ "properties": {
178
+ "query": {"type": "string", "description": "Search query to find bundles (searches in name, repository, and description). Leave empty to list all bundles."},
179
+ "unique_only": {"type": "boolean", "description": "If true, show only the most recent version of each package. If false, show all versions.", "default": False}
180
+ }
181
+ }
182
+ },
183
+ "get_repository_stats": {
184
+ "name": "get_repository_stats",
185
+ "description": "Get statistics about indexed repositories, including counts of files, functions, classes, and modules. Can show overall database statistics or stats for a specific repository.",
186
+ "inputSchema": {
187
+ "type": "object",
188
+ "properties": {
189
+ "repo_path": {"type": "string", "description": "Optional: Path to a specific repository. If not provided, returns overall database statistics."}
190
+ }
191
+ }
159
192
  }
160
193
  }
@@ -70,6 +70,9 @@ class TreeSitterParser:
70
70
  elif self.language_name == 'swift':
71
71
  from .languages.swift import SwiftTreeSitterParser
72
72
  self.language_specific_parser = SwiftTreeSitterParser(self)
73
+ elif self.language_name == 'haskell':
74
+ from .languages.haskell import HaskellTreeSitterParser
75
+ self.language_specific_parser = HaskellTreeSitterParser(self)
73
76
 
74
77
 
75
78
 
@@ -114,6 +117,7 @@ class GraphBuilder:
114
117
  '.scala': TreeSitterParser('scala'),
115
118
  '.sc': TreeSitterParser('scala'),
116
119
  '.swift': TreeSitterParser('swift'),
120
+ '.hs': TreeSitterParser('haskell'),
117
121
  }
118
122
  self.create_schema()
119
123
 
@@ -0,0 +1,304 @@
1
+ from typing import Any, Dict
2
+ from dataclasses import asdict
3
+ from datetime import datetime
4
+ from ...core.jobs import JobManager, JobStatus
5
+ from ...utils.debug_log import debug_log
6
+ from ..code_finder import CodeFinder
7
+ from ..graph_builder import GraphBuilder
8
+
9
+ def list_indexed_repositories(code_finder: CodeFinder, **args) -> Dict[str, Any]:
10
+ """Tool to list indexed repositories."""
11
+ try:
12
+ debug_log("Listing indexed repositories.")
13
+ results = code_finder.list_indexed_repositories()
14
+ return {
15
+ "success": True,
16
+ "repositories": results
17
+ }
18
+ except Exception as e:
19
+ debug_log(f"Error listing indexed repositories: {str(e)}")
20
+ return {"error": f"Failed to list indexed repositories: {str(e)}"}
21
+
22
+ def delete_repository(graph_builder: GraphBuilder, **args) -> Dict[str, Any]:
23
+ """Tool to delete a repository from the graph."""
24
+ repo_path = args.get("repo_path")
25
+ try:
26
+ debug_log(f"Deleting repository: {repo_path}")
27
+ if graph_builder.delete_repository_from_graph(repo_path):
28
+ return {
29
+ "success": True,
30
+ "message": f"Repository '{repo_path}' deleted successfully."
31
+ }
32
+ else:
33
+ return {
34
+ "success": False,
35
+ "message": f"Repository '{repo_path}' not found in the graph."
36
+ }
37
+ except Exception as e:
38
+ debug_log(f"Error deleting repository: {str(e)}")
39
+ return {"error": f"Failed to delete repository: {str(e)}"}
40
+
41
+ def check_job_status(job_manager: JobManager, **args) -> Dict[str, Any]:
42
+ """Tool to check job status"""
43
+ job_id = args.get("job_id")
44
+ if not job_id:
45
+ return {"error": "Job ID is a required argument."}
46
+
47
+ try:
48
+ job = job_manager.get_job(job_id)
49
+
50
+ if not job:
51
+ return {
52
+ "success": True, # Return success to avoid generic error wrapper
53
+ "status": "not_found",
54
+ "message": f"Job with ID '{job_id}' not found. The ID may be incorrect or the job may have been cleared after a server restart."
55
+ }
56
+
57
+ job_dict = asdict(job)
58
+
59
+ if job.status == JobStatus.RUNNING:
60
+ if job.estimated_time_remaining:
61
+ remaining = job.estimated_time_remaining
62
+ job_dict["estimated_time_remaining_human"] = (
63
+ f"{int(remaining // 60)}m {int(remaining % 60)}s"
64
+ if remaining >= 60 else f"{int(remaining)}s"
65
+ )
66
+
67
+ if job.start_time:
68
+ elapsed = (datetime.now() - job.start_time).total_seconds()
69
+ job_dict["elapsed_time_human"] = (
70
+ f"{int(elapsed // 60)}m {int(elapsed % 60)}s"
71
+ if elapsed >= 60 else f"{int(elapsed)}s"
72
+ )
73
+
74
+ elif job.status == JobStatus.COMPLETED and job.start_time and job.end_time:
75
+ duration = (job.end_time - job.start_time).total_seconds()
76
+ job_dict["actual_duration_human"] = (
77
+ f"{int(duration // 60)}m {int(duration % 60)}s"
78
+ if duration >= 60 else f"{int(duration)}s"
79
+ )
80
+
81
+ job_dict["start_time"] = job.start_time.strftime("%Y-%m-%d %H:%M:%S")
82
+ if job.end_time:
83
+ job_dict["end_time"] = job.end_time.strftime("%Y-%m-%d %H:%M:%S")
84
+
85
+ job_dict["status"] = job.status.value
86
+
87
+ return {"success": True, "job": job_dict}
88
+
89
+ except Exception as e:
90
+ debug_log(f"Error checking job status: {str(e)}")
91
+ return {"error": f"Failed to check job status: {str(e)}"}
92
+
93
+ def list_jobs(job_manager: JobManager) -> Dict[str, Any]:
94
+ """Tool to list all jobs"""
95
+ try:
96
+ jobs = job_manager.list_jobs()
97
+
98
+ jobs_data = []
99
+ for job in jobs:
100
+ job_dict = asdict(job)
101
+ job_dict["status"] = job.status.value
102
+ job_dict["start_time"] = job.start_time.strftime("%Y-%m-%d %H:%M:%S")
103
+ if job.end_time:
104
+ job_dict["end_time"] = job.end_time.strftime("%Y-%m-%d %H:%M:%S")
105
+ jobs_data.append(job_dict)
106
+
107
+ jobs_data.sort(key=lambda x: x["start_time"], reverse=True)
108
+
109
+ return {"success": True, "jobs": jobs_data, "total_jobs": len(jobs_data)}
110
+
111
+ except Exception as e:
112
+ debug_log(f"Error listing jobs: {str(e)}")
113
+ return {"error": f"Failed to list jobs: {str(e)}"}
114
+
115
+
116
+ def load_bundle(code_finder: CodeFinder, **args) -> Dict[str, Any]:
117
+ """Tool to load a .cgc bundle into the database."""
118
+ from pathlib import Path
119
+ from ...cli.registry_commands import load_bundle_command
120
+
121
+ bundle_name = args.get("bundle_name")
122
+ clear_existing = args.get("clear_existing", False)
123
+
124
+ if not bundle_name:
125
+ return {"error": "bundle_name is required"}
126
+
127
+ try:
128
+ debug_log(f"Loading bundle: {bundle_name}")
129
+
130
+ # Use the existing load_bundle_command from CLI
131
+ # This handles both local files and auto-download from registry
132
+ success, message, stats = load_bundle_command(
133
+ bundle_name=bundle_name,
134
+ clear_existing=clear_existing
135
+ )
136
+
137
+ if success:
138
+ return {
139
+ "success": True,
140
+ "message": message,
141
+ "stats": stats
142
+ }
143
+ else:
144
+ return {"error": message}
145
+
146
+ except Exception as e:
147
+ debug_log(f"Error loading bundle: {str(e)}")
148
+ return {"error": f"Failed to load bundle: {str(e)}"}
149
+
150
+
151
+ def search_registry_bundles(code_finder: CodeFinder, **args) -> Dict[str, Any]:
152
+ """Tool to search for bundles in the registry."""
153
+ from ...cli.registry_commands import fetch_available_bundles
154
+
155
+ query = args.get("query", "").lower()
156
+ unique_only = args.get("unique_only", False)
157
+
158
+ try:
159
+ debug_log(f"Searching registry for: {query}")
160
+
161
+ # Fetch all bundles from registry
162
+ bundles = fetch_available_bundles()
163
+
164
+ if not bundles:
165
+ return {
166
+ "success": True,
167
+ "bundles": [],
168
+ "total": 0,
169
+ "message": "No bundles found in registry"
170
+ }
171
+
172
+ # Filter by query if provided
173
+ if query:
174
+ filtered_bundles = []
175
+ for bundle in bundles:
176
+ name = bundle.get('name', '').lower()
177
+ repo = bundle.get('repo', '').lower()
178
+ full_name = bundle.get('full_name', '').lower()
179
+
180
+ if query in name or query in repo or query in full_name:
181
+ filtered_bundles.append(bundle)
182
+ bundles = filtered_bundles
183
+
184
+ # If unique_only, keep only most recent version per package
185
+ if unique_only:
186
+ unique_bundles = {}
187
+ for bundle in bundles:
188
+ base_name = bundle.get('name', 'unknown')
189
+ if base_name not in unique_bundles:
190
+ unique_bundles[base_name] = bundle
191
+ else:
192
+ current_time = bundle.get('generated_at', '')
193
+ existing_time = unique_bundles[base_name].get('generated_at', '')
194
+ if current_time > existing_time:
195
+ unique_bundles[base_name] = bundle
196
+ bundles = list(unique_bundles.values())
197
+
198
+ # Sort by name
199
+ bundles.sort(key=lambda b: (b.get('name', ''), b.get('full_name', '')))
200
+
201
+ return {
202
+ "success": True,
203
+ "bundles": bundles,
204
+ "total": len(bundles),
205
+ "query": query if query else "all",
206
+ "unique_only": unique_only
207
+ }
208
+
209
+ except Exception as e:
210
+ debug_log(f"Error searching registry: {str(e)}")
211
+ return {"error": f"Failed to search registry: {str(e)}"}
212
+
213
+
214
+ def get_repository_stats(code_finder: CodeFinder, **args) -> Dict[str, Any]:
215
+ """Tool to get statistics about indexed repositories."""
216
+ from pathlib import Path
217
+
218
+ repo_path = args.get("repo_path")
219
+
220
+ try:
221
+ debug_log(f"Getting stats for: {repo_path or 'all repositories'}")
222
+
223
+ with code_finder.db_manager.get_driver().session() as session:
224
+ if repo_path:
225
+ # Stats for specific repository
226
+ repo_path_obj = str(Path(repo_path).resolve())
227
+
228
+ # Check if repository exists
229
+ repo_query = """
230
+ MATCH (r:Repository {path: $path})
231
+ RETURN r
232
+ """
233
+ result = session.run(repo_query, path=repo_path_obj)
234
+ if not result.single():
235
+ return {
236
+ "success": False,
237
+ "error": f"Repository not found: {repo_path_obj}"
238
+ }
239
+
240
+ # Get stats for specific repo
241
+ stats_query = """
242
+ MATCH (r:Repository {path: $path})-[:CONTAINS]->(f:File)
243
+ WITH r, count(f) as file_count, f
244
+ OPTIONAL MATCH (f)-[:CONTAINS]->(func:Function)
245
+ OPTIONAL MATCH (f)-[:CONTAINS]->(cls:Class)
246
+ OPTIONAL MATCH (f)-[:IMPORTS]->(m:Module)
247
+ RETURN
248
+ file_count,
249
+ count(DISTINCT func) as function_count,
250
+ count(DISTINCT cls) as class_count,
251
+ count(DISTINCT m) as module_count
252
+ """
253
+ result = session.run(stats_query, path=repo_path_obj)
254
+ record = result.single()
255
+
256
+ return {
257
+ "success": True,
258
+ "repository": repo_path_obj,
259
+ "stats": {
260
+ "files": record["file_count"] if record else 0,
261
+ "functions": record["function_count"] if record else 0,
262
+ "classes": record["class_count"] if record else 0,
263
+ "modules": record["module_count"] if record else 0
264
+ }
265
+ }
266
+ else:
267
+ # Overall database stats
268
+ stats_query = """
269
+ MATCH (r:Repository)
270
+ OPTIONAL MATCH (f:File)
271
+ OPTIONAL MATCH (func:Function)
272
+ OPTIONAL MATCH (cls:Class)
273
+ OPTIONAL MATCH (m:Module)
274
+ RETURN
275
+ count(DISTINCT r) as repo_count,
276
+ count(DISTINCT f) as file_count,
277
+ count(DISTINCT func) as function_count,
278
+ count(DISTINCT cls) as class_count,
279
+ count(DISTINCT m) as module_count
280
+ """
281
+ result = session.run(stats_query)
282
+ record = result.single()
283
+
284
+ if record and record["repo_count"] > 0:
285
+ return {
286
+ "success": True,
287
+ "stats": {
288
+ "repositories": record["repo_count"],
289
+ "files": record["file_count"],
290
+ "functions": record["function_count"],
291
+ "classes": record["class_count"],
292
+ "modules": record["module_count"]
293
+ }
294
+ }
295
+ else:
296
+ return {
297
+ "success": True,
298
+ "stats": {},
299
+ "message": "No data indexed yet"
300
+ }
301
+
302
+ except Exception as e:
303
+ debug_log(f"Error getting stats: {str(e)}")
304
+ return {"error": f"Failed to get stats: {str(e)}"}