codebase-mcp 0.1.0__py3-none-any.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.
- codebase_mcp/__init__.py +3 -0
- codebase_mcp/__main__.py +524 -0
- codebase_mcp/config.py +211 -0
- codebase_mcp/db.py +541 -0
- codebase_mcp/exporter.py +243 -0
- codebase_mcp/handoff.py +317 -0
- codebase_mcp/indexer.py +415 -0
- codebase_mcp/models.py +46 -0
- codebase_mcp/parsers/__init__.py +15 -0
- codebase_mcp/parsers/base.py +157 -0
- codebase_mcp/parsers/config_parsers.py +462 -0
- codebase_mcp/parsers/generic.py +95 -0
- codebase_mcp/parsers/go.py +222 -0
- codebase_mcp/parsers/python.py +231 -0
- codebase_mcp/parsers/rust.py +205 -0
- codebase_mcp/parsers/typescript.py +303 -0
- codebase_mcp/parsers/universal.py +625 -0
- codebase_mcp/server.py +1291 -0
- codebase_mcp/watcher.py +169 -0
- codebase_mcp/webui.py +611 -0
- codebase_mcp-0.1.0.dist-info/METADATA +424 -0
- codebase_mcp-0.1.0.dist-info/RECORD +24 -0
- codebase_mcp-0.1.0.dist-info/WHEEL +4 -0
- codebase_mcp-0.1.0.dist-info/entry_points.txt +2 -0
codebase_mcp/watcher.py
ADDED
|
@@ -0,0 +1,169 @@
|
|
|
1
|
+
"""
|
|
2
|
+
File system watcher: monitors the project directory and triggers incremental
|
|
3
|
+
re-index when source files change.
|
|
4
|
+
|
|
5
|
+
Uses watchdog for cross-platform file watching (Windows ReadDirectoryChangesW,
|
|
6
|
+
Linux inotify, macOS FSEvents). Debounces rapid changes with a 2-second window.
|
|
7
|
+
|
|
8
|
+
Optional dependency — gracefully fails with a clear message if watchdog is not installed.
|
|
9
|
+
"""
|
|
10
|
+
|
|
11
|
+
from __future__ import annotations
|
|
12
|
+
import logging
|
|
13
|
+
import queue
|
|
14
|
+
import threading
|
|
15
|
+
import time
|
|
16
|
+
from typing import TYPE_CHECKING
|
|
17
|
+
|
|
18
|
+
if TYPE_CHECKING:
|
|
19
|
+
from .config import Config
|
|
20
|
+
|
|
21
|
+
log = logging.getLogger(__name__)
|
|
22
|
+
|
|
23
|
+
# Global watcher instance (one per process)
|
|
24
|
+
_active_watcher: "CodebaseWatcher | None" = None
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
class WatcherNotAvailable(RuntimeError):
|
|
28
|
+
pass
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
class CodebaseWatcher:
|
|
32
|
+
"""
|
|
33
|
+
Background file watcher with debounced incremental re-index.
|
|
34
|
+
|
|
35
|
+
Architecture:
|
|
36
|
+
Main thread: FastMCP / CLI (calls start/stop)
|
|
37
|
+
Observer thread: watchdog OS-level event loop
|
|
38
|
+
Debounce thread: waits 2s after last event, then calls run_index
|
|
39
|
+
"""
|
|
40
|
+
|
|
41
|
+
def __init__(self, project_root: str, db_path: str, config: "Config"):
|
|
42
|
+
try:
|
|
43
|
+
from watchdog.observers import Observer # type: ignore
|
|
44
|
+
except ImportError:
|
|
45
|
+
raise WatcherNotAvailable(
|
|
46
|
+
"watchdog is not installed. Install it with: pip install 'codebase-mcp[watch]'"
|
|
47
|
+
)
|
|
48
|
+
|
|
49
|
+
self._root = project_root
|
|
50
|
+
self._db_path = db_path
|
|
51
|
+
self._config = config
|
|
52
|
+
self._queue: queue.Queue[str] = queue.Queue()
|
|
53
|
+
self._observer = Observer()
|
|
54
|
+
self._running = False
|
|
55
|
+
self._debounce_thread: threading.Thread | None = None
|
|
56
|
+
self._stats = {"events": 0, "reindexes": 0, "last_reindex": None}
|
|
57
|
+
|
|
58
|
+
def start(self) -> None:
|
|
59
|
+
from watchdog.events import FileSystemEventHandler, FileSystemEvent # type: ignore
|
|
60
|
+
|
|
61
|
+
config = self._config
|
|
62
|
+
|
|
63
|
+
class _ChangeHandler(FileSystemEventHandler):
|
|
64
|
+
def on_any_event(self, event: FileSystemEvent):
|
|
65
|
+
if event.is_directory:
|
|
66
|
+
return
|
|
67
|
+
src = getattr(event, "src_path", "")
|
|
68
|
+
if not src:
|
|
69
|
+
return
|
|
70
|
+
# Only queue files we actually index
|
|
71
|
+
import os
|
|
72
|
+
ext = os.path.splitext(src)[1]
|
|
73
|
+
basename = os.path.basename(src)
|
|
74
|
+
lang = config.language_for(ext) or config.language_for_basename(basename)
|
|
75
|
+
if lang:
|
|
76
|
+
self.queue.put(src)
|
|
77
|
+
|
|
78
|
+
handler = _ChangeHandler()
|
|
79
|
+
handler.queue = self._queue
|
|
80
|
+
|
|
81
|
+
if not __import__("os").path.isdir(self._root):
|
|
82
|
+
log.warning("Watcher: project root does not exist: %s", self._root)
|
|
83
|
+
return
|
|
84
|
+
|
|
85
|
+
self._observer.schedule(handler, self._root, recursive=True)
|
|
86
|
+
self._observer.start()
|
|
87
|
+
|
|
88
|
+
self._running = True
|
|
89
|
+
self._debounce_thread = threading.Thread(
|
|
90
|
+
target=self._debounce_loop, name="codebase-mcp-watcher", daemon=True
|
|
91
|
+
)
|
|
92
|
+
self._debounce_thread.start()
|
|
93
|
+
log.info("File watcher started for %s", self._root)
|
|
94
|
+
|
|
95
|
+
def stop(self) -> None:
|
|
96
|
+
self._running = False
|
|
97
|
+
self._observer.stop()
|
|
98
|
+
self._observer.join(timeout=5)
|
|
99
|
+
log.info("File watcher stopped")
|
|
100
|
+
|
|
101
|
+
@property
|
|
102
|
+
def is_running(self) -> bool:
|
|
103
|
+
return self._running
|
|
104
|
+
|
|
105
|
+
@property
|
|
106
|
+
def stats(self) -> dict:
|
|
107
|
+
return dict(self._stats)
|
|
108
|
+
|
|
109
|
+
def _debounce_loop(self) -> None:
|
|
110
|
+
from .indexer import run_index
|
|
111
|
+
|
|
112
|
+
while self._running:
|
|
113
|
+
# Block until the first change arrives (poll every 5s to check running)
|
|
114
|
+
try:
|
|
115
|
+
self._queue.get(timeout=5.0)
|
|
116
|
+
self._stats["events"] += 1
|
|
117
|
+
except queue.Empty:
|
|
118
|
+
continue
|
|
119
|
+
|
|
120
|
+
# Drain for 2 seconds to batch rapid changes (e.g., git checkout)
|
|
121
|
+
deadline = time.monotonic() + 2.0
|
|
122
|
+
while time.monotonic() < deadline:
|
|
123
|
+
try:
|
|
124
|
+
self._queue.get_nowait()
|
|
125
|
+
self._stats["events"] += 1
|
|
126
|
+
except queue.Empty:
|
|
127
|
+
time.sleep(0.1)
|
|
128
|
+
|
|
129
|
+
# Drain any remaining
|
|
130
|
+
while not self._queue.empty():
|
|
131
|
+
try:
|
|
132
|
+
self._queue.get_nowait()
|
|
133
|
+
except queue.Empty:
|
|
134
|
+
break
|
|
135
|
+
|
|
136
|
+
# Run incremental index
|
|
137
|
+
log.debug("Watcher: triggering incremental re-index")
|
|
138
|
+
try:
|
|
139
|
+
result = run_index(self._db_path, self._root, self._config, full_reindex=False)
|
|
140
|
+
self._stats["reindexes"] += 1
|
|
141
|
+
self._stats["last_reindex"] = __import__("datetime").datetime.utcnow().isoformat() + "Z"
|
|
142
|
+
log.info(
|
|
143
|
+
"Auto-reindex: %d files changed, %d symbols updated (%.0fms)",
|
|
144
|
+
result.files_changed, result.symbols_added, result.duration_ms
|
|
145
|
+
)
|
|
146
|
+
except Exception as e:
|
|
147
|
+
log.error("Auto-reindex failed: %s", e)
|
|
148
|
+
|
|
149
|
+
|
|
150
|
+
# ─── Global watcher management ───────────────────────────────────────────────
|
|
151
|
+
|
|
152
|
+
def start_watcher(project_root: str, db_path: str, config: "Config") -> CodebaseWatcher:
|
|
153
|
+
global _active_watcher
|
|
154
|
+
if _active_watcher and _active_watcher.is_running:
|
|
155
|
+
_active_watcher.stop()
|
|
156
|
+
_active_watcher = CodebaseWatcher(project_root, db_path, config)
|
|
157
|
+
_active_watcher.start()
|
|
158
|
+
return _active_watcher
|
|
159
|
+
|
|
160
|
+
|
|
161
|
+
def stop_watcher() -> None:
|
|
162
|
+
global _active_watcher
|
|
163
|
+
if _active_watcher:
|
|
164
|
+
_active_watcher.stop()
|
|
165
|
+
_active_watcher = None
|
|
166
|
+
|
|
167
|
+
|
|
168
|
+
def get_watcher() -> "CodebaseWatcher | None":
|
|
169
|
+
return _active_watcher
|