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,132 @@
|
|
|
1
|
+
import sys
|
|
2
|
+
import os
|
|
3
|
+
import time
|
|
4
|
+
import signal
|
|
5
|
+
from pathlib import Path
|
|
6
|
+
import logging
|
|
7
|
+
|
|
8
|
+
# Configure logging
|
|
9
|
+
logging.basicConfig(level=logging.INFO, format='%(asctime)s - %(name)s - %(levelname)s - %(message)s')
|
|
10
|
+
logger = logging.getLogger("falkor_worker")
|
|
11
|
+
|
|
12
|
+
# Global to handle shutdown
|
|
13
|
+
db_instance = None
|
|
14
|
+
|
|
15
|
+
def handle_signal(signum, frame):
|
|
16
|
+
logger.info(f"Received signal {signum}. Stopping FalkorDB worker...")
|
|
17
|
+
sys.exit(0)
|
|
18
|
+
|
|
19
|
+
def run_worker():
|
|
20
|
+
global db_instance
|
|
21
|
+
|
|
22
|
+
# Get configuration from env
|
|
23
|
+
db_path = os.getenv('FALKORDB_PATH')
|
|
24
|
+
socket_path = os.getenv('FALKORDB_SOCKET_PATH')
|
|
25
|
+
|
|
26
|
+
if not db_path or not socket_path:
|
|
27
|
+
logger.error("Missing configuration. FALKORDB_PATH and FALKORDB_SOCKET_PATH must be set.")
|
|
28
|
+
sys.exit(1)
|
|
29
|
+
|
|
30
|
+
# Ensure dir exists
|
|
31
|
+
Path(db_path).parent.mkdir(parents=True, exist_ok=True)
|
|
32
|
+
|
|
33
|
+
logger.info(f"Starting FalkorDB Lite worker...")
|
|
34
|
+
logger.info(f"DB Path: {db_path}")
|
|
35
|
+
logger.info(f"Socket: {socket_path}")
|
|
36
|
+
|
|
37
|
+
try:
|
|
38
|
+
import platform
|
|
39
|
+
|
|
40
|
+
if platform.system() == "Windows":
|
|
41
|
+
raise RuntimeError(
|
|
42
|
+
"CodeGraphContext uses redislite/FalkorDB, which does not support Windows.\n"
|
|
43
|
+
"Please run the project using WSL or Docker."
|
|
44
|
+
)
|
|
45
|
+
|
|
46
|
+
from redislite.falkordb_client import FalkorDB
|
|
47
|
+
|
|
48
|
+
# Determine module path for frozen bundles
|
|
49
|
+
server_config = {}
|
|
50
|
+
if getattr(sys, 'frozen', False):
|
|
51
|
+
mei_pass = getattr(sys, '_MEIPASS', os.path.dirname(sys.executable))
|
|
52
|
+
exe_dir = os.path.dirname(sys.executable)
|
|
53
|
+
|
|
54
|
+
# All known locations PyInstaller may extract falkordb.so to
|
|
55
|
+
potential_paths = [
|
|
56
|
+
# Standard redislite layout
|
|
57
|
+
os.path.join(mei_pass, 'redislite', 'bin', 'falkordb.so'),
|
|
58
|
+
# falkordblite scripts layout
|
|
59
|
+
os.path.join(mei_pass, 'falkordblite.scripts', 'falkordb.so'),
|
|
60
|
+
# Root of the bundle (add_binary with '.' target)
|
|
61
|
+
os.path.join(mei_pass, 'falkordb.so'),
|
|
62
|
+
# Alongside the executable itself
|
|
63
|
+
os.path.join(exe_dir, 'falkordb.so'),
|
|
64
|
+
# redislite data dir variant
|
|
65
|
+
os.path.join(mei_pass, 'redislite', 'falkordb.so'),
|
|
66
|
+
# falkordblite.libs (shared-lib bundle)
|
|
67
|
+
os.path.join(mei_pass, 'falkordblite.libs', 'falkordb.so'),
|
|
68
|
+
]
|
|
69
|
+
|
|
70
|
+
module_path = None
|
|
71
|
+
for p in potential_paths:
|
|
72
|
+
if os.path.exists(p):
|
|
73
|
+
module_path = p
|
|
74
|
+
break
|
|
75
|
+
|
|
76
|
+
if module_path:
|
|
77
|
+
logger.info(f"Using FalkorDB module from bundle: {module_path}")
|
|
78
|
+
server_config['loadmodule'] = module_path
|
|
79
|
+
else:
|
|
80
|
+
logger.error(
|
|
81
|
+
"Could not find falkordb.so in the PyInstaller bundle. "
|
|
82
|
+
"Searched: " + str(potential_paths)
|
|
83
|
+
)
|
|
84
|
+
# Exit with a distinct code so the parent can detect FalkorDB is
|
|
85
|
+
# unavailable in this environment and fall back to KùzuDB.
|
|
86
|
+
sys.exit(2)
|
|
87
|
+
|
|
88
|
+
# Start Embedded DB
|
|
89
|
+
if os.path.exists(socket_path):
|
|
90
|
+
try:
|
|
91
|
+
os.remove(socket_path)
|
|
92
|
+
except OSError:
|
|
93
|
+
pass
|
|
94
|
+
|
|
95
|
+
db_instance = FalkorDB(db_path, unix_socket_path=socket_path, serverconfig=server_config)
|
|
96
|
+
logger.info("FalkorDB Lite is running.")
|
|
97
|
+
|
|
98
|
+
# Validate that FalkorDB module actually loaded by running a GRAPH.QUERY
|
|
99
|
+
try:
|
|
100
|
+
test_graph = db_instance.select_graph('__cgc_health_check')
|
|
101
|
+
test_graph.query('RETURN 1')
|
|
102
|
+
logger.info("FalkorDB GRAPH.QUERY OK.")
|
|
103
|
+
except Exception as health_err:
|
|
104
|
+
err_str = str(health_err).lower()
|
|
105
|
+
if 'graph.query' in err_str or 'unknown command' in err_str:
|
|
106
|
+
logger.error(
|
|
107
|
+
f"FalkorDB module not loaded — GRAPH.QUERY unavailable: {health_err}. "
|
|
108
|
+
"The Redis server started but the FalkorDB .so module was not loaded."
|
|
109
|
+
)
|
|
110
|
+
sys.exit(2) # same exit code: parent will fall back to KùzuDB
|
|
111
|
+
else:
|
|
112
|
+
logger.warning(f"FalkorDB health-check warning (non-fatal): {health_err}")
|
|
113
|
+
|
|
114
|
+
# Keep alive loop
|
|
115
|
+
while True:
|
|
116
|
+
time.sleep(1)
|
|
117
|
+
|
|
118
|
+
except ImportError as e:
|
|
119
|
+
logger.error(f"Failed to import redislite.falkordb_client: {e}")
|
|
120
|
+
import traceback
|
|
121
|
+
traceback.print_exc()
|
|
122
|
+
sys.exit(1)
|
|
123
|
+
except Exception as e:
|
|
124
|
+
logger.error(f"FalkorDB Worker Critical Failure: {e}")
|
|
125
|
+
import traceback
|
|
126
|
+
traceback.print_exc()
|
|
127
|
+
sys.exit(1)
|
|
128
|
+
|
|
129
|
+
if __name__ == '__main__':
|
|
130
|
+
signal.signal(signal.SIGTERM, handle_signal)
|
|
131
|
+
signal.signal(signal.SIGINT, handle_signal)
|
|
132
|
+
run_worker()
|
|
@@ -0,0 +1,132 @@
|
|
|
1
|
+
# src/codegraphcontext/core/jobs.py
|
|
2
|
+
"""
|
|
3
|
+
This module defines the data structures and manager for handling long-running,
|
|
4
|
+
background jobs, such as code indexing.
|
|
5
|
+
"""
|
|
6
|
+
import uuid
|
|
7
|
+
import threading
|
|
8
|
+
from datetime import datetime, timedelta
|
|
9
|
+
from dataclasses import dataclass, asdict
|
|
10
|
+
from enum import Enum
|
|
11
|
+
from typing import Any, Dict, List, Optional
|
|
12
|
+
from pathlib import Path
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
class JobStatus(Enum):
|
|
16
|
+
"""Enumeration for the possible statuses of a background job."""
|
|
17
|
+
PENDING = "pending"
|
|
18
|
+
RUNNING = "running"
|
|
19
|
+
COMPLETED = "completed"
|
|
20
|
+
FAILED = "failed"
|
|
21
|
+
CANCELLED = "cancelled"
|
|
22
|
+
|
|
23
|
+
@dataclass
|
|
24
|
+
class JobInfo:
|
|
25
|
+
"""
|
|
26
|
+
A data class to hold all information about a single background job.
|
|
27
|
+
This makes it easy to track the job's progress, status, and results.
|
|
28
|
+
"""
|
|
29
|
+
job_id: str
|
|
30
|
+
status: JobStatus
|
|
31
|
+
start_time: datetime
|
|
32
|
+
end_time: Optional[datetime] = None
|
|
33
|
+
total_files: int = 0
|
|
34
|
+
processed_files: int = 0
|
|
35
|
+
current_file: Optional[str] = None
|
|
36
|
+
estimated_duration: Optional[float] = None
|
|
37
|
+
actual_duration: Optional[float] = None
|
|
38
|
+
errors: List[str] = None
|
|
39
|
+
result: Optional[Dict[str, Any]] = None
|
|
40
|
+
path: Optional[str] = None
|
|
41
|
+
is_dependency: bool = False
|
|
42
|
+
|
|
43
|
+
def __post_init__(self):
|
|
44
|
+
"""Ensures the errors list is initialized after the object is created."""
|
|
45
|
+
if self.errors is None:
|
|
46
|
+
self.errors = []
|
|
47
|
+
|
|
48
|
+
@property
|
|
49
|
+
def progress_percentage(self) -> float:
|
|
50
|
+
"""Calculates the completion percentage of the job."""
|
|
51
|
+
if self.total_files == 0:
|
|
52
|
+
return 0.0
|
|
53
|
+
return (self.processed_files / self.total_files) * 100
|
|
54
|
+
|
|
55
|
+
@property
|
|
56
|
+
def estimated_time_remaining(self) -> Optional[float]:
|
|
57
|
+
"""Calculates the estimated time remaining based on the average time per file."""
|
|
58
|
+
if self.status != JobStatus.RUNNING or self.processed_files == 0:
|
|
59
|
+
return None
|
|
60
|
+
elapsed = (datetime.now() - self.start_time).total_seconds()
|
|
61
|
+
avg_time_per_file = elapsed / self.processed_files
|
|
62
|
+
remaining_files = self.total_files - self.processed_files
|
|
63
|
+
return remaining_files * avg_time_per_file
|
|
64
|
+
|
|
65
|
+
class JobManager:
|
|
66
|
+
"""
|
|
67
|
+
A thread-safe manager for creating, updating, and retrieving information
|
|
68
|
+
about background jobs. It stores job information in memory.
|
|
69
|
+
"""
|
|
70
|
+
def __init__(self):
|
|
71
|
+
self.jobs: Dict[str, JobInfo] = {}
|
|
72
|
+
self.lock = threading.Lock() # A lock to ensure thread-safe access to the jobs dictionary.
|
|
73
|
+
|
|
74
|
+
def create_job(self, path: str, is_dependency: bool = False) -> str:
|
|
75
|
+
"""Creates a new job, assigns it a unique ID, and stores it."""
|
|
76
|
+
job_id = str(uuid.uuid4())
|
|
77
|
+
with self.lock:
|
|
78
|
+
self.jobs[job_id] = JobInfo(
|
|
79
|
+
job_id=job_id,
|
|
80
|
+
status=JobStatus.PENDING,
|
|
81
|
+
start_time=datetime.now(),
|
|
82
|
+
path=path,
|
|
83
|
+
is_dependency=is_dependency
|
|
84
|
+
)
|
|
85
|
+
return job_id
|
|
86
|
+
|
|
87
|
+
def update_job(self, job_id: str, **kwargs):
|
|
88
|
+
"""Updates the information for a specific job in a thread-safe manner."""
|
|
89
|
+
with self.lock:
|
|
90
|
+
if job_id in self.jobs:
|
|
91
|
+
job = self.jobs[job_id]
|
|
92
|
+
for key, value in kwargs.items():
|
|
93
|
+
if hasattr(job, key):
|
|
94
|
+
setattr(job, key, value)
|
|
95
|
+
|
|
96
|
+
def get_job(self, job_id: str) -> Optional[JobInfo]:
|
|
97
|
+
"""Retrieves the information for a single job."""
|
|
98
|
+
with self.lock:
|
|
99
|
+
return self.jobs.get(job_id)
|
|
100
|
+
|
|
101
|
+
def list_jobs(self) -> List[JobInfo]:
|
|
102
|
+
"""Returns a list of all jobs currently in the manager."""
|
|
103
|
+
with self.lock:
|
|
104
|
+
return list(self.jobs.values())
|
|
105
|
+
|
|
106
|
+
def find_active_job_by_path(self, path: str) -> Optional[JobInfo]:
|
|
107
|
+
"""Finds the most recent, currently active (pending or running) job for a given path."""
|
|
108
|
+
with self.lock:
|
|
109
|
+
path_obj = Path(path).resolve()
|
|
110
|
+
|
|
111
|
+
matching_jobs = sorted(
|
|
112
|
+
[job for job in self.jobs.values() if job.path and Path(job.path).resolve() == path_obj],
|
|
113
|
+
key=lambda j: j.start_time,
|
|
114
|
+
reverse=True
|
|
115
|
+
)
|
|
116
|
+
|
|
117
|
+
for job in matching_jobs:
|
|
118
|
+
if job.status in [JobStatus.PENDING, JobStatus.RUNNING]:
|
|
119
|
+
return job
|
|
120
|
+
|
|
121
|
+
return None
|
|
122
|
+
|
|
123
|
+
def cleanup_old_jobs(self, max_age_hours: int = 24):
|
|
124
|
+
"""Removes old, completed jobs from memory to prevent memory leaks."""
|
|
125
|
+
cutoff_time = datetime.now() - timedelta(hours=max_age_hours)
|
|
126
|
+
with self.lock:
|
|
127
|
+
jobs_to_remove = [
|
|
128
|
+
job_id for job_id, job in self.jobs.items()
|
|
129
|
+
if job.end_time and job.end_time < cutoff_time
|
|
130
|
+
]
|
|
131
|
+
for job_id in jobs_to_remove:
|
|
132
|
+
del self.jobs[job_id]
|
|
@@ -0,0 +1,260 @@
|
|
|
1
|
+
# src/codegraphcontext/core/watcher.py
|
|
2
|
+
"""
|
|
3
|
+
This module implements the live file-watching functionality using the `watchdog` library.
|
|
4
|
+
It observes directories for changes and triggers updates to the code graph.
|
|
5
|
+
"""
|
|
6
|
+
import threading
|
|
7
|
+
from pathlib import Path
|
|
8
|
+
import typing
|
|
9
|
+
from watchdog.observers import Observer
|
|
10
|
+
from watchdog.events import FileSystemEventHandler
|
|
11
|
+
|
|
12
|
+
if typing.TYPE_CHECKING:
|
|
13
|
+
from codegraphcontext.tools.graph_builder import GraphBuilder
|
|
14
|
+
from codegraphcontext.core.jobs import JobManager
|
|
15
|
+
|
|
16
|
+
from codegraphcontext.utils.debug_log import debug_log, info_logger, error_logger, warning_logger
|
|
17
|
+
|
|
18
|
+
class RepositoryEventHandler(FileSystemEventHandler):
|
|
19
|
+
"""
|
|
20
|
+
A dedicated event handler for a single repository being watched.
|
|
21
|
+
|
|
22
|
+
This handler is stateful. It performs an initial scan of the repository
|
|
23
|
+
to build a baseline and then uses this cached state to perform efficient
|
|
24
|
+
updates when files are changed, created, or deleted.
|
|
25
|
+
"""
|
|
26
|
+
def __init__(self, graph_builder: "GraphBuilder", repo_path: Path, debounce_interval=2.0, perform_initial_scan: bool = True):
|
|
27
|
+
"""
|
|
28
|
+
Initializes the event handler.
|
|
29
|
+
|
|
30
|
+
Args:
|
|
31
|
+
graph_builder: An instance of the GraphBuilder to perform graph operations.
|
|
32
|
+
repo_path: The absolute path to the repository directory to watch.
|
|
33
|
+
debounce_interval: The time in seconds to wait for more changes before processing an event.
|
|
34
|
+
perform_initial_scan: Whether to perform an initial scan of the repository.
|
|
35
|
+
"""
|
|
36
|
+
super().__init__()
|
|
37
|
+
self.graph_builder = graph_builder
|
|
38
|
+
self.repo_path = repo_path
|
|
39
|
+
self.debounce_interval = debounce_interval
|
|
40
|
+
self.timers = {} # A dictionary to manage debounce timers for file paths.
|
|
41
|
+
|
|
42
|
+
# Caches for the repository's state.
|
|
43
|
+
self.all_file_data = []
|
|
44
|
+
self.imports_map = {}
|
|
45
|
+
|
|
46
|
+
# Perform the initial scan and linking when the watcher is created.
|
|
47
|
+
if perform_initial_scan:
|
|
48
|
+
self._initial_scan()
|
|
49
|
+
|
|
50
|
+
def _initial_scan(self):
|
|
51
|
+
"""Scans the entire repository, parses all files, and builds the initial graph."""
|
|
52
|
+
info_logger(f"Performing initial scan for watcher: {self.repo_path}")
|
|
53
|
+
supported_extensions = self.graph_builder.parsers.keys()
|
|
54
|
+
all_files = [f for f in self.repo_path.rglob("*") if f.is_file() and f.suffix in supported_extensions]
|
|
55
|
+
|
|
56
|
+
# 1. Pre-scan all files to get a global map of where every symbol is defined.
|
|
57
|
+
self.imports_map = self.graph_builder.pre_scan_imports(all_files)
|
|
58
|
+
|
|
59
|
+
# 2. Parse all files in detail and cache the parsed data.
|
|
60
|
+
for f in all_files:
|
|
61
|
+
parsed_data = self.graph_builder.parse_file(self.repo_path, f)
|
|
62
|
+
if "error" not in parsed_data:
|
|
63
|
+
self.all_file_data.append(parsed_data)
|
|
64
|
+
|
|
65
|
+
# 3. After all files are parsed, create the relationships (e.g., function calls) between them.
|
|
66
|
+
self.graph_builder.link_function_calls(self.all_file_data, self.imports_map)
|
|
67
|
+
self.graph_builder.link_inheritance(self.all_file_data, self.imports_map)
|
|
68
|
+
# Free memory — all_file_data is only needed during the linking pass.
|
|
69
|
+
self.all_file_data.clear()
|
|
70
|
+
info_logger(f"Initial scan and graph linking complete for: {self.repo_path}")
|
|
71
|
+
|
|
72
|
+
def _debounce(self, event_path, action):
|
|
73
|
+
"""
|
|
74
|
+
Schedules an action to run after a debounce interval.
|
|
75
|
+
This prevents the handler from firing on every single file save event in rapid
|
|
76
|
+
succession, which is common in IDEs. It waits for a quiet period before processing.
|
|
77
|
+
"""
|
|
78
|
+
# If a timer already exists for this path, cancel it.
|
|
79
|
+
if event_path in self.timers:
|
|
80
|
+
self.timers[event_path].cancel()
|
|
81
|
+
# Create and start a new timer.
|
|
82
|
+
timer = threading.Timer(self.debounce_interval, action)
|
|
83
|
+
timer.start()
|
|
84
|
+
self.timers[event_path] = timer
|
|
85
|
+
|
|
86
|
+
def _update_imports_map_for_file(self, changed_path: Path):
|
|
87
|
+
"""Re-scan a single file and merge its contributions into self.imports_map.
|
|
88
|
+
Removes stale paths for the file before inserting new ones so renamed/deleted
|
|
89
|
+
symbols don't leave dangling entries."""
|
|
90
|
+
changed_str = str(changed_path.resolve())
|
|
91
|
+
# Remove old contributions of this file from every symbol it previously exported.
|
|
92
|
+
for symbol in list(self.imports_map.keys()):
|
|
93
|
+
old_list = self.imports_map[symbol]
|
|
94
|
+
if changed_str in old_list:
|
|
95
|
+
new_list = [p for p in old_list if p != changed_str]
|
|
96
|
+
if new_list:
|
|
97
|
+
self.imports_map[symbol] = new_list
|
|
98
|
+
else:
|
|
99
|
+
del self.imports_map[symbol]
|
|
100
|
+
# Merge new contributions (if the file still exists).
|
|
101
|
+
if changed_path.exists():
|
|
102
|
+
new_map = self.graph_builder.pre_scan_imports([changed_path])
|
|
103
|
+
for symbol, paths in new_map.items():
|
|
104
|
+
if symbol not in self.imports_map:
|
|
105
|
+
self.imports_map[symbol] = []
|
|
106
|
+
self.imports_map[symbol].extend(paths)
|
|
107
|
+
|
|
108
|
+
def _handle_modification(self, event_path_str: str):
|
|
109
|
+
"""
|
|
110
|
+
Incremental update: only re-parse and re-link the changed file plus the files
|
|
111
|
+
that previously called into it. O(k) instead of O(n) for every event.
|
|
112
|
+
|
|
113
|
+
Algorithm:
|
|
114
|
+
1. Query Neo4j for files that have CALLS/INHERITS touching the changed file
|
|
115
|
+
(must happen BEFORE nodes are deleted, so the graph still has the old edges).
|
|
116
|
+
2. Update self.imports_map for the changed file only (O(1) file scan).
|
|
117
|
+
3. update_file_in_graph — DETACH DELETE cleans up ALL CALLS/INHERITS on the
|
|
118
|
+
changed file's nodes (both incoming and outgoing) automatically.
|
|
119
|
+
4. Delete outgoing CALLS from affected *caller* files (their CALLS to the changed
|
|
120
|
+
file were removed by DETACH DELETE, but their CALLS to unrelated files are
|
|
121
|
+
still there; we must delete all their outgoing CALLS before re-creating so we
|
|
122
|
+
don't leave stale CALLS to functions that have moved/been renamed).
|
|
123
|
+
5. Re-parse only the affected subset (changed file + callers + inheritors).
|
|
124
|
+
6. Build file_class_lookup cheaply from Neo4j (no full re-parse needed).
|
|
125
|
+
7. Re-create CALLS/INHERITS for the subset only.
|
|
126
|
+
"""
|
|
127
|
+
info_logger(f"File change detected (incremental update): {event_path_str}")
|
|
128
|
+
changed_path = Path(event_path_str)
|
|
129
|
+
changed_path_str = str(changed_path.resolve())
|
|
130
|
+
supported_extensions = self.graph_builder.parsers.keys()
|
|
131
|
+
|
|
132
|
+
# Step 1: Find affected neighbours BEFORE nodes are destroyed.
|
|
133
|
+
caller_paths = self.graph_builder.get_caller_file_paths(changed_path_str)
|
|
134
|
+
inheritor_paths = self.graph_builder.get_inheritance_neighbor_paths(changed_path_str)
|
|
135
|
+
affected_paths = {changed_path_str} | caller_paths | inheritor_paths
|
|
136
|
+
info_logger(
|
|
137
|
+
f"[INCREMENTAL] affected={len(affected_paths)} files "
|
|
138
|
+
f"(callers={len(caller_paths)}, inheritors={len(inheritor_paths)})"
|
|
139
|
+
)
|
|
140
|
+
|
|
141
|
+
# Step 2: Update imports_map for the changed file only.
|
|
142
|
+
self._update_imports_map_for_file(changed_path)
|
|
143
|
+
|
|
144
|
+
# Step 3: Delete + re-add nodes for the changed file.
|
|
145
|
+
# DETACH DELETE inside update_file_in_graph removes all CALLS/INHERITS on its nodes.
|
|
146
|
+
self.graph_builder.update_file_in_graph(changed_path, self.repo_path, self.imports_map)
|
|
147
|
+
|
|
148
|
+
# Step 4: Clean up CALLS/INHERITS from the affected *caller/inheritor* files.
|
|
149
|
+
# Their CALLS to the changed file were already removed by DETACH DELETE, but their
|
|
150
|
+
# CALLS to other files are still intact. We delete all their outgoing CALLS so we
|
|
151
|
+
# can safely re-create the full set from scratch for the subset.
|
|
152
|
+
other_callers = list(caller_paths) # does NOT include changed_path_str
|
|
153
|
+
other_inheritors = list(inheritor_paths)
|
|
154
|
+
if other_callers:
|
|
155
|
+
self.graph_builder.delete_outgoing_calls_from_files(other_callers)
|
|
156
|
+
if other_inheritors:
|
|
157
|
+
self.graph_builder.delete_inherits_for_files(other_inheritors)
|
|
158
|
+
|
|
159
|
+
# Step 5: Re-parse only the affected subset.
|
|
160
|
+
subset_file_data = []
|
|
161
|
+
for path_str in affected_paths:
|
|
162
|
+
p = Path(path_str)
|
|
163
|
+
if p.exists() and p.suffix in supported_extensions:
|
|
164
|
+
parsed = self.graph_builder.parse_file(self.repo_path, p)
|
|
165
|
+
if "error" not in parsed:
|
|
166
|
+
subset_file_data.append(parsed)
|
|
167
|
+
|
|
168
|
+
# Step 6: Get full-repo file_class_lookup from Neo4j (avoids re-parsing all files).
|
|
169
|
+
# The changed file's new classes are already overlaid inside _create_all_function_calls.
|
|
170
|
+
file_class_lookup = self.graph_builder.get_repo_class_lookup(self.repo_path)
|
|
171
|
+
|
|
172
|
+
# Step 7: Re-create CALLS/INHERITS for the affected subset only.
|
|
173
|
+
info_logger(f"[INCREMENTAL] Re-linking {len(subset_file_data)} files...")
|
|
174
|
+
self.graph_builder.link_function_calls(subset_file_data, self.imports_map, file_class_lookup)
|
|
175
|
+
self.graph_builder.link_inheritance(subset_file_data, self.imports_map)
|
|
176
|
+
info_logger(f"[INCREMENTAL] Done. Graph refresh for {event_path_str} complete! ✅")
|
|
177
|
+
|
|
178
|
+
# The following methods are called by the watchdog observer when a file event occurs.
|
|
179
|
+
def on_created(self, event):
|
|
180
|
+
if not event.is_directory and Path(event.src_path).suffix in self.graph_builder.parsers:
|
|
181
|
+
self._debounce(event.src_path, lambda: self._handle_modification(event.src_path))
|
|
182
|
+
|
|
183
|
+
def on_modified(self, event):
|
|
184
|
+
if not event.is_directory and Path(event.src_path).suffix in self.graph_builder.parsers:
|
|
185
|
+
self._debounce(event.src_path, lambda: self._handle_modification(event.src_path))
|
|
186
|
+
|
|
187
|
+
def on_deleted(self, event):
|
|
188
|
+
if not event.is_directory and Path(event.src_path).suffix in self.graph_builder.parsers:
|
|
189
|
+
self._debounce(event.src_path, lambda: self._handle_modification(event.src_path))
|
|
190
|
+
|
|
191
|
+
def on_moved(self, event):
|
|
192
|
+
if not event.is_directory:
|
|
193
|
+
if Path(event.src_path).suffix in self.graph_builder.parsers:
|
|
194
|
+
self._debounce(event.src_path, lambda: self._handle_modification(event.src_path))
|
|
195
|
+
if Path(event.dest_path).suffix in self.graph_builder.parsers:
|
|
196
|
+
self._debounce(event.dest_path, lambda: self._handle_modification(event.dest_path))
|
|
197
|
+
|
|
198
|
+
|
|
199
|
+
class CodeWatcher:
|
|
200
|
+
"""
|
|
201
|
+
Manages the file system observer thread. It can watch multiple directories,
|
|
202
|
+
assigning a separate `RepositoryEventHandler` to each one.
|
|
203
|
+
"""
|
|
204
|
+
def __init__(self, graph_builder: "GraphBuilder", job_manager= "JobManager"):
|
|
205
|
+
self.graph_builder = graph_builder
|
|
206
|
+
self.observer = Observer()
|
|
207
|
+
self.watched_paths = set() # Keep track of paths already being watched.
|
|
208
|
+
self.watches = {} # Store watch objects to allow unscheduling
|
|
209
|
+
|
|
210
|
+
def watch_directory(self, path: str, perform_initial_scan: bool = True):
|
|
211
|
+
"""Schedules a directory to be watched for changes."""
|
|
212
|
+
path_obj = Path(path).resolve()
|
|
213
|
+
path_str = str(path_obj)
|
|
214
|
+
|
|
215
|
+
if path_str in self.watched_paths:
|
|
216
|
+
info_logger(f"Path already being watched: {path_str}")
|
|
217
|
+
return {"message": f"Path already being watched: {path_str}"}
|
|
218
|
+
|
|
219
|
+
# Create a new, dedicated event handler for this specific repository path.
|
|
220
|
+
event_handler = RepositoryEventHandler(self.graph_builder, path_obj, perform_initial_scan=perform_initial_scan)
|
|
221
|
+
|
|
222
|
+
watch = self.observer.schedule(event_handler, path_str, recursive=True)
|
|
223
|
+
self.watches[path_str] = watch
|
|
224
|
+
self.watched_paths.add(path_str)
|
|
225
|
+
info_logger(f"Started watching for code changes in: {path_str}")
|
|
226
|
+
|
|
227
|
+
return {"message": f"Started watching {path_str}."}
|
|
228
|
+
def unwatch_directory(self, path: str):
|
|
229
|
+
"""Stops watching a directory for changes."""
|
|
230
|
+
path_obj = Path(path).resolve()
|
|
231
|
+
path_str = str(path_obj)
|
|
232
|
+
|
|
233
|
+
if path_str not in self.watched_paths:
|
|
234
|
+
warning_logger(f"Attempted to unwatch a path that is not being watched: {path_str}")
|
|
235
|
+
return {"error": f"Path not currently being watched: {path_str}"}
|
|
236
|
+
|
|
237
|
+
watch = self.watches.pop(path_str, None)
|
|
238
|
+
if watch:
|
|
239
|
+
self.observer.unschedule(watch)
|
|
240
|
+
|
|
241
|
+
self.watched_paths.discard(path_str)
|
|
242
|
+
info_logger(f"Stopped watching for code changes in: {path_str}")
|
|
243
|
+
return {"message": f"Stopped watching {path_str}."}
|
|
244
|
+
|
|
245
|
+
def list_watched_paths(self) -> list:
|
|
246
|
+
"""Returns a list of all currently watched directory paths."""
|
|
247
|
+
return list(self.watched_paths)
|
|
248
|
+
|
|
249
|
+
def start(self):
|
|
250
|
+
"""Starts the observer thread."""
|
|
251
|
+
if not self.observer.is_alive():
|
|
252
|
+
self.observer.start()
|
|
253
|
+
info_logger("Code watcher observer thread started.")
|
|
254
|
+
|
|
255
|
+
def stop(self):
|
|
256
|
+
"""Stops the observer thread gracefully."""
|
|
257
|
+
if self.observer.is_alive():
|
|
258
|
+
self.observer.stop()
|
|
259
|
+
self.observer.join() # Wait for the thread to terminate.
|
|
260
|
+
info_logger("Code watcher observer thread stopped.")
|
|
@@ -0,0 +1,125 @@
|
|
|
1
|
+
# src/codegraphcontext/prompts.py
|
|
2
|
+
"""
|
|
3
|
+
This file contains the system prompt for the language model.
|
|
4
|
+
This prompt provides the core instructions, principles, and standard operating
|
|
5
|
+
procedures for the AI assistant, guiding it on how to effectively use the tools
|
|
6
|
+
provided by this MCP server.
|
|
7
|
+
"""
|
|
8
|
+
|
|
9
|
+
LLM_SYSTEM_PROMPT = """# AI Pair Programmer Instructions
|
|
10
|
+
|
|
11
|
+
## 1. Your Role and Goal
|
|
12
|
+
|
|
13
|
+
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.
|
|
14
|
+
**Always prioritize using this MCP tools when they can simplify or enhance your workflow compared to guessing.**
|
|
15
|
+
|
|
16
|
+
## 2. Your Core Principles
|
|
17
|
+
|
|
18
|
+
### Principle I: Ground Your Answers in Fact
|
|
19
|
+
**Your CORE DIRECTIVE is to use the provided tools to gather facts from the MCP server *before* answering questions or generating code.** Do not guess. Your value comes from providing contextually-aware, accurate assistance.
|
|
20
|
+
|
|
21
|
+
### Principle II: Be an Agent, Not Just a Planner
|
|
22
|
+
**Your goal is to complete the user's task in the fewest steps possible.**
|
|
23
|
+
* If the user's request maps directly to a single tool, **execute that tool immediately.**
|
|
24
|
+
* Do not create a multi-step plan for a one-step task. The Standard Operating Procedures (SOPs) below are for complex queries that require reasoning and combining information from multiple tools.
|
|
25
|
+
|
|
26
|
+
**Example of what NOT to do:**
|
|
27
|
+
|
|
28
|
+
> **User:** "Start watching the `my-project` folder."
|
|
29
|
+
> **Incorrect Plan:**
|
|
30
|
+
> 1. Check if `watchdog` is installed.
|
|
31
|
+
> 2. Use the `watch_directory` tool on `my-project`.
|
|
32
|
+
> 3. Update a todo list.
|
|
33
|
+
|
|
34
|
+
**Example of the CORRECT, direct action:**
|
|
35
|
+
|
|
36
|
+
> **User:** "Start watching the `my-project` folder."
|
|
37
|
+
> **Correct Action:** Immediately call the `watch_directory` tool.
|
|
38
|
+
> ```json
|
|
39
|
+
> {
|
|
40
|
+
> "tool_name": "watch_directory",
|
|
41
|
+
> "arguments": { "path": "my-project" }
|
|
42
|
+
> }
|
|
43
|
+
> ```
|
|
44
|
+
|
|
45
|
+
## 3. Tool Manifest & Usage
|
|
46
|
+
|
|
47
|
+
| Tool Name | Purpose & When to Use |
|
|
48
|
+
| :--------------------------- | :------------------------------------------------------------------------------------------------------------------------------------ |
|
|
49
|
+
| **`find_code`** | **Your primary search tool.** Use this first for almost any query about locating code. t |
|
|
50
|
+
| **`analyze_code_relationships`** | **Your deep analysis tool.** Use this after locating a specific item. Use query types like `find_callers` or `find_callees`. |
|
|
51
|
+
| **`add_code_to_graph`** | **Your indexing tool.** Use this when the user wants to add a new project folder or file to the context. |
|
|
52
|
+
| **`add_package_to_graph`** | **Your dependency indexing tool.** Use this to add a `pip` package to the context. |
|
|
53
|
+
| **`list_jobs`** & **`check_job_status`** | **Your job monitoring tools.** |
|
|
54
|
+
| **`watch_directory`** | **Your live-update tool.** Use this if the user wants to automatically keep the context updated as they work. |
|
|
55
|
+
| **`execute_cypher_query`** | **Expert Fallback Tool.** Use this *only* when other tools cannot answer a very specific or complex question about the code graph. Requires knowledge of Cypher. |
|
|
56
|
+
|
|
57
|
+
## 4. Graph Schema Reference
|
|
58
|
+
**CRITICAL FOR CYPHER QUERIES:** The database schema uses specific property names.
|
|
59
|
+
|
|
60
|
+
### Nodes & Properties
|
|
61
|
+
* **`Repository`**
|
|
62
|
+
* `name` (string)
|
|
63
|
+
* `path` (string, absolute path)
|
|
64
|
+
* `is_dependency` (boolean)
|
|
65
|
+
* **`File`**
|
|
66
|
+
* `name` (string)
|
|
67
|
+
* `path` (string, absolute path)
|
|
68
|
+
* `relative_path` (string)
|
|
69
|
+
* `is_dependency` (boolean)
|
|
70
|
+
* **`Function`**
|
|
71
|
+
* `name` (string)
|
|
72
|
+
* `path` (string, absolute path) **<-- NOTE: Use `path`, NOT `path`**
|
|
73
|
+
* `line_number` (int)
|
|
74
|
+
* `end_line` (int)
|
|
75
|
+
* `args` (list)
|
|
76
|
+
* `cyclomatic_complexity` (int)
|
|
77
|
+
* `decorators` (list)
|
|
78
|
+
* `lang` (string)
|
|
79
|
+
* `source` (string, the full source code of the function)
|
|
80
|
+
* `is_dependency` (boolean)
|
|
81
|
+
* **`Class`**
|
|
82
|
+
* `name` (string)
|
|
83
|
+
* `path` (string, absolute path) **<-- NOTE: Use `path`, NOT `path`**
|
|
84
|
+
* `line_number` (int)
|
|
85
|
+
* `end_line` (int)
|
|
86
|
+
* `bases` (list)
|
|
87
|
+
* `decorators` (list)
|
|
88
|
+
* `lang` (string)
|
|
89
|
+
* `source` (string, the full source code of the class)
|
|
90
|
+
* `is_dependency` (boolean)
|
|
91
|
+
|
|
92
|
+
### Relationships
|
|
93
|
+
* **`CONTAINS`**:
|
|
94
|
+
* `(Repository)-[:CONTAINS]->(File)`
|
|
95
|
+
* `(File)-[:CONTAINS]->(Function)`
|
|
96
|
+
* `(File)-[:CONTAINS]->(Class)`
|
|
97
|
+
* **`CALLS`**: `(Function)-[:CALLS]->(Function)`
|
|
98
|
+
* **`IMPORTS`**: `(File)-[:IMPORTS]->(Module)`
|
|
99
|
+
* **`INHERITS`**: `(Class)-[:INHERITS]->(Class)`
|
|
100
|
+
|
|
101
|
+
## 5. Standard Operating Procedures (SOPs) for Complex Tasks
|
|
102
|
+
|
|
103
|
+
**Note:** Follow these methodical workflows for **complex requests** that require multiple steps of reasoning or combining information from several tools. For direct commands, refer to Principle II and act immediately.
|
|
104
|
+
|
|
105
|
+
### SOP-1: Answering "Where is...?" or "How does...?" Questions
|
|
106
|
+
1. **Locate:** Use `find_code` to find the relevant code.
|
|
107
|
+
2. **Analyze:** Use `analyze_code_relationships` to understand its usage.
|
|
108
|
+
3. **Synthesize:** Combine the information into a clear explanation.
|
|
109
|
+
|
|
110
|
+
### SOP-2: Generating New Code
|
|
111
|
+
1. **Find Context:** Use `find_code` to find similar, existing code to match the style.
|
|
112
|
+
2. **Find Reusable Code:** Use `find_code` to locate specific helper functions the user wants you to use.
|
|
113
|
+
3. **Generate:** Write the code using the correct imports and signatures.
|
|
114
|
+
|
|
115
|
+
### SOP-3: Refactoring or Analyzing Impact
|
|
116
|
+
1. **Identify & Locate:** Use `find_code` to get the canonical path of the item to be changed.
|
|
117
|
+
2. **Assess Impact:** Use `analyze_code_relationships` with the `find_callers` query type to find all affected locations.
|
|
118
|
+
3. **Report Findings:** Present a clear list of all affected files.
|
|
119
|
+
|
|
120
|
+
### SOP-4: Using the Cypher Fallback
|
|
121
|
+
1. **Attempt Standard Tools:** First, always try to use `find_code` and `analyze_code_relationships`.
|
|
122
|
+
2. **Identify Failure:** If the standard tools cannot answer a complex, multi-step relationship query (e.g., "Find all functions that are called by a method in a class that inherits from 'BaseHandler'"), then and only then, resort to the fallback.
|
|
123
|
+
3. **Formulate & Execute:** Construct a Cypher query to find the answer and execute it using `execute_cypher_query`. **Consult the Graph Schema Reference above to ensure you use the correct property names (e.g. `path` vs `path`).**
|
|
124
|
+
4. **Present Results:** Explain the results to the user based on the query output.
|
|
125
|
+
"""
|