codegraphcontext-rust 0.5.5__cp310-cp310-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.
- codegraphcontext/__init__.py +2 -0
- codegraphcontext/__main__.py +5 -0
- codegraphcontext/_cgc_rust.cp310-win_amd64.pyd +0 -0
- codegraphcontext/cli/__init__.py +1 -0
- codegraphcontext/cli/cli_helpers.py +774 -0
- codegraphcontext/cli/config_manager.py +1051 -0
- codegraphcontext/cli/main.py +2386 -0
- codegraphcontext/cli/registry_commands.py +404 -0
- codegraphcontext/cli/setup_macos.py +93 -0
- codegraphcontext/cli/setup_wizard.py +991 -0
- codegraphcontext/cli/visualizer.py +50 -0
- codegraphcontext/core/__init__.py +165 -0
- codegraphcontext/core/bundle_registry.py +181 -0
- codegraphcontext/core/cgc_bundle.py +858 -0
- codegraphcontext/core/cgcignore.py +118 -0
- codegraphcontext/core/database.py +273 -0
- codegraphcontext/core/database_falkordb.py +480 -0
- codegraphcontext/core/database_falkordb_remote.py +199 -0
- codegraphcontext/core/database_kuzu.py +636 -0
- codegraphcontext/core/falkor_worker.py +132 -0
- codegraphcontext/core/jobs.py +132 -0
- codegraphcontext/core/watcher.py +260 -0
- codegraphcontext/prompts.py +125 -0
- codegraphcontext/server.py +471 -0
- codegraphcontext/tool_definitions.py +220 -0
- codegraphcontext/tools/__init__.py +0 -0
- codegraphcontext/tools/advanced_language_query_tool.py +103 -0
- codegraphcontext/tools/code_finder.py +1160 -0
- codegraphcontext/tools/graph_builder.py +337 -0
- codegraphcontext/tools/handlers/analysis_handlers.py +114 -0
- codegraphcontext/tools/handlers/indexing_handlers.py +116 -0
- codegraphcontext/tools/handlers/management_handlers.py +339 -0
- codegraphcontext/tools/handlers/query_handlers.py +83 -0
- codegraphcontext/tools/handlers/watcher_handlers.py +83 -0
- codegraphcontext/tools/indexing/__init__.py +1 -0
- codegraphcontext/tools/indexing/constants.py +25 -0
- codegraphcontext/tools/indexing/discovery.py +64 -0
- codegraphcontext/tools/indexing/engine.py +109 -0
- codegraphcontext/tools/indexing/persistence/__init__.py +3 -0
- codegraphcontext/tools/indexing/persistence/parquet_loader.py +643 -0
- codegraphcontext/tools/indexing/persistence/writer.py +1114 -0
- codegraphcontext/tools/indexing/pipeline.py +171 -0
- codegraphcontext/tools/indexing/pre_scan.py +105 -0
- codegraphcontext/tools/indexing/resolution/__init__.py +8 -0
- codegraphcontext/tools/indexing/resolution/calls.py +253 -0
- codegraphcontext/tools/indexing/resolution/inheritance.py +91 -0
- codegraphcontext/tools/indexing/sanitize.py +41 -0
- codegraphcontext/tools/indexing/schema.py +79 -0
- codegraphcontext/tools/indexing/schema_contract.py +44 -0
- codegraphcontext/tools/indexing/scip_pipeline.py +140 -0
- codegraphcontext/tools/languages/c.py +563 -0
- codegraphcontext/tools/languages/cpp.py +616 -0
- codegraphcontext/tools/languages/csharp.py +551 -0
- codegraphcontext/tools/languages/dart.py +378 -0
- codegraphcontext/tools/languages/elixir.py +460 -0
- codegraphcontext/tools/languages/go.py +499 -0
- codegraphcontext/tools/languages/haskell.py +426 -0
- codegraphcontext/tools/languages/java.py +500 -0
- codegraphcontext/tools/languages/javascript.py +589 -0
- codegraphcontext/tools/languages/kotlin.py +640 -0
- codegraphcontext/tools/languages/perl.py +260 -0
- codegraphcontext/tools/languages/php.py +519 -0
- codegraphcontext/tools/languages/python.py +576 -0
- codegraphcontext/tools/languages/ruby.py +537 -0
- codegraphcontext/tools/languages/rust.py +295 -0
- codegraphcontext/tools/languages/scala.py +519 -0
- codegraphcontext/tools/languages/swift.py +491 -0
- codegraphcontext/tools/languages/typescript.py +575 -0
- codegraphcontext/tools/languages/typescriptjsx.py +151 -0
- codegraphcontext/tools/package_resolver.py +472 -0
- codegraphcontext/tools/query_tool_languages/c_toolkit.py +5 -0
- codegraphcontext/tools/query_tool_languages/cpp_toolkit.py +87 -0
- codegraphcontext/tools/query_tool_languages/csharp_toolkit.py +5 -0
- codegraphcontext/tools/query_tool_languages/dart_toolkit.py +5 -0
- codegraphcontext/tools/query_tool_languages/go_toolkit.py +5 -0
- codegraphcontext/tools/query_tool_languages/haskell_toolkit.py +5 -0
- codegraphcontext/tools/query_tool_languages/java_toolkit.py +5 -0
- codegraphcontext/tools/query_tool_languages/javascript_toolkit.py +5 -0
- codegraphcontext/tools/query_tool_languages/perl_toolkit.py +5 -0
- codegraphcontext/tools/query_tool_languages/python_toolkit.py +5 -0
- codegraphcontext/tools/query_tool_languages/ruby_toolkit.py +5 -0
- codegraphcontext/tools/query_tool_languages/rust_toolkit.py +5 -0
- codegraphcontext/tools/query_tool_languages/scala_toolkit.py +5 -0
- codegraphcontext/tools/query_tool_languages/swift_toolkit.py +5 -0
- codegraphcontext/tools/query_tool_languages/typescript_toolkit.py +5 -0
- codegraphcontext/tools/scip_indexer.py +467 -0
- codegraphcontext/tools/scip_pb2.py +2455 -0
- codegraphcontext/tools/system.py +133 -0
- codegraphcontext/tools/tree_sitter_parser.py +103 -0
- codegraphcontext/utils/debug_log.py +90 -0
- codegraphcontext/utils/path_ignore.py +55 -0
- codegraphcontext/utils/repo_path.py +27 -0
- codegraphcontext/utils/tree_sitter_manager.py +285 -0
- codegraphcontext/utils/visualize_graph.py +0 -0
- codegraphcontext/viz/server.py +282 -0
- codegraphcontext_rust-0.5.5.dist-info/METADATA +537 -0
- codegraphcontext_rust-0.5.5.dist-info/RECORD +101 -0
- codegraphcontext_rust-0.5.5.dist-info/WHEEL +4 -0
- codegraphcontext_rust-0.5.5.dist-info/entry_points.txt +3 -0
- codegraphcontext_rust-0.5.5.dist-info/licenses/LICENSE +21 -0
- codegraphcontext_rust-0.5.5.dist-info/sboms/cgc-python.cyclonedx.json +2333 -0
|
@@ -0,0 +1,339 @@
|
|
|
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 ...core.bundle_registry import BundleRegistry
|
|
120
|
+
from ...core.cgc_bundle import CGCBundle
|
|
121
|
+
|
|
122
|
+
bundle_name = args.get("bundle_name")
|
|
123
|
+
clear_existing = args.get("clear_existing", False)
|
|
124
|
+
|
|
125
|
+
if not bundle_name:
|
|
126
|
+
return {"error": "bundle_name is required"}
|
|
127
|
+
|
|
128
|
+
try:
|
|
129
|
+
debug_log(f"Loading bundle: {bundle_name}")
|
|
130
|
+
|
|
131
|
+
# Check if bundle exists locally
|
|
132
|
+
bundle_path = Path(bundle_name)
|
|
133
|
+
|
|
134
|
+
# If it doesn't exist as-is, try with .cgc extension
|
|
135
|
+
if not bundle_path.exists() and not str(bundle_name).endswith('.cgc'):
|
|
136
|
+
bundle_path = Path(f"{bundle_name}.cgc")
|
|
137
|
+
|
|
138
|
+
if not bundle_path.exists():
|
|
139
|
+
# Try to download from registry
|
|
140
|
+
debug_log(f"Bundle {bundle_name} not found locally, checking registry...")
|
|
141
|
+
download_url, bundle_meta, error = BundleRegistry.find_bundle_download_info(bundle_name)
|
|
142
|
+
|
|
143
|
+
if not download_url:
|
|
144
|
+
return {"error": f"Bundle not found locally or in registry: {bundle_name}. {error}"}
|
|
145
|
+
|
|
146
|
+
# Determine output filename from metadata
|
|
147
|
+
filename = bundle_meta.get('bundle_name', f"{bundle_name}.cgc")
|
|
148
|
+
# Save to current working directory
|
|
149
|
+
target_path = Path.cwd() / filename
|
|
150
|
+
|
|
151
|
+
debug_log(f"Downloading bundle to {target_path}...")
|
|
152
|
+
try:
|
|
153
|
+
BundleRegistry.download_file(download_url, target_path)
|
|
154
|
+
bundle_path = target_path
|
|
155
|
+
debug_log(f"Successfully downloaded to {bundle_path}")
|
|
156
|
+
except Exception as e:
|
|
157
|
+
return {"error": f"Failed to download bundle: {str(e)}"}
|
|
158
|
+
|
|
159
|
+
# Verify the downloaded file exists
|
|
160
|
+
if not bundle_path.exists():
|
|
161
|
+
return {"error": f"Download completed but file not found at {bundle_path}"}
|
|
162
|
+
|
|
163
|
+
# Load the bundle using CGCBundle core class
|
|
164
|
+
bundle = CGCBundle(code_finder.db_manager)
|
|
165
|
+
success, message = bundle.import_from_bundle(
|
|
166
|
+
bundle_path=bundle_path,
|
|
167
|
+
clear_existing=clear_existing
|
|
168
|
+
)
|
|
169
|
+
|
|
170
|
+
if success:
|
|
171
|
+
stats = {}
|
|
172
|
+
# Parse simple stats from message if possible, or just return success
|
|
173
|
+
if "Nodes:" in message:
|
|
174
|
+
import re as _re
|
|
175
|
+
nodes_match = _re.search(r'Nodes:\s*(\d+)', message)
|
|
176
|
+
edges_match = _re.search(r'Edges:\s*(\d+)', message)
|
|
177
|
+
if nodes_match:
|
|
178
|
+
stats["nodes"] = int(nodes_match.group(1))
|
|
179
|
+
if edges_match:
|
|
180
|
+
stats["edges"] = int(edges_match.group(1))
|
|
181
|
+
|
|
182
|
+
return {
|
|
183
|
+
"success": True,
|
|
184
|
+
"message": message,
|
|
185
|
+
"stats": stats
|
|
186
|
+
}
|
|
187
|
+
else:
|
|
188
|
+
return {"error": message}
|
|
189
|
+
|
|
190
|
+
except Exception as e:
|
|
191
|
+
debug_log(f"Error loading bundle: {str(e)}")
|
|
192
|
+
return {"error": f"Failed to load bundle: {str(e)}"}
|
|
193
|
+
|
|
194
|
+
|
|
195
|
+
def search_registry_bundles(code_finder: CodeFinder, **args) -> Dict[str, Any]:
|
|
196
|
+
"""Tool to search for bundles in the registry."""
|
|
197
|
+
from ...core.bundle_registry import BundleRegistry
|
|
198
|
+
|
|
199
|
+
query = args.get("query", "").lower()
|
|
200
|
+
unique_only = args.get("unique_only", False)
|
|
201
|
+
|
|
202
|
+
try:
|
|
203
|
+
debug_log(f"Searching registry for: {query}")
|
|
204
|
+
|
|
205
|
+
# Fetch directly from core registry
|
|
206
|
+
bundles = BundleRegistry.fetch_available_bundles()
|
|
207
|
+
|
|
208
|
+
if not bundles:
|
|
209
|
+
return {
|
|
210
|
+
"success": True,
|
|
211
|
+
"bundles": [],
|
|
212
|
+
"total": 0,
|
|
213
|
+
"message": "No bundles found in registry"
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
# Filter by query if provided
|
|
217
|
+
if query:
|
|
218
|
+
filtered_bundles = []
|
|
219
|
+
for bundle in bundles:
|
|
220
|
+
name = bundle.get('name', '').lower()
|
|
221
|
+
repo = bundle.get('repo', '').lower()
|
|
222
|
+
full_name = bundle.get('full_name', '').lower()
|
|
223
|
+
|
|
224
|
+
if query in name or query in repo or query in full_name:
|
|
225
|
+
filtered_bundles.append(bundle)
|
|
226
|
+
bundles = filtered_bundles
|
|
227
|
+
|
|
228
|
+
# If unique_only, keep only most recent version per package
|
|
229
|
+
if unique_only:
|
|
230
|
+
unique_bundles = {}
|
|
231
|
+
for bundle in bundles:
|
|
232
|
+
base_name = bundle.get('name', 'unknown')
|
|
233
|
+
if base_name not in unique_bundles:
|
|
234
|
+
unique_bundles[base_name] = bundle
|
|
235
|
+
else:
|
|
236
|
+
current_time = bundle.get('generated_at', '')
|
|
237
|
+
existing_time = unique_bundles[base_name].get('generated_at', '')
|
|
238
|
+
if current_time > existing_time:
|
|
239
|
+
unique_bundles[base_name] = bundle
|
|
240
|
+
bundles = list(unique_bundles.values())
|
|
241
|
+
|
|
242
|
+
# Sort by name
|
|
243
|
+
bundles.sort(key=lambda b: (b.get('name', ''), b.get('full_name', '')))
|
|
244
|
+
|
|
245
|
+
return {
|
|
246
|
+
"success": True,
|
|
247
|
+
"bundles": bundles,
|
|
248
|
+
"total": len(bundles),
|
|
249
|
+
"query": query if query else "all",
|
|
250
|
+
"unique_only": unique_only
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
except Exception as e:
|
|
254
|
+
debug_log(f"Error searching registry: {str(e)}")
|
|
255
|
+
return {"error": f"Failed to search registry: {str(e)}"}
|
|
256
|
+
|
|
257
|
+
|
|
258
|
+
def get_repository_stats(code_finder: CodeFinder, **args) -> Dict[str, Any]:
|
|
259
|
+
"""Tool to get statistics about indexed repositories."""
|
|
260
|
+
from pathlib import Path
|
|
261
|
+
|
|
262
|
+
repo_path = args.get("repo_path")
|
|
263
|
+
|
|
264
|
+
try:
|
|
265
|
+
debug_log(f"Getting stats for: {repo_path or 'all repositories'}")
|
|
266
|
+
|
|
267
|
+
with code_finder.db_manager.get_driver().session() as session:
|
|
268
|
+
if repo_path:
|
|
269
|
+
# Stats for specific repository
|
|
270
|
+
repo_path_obj = str(Path(repo_path).resolve())
|
|
271
|
+
|
|
272
|
+
# Check if repository exists
|
|
273
|
+
repo_query = """
|
|
274
|
+
MATCH (r:Repository {path: $path})
|
|
275
|
+
RETURN r
|
|
276
|
+
"""
|
|
277
|
+
result = session.run(repo_query, path=repo_path_obj)
|
|
278
|
+
if not result.single():
|
|
279
|
+
return {
|
|
280
|
+
"success": False,
|
|
281
|
+
"error": f"Repository not found: {repo_path_obj}"
|
|
282
|
+
}
|
|
283
|
+
|
|
284
|
+
# 1. Files
|
|
285
|
+
file_query = "MATCH (r:Repository {path: $path})-[:CONTAINS*]->(f:File) RETURN count(f) as c"
|
|
286
|
+
file_count = session.run(file_query, path=repo_path_obj).single()["c"]
|
|
287
|
+
|
|
288
|
+
# 2. Functions
|
|
289
|
+
func_query = "MATCH (r:Repository {path: $path})-[:CONTAINS*]->(func:Function) RETURN count(func) as c"
|
|
290
|
+
func_count = session.run(func_query, path=repo_path_obj).single()["c"]
|
|
291
|
+
|
|
292
|
+
# 3. Classes
|
|
293
|
+
class_query = "MATCH (r:Repository {path: $path})-[:CONTAINS*]->(cls:Class) RETURN count(cls) as c"
|
|
294
|
+
class_count = session.run(class_query, path=repo_path_obj).single()["c"]
|
|
295
|
+
|
|
296
|
+
# 4. Modules (imported)
|
|
297
|
+
module_query = "MATCH (r:Repository {path: $path})-[:CONTAINS*]->(f:File)-[:IMPORTS]->(m:Module) RETURN count(DISTINCT m) as c"
|
|
298
|
+
module_count = session.run(module_query, path=repo_path_obj).single()["c"]
|
|
299
|
+
|
|
300
|
+
return {
|
|
301
|
+
"success": True,
|
|
302
|
+
"repository": repo_path_obj,
|
|
303
|
+
"stats": {
|
|
304
|
+
"files": file_count,
|
|
305
|
+
"functions": func_count,
|
|
306
|
+
"classes": class_count,
|
|
307
|
+
"modules": module_count
|
|
308
|
+
}
|
|
309
|
+
}
|
|
310
|
+
else:
|
|
311
|
+
# Overall database stats
|
|
312
|
+
repo_count = session.run("MATCH (r:Repository) RETURN count(r) as c").single()["c"]
|
|
313
|
+
|
|
314
|
+
if repo_count > 0:
|
|
315
|
+
file_count = session.run("MATCH (f:File) RETURN count(f) as c").single()["c"]
|
|
316
|
+
func_count = session.run("MATCH (func:Function) RETURN count(func) as c").single()["c"]
|
|
317
|
+
class_count = session.run("MATCH (cls:Class) RETURN count(cls) as c").single()["c"]
|
|
318
|
+
module_count = session.run("MATCH (m:Module) RETURN count(m) as c").single()["c"]
|
|
319
|
+
|
|
320
|
+
return {
|
|
321
|
+
"success": True,
|
|
322
|
+
"stats": {
|
|
323
|
+
"repositories": repo_count,
|
|
324
|
+
"files": file_count,
|
|
325
|
+
"functions": func_count,
|
|
326
|
+
"classes": class_count,
|
|
327
|
+
"modules": module_count
|
|
328
|
+
}
|
|
329
|
+
}
|
|
330
|
+
else:
|
|
331
|
+
return {
|
|
332
|
+
"success": True,
|
|
333
|
+
"stats": {},
|
|
334
|
+
"message": "No data indexed yet"
|
|
335
|
+
}
|
|
336
|
+
|
|
337
|
+
except Exception as e:
|
|
338
|
+
debug_log(f"Error getting stats: {str(e)}")
|
|
339
|
+
return {"error": f"Failed to get stats: {str(e)}"}
|
|
@@ -0,0 +1,83 @@
|
|
|
1
|
+
import re
|
|
2
|
+
import urllib.parse
|
|
3
|
+
from typing import Any, Dict
|
|
4
|
+
from neo4j.exceptions import CypherSyntaxError
|
|
5
|
+
from ...utils.debug_log import debug_log
|
|
6
|
+
|
|
7
|
+
def execute_cypher_query(db_manager, **args) -> Dict[str, Any]:
|
|
8
|
+
"""
|
|
9
|
+
Tool implementation for executing a read-only Cypher query.
|
|
10
|
+
|
|
11
|
+
Important: Includes a safety check to prevent any database modification
|
|
12
|
+
by disallowing keywords like CREATE, MERGE, DELETE, etc.
|
|
13
|
+
"""
|
|
14
|
+
cypher_query = args.get("cypher_query")
|
|
15
|
+
if not cypher_query:
|
|
16
|
+
return {"error": "Cypher query cannot be empty."}
|
|
17
|
+
|
|
18
|
+
# Safety Check: Prevent any write operations to the database.
|
|
19
|
+
# This check first removes all string literals and then checks for forbidden keywords.
|
|
20
|
+
forbidden_keywords = ['CREATE', 'MERGE', 'DELETE', 'SET', 'REMOVE', 'DROP', 'CALL apoc']
|
|
21
|
+
|
|
22
|
+
# Regex to match single or double quoted strings, handling escaped quotes.
|
|
23
|
+
string_literal_pattern = r'"(?:\\.|[^"\\])*"|\'(?:\\.|[^\'\\])*\''
|
|
24
|
+
|
|
25
|
+
# Remove all string literals from the query.
|
|
26
|
+
query_without_strings = re.sub(string_literal_pattern, '', cypher_query)
|
|
27
|
+
|
|
28
|
+
# Now, check for forbidden keywords in the query without strings.
|
|
29
|
+
for keyword in forbidden_keywords:
|
|
30
|
+
if re.search(r'\b' + keyword + r'\b', query_without_strings, re.IGNORECASE):
|
|
31
|
+
return {
|
|
32
|
+
"error": "This tool only supports read-only queries. Prohibited keywords like CREATE, MERGE, DELETE, SET, etc., are not allowed."
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
try:
|
|
36
|
+
debug_log(f"Executing Cypher query: {cypher_query}")
|
|
37
|
+
with db_manager.get_driver().session() as session:
|
|
38
|
+
result = session.run(cypher_query)
|
|
39
|
+
# Convert results to a list of dictionaries for clean JSON serialization.
|
|
40
|
+
records = [record.data() for record in result]
|
|
41
|
+
|
|
42
|
+
return {
|
|
43
|
+
"success": True,
|
|
44
|
+
"query": cypher_query,
|
|
45
|
+
"record_count": len(records),
|
|
46
|
+
"results": records
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
except CypherSyntaxError as e:
|
|
50
|
+
debug_log(f"Cypher syntax error: {str(e)}")
|
|
51
|
+
return {
|
|
52
|
+
"error": "Cypher syntax error.",
|
|
53
|
+
"details": str(e),
|
|
54
|
+
"query": cypher_query
|
|
55
|
+
}
|
|
56
|
+
except Exception as e:
|
|
57
|
+
debug_log(f"Error executing Cypher query: {str(e)}")
|
|
58
|
+
return {
|
|
59
|
+
"error": "An unexpected error occurred while executing the query.",
|
|
60
|
+
"details": str(e)
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
def visualize_graph_query(db_manager, **args) -> Dict[str, Any]:
|
|
64
|
+
"""Tool to generate a visualization URL for the local Playground UI."""
|
|
65
|
+
cypher_query = args.get("cypher_query")
|
|
66
|
+
if not cypher_query:
|
|
67
|
+
return {"error": "Cypher query cannot be empty."}
|
|
68
|
+
|
|
69
|
+
try:
|
|
70
|
+
# We point to the local server started by 'cgc visualize'
|
|
71
|
+
# By default it runs on port 8000
|
|
72
|
+
port = 8000
|
|
73
|
+
encoded_query = urllib.parse.quote(cypher_query)
|
|
74
|
+
visualization_url = f"http://localhost:{port}/index.html?cypher_query={encoded_query}"
|
|
75
|
+
|
|
76
|
+
return {
|
|
77
|
+
"success": True,
|
|
78
|
+
"visualization_url": visualization_url,
|
|
79
|
+
"message": "Click the URL to visualize this specific query in the Playground UI. (Ensure 'cgc visualize' is running)"
|
|
80
|
+
}
|
|
81
|
+
except Exception as e:
|
|
82
|
+
debug_log(f"Error generating visualization URL: {str(e)}")
|
|
83
|
+
return {"error": f"Failed to generate visualization URL: {str(e)}"}
|
|
@@ -0,0 +1,83 @@
|
|
|
1
|
+
from typing import Any, Dict
|
|
2
|
+
|
|
3
|
+
from ...utils.debug_log import error_logger
|
|
4
|
+
from ...utils.repo_path import any_repo_matches_path
|
|
5
|
+
|
|
6
|
+
def list_watched_paths(code_watcher, **args) -> Dict[str, Any]:
|
|
7
|
+
"""Tool to list all currently watched directory paths."""
|
|
8
|
+
try:
|
|
9
|
+
paths = code_watcher.list_watched_paths()
|
|
10
|
+
return {"success": True, "watched_paths": paths}
|
|
11
|
+
except Exception as e:
|
|
12
|
+
return {"error": f"Failed to list watched paths: {str(e)}"}
|
|
13
|
+
|
|
14
|
+
def unwatch_directory(code_watcher, **args) -> Dict[str, Any]:
|
|
15
|
+
"""Tool to stop watching a directory."""
|
|
16
|
+
path = args.get("path")
|
|
17
|
+
if not path:
|
|
18
|
+
return {"error": "Path is a required argument."}
|
|
19
|
+
return code_watcher.unwatch_directory(path)
|
|
20
|
+
|
|
21
|
+
# watch_directory is complex as it depends on other tools and handlers
|
|
22
|
+
# We will keep it in server.py or implement it here passing all dependencies.
|
|
23
|
+
# Let's implement it here as a pure function accepting dependencies.
|
|
24
|
+
# Dependencies: code_watcher, list_repositories_func, add_code_func
|
|
25
|
+
|
|
26
|
+
def watch_directory(code_watcher, list_repositories_func, add_code_func, **args) -> Dict[str, Any]:
|
|
27
|
+
"""
|
|
28
|
+
Tool implementation to start watching a directory for changes.
|
|
29
|
+
It checks if the path exists, if it's already watched, or if it needs indexing.
|
|
30
|
+
"""
|
|
31
|
+
path = args.get("path")
|
|
32
|
+
from pathlib import Path
|
|
33
|
+
|
|
34
|
+
if not path:
|
|
35
|
+
return {"error": "Path is a required argument."}
|
|
36
|
+
|
|
37
|
+
path_obj = Path(path).resolve()
|
|
38
|
+
path_str = str(path_obj)
|
|
39
|
+
|
|
40
|
+
# 1. Validate the path
|
|
41
|
+
if not path_obj.is_dir():
|
|
42
|
+
return {
|
|
43
|
+
"success": True,
|
|
44
|
+
"status": "path_not_found",
|
|
45
|
+
"message": f"Path '{path_str}' does not exist or is not a directory."
|
|
46
|
+
}
|
|
47
|
+
try:
|
|
48
|
+
# Check if already watching
|
|
49
|
+
if path_str in code_watcher.watched_paths:
|
|
50
|
+
return {"success": True, "message": f"Already watching directory: {path_str}"}
|
|
51
|
+
|
|
52
|
+
# 2. Check if the repository is already indexed
|
|
53
|
+
indexed_repos_result = list_repositories_func()
|
|
54
|
+
indexed_repos = indexed_repos_result.get("repositories", [])
|
|
55
|
+
is_already_indexed = any_repo_matches_path(indexed_repos, path_obj)
|
|
56
|
+
|
|
57
|
+
# 3. Decide whether to perform an initial scan
|
|
58
|
+
if is_already_indexed:
|
|
59
|
+
# If already indexed, just start the watcher without a scan
|
|
60
|
+
code_watcher.watch_directory(path_str, perform_initial_scan=False)
|
|
61
|
+
return {
|
|
62
|
+
"success": True,
|
|
63
|
+
"message": f"Path '{path_str}' is already indexed. Now watching for live changes."
|
|
64
|
+
}
|
|
65
|
+
else:
|
|
66
|
+
# If not indexed, perform the scan AND start the watcher
|
|
67
|
+
scan_job_result = add_code_func(path=path_str, is_dependency=False)
|
|
68
|
+
|
|
69
|
+
if "error" in scan_job_result:
|
|
70
|
+
return scan_job_result
|
|
71
|
+
|
|
72
|
+
code_watcher.watch_directory(path_str, perform_initial_scan=True)
|
|
73
|
+
|
|
74
|
+
return {
|
|
75
|
+
"success": True,
|
|
76
|
+
"message": f"Path '{path_str}' was not indexed. Started initial scan and now watching for live changes.",
|
|
77
|
+
"job_id": scan_job_result.get("job_id"),
|
|
78
|
+
"details": "Use check_job_status to monitor the initial scan."
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
except Exception as e:
|
|
82
|
+
error_logger(f"Failed to start watching directory {path}: {e}")
|
|
83
|
+
return {"error": f"Failed to start watching directory: {str(e)}"}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
# Indexing pipeline package (discovery, pre-scan, persistence, resolution).
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
"""Shared constants for the indexing pipeline."""
|
|
2
|
+
|
|
3
|
+
DEFAULT_IGNORE_PATTERNS = [
|
|
4
|
+
"node_modules/",
|
|
5
|
+
"venv/",
|
|
6
|
+
".venv/",
|
|
7
|
+
"env/",
|
|
8
|
+
".env/",
|
|
9
|
+
"dist/",
|
|
10
|
+
"build/",
|
|
11
|
+
"target/",
|
|
12
|
+
"out/",
|
|
13
|
+
".git/",
|
|
14
|
+
"__pycache__/",
|
|
15
|
+
"*.png",
|
|
16
|
+
"*.jpg",
|
|
17
|
+
"*.jpeg",
|
|
18
|
+
"*.gif",
|
|
19
|
+
"*.svg",
|
|
20
|
+
"*.mp4",
|
|
21
|
+
"*.mp3",
|
|
22
|
+
"*.zip",
|
|
23
|
+
"*.tar",
|
|
24
|
+
"*.gz",
|
|
25
|
+
]
|
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
"""Enumerate files to index with ignore rules."""
|
|
2
|
+
|
|
3
|
+
from pathlib import Path
|
|
4
|
+
from typing import List, Optional, Tuple
|
|
5
|
+
|
|
6
|
+
from ...core.cgcignore import build_ignore_spec
|
|
7
|
+
from ...utils.debug_log import debug_log, warning_logger
|
|
8
|
+
from .constants import DEFAULT_IGNORE_PATTERNS
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
def discover_files_to_index(
|
|
12
|
+
path: Path,
|
|
13
|
+
cgcignore_path: Optional[str] = None,
|
|
14
|
+
) -> Tuple[List[Path], Path]:
|
|
15
|
+
"""
|
|
16
|
+
Returns (files, ignore_root). *ignore_root* is used for .cgcignore relative matching.
|
|
17
|
+
"""
|
|
18
|
+
ignore_root = path.resolve() if path.is_dir() else path.resolve().parent
|
|
19
|
+
|
|
20
|
+
spec = None
|
|
21
|
+
try:
|
|
22
|
+
spec, resolved_cgcignore = build_ignore_spec(
|
|
23
|
+
ignore_root=ignore_root,
|
|
24
|
+
default_patterns=DEFAULT_IGNORE_PATTERNS,
|
|
25
|
+
explicit_path=cgcignore_path,
|
|
26
|
+
)
|
|
27
|
+
if resolved_cgcignore:
|
|
28
|
+
debug_log(f"Using .cgcignore at {resolved_cgcignore} (filtering relative to {ignore_root})")
|
|
29
|
+
except OSError as e:
|
|
30
|
+
warning_logger(f"Could not load/create .cgcignore: {e}")
|
|
31
|
+
|
|
32
|
+
all_files = path.rglob("*") if path.is_dir() else [path]
|
|
33
|
+
files = [f for f in all_files if f.is_file()]
|
|
34
|
+
|
|
35
|
+
from ...cli.config_manager import get_config_value
|
|
36
|
+
|
|
37
|
+
ignore_dirs_str = get_config_value("IGNORE_DIRS") or ""
|
|
38
|
+
if ignore_dirs_str and path.is_dir():
|
|
39
|
+
ignore_dirs = {d.strip().lower() for d in ignore_dirs_str.split(",") if d.strip()}
|
|
40
|
+
if ignore_dirs:
|
|
41
|
+
kept_files = []
|
|
42
|
+
for f in files:
|
|
43
|
+
try:
|
|
44
|
+
parts = set(p.lower() for p in f.relative_to(path).parent.parts)
|
|
45
|
+
if not parts.intersection(ignore_dirs):
|
|
46
|
+
kept_files.append(f)
|
|
47
|
+
except ValueError:
|
|
48
|
+
kept_files.append(f)
|
|
49
|
+
files = kept_files
|
|
50
|
+
|
|
51
|
+
if spec:
|
|
52
|
+
filtered_files = []
|
|
53
|
+
for f in files:
|
|
54
|
+
try:
|
|
55
|
+
rel_path = f.relative_to(ignore_root).as_posix()
|
|
56
|
+
if not spec.match_file(rel_path):
|
|
57
|
+
filtered_files.append(f)
|
|
58
|
+
else:
|
|
59
|
+
debug_log(f"Ignored file based on .cgcignore: {rel_path}")
|
|
60
|
+
except ValueError:
|
|
61
|
+
filtered_files.append(f)
|
|
62
|
+
files = filtered_files
|
|
63
|
+
|
|
64
|
+
return files, ignore_root
|