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.

Files changed (86) hide show
  1. mcp_vector_search/__init__.py +10 -0
  2. mcp_vector_search/cli/__init__.py +1 -0
  3. mcp_vector_search/cli/commands/__init__.py +1 -0
  4. mcp_vector_search/cli/commands/auto_index.py +397 -0
  5. mcp_vector_search/cli/commands/chat.py +534 -0
  6. mcp_vector_search/cli/commands/config.py +393 -0
  7. mcp_vector_search/cli/commands/demo.py +358 -0
  8. mcp_vector_search/cli/commands/index.py +762 -0
  9. mcp_vector_search/cli/commands/init.py +658 -0
  10. mcp_vector_search/cli/commands/install.py +869 -0
  11. mcp_vector_search/cli/commands/install_old.py +700 -0
  12. mcp_vector_search/cli/commands/mcp.py +1254 -0
  13. mcp_vector_search/cli/commands/reset.py +393 -0
  14. mcp_vector_search/cli/commands/search.py +796 -0
  15. mcp_vector_search/cli/commands/setup.py +1133 -0
  16. mcp_vector_search/cli/commands/status.py +584 -0
  17. mcp_vector_search/cli/commands/uninstall.py +404 -0
  18. mcp_vector_search/cli/commands/visualize/__init__.py +39 -0
  19. mcp_vector_search/cli/commands/visualize/cli.py +265 -0
  20. mcp_vector_search/cli/commands/visualize/exporters/__init__.py +12 -0
  21. mcp_vector_search/cli/commands/visualize/exporters/html_exporter.py +33 -0
  22. mcp_vector_search/cli/commands/visualize/exporters/json_exporter.py +29 -0
  23. mcp_vector_search/cli/commands/visualize/graph_builder.py +709 -0
  24. mcp_vector_search/cli/commands/visualize/layout_engine.py +469 -0
  25. mcp_vector_search/cli/commands/visualize/server.py +201 -0
  26. mcp_vector_search/cli/commands/visualize/state_manager.py +428 -0
  27. mcp_vector_search/cli/commands/visualize/templates/__init__.py +16 -0
  28. mcp_vector_search/cli/commands/visualize/templates/base.py +218 -0
  29. mcp_vector_search/cli/commands/visualize/templates/scripts.py +3670 -0
  30. mcp_vector_search/cli/commands/visualize/templates/styles.py +779 -0
  31. mcp_vector_search/cli/commands/visualize.py.original +2536 -0
  32. mcp_vector_search/cli/commands/watch.py +287 -0
  33. mcp_vector_search/cli/didyoumean.py +520 -0
  34. mcp_vector_search/cli/export.py +320 -0
  35. mcp_vector_search/cli/history.py +295 -0
  36. mcp_vector_search/cli/interactive.py +342 -0
  37. mcp_vector_search/cli/main.py +484 -0
  38. mcp_vector_search/cli/output.py +414 -0
  39. mcp_vector_search/cli/suggestions.py +375 -0
  40. mcp_vector_search/config/__init__.py +1 -0
  41. mcp_vector_search/config/constants.py +24 -0
  42. mcp_vector_search/config/defaults.py +200 -0
  43. mcp_vector_search/config/settings.py +146 -0
  44. mcp_vector_search/core/__init__.py +1 -0
  45. mcp_vector_search/core/auto_indexer.py +298 -0
  46. mcp_vector_search/core/config_utils.py +394 -0
  47. mcp_vector_search/core/connection_pool.py +360 -0
  48. mcp_vector_search/core/database.py +1237 -0
  49. mcp_vector_search/core/directory_index.py +318 -0
  50. mcp_vector_search/core/embeddings.py +294 -0
  51. mcp_vector_search/core/exceptions.py +89 -0
  52. mcp_vector_search/core/factory.py +318 -0
  53. mcp_vector_search/core/git_hooks.py +345 -0
  54. mcp_vector_search/core/indexer.py +1002 -0
  55. mcp_vector_search/core/llm_client.py +453 -0
  56. mcp_vector_search/core/models.py +294 -0
  57. mcp_vector_search/core/project.py +350 -0
  58. mcp_vector_search/core/scheduler.py +330 -0
  59. mcp_vector_search/core/search.py +952 -0
  60. mcp_vector_search/core/watcher.py +322 -0
  61. mcp_vector_search/mcp/__init__.py +5 -0
  62. mcp_vector_search/mcp/__main__.py +25 -0
  63. mcp_vector_search/mcp/server.py +752 -0
  64. mcp_vector_search/parsers/__init__.py +8 -0
  65. mcp_vector_search/parsers/base.py +296 -0
  66. mcp_vector_search/parsers/dart.py +605 -0
  67. mcp_vector_search/parsers/html.py +413 -0
  68. mcp_vector_search/parsers/javascript.py +643 -0
  69. mcp_vector_search/parsers/php.py +694 -0
  70. mcp_vector_search/parsers/python.py +502 -0
  71. mcp_vector_search/parsers/registry.py +223 -0
  72. mcp_vector_search/parsers/ruby.py +678 -0
  73. mcp_vector_search/parsers/text.py +186 -0
  74. mcp_vector_search/parsers/utils.py +265 -0
  75. mcp_vector_search/py.typed +1 -0
  76. mcp_vector_search/utils/__init__.py +42 -0
  77. mcp_vector_search/utils/gitignore.py +250 -0
  78. mcp_vector_search/utils/gitignore_updater.py +212 -0
  79. mcp_vector_search/utils/monorepo.py +339 -0
  80. mcp_vector_search/utils/timing.py +338 -0
  81. mcp_vector_search/utils/version.py +47 -0
  82. mcp_vector_search-0.15.7.dist-info/METADATA +884 -0
  83. mcp_vector_search-0.15.7.dist-info/RECORD +86 -0
  84. mcp_vector_search-0.15.7.dist-info/WHEEL +4 -0
  85. mcp_vector_search-0.15.7.dist-info/entry_points.txt +3 -0
  86. 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()