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,796 @@
|
|
|
1
|
+
"""Search command for MCP Vector Search CLI."""
|
|
2
|
+
|
|
3
|
+
import asyncio
|
|
4
|
+
import os
|
|
5
|
+
from fnmatch import fnmatch
|
|
6
|
+
from pathlib import Path
|
|
7
|
+
|
|
8
|
+
import typer
|
|
9
|
+
from loguru import logger
|
|
10
|
+
|
|
11
|
+
from ...core.database import ChromaVectorDatabase
|
|
12
|
+
from ...core.embeddings import create_embedding_function
|
|
13
|
+
from ...core.exceptions import ProjectNotFoundError
|
|
14
|
+
from ...core.indexer import SemanticIndexer
|
|
15
|
+
from ...core.project import ProjectManager
|
|
16
|
+
from ...core.search import SemanticSearchEngine
|
|
17
|
+
from ..didyoumean import create_enhanced_typer
|
|
18
|
+
from ..output import (
|
|
19
|
+
print_error,
|
|
20
|
+
print_info,
|
|
21
|
+
print_search_results,
|
|
22
|
+
print_tip,
|
|
23
|
+
)
|
|
24
|
+
|
|
25
|
+
# Create search subcommand app with "did you mean" functionality
|
|
26
|
+
search_app = create_enhanced_typer(
|
|
27
|
+
help="🔍 Search code semantically",
|
|
28
|
+
invoke_without_command=True,
|
|
29
|
+
)
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
# Define search_main as the callback for the search command
|
|
33
|
+
# This makes `mcp-vector-search search "query"` work as main search
|
|
34
|
+
# and `mcp-vector-search search SUBCOMMAND` work for subcommands
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
@search_app.callback(invoke_without_command=True)
|
|
38
|
+
def search_main(
|
|
39
|
+
ctx: typer.Context,
|
|
40
|
+
query: str | None = typer.Argument(
|
|
41
|
+
None, help="Search query or file path (for --similar)"
|
|
42
|
+
),
|
|
43
|
+
project_root: Path | None = typer.Option(
|
|
44
|
+
None,
|
|
45
|
+
"--project-root",
|
|
46
|
+
"-p",
|
|
47
|
+
help="Project root directory (auto-detected if not specified)",
|
|
48
|
+
exists=True,
|
|
49
|
+
file_okay=False,
|
|
50
|
+
dir_okay=True,
|
|
51
|
+
readable=True,
|
|
52
|
+
rich_help_panel="🔧 Global Options",
|
|
53
|
+
),
|
|
54
|
+
limit: int = typer.Option(
|
|
55
|
+
10,
|
|
56
|
+
"--limit",
|
|
57
|
+
"-l",
|
|
58
|
+
help="Maximum number of results to return",
|
|
59
|
+
min=1,
|
|
60
|
+
max=100,
|
|
61
|
+
rich_help_panel="📊 Result Options",
|
|
62
|
+
),
|
|
63
|
+
files: str | None = typer.Option(
|
|
64
|
+
None,
|
|
65
|
+
"--files",
|
|
66
|
+
"-f",
|
|
67
|
+
help="Filter by file glob patterns (e.g., '*.py', 'src/*.js', 'tests/*.ts'). Matches basename or relative path.",
|
|
68
|
+
rich_help_panel="🔍 Filters",
|
|
69
|
+
),
|
|
70
|
+
language: str | None = typer.Option(
|
|
71
|
+
None,
|
|
72
|
+
"--language",
|
|
73
|
+
help="Filter by programming language (python, javascript, typescript)",
|
|
74
|
+
rich_help_panel="🔍 Filters",
|
|
75
|
+
),
|
|
76
|
+
function_name: str | None = typer.Option(
|
|
77
|
+
None,
|
|
78
|
+
"--function",
|
|
79
|
+
help="Filter by function name",
|
|
80
|
+
rich_help_panel="🔍 Filters",
|
|
81
|
+
),
|
|
82
|
+
class_name: str | None = typer.Option(
|
|
83
|
+
None,
|
|
84
|
+
"--class",
|
|
85
|
+
help="Filter by class name",
|
|
86
|
+
rich_help_panel="🔍 Filters",
|
|
87
|
+
),
|
|
88
|
+
similarity_threshold: float | None = typer.Option(
|
|
89
|
+
None,
|
|
90
|
+
"--threshold",
|
|
91
|
+
"-t",
|
|
92
|
+
help="Minimum similarity threshold (0.0 to 1.0)",
|
|
93
|
+
min=0.0,
|
|
94
|
+
max=1.0,
|
|
95
|
+
rich_help_panel="🎯 Search Options",
|
|
96
|
+
),
|
|
97
|
+
similar: bool = typer.Option(
|
|
98
|
+
False,
|
|
99
|
+
"--similar",
|
|
100
|
+
help="Find code similar to the query (treats query as file path)",
|
|
101
|
+
rich_help_panel="🎯 Search Options",
|
|
102
|
+
),
|
|
103
|
+
context: bool = typer.Option(
|
|
104
|
+
False,
|
|
105
|
+
"--context",
|
|
106
|
+
help="Search for code based on contextual description",
|
|
107
|
+
rich_help_panel="🎯 Search Options",
|
|
108
|
+
),
|
|
109
|
+
focus: str | None = typer.Option(
|
|
110
|
+
None,
|
|
111
|
+
"--focus",
|
|
112
|
+
help="Focus areas for context search (comma-separated)",
|
|
113
|
+
rich_help_panel="🎯 Search Options",
|
|
114
|
+
),
|
|
115
|
+
no_content: bool = typer.Option(
|
|
116
|
+
False,
|
|
117
|
+
"--no-content",
|
|
118
|
+
help="Don't show code content in results",
|
|
119
|
+
rich_help_panel="📊 Result Options",
|
|
120
|
+
),
|
|
121
|
+
json_output: bool = typer.Option(
|
|
122
|
+
False,
|
|
123
|
+
"--json",
|
|
124
|
+
help="Output results in JSON format",
|
|
125
|
+
rich_help_panel="📊 Result Options",
|
|
126
|
+
),
|
|
127
|
+
export_format: str | None = typer.Option(
|
|
128
|
+
None,
|
|
129
|
+
"--export",
|
|
130
|
+
help="Export results to file (json, csv, markdown, summary)",
|
|
131
|
+
rich_help_panel="💾 Export Options",
|
|
132
|
+
),
|
|
133
|
+
export_path: Path | None = typer.Option(
|
|
134
|
+
None,
|
|
135
|
+
"--export-path",
|
|
136
|
+
help="Custom export file path",
|
|
137
|
+
rich_help_panel="💾 Export Options",
|
|
138
|
+
),
|
|
139
|
+
) -> None:
|
|
140
|
+
"""🔍 Search your codebase semantically.
|
|
141
|
+
|
|
142
|
+
Performs vector similarity search across your indexed code to find relevant
|
|
143
|
+
functions, classes, and patterns based on semantic meaning, not just keywords.
|
|
144
|
+
|
|
145
|
+
[bold cyan]Basic Search Examples:[/bold cyan]
|
|
146
|
+
|
|
147
|
+
[green]Simple semantic search:[/green]
|
|
148
|
+
$ mcp-vector-search search "authentication middleware"
|
|
149
|
+
|
|
150
|
+
[green]Search with language filter:[/green]
|
|
151
|
+
$ mcp-vector-search search "database connection" --language python
|
|
152
|
+
|
|
153
|
+
[green]Limit results:[/green]
|
|
154
|
+
$ mcp-vector-search search "error handling" --limit 5
|
|
155
|
+
|
|
156
|
+
[bold cyan]Advanced Search:[/bold cyan]
|
|
157
|
+
|
|
158
|
+
[green]Filter by file pattern (glob):[/green]
|
|
159
|
+
$ mcp-vector-search search "validation" --files "*.py"
|
|
160
|
+
$ mcp-vector-search search "component" --files "src/*.tsx"
|
|
161
|
+
$ mcp-vector-search search "test utils" --files "tests/*.ts"
|
|
162
|
+
|
|
163
|
+
[green]Find similar code:[/green]
|
|
164
|
+
$ mcp-vector-search search "src/auth.py" --similar
|
|
165
|
+
|
|
166
|
+
[green]Context-based search:[/green]
|
|
167
|
+
$ mcp-vector-search search "implement rate limiting" --context --focus security
|
|
168
|
+
|
|
169
|
+
[bold cyan]Export Results:[/bold cyan]
|
|
170
|
+
|
|
171
|
+
[green]Export to JSON:[/green]
|
|
172
|
+
$ mcp-vector-search search "api endpoints" --export json
|
|
173
|
+
|
|
174
|
+
[green]Export to markdown:[/green]
|
|
175
|
+
$ mcp-vector-search search "utils" --export markdown
|
|
176
|
+
|
|
177
|
+
[dim]💡 Tip: Use quotes for multi-word queries. Adjust --threshold for more/fewer results.[/dim]
|
|
178
|
+
"""
|
|
179
|
+
# If no query provided and no subcommand invoked, exit (show help)
|
|
180
|
+
if query is None:
|
|
181
|
+
if ctx.invoked_subcommand is None:
|
|
182
|
+
# No query and no subcommand - show help
|
|
183
|
+
raise typer.Exit()
|
|
184
|
+
else:
|
|
185
|
+
# A subcommand was invoked - let it handle the request
|
|
186
|
+
return
|
|
187
|
+
|
|
188
|
+
try:
|
|
189
|
+
project_root = project_root or ctx.obj.get("project_root") or Path.cwd()
|
|
190
|
+
|
|
191
|
+
# Validate mutually exclusive options
|
|
192
|
+
if similar and context:
|
|
193
|
+
print_error("Cannot use both --similar and --context flags together")
|
|
194
|
+
raise typer.Exit(1)
|
|
195
|
+
|
|
196
|
+
# Route to appropriate search function
|
|
197
|
+
if similar:
|
|
198
|
+
# Similar search - treat query as file path
|
|
199
|
+
file_path = Path(query)
|
|
200
|
+
if not file_path.exists():
|
|
201
|
+
print_error(f"File not found: {query}")
|
|
202
|
+
raise typer.Exit(1)
|
|
203
|
+
|
|
204
|
+
asyncio.run(
|
|
205
|
+
run_similar_search(
|
|
206
|
+
project_root=project_root,
|
|
207
|
+
file_path=file_path,
|
|
208
|
+
function_name=function_name,
|
|
209
|
+
limit=limit,
|
|
210
|
+
similarity_threshold=similarity_threshold,
|
|
211
|
+
json_output=json_output,
|
|
212
|
+
)
|
|
213
|
+
)
|
|
214
|
+
elif context:
|
|
215
|
+
# Context search
|
|
216
|
+
focus_areas = None
|
|
217
|
+
if focus:
|
|
218
|
+
focus_areas = [area.strip() for area in focus.split(",")]
|
|
219
|
+
|
|
220
|
+
asyncio.run(
|
|
221
|
+
run_context_search(
|
|
222
|
+
project_root=project_root,
|
|
223
|
+
description=query,
|
|
224
|
+
focus_areas=focus_areas,
|
|
225
|
+
limit=limit,
|
|
226
|
+
json_output=json_output,
|
|
227
|
+
)
|
|
228
|
+
)
|
|
229
|
+
else:
|
|
230
|
+
# Default semantic search
|
|
231
|
+
asyncio.run(
|
|
232
|
+
run_search(
|
|
233
|
+
project_root=project_root,
|
|
234
|
+
query=query,
|
|
235
|
+
limit=limit,
|
|
236
|
+
files=files,
|
|
237
|
+
language=language,
|
|
238
|
+
function_name=function_name,
|
|
239
|
+
class_name=class_name,
|
|
240
|
+
similarity_threshold=similarity_threshold,
|
|
241
|
+
show_content=not no_content,
|
|
242
|
+
json_output=json_output,
|
|
243
|
+
export_format=export_format,
|
|
244
|
+
export_path=export_path,
|
|
245
|
+
)
|
|
246
|
+
)
|
|
247
|
+
|
|
248
|
+
except Exception as e:
|
|
249
|
+
logger.error(f"Search failed: {e}")
|
|
250
|
+
print_error(f"Search failed: {e}")
|
|
251
|
+
raise typer.Exit(1)
|
|
252
|
+
|
|
253
|
+
|
|
254
|
+
async def run_search(
|
|
255
|
+
project_root: Path,
|
|
256
|
+
query: str,
|
|
257
|
+
limit: int = 10,
|
|
258
|
+
files: str | None = None,
|
|
259
|
+
language: str | None = None,
|
|
260
|
+
function_name: str | None = None,
|
|
261
|
+
class_name: str | None = None,
|
|
262
|
+
similarity_threshold: float | None = None,
|
|
263
|
+
show_content: bool = True,
|
|
264
|
+
json_output: bool = False,
|
|
265
|
+
export_format: str | None = None,
|
|
266
|
+
export_path: Path | None = None,
|
|
267
|
+
) -> None:
|
|
268
|
+
"""Run semantic search."""
|
|
269
|
+
# Load project configuration
|
|
270
|
+
project_manager = ProjectManager(project_root)
|
|
271
|
+
|
|
272
|
+
if not project_manager.is_initialized():
|
|
273
|
+
raise ProjectNotFoundError(
|
|
274
|
+
f"Project not initialized at {project_root}. Run 'mcp-vector-search init' first."
|
|
275
|
+
)
|
|
276
|
+
|
|
277
|
+
config = project_manager.load_config()
|
|
278
|
+
|
|
279
|
+
# Setup database and search engine
|
|
280
|
+
embedding_function, _ = create_embedding_function(config.embedding_model)
|
|
281
|
+
database = ChromaVectorDatabase(
|
|
282
|
+
persist_directory=config.index_path,
|
|
283
|
+
embedding_function=embedding_function,
|
|
284
|
+
)
|
|
285
|
+
|
|
286
|
+
# Create indexer for version check
|
|
287
|
+
indexer = SemanticIndexer(
|
|
288
|
+
database=database,
|
|
289
|
+
project_root=project_root,
|
|
290
|
+
config=config,
|
|
291
|
+
)
|
|
292
|
+
|
|
293
|
+
# Check if reindex is needed due to version upgrade
|
|
294
|
+
if config.auto_reindex_on_upgrade and indexer.needs_reindex_for_version():
|
|
295
|
+
from ..output import console
|
|
296
|
+
|
|
297
|
+
index_version = indexer.get_index_version()
|
|
298
|
+
from ... import __version__
|
|
299
|
+
|
|
300
|
+
if index_version:
|
|
301
|
+
console.print(
|
|
302
|
+
f"[yellow]⚠️ Index created with version {index_version} (current: {__version__})[/yellow]"
|
|
303
|
+
)
|
|
304
|
+
else:
|
|
305
|
+
console.print(
|
|
306
|
+
"[yellow]⚠️ Index version not found (legacy format detected)[/yellow]"
|
|
307
|
+
)
|
|
308
|
+
|
|
309
|
+
console.print(
|
|
310
|
+
"[yellow] Reindexing to take advantage of improvements...[/yellow]"
|
|
311
|
+
)
|
|
312
|
+
|
|
313
|
+
# Auto-reindex with progress
|
|
314
|
+
try:
|
|
315
|
+
indexed_count = await indexer.index_project(
|
|
316
|
+
force_reindex=True, show_progress=False
|
|
317
|
+
)
|
|
318
|
+
console.print(
|
|
319
|
+
f"[green]✓ Index updated to version {__version__} ({indexed_count} files reindexed)[/green]\n"
|
|
320
|
+
)
|
|
321
|
+
except Exception as e:
|
|
322
|
+
console.print(f"[red]✗ Reindexing failed: {e}[/red]")
|
|
323
|
+
console.print(
|
|
324
|
+
"[yellow] Continuing with existing index (may have outdated patterns)[/yellow]\n"
|
|
325
|
+
)
|
|
326
|
+
|
|
327
|
+
search_engine = SemanticSearchEngine(
|
|
328
|
+
database=database,
|
|
329
|
+
project_root=project_root,
|
|
330
|
+
similarity_threshold=similarity_threshold or config.similarity_threshold,
|
|
331
|
+
)
|
|
332
|
+
|
|
333
|
+
# Build filters (exclude file_path - will be handled with post-filtering)
|
|
334
|
+
filters = {}
|
|
335
|
+
if language:
|
|
336
|
+
filters["language"] = language
|
|
337
|
+
if function_name:
|
|
338
|
+
filters["function_name"] = function_name
|
|
339
|
+
if class_name:
|
|
340
|
+
filters["class_name"] = class_name
|
|
341
|
+
|
|
342
|
+
try:
|
|
343
|
+
async with database:
|
|
344
|
+
results = await search_engine.search(
|
|
345
|
+
query=query,
|
|
346
|
+
limit=limit,
|
|
347
|
+
filters=filters if filters else None,
|
|
348
|
+
similarity_threshold=similarity_threshold,
|
|
349
|
+
include_context=show_content,
|
|
350
|
+
)
|
|
351
|
+
|
|
352
|
+
# Post-filter results by file pattern if specified
|
|
353
|
+
if files and results:
|
|
354
|
+
filtered_results = []
|
|
355
|
+
for result in results:
|
|
356
|
+
# Get relative path from project root
|
|
357
|
+
try:
|
|
358
|
+
rel_path = str(result.file_path.relative_to(project_root))
|
|
359
|
+
except ValueError:
|
|
360
|
+
# If file is outside project root, use absolute path
|
|
361
|
+
rel_path = str(result.file_path)
|
|
362
|
+
|
|
363
|
+
# Match against glob pattern (both full path and basename)
|
|
364
|
+
if fnmatch(rel_path, files) or fnmatch(
|
|
365
|
+
os.path.basename(rel_path), files
|
|
366
|
+
):
|
|
367
|
+
filtered_results.append(result)
|
|
368
|
+
|
|
369
|
+
results = filtered_results
|
|
370
|
+
logger.debug(
|
|
371
|
+
f"File pattern '{files}' filtered results to {len(results)} matches"
|
|
372
|
+
)
|
|
373
|
+
|
|
374
|
+
# Handle export if requested
|
|
375
|
+
if export_format:
|
|
376
|
+
from ..export import SearchResultExporter, get_export_path
|
|
377
|
+
|
|
378
|
+
exporter = SearchResultExporter()
|
|
379
|
+
|
|
380
|
+
# Determine export path
|
|
381
|
+
if export_path:
|
|
382
|
+
output_path = export_path
|
|
383
|
+
else:
|
|
384
|
+
output_path = get_export_path(export_format, query, project_root)
|
|
385
|
+
|
|
386
|
+
# Export based on format
|
|
387
|
+
success = False
|
|
388
|
+
if export_format == "json":
|
|
389
|
+
success = exporter.export_to_json(results, output_path, query)
|
|
390
|
+
elif export_format == "csv":
|
|
391
|
+
success = exporter.export_to_csv(results, output_path, query)
|
|
392
|
+
elif export_format == "markdown":
|
|
393
|
+
success = exporter.export_to_markdown(
|
|
394
|
+
results, output_path, query, show_content
|
|
395
|
+
)
|
|
396
|
+
elif export_format == "summary":
|
|
397
|
+
success = exporter.export_summary_table(results, output_path, query)
|
|
398
|
+
else:
|
|
399
|
+
from ..output import print_error
|
|
400
|
+
|
|
401
|
+
print_error(f"Unsupported export format: {export_format}")
|
|
402
|
+
|
|
403
|
+
if not success:
|
|
404
|
+
return
|
|
405
|
+
|
|
406
|
+
# Save to search history
|
|
407
|
+
from ..history import SearchHistory
|
|
408
|
+
|
|
409
|
+
history_manager = SearchHistory(project_root)
|
|
410
|
+
history_manager.add_search(
|
|
411
|
+
query=query,
|
|
412
|
+
results_count=len(results),
|
|
413
|
+
filters=filters if filters else None,
|
|
414
|
+
)
|
|
415
|
+
|
|
416
|
+
# Display results
|
|
417
|
+
if json_output:
|
|
418
|
+
from ..output import print_json
|
|
419
|
+
|
|
420
|
+
results_data = [result.to_dict() for result in results]
|
|
421
|
+
print_json(results_data, title="Search Results")
|
|
422
|
+
else:
|
|
423
|
+
print_search_results(
|
|
424
|
+
results=results,
|
|
425
|
+
query=query,
|
|
426
|
+
show_content=show_content,
|
|
427
|
+
)
|
|
428
|
+
|
|
429
|
+
# Add contextual tips based on results
|
|
430
|
+
if results:
|
|
431
|
+
if len(results) >= limit:
|
|
432
|
+
print_tip(
|
|
433
|
+
f"More results may be available. Use [cyan]--limit {limit * 2}[/cyan] to see more."
|
|
434
|
+
)
|
|
435
|
+
if not export_format:
|
|
436
|
+
print_tip(
|
|
437
|
+
"Export results with [cyan]--export json[/cyan] or [cyan]--export markdown[/cyan]"
|
|
438
|
+
)
|
|
439
|
+
else:
|
|
440
|
+
# No results - provide helpful suggestions
|
|
441
|
+
print_info("\n[bold]No results found. Try:[/bold]")
|
|
442
|
+
print_info(" • Use more general terms in your query")
|
|
443
|
+
print_info(
|
|
444
|
+
" • Lower the similarity threshold with [cyan]--threshold 0.3[/cyan]"
|
|
445
|
+
)
|
|
446
|
+
print_info(
|
|
447
|
+
" • Check if files are indexed with [cyan]mcp-vector-search status[/cyan]"
|
|
448
|
+
)
|
|
449
|
+
|
|
450
|
+
except Exception as e:
|
|
451
|
+
logger.error(f"Search execution failed: {e}")
|
|
452
|
+
raise
|
|
453
|
+
|
|
454
|
+
|
|
455
|
+
def search_similar_cmd(
|
|
456
|
+
ctx: typer.Context,
|
|
457
|
+
file_path: Path = typer.Argument(
|
|
458
|
+
...,
|
|
459
|
+
help="Reference file path",
|
|
460
|
+
exists=True,
|
|
461
|
+
file_okay=True,
|
|
462
|
+
dir_okay=False,
|
|
463
|
+
readable=True,
|
|
464
|
+
),
|
|
465
|
+
project_root: Path | None = typer.Option(
|
|
466
|
+
None,
|
|
467
|
+
"--project-root",
|
|
468
|
+
"-p",
|
|
469
|
+
help="Project root directory (auto-detected if not specified)",
|
|
470
|
+
exists=True,
|
|
471
|
+
file_okay=False,
|
|
472
|
+
dir_okay=True,
|
|
473
|
+
readable=True,
|
|
474
|
+
),
|
|
475
|
+
function_name: str | None = typer.Option(
|
|
476
|
+
None,
|
|
477
|
+
"--function",
|
|
478
|
+
"-f",
|
|
479
|
+
help="Specific function name to find similar code for",
|
|
480
|
+
),
|
|
481
|
+
limit: int = typer.Option(
|
|
482
|
+
10,
|
|
483
|
+
"--limit",
|
|
484
|
+
"-l",
|
|
485
|
+
help="Maximum number of results",
|
|
486
|
+
min=1,
|
|
487
|
+
max=100,
|
|
488
|
+
),
|
|
489
|
+
similarity_threshold: float | None = typer.Option(
|
|
490
|
+
None,
|
|
491
|
+
"--threshold",
|
|
492
|
+
"-t",
|
|
493
|
+
help="Minimum similarity threshold",
|
|
494
|
+
min=0.0,
|
|
495
|
+
max=1.0,
|
|
496
|
+
),
|
|
497
|
+
json_output: bool = typer.Option(
|
|
498
|
+
False,
|
|
499
|
+
"--json",
|
|
500
|
+
help="Output results in JSON format",
|
|
501
|
+
),
|
|
502
|
+
) -> None:
|
|
503
|
+
"""Find code similar to a specific file or function.
|
|
504
|
+
|
|
505
|
+
Examples:
|
|
506
|
+
mcp-vector-search search similar src/auth.py
|
|
507
|
+
mcp-vector-search search similar src/utils.py --function validate_email
|
|
508
|
+
"""
|
|
509
|
+
try:
|
|
510
|
+
project_root = project_root or ctx.obj.get("project_root") or Path.cwd()
|
|
511
|
+
|
|
512
|
+
asyncio.run(
|
|
513
|
+
run_similar_search(
|
|
514
|
+
project_root=project_root,
|
|
515
|
+
file_path=file_path,
|
|
516
|
+
function_name=function_name,
|
|
517
|
+
limit=limit,
|
|
518
|
+
similarity_threshold=similarity_threshold,
|
|
519
|
+
json_output=json_output,
|
|
520
|
+
)
|
|
521
|
+
)
|
|
522
|
+
|
|
523
|
+
except Exception as e:
|
|
524
|
+
logger.error(f"Similar search failed: {e}")
|
|
525
|
+
print_error(f"Similar search failed: {e}")
|
|
526
|
+
raise typer.Exit(1)
|
|
527
|
+
|
|
528
|
+
|
|
529
|
+
async def run_similar_search(
|
|
530
|
+
project_root: Path,
|
|
531
|
+
file_path: Path,
|
|
532
|
+
function_name: str | None = None,
|
|
533
|
+
limit: int = 10,
|
|
534
|
+
similarity_threshold: float | None = None,
|
|
535
|
+
json_output: bool = False,
|
|
536
|
+
) -> None:
|
|
537
|
+
"""Run similar code search."""
|
|
538
|
+
project_manager = ProjectManager(project_root)
|
|
539
|
+
config = project_manager.load_config()
|
|
540
|
+
|
|
541
|
+
embedding_function, _ = create_embedding_function(config.embedding_model)
|
|
542
|
+
database = ChromaVectorDatabase(
|
|
543
|
+
persist_directory=config.index_path,
|
|
544
|
+
embedding_function=embedding_function,
|
|
545
|
+
)
|
|
546
|
+
|
|
547
|
+
search_engine = SemanticSearchEngine(
|
|
548
|
+
database=database,
|
|
549
|
+
project_root=project_root,
|
|
550
|
+
similarity_threshold=similarity_threshold or config.similarity_threshold,
|
|
551
|
+
)
|
|
552
|
+
|
|
553
|
+
async with database:
|
|
554
|
+
results = await search_engine.search_similar(
|
|
555
|
+
file_path=file_path,
|
|
556
|
+
function_name=function_name,
|
|
557
|
+
limit=limit,
|
|
558
|
+
similarity_threshold=similarity_threshold,
|
|
559
|
+
)
|
|
560
|
+
|
|
561
|
+
if json_output:
|
|
562
|
+
from ..output import print_json
|
|
563
|
+
|
|
564
|
+
results_data = [result.to_dict() for result in results]
|
|
565
|
+
print_json(results_data, title="Similar Code Results")
|
|
566
|
+
else:
|
|
567
|
+
query_desc = f"{file_path}"
|
|
568
|
+
if function_name:
|
|
569
|
+
query_desc += f" → {function_name}()"
|
|
570
|
+
|
|
571
|
+
print_search_results(
|
|
572
|
+
results=results,
|
|
573
|
+
query=f"Similar to: {query_desc}",
|
|
574
|
+
show_content=True,
|
|
575
|
+
)
|
|
576
|
+
|
|
577
|
+
|
|
578
|
+
def search_context_cmd(
|
|
579
|
+
ctx: typer.Context,
|
|
580
|
+
description: str = typer.Argument(..., help="Context description"),
|
|
581
|
+
project_root: Path | None = typer.Option(
|
|
582
|
+
None,
|
|
583
|
+
"--project-root",
|
|
584
|
+
"-p",
|
|
585
|
+
help="Project root directory (auto-detected if not specified)",
|
|
586
|
+
exists=True,
|
|
587
|
+
file_okay=False,
|
|
588
|
+
dir_okay=True,
|
|
589
|
+
readable=True,
|
|
590
|
+
),
|
|
591
|
+
focus: str | None = typer.Option(
|
|
592
|
+
None,
|
|
593
|
+
"--focus",
|
|
594
|
+
help="Comma-separated focus areas (e.g., 'security,authentication')",
|
|
595
|
+
),
|
|
596
|
+
limit: int = typer.Option(
|
|
597
|
+
10,
|
|
598
|
+
"--limit",
|
|
599
|
+
"-l",
|
|
600
|
+
help="Maximum number of results",
|
|
601
|
+
min=1,
|
|
602
|
+
max=100,
|
|
603
|
+
),
|
|
604
|
+
json_output: bool = typer.Option(
|
|
605
|
+
False,
|
|
606
|
+
"--json",
|
|
607
|
+
help="Output results in JSON format",
|
|
608
|
+
),
|
|
609
|
+
) -> None:
|
|
610
|
+
"""Search for code based on contextual description.
|
|
611
|
+
|
|
612
|
+
Examples:
|
|
613
|
+
mcp-vector-search search context "implement rate limiting"
|
|
614
|
+
mcp-vector-search search context "user authentication" --focus security,middleware
|
|
615
|
+
"""
|
|
616
|
+
try:
|
|
617
|
+
project_root = project_root or ctx.obj.get("project_root") or Path.cwd()
|
|
618
|
+
|
|
619
|
+
focus_areas = None
|
|
620
|
+
if focus:
|
|
621
|
+
focus_areas = [area.strip() for area in focus.split(",")]
|
|
622
|
+
|
|
623
|
+
asyncio.run(
|
|
624
|
+
run_context_search(
|
|
625
|
+
project_root=project_root,
|
|
626
|
+
description=description,
|
|
627
|
+
focus_areas=focus_areas,
|
|
628
|
+
limit=limit,
|
|
629
|
+
json_output=json_output,
|
|
630
|
+
)
|
|
631
|
+
)
|
|
632
|
+
|
|
633
|
+
except Exception as e:
|
|
634
|
+
logger.error(f"Context search failed: {e}")
|
|
635
|
+
print_error(f"Context search failed: {e}")
|
|
636
|
+
raise typer.Exit(1)
|
|
637
|
+
|
|
638
|
+
|
|
639
|
+
async def run_context_search(
|
|
640
|
+
project_root: Path,
|
|
641
|
+
description: str,
|
|
642
|
+
focus_areas: list[str] | None = None,
|
|
643
|
+
limit: int = 10,
|
|
644
|
+
json_output: bool = False,
|
|
645
|
+
) -> None:
|
|
646
|
+
"""Run contextual search."""
|
|
647
|
+
project_manager = ProjectManager(project_root)
|
|
648
|
+
config = project_manager.load_config()
|
|
649
|
+
|
|
650
|
+
embedding_function, _ = create_embedding_function(config.embedding_model)
|
|
651
|
+
database = ChromaVectorDatabase(
|
|
652
|
+
persist_directory=config.index_path,
|
|
653
|
+
embedding_function=embedding_function,
|
|
654
|
+
)
|
|
655
|
+
|
|
656
|
+
search_engine = SemanticSearchEngine(
|
|
657
|
+
database=database,
|
|
658
|
+
project_root=project_root,
|
|
659
|
+
similarity_threshold=config.similarity_threshold,
|
|
660
|
+
)
|
|
661
|
+
|
|
662
|
+
async with database:
|
|
663
|
+
results = await search_engine.search_by_context(
|
|
664
|
+
context_description=description,
|
|
665
|
+
focus_areas=focus_areas,
|
|
666
|
+
limit=limit,
|
|
667
|
+
)
|
|
668
|
+
|
|
669
|
+
if json_output:
|
|
670
|
+
from ..output import print_json
|
|
671
|
+
|
|
672
|
+
results_data = [result.to_dict() for result in results]
|
|
673
|
+
print_json(results_data, title="Context Search Results")
|
|
674
|
+
else:
|
|
675
|
+
query_desc = description
|
|
676
|
+
if focus_areas:
|
|
677
|
+
query_desc += f" (focus: {', '.join(focus_areas)})"
|
|
678
|
+
|
|
679
|
+
print_search_results(
|
|
680
|
+
results=results,
|
|
681
|
+
query=query_desc,
|
|
682
|
+
show_content=True,
|
|
683
|
+
)
|
|
684
|
+
|
|
685
|
+
|
|
686
|
+
# ============================================================================
|
|
687
|
+
# SEARCH SUBCOMMANDS
|
|
688
|
+
# ============================================================================
|
|
689
|
+
|
|
690
|
+
|
|
691
|
+
@search_app.command("interactive")
|
|
692
|
+
def interactive_search(
|
|
693
|
+
ctx: typer.Context,
|
|
694
|
+
project_root: Path | None = typer.Option(
|
|
695
|
+
None, "--project-root", "-p", help="Project root directory"
|
|
696
|
+
),
|
|
697
|
+
) -> None:
|
|
698
|
+
"""🎯 Start an interactive search session.
|
|
699
|
+
|
|
700
|
+
Provides a rich terminal interface for searching your codebase with real-time
|
|
701
|
+
filtering, query refinement, and result navigation.
|
|
702
|
+
|
|
703
|
+
Examples:
|
|
704
|
+
mcp-vector-search search interactive
|
|
705
|
+
mcp-vector-search search interactive --project-root /path/to/project
|
|
706
|
+
"""
|
|
707
|
+
import asyncio
|
|
708
|
+
|
|
709
|
+
from ..interactive import start_interactive_search
|
|
710
|
+
from ..output import console
|
|
711
|
+
|
|
712
|
+
root = project_root or ctx.obj.get("project_root") or Path.cwd()
|
|
713
|
+
|
|
714
|
+
try:
|
|
715
|
+
asyncio.run(start_interactive_search(root))
|
|
716
|
+
except KeyboardInterrupt:
|
|
717
|
+
console.print("\n[yellow]Interactive search cancelled[/yellow]")
|
|
718
|
+
except Exception as e:
|
|
719
|
+
print_error(f"Interactive search failed: {e}")
|
|
720
|
+
raise typer.Exit(1)
|
|
721
|
+
|
|
722
|
+
|
|
723
|
+
@search_app.command("history")
|
|
724
|
+
def show_history(
|
|
725
|
+
ctx: typer.Context,
|
|
726
|
+
limit: int = typer.Option(20, "--limit", "-l", help="Number of entries to show"),
|
|
727
|
+
project_root: Path | None = typer.Option(
|
|
728
|
+
None, "--project-root", "-p", help="Project root directory"
|
|
729
|
+
),
|
|
730
|
+
) -> None:
|
|
731
|
+
"""📜 Show search history.
|
|
732
|
+
|
|
733
|
+
Displays your recent search queries with timestamps and result counts.
|
|
734
|
+
Use this to revisit previous searches or track your search patterns.
|
|
735
|
+
|
|
736
|
+
Examples:
|
|
737
|
+
mcp-vector-search search history
|
|
738
|
+
mcp-vector-search search history --limit 50
|
|
739
|
+
"""
|
|
740
|
+
from ..history import show_search_history
|
|
741
|
+
|
|
742
|
+
root = project_root or ctx.obj.get("project_root") or Path.cwd()
|
|
743
|
+
show_search_history(root, limit)
|
|
744
|
+
|
|
745
|
+
|
|
746
|
+
@search_app.command("favorites")
|
|
747
|
+
def show_favorites_cmd(
|
|
748
|
+
ctx: typer.Context,
|
|
749
|
+
action: str | None = typer.Argument(None, help="Action: list, add, remove"),
|
|
750
|
+
query: str | None = typer.Argument(None, help="Query to add/remove"),
|
|
751
|
+
description: str | None = typer.Option(
|
|
752
|
+
None, "--desc", help="Description for favorite"
|
|
753
|
+
),
|
|
754
|
+
project_root: Path | None = typer.Option(
|
|
755
|
+
None, "--project-root", "-p", help="Project root directory"
|
|
756
|
+
),
|
|
757
|
+
) -> None:
|
|
758
|
+
"""⭐ Manage favorite queries.
|
|
759
|
+
|
|
760
|
+
List, add, or remove favorite search queries for quick access.
|
|
761
|
+
|
|
762
|
+
Examples:
|
|
763
|
+
mcp-vector-search search favorites # List all favorites
|
|
764
|
+
mcp-vector-search search favorites list # List all favorites
|
|
765
|
+
mcp-vector-search search favorites add "auth" # Add favorite
|
|
766
|
+
mcp-vector-search search favorites remove "auth" # Remove favorite
|
|
767
|
+
"""
|
|
768
|
+
from ..history import SearchHistory, show_favorites
|
|
769
|
+
|
|
770
|
+
root = project_root or ctx.obj.get("project_root") or Path.cwd()
|
|
771
|
+
history_manager = SearchHistory(root)
|
|
772
|
+
|
|
773
|
+
# Default to list if no action provided
|
|
774
|
+
if not action or action == "list":
|
|
775
|
+
show_favorites(root)
|
|
776
|
+
elif action == "add":
|
|
777
|
+
if not query:
|
|
778
|
+
print_error("Query is required for 'add' action")
|
|
779
|
+
raise typer.Exit(1)
|
|
780
|
+
history_manager.add_favorite(query, description)
|
|
781
|
+
elif action == "remove":
|
|
782
|
+
if not query:
|
|
783
|
+
print_error("Query is required for 'remove' action")
|
|
784
|
+
raise typer.Exit(1)
|
|
785
|
+
history_manager.remove_favorite(query)
|
|
786
|
+
else:
|
|
787
|
+
print_error(f"Unknown action: {action}. Use: list, add, or remove")
|
|
788
|
+
raise typer.Exit(1)
|
|
789
|
+
|
|
790
|
+
|
|
791
|
+
# Add main command to search_app (allows: mcp-vector-search search main "query")
|
|
792
|
+
search_app.command("main")(search_main)
|
|
793
|
+
|
|
794
|
+
|
|
795
|
+
if __name__ == "__main__":
|
|
796
|
+
search_app()
|