mcp-vector-search 0.12.6__py3-none-any.whl → 1.1.22__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.
- mcp_vector_search/__init__.py +3 -3
- mcp_vector_search/analysis/__init__.py +111 -0
- mcp_vector_search/analysis/baseline/__init__.py +68 -0
- mcp_vector_search/analysis/baseline/comparator.py +462 -0
- mcp_vector_search/analysis/baseline/manager.py +621 -0
- mcp_vector_search/analysis/collectors/__init__.py +74 -0
- mcp_vector_search/analysis/collectors/base.py +164 -0
- mcp_vector_search/analysis/collectors/cohesion.py +463 -0
- mcp_vector_search/analysis/collectors/complexity.py +743 -0
- mcp_vector_search/analysis/collectors/coupling.py +1162 -0
- mcp_vector_search/analysis/collectors/halstead.py +514 -0
- mcp_vector_search/analysis/collectors/smells.py +325 -0
- mcp_vector_search/analysis/debt.py +516 -0
- mcp_vector_search/analysis/interpretation.py +685 -0
- mcp_vector_search/analysis/metrics.py +414 -0
- mcp_vector_search/analysis/reporters/__init__.py +7 -0
- mcp_vector_search/analysis/reporters/console.py +646 -0
- mcp_vector_search/analysis/reporters/markdown.py +480 -0
- mcp_vector_search/analysis/reporters/sarif.py +377 -0
- mcp_vector_search/analysis/storage/__init__.py +93 -0
- mcp_vector_search/analysis/storage/metrics_store.py +762 -0
- mcp_vector_search/analysis/storage/schema.py +245 -0
- mcp_vector_search/analysis/storage/trend_tracker.py +560 -0
- mcp_vector_search/analysis/trends.py +308 -0
- mcp_vector_search/analysis/visualizer/__init__.py +90 -0
- mcp_vector_search/analysis/visualizer/d3_data.py +534 -0
- mcp_vector_search/analysis/visualizer/exporter.py +484 -0
- mcp_vector_search/analysis/visualizer/html_report.py +2895 -0
- mcp_vector_search/analysis/visualizer/schemas.py +525 -0
- mcp_vector_search/cli/commands/analyze.py +1062 -0
- mcp_vector_search/cli/commands/chat.py +1455 -0
- mcp_vector_search/cli/commands/index.py +621 -5
- mcp_vector_search/cli/commands/index_background.py +467 -0
- mcp_vector_search/cli/commands/init.py +13 -0
- mcp_vector_search/cli/commands/install.py +597 -335
- mcp_vector_search/cli/commands/install_old.py +8 -4
- mcp_vector_search/cli/commands/mcp.py +78 -6
- mcp_vector_search/cli/commands/reset.py +68 -26
- mcp_vector_search/cli/commands/search.py +224 -8
- mcp_vector_search/cli/commands/setup.py +1184 -0
- mcp_vector_search/cli/commands/status.py +339 -5
- mcp_vector_search/cli/commands/uninstall.py +276 -357
- mcp_vector_search/cli/commands/visualize/__init__.py +39 -0
- mcp_vector_search/cli/commands/visualize/cli.py +292 -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 +33 -0
- mcp_vector_search/cli/commands/visualize/graph_builder.py +647 -0
- mcp_vector_search/cli/commands/visualize/layout_engine.py +469 -0
- mcp_vector_search/cli/commands/visualize/server.py +600 -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 +234 -0
- mcp_vector_search/cli/commands/visualize/templates/scripts.py +4542 -0
- mcp_vector_search/cli/commands/visualize/templates/styles.py +2522 -0
- mcp_vector_search/cli/didyoumean.py +27 -2
- mcp_vector_search/cli/main.py +127 -160
- mcp_vector_search/cli/output.py +158 -13
- mcp_vector_search/config/__init__.py +4 -0
- mcp_vector_search/config/default_thresholds.yaml +52 -0
- mcp_vector_search/config/settings.py +12 -0
- mcp_vector_search/config/thresholds.py +273 -0
- mcp_vector_search/core/__init__.py +16 -0
- mcp_vector_search/core/auto_indexer.py +3 -3
- mcp_vector_search/core/boilerplate.py +186 -0
- mcp_vector_search/core/config_utils.py +394 -0
- mcp_vector_search/core/database.py +406 -94
- mcp_vector_search/core/embeddings.py +24 -0
- mcp_vector_search/core/exceptions.py +11 -0
- mcp_vector_search/core/git.py +380 -0
- mcp_vector_search/core/git_hooks.py +4 -4
- mcp_vector_search/core/indexer.py +632 -54
- mcp_vector_search/core/llm_client.py +756 -0
- mcp_vector_search/core/models.py +91 -1
- mcp_vector_search/core/project.py +17 -0
- mcp_vector_search/core/relationships.py +473 -0
- mcp_vector_search/core/scheduler.py +11 -11
- mcp_vector_search/core/search.py +179 -29
- mcp_vector_search/mcp/server.py +819 -9
- mcp_vector_search/parsers/python.py +285 -5
- mcp_vector_search/utils/__init__.py +2 -0
- mcp_vector_search/utils/gitignore.py +0 -3
- mcp_vector_search/utils/gitignore_updater.py +212 -0
- mcp_vector_search/utils/monorepo.py +66 -4
- mcp_vector_search/utils/timing.py +10 -6
- {mcp_vector_search-0.12.6.dist-info → mcp_vector_search-1.1.22.dist-info}/METADATA +184 -53
- mcp_vector_search-1.1.22.dist-info/RECORD +120 -0
- {mcp_vector_search-0.12.6.dist-info → mcp_vector_search-1.1.22.dist-info}/WHEEL +1 -1
- {mcp_vector_search-0.12.6.dist-info → mcp_vector_search-1.1.22.dist-info}/entry_points.txt +1 -0
- mcp_vector_search/cli/commands/visualize.py +0 -1467
- mcp_vector_search-0.12.6.dist-info/RECORD +0 -68
- {mcp_vector_search-0.12.6.dist-info → mcp_vector_search-1.1.22.dist-info}/licenses/LICENSE +0 -0
|
@@ -578,7 +578,8 @@ def demo(
|
|
|
578
578
|
demo_dir.mkdir()
|
|
579
579
|
|
|
580
580
|
# Create sample files
|
|
581
|
-
(demo_dir / "main.py").write_text(
|
|
581
|
+
(demo_dir / "main.py").write_text(
|
|
582
|
+
"""
|
|
582
583
|
def main():
|
|
583
584
|
'''Main entry point for the application.'''
|
|
584
585
|
print("Hello, World!")
|
|
@@ -600,9 +601,11 @@ class UserService:
|
|
|
600
601
|
|
|
601
602
|
if __name__ == "__main__":
|
|
602
603
|
main()
|
|
603
|
-
"""
|
|
604
|
+
"""
|
|
605
|
+
)
|
|
604
606
|
|
|
605
|
-
(demo_dir / "utils.py").write_text(
|
|
607
|
+
(demo_dir / "utils.py").write_text(
|
|
608
|
+
"""
|
|
606
609
|
import json
|
|
607
610
|
from typing import Dict, Any
|
|
608
611
|
|
|
@@ -619,7 +622,8 @@ def hash_password(password: str) -> str:
|
|
|
619
622
|
'''Hash password for secure storage.'''
|
|
620
623
|
import hashlib
|
|
621
624
|
return hashlib.sha256(password.encode()).hexdigest()
|
|
622
|
-
"""
|
|
625
|
+
"""
|
|
626
|
+
)
|
|
623
627
|
|
|
624
628
|
console.print(
|
|
625
629
|
f"\n[bold blue]📁 Created demo project at:[/bold blue] {demo_dir}"
|
|
@@ -38,11 +38,50 @@ Each tool has its own configuration format and location.
|
|
|
38
38
|
3. Test setup: [green]mcp-vector-search mcp test[/green]
|
|
39
39
|
|
|
40
40
|
[dim]Use --force to overwrite existing configurations[/dim]
|
|
41
|
-
"""
|
|
41
|
+
""",
|
|
42
|
+
no_args_is_help=False, # Allow running without subcommand
|
|
43
|
+
invoke_without_command=True, # Call callback even without subcommand
|
|
42
44
|
)
|
|
43
45
|
|
|
44
46
|
console = Console()
|
|
45
47
|
|
|
48
|
+
|
|
49
|
+
@mcp_app.callback()
|
|
50
|
+
def mcp_callback(ctx: typer.Context):
|
|
51
|
+
"""MCP server management.
|
|
52
|
+
|
|
53
|
+
When invoked without a subcommand, starts the MCP server over stdio.
|
|
54
|
+
Use subcommands to configure MCP integration for different AI tools.
|
|
55
|
+
"""
|
|
56
|
+
# Store context for subcommands
|
|
57
|
+
if not ctx.obj:
|
|
58
|
+
ctx.obj = {}
|
|
59
|
+
|
|
60
|
+
# If a subcommand was invoked, let it handle things (check this FIRST)
|
|
61
|
+
if ctx.invoked_subcommand is not None:
|
|
62
|
+
return
|
|
63
|
+
|
|
64
|
+
# No subcommand - start the MCP server
|
|
65
|
+
import asyncio
|
|
66
|
+
from pathlib import Path
|
|
67
|
+
|
|
68
|
+
from ...mcp.server import run_mcp_server
|
|
69
|
+
|
|
70
|
+
project_root = ctx.obj.get("project_root") if ctx.obj else None
|
|
71
|
+
if project_root is None:
|
|
72
|
+
project_root = Path.cwd()
|
|
73
|
+
|
|
74
|
+
# Start the MCP server over stdio
|
|
75
|
+
try:
|
|
76
|
+
asyncio.run(run_mcp_server(project_root))
|
|
77
|
+
raise typer.Exit(0)
|
|
78
|
+
except KeyboardInterrupt:
|
|
79
|
+
raise typer.Exit(0)
|
|
80
|
+
except Exception as e:
|
|
81
|
+
print(f"MCP server error: {e}", file=sys.stderr)
|
|
82
|
+
raise typer.Exit(1)
|
|
83
|
+
|
|
84
|
+
|
|
46
85
|
# Supported AI tools and their configuration details
|
|
47
86
|
SUPPORTED_TOOLS = {
|
|
48
87
|
"auggie": {
|
|
@@ -123,6 +162,29 @@ def get_mcp_server_command(
|
|
|
123
162
|
return f"{python_exe} -m mcp_vector_search.mcp.server{watch_flag} {project_root}"
|
|
124
163
|
|
|
125
164
|
|
|
165
|
+
def detect_install_method() -> tuple[str, list[str]]:
|
|
166
|
+
"""Detect how mcp-vector-search is installed and return appropriate command.
|
|
167
|
+
|
|
168
|
+
Returns:
|
|
169
|
+
Tuple of (command, args) for running mcp-vector-search mcp
|
|
170
|
+
"""
|
|
171
|
+
# Check if we're in a uv-managed environment
|
|
172
|
+
# uv sets UV_PROJECT_ENVIRONMENT or has .venv structure
|
|
173
|
+
if os.environ.get("VIRTUAL_ENV") and ".venv" in os.environ.get("VIRTUAL_ENV", ""):
|
|
174
|
+
# Likely uv project environment
|
|
175
|
+
if shutil.which("uv"):
|
|
176
|
+
return ("uv", ["run", "mcp-vector-search", "mcp"])
|
|
177
|
+
|
|
178
|
+
# Check if mcp-vector-search is directly available in PATH
|
|
179
|
+
mcp_cmd = shutil.which("mcp-vector-search")
|
|
180
|
+
if mcp_cmd:
|
|
181
|
+
# Installed via pipx or pip - use direct command
|
|
182
|
+
return ("mcp-vector-search", ["mcp"])
|
|
183
|
+
|
|
184
|
+
# Fallback to uv run (development mode)
|
|
185
|
+
return ("uv", ["run", "mcp-vector-search", "mcp"])
|
|
186
|
+
|
|
187
|
+
|
|
126
188
|
def get_mcp_server_config_for_tool(
|
|
127
189
|
project_root: Path,
|
|
128
190
|
tool_name: str,
|
|
@@ -130,9 +192,11 @@ def get_mcp_server_config_for_tool(
|
|
|
130
192
|
enable_file_watching: bool = True,
|
|
131
193
|
) -> dict[str, Any]:
|
|
132
194
|
"""Generate MCP server configuration for a specific tool."""
|
|
195
|
+
command, args = detect_install_method()
|
|
196
|
+
|
|
133
197
|
base_config = {
|
|
134
|
-
"command":
|
|
135
|
-
"args":
|
|
198
|
+
"command": command,
|
|
199
|
+
"args": args,
|
|
136
200
|
"env": {
|
|
137
201
|
"MCP_ENABLE_FILE_WATCHING": "true" if enable_file_watching else "false"
|
|
138
202
|
},
|
|
@@ -183,11 +247,12 @@ def create_project_claude_config(
|
|
|
183
247
|
if "mcpServers" not in config:
|
|
184
248
|
config["mcpServers"] = {}
|
|
185
249
|
|
|
186
|
-
#
|
|
250
|
+
# Detect installation method and use appropriate command
|
|
251
|
+
command, args = detect_install_method()
|
|
187
252
|
config["mcpServers"][server_name] = {
|
|
188
253
|
"type": "stdio",
|
|
189
|
-
"command":
|
|
190
|
-
"args":
|
|
254
|
+
"command": command,
|
|
255
|
+
"args": args,
|
|
191
256
|
"env": {
|
|
192
257
|
"MCP_ENABLE_FILE_WATCHING": "true" if enable_file_watching else "false"
|
|
193
258
|
},
|
|
@@ -198,6 +263,13 @@ def create_project_claude_config(
|
|
|
198
263
|
json.dump(config, f, indent=2)
|
|
199
264
|
|
|
200
265
|
print_success("Created project-level .mcp.json with MCP server configuration")
|
|
266
|
+
|
|
267
|
+
# Show which command will be used
|
|
268
|
+
if command == "uv":
|
|
269
|
+
print_info(f"Using uv: {command} {' '.join(args)}")
|
|
270
|
+
else:
|
|
271
|
+
print_info(f"Using direct command: {command} {' '.join(args)}")
|
|
272
|
+
|
|
201
273
|
if enable_file_watching:
|
|
202
274
|
print_info("File watching is enabled for automatic reindexing")
|
|
203
275
|
else:
|
|
@@ -82,27 +82,58 @@ def reset_index(
|
|
|
82
82
|
console.print("[yellow]Reset cancelled[/yellow]")
|
|
83
83
|
raise typer.Exit(0)
|
|
84
84
|
|
|
85
|
-
# Get the database directory
|
|
86
|
-
project_manager.load_config()
|
|
87
|
-
db_path =
|
|
85
|
+
# Get the database directory from config
|
|
86
|
+
config = project_manager.load_config()
|
|
87
|
+
db_path = Path(config.index_path)
|
|
88
|
+
|
|
89
|
+
# Check if index exists (look for chroma.sqlite3 or collection directories)
|
|
90
|
+
has_index = (db_path / "chroma.sqlite3").exists()
|
|
88
91
|
|
|
89
|
-
if not
|
|
92
|
+
if not has_index:
|
|
90
93
|
print_warning("No index found. Nothing to reset.")
|
|
91
94
|
raise typer.Exit(0)
|
|
92
95
|
|
|
96
|
+
# Files/dirs to remove (index data)
|
|
97
|
+
index_files = [
|
|
98
|
+
"chroma.sqlite3",
|
|
99
|
+
"cache",
|
|
100
|
+
"indexing_errors.log",
|
|
101
|
+
"index_metadata.json",
|
|
102
|
+
"directory_index.json",
|
|
103
|
+
]
|
|
104
|
+
|
|
105
|
+
# Also remove any UUID-named directories (ChromaDB collections)
|
|
106
|
+
if db_path.exists():
|
|
107
|
+
for item in db_path.iterdir():
|
|
108
|
+
if item.is_dir() and len(item.name) == 36 and "-" in item.name:
|
|
109
|
+
# Looks like a UUID directory
|
|
110
|
+
index_files.append(item.name)
|
|
111
|
+
|
|
93
112
|
# Create backup if requested
|
|
94
113
|
if backup:
|
|
95
|
-
backup_dir =
|
|
114
|
+
backup_dir = db_path / "backups"
|
|
96
115
|
backup_dir.mkdir(exist_ok=True)
|
|
97
116
|
|
|
98
117
|
import time
|
|
99
118
|
|
|
100
119
|
timestamp = int(time.time())
|
|
101
|
-
backup_path = backup_dir / f"
|
|
120
|
+
backup_path = backup_dir / f"index_backup_{timestamp}"
|
|
121
|
+
backup_path.mkdir(exist_ok=True)
|
|
102
122
|
|
|
103
123
|
try:
|
|
104
|
-
|
|
105
|
-
|
|
124
|
+
backed_up = []
|
|
125
|
+
for file in index_files:
|
|
126
|
+
src = db_path / file
|
|
127
|
+
if src.exists():
|
|
128
|
+
dest = backup_path / file
|
|
129
|
+
if src.is_dir():
|
|
130
|
+
shutil.copytree(src, dest)
|
|
131
|
+
else:
|
|
132
|
+
shutil.copy2(src, dest)
|
|
133
|
+
backed_up.append(file)
|
|
134
|
+
|
|
135
|
+
if backed_up:
|
|
136
|
+
print_success(f"Created backup at: {backup_path.relative_to(root)}")
|
|
106
137
|
except Exception as e:
|
|
107
138
|
print_warning(f"Could not create backup: {e}")
|
|
108
139
|
if not force:
|
|
@@ -110,12 +141,25 @@ def reset_index(
|
|
|
110
141
|
console.print("[yellow]Reset cancelled[/yellow]")
|
|
111
142
|
raise typer.Exit(0)
|
|
112
143
|
|
|
113
|
-
# Clear the index
|
|
144
|
+
# Clear the index files
|
|
114
145
|
console.print("[cyan]Clearing index...[/cyan]")
|
|
146
|
+
removed_count = 0
|
|
115
147
|
try:
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
148
|
+
for file in index_files:
|
|
149
|
+
path = db_path / file
|
|
150
|
+
if path.exists():
|
|
151
|
+
if path.is_dir():
|
|
152
|
+
shutil.rmtree(path)
|
|
153
|
+
else:
|
|
154
|
+
path.unlink()
|
|
155
|
+
removed_count += 1
|
|
156
|
+
|
|
157
|
+
if removed_count > 0:
|
|
158
|
+
print_success(
|
|
159
|
+
f"Index cleared successfully! ({removed_count} items removed)"
|
|
160
|
+
)
|
|
161
|
+
else:
|
|
162
|
+
print_warning("No index files found to remove.")
|
|
119
163
|
except Exception as e:
|
|
120
164
|
print_error(f"Failed to clear index: {e}")
|
|
121
165
|
raise typer.Exit(1)
|
|
@@ -215,19 +259,9 @@ def reset_all(
|
|
|
215
259
|
raise typer.Exit(1)
|
|
216
260
|
|
|
217
261
|
|
|
218
|
-
@reset_app.command("health")
|
|
219
262
|
async def check_health(
|
|
220
|
-
project_root: Path
|
|
221
|
-
|
|
222
|
-
"--project-root",
|
|
223
|
-
"-p",
|
|
224
|
-
help="Project root directory",
|
|
225
|
-
),
|
|
226
|
-
fix: bool = typer.Option(
|
|
227
|
-
False,
|
|
228
|
-
"--fix",
|
|
229
|
-
help="Attempt to fix issues if found",
|
|
230
|
-
),
|
|
263
|
+
project_root: Path,
|
|
264
|
+
fix: bool,
|
|
231
265
|
) -> None:
|
|
232
266
|
"""Check the health of the search index.
|
|
233
267
|
|
|
@@ -254,7 +288,7 @@ async def check_health(
|
|
|
254
288
|
from ...core.embeddings import create_embedding_function
|
|
255
289
|
|
|
256
290
|
config = project_manager.load_config()
|
|
257
|
-
db_path =
|
|
291
|
+
db_path = Path(config.index_path)
|
|
258
292
|
|
|
259
293
|
# Setup embedding function and cache
|
|
260
294
|
cache_dir = get_default_cache_path(root) if config.cache_embeddings else None
|
|
@@ -376,6 +410,7 @@ main = reset_main
|
|
|
376
410
|
|
|
377
411
|
|
|
378
412
|
# Make health check synchronous for CLI
|
|
413
|
+
@reset_app.command("health")
|
|
379
414
|
def health_main(
|
|
380
415
|
project_root: Path = typer.Option(
|
|
381
416
|
None,
|
|
@@ -389,5 +424,12 @@ def health_main(
|
|
|
389
424
|
help="Attempt to fix issues if found",
|
|
390
425
|
),
|
|
391
426
|
) -> None:
|
|
392
|
-
"""Check the health of the search index
|
|
427
|
+
"""Check the health of the search index.
|
|
428
|
+
|
|
429
|
+
This command will:
|
|
430
|
+
- Verify database connectivity
|
|
431
|
+
- Check for index corruption
|
|
432
|
+
- Validate collection integrity
|
|
433
|
+
- Optionally attempt repairs with --fix
|
|
434
|
+
"""
|
|
393
435
|
asyncio.run(check_health(project_root, fix))
|
|
@@ -1,6 +1,8 @@
|
|
|
1
1
|
"""Search command for MCP Vector Search CLI."""
|
|
2
2
|
|
|
3
3
|
import asyncio
|
|
4
|
+
import os
|
|
5
|
+
from fnmatch import fnmatch
|
|
4
6
|
from pathlib import Path
|
|
5
7
|
|
|
6
8
|
import typer
|
|
@@ -62,7 +64,7 @@ def search_main(
|
|
|
62
64
|
None,
|
|
63
65
|
"--files",
|
|
64
66
|
"-f",
|
|
65
|
-
help="Filter by file patterns (e.g., '*.py'
|
|
67
|
+
help="Filter by file glob patterns (e.g., '*.py', 'src/*.js', 'tests/*.ts'). Matches basename or relative path.",
|
|
66
68
|
rich_help_panel="🔍 Filters",
|
|
67
69
|
),
|
|
68
70
|
language: str | None = typer.Option(
|
|
@@ -134,6 +136,41 @@ def search_main(
|
|
|
134
136
|
help="Custom export file path",
|
|
135
137
|
rich_help_panel="💾 Export Options",
|
|
136
138
|
),
|
|
139
|
+
max_complexity: int | None = typer.Option(
|
|
140
|
+
None,
|
|
141
|
+
"--max-complexity",
|
|
142
|
+
help="Filter results with cognitive complexity greater than N",
|
|
143
|
+
min=1,
|
|
144
|
+
rich_help_panel="🎯 Quality Filters",
|
|
145
|
+
),
|
|
146
|
+
no_smells: bool = typer.Option(
|
|
147
|
+
False,
|
|
148
|
+
"--no-smells",
|
|
149
|
+
help="Exclude results with code smells",
|
|
150
|
+
rich_help_panel="🎯 Quality Filters",
|
|
151
|
+
),
|
|
152
|
+
grade: str | None = typer.Option(
|
|
153
|
+
None,
|
|
154
|
+
"--grade",
|
|
155
|
+
help="Filter by complexity grade (e.g., 'A,B,C' or 'A-C')",
|
|
156
|
+
rich_help_panel="🎯 Quality Filters",
|
|
157
|
+
),
|
|
158
|
+
min_quality: int | None = typer.Option(
|
|
159
|
+
None,
|
|
160
|
+
"--min-quality",
|
|
161
|
+
help="Filter by minimum quality score (0-100)",
|
|
162
|
+
min=0,
|
|
163
|
+
max=100,
|
|
164
|
+
rich_help_panel="🎯 Quality Filters",
|
|
165
|
+
),
|
|
166
|
+
quality_weight: float = typer.Option(
|
|
167
|
+
0.3,
|
|
168
|
+
"--quality-weight",
|
|
169
|
+
help="Weight for quality ranking (0.0=pure relevance, 1.0=pure quality, default=0.3)",
|
|
170
|
+
min=0.0,
|
|
171
|
+
max=1.0,
|
|
172
|
+
rich_help_panel="🎯 Quality Filters",
|
|
173
|
+
),
|
|
137
174
|
) -> None:
|
|
138
175
|
"""🔍 Search your codebase semantically.
|
|
139
176
|
|
|
@@ -153,8 +190,10 @@ def search_main(
|
|
|
153
190
|
|
|
154
191
|
[bold cyan]Advanced Search:[/bold cyan]
|
|
155
192
|
|
|
156
|
-
[green]Filter by file pattern:[/green]
|
|
157
|
-
$ mcp-vector-search search "validation" --files "
|
|
193
|
+
[green]Filter by file pattern (glob):[/green]
|
|
194
|
+
$ mcp-vector-search search "validation" --files "*.py"
|
|
195
|
+
$ mcp-vector-search search "component" --files "src/*.tsx"
|
|
196
|
+
$ mcp-vector-search search "test utils" --files "tests/*.ts"
|
|
158
197
|
|
|
159
198
|
[green]Find similar code:[/green]
|
|
160
199
|
$ mcp-vector-search search "src/auth.py" --similar
|
|
@@ -162,6 +201,25 @@ def search_main(
|
|
|
162
201
|
[green]Context-based search:[/green]
|
|
163
202
|
$ mcp-vector-search search "implement rate limiting" --context --focus security
|
|
164
203
|
|
|
204
|
+
[bold cyan]Quality Filters:[/bold cyan]
|
|
205
|
+
|
|
206
|
+
[green]Filter by complexity:[/green]
|
|
207
|
+
$ mcp-vector-search search "authentication" --max-complexity 15
|
|
208
|
+
|
|
209
|
+
[green]Exclude code smells:[/green]
|
|
210
|
+
$ mcp-vector-search search "login" --no-smells
|
|
211
|
+
|
|
212
|
+
[green]Filter by grade:[/green]
|
|
213
|
+
$ mcp-vector-search search "api" --grade A,B
|
|
214
|
+
|
|
215
|
+
[green]Minimum quality score:[/green]
|
|
216
|
+
$ mcp-vector-search search "handler" --min-quality 80
|
|
217
|
+
|
|
218
|
+
[green]Quality-aware ranking:[/green]
|
|
219
|
+
$ mcp-vector-search search "auth" --quality-weight 0.5 # Balance relevance and quality
|
|
220
|
+
$ mcp-vector-search search "api" --quality-weight 0.0 # Pure semantic search
|
|
221
|
+
$ mcp-vector-search search "util" --quality-weight 1.0 # Pure quality ranking
|
|
222
|
+
|
|
165
223
|
[bold cyan]Export Results:[/bold cyan]
|
|
166
224
|
|
|
167
225
|
[green]Export to JSON:[/green]
|
|
@@ -238,6 +296,11 @@ def search_main(
|
|
|
238
296
|
json_output=json_output,
|
|
239
297
|
export_format=export_format,
|
|
240
298
|
export_path=export_path,
|
|
299
|
+
max_complexity=max_complexity,
|
|
300
|
+
no_smells=no_smells,
|
|
301
|
+
grade=grade,
|
|
302
|
+
min_quality=min_quality,
|
|
303
|
+
quality_weight=quality_weight,
|
|
241
304
|
)
|
|
242
305
|
)
|
|
243
306
|
|
|
@@ -247,6 +310,52 @@ def search_main(
|
|
|
247
310
|
raise typer.Exit(1)
|
|
248
311
|
|
|
249
312
|
|
|
313
|
+
def _parse_grade_filter(grade_str: str) -> set[str]:
|
|
314
|
+
"""Parse grade filter string into set of allowed grades.
|
|
315
|
+
|
|
316
|
+
Supports formats:
|
|
317
|
+
- Comma-separated: "A,B,C"
|
|
318
|
+
- Range: "A-C" (expands to A, B, C)
|
|
319
|
+
- Mixed: "A,C-D" (expands to A, C, D)
|
|
320
|
+
|
|
321
|
+
Args:
|
|
322
|
+
grade_str: Grade filter string
|
|
323
|
+
|
|
324
|
+
Returns:
|
|
325
|
+
Set of allowed grade letters
|
|
326
|
+
"""
|
|
327
|
+
allowed_grades = set()
|
|
328
|
+
grade_order = ["A", "B", "C", "D", "F"]
|
|
329
|
+
|
|
330
|
+
# Split by comma
|
|
331
|
+
parts = [part.strip().upper() for part in grade_str.split(",")]
|
|
332
|
+
|
|
333
|
+
for part in parts:
|
|
334
|
+
if "-" in part:
|
|
335
|
+
# Range format (e.g., "A-C")
|
|
336
|
+
start, end = part.split("-", 1)
|
|
337
|
+
start = start.strip()
|
|
338
|
+
end = end.strip()
|
|
339
|
+
|
|
340
|
+
if start in grade_order and end in grade_order:
|
|
341
|
+
start_idx = grade_order.index(start)
|
|
342
|
+
end_idx = grade_order.index(end)
|
|
343
|
+
|
|
344
|
+
# Handle reverse ranges (C-A becomes A-C)
|
|
345
|
+
if start_idx > end_idx:
|
|
346
|
+
start_idx, end_idx = end_idx, start_idx
|
|
347
|
+
|
|
348
|
+
# Add all grades in range
|
|
349
|
+
for grade in grade_order[start_idx : end_idx + 1]:
|
|
350
|
+
allowed_grades.add(grade)
|
|
351
|
+
else:
|
|
352
|
+
# Single grade
|
|
353
|
+
if part in grade_order:
|
|
354
|
+
allowed_grades.add(part)
|
|
355
|
+
|
|
356
|
+
return allowed_grades
|
|
357
|
+
|
|
358
|
+
|
|
250
359
|
async def run_search(
|
|
251
360
|
project_root: Path,
|
|
252
361
|
query: str,
|
|
@@ -260,8 +369,13 @@ async def run_search(
|
|
|
260
369
|
json_output: bool = False,
|
|
261
370
|
export_format: str | None = None,
|
|
262
371
|
export_path: Path | None = None,
|
|
372
|
+
max_complexity: int | None = None,
|
|
373
|
+
no_smells: bool = False,
|
|
374
|
+
grade: str | None = None,
|
|
375
|
+
min_quality: int | None = None,
|
|
376
|
+
quality_weight: float = 0.3,
|
|
263
377
|
) -> None:
|
|
264
|
-
"""Run semantic search."""
|
|
378
|
+
"""Run semantic search with optional quality filters and quality-aware ranking."""
|
|
265
379
|
# Load project configuration
|
|
266
380
|
project_manager = ProjectManager(project_root)
|
|
267
381
|
|
|
@@ -326,7 +440,7 @@ async def run_search(
|
|
|
326
440
|
similarity_threshold=similarity_threshold or config.similarity_threshold,
|
|
327
441
|
)
|
|
328
442
|
|
|
329
|
-
# Build filters
|
|
443
|
+
# Build filters (exclude file_path - will be handled with post-filtering)
|
|
330
444
|
filters = {}
|
|
331
445
|
if language:
|
|
332
446
|
filters["language"] = language
|
|
@@ -334,9 +448,6 @@ async def run_search(
|
|
|
334
448
|
filters["function_name"] = function_name
|
|
335
449
|
if class_name:
|
|
336
450
|
filters["class_name"] = class_name
|
|
337
|
-
if files:
|
|
338
|
-
# Simple file pattern matching (could be enhanced)
|
|
339
|
-
filters["file_path"] = files
|
|
340
451
|
|
|
341
452
|
try:
|
|
342
453
|
async with database:
|
|
@@ -348,6 +459,110 @@ async def run_search(
|
|
|
348
459
|
include_context=show_content,
|
|
349
460
|
)
|
|
350
461
|
|
|
462
|
+
# Post-filter results by file pattern if specified
|
|
463
|
+
if files and results:
|
|
464
|
+
filtered_results = []
|
|
465
|
+
for result in results:
|
|
466
|
+
# Get relative path from project root
|
|
467
|
+
try:
|
|
468
|
+
rel_path = str(result.file_path.relative_to(project_root))
|
|
469
|
+
except ValueError:
|
|
470
|
+
# If file is outside project root, use absolute path
|
|
471
|
+
rel_path = str(result.file_path)
|
|
472
|
+
|
|
473
|
+
# Match against glob pattern (both full path and basename)
|
|
474
|
+
if fnmatch(rel_path, files) or fnmatch(
|
|
475
|
+
os.path.basename(rel_path), files
|
|
476
|
+
):
|
|
477
|
+
filtered_results.append(result)
|
|
478
|
+
|
|
479
|
+
results = filtered_results
|
|
480
|
+
logger.debug(
|
|
481
|
+
f"File pattern '{files}' filtered results to {len(results)} matches"
|
|
482
|
+
)
|
|
483
|
+
|
|
484
|
+
# Apply quality filters if specified
|
|
485
|
+
if any([max_complexity, no_smells, grade, min_quality]) and results:
|
|
486
|
+
filtered_results = []
|
|
487
|
+
for result in results:
|
|
488
|
+
# Parse quality metrics from result metadata
|
|
489
|
+
cognitive_complexity = getattr(result, "cognitive_complexity", None)
|
|
490
|
+
complexity_grade = getattr(result, "complexity_grade", None)
|
|
491
|
+
smell_count = getattr(result, "smell_count", None)
|
|
492
|
+
quality_score = getattr(result, "quality_score", None)
|
|
493
|
+
|
|
494
|
+
# Filter by max complexity
|
|
495
|
+
if max_complexity is not None and cognitive_complexity is not None:
|
|
496
|
+
if cognitive_complexity > max_complexity:
|
|
497
|
+
continue
|
|
498
|
+
|
|
499
|
+
# Filter by code smells
|
|
500
|
+
if no_smells and smell_count is not None:
|
|
501
|
+
if smell_count > 0:
|
|
502
|
+
continue
|
|
503
|
+
|
|
504
|
+
# Filter by grade
|
|
505
|
+
if grade and complexity_grade:
|
|
506
|
+
allowed_grades = _parse_grade_filter(grade)
|
|
507
|
+
if complexity_grade not in allowed_grades:
|
|
508
|
+
continue
|
|
509
|
+
|
|
510
|
+
# Filter by minimum quality score
|
|
511
|
+
if min_quality is not None and quality_score is not None:
|
|
512
|
+
if quality_score < min_quality:
|
|
513
|
+
continue
|
|
514
|
+
|
|
515
|
+
filtered_results.append(result)
|
|
516
|
+
|
|
517
|
+
initial_count = len(results)
|
|
518
|
+
results = filtered_results
|
|
519
|
+
logger.debug(
|
|
520
|
+
f"Quality filters reduced results from {initial_count} to {len(results)}"
|
|
521
|
+
)
|
|
522
|
+
|
|
523
|
+
# Apply quality-aware ranking if quality_weight > 0 and results have quality metrics
|
|
524
|
+
if quality_weight > 0.0 and results:
|
|
525
|
+
# Calculate quality scores for results that don't have them
|
|
526
|
+
for result in results:
|
|
527
|
+
if result.quality_score is None:
|
|
528
|
+
# Calculate quality score using the formula
|
|
529
|
+
calculated_score = result.calculate_quality_score()
|
|
530
|
+
if calculated_score is not None:
|
|
531
|
+
result.quality_score = calculated_score
|
|
532
|
+
|
|
533
|
+
# Re-rank results based on combined score
|
|
534
|
+
# Store original similarity score for display
|
|
535
|
+
for result in results:
|
|
536
|
+
# Store original relevance score
|
|
537
|
+
if not hasattr(result, "_original_similarity"):
|
|
538
|
+
result._original_similarity = result.similarity_score
|
|
539
|
+
|
|
540
|
+
# Calculate combined score
|
|
541
|
+
if result.quality_score is not None:
|
|
542
|
+
# Normalize quality score to 0-1 range (it's 0-100)
|
|
543
|
+
normalized_quality = result.quality_score / 100.0
|
|
544
|
+
|
|
545
|
+
# Combined score: (1-W) × relevance + W × quality
|
|
546
|
+
combined_score = (
|
|
547
|
+
(1.0 - quality_weight) * result.similarity_score
|
|
548
|
+
+ quality_weight * normalized_quality
|
|
549
|
+
)
|
|
550
|
+
|
|
551
|
+
# Update similarity_score with combined score for sorting
|
|
552
|
+
result.similarity_score = combined_score
|
|
553
|
+
# If no quality score, keep original similarity_score
|
|
554
|
+
|
|
555
|
+
# Re-sort by combined score
|
|
556
|
+
results.sort(key=lambda r: r.similarity_score, reverse=True)
|
|
557
|
+
|
|
558
|
+
# Update ranks
|
|
559
|
+
for i, result in enumerate(results):
|
|
560
|
+
result.rank = i + 1
|
|
561
|
+
|
|
562
|
+
logger.debug(
|
|
563
|
+
f"Quality-aware ranking applied with weight {quality_weight:.2f}"
|
|
564
|
+
)
|
|
565
|
+
|
|
351
566
|
# Handle export if requested
|
|
352
567
|
if export_format:
|
|
353
568
|
from ..export import SearchResultExporter, get_export_path
|
|
@@ -401,6 +616,7 @@ async def run_search(
|
|
|
401
616
|
results=results,
|
|
402
617
|
query=query,
|
|
403
618
|
show_content=show_content,
|
|
619
|
+
quality_weight=quality_weight,
|
|
404
620
|
)
|
|
405
621
|
|
|
406
622
|
# Add contextual tips based on results
|