codegraphcontext 0.1.4__tar.gz → 0.1.5__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.
- {codegraphcontext-0.1.4/src/codegraphcontext.egg-info → codegraphcontext-0.1.5}/PKG-INFO +7 -1
- {codegraphcontext-0.1.4 → codegraphcontext-0.1.5}/README.md +6 -0
- {codegraphcontext-0.1.4 → codegraphcontext-0.1.5}/pyproject.toml +1 -1
- {codegraphcontext-0.1.4 → codegraphcontext-0.1.5}/src/codegraphcontext/core/jobs.py +19 -0
- {codegraphcontext-0.1.4 → codegraphcontext-0.1.5}/src/codegraphcontext/core/watcher.py +28 -9
- {codegraphcontext-0.1.4 → codegraphcontext-0.1.5}/src/codegraphcontext/server.py +126 -11
- {codegraphcontext-0.1.4 → codegraphcontext-0.1.5}/src/codegraphcontext/tools/code_finder.py +52 -1
- {codegraphcontext-0.1.4 → codegraphcontext-0.1.5}/src/codegraphcontext/tools/graph_builder.py +127 -3
- {codegraphcontext-0.1.4 → codegraphcontext-0.1.5/src/codegraphcontext.egg-info}/PKG-INFO +7 -1
- {codegraphcontext-0.1.4 → codegraphcontext-0.1.5}/LICENSE +0 -0
- {codegraphcontext-0.1.4 → codegraphcontext-0.1.5}/setup.cfg +0 -0
- {codegraphcontext-0.1.4 → codegraphcontext-0.1.5}/src/codegraphcontext/__init__.py +0 -0
- {codegraphcontext-0.1.4 → codegraphcontext-0.1.5}/src/codegraphcontext/__main__.py +0 -0
- {codegraphcontext-0.1.4 → codegraphcontext-0.1.5}/src/codegraphcontext/cli/__init__.py +0 -0
- {codegraphcontext-0.1.4 → codegraphcontext-0.1.5}/src/codegraphcontext/cli/main.py +0 -0
- {codegraphcontext-0.1.4 → codegraphcontext-0.1.5}/src/codegraphcontext/cli/setup_wizard.py +0 -0
- {codegraphcontext-0.1.4 → codegraphcontext-0.1.5}/src/codegraphcontext/core/__init__.py +0 -0
- {codegraphcontext-0.1.4 → codegraphcontext-0.1.5}/src/codegraphcontext/core/database.py +0 -0
- {codegraphcontext-0.1.4 → codegraphcontext-0.1.5}/src/codegraphcontext/prompts.py +0 -0
- {codegraphcontext-0.1.4 → codegraphcontext-0.1.5}/src/codegraphcontext/tools/__init__.py +0 -0
- {codegraphcontext-0.1.4 → codegraphcontext-0.1.5}/src/codegraphcontext/tools/import_extractor.py +0 -0
- {codegraphcontext-0.1.4 → codegraphcontext-0.1.5}/src/codegraphcontext/tools/system.py +0 -0
- {codegraphcontext-0.1.4 → codegraphcontext-0.1.5}/src/codegraphcontext.egg-info/SOURCES.txt +0 -0
- {codegraphcontext-0.1.4 → codegraphcontext-0.1.5}/src/codegraphcontext.egg-info/dependency_links.txt +0 -0
- {codegraphcontext-0.1.4 → codegraphcontext-0.1.5}/src/codegraphcontext.egg-info/entry_points.txt +0 -0
- {codegraphcontext-0.1.4 → codegraphcontext-0.1.5}/src/codegraphcontext.egg-info/requires.txt +0 -0
- {codegraphcontext-0.1.4 → codegraphcontext-0.1.5}/src/codegraphcontext.egg-info/top_level.txt +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: codegraphcontext
|
|
3
|
-
Version: 0.1.
|
|
3
|
+
Version: 0.1.5
|
|
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
|
|
@@ -123,6 +123,12 @@ Once the server is running, you can interact with it through your AI assistant u
|
|
|
123
123
|
OR
|
|
124
124
|
- "Keep the code graph updated for the project I'm working on at `~/dev/main-app`."
|
|
125
125
|
|
|
126
|
+
When you ask to watch a directory, the system performs two actions at once:
|
|
127
|
+
1. It kicks off a full scan to index all the code in that directory. This process runs in the background, and you'll receive a `job_id` to track its progress.
|
|
128
|
+
2. It begins watching the directory for any file changes to keep the graph updated in real-time.
|
|
129
|
+
|
|
130
|
+
This means you can start by simply telling the system to watch a directory, and it will handle both the initial indexing and the continuous updates automatically.
|
|
131
|
+
|
|
126
132
|
### Querying and Understanding Code
|
|
127
133
|
|
|
128
134
|
- **Finding where code is defined:**
|
|
@@ -72,6 +72,12 @@ Once the server is running, you can interact with it through your AI assistant u
|
|
|
72
72
|
OR
|
|
73
73
|
- "Keep the code graph updated for the project I'm working on at `~/dev/main-app`."
|
|
74
74
|
|
|
75
|
+
When you ask to watch a directory, the system performs two actions at once:
|
|
76
|
+
1. It kicks off a full scan to index all the code in that directory. This process runs in the background, and you'll receive a `job_id` to track its progress.
|
|
77
|
+
2. It begins watching the directory for any file changes to keep the graph updated in real-time.
|
|
78
|
+
|
|
79
|
+
This means you can start by simply telling the system to watch a directory, and it will handle both the initial indexing and the continuous updates automatically.
|
|
80
|
+
|
|
75
81
|
### Querying and Understanding Code
|
|
76
82
|
|
|
77
83
|
- **Finding where code is defined:**
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
[project]
|
|
2
2
|
name = "codegraphcontext"
|
|
3
|
-
version = "0.1.
|
|
3
|
+
version = "0.1.5"
|
|
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"
|
|
@@ -5,6 +5,8 @@ from datetime import datetime, timedelta
|
|
|
5
5
|
from dataclasses import dataclass, asdict
|
|
6
6
|
from enum import Enum
|
|
7
7
|
from typing import Any, Dict, List, Optional
|
|
8
|
+
from pathlib import Path
|
|
9
|
+
|
|
8
10
|
|
|
9
11
|
class JobStatus(Enum):
|
|
10
12
|
"""Job status enumeration"""
|
|
@@ -90,6 +92,23 @@ class JobManager:
|
|
|
90
92
|
with self.lock:
|
|
91
93
|
return list(self.jobs.values())
|
|
92
94
|
|
|
95
|
+
def find_active_job_by_path(self, path: str) -> Optional[JobInfo]:
|
|
96
|
+
"""Finds the most recent, active job for a given path."""
|
|
97
|
+
with self.lock:
|
|
98
|
+
path_obj = Path(path).resolve()
|
|
99
|
+
|
|
100
|
+
matching_jobs = sorted(
|
|
101
|
+
[job for job in self.jobs.values() if job.path and Path(job.path).resolve() == path_obj],
|
|
102
|
+
key=lambda j: j.start_time,
|
|
103
|
+
reverse=True
|
|
104
|
+
)
|
|
105
|
+
|
|
106
|
+
for job in matching_jobs:
|
|
107
|
+
if job.status in [JobStatus.PENDING, JobStatus.RUNNING]:
|
|
108
|
+
return job
|
|
109
|
+
|
|
110
|
+
return None
|
|
111
|
+
|
|
93
112
|
def cleanup_old_jobs(self, max_age_hours: int = 24):
|
|
94
113
|
"""Clean up jobs older than specified hours"""
|
|
95
114
|
cutoff_time = datetime.now() - timedelta(hours=max_age_hours)
|
|
@@ -10,6 +10,7 @@ from watchdog.events import FileSystemEventHandler
|
|
|
10
10
|
# The actual object is passed in __init__
|
|
11
11
|
if typing.TYPE_CHECKING:
|
|
12
12
|
from codegraphcontext.tools.graph_builder import GraphBuilder
|
|
13
|
+
from codegraphcontext.core.jobs import JobManager
|
|
13
14
|
|
|
14
15
|
|
|
15
16
|
logger = logging.getLogger(__name__)
|
|
@@ -69,22 +70,40 @@ class DebouncedEventHandler(FileSystemEventHandler):
|
|
|
69
70
|
|
|
70
71
|
class CodeWatcher:
|
|
71
72
|
"""Manages file system watching in a background thread."""
|
|
72
|
-
def __init__(self, graph_builder: "GraphBuilder"):
|
|
73
|
+
def __init__(self, graph_builder: "GraphBuilder", job_manager: "JobManager"):
|
|
73
74
|
self.graph_builder = graph_builder
|
|
75
|
+
self.job_manager = job_manager
|
|
74
76
|
self.observer = Observer()
|
|
75
77
|
self.watched_paths = set()
|
|
76
78
|
self.event_handler = DebouncedEventHandler(self.graph_builder)
|
|
77
79
|
|
|
78
80
|
def watch_directory(self, path: str):
|
|
79
|
-
"""
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
81
|
+
"""
|
|
82
|
+
Starts watching a directory and returns the job ID for the initial scan if found.
|
|
83
|
+
"""
|
|
84
|
+
path_obj = Path(path).resolve()
|
|
85
|
+
path_str = str(path_obj)
|
|
86
|
+
|
|
87
|
+
if path_str in self.watched_paths:
|
|
88
|
+
logger.info(f"Path already being watched: {path_str}")
|
|
89
|
+
return {"message": f"Path already being watched: {path_str}"}
|
|
84
90
|
|
|
85
|
-
self.observer.schedule(self.event_handler,
|
|
86
|
-
self.watched_paths.add(
|
|
87
|
-
logger.info(f"Started watching for code changes in: {
|
|
91
|
+
self.observer.schedule(self.event_handler, path_str, recursive=True)
|
|
92
|
+
self.watched_paths.add(path_str)
|
|
93
|
+
logger.info(f"Started watching for code changes in: {path_str}")
|
|
94
|
+
|
|
95
|
+
active_job = self.job_manager.find_active_job_by_path(path_str)
|
|
96
|
+
|
|
97
|
+
response = {
|
|
98
|
+
"message": f"Started watching {path_str} for changes."
|
|
99
|
+
}
|
|
100
|
+
if active_job:
|
|
101
|
+
response["job_id"] = active_job.job_id
|
|
102
|
+
response["message"] += f" An initial scan is in progress under job ID: {active_job.job_id}"
|
|
103
|
+
else:
|
|
104
|
+
response["note"] = "No active initial scan was found for this path. The watcher will only process new changes."
|
|
105
|
+
|
|
106
|
+
return response
|
|
88
107
|
|
|
89
108
|
def start(self):
|
|
90
109
|
"""Starts the observer thread."""
|
|
@@ -60,7 +60,7 @@ class MCPServer:
|
|
|
60
60
|
self.code_finder = CodeFinder(self.db_manager)
|
|
61
61
|
self.import_extractor = ImportExtractor()
|
|
62
62
|
|
|
63
|
-
self.code_watcher = CodeWatcher(self.graph_builder)
|
|
63
|
+
self.code_watcher = CodeWatcher(self.graph_builder, self.job_manager)
|
|
64
64
|
|
|
65
65
|
self._init_tools()
|
|
66
66
|
|
|
@@ -69,7 +69,7 @@ class MCPServer:
|
|
|
69
69
|
self.tools = {
|
|
70
70
|
"add_code_to_graph": {
|
|
71
71
|
"name": "add_code_to_graph",
|
|
72
|
-
"description": "
|
|
72
|
+
"description": "Performs a one-time scan of a local folder to add its code to the graph. Ideal for indexing libraries, dependencies, or projects not being actively modified. Returns a job ID for background processing.",
|
|
73
73
|
"inputSchema": {
|
|
74
74
|
"type": "object",
|
|
75
75
|
"properties": {
|
|
@@ -117,7 +117,7 @@ class MCPServer:
|
|
|
117
117
|
},
|
|
118
118
|
"watch_directory": {
|
|
119
119
|
"name": "watch_directory",
|
|
120
|
-
"description": "
|
|
120
|
+
"description": "Performs an initial scan of a directory and then continuously monitors it for changes, automatically keeping the graph up-to-date. Ideal for projects under active development. Returns a job ID for the initial scan.",
|
|
121
121
|
"inputSchema": {
|
|
122
122
|
"type": "object",
|
|
123
123
|
"properties": { "path": {"type": "string", "description": "Path to directory to watch"} },
|
|
@@ -166,6 +166,47 @@ class MCPServer:
|
|
|
166
166
|
"properties": {},
|
|
167
167
|
"additionalProperties": False
|
|
168
168
|
}
|
|
169
|
+
},
|
|
170
|
+
"calculate_cyclomatic_complexity": {
|
|
171
|
+
"name": "calculate_cyclomatic_complexity",
|
|
172
|
+
"description": "Calculate the cyclomatic complexity of a specific function to measure its complexity.",
|
|
173
|
+
"inputSchema": {
|
|
174
|
+
"type": "object",
|
|
175
|
+
"properties": {
|
|
176
|
+
"function_name": {"type": "string", "description": "The name of the function to analyze."},
|
|
177
|
+
"file_path": {"type": "string", "description": "Optional: The full path to the file containing the function for a more specific query."}
|
|
178
|
+
},
|
|
179
|
+
"required": ["function_name"]
|
|
180
|
+
}
|
|
181
|
+
},
|
|
182
|
+
"find_most_complex_functions": {
|
|
183
|
+
"name": "find_most_complex_functions",
|
|
184
|
+
"description": "Find the most complex functions in the codebase based on cyclomatic complexity.",
|
|
185
|
+
"inputSchema": {
|
|
186
|
+
"type": "object",
|
|
187
|
+
"properties": {
|
|
188
|
+
"limit": {"type": "integer", "description": "The maximum number of complex functions to return.", "default": 10}
|
|
189
|
+
}
|
|
190
|
+
}
|
|
191
|
+
},
|
|
192
|
+
"list_indexed_repositories": {
|
|
193
|
+
"name": "list_indexed_repositories",
|
|
194
|
+
"description": "List all indexed repositories.",
|
|
195
|
+
"inputSchema": {
|
|
196
|
+
"type": "object",
|
|
197
|
+
"properties": {}
|
|
198
|
+
}
|
|
199
|
+
},
|
|
200
|
+
"delete_repository": {
|
|
201
|
+
"name": "delete_repository",
|
|
202
|
+
"description": "Delete an indexed repository from the graph.",
|
|
203
|
+
"inputSchema": {
|
|
204
|
+
"type": "object",
|
|
205
|
+
"properties": {
|
|
206
|
+
"repo_path": {"type": "string", "description": "The path of the repository to delete."}
|
|
207
|
+
},
|
|
208
|
+
"required": ["repo_path"]
|
|
209
|
+
}
|
|
169
210
|
}
|
|
170
211
|
# Other tools like list_imports, add_package_to_graph can be added here following the same pattern
|
|
171
212
|
}
|
|
@@ -276,6 +317,70 @@ class MCPServer:
|
|
|
276
317
|
debug_log(f"Error finding dead code: {str(e)}")
|
|
277
318
|
return {"error": f"Failed to find dead code: {str(e)}"}
|
|
278
319
|
|
|
320
|
+
def calculate_cyclomatic_complexity_tool(self, **args) -> Dict[str, Any]:
|
|
321
|
+
"""Tool to calculate cyclomatic complexity for a given function."""
|
|
322
|
+
function_name = args.get("function_name")
|
|
323
|
+
file_path = args.get("file_path")
|
|
324
|
+
|
|
325
|
+
try:
|
|
326
|
+
debug_log(f"Calculating cyclomatic complexity for function: {function_name}")
|
|
327
|
+
results = self.code_finder.get_cyclomatic_complexity(function_name, file_path)
|
|
328
|
+
|
|
329
|
+
response = {
|
|
330
|
+
"success": True,
|
|
331
|
+
"function_name": function_name,
|
|
332
|
+
"results": results
|
|
333
|
+
}
|
|
334
|
+
if file_path:
|
|
335
|
+
response["file_path"] = file_path
|
|
336
|
+
|
|
337
|
+
return response
|
|
338
|
+
except Exception as e:
|
|
339
|
+
debug_log(f"Error calculating cyclomatic complexity: {str(e)}")
|
|
340
|
+
return {"error": f"Failed to calculate cyclomatic complexity: {str(e)}"}
|
|
341
|
+
|
|
342
|
+
def find_most_complex_functions_tool(self, **args) -> Dict[str, Any]:
|
|
343
|
+
"""Tool to find the most complex functions."""
|
|
344
|
+
limit = args.get("limit", 10)
|
|
345
|
+
try:
|
|
346
|
+
debug_log(f"Finding the top {limit} most complex functions.")
|
|
347
|
+
results = self.code_finder.find_most_complex_functions(limit)
|
|
348
|
+
return {
|
|
349
|
+
"success": True,
|
|
350
|
+
"limit": limit,
|
|
351
|
+
"results": results
|
|
352
|
+
}
|
|
353
|
+
except Exception as e:
|
|
354
|
+
debug_log(f"Error finding most complex functions: {str(e)}")
|
|
355
|
+
return {"error": f"Failed to find most complex functions: {str(e)}"}
|
|
356
|
+
|
|
357
|
+
def list_indexed_repositories_tool(self, **args) -> Dict[str, Any]:
|
|
358
|
+
"""Tool to list indexed repositories."""
|
|
359
|
+
try:
|
|
360
|
+
debug_log("Listing indexed repositories.")
|
|
361
|
+
results = self.code_finder.list_indexed_repositories()
|
|
362
|
+
return {
|
|
363
|
+
"success": True,
|
|
364
|
+
"repositories": results
|
|
365
|
+
}
|
|
366
|
+
except Exception as e:
|
|
367
|
+
debug_log(f"Error listing indexed repositories: {str(e)}")
|
|
368
|
+
return {"error": f"Failed to list indexed repositories: {str(e)}"}
|
|
369
|
+
|
|
370
|
+
def delete_repository_tool(self, **args) -> Dict[str, Any]:
|
|
371
|
+
"""Tool to delete a repository from the graph."""
|
|
372
|
+
repo_path = args.get("repo_path")
|
|
373
|
+
try:
|
|
374
|
+
debug_log(f"Deleting repository: {repo_path}")
|
|
375
|
+
self.graph_builder.delete_repository_from_graph(repo_path)
|
|
376
|
+
return {
|
|
377
|
+
"success": True,
|
|
378
|
+
"message": f"Repository '{repo_path}' deleted successfully."
|
|
379
|
+
}
|
|
380
|
+
except Exception as e:
|
|
381
|
+
debug_log(f"Error deleting repository: {str(e)}")
|
|
382
|
+
return {"error": f"Failed to delete repository: {str(e)}"}
|
|
383
|
+
|
|
279
384
|
def watch_directory_tool(self, **args) -> Dict[str, Any]:
|
|
280
385
|
"""Tool to start watching a directory."""
|
|
281
386
|
path = args.get("path")
|
|
@@ -283,17 +388,23 @@ class MCPServer:
|
|
|
283
388
|
return {"error": f"Invalid path provided: {path}. Must be a directory."}
|
|
284
389
|
|
|
285
390
|
try:
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
391
|
+
# First, ensure the code is added/scanned
|
|
392
|
+
scan_job_result = self.add_code_to_graph_tool(path=path, is_dependency=False)
|
|
393
|
+
if "error" in scan_job_result:
|
|
394
|
+
return scan_job_result
|
|
289
395
|
|
|
290
|
-
|
|
396
|
+
# Now, start the watcher
|
|
397
|
+
watch_result = self.code_watcher.watch_directory(path)
|
|
291
398
|
|
|
292
|
-
|
|
399
|
+
# Combine results
|
|
400
|
+
final_result = {
|
|
293
401
|
"success": True,
|
|
294
|
-
"message": f"Initial scan started
|
|
295
|
-
"
|
|
402
|
+
"message": f"Initial scan started for {path}. Now watching for live changes.",
|
|
403
|
+
"job_id": scan_job_result.get("job_id"),
|
|
404
|
+
"details": watch_result
|
|
296
405
|
}
|
|
406
|
+
return final_result
|
|
407
|
+
|
|
297
408
|
except Exception as e:
|
|
298
409
|
logger.error(f"Failed to start watching directory {path}: {e}")
|
|
299
410
|
return {"error": f"Failed to start watching directory: {str(e)}"}
|
|
@@ -555,7 +666,11 @@ class MCPServer:
|
|
|
555
666
|
"execute_cypher_query": self.execute_cypher_query_tool,
|
|
556
667
|
"add_code_to_graph": self.add_code_to_graph_tool,
|
|
557
668
|
"check_job_status": self.check_job_status_tool,
|
|
558
|
-
"list_jobs": self.list_jobs_tool
|
|
669
|
+
"list_jobs": self.list_jobs_tool,
|
|
670
|
+
"calculate_cyclomatic_complexity": self.calculate_cyclomatic_complexity_tool,
|
|
671
|
+
"find_most_complex_functions": self.find_most_complex_functions_tool,
|
|
672
|
+
"list_indexed_repositories": self.list_indexed_repositories_tool,
|
|
673
|
+
"delete_repository": self.delete_repository_tool
|
|
559
674
|
}
|
|
560
675
|
handler = tool_map.get(tool_name)
|
|
561
676
|
if handler:
|
|
@@ -522,6 +522,14 @@ class CodeFinder:
|
|
|
522
522
|
"summary": f"Found {len(results['potentially_unused_functions'])} potentially unused functions"
|
|
523
523
|
}
|
|
524
524
|
|
|
525
|
+
elif query_type == "find_complexity":
|
|
526
|
+
limit = int(context) if context and context.isdigit() else 10
|
|
527
|
+
results = self.find_most_complex_functions(limit)
|
|
528
|
+
return {
|
|
529
|
+
"query_type": "find_complexity", "limit": limit, "results": results,
|
|
530
|
+
"summary": f"Found the top {len(results)} most complex functions"
|
|
531
|
+
}
|
|
532
|
+
|
|
525
533
|
elif query_type in ["call_chain", "path", "chain"]:
|
|
526
534
|
if '->' in target:
|
|
527
535
|
start_func, end_func = target.split('->', 1)
|
|
@@ -556,7 +564,7 @@ class CodeFinder:
|
|
|
556
564
|
"supported_types": [
|
|
557
565
|
"find_callers", "find_callees", "find_importers", "who_modifies",
|
|
558
566
|
"class_hierarchy", "overrides", "dead_code", "call_chain",
|
|
559
|
-
"module_deps", "variable_scope"
|
|
567
|
+
"module_deps", "variable_scope", "find_complexity"
|
|
560
568
|
]
|
|
561
569
|
}
|
|
562
570
|
|
|
@@ -566,3 +574,46 @@ class CodeFinder:
|
|
|
566
574
|
"query_type": query_type,
|
|
567
575
|
"target": target
|
|
568
576
|
}
|
|
577
|
+
|
|
578
|
+
def get_cyclomatic_complexity(self, function_name: str, file_path: str = None) -> List[Dict]:
|
|
579
|
+
"""Get the cyclomatic complexity of a function."""
|
|
580
|
+
with self.driver.session() as session:
|
|
581
|
+
if file_path:
|
|
582
|
+
# Use ENDS WITH for flexible path matching
|
|
583
|
+
query = """
|
|
584
|
+
MATCH (f:Function {name: $function_name})
|
|
585
|
+
WHERE f.file_path ENDS WITH $file_path
|
|
586
|
+
RETURN f.name as function_name, f.file_path as file_path, f.cyclomatic_complexity as complexity
|
|
587
|
+
"""
|
|
588
|
+
result = session.run(query, function_name=function_name, file_path=file_path)
|
|
589
|
+
else:
|
|
590
|
+
query = """
|
|
591
|
+
MATCH (f:Function {name: $function_name})
|
|
592
|
+
RETURN f.name as function_name, f.file_path as file_path, f.cyclomatic_complexity as complexity
|
|
593
|
+
"""
|
|
594
|
+
result = session.run(query, function_name=function_name)
|
|
595
|
+
|
|
596
|
+
return [dict(record) for record in result]
|
|
597
|
+
|
|
598
|
+
def find_most_complex_functions(self, limit: int = 10) -> List[Dict]:
|
|
599
|
+
"""Find the most complex functions based on cyclomatic complexity."""
|
|
600
|
+
with self.driver.session() as session:
|
|
601
|
+
query = """
|
|
602
|
+
MATCH (f:Function)
|
|
603
|
+
WHERE f.cyclomatic_complexity IS NOT NULL
|
|
604
|
+
RETURN f.name as function_name, f.file_path as file_path, f.cyclomatic_complexity as complexity, f.line_number as line_number
|
|
605
|
+
ORDER BY f.cyclomatic_complexity DESC
|
|
606
|
+
LIMIT $limit
|
|
607
|
+
"""
|
|
608
|
+
result = session.run(query, limit=limit)
|
|
609
|
+
return [dict(record) for record in result]
|
|
610
|
+
|
|
611
|
+
def list_indexed_repositories(self) -> List[Dict]:
|
|
612
|
+
"""List all indexed repositories."""
|
|
613
|
+
with self.driver.session() as session:
|
|
614
|
+
result = session.run("""
|
|
615
|
+
MATCH (r:Repository)
|
|
616
|
+
RETURN r.name as name, r.path as path, r.is_dependency as is_dependency
|
|
617
|
+
ORDER BY r.name
|
|
618
|
+
""")
|
|
619
|
+
return [dict(record) for record in result]
|
{codegraphcontext-0.1.4 → codegraphcontext-0.1.5}/src/codegraphcontext/tools/graph_builder.py
RENAMED
|
@@ -23,6 +23,68 @@ def debug_log(message):
|
|
|
23
23
|
f.flush()
|
|
24
24
|
|
|
25
25
|
|
|
26
|
+
class CyclomaticComplexityVisitor(ast.NodeVisitor):
|
|
27
|
+
"""Calculates cyclomatic complexity for a given AST node."""
|
|
28
|
+
def __init__(self):
|
|
29
|
+
self.complexity = 1
|
|
30
|
+
|
|
31
|
+
def visit_If(self, node):
|
|
32
|
+
self.complexity += 1
|
|
33
|
+
self.generic_visit(node)
|
|
34
|
+
|
|
35
|
+
def visit_For(self, node):
|
|
36
|
+
self.complexity += 1
|
|
37
|
+
self.generic_visit(node)
|
|
38
|
+
|
|
39
|
+
def visit_While(self, node):
|
|
40
|
+
self.complexity += 1
|
|
41
|
+
self.generic_visit(node)
|
|
42
|
+
|
|
43
|
+
def visit_With(self, node):
|
|
44
|
+
self.complexity += len(node.items)
|
|
45
|
+
self.generic_visit(node)
|
|
46
|
+
|
|
47
|
+
def visit_AsyncFor(self, node):
|
|
48
|
+
self.complexity += 1
|
|
49
|
+
self.generic_visit(node)
|
|
50
|
+
|
|
51
|
+
def visit_AsyncWith(self, node):
|
|
52
|
+
self.complexity += len(node.items)
|
|
53
|
+
self.generic_visit(node)
|
|
54
|
+
|
|
55
|
+
def visit_ExceptHandler(self, node):
|
|
56
|
+
self.complexity += 1
|
|
57
|
+
self.generic_visit(node)
|
|
58
|
+
|
|
59
|
+
def visit_BoolOp(self, node):
|
|
60
|
+
self.complexity += len(node.values) - 1
|
|
61
|
+
self.generic_visit(node)
|
|
62
|
+
|
|
63
|
+
def visit_ListComp(self, node):
|
|
64
|
+
self.complexity += len(node.generators)
|
|
65
|
+
self.generic_visit(node)
|
|
66
|
+
|
|
67
|
+
def visit_SetComp(self, node):
|
|
68
|
+
self.complexity += len(node.generators)
|
|
69
|
+
self.generic_visit(node)
|
|
70
|
+
|
|
71
|
+
def visit_DictComp(self, node):
|
|
72
|
+
self.complexity += len(node.generators)
|
|
73
|
+
self.generic_visit(node)
|
|
74
|
+
|
|
75
|
+
def visit_GeneratorExp(self, node):
|
|
76
|
+
self.complexity += len(node.generators)
|
|
77
|
+
self.generic_visit(node)
|
|
78
|
+
|
|
79
|
+
def visit_IfExp(self, node):
|
|
80
|
+
self.complexity += 1
|
|
81
|
+
self.generic_visit(node)
|
|
82
|
+
|
|
83
|
+
def visit_match_case(self, node):
|
|
84
|
+
self.complexity += 1
|
|
85
|
+
self.generic_visit(node)
|
|
86
|
+
|
|
87
|
+
|
|
26
88
|
class CodeVisitor(ast.NodeVisitor):
|
|
27
89
|
"""Enhanced AST visitor to extract code elements with better function call detection"""
|
|
28
90
|
|
|
@@ -62,6 +124,9 @@ class CodeVisitor(ast.NodeVisitor):
|
|
|
62
124
|
|
|
63
125
|
def visit_FunctionDef(self, node):
|
|
64
126
|
"""Visit function definitions"""
|
|
127
|
+
complexity_visitor = CyclomaticComplexityVisitor()
|
|
128
|
+
complexity_visitor.visit(node)
|
|
129
|
+
|
|
65
130
|
func_data = {
|
|
66
131
|
"name": node.name,
|
|
67
132
|
"line_number": node.lineno,
|
|
@@ -76,6 +141,7 @@ class CodeVisitor(ast.NodeVisitor):
|
|
|
76
141
|
ast.unparse(dec) if hasattr(ast, "unparse") else ""
|
|
77
142
|
for dec in node.decorator_list
|
|
78
143
|
],
|
|
144
|
+
"cyclomatic_complexity": complexity_visitor.complexity,
|
|
79
145
|
}
|
|
80
146
|
self.functions.append(func_data)
|
|
81
147
|
self._push_context(node.name, "function", node.lineno)
|
|
@@ -236,6 +302,7 @@ class GraphBuilder:
|
|
|
236
302
|
try:
|
|
237
303
|
session.run("CREATE CONSTRAINT repository_path IF NOT EXISTS FOR (r:Repository) REQUIRE r.path IS UNIQUE")
|
|
238
304
|
session.run("CREATE CONSTRAINT file_path IF NOT EXISTS FOR (f:File) REQUIRE f.path IS UNIQUE")
|
|
305
|
+
session.run("CREATE CONSTRAINT directory_path IF NOT EXISTS FOR (d:Directory) REQUIRE d.path IS UNIQUE")
|
|
239
306
|
session.run("CREATE CONSTRAINT function_unique IF NOT EXISTS FOR (f:Function) REQUIRE (f.name, f.file_path, f.line_number) IS UNIQUE")
|
|
240
307
|
session.run("CREATE CONSTRAINT class_unique IF NOT EXISTS FOR (c:Class) REQUIRE (c.name, c.file_path, c.line_number) IS UNIQUE")
|
|
241
308
|
session.run("CREATE CONSTRAINT variable_unique IF NOT EXISTS FOR (v:Variable) REQUIRE (v.name, v.file_path, v.line_number) IS UNIQUE")
|
|
@@ -280,12 +347,42 @@ class GraphBuilder:
|
|
|
280
347
|
except ValueError:
|
|
281
348
|
relative_path = file_name
|
|
282
349
|
|
|
350
|
+
# Create/Merge the file node
|
|
283
351
|
session.run("""
|
|
284
|
-
MATCH (r:Repository {name: $repo_name})
|
|
285
352
|
MERGE (f:File {path: $path})
|
|
286
353
|
SET f.name = $name, f.relative_path = $relative_path, f.is_dependency = $is_dependency
|
|
287
|
-
|
|
288
|
-
|
|
354
|
+
""", path=file_path_str, name=file_name, relative_path=relative_path, is_dependency=is_dependency)
|
|
355
|
+
|
|
356
|
+
# Create directory structure and link it
|
|
357
|
+
file_path_obj = Path(file_path_str)
|
|
358
|
+
repo_path_obj = Path(repo_result['path'])
|
|
359
|
+
|
|
360
|
+
relative_path_to_file = file_path_obj.relative_to(repo_path_obj)
|
|
361
|
+
|
|
362
|
+
parent_path = str(repo_path_obj)
|
|
363
|
+
parent_label = 'Repository'
|
|
364
|
+
|
|
365
|
+
# Create nodes for each directory part of the path
|
|
366
|
+
for part in relative_path_to_file.parts[:-1]: # For each directory in the path
|
|
367
|
+
current_path = Path(parent_path) / part
|
|
368
|
+
current_path_str = str(current_path)
|
|
369
|
+
|
|
370
|
+
session.run(f"""
|
|
371
|
+
MATCH (p:{parent_label} {{path: $parent_path}})
|
|
372
|
+
MERGE (d:Directory {{path: $current_path}})
|
|
373
|
+
SET d.name = $part
|
|
374
|
+
MERGE (p)-[:CONTAINS]->(d)
|
|
375
|
+
""", parent_path=parent_path, current_path=current_path_str, part=part)
|
|
376
|
+
|
|
377
|
+
parent_path = current_path_str
|
|
378
|
+
parent_label = 'Directory'
|
|
379
|
+
|
|
380
|
+
# Link the last directory/repository to the file
|
|
381
|
+
session.run(f"""
|
|
382
|
+
MATCH (p:{parent_label} {{path: $parent_path}})
|
|
383
|
+
MATCH (f:File {{path: $file_path}})
|
|
384
|
+
MERGE (p)-[:CONTAINS]->(f)
|
|
385
|
+
""", parent_path=parent_path, file_path=file_path_str)
|
|
289
386
|
|
|
290
387
|
for item_data, label in [(file_data['functions'], 'Function'), (file_data['classes'], 'Class'), (file_data['variables'], 'Variable')]:
|
|
291
388
|
for item in item_data:
|
|
@@ -444,6 +541,14 @@ class GraphBuilder:
|
|
|
444
541
|
"""Deletes a file and all its contained elements and relationships."""
|
|
445
542
|
file_path_str = str(Path(file_path).resolve())
|
|
446
543
|
with self.driver.session() as session:
|
|
544
|
+
# Get parent directories
|
|
545
|
+
parents_res = session.run("""
|
|
546
|
+
MATCH (f:File {path: $path})<-[:CONTAINS*]-(d:Directory)
|
|
547
|
+
RETURN d.path as path ORDER BY length(d.path) DESC
|
|
548
|
+
""", path=file_path_str)
|
|
549
|
+
parent_paths = [record["path"] for record in parents_res]
|
|
550
|
+
|
|
551
|
+
# Delete the file and its contents
|
|
447
552
|
session.run(
|
|
448
553
|
"""
|
|
449
554
|
MATCH (f:File {path: $path})
|
|
@@ -454,6 +559,25 @@ class GraphBuilder:
|
|
|
454
559
|
)
|
|
455
560
|
logger.info(f"Deleted file and its elements from graph: {file_path_str}")
|
|
456
561
|
|
|
562
|
+
# Clean up empty parent directories, starting from the deepest
|
|
563
|
+
for path in parent_paths:
|
|
564
|
+
session.run("""
|
|
565
|
+
MATCH (d:Directory {path: $path})
|
|
566
|
+
WHERE NOT (d)-[:CONTAINS]->()
|
|
567
|
+
DETACH DELETE d
|
|
568
|
+
""", path=path)
|
|
569
|
+
|
|
570
|
+
def delete_repository_from_graph(self, repo_path: str):
|
|
571
|
+
"""Deletes a repository and all its contents from the graph."""
|
|
572
|
+
repo_path_str = str(Path(repo_path).resolve())
|
|
573
|
+
with self.driver.session() as session:
|
|
574
|
+
session.run("""
|
|
575
|
+
MATCH (r:Repository {path: $path})
|
|
576
|
+
OPTIONAL MATCH (r)-[:CONTAINS*]->(e)
|
|
577
|
+
DETACH DELETE r, e
|
|
578
|
+
""", path=repo_path_str)
|
|
579
|
+
logger.info(f"Deleted repository and its contents from graph: {repo_path_str}")
|
|
580
|
+
|
|
457
581
|
def update_file_in_graph(self, file_path: Path):
|
|
458
582
|
"""Updates a file by deleting and re-adding it."""
|
|
459
583
|
file_path_str = str(file_path.resolve())
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: codegraphcontext
|
|
3
|
-
Version: 0.1.
|
|
3
|
+
Version: 0.1.5
|
|
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
|
|
@@ -123,6 +123,12 @@ Once the server is running, you can interact with it through your AI assistant u
|
|
|
123
123
|
OR
|
|
124
124
|
- "Keep the code graph updated for the project I'm working on at `~/dev/main-app`."
|
|
125
125
|
|
|
126
|
+
When you ask to watch a directory, the system performs two actions at once:
|
|
127
|
+
1. It kicks off a full scan to index all the code in that directory. This process runs in the background, and you'll receive a `job_id` to track its progress.
|
|
128
|
+
2. It begins watching the directory for any file changes to keep the graph updated in real-time.
|
|
129
|
+
|
|
130
|
+
This means you can start by simply telling the system to watch a directory, and it will handle both the initial indexing and the continuous updates automatically.
|
|
131
|
+
|
|
126
132
|
### Querying and Understanding Code
|
|
127
133
|
|
|
128
134
|
- **Finding where code is defined:**
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{codegraphcontext-0.1.4 → codegraphcontext-0.1.5}/src/codegraphcontext/tools/import_extractor.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
{codegraphcontext-0.1.4 → codegraphcontext-0.1.5}/src/codegraphcontext.egg-info/dependency_links.txt
RENAMED
|
File without changes
|
{codegraphcontext-0.1.4 → codegraphcontext-0.1.5}/src/codegraphcontext.egg-info/entry_points.txt
RENAMED
|
File without changes
|
{codegraphcontext-0.1.4 → codegraphcontext-0.1.5}/src/codegraphcontext.egg-info/requires.txt
RENAMED
|
File without changes
|
{codegraphcontext-0.1.4 → codegraphcontext-0.1.5}/src/codegraphcontext.egg-info/top_level.txt
RENAMED
|
File without changes
|