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.
Files changed (101) hide show
  1. codegraphcontext/__init__.py +2 -0
  2. codegraphcontext/__main__.py +5 -0
  3. codegraphcontext/_cgc_rust.cp310-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,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