mcp-vector-search 0.15.7__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.
Potentially problematic release.
This version of mcp-vector-search might be problematic. Click here for more details.
- mcp_vector_search/__init__.py +10 -0
- mcp_vector_search/cli/__init__.py +1 -0
- mcp_vector_search/cli/commands/__init__.py +1 -0
- mcp_vector_search/cli/commands/auto_index.py +397 -0
- mcp_vector_search/cli/commands/chat.py +534 -0
- mcp_vector_search/cli/commands/config.py +393 -0
- mcp_vector_search/cli/commands/demo.py +358 -0
- mcp_vector_search/cli/commands/index.py +762 -0
- mcp_vector_search/cli/commands/init.py +658 -0
- mcp_vector_search/cli/commands/install.py +869 -0
- mcp_vector_search/cli/commands/install_old.py +700 -0
- mcp_vector_search/cli/commands/mcp.py +1254 -0
- mcp_vector_search/cli/commands/reset.py +393 -0
- mcp_vector_search/cli/commands/search.py +796 -0
- mcp_vector_search/cli/commands/setup.py +1133 -0
- mcp_vector_search/cli/commands/status.py +584 -0
- mcp_vector_search/cli/commands/uninstall.py +404 -0
- mcp_vector_search/cli/commands/visualize/__init__.py +39 -0
- mcp_vector_search/cli/commands/visualize/cli.py +265 -0
- mcp_vector_search/cli/commands/visualize/exporters/__init__.py +12 -0
- mcp_vector_search/cli/commands/visualize/exporters/html_exporter.py +33 -0
- mcp_vector_search/cli/commands/visualize/exporters/json_exporter.py +29 -0
- mcp_vector_search/cli/commands/visualize/graph_builder.py +709 -0
- mcp_vector_search/cli/commands/visualize/layout_engine.py +469 -0
- mcp_vector_search/cli/commands/visualize/server.py +201 -0
- mcp_vector_search/cli/commands/visualize/state_manager.py +428 -0
- mcp_vector_search/cli/commands/visualize/templates/__init__.py +16 -0
- mcp_vector_search/cli/commands/visualize/templates/base.py +218 -0
- mcp_vector_search/cli/commands/visualize/templates/scripts.py +3670 -0
- mcp_vector_search/cli/commands/visualize/templates/styles.py +779 -0
- mcp_vector_search/cli/commands/visualize.py.original +2536 -0
- mcp_vector_search/cli/commands/watch.py +287 -0
- mcp_vector_search/cli/didyoumean.py +520 -0
- mcp_vector_search/cli/export.py +320 -0
- mcp_vector_search/cli/history.py +295 -0
- mcp_vector_search/cli/interactive.py +342 -0
- mcp_vector_search/cli/main.py +484 -0
- mcp_vector_search/cli/output.py +414 -0
- mcp_vector_search/cli/suggestions.py +375 -0
- mcp_vector_search/config/__init__.py +1 -0
- mcp_vector_search/config/constants.py +24 -0
- mcp_vector_search/config/defaults.py +200 -0
- mcp_vector_search/config/settings.py +146 -0
- mcp_vector_search/core/__init__.py +1 -0
- mcp_vector_search/core/auto_indexer.py +298 -0
- mcp_vector_search/core/config_utils.py +394 -0
- mcp_vector_search/core/connection_pool.py +360 -0
- mcp_vector_search/core/database.py +1237 -0
- mcp_vector_search/core/directory_index.py +318 -0
- mcp_vector_search/core/embeddings.py +294 -0
- mcp_vector_search/core/exceptions.py +89 -0
- mcp_vector_search/core/factory.py +318 -0
- mcp_vector_search/core/git_hooks.py +345 -0
- mcp_vector_search/core/indexer.py +1002 -0
- mcp_vector_search/core/llm_client.py +453 -0
- mcp_vector_search/core/models.py +294 -0
- mcp_vector_search/core/project.py +350 -0
- mcp_vector_search/core/scheduler.py +330 -0
- mcp_vector_search/core/search.py +952 -0
- mcp_vector_search/core/watcher.py +322 -0
- mcp_vector_search/mcp/__init__.py +5 -0
- mcp_vector_search/mcp/__main__.py +25 -0
- mcp_vector_search/mcp/server.py +752 -0
- mcp_vector_search/parsers/__init__.py +8 -0
- mcp_vector_search/parsers/base.py +296 -0
- mcp_vector_search/parsers/dart.py +605 -0
- mcp_vector_search/parsers/html.py +413 -0
- mcp_vector_search/parsers/javascript.py +643 -0
- mcp_vector_search/parsers/php.py +694 -0
- mcp_vector_search/parsers/python.py +502 -0
- mcp_vector_search/parsers/registry.py +223 -0
- mcp_vector_search/parsers/ruby.py +678 -0
- mcp_vector_search/parsers/text.py +186 -0
- mcp_vector_search/parsers/utils.py +265 -0
- mcp_vector_search/py.typed +1 -0
- mcp_vector_search/utils/__init__.py +42 -0
- mcp_vector_search/utils/gitignore.py +250 -0
- mcp_vector_search/utils/gitignore_updater.py +212 -0
- mcp_vector_search/utils/monorepo.py +339 -0
- mcp_vector_search/utils/timing.py +338 -0
- mcp_vector_search/utils/version.py +47 -0
- mcp_vector_search-0.15.7.dist-info/METADATA +884 -0
- mcp_vector_search-0.15.7.dist-info/RECORD +86 -0
- mcp_vector_search-0.15.7.dist-info/WHEEL +4 -0
- mcp_vector_search-0.15.7.dist-info/entry_points.txt +3 -0
- mcp_vector_search-0.15.7.dist-info/licenses/LICENSE +21 -0
|
@@ -0,0 +1,318 @@
|
|
|
1
|
+
"""Component factory for creating commonly used objects."""
|
|
2
|
+
|
|
3
|
+
import functools
|
|
4
|
+
from collections.abc import Callable
|
|
5
|
+
from dataclasses import dataclass
|
|
6
|
+
from pathlib import Path
|
|
7
|
+
from typing import Any, TypeVar
|
|
8
|
+
|
|
9
|
+
import typer
|
|
10
|
+
from loguru import logger
|
|
11
|
+
|
|
12
|
+
from ..cli.output import print_error
|
|
13
|
+
from ..config.settings import ProjectConfig
|
|
14
|
+
from .auto_indexer import AutoIndexer
|
|
15
|
+
from .database import ChromaVectorDatabase, PooledChromaVectorDatabase, VectorDatabase
|
|
16
|
+
from .embeddings import CodeBERTEmbeddingFunction, create_embedding_function
|
|
17
|
+
from .indexer import SemanticIndexer
|
|
18
|
+
from .project import ProjectManager
|
|
19
|
+
from .search import SemanticSearchEngine
|
|
20
|
+
|
|
21
|
+
F = TypeVar("F", bound=Callable[..., Any])
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
@dataclass
|
|
25
|
+
class ComponentBundle:
|
|
26
|
+
"""Bundle of commonly used components."""
|
|
27
|
+
|
|
28
|
+
project_manager: ProjectManager
|
|
29
|
+
config: ProjectConfig
|
|
30
|
+
database: VectorDatabase
|
|
31
|
+
indexer: SemanticIndexer
|
|
32
|
+
embedding_function: CodeBERTEmbeddingFunction
|
|
33
|
+
search_engine: SemanticSearchEngine | None = None
|
|
34
|
+
auto_indexer: AutoIndexer | None = None
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
class ComponentFactory:
|
|
38
|
+
"""Factory for creating commonly used components."""
|
|
39
|
+
|
|
40
|
+
@staticmethod
|
|
41
|
+
def create_project_manager(project_root: Path) -> ProjectManager:
|
|
42
|
+
"""Create a project manager."""
|
|
43
|
+
return ProjectManager(project_root)
|
|
44
|
+
|
|
45
|
+
@staticmethod
|
|
46
|
+
def load_config(project_root: Path) -> tuple[ProjectManager, ProjectConfig]:
|
|
47
|
+
"""Load project configuration."""
|
|
48
|
+
project_manager = ComponentFactory.create_project_manager(project_root)
|
|
49
|
+
config = project_manager.load_config()
|
|
50
|
+
return project_manager, config
|
|
51
|
+
|
|
52
|
+
@staticmethod
|
|
53
|
+
def create_embedding_function(
|
|
54
|
+
model_name: str,
|
|
55
|
+
) -> tuple[CodeBERTEmbeddingFunction, Any]:
|
|
56
|
+
"""Create embedding function."""
|
|
57
|
+
return create_embedding_function(model_name)
|
|
58
|
+
|
|
59
|
+
@staticmethod
|
|
60
|
+
def create_database(
|
|
61
|
+
config: ProjectConfig,
|
|
62
|
+
embedding_function: CodeBERTEmbeddingFunction,
|
|
63
|
+
use_pooling: bool = True, # Enable pooling by default for 13.6% performance boost
|
|
64
|
+
**pool_kwargs,
|
|
65
|
+
) -> VectorDatabase:
|
|
66
|
+
"""Create vector database."""
|
|
67
|
+
if use_pooling:
|
|
68
|
+
# Set default pool parameters if not provided
|
|
69
|
+
pool_defaults = {
|
|
70
|
+
"max_connections": 10,
|
|
71
|
+
"min_connections": 2,
|
|
72
|
+
"max_idle_time": 300.0,
|
|
73
|
+
}
|
|
74
|
+
pool_defaults.update(pool_kwargs)
|
|
75
|
+
|
|
76
|
+
return PooledChromaVectorDatabase(
|
|
77
|
+
persist_directory=config.index_path,
|
|
78
|
+
embedding_function=embedding_function,
|
|
79
|
+
collection_name="code_search",
|
|
80
|
+
**pool_defaults,
|
|
81
|
+
)
|
|
82
|
+
else:
|
|
83
|
+
return ChromaVectorDatabase(
|
|
84
|
+
persist_directory=config.index_path,
|
|
85
|
+
embedding_function=embedding_function,
|
|
86
|
+
collection_name="code_search",
|
|
87
|
+
)
|
|
88
|
+
|
|
89
|
+
@staticmethod
|
|
90
|
+
def create_indexer(
|
|
91
|
+
database: VectorDatabase, project_root: Path, config: ProjectConfig
|
|
92
|
+
) -> SemanticIndexer:
|
|
93
|
+
"""Create semantic indexer."""
|
|
94
|
+
return SemanticIndexer(
|
|
95
|
+
database=database,
|
|
96
|
+
project_root=project_root,
|
|
97
|
+
config=config,
|
|
98
|
+
)
|
|
99
|
+
|
|
100
|
+
@staticmethod
|
|
101
|
+
def create_search_engine(
|
|
102
|
+
database: VectorDatabase,
|
|
103
|
+
project_root: Path,
|
|
104
|
+
similarity_threshold: float = 0.7,
|
|
105
|
+
auto_indexer: AutoIndexer | None = None,
|
|
106
|
+
enable_auto_reindex: bool = True,
|
|
107
|
+
) -> SemanticSearchEngine:
|
|
108
|
+
"""Create semantic search engine."""
|
|
109
|
+
return SemanticSearchEngine(
|
|
110
|
+
database=database,
|
|
111
|
+
project_root=project_root,
|
|
112
|
+
similarity_threshold=similarity_threshold,
|
|
113
|
+
auto_indexer=auto_indexer,
|
|
114
|
+
enable_auto_reindex=enable_auto_reindex,
|
|
115
|
+
)
|
|
116
|
+
|
|
117
|
+
@staticmethod
|
|
118
|
+
def create_auto_indexer(
|
|
119
|
+
indexer: SemanticIndexer,
|
|
120
|
+
database: VectorDatabase,
|
|
121
|
+
auto_reindex_threshold: int = 5,
|
|
122
|
+
staleness_threshold: float = 300.0,
|
|
123
|
+
) -> AutoIndexer:
|
|
124
|
+
"""Create auto-indexer."""
|
|
125
|
+
return AutoIndexer(
|
|
126
|
+
indexer=indexer,
|
|
127
|
+
database=database,
|
|
128
|
+
auto_reindex_threshold=auto_reindex_threshold,
|
|
129
|
+
staleness_threshold=staleness_threshold,
|
|
130
|
+
)
|
|
131
|
+
|
|
132
|
+
@staticmethod
|
|
133
|
+
async def create_standard_components(
|
|
134
|
+
project_root: Path,
|
|
135
|
+
use_pooling: bool = True, # Enable pooling by default for performance
|
|
136
|
+
include_search_engine: bool = False,
|
|
137
|
+
include_auto_indexer: bool = False,
|
|
138
|
+
similarity_threshold: float = 0.7,
|
|
139
|
+
auto_reindex_threshold: int = 5,
|
|
140
|
+
**pool_kwargs,
|
|
141
|
+
) -> ComponentBundle:
|
|
142
|
+
"""Create standard set of components for CLI commands.
|
|
143
|
+
|
|
144
|
+
Args:
|
|
145
|
+
project_root: Project root directory
|
|
146
|
+
use_pooling: Whether to use connection pooling
|
|
147
|
+
include_search_engine: Whether to create search engine
|
|
148
|
+
include_auto_indexer: Whether to create auto-indexer
|
|
149
|
+
similarity_threshold: Default similarity threshold for search
|
|
150
|
+
auto_reindex_threshold: Max files to auto-reindex
|
|
151
|
+
**pool_kwargs: Additional arguments for connection pool
|
|
152
|
+
|
|
153
|
+
Returns:
|
|
154
|
+
ComponentBundle with requested components
|
|
155
|
+
"""
|
|
156
|
+
# Load configuration
|
|
157
|
+
project_manager, config = ComponentFactory.load_config(project_root)
|
|
158
|
+
|
|
159
|
+
# Create embedding function
|
|
160
|
+
embedding_function, _ = ComponentFactory.create_embedding_function(
|
|
161
|
+
config.embedding_model
|
|
162
|
+
)
|
|
163
|
+
|
|
164
|
+
# Create database
|
|
165
|
+
database = ComponentFactory.create_database(
|
|
166
|
+
config=config,
|
|
167
|
+
embedding_function=embedding_function,
|
|
168
|
+
use_pooling=use_pooling,
|
|
169
|
+
**pool_kwargs,
|
|
170
|
+
)
|
|
171
|
+
|
|
172
|
+
# Create indexer
|
|
173
|
+
indexer = ComponentFactory.create_indexer(
|
|
174
|
+
database=database,
|
|
175
|
+
project_root=project_root,
|
|
176
|
+
config=config,
|
|
177
|
+
)
|
|
178
|
+
|
|
179
|
+
# Create optional components
|
|
180
|
+
search_engine = None
|
|
181
|
+
auto_indexer = None
|
|
182
|
+
|
|
183
|
+
if include_auto_indexer:
|
|
184
|
+
auto_indexer = ComponentFactory.create_auto_indexer(
|
|
185
|
+
indexer=indexer,
|
|
186
|
+
database=database,
|
|
187
|
+
auto_reindex_threshold=auto_reindex_threshold,
|
|
188
|
+
)
|
|
189
|
+
|
|
190
|
+
if include_search_engine:
|
|
191
|
+
search_engine = ComponentFactory.create_search_engine(
|
|
192
|
+
database=database,
|
|
193
|
+
project_root=project_root,
|
|
194
|
+
similarity_threshold=similarity_threshold,
|
|
195
|
+
auto_indexer=auto_indexer,
|
|
196
|
+
enable_auto_reindex=include_auto_indexer,
|
|
197
|
+
)
|
|
198
|
+
|
|
199
|
+
return ComponentBundle(
|
|
200
|
+
project_manager=project_manager,
|
|
201
|
+
config=config,
|
|
202
|
+
database=database,
|
|
203
|
+
indexer=indexer,
|
|
204
|
+
embedding_function=embedding_function,
|
|
205
|
+
search_engine=search_engine,
|
|
206
|
+
auto_indexer=auto_indexer,
|
|
207
|
+
)
|
|
208
|
+
|
|
209
|
+
|
|
210
|
+
class DatabaseContext:
|
|
211
|
+
"""Context manager for database lifecycle management."""
|
|
212
|
+
|
|
213
|
+
def __init__(self, database: VectorDatabase):
|
|
214
|
+
"""Initialize database context.
|
|
215
|
+
|
|
216
|
+
Args:
|
|
217
|
+
database: Vector database instance
|
|
218
|
+
"""
|
|
219
|
+
self.database = database
|
|
220
|
+
|
|
221
|
+
async def __aenter__(self) -> VectorDatabase:
|
|
222
|
+
"""Enter context and initialize database."""
|
|
223
|
+
await self.database.initialize()
|
|
224
|
+
return self.database
|
|
225
|
+
|
|
226
|
+
async def __aexit__(self, exc_type, exc_val, exc_tb) -> None:
|
|
227
|
+
"""Exit context and close database."""
|
|
228
|
+
await self.database.close()
|
|
229
|
+
|
|
230
|
+
|
|
231
|
+
def handle_cli_errors(operation_name: str) -> Callable[[F], F]:
|
|
232
|
+
"""Decorator for consistent CLI error handling.
|
|
233
|
+
|
|
234
|
+
Args:
|
|
235
|
+
operation_name: Name of the operation for error messages
|
|
236
|
+
|
|
237
|
+
Returns:
|
|
238
|
+
Decorator function
|
|
239
|
+
"""
|
|
240
|
+
|
|
241
|
+
def decorator(func: F) -> F:
|
|
242
|
+
@functools.wraps(func)
|
|
243
|
+
async def async_wrapper(*args, **kwargs):
|
|
244
|
+
try:
|
|
245
|
+
return await func(*args, **kwargs)
|
|
246
|
+
except Exception as e:
|
|
247
|
+
logger.error(f"{operation_name} failed: {e}")
|
|
248
|
+
print_error(f"{operation_name} failed: {e}")
|
|
249
|
+
raise typer.Exit(1)
|
|
250
|
+
|
|
251
|
+
@functools.wraps(func)
|
|
252
|
+
def sync_wrapper(*args, **kwargs):
|
|
253
|
+
try:
|
|
254
|
+
return func(*args, **kwargs)
|
|
255
|
+
except Exception as e:
|
|
256
|
+
logger.error(f"{operation_name} failed: {e}")
|
|
257
|
+
print_error(f"{operation_name} failed: {e}")
|
|
258
|
+
raise typer.Exit(1)
|
|
259
|
+
|
|
260
|
+
# Return appropriate wrapper based on function type
|
|
261
|
+
if hasattr(func, "__code__") and "await" in func.__code__.co_names:
|
|
262
|
+
return async_wrapper
|
|
263
|
+
else:
|
|
264
|
+
return sync_wrapper
|
|
265
|
+
|
|
266
|
+
return decorator
|
|
267
|
+
|
|
268
|
+
|
|
269
|
+
class ConfigurationService:
|
|
270
|
+
"""Centralized configuration management service."""
|
|
271
|
+
|
|
272
|
+
def __init__(self, project_root: Path):
|
|
273
|
+
"""Initialize configuration service.
|
|
274
|
+
|
|
275
|
+
Args:
|
|
276
|
+
project_root: Project root directory
|
|
277
|
+
"""
|
|
278
|
+
self.project_root = project_root
|
|
279
|
+
self._project_manager: ProjectManager | None = None
|
|
280
|
+
self._config: ProjectConfig | None = None
|
|
281
|
+
|
|
282
|
+
@property
|
|
283
|
+
def project_manager(self) -> ProjectManager:
|
|
284
|
+
"""Get project manager (lazy loaded)."""
|
|
285
|
+
if self._project_manager is None:
|
|
286
|
+
self._project_manager = ProjectManager(self.project_root)
|
|
287
|
+
return self._project_manager
|
|
288
|
+
|
|
289
|
+
@property
|
|
290
|
+
def config(self) -> ProjectConfig:
|
|
291
|
+
"""Get project configuration (lazy loaded)."""
|
|
292
|
+
if self._config is None:
|
|
293
|
+
self._config = self.project_manager.load_config()
|
|
294
|
+
return self._config
|
|
295
|
+
|
|
296
|
+
def ensure_initialized(self) -> bool:
|
|
297
|
+
"""Ensure project is initialized.
|
|
298
|
+
|
|
299
|
+
Returns:
|
|
300
|
+
True if project is initialized, False otherwise
|
|
301
|
+
"""
|
|
302
|
+
if not self.project_manager.is_initialized():
|
|
303
|
+
print_error("Project not initialized. Run 'mcp-vector-search init' first.")
|
|
304
|
+
return False
|
|
305
|
+
return True
|
|
306
|
+
|
|
307
|
+
def reload_config(self) -> None:
|
|
308
|
+
"""Reload configuration from disk."""
|
|
309
|
+
self._config = None
|
|
310
|
+
|
|
311
|
+
def save_config(self, config: ProjectConfig) -> None:
|
|
312
|
+
"""Save configuration to disk.
|
|
313
|
+
|
|
314
|
+
Args:
|
|
315
|
+
config: Configuration to save
|
|
316
|
+
"""
|
|
317
|
+
self.project_manager.save_config(config)
|
|
318
|
+
self._config = config
|
|
@@ -0,0 +1,345 @@
|
|
|
1
|
+
"""Git hooks for automatic reindexing."""
|
|
2
|
+
|
|
3
|
+
import os
|
|
4
|
+
import subprocess
|
|
5
|
+
import sys
|
|
6
|
+
from pathlib import Path
|
|
7
|
+
|
|
8
|
+
from loguru import logger
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
class GitHookManager:
|
|
12
|
+
"""Manages Git hooks for automatic reindexing."""
|
|
13
|
+
|
|
14
|
+
def __init__(self, project_root: Path):
|
|
15
|
+
"""Initialize Git hook manager.
|
|
16
|
+
|
|
17
|
+
Args:
|
|
18
|
+
project_root: Project root directory
|
|
19
|
+
"""
|
|
20
|
+
self.project_root = project_root
|
|
21
|
+
self.git_dir = project_root / ".git"
|
|
22
|
+
self.hooks_dir = self.git_dir / "hooks"
|
|
23
|
+
|
|
24
|
+
def is_git_repo(self) -> bool:
|
|
25
|
+
"""Check if project is a Git repository."""
|
|
26
|
+
return self.git_dir.exists() and self.git_dir.is_dir()
|
|
27
|
+
|
|
28
|
+
def install_hooks(self, hook_types: list[str] | None = None) -> bool:
|
|
29
|
+
"""Install Git hooks for automatic reindexing.
|
|
30
|
+
|
|
31
|
+
Args:
|
|
32
|
+
hook_types: List of hook types to install (default: ['post-commit', 'post-merge'])
|
|
33
|
+
|
|
34
|
+
Returns:
|
|
35
|
+
True if hooks were installed successfully
|
|
36
|
+
"""
|
|
37
|
+
if not self.is_git_repo():
|
|
38
|
+
logger.error("Not a Git repository")
|
|
39
|
+
return False
|
|
40
|
+
|
|
41
|
+
if hook_types is None:
|
|
42
|
+
hook_types = ["post-commit", "post-merge", "post-checkout"]
|
|
43
|
+
|
|
44
|
+
success = True
|
|
45
|
+
for hook_type in hook_types:
|
|
46
|
+
if not self._install_hook(hook_type):
|
|
47
|
+
success = False
|
|
48
|
+
|
|
49
|
+
return success
|
|
50
|
+
|
|
51
|
+
def uninstall_hooks(self, hook_types: list[str] | None = None) -> bool:
|
|
52
|
+
"""Uninstall Git hooks.
|
|
53
|
+
|
|
54
|
+
Args:
|
|
55
|
+
hook_types: List of hook types to uninstall (default: all MCP hooks)
|
|
56
|
+
|
|
57
|
+
Returns:
|
|
58
|
+
True if hooks were uninstalled successfully
|
|
59
|
+
"""
|
|
60
|
+
if hook_types is None:
|
|
61
|
+
hook_types = ["post-commit", "post-merge", "post-checkout"]
|
|
62
|
+
|
|
63
|
+
success = True
|
|
64
|
+
for hook_type in hook_types:
|
|
65
|
+
if not self._uninstall_hook(hook_type):
|
|
66
|
+
success = False
|
|
67
|
+
|
|
68
|
+
return success
|
|
69
|
+
|
|
70
|
+
def _install_hook(self, hook_type: str) -> bool:
|
|
71
|
+
"""Install a specific Git hook."""
|
|
72
|
+
try:
|
|
73
|
+
hook_file = self.hooks_dir / hook_type
|
|
74
|
+
|
|
75
|
+
# Create hooks directory if it doesn't exist
|
|
76
|
+
self.hooks_dir.mkdir(exist_ok=True)
|
|
77
|
+
|
|
78
|
+
# Generate hook script
|
|
79
|
+
hook_script = self._generate_hook_script(hook_type)
|
|
80
|
+
|
|
81
|
+
if hook_file.exists():
|
|
82
|
+
# If hook already exists, try to integrate with it
|
|
83
|
+
return self._integrate_with_existing_hook(hook_file, hook_script)
|
|
84
|
+
else:
|
|
85
|
+
# Create new hook
|
|
86
|
+
hook_file.write_text(hook_script)
|
|
87
|
+
hook_file.chmod(0o755) # Make executable
|
|
88
|
+
logger.info(f"Installed {hook_type} hook")
|
|
89
|
+
return True
|
|
90
|
+
|
|
91
|
+
except Exception as e:
|
|
92
|
+
logger.error(f"Failed to install {hook_type} hook: {e}")
|
|
93
|
+
return False
|
|
94
|
+
|
|
95
|
+
def _uninstall_hook(self, hook_type: str) -> bool:
|
|
96
|
+
"""Uninstall a specific Git hook."""
|
|
97
|
+
try:
|
|
98
|
+
hook_file = self.hooks_dir / hook_type
|
|
99
|
+
|
|
100
|
+
if not hook_file.exists():
|
|
101
|
+
return True # Already uninstalled
|
|
102
|
+
|
|
103
|
+
content = hook_file.read_text()
|
|
104
|
+
|
|
105
|
+
# Check if this is our hook or integrated
|
|
106
|
+
if "# MCP Vector Search Hook" in content:
|
|
107
|
+
if (
|
|
108
|
+
content.strip().startswith("#!/bin/bash")
|
|
109
|
+
and "# MCP Vector Search Hook" in content
|
|
110
|
+
):
|
|
111
|
+
# This is our hook, remove it
|
|
112
|
+
hook_file.unlink()
|
|
113
|
+
logger.info(f"Uninstalled {hook_type} hook")
|
|
114
|
+
else:
|
|
115
|
+
# This is integrated, remove our part
|
|
116
|
+
return self._remove_from_existing_hook(hook_file)
|
|
117
|
+
|
|
118
|
+
return True
|
|
119
|
+
|
|
120
|
+
except Exception as e:
|
|
121
|
+
logger.error(f"Failed to uninstall {hook_type} hook: {e}")
|
|
122
|
+
return False
|
|
123
|
+
|
|
124
|
+
def _generate_hook_script(self, hook_type: str) -> str:
|
|
125
|
+
"""Generate Git hook script."""
|
|
126
|
+
python_path = sys.executable
|
|
127
|
+
project_root = str(self.project_root)
|
|
128
|
+
|
|
129
|
+
script = f"""#!/bin/bash
|
|
130
|
+
# MCP Vector Search Hook - {hook_type}
|
|
131
|
+
# Auto-generated - do not edit manually
|
|
132
|
+
|
|
133
|
+
# Check if mcp-vector-search is available
|
|
134
|
+
if ! command -v mcp-vector-search &> /dev/null; then
|
|
135
|
+
# Try using Python directly
|
|
136
|
+
if [ -f "{python_path}" ]; then
|
|
137
|
+
PYTHON_CMD="{python_path}"
|
|
138
|
+
else
|
|
139
|
+
PYTHON_CMD="python3"
|
|
140
|
+
fi
|
|
141
|
+
|
|
142
|
+
# Try to run via Python module
|
|
143
|
+
if $PYTHON_CMD -m mcp_vector_search --help &> /dev/null; then
|
|
144
|
+
MCP_CMD="$PYTHON_CMD -m mcp_vector_search"
|
|
145
|
+
else
|
|
146
|
+
# Silently exit if not available
|
|
147
|
+
exit 0
|
|
148
|
+
fi
|
|
149
|
+
else
|
|
150
|
+
MCP_CMD="mcp-vector-search"
|
|
151
|
+
fi
|
|
152
|
+
|
|
153
|
+
# Change to project directory
|
|
154
|
+
cd "{project_root}" || exit 0
|
|
155
|
+
|
|
156
|
+
# Run auto-indexing check
|
|
157
|
+
$MCP_CMD auto-index check --auto-reindex --max-files 10 &> /dev/null || true
|
|
158
|
+
|
|
159
|
+
# Exit successfully (don't block Git operations)
|
|
160
|
+
exit 0
|
|
161
|
+
"""
|
|
162
|
+
return script
|
|
163
|
+
|
|
164
|
+
def _integrate_with_existing_hook(self, hook_file: Path, our_script: str) -> bool:
|
|
165
|
+
"""Integrate our hook with an existing hook."""
|
|
166
|
+
try:
|
|
167
|
+
existing_content = hook_file.read_text()
|
|
168
|
+
|
|
169
|
+
# Check if our hook is already integrated
|
|
170
|
+
if "# MCP Vector Search Hook" in existing_content:
|
|
171
|
+
logger.info(f"Hook {hook_file.name} already integrated")
|
|
172
|
+
return True
|
|
173
|
+
|
|
174
|
+
# Add our hook to the end
|
|
175
|
+
integrated_content = existing_content.rstrip() + "\n\n" + our_script
|
|
176
|
+
|
|
177
|
+
# Backup original
|
|
178
|
+
backup_file = hook_file.with_suffix(hook_file.suffix + ".backup")
|
|
179
|
+
backup_file.write_text(existing_content)
|
|
180
|
+
|
|
181
|
+
# Write integrated version
|
|
182
|
+
hook_file.write_text(integrated_content)
|
|
183
|
+
|
|
184
|
+
logger.info(f"Integrated with existing {hook_file.name} hook")
|
|
185
|
+
return True
|
|
186
|
+
|
|
187
|
+
except Exception as e:
|
|
188
|
+
logger.error(f"Failed to integrate with existing hook: {e}")
|
|
189
|
+
return False
|
|
190
|
+
|
|
191
|
+
def _remove_from_existing_hook(self, hook_file: Path) -> bool:
|
|
192
|
+
"""Remove our hook from an existing integrated hook."""
|
|
193
|
+
try:
|
|
194
|
+
content = hook_file.read_text()
|
|
195
|
+
|
|
196
|
+
# Find and remove our section
|
|
197
|
+
lines = content.split("\n")
|
|
198
|
+
new_lines = []
|
|
199
|
+
skip_section = False
|
|
200
|
+
|
|
201
|
+
for line in lines:
|
|
202
|
+
if "# MCP Vector Search Hook" in line:
|
|
203
|
+
skip_section = True
|
|
204
|
+
continue
|
|
205
|
+
elif skip_section and line.strip() == "":
|
|
206
|
+
# End of our section
|
|
207
|
+
skip_section = False
|
|
208
|
+
continue
|
|
209
|
+
elif not skip_section:
|
|
210
|
+
new_lines.append(line)
|
|
211
|
+
|
|
212
|
+
# Write back the cleaned content
|
|
213
|
+
hook_file.write_text("\n".join(new_lines))
|
|
214
|
+
|
|
215
|
+
logger.info(f"Removed MCP hook from {hook_file.name}")
|
|
216
|
+
return True
|
|
217
|
+
|
|
218
|
+
except Exception as e:
|
|
219
|
+
logger.error(f"Failed to remove from existing hook: {e}")
|
|
220
|
+
return False
|
|
221
|
+
|
|
222
|
+
def get_hook_status(self) -> dict:
|
|
223
|
+
"""Get status of Git hooks."""
|
|
224
|
+
if not self.is_git_repo():
|
|
225
|
+
return {"is_git_repo": False}
|
|
226
|
+
|
|
227
|
+
hook_types = ["post-commit", "post-merge", "post-checkout"]
|
|
228
|
+
status = {
|
|
229
|
+
"is_git_repo": True,
|
|
230
|
+
"hooks_dir_exists": self.hooks_dir.exists(),
|
|
231
|
+
"hooks": {},
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
for hook_type in hook_types:
|
|
235
|
+
hook_file = self.hooks_dir / hook_type
|
|
236
|
+
hook_status = {
|
|
237
|
+
"exists": hook_file.exists(),
|
|
238
|
+
"executable": False,
|
|
239
|
+
"has_mcp_hook": False,
|
|
240
|
+
"is_mcp_only": False,
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
if hook_file.exists():
|
|
244
|
+
try:
|
|
245
|
+
hook_status["executable"] = os.access(hook_file, os.X_OK)
|
|
246
|
+
content = hook_file.read_text()
|
|
247
|
+
hook_status["has_mcp_hook"] = "# MCP Vector Search Hook" in content
|
|
248
|
+
hook_status["is_mcp_only"] = (
|
|
249
|
+
hook_status["has_mcp_hook"]
|
|
250
|
+
and content.strip().startswith("#!/bin/bash")
|
|
251
|
+
and content.count("# MCP Vector Search Hook") == 1
|
|
252
|
+
)
|
|
253
|
+
except Exception:
|
|
254
|
+
pass
|
|
255
|
+
|
|
256
|
+
status["hooks"][hook_type] = hook_status
|
|
257
|
+
|
|
258
|
+
return status
|
|
259
|
+
|
|
260
|
+
|
|
261
|
+
class GitChangeDetector:
|
|
262
|
+
"""Detects changed files from Git operations."""
|
|
263
|
+
|
|
264
|
+
@staticmethod
|
|
265
|
+
def get_changed_files_since_commit(
|
|
266
|
+
commit_hash: str, project_root: Path
|
|
267
|
+
) -> set[Path]:
|
|
268
|
+
"""Get files changed since a specific commit.
|
|
269
|
+
|
|
270
|
+
Args:
|
|
271
|
+
commit_hash: Git commit hash
|
|
272
|
+
project_root: Project root directory
|
|
273
|
+
|
|
274
|
+
Returns:
|
|
275
|
+
Set of changed file paths
|
|
276
|
+
"""
|
|
277
|
+
try:
|
|
278
|
+
result = subprocess.run( # nosec B607
|
|
279
|
+
["git", "diff", "--name-only", commit_hash, "HEAD"],
|
|
280
|
+
cwd=project_root,
|
|
281
|
+
capture_output=True,
|
|
282
|
+
text=True,
|
|
283
|
+
check=True,
|
|
284
|
+
)
|
|
285
|
+
|
|
286
|
+
changed_files = set()
|
|
287
|
+
for line in result.stdout.strip().split("\n"):
|
|
288
|
+
if line:
|
|
289
|
+
file_path = project_root / line
|
|
290
|
+
if file_path.exists():
|
|
291
|
+
changed_files.add(file_path)
|
|
292
|
+
|
|
293
|
+
return changed_files
|
|
294
|
+
|
|
295
|
+
except subprocess.CalledProcessError:
|
|
296
|
+
return set()
|
|
297
|
+
|
|
298
|
+
@staticmethod
|
|
299
|
+
def get_changed_files_in_last_commit(project_root: Path) -> set[Path]:
|
|
300
|
+
"""Get files changed in the last commit.
|
|
301
|
+
|
|
302
|
+
Args:
|
|
303
|
+
project_root: Project root directory
|
|
304
|
+
|
|
305
|
+
Returns:
|
|
306
|
+
Set of changed file paths
|
|
307
|
+
"""
|
|
308
|
+
try:
|
|
309
|
+
result = subprocess.run( # nosec B607
|
|
310
|
+
["git", "diff", "--name-only", "HEAD~1", "HEAD"],
|
|
311
|
+
cwd=project_root,
|
|
312
|
+
capture_output=True,
|
|
313
|
+
text=True,
|
|
314
|
+
check=True,
|
|
315
|
+
)
|
|
316
|
+
|
|
317
|
+
changed_files = set()
|
|
318
|
+
for line in result.stdout.strip().split("\n"):
|
|
319
|
+
if line:
|
|
320
|
+
file_path = project_root / line
|
|
321
|
+
if file_path.exists():
|
|
322
|
+
changed_files.add(file_path)
|
|
323
|
+
|
|
324
|
+
return changed_files
|
|
325
|
+
|
|
326
|
+
except subprocess.CalledProcessError:
|
|
327
|
+
return set()
|
|
328
|
+
|
|
329
|
+
@staticmethod
|
|
330
|
+
def should_trigger_reindex(
|
|
331
|
+
changed_files: set[Path], file_extensions: list[str]
|
|
332
|
+
) -> bool:
|
|
333
|
+
"""Check if changed files should trigger reindexing.
|
|
334
|
+
|
|
335
|
+
Args:
|
|
336
|
+
changed_files: Set of changed file paths
|
|
337
|
+
file_extensions: List of file extensions to monitor
|
|
338
|
+
|
|
339
|
+
Returns:
|
|
340
|
+
True if reindexing should be triggered
|
|
341
|
+
"""
|
|
342
|
+
for file_path in changed_files:
|
|
343
|
+
if file_path.suffix in file_extensions:
|
|
344
|
+
return True
|
|
345
|
+
return False
|