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