codegraphcontext 0.1.5__tar.gz → 0.1.7__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.5/src/codegraphcontext.egg-info → codegraphcontext-0.1.7}/PKG-INFO +4 -1
- {codegraphcontext-0.1.5 → codegraphcontext-0.1.7}/README.md +3 -1
- {codegraphcontext-0.1.5 → codegraphcontext-0.1.7}/pyproject.toml +2 -1
- codegraphcontext-0.1.7/src/codegraphcontext/cli/main.py +93 -0
- {codegraphcontext-0.1.5 → codegraphcontext-0.1.7}/src/codegraphcontext/cli/setup_wizard.py +139 -16
- codegraphcontext-0.1.7/src/codegraphcontext/core/watcher.py +141 -0
- {codegraphcontext-0.1.5 → codegraphcontext-0.1.7}/src/codegraphcontext/prompts.py +1 -0
- {codegraphcontext-0.1.5 → codegraphcontext-0.1.7}/src/codegraphcontext/server.py +37 -16
- codegraphcontext-0.1.7/src/codegraphcontext/tools/graph_builder.py +1018 -0
- {codegraphcontext-0.1.5 → codegraphcontext-0.1.7}/src/codegraphcontext/tools/import_extractor.py +15 -13
- {codegraphcontext-0.1.5 → codegraphcontext-0.1.7/src/codegraphcontext.egg-info}/PKG-INFO +4 -1
- {codegraphcontext-0.1.5 → codegraphcontext-0.1.7}/src/codegraphcontext.egg-info/requires.txt +1 -0
- codegraphcontext-0.1.5/src/codegraphcontext/cli/main.py +0 -66
- codegraphcontext-0.1.5/src/codegraphcontext/core/watcher.py +0 -119
- codegraphcontext-0.1.5/src/codegraphcontext/tools/graph_builder.py +0 -732
- {codegraphcontext-0.1.5 → codegraphcontext-0.1.7}/LICENSE +0 -0
- {codegraphcontext-0.1.5 → codegraphcontext-0.1.7}/setup.cfg +0 -0
- {codegraphcontext-0.1.5 → codegraphcontext-0.1.7}/src/codegraphcontext/__init__.py +0 -0
- {codegraphcontext-0.1.5 → codegraphcontext-0.1.7}/src/codegraphcontext/__main__.py +0 -0
- {codegraphcontext-0.1.5 → codegraphcontext-0.1.7}/src/codegraphcontext/cli/__init__.py +0 -0
- {codegraphcontext-0.1.5 → codegraphcontext-0.1.7}/src/codegraphcontext/core/__init__.py +0 -0
- {codegraphcontext-0.1.5 → codegraphcontext-0.1.7}/src/codegraphcontext/core/database.py +0 -0
- {codegraphcontext-0.1.5 → codegraphcontext-0.1.7}/src/codegraphcontext/core/jobs.py +0 -0
- {codegraphcontext-0.1.5 → codegraphcontext-0.1.7}/src/codegraphcontext/tools/__init__.py +0 -0
- {codegraphcontext-0.1.5 → codegraphcontext-0.1.7}/src/codegraphcontext/tools/code_finder.py +0 -0
- {codegraphcontext-0.1.5 → codegraphcontext-0.1.7}/src/codegraphcontext/tools/system.py +0 -0
- {codegraphcontext-0.1.5 → codegraphcontext-0.1.7}/src/codegraphcontext.egg-info/SOURCES.txt +0 -0
- {codegraphcontext-0.1.5 → codegraphcontext-0.1.7}/src/codegraphcontext.egg-info/dependency_links.txt +0 -0
- {codegraphcontext-0.1.5 → codegraphcontext-0.1.7}/src/codegraphcontext.egg-info/entry_points.txt +0 -0
- {codegraphcontext-0.1.5 → codegraphcontext-0.1.7}/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.7
|
|
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
|
|
@@ -47,9 +47,12 @@ Requires-Dist: python-dotenv>=1.0.0
|
|
|
47
47
|
Provides-Extra: dev
|
|
48
48
|
Requires-Dist: pytest>=7.4.0; extra == "dev"
|
|
49
49
|
Requires-Dist: black>=23.11.0; extra == "dev"
|
|
50
|
+
Requires-Dist: pytest-asyncio>=0.21.0; extra == "dev"
|
|
50
51
|
Dynamic: license-file
|
|
51
52
|
|
|
52
53
|
# CodeGraphContext
|
|
54
|
+
[](https://github.com/Shashankss1205/CodeGraphContext/actions/workflows/test.yml)
|
|
55
|
+
|
|
53
56
|
|
|
54
57
|
An MCP server that indexes local code into a graph database to provide context to AI assistants.
|
|
55
58
|
|
|
@@ -1,4 +1,6 @@
|
|
|
1
1
|
# CodeGraphContext
|
|
2
|
+
[](https://github.com/Shashankss1205/CodeGraphContext/actions/workflows/test.yml)
|
|
3
|
+
|
|
2
4
|
|
|
3
5
|
An MCP server that indexes local code into a graph database to provide context to AI assistants.
|
|
4
6
|
|
|
@@ -96,4 +98,4 @@ Once the server is running, you can interact with it through your AI assistant u
|
|
|
96
98
|
- "Find all implementations of the `render` method."
|
|
97
99
|
|
|
98
100
|
- **Code Quality and Maintenance:**
|
|
99
|
-
- "Is there any dead or unused code in this project?"
|
|
101
|
+
- "Is there any dead or unused code in this project?"
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
[project]
|
|
2
2
|
name = "codegraphcontext"
|
|
3
|
-
version = "0.1.
|
|
3
|
+
version = "0.1.7"
|
|
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"
|
|
@@ -36,6 +36,7 @@ cgc = "codegraphcontext.cli.main:app"
|
|
|
36
36
|
dev = [
|
|
37
37
|
"pytest>=7.4.0",
|
|
38
38
|
"black>=23.11.0",
|
|
39
|
+
"pytest-asyncio>=0.21.0",
|
|
39
40
|
]
|
|
40
41
|
|
|
41
42
|
[tool.setuptools]
|
|
@@ -0,0 +1,93 @@
|
|
|
1
|
+
# src/codegraphcontext/cli/main.py
|
|
2
|
+
import typer
|
|
3
|
+
from rich.console import Console
|
|
4
|
+
import asyncio
|
|
5
|
+
import logging
|
|
6
|
+
from codegraphcontext.server import MCPServer
|
|
7
|
+
from .setup_wizard import run_setup_wizard
|
|
8
|
+
|
|
9
|
+
# Set the log level for the noisy neo4j logger to WARNING
|
|
10
|
+
logging.getLogger("neo4j").setLevel(logging.WARNING) # <-- ADD THIS LINE
|
|
11
|
+
|
|
12
|
+
app = typer.Typer(
|
|
13
|
+
name="cgc",
|
|
14
|
+
help="CodeGraphContext: An MCP server for AI-powered code analysis.",
|
|
15
|
+
add_completion=False,
|
|
16
|
+
)
|
|
17
|
+
console = Console()
|
|
18
|
+
|
|
19
|
+
logging.basicConfig(level=logging.INFO, format='%(asctime)s - %(levelname)s - %(name)s - %(message)s')
|
|
20
|
+
|
|
21
|
+
@app.command()
|
|
22
|
+
def setup():
|
|
23
|
+
"""
|
|
24
|
+
Run the interactive setup wizard to configure the server and database.
|
|
25
|
+
"""
|
|
26
|
+
run_setup_wizard()
|
|
27
|
+
|
|
28
|
+
@app.command()
|
|
29
|
+
def start():
|
|
30
|
+
"""
|
|
31
|
+
Start the CodeGraphContext MCP server.
|
|
32
|
+
"""
|
|
33
|
+
console.print("[bold green]Starting CodeGraphContext Server...[/bold green]")
|
|
34
|
+
server = None
|
|
35
|
+
loop = asyncio.new_event_loop()
|
|
36
|
+
asyncio.set_event_loop(loop)
|
|
37
|
+
try:
|
|
38
|
+
server = MCPServer(loop=loop)
|
|
39
|
+
loop.run_until_complete(server.run())
|
|
40
|
+
except ValueError as e:
|
|
41
|
+
console.print(f"[bold red]Configuration Error:[/bold red] {e}")
|
|
42
|
+
console.print("Please run `cgc setup` to configure the server.")
|
|
43
|
+
except KeyboardInterrupt:
|
|
44
|
+
console.print("\n[bold yellow]Server stopped by user.[/bold yellow]")
|
|
45
|
+
finally:
|
|
46
|
+
if server:
|
|
47
|
+
server.shutdown()
|
|
48
|
+
loop.close()
|
|
49
|
+
|
|
50
|
+
|
|
51
|
+
@app.command()
|
|
52
|
+
def tool(
|
|
53
|
+
name: str = typer.Argument(..., help="The name of the tool to call."),
|
|
54
|
+
args: str = typer.Argument("{}", help="A JSON string of arguments for the tool."),
|
|
55
|
+
):
|
|
56
|
+
"""
|
|
57
|
+
Directly call a CodeGraphContext tool.
|
|
58
|
+
Note: This command instantiates a new, independent MCP server instance for each call.
|
|
59
|
+
Therefore, it does not share state (like job IDs) with a server started via `cgc start`.
|
|
60
|
+
|
|
61
|
+
This command can be used for:\n
|
|
62
|
+
- `add_code_to_graph`: Index a new project or directory. Args: `path` (str), `is_dependency` (bool, optional)\n
|
|
63
|
+
- `add_package_to_graph`: Add a Python package to the graph. Args: `package_name` (str), `is_dependency` (bool, optional)\n
|
|
64
|
+
- `find_code`: Search for code snippets. Args: `query` (str)\n
|
|
65
|
+
- `analyze_code_relationships`: Analyze code relationships (e.g., callers, callees). Args: `query_type` (str), `target` (str), `context` (str, optional)\n
|
|
66
|
+
- `watch_directory`: Start watching a directory for changes. Args: `path` (str)\n
|
|
67
|
+
- `execute_cypher_query`: Run direct Cypher queries. Args: `cypher_query` (str)\n
|
|
68
|
+
- `list_imports`: List imports from files. Args: `path` (str), `language` (str, optional), `recursive` (bool, optional)\n
|
|
69
|
+
- `find_dead_code`: Find potentially unused functions. Args: None\n
|
|
70
|
+
- `calculate_cyclomatic_complexity`: Calculate function complexity. Args: `function_name` (str), `file_path` (str, optional)\n
|
|
71
|
+
- `find_most_complex_functions`: Find the most complex functions. Args: `limit` (int, optional)\n
|
|
72
|
+
- `list_indexed_repositories`: List indexed repositories. Args: None\n
|
|
73
|
+
- `delete_repository`: Delete an indexed repository. Args: `repo_path` (str)
|
|
74
|
+
"""
|
|
75
|
+
|
|
76
|
+
# This is a placeholder for a more advanced tool caller that would
|
|
77
|
+
# connect to the running server via a different mechanism (e.g., a socket).
|
|
78
|
+
# For now, it's a conceptual part of the CLI.
|
|
79
|
+
console.print(f"Calling tool [bold cyan]{name}[/bold cyan] with args: {args}")
|
|
80
|
+
console.print("[yellow]Note: This is a placeholder for direct tool invocation.[/yellow]")
|
|
81
|
+
|
|
82
|
+
|
|
83
|
+
@app.command(name="help")
|
|
84
|
+
def show_help():
|
|
85
|
+
"""
|
|
86
|
+
Show a list of available commands and their descriptions.
|
|
87
|
+
"""
|
|
88
|
+
console.print("[bold]CodeGraphContext CLI Commands:[/bold]")
|
|
89
|
+
for command in app.registered_commands:
|
|
90
|
+
# Get the first line of the docstring as a short description
|
|
91
|
+
description = command.help.split('\n')[0].strip() if command.help else "No description."
|
|
92
|
+
console.print(f" [bold cyan]{command.name}[/bold cyan]: {description}")
|
|
93
|
+
console.print("\nFor more information on a specific command, run: [bold]cgc <command> --help[/bold]")
|
|
@@ -38,7 +38,9 @@ def _generate_mcp_json(creds):
|
|
|
38
38
|
"list_imports", "add_code_to_graph", "add_package_to_graph",
|
|
39
39
|
"check_job_status", "list_jobs", "find_code",
|
|
40
40
|
"analyze_code_relationships", "watch_directory",
|
|
41
|
-
"find_dead_code", "execute_cypher_query"
|
|
41
|
+
"find_dead_code", "execute_cypher_query",
|
|
42
|
+
"calculate_cyclomatic_complexity", "find_most_complex_functions",
|
|
43
|
+
"list_indexed_repositories", "delete_repository"
|
|
42
44
|
],
|
|
43
45
|
"disabled": False
|
|
44
46
|
},
|
|
@@ -99,7 +101,7 @@ def run_setup_wizard():
|
|
|
99
101
|
questions = [
|
|
100
102
|
{
|
|
101
103
|
"type": "list",
|
|
102
|
-
"message": "Where is your Neo4j database located?",
|
|
104
|
+
"message": "Where is your Neo4j database located? We can help you get one, if you don't have.",
|
|
103
105
|
"choices": [
|
|
104
106
|
"Local (Recommended: I'll help you run it on this machine)",
|
|
105
107
|
"Hosted (Connect to a remote database like AuraDB)",
|
|
@@ -227,20 +229,141 @@ def setup_local_db():
|
|
|
227
229
|
setup_local_binary()
|
|
228
230
|
|
|
229
231
|
def setup_docker():
|
|
230
|
-
"""Creates Docker files and runs docker-compose."""
|
|
231
|
-
console.print("
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
console.print("Please set
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
232
|
+
"""Creates Docker files and runs docker-compose for Neo4j."""
|
|
233
|
+
console.print("\n[bold cyan]Setting up Neo4j with Docker...[/bold cyan]")
|
|
234
|
+
|
|
235
|
+
# Prompt for password first
|
|
236
|
+
console.print("Please set a secure password for your Neo4j database:")
|
|
237
|
+
password_questions = [
|
|
238
|
+
{"type": "password", "message": "Enter Neo4j password:", "name": "password"},
|
|
239
|
+
{"type": "password", "message": "Confirm password:", "name": "password_confirm"},
|
|
240
|
+
]
|
|
241
|
+
|
|
242
|
+
while True:
|
|
243
|
+
passwords = prompt(password_questions)
|
|
244
|
+
if not passwords:
|
|
245
|
+
return # User cancelled
|
|
246
|
+
|
|
247
|
+
password = passwords.get("password", "")
|
|
248
|
+
if password and password == passwords.get("password_confirm"):
|
|
249
|
+
break
|
|
250
|
+
console.print("[red]Passwords do not match or are empty. Please try again.[/red]")
|
|
251
|
+
|
|
252
|
+
# Create data directories
|
|
253
|
+
neo4j_dir = Path.cwd() / "neo4j_data"
|
|
254
|
+
for subdir in ["data", "logs", "conf", "plugins"]:
|
|
255
|
+
(neo4j_dir / subdir).mkdir(parents=True, exist_ok=True)
|
|
256
|
+
|
|
257
|
+
# Fixed docker-compose.yml content
|
|
258
|
+
docker_compose_content = f"""
|
|
259
|
+
services:
|
|
260
|
+
neo4j:
|
|
261
|
+
image: neo4j:5.21
|
|
262
|
+
container_name: neo4j-cgc
|
|
263
|
+
restart: unless-stopped
|
|
264
|
+
ports:
|
|
265
|
+
- "7474:7474"
|
|
266
|
+
- "7687:7687"
|
|
267
|
+
environment:
|
|
268
|
+
- NEO4J_AUTH=neo4j/12345678
|
|
269
|
+
- NEO4J_ACCEPT_LICENSE_AGREEMENT=yes
|
|
270
|
+
volumes:
|
|
271
|
+
- neo4j_data:/data
|
|
272
|
+
- neo4j_logs:/logs
|
|
273
|
+
|
|
274
|
+
volumes:
|
|
275
|
+
neo4j_data:
|
|
276
|
+
neo4j_logs:
|
|
277
|
+
"""
|
|
278
|
+
|
|
279
|
+
# Write docker-compose.yml
|
|
280
|
+
compose_file = Path.cwd() / "docker-compose.yml"
|
|
281
|
+
with open(compose_file, "w") as f:
|
|
282
|
+
f.write(docker_compose_content)
|
|
283
|
+
|
|
284
|
+
console.print("[green]✅ docker-compose.yml created with secure password.[/green]")
|
|
285
|
+
|
|
286
|
+
# Check if Docker is running
|
|
287
|
+
docker_check = run_command(["docker", "--version"], console, check=False)
|
|
288
|
+
if not docker_check:
|
|
289
|
+
console.print("[red]❌ Docker is not installed or not running. Please install Docker first.[/red]")
|
|
290
|
+
return
|
|
291
|
+
|
|
292
|
+
# Check if docker-compose is available
|
|
293
|
+
compose_check = run_command(["docker", "compose", "version"], console, check=False)
|
|
294
|
+
if not compose_check:
|
|
295
|
+
console.print("[red]❌ Docker Compose is not available. Please install Docker Compose.[/red]")
|
|
296
|
+
return
|
|
297
|
+
|
|
298
|
+
confirm_q = [{"type": "confirm", "message": "Ready to launch Neo4j in Docker?", "name": "proceed", "default": True}]
|
|
299
|
+
if not prompt(confirm_q).get("proceed"):
|
|
300
|
+
return
|
|
301
|
+
|
|
302
|
+
try:
|
|
303
|
+
# Pull the image first
|
|
304
|
+
console.print("[cyan]Pulling Neo4j Docker image...[/cyan]")
|
|
305
|
+
pull_process = run_command(["docker", "pull", "neo4j:5.21"], console, check=True)
|
|
306
|
+
if not pull_process:
|
|
307
|
+
console.print("[yellow]⚠️ Could not pull image, but continuing anyway...[/yellow]")
|
|
308
|
+
|
|
309
|
+
# Start containers
|
|
310
|
+
console.print("[cyan]Starting Neo4j container...[/cyan]")
|
|
311
|
+
docker_process = run_command(["docker", "compose", "up", "-d"], console, check=True)
|
|
312
|
+
|
|
313
|
+
if docker_process:
|
|
314
|
+
console.print("[bold green]🚀 Neo4j Docker container started successfully![/bold green]")
|
|
315
|
+
|
|
316
|
+
# Wait for Neo4j to be ready
|
|
317
|
+
console.print("[cyan]Waiting for Neo4j to be ready (this may take 30-60 seconds)...[/cyan]")
|
|
318
|
+
|
|
319
|
+
# Try to connect for up to 2 minutes
|
|
320
|
+
max_attempts = 24 # 24 * 5 seconds = 2 minutes
|
|
321
|
+
for attempt in range(max_attempts):
|
|
322
|
+
time.sleep(5)
|
|
323
|
+
|
|
324
|
+
# Check if container is still running
|
|
325
|
+
status_check = run_command(["docker", "compose", "ps", "-q", "neo4j"], console, check=False)
|
|
326
|
+
if not status_check or not status_check.stdout.strip():
|
|
327
|
+
console.print("[red]❌ Neo4j container stopped unexpectedly. Check logs with: docker compose logs neo4j[/red]")
|
|
328
|
+
return
|
|
329
|
+
|
|
330
|
+
# Try to connect
|
|
331
|
+
health_check = run_command([
|
|
332
|
+
"docker", "exec", "neo4j-cgc", "cypher-shell",
|
|
333
|
+
"-u", "neo4j", "-p", password,
|
|
334
|
+
"RETURN 'Connection successful' as status"
|
|
335
|
+
], console, check=False)
|
|
336
|
+
|
|
337
|
+
if health_check and health_check.returncode == 0:
|
|
338
|
+
console.print("[bold green]✅ Neo4j is ready and accepting connections![/bold green]")
|
|
339
|
+
break
|
|
340
|
+
|
|
341
|
+
if attempt < max_attempts - 1:
|
|
342
|
+
console.print(f"[yellow]Still waiting... (attempt {attempt + 1}/{max_attempts})[/yellow]")
|
|
343
|
+
else:
|
|
344
|
+
console.print("[red]❌ Neo4j did not become ready within 2 minutes. Check logs with: docker compose logs neo4j[/red]")
|
|
345
|
+
return
|
|
346
|
+
|
|
347
|
+
# Generate MCP configuration
|
|
348
|
+
creds = {
|
|
349
|
+
"uri": "neo4j://localhost:7687", # Use neo4j:// protocol for Neo4j 5.x
|
|
350
|
+
"username": "neo4j",
|
|
351
|
+
"password": password
|
|
352
|
+
}
|
|
353
|
+
_generate_mcp_json(creds)
|
|
354
|
+
|
|
355
|
+
console.print("\n[bold green]🎉 Setup complete![/bold green]")
|
|
356
|
+
console.print("Neo4j is running at:")
|
|
357
|
+
console.print(" • Web interface: http://localhost:7474")
|
|
358
|
+
console.print(" • Bolt connection: neo4j://localhost:7687")
|
|
359
|
+
console.print("\n[cyan]Useful commands:[/cyan]")
|
|
360
|
+
console.print(" • Stop: docker compose down")
|
|
361
|
+
console.print(" • Restart: docker compose restart")
|
|
362
|
+
console.print(" • View logs: docker compose logs neo4j")
|
|
363
|
+
|
|
364
|
+
except Exception as e:
|
|
365
|
+
console.print(f"[bold red]❌ Failed to start Neo4j Docker container:[/bold red] {e}")
|
|
366
|
+
console.print("[cyan]Try checking the logs with: docker compose logs neo4j[/cyan]")
|
|
244
367
|
|
|
245
368
|
def setup_local_binary():
|
|
246
369
|
"""Automates the installation and configuration of Neo4j on Ubuntu/Debian."""
|
|
@@ -0,0 +1,141 @@
|
|
|
1
|
+
# src/codegraphcontext/core/watcher.py
|
|
2
|
+
import logging
|
|
3
|
+
import threading
|
|
4
|
+
from pathlib import Path
|
|
5
|
+
import typing
|
|
6
|
+
from watchdog.observers import Observer
|
|
7
|
+
from watchdog.events import FileSystemEventHandler
|
|
8
|
+
|
|
9
|
+
if typing.TYPE_CHECKING:
|
|
10
|
+
from codegraphcontext.tools.graph_builder import GraphBuilder
|
|
11
|
+
from codegraphcontext.core.jobs import JobManager
|
|
12
|
+
|
|
13
|
+
logger = logging.getLogger(__name__)
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
# --- NEW, MORE POWERFUL EVENT HANDLER ---
|
|
17
|
+
class RepositoryEventHandler(FileSystemEventHandler):
|
|
18
|
+
"""
|
|
19
|
+
An event handler that manages the state for a single repository.
|
|
20
|
+
It performs an initial scan and uses cached data for efficient updates.
|
|
21
|
+
"""
|
|
22
|
+
def __init__(self, graph_builder: "GraphBuilder", repo_path: Path, debounce_interval=2.0):
|
|
23
|
+
super().__init__()
|
|
24
|
+
self.graph_builder = graph_builder
|
|
25
|
+
self.repo_path = repo_path
|
|
26
|
+
self.debounce_interval = debounce_interval
|
|
27
|
+
self.timers = {}
|
|
28
|
+
|
|
29
|
+
# --- STATE CACHING ---
|
|
30
|
+
self.all_file_data = []
|
|
31
|
+
self.imports_map = {}
|
|
32
|
+
|
|
33
|
+
# Perform the initial scan and linking when created
|
|
34
|
+
self._initial_scan()
|
|
35
|
+
|
|
36
|
+
def _initial_scan(self):
|
|
37
|
+
"""Scans the entire repository and builds the initial graph."""
|
|
38
|
+
logger.info(f"Performing initial scan for watcher: {self.repo_path}")
|
|
39
|
+
all_files = list(self.repo_path.rglob("*.py"))
|
|
40
|
+
|
|
41
|
+
# 1. Pre-scan for the global import map
|
|
42
|
+
self.imports_map = self.graph_builder._pre_scan_for_imports(all_files)
|
|
43
|
+
|
|
44
|
+
# 2. Parse all files and cache their data
|
|
45
|
+
for f in all_files:
|
|
46
|
+
parsed_data = self.graph_builder.parse_python_file(self.repo_path, f, self.imports_map)
|
|
47
|
+
if "error" not in parsed_data:
|
|
48
|
+
self.all_file_data.append(parsed_data)
|
|
49
|
+
|
|
50
|
+
# 3. Perform the initial linking of the entire graph
|
|
51
|
+
self.graph_builder._create_all_function_calls(self.all_file_data, self.imports_map)
|
|
52
|
+
logger.info(f"Initial scan and graph linking complete for: {self.repo_path}")
|
|
53
|
+
|
|
54
|
+
def _debounce(self, event_path, action):
|
|
55
|
+
"""Schedules an action after a debounce interval to avoid rapid firing."""
|
|
56
|
+
if event_path in self.timers:
|
|
57
|
+
self.timers[event_path].cancel()
|
|
58
|
+
timer = threading.Timer(self.debounce_interval, action)
|
|
59
|
+
timer.start()
|
|
60
|
+
self.timers[event_path] = timer
|
|
61
|
+
|
|
62
|
+
def _handle_modification(self, event_path_str):
|
|
63
|
+
"""Orchestrates the complete and correct update cycle for a modified file."""
|
|
64
|
+
logger.info(f"File change detected, starting full update for: {event_path_str}")
|
|
65
|
+
modified_path = Path(event_path_str)
|
|
66
|
+
|
|
67
|
+
# 1. Use the helper to update the file's nodes and get new data
|
|
68
|
+
# NOTE: You will need to modify `update_file_in_graph` to accept `repo_path`
|
|
69
|
+
# and `imports_map` as arguments and to return the new parsed data.
|
|
70
|
+
new_file_data = self.graph_builder.update_file_in_graph(
|
|
71
|
+
modified_path, self.repo_path, self.imports_map
|
|
72
|
+
)
|
|
73
|
+
|
|
74
|
+
if not new_file_data:
|
|
75
|
+
logger.error(f"Update failed for {event_path_str}, skipping re-link.")
|
|
76
|
+
return
|
|
77
|
+
|
|
78
|
+
# 2. Update the cache
|
|
79
|
+
self.all_file_data = [d for d in self.all_file_data if d.get("file_path") != event_path_str]
|
|
80
|
+
if not new_file_data.get("deleted"):
|
|
81
|
+
self.all_file_data.append(new_file_data)
|
|
82
|
+
|
|
83
|
+
# 3. CRITICAL: Re-link the entire graph using the updated cache
|
|
84
|
+
logger.info("Re-linking the call graph with updated information...")
|
|
85
|
+
self.graph_builder._create_all_function_calls(self.all_file_data, self.imports_map)
|
|
86
|
+
logger.info(f"Graph update for {event_path_str} complete! ✅")
|
|
87
|
+
|
|
88
|
+
def on_created(self, event):
|
|
89
|
+
if not event.is_directory and event.src_path.endswith('.py'):
|
|
90
|
+
self._debounce(event.src_path, lambda: self._handle_modification(event.src_path))
|
|
91
|
+
|
|
92
|
+
def on_modified(self, event):
|
|
93
|
+
if not event.is_directory and event.src_path.endswith('.py'):
|
|
94
|
+
self._debounce(event.src_path, lambda: self._handle_modification(event.src_path))
|
|
95
|
+
|
|
96
|
+
def on_deleted(self, event):
|
|
97
|
+
if not event.is_directory and event.src_path.endswith('.py'):
|
|
98
|
+
# Deletion is handled inside _handle_modification
|
|
99
|
+
self._debounce(event.src_path, lambda: self._handle_modification(event.src_path))
|
|
100
|
+
|
|
101
|
+
def on_moved(self, event):
|
|
102
|
+
if not event.is_directory and event.src_path.endswith('.py'):
|
|
103
|
+
# A move is a deletion from the old path and a creation at the new path
|
|
104
|
+
self._debounce(event.src_path, lambda: self._handle_modification(event.src_path))
|
|
105
|
+
self._debounce(event.dest_path, lambda: self._handle_modification(event.dest_path))
|
|
106
|
+
|
|
107
|
+
|
|
108
|
+
class CodeWatcher:
|
|
109
|
+
"""Manages file system watching in a background thread."""
|
|
110
|
+
def __init__(self, graph_builder: "GraphBuilder", job_manager= "JobManager"):
|
|
111
|
+
self.graph_builder = graph_builder
|
|
112
|
+
self.observer = Observer()
|
|
113
|
+
self.watched_paths = set()
|
|
114
|
+
|
|
115
|
+
def watch_directory(self, path: str):
|
|
116
|
+
path_obj = Path(path).resolve()
|
|
117
|
+
path_str = str(path_obj)
|
|
118
|
+
|
|
119
|
+
if path_str in self.watched_paths:
|
|
120
|
+
logger.info(f"Path already being watched: {path_str}")
|
|
121
|
+
return {"message": f"Path already being watched: {path_str}"}
|
|
122
|
+
|
|
123
|
+
# --- Create a NEW, DEDICATED handler for this path ---
|
|
124
|
+
event_handler = RepositoryEventHandler(self.graph_builder, path_obj)
|
|
125
|
+
|
|
126
|
+
self.observer.schedule(event_handler, path_str, recursive=True)
|
|
127
|
+
self.watched_paths.add(path_str)
|
|
128
|
+
logger.info(f"Started watching for code changes in: {path_str}")
|
|
129
|
+
|
|
130
|
+
return {"message": f"Started watching {path_str}. Initial scan is in progress."}
|
|
131
|
+
|
|
132
|
+
def start(self):
|
|
133
|
+
if not self.observer.is_alive():
|
|
134
|
+
self.observer.start()
|
|
135
|
+
logger.info("Code watcher observer thread started.")
|
|
136
|
+
|
|
137
|
+
def stop(self):
|
|
138
|
+
if self.observer.is_alive():
|
|
139
|
+
self.observer.stop()
|
|
140
|
+
self.observer.join()
|
|
141
|
+
logger.info("Code watcher observer thread stopped.")
|
|
@@ -5,6 +5,7 @@ LLM_SYSTEM_PROMPT = """# AI Pair Programmer Instructions
|
|
|
5
5
|
## 1. Your Role and Goal
|
|
6
6
|
|
|
7
7
|
You are an expert AI pair programmer. Your primary goal is to help a developer understand, write, and refactor code within their **local project**. Your defining feature is your connection to a local Model Context Protocol (MCP) server, which gives you real-time, accurate information about the codebase.
|
|
8
|
+
**Always prioritize using this MCP tools when they can simplify or enhance your workflow compared to guessing.**
|
|
8
9
|
|
|
9
10
|
## 2. Your Core Principles
|
|
10
11
|
|
|
@@ -11,7 +11,6 @@ from datetime import datetime
|
|
|
11
11
|
from pathlib import Path
|
|
12
12
|
from neo4j.exceptions import CypherSyntaxError
|
|
13
13
|
from dataclasses import asdict
|
|
14
|
-
from codegraphcontext.core.database import DatabaseManager
|
|
15
14
|
|
|
16
15
|
from typing import Any, Dict, Coroutine, Optional
|
|
17
16
|
|
|
@@ -126,7 +125,7 @@ class MCPServer:
|
|
|
126
125
|
},
|
|
127
126
|
"execute_cypher_query": {
|
|
128
127
|
"name": "execute_cypher_query",
|
|
129
|
-
"description": "Fallback tool to run a direct, read-only Cypher query against the code graph.",
|
|
128
|
+
"description": "Fallback tool to run a direct, read-only Cypher query against the code graph. Use this for complex questions not covered by other tools. The graph contains nodes representing code structures and relationships between them. **Schema Overview:**\n- **Nodes:** `Repository`, `File`, `Module`, `Class`, `Function`.\n- **Properties:** Nodes have properties like `name`, `path`, `cyclomatic_complexity` (on Function nodes), and `code`.\n- **Relationships:** `CONTAINS` (e.g., File-[:CONTAINS]->Function), `CALLS` (Function-[:CALLS]->Function or File-[:CALLS]->Function), `IMPORTS` (File-[:IMPORTS]->Module), `INHERITS` (Class-[:INHERITS]->Class).",
|
|
130
129
|
"inputSchema": {
|
|
131
130
|
"type": "object",
|
|
132
131
|
"properties": { "cypher_query": {"type": "string", "description": "The read-only Cypher query to execute."} },
|
|
@@ -209,9 +208,7 @@ class MCPServer:
|
|
|
209
208
|
}
|
|
210
209
|
}
|
|
211
210
|
# Other tools like list_imports, add_package_to_graph can be added here following the same pattern
|
|
212
|
-
}
|
|
213
|
-
|
|
214
|
-
|
|
211
|
+
}
|
|
215
212
|
|
|
216
213
|
def get_database_status(self) -> dict:
|
|
217
214
|
"""Get current database connection status"""
|
|
@@ -225,24 +222,30 @@ class MCPServer:
|
|
|
225
222
|
module = importlib.import_module(package_name)
|
|
226
223
|
|
|
227
224
|
if hasattr(module, '__file__') and module.__file__:
|
|
228
|
-
module_file = module.__file__
|
|
225
|
+
module_file = Path(module.__file__)
|
|
229
226
|
debug_log(f"Module file: {module_file}")
|
|
230
|
-
|
|
231
|
-
if module_file.
|
|
232
|
-
|
|
227
|
+
|
|
228
|
+
if module_file.name == '__init__.py':
|
|
229
|
+
# It's a package (directory)
|
|
230
|
+
package_path = str(module_file.parent)
|
|
231
|
+
elif package_name in stdlibs.module_names:
|
|
232
|
+
# It's a single-file standard library module (e.g., os.py, io.py)
|
|
233
|
+
package_path = str(module_file)
|
|
233
234
|
else:
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
235
|
+
# Default to parent directory for other single-file modules
|
|
236
|
+
package_path = str(module_file.parent)
|
|
237
|
+
|
|
238
|
+
debug_log(f"Determined package path: {package_path}")
|
|
237
239
|
return package_path
|
|
238
|
-
|
|
240
|
+
|
|
239
241
|
elif hasattr(module, '__path__'):
|
|
242
|
+
# This handles namespace packages or packages without __init__.py
|
|
240
243
|
if isinstance(module.__path__, list) and module.__path__:
|
|
241
|
-
package_path = module.__path__[0]
|
|
244
|
+
package_path = str(Path(module.__path__[0]))
|
|
242
245
|
debug_log(f"Package path from __path__: {package_path}")
|
|
243
246
|
return package_path
|
|
244
247
|
else:
|
|
245
|
-
package_path = str(module.__path__)
|
|
248
|
+
package_path = str(Path(str(module.__path__)))
|
|
246
249
|
debug_log(f"Package path from __path__ (str): {package_path}")
|
|
247
250
|
return package_path
|
|
248
251
|
|
|
@@ -473,6 +476,15 @@ class MCPServer:
|
|
|
473
476
|
|
|
474
477
|
if not path_obj.exists():
|
|
475
478
|
return {"error": f"Path {path} does not exist"}
|
|
479
|
+
|
|
480
|
+
# Check if the repository is already indexed
|
|
481
|
+
indexed_repos = self.list_indexed_repositories_tool().get("repositories", [])
|
|
482
|
+
for repo in indexed_repos:
|
|
483
|
+
if Path(repo["path"]).resolve() == path_obj:
|
|
484
|
+
return {
|
|
485
|
+
"success": False,
|
|
486
|
+
"message": f"Repository '{path}' is already indexed."
|
|
487
|
+
}
|
|
476
488
|
|
|
477
489
|
total_files, estimated_time = self.graph_builder.estimate_processing_time(path_obj)
|
|
478
490
|
|
|
@@ -506,6 +518,15 @@ class MCPServer:
|
|
|
506
518
|
is_dependency = args.get("is_dependency", True)
|
|
507
519
|
|
|
508
520
|
try:
|
|
521
|
+
# Check if the package is already indexed
|
|
522
|
+
indexed_repos = self.list_indexed_repositories_tool().get("repositories", [])
|
|
523
|
+
for repo in indexed_repos:
|
|
524
|
+
if repo.get("is_dependency") and (repo.get("name") == package_name or repo.get("name") == f"{package_name}.py"):
|
|
525
|
+
return {
|
|
526
|
+
"success": False,
|
|
527
|
+
"message": f"Package '{package_name}' is already indexed."
|
|
528
|
+
}
|
|
529
|
+
|
|
509
530
|
package_path = self.get_local_package_path(package_name)
|
|
510
531
|
|
|
511
532
|
if not package_path:
|
|
@@ -742,4 +763,4 @@ class MCPServer:
|
|
|
742
763
|
def shutdown(self):
|
|
743
764
|
logger.info("Shutting down server...")
|
|
744
765
|
self.code_watcher.stop()
|
|
745
|
-
self.db_manager.close_driver()
|
|
766
|
+
self.db_manager.close_driver()
|