codegraphcontext-rust 0.5.5__cp310-cp310-win_amd64.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (101) hide show
  1. codegraphcontext/__init__.py +2 -0
  2. codegraphcontext/__main__.py +5 -0
  3. codegraphcontext/_cgc_rust.cp310-win_amd64.pyd +0 -0
  4. codegraphcontext/cli/__init__.py +1 -0
  5. codegraphcontext/cli/cli_helpers.py +774 -0
  6. codegraphcontext/cli/config_manager.py +1051 -0
  7. codegraphcontext/cli/main.py +2386 -0
  8. codegraphcontext/cli/registry_commands.py +404 -0
  9. codegraphcontext/cli/setup_macos.py +93 -0
  10. codegraphcontext/cli/setup_wizard.py +991 -0
  11. codegraphcontext/cli/visualizer.py +50 -0
  12. codegraphcontext/core/__init__.py +165 -0
  13. codegraphcontext/core/bundle_registry.py +181 -0
  14. codegraphcontext/core/cgc_bundle.py +858 -0
  15. codegraphcontext/core/cgcignore.py +118 -0
  16. codegraphcontext/core/database.py +273 -0
  17. codegraphcontext/core/database_falkordb.py +480 -0
  18. codegraphcontext/core/database_falkordb_remote.py +199 -0
  19. codegraphcontext/core/database_kuzu.py +636 -0
  20. codegraphcontext/core/falkor_worker.py +132 -0
  21. codegraphcontext/core/jobs.py +132 -0
  22. codegraphcontext/core/watcher.py +260 -0
  23. codegraphcontext/prompts.py +125 -0
  24. codegraphcontext/server.py +471 -0
  25. codegraphcontext/tool_definitions.py +220 -0
  26. codegraphcontext/tools/__init__.py +0 -0
  27. codegraphcontext/tools/advanced_language_query_tool.py +103 -0
  28. codegraphcontext/tools/code_finder.py +1160 -0
  29. codegraphcontext/tools/graph_builder.py +337 -0
  30. codegraphcontext/tools/handlers/analysis_handlers.py +114 -0
  31. codegraphcontext/tools/handlers/indexing_handlers.py +116 -0
  32. codegraphcontext/tools/handlers/management_handlers.py +339 -0
  33. codegraphcontext/tools/handlers/query_handlers.py +83 -0
  34. codegraphcontext/tools/handlers/watcher_handlers.py +83 -0
  35. codegraphcontext/tools/indexing/__init__.py +1 -0
  36. codegraphcontext/tools/indexing/constants.py +25 -0
  37. codegraphcontext/tools/indexing/discovery.py +64 -0
  38. codegraphcontext/tools/indexing/engine.py +109 -0
  39. codegraphcontext/tools/indexing/persistence/__init__.py +3 -0
  40. codegraphcontext/tools/indexing/persistence/parquet_loader.py +643 -0
  41. codegraphcontext/tools/indexing/persistence/writer.py +1114 -0
  42. codegraphcontext/tools/indexing/pipeline.py +171 -0
  43. codegraphcontext/tools/indexing/pre_scan.py +105 -0
  44. codegraphcontext/tools/indexing/resolution/__init__.py +8 -0
  45. codegraphcontext/tools/indexing/resolution/calls.py +253 -0
  46. codegraphcontext/tools/indexing/resolution/inheritance.py +91 -0
  47. codegraphcontext/tools/indexing/sanitize.py +41 -0
  48. codegraphcontext/tools/indexing/schema.py +79 -0
  49. codegraphcontext/tools/indexing/schema_contract.py +44 -0
  50. codegraphcontext/tools/indexing/scip_pipeline.py +140 -0
  51. codegraphcontext/tools/languages/c.py +563 -0
  52. codegraphcontext/tools/languages/cpp.py +616 -0
  53. codegraphcontext/tools/languages/csharp.py +551 -0
  54. codegraphcontext/tools/languages/dart.py +378 -0
  55. codegraphcontext/tools/languages/elixir.py +460 -0
  56. codegraphcontext/tools/languages/go.py +499 -0
  57. codegraphcontext/tools/languages/haskell.py +426 -0
  58. codegraphcontext/tools/languages/java.py +500 -0
  59. codegraphcontext/tools/languages/javascript.py +589 -0
  60. codegraphcontext/tools/languages/kotlin.py +640 -0
  61. codegraphcontext/tools/languages/perl.py +260 -0
  62. codegraphcontext/tools/languages/php.py +519 -0
  63. codegraphcontext/tools/languages/python.py +576 -0
  64. codegraphcontext/tools/languages/ruby.py +537 -0
  65. codegraphcontext/tools/languages/rust.py +295 -0
  66. codegraphcontext/tools/languages/scala.py +519 -0
  67. codegraphcontext/tools/languages/swift.py +491 -0
  68. codegraphcontext/tools/languages/typescript.py +575 -0
  69. codegraphcontext/tools/languages/typescriptjsx.py +151 -0
  70. codegraphcontext/tools/package_resolver.py +472 -0
  71. codegraphcontext/tools/query_tool_languages/c_toolkit.py +5 -0
  72. codegraphcontext/tools/query_tool_languages/cpp_toolkit.py +87 -0
  73. codegraphcontext/tools/query_tool_languages/csharp_toolkit.py +5 -0
  74. codegraphcontext/tools/query_tool_languages/dart_toolkit.py +5 -0
  75. codegraphcontext/tools/query_tool_languages/go_toolkit.py +5 -0
  76. codegraphcontext/tools/query_tool_languages/haskell_toolkit.py +5 -0
  77. codegraphcontext/tools/query_tool_languages/java_toolkit.py +5 -0
  78. codegraphcontext/tools/query_tool_languages/javascript_toolkit.py +5 -0
  79. codegraphcontext/tools/query_tool_languages/perl_toolkit.py +5 -0
  80. codegraphcontext/tools/query_tool_languages/python_toolkit.py +5 -0
  81. codegraphcontext/tools/query_tool_languages/ruby_toolkit.py +5 -0
  82. codegraphcontext/tools/query_tool_languages/rust_toolkit.py +5 -0
  83. codegraphcontext/tools/query_tool_languages/scala_toolkit.py +5 -0
  84. codegraphcontext/tools/query_tool_languages/swift_toolkit.py +5 -0
  85. codegraphcontext/tools/query_tool_languages/typescript_toolkit.py +5 -0
  86. codegraphcontext/tools/scip_indexer.py +467 -0
  87. codegraphcontext/tools/scip_pb2.py +2455 -0
  88. codegraphcontext/tools/system.py +133 -0
  89. codegraphcontext/tools/tree_sitter_parser.py +103 -0
  90. codegraphcontext/utils/debug_log.py +90 -0
  91. codegraphcontext/utils/path_ignore.py +55 -0
  92. codegraphcontext/utils/repo_path.py +27 -0
  93. codegraphcontext/utils/tree_sitter_manager.py +285 -0
  94. codegraphcontext/utils/visualize_graph.py +0 -0
  95. codegraphcontext/viz/server.py +282 -0
  96. codegraphcontext_rust-0.5.5.dist-info/METADATA +537 -0
  97. codegraphcontext_rust-0.5.5.dist-info/RECORD +101 -0
  98. codegraphcontext_rust-0.5.5.dist-info/WHEEL +4 -0
  99. codegraphcontext_rust-0.5.5.dist-info/entry_points.txt +3 -0
  100. codegraphcontext_rust-0.5.5.dist-info/licenses/LICENSE +21 -0
  101. codegraphcontext_rust-0.5.5.dist-info/sboms/cgc-python.cyclonedx.json +2333 -0
@@ -0,0 +1,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
+ """