mcp-vector-search 0.0.3__py3-none-any.whl → 0.4.12__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 +3 -2
- mcp_vector_search/cli/commands/auto_index.py +397 -0
- mcp_vector_search/cli/commands/config.py +88 -40
- mcp_vector_search/cli/commands/index.py +198 -52
- mcp_vector_search/cli/commands/init.py +471 -58
- mcp_vector_search/cli/commands/install.py +284 -0
- mcp_vector_search/cli/commands/mcp.py +495 -0
- mcp_vector_search/cli/commands/search.py +241 -87
- mcp_vector_search/cli/commands/status.py +184 -58
- mcp_vector_search/cli/commands/watch.py +34 -35
- mcp_vector_search/cli/didyoumean.py +184 -0
- mcp_vector_search/cli/export.py +320 -0
- mcp_vector_search/cli/history.py +292 -0
- mcp_vector_search/cli/interactive.py +342 -0
- mcp_vector_search/cli/main.py +175 -27
- mcp_vector_search/cli/output.py +63 -45
- mcp_vector_search/config/defaults.py +50 -36
- mcp_vector_search/config/settings.py +49 -35
- mcp_vector_search/core/auto_indexer.py +298 -0
- mcp_vector_search/core/connection_pool.py +322 -0
- mcp_vector_search/core/database.py +335 -25
- mcp_vector_search/core/embeddings.py +73 -29
- mcp_vector_search/core/exceptions.py +19 -2
- mcp_vector_search/core/factory.py +310 -0
- mcp_vector_search/core/git_hooks.py +345 -0
- mcp_vector_search/core/indexer.py +237 -73
- mcp_vector_search/core/models.py +21 -19
- mcp_vector_search/core/project.py +73 -58
- mcp_vector_search/core/scheduler.py +330 -0
- mcp_vector_search/core/search.py +574 -86
- mcp_vector_search/core/watcher.py +48 -46
- mcp_vector_search/mcp/__init__.py +4 -0
- mcp_vector_search/mcp/__main__.py +25 -0
- mcp_vector_search/mcp/server.py +701 -0
- mcp_vector_search/parsers/base.py +30 -31
- mcp_vector_search/parsers/javascript.py +74 -48
- mcp_vector_search/parsers/python.py +57 -49
- mcp_vector_search/parsers/registry.py +47 -32
- mcp_vector_search/parsers/text.py +179 -0
- mcp_vector_search/utils/__init__.py +40 -0
- mcp_vector_search/utils/gitignore.py +229 -0
- mcp_vector_search/utils/timing.py +334 -0
- mcp_vector_search/utils/version.py +47 -0
- {mcp_vector_search-0.0.3.dist-info → mcp_vector_search-0.4.12.dist-info}/METADATA +173 -7
- mcp_vector_search-0.4.12.dist-info/RECORD +54 -0
- mcp_vector_search-0.0.3.dist-info/RECORD +0 -35
- {mcp_vector_search-0.0.3.dist-info → mcp_vector_search-0.4.12.dist-info}/WHEEL +0 -0
- {mcp_vector_search-0.0.3.dist-info → mcp_vector_search-0.4.12.dist-info}/entry_points.txt +0 -0
- {mcp_vector_search-0.0.3.dist-info → mcp_vector_search-0.4.12.dist-info}/licenses/LICENSE +0 -0
|
@@ -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(
|
|
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(
|
|
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
|