mcp-vector-search 0.0.3__py3-none-any.whl → 0.4.11__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 (49) hide show
  1. mcp_vector_search/__init__.py +3 -2
  2. mcp_vector_search/cli/commands/auto_index.py +397 -0
  3. mcp_vector_search/cli/commands/config.py +88 -40
  4. mcp_vector_search/cli/commands/index.py +198 -52
  5. mcp_vector_search/cli/commands/init.py +472 -58
  6. mcp_vector_search/cli/commands/install.py +284 -0
  7. mcp_vector_search/cli/commands/mcp.py +495 -0
  8. mcp_vector_search/cli/commands/search.py +241 -87
  9. mcp_vector_search/cli/commands/status.py +184 -58
  10. mcp_vector_search/cli/commands/watch.py +34 -35
  11. mcp_vector_search/cli/didyoumean.py +184 -0
  12. mcp_vector_search/cli/export.py +320 -0
  13. mcp_vector_search/cli/history.py +292 -0
  14. mcp_vector_search/cli/interactive.py +342 -0
  15. mcp_vector_search/cli/main.py +163 -26
  16. mcp_vector_search/cli/output.py +63 -45
  17. mcp_vector_search/config/defaults.py +50 -36
  18. mcp_vector_search/config/settings.py +49 -35
  19. mcp_vector_search/core/auto_indexer.py +298 -0
  20. mcp_vector_search/core/connection_pool.py +322 -0
  21. mcp_vector_search/core/database.py +335 -25
  22. mcp_vector_search/core/embeddings.py +73 -29
  23. mcp_vector_search/core/exceptions.py +19 -2
  24. mcp_vector_search/core/factory.py +310 -0
  25. mcp_vector_search/core/git_hooks.py +345 -0
  26. mcp_vector_search/core/indexer.py +237 -73
  27. mcp_vector_search/core/models.py +21 -19
  28. mcp_vector_search/core/project.py +73 -58
  29. mcp_vector_search/core/scheduler.py +330 -0
  30. mcp_vector_search/core/search.py +574 -86
  31. mcp_vector_search/core/watcher.py +48 -46
  32. mcp_vector_search/mcp/__init__.py +4 -0
  33. mcp_vector_search/mcp/__main__.py +25 -0
  34. mcp_vector_search/mcp/server.py +701 -0
  35. mcp_vector_search/parsers/base.py +30 -31
  36. mcp_vector_search/parsers/javascript.py +74 -48
  37. mcp_vector_search/parsers/python.py +57 -49
  38. mcp_vector_search/parsers/registry.py +47 -32
  39. mcp_vector_search/parsers/text.py +179 -0
  40. mcp_vector_search/utils/__init__.py +40 -0
  41. mcp_vector_search/utils/gitignore.py +229 -0
  42. mcp_vector_search/utils/timing.py +334 -0
  43. mcp_vector_search/utils/version.py +47 -0
  44. {mcp_vector_search-0.0.3.dist-info → mcp_vector_search-0.4.11.dist-info}/METADATA +173 -7
  45. mcp_vector_search-0.4.11.dist-info/RECORD +54 -0
  46. mcp_vector_search-0.0.3.dist-info/RECORD +0 -35
  47. {mcp_vector_search-0.0.3.dist-info → mcp_vector_search-0.4.11.dist-info}/WHEEL +0 -0
  48. {mcp_vector_search-0.0.3.dist-info → mcp_vector_search-0.4.11.dist-info}/entry_points.txt +0 -0
  49. {mcp_vector_search-0.0.3.dist-info → mcp_vector_search-0.4.11.dist-info}/licenses/LICENSE +0 -0
@@ -0,0 +1,292 @@
1
+ """Search history and favorites management."""
2
+
3
+ import json
4
+ from datetime import datetime
5
+ from pathlib import Path
6
+ from typing import Any
7
+
8
+ from rich.console import Console
9
+ from rich.table import Table
10
+
11
+ from .output import print_error, print_info, print_success
12
+
13
+ console = Console()
14
+
15
+
16
+ class SearchHistory:
17
+ """Manage search history and favorites."""
18
+
19
+ def __init__(self, project_root: Path):
20
+ """Initialize search history manager."""
21
+ self.project_root = project_root
22
+ self.history_file = project_root / ".mcp-vector-search" / "search_history.json"
23
+ self.favorites_file = project_root / ".mcp-vector-search" / "favorites.json"
24
+
25
+ # Ensure directory exists
26
+ self.history_file.parent.mkdir(parents=True, exist_ok=True)
27
+
28
+ def add_search(
29
+ self,
30
+ query: str,
31
+ results_count: int,
32
+ filters: dict[str, Any] | None = None,
33
+ execution_time: float | None = None,
34
+ ) -> None:
35
+ """Add a search to history.
36
+
37
+ Args:
38
+ query: Search query
39
+ results_count: Number of results found
40
+ filters: Applied filters
41
+ execution_time: Search execution time in seconds
42
+ """
43
+ try:
44
+ history = self._load_history()
45
+
46
+ search_entry = {
47
+ "query": query,
48
+ "timestamp": datetime.now().isoformat(),
49
+ "results_count": results_count,
50
+ "filters": filters or {},
51
+ "execution_time": execution_time,
52
+ }
53
+
54
+ # Add to beginning of history
55
+ history.insert(0, search_entry)
56
+
57
+ # Keep only last 100 searches
58
+ history = history[:100]
59
+
60
+ self._save_history(history)
61
+
62
+ except Exception as e:
63
+ print_error(f"Failed to save search history: {e}")
64
+
65
+ def get_history(self, limit: int = 20) -> list[dict[str, Any]]:
66
+ """Get search history.
67
+
68
+ Args:
69
+ limit: Maximum number of entries to return
70
+
71
+ Returns:
72
+ List of search history entries
73
+ """
74
+ try:
75
+ history = self._load_history()
76
+ return history[:limit]
77
+ except Exception as e:
78
+ print_error(f"Failed to load search history: {e}")
79
+ return []
80
+
81
+ def clear_history(self) -> bool:
82
+ """Clear search history.
83
+
84
+ Returns:
85
+ True if successful
86
+ """
87
+ try:
88
+ self._save_history([])
89
+ print_success("Search history cleared")
90
+ return True
91
+ except Exception as e:
92
+ print_error(f"Failed to clear search history: {e}")
93
+ return False
94
+
95
+ def add_favorite(self, query: str, description: str | None = None) -> bool:
96
+ """Add a search query to favorites.
97
+
98
+ Args:
99
+ query: Search query to favorite
100
+ description: Optional description
101
+
102
+ Returns:
103
+ True if successful
104
+ """
105
+ try:
106
+ favorites = self._load_favorites()
107
+
108
+ # Check if already exists
109
+ for fav in favorites:
110
+ if fav["query"] == query:
111
+ print_info(f"Query already in favorites: {query}")
112
+ return True
113
+
114
+ favorite_entry = {
115
+ "query": query,
116
+ "description": description or "",
117
+ "created": datetime.now().isoformat(),
118
+ "usage_count": 0,
119
+ }
120
+
121
+ favorites.append(favorite_entry)
122
+ self._save_favorites(favorites)
123
+
124
+ print_success(f"Added to favorites: {query}")
125
+ return True
126
+
127
+ except Exception as e:
128
+ print_error(f"Failed to add favorite: {e}")
129
+ return False
130
+
131
+ def remove_favorite(self, query: str) -> bool:
132
+ """Remove a query from favorites.
133
+
134
+ Args:
135
+ query: Query to remove
136
+
137
+ Returns:
138
+ True if successful
139
+ """
140
+ try:
141
+ favorites = self._load_favorites()
142
+ original_count = len(favorites)
143
+
144
+ favorites = [fav for fav in favorites if fav["query"] != query]
145
+
146
+ if len(favorites) < original_count:
147
+ self._save_favorites(favorites)
148
+ print_success(f"Removed from favorites: {query}")
149
+ return True
150
+ else:
151
+ print_info(f"Query not found in favorites: {query}")
152
+ return False
153
+
154
+ except Exception as e:
155
+ print_error(f"Failed to remove favorite: {e}")
156
+ return False
157
+
158
+ def get_favorites(self) -> list[dict[str, Any]]:
159
+ """Get favorite queries.
160
+
161
+ Returns:
162
+ List of favorite queries
163
+ """
164
+ try:
165
+ return self._load_favorites()
166
+ except Exception as e:
167
+ print_error(f"Failed to load favorites: {e}")
168
+ return []
169
+
170
+ def increment_favorite_usage(self, query: str) -> None:
171
+ """Increment usage count for a favorite query.
172
+
173
+ Args:
174
+ query: Query that was used
175
+ """
176
+ try:
177
+ favorites = self._load_favorites()
178
+
179
+ for fav in favorites:
180
+ if fav["query"] == query:
181
+ fav["usage_count"] = fav.get("usage_count", 0) + 1
182
+ fav["last_used"] = datetime.now().isoformat()
183
+ break
184
+
185
+ self._save_favorites(favorites)
186
+
187
+ except Exception:
188
+ # Don't show error for this non-critical operation
189
+ pass
190
+
191
+ def _load_history(self) -> list[dict[str, Any]]:
192
+ """Load search history from file."""
193
+ if not self.history_file.exists():
194
+ return []
195
+
196
+ try:
197
+ with open(self.history_file, encoding="utf-8") as f:
198
+ return json.load(f)
199
+ except Exception:
200
+ return []
201
+
202
+ def _save_history(self, history: list[dict[str, Any]]) -> None:
203
+ """Save search history to file."""
204
+ with open(self.history_file, "w", encoding="utf-8") as f:
205
+ json.dump(history, f, indent=2, ensure_ascii=False)
206
+
207
+ def _load_favorites(self) -> list[dict[str, Any]]:
208
+ """Load favorites from file."""
209
+ if not self.favorites_file.exists():
210
+ return []
211
+
212
+ try:
213
+ with open(self.favorites_file, encoding="utf-8") as f:
214
+ return json.load(f)
215
+ except Exception:
216
+ return []
217
+
218
+ def _save_favorites(self, favorites: list[dict[str, Any]]) -> None:
219
+ """Save favorites to file."""
220
+ with open(self.favorites_file, "w", encoding="utf-8") as f:
221
+ json.dump(favorites, f, indent=2, ensure_ascii=False)
222
+
223
+
224
+ def show_search_history(project_root: Path, limit: int = 20) -> None:
225
+ """Display search history in a formatted table."""
226
+ history_manager = SearchHistory(project_root)
227
+ history = history_manager.get_history(limit)
228
+
229
+ if not history:
230
+ print_info("No search history found")
231
+ return
232
+
233
+ table = Table(
234
+ title=f"Search History (Last {len(history)} searches)", show_header=True
235
+ )
236
+ table.add_column("#", style="cyan", width=3)
237
+ table.add_column("Query", style="white", min_width=20)
238
+ table.add_column("Results", style="green", width=8)
239
+ table.add_column("Time", style="dim", width=16)
240
+ table.add_column("Filters", style="yellow", width=15)
241
+
242
+ for i, entry in enumerate(history, 1):
243
+ timestamp = datetime.fromisoformat(entry["timestamp"]).strftime("%m-%d %H:%M")
244
+ filters_str = ", ".join(f"{k}:{v}" for k, v in entry.get("filters", {}).items())
245
+ if not filters_str:
246
+ filters_str = "-"
247
+
248
+ table.add_row(
249
+ str(i),
250
+ entry["query"][:40] + "..." if len(entry["query"]) > 40 else entry["query"],
251
+ str(entry["results_count"]),
252
+ timestamp,
253
+ filters_str[:15] + "..." if len(filters_str) > 15 else filters_str,
254
+ )
255
+
256
+ console.print(table)
257
+
258
+
259
+ def show_favorites(project_root: Path) -> None:
260
+ """Display favorite queries in a formatted table."""
261
+ history_manager = SearchHistory(project_root)
262
+ favorites = history_manager.get_favorites()
263
+
264
+ if not favorites:
265
+ print_info("No favorite queries found")
266
+ return
267
+
268
+ # Sort by usage count (descending)
269
+ favorites.sort(key=lambda x: x.get("usage_count", 0), reverse=True)
270
+
271
+ table = Table(title="Favorite Queries", show_header=True)
272
+ table.add_column("#", style="cyan", width=3)
273
+ table.add_column("Query", style="white", min_width=25)
274
+ table.add_column("Description", style="dim", min_width=20)
275
+ table.add_column("Usage", style="green", width=6)
276
+ table.add_column("Created", style="dim", width=10)
277
+
278
+ for i, fav in enumerate(favorites, 1):
279
+ created = datetime.fromisoformat(fav["created"]).strftime("%m-%d")
280
+ description = fav.get("description", "")[:30]
281
+ if len(fav.get("description", "")) > 30:
282
+ description += "..."
283
+
284
+ table.add_row(
285
+ str(i),
286
+ fav["query"],
287
+ description or "-",
288
+ str(fav.get("usage_count", 0)),
289
+ created,
290
+ )
291
+
292
+ console.print(table)
@@ -0,0 +1,342 @@
1
+ """Interactive search features for MCP Vector Search."""
2
+
3
+ from pathlib import Path
4
+
5
+ from rich.console import Console
6
+ from rich.panel import Panel
7
+ from rich.prompt import Prompt
8
+ from rich.table import Table
9
+
10
+ from ..core.database import ChromaVectorDatabase
11
+ from ..core.embeddings import create_embedding_function
12
+ from ..core.exceptions import ProjectNotFoundError
13
+ from ..core.models import SearchResult
14
+ from ..core.project import ProjectManager
15
+ from ..core.search import SemanticSearchEngine
16
+ from .output import print_error, print_info, print_search_results, print_warning
17
+
18
+ console = Console()
19
+
20
+
21
+ class InteractiveSearchSession:
22
+ """Interactive search session with filtering and refinement."""
23
+
24
+ def __init__(self, project_root: Path):
25
+ """Initialize interactive search session."""
26
+ self.project_root = project_root
27
+ self.project_manager = ProjectManager(project_root)
28
+ self.search_engine: SemanticSearchEngine | None = None
29
+ self.database: ChromaVectorDatabase | None = None
30
+ self.last_results: list[SearchResult] = []
31
+ self.search_history: list[str] = []
32
+
33
+ async def start(self) -> None:
34
+ """Start interactive search session."""
35
+ if not self.project_manager.is_initialized():
36
+ raise ProjectNotFoundError(
37
+ f"Project not initialized at {self.project_root}. Run 'mcp-vector-search init' first."
38
+ )
39
+
40
+ config = self.project_manager.load_config()
41
+
42
+ # Setup database and search engine
43
+ embedding_function, _ = create_embedding_function(config.embedding_model)
44
+ self.database = ChromaVectorDatabase(
45
+ persist_directory=config.index_path,
46
+ embedding_function=embedding_function,
47
+ )
48
+
49
+ self.search_engine = SemanticSearchEngine(
50
+ database=self.database,
51
+ project_root=self.project_root,
52
+ similarity_threshold=config.similarity_threshold,
53
+ )
54
+
55
+ await self.database.initialize()
56
+
57
+ # Show welcome message
58
+ self._show_welcome()
59
+
60
+ # Main interactive loop
61
+ try:
62
+ await self._interactive_loop()
63
+ finally:
64
+ await self.database.close()
65
+
66
+ def _show_welcome(self) -> None:
67
+ """Show welcome message and help."""
68
+ welcome_text = """
69
+ [bold blue]Interactive Search Session[/bold blue]
70
+
71
+ Available commands:
72
+ • [cyan]search <query>[/cyan] - Perform semantic search
73
+ • [cyan]filter[/cyan] - Filter current results
74
+ • [cyan]refine[/cyan] - Refine last search
75
+ • [cyan]history[/cyan] - Show search history
76
+ • [cyan]stats[/cyan] - Show result statistics
77
+ • [cyan]help[/cyan] - Show this help
78
+ • [cyan]quit[/cyan] - Exit session
79
+
80
+ Type your search query or command:
81
+ """
82
+ console.print(Panel(welcome_text.strip(), border_style="blue"))
83
+
84
+ async def _interactive_loop(self) -> None:
85
+ """Main interactive loop."""
86
+ while True:
87
+ try:
88
+ # Get user input
89
+ user_input = Prompt.ask("\n[bold cyan]Search[/bold cyan]").strip()
90
+
91
+ if not user_input:
92
+ continue
93
+
94
+ # Handle commands
95
+ if user_input.lower() in ["quit", "exit", "q"]:
96
+ console.print("[yellow]Goodbye![/yellow]")
97
+ break
98
+ elif user_input.lower() == "help":
99
+ self._show_welcome()
100
+ elif user_input.lower() == "history":
101
+ self._show_history()
102
+ elif user_input.lower() == "stats":
103
+ self._show_stats()
104
+ elif user_input.lower() == "filter":
105
+ await self._filter_results()
106
+ elif user_input.lower() == "refine":
107
+ await self._refine_search()
108
+ elif user_input.startswith("search "):
109
+ query = user_input[7:].strip()
110
+ await self._perform_search(query)
111
+ else:
112
+ # Treat as search query
113
+ await self._perform_search(user_input)
114
+
115
+ except KeyboardInterrupt:
116
+ console.print("\n[yellow]Use 'quit' to exit[/yellow]")
117
+ except Exception as e:
118
+ print_error(f"Error: {e}")
119
+
120
+ async def _perform_search(self, query: str) -> None:
121
+ """Perform a search and display results."""
122
+ if not query:
123
+ print_warning("Please provide a search query")
124
+ return
125
+
126
+ try:
127
+ console.print(f"\n[dim]Searching for: {query}[/dim]")
128
+
129
+ results = await self.search_engine.search(
130
+ query=query,
131
+ limit=20, # Get more results for filtering
132
+ include_context=True,
133
+ )
134
+
135
+ self.last_results = results
136
+ self.search_history.append(query)
137
+
138
+ if results:
139
+ print_search_results(results[:10], query, show_content=False)
140
+
141
+ if len(results) > 10:
142
+ console.print(
143
+ f"\n[dim]Showing top 10 of {len(results)} results. Use 'filter' to refine.[/dim]"
144
+ )
145
+
146
+ # Show quick actions
147
+ self._show_quick_actions()
148
+ else:
149
+ print_warning(f"No results found for: {query}")
150
+ self._suggest_alternatives(query)
151
+
152
+ except Exception as e:
153
+ print_error(f"Search failed: {e}")
154
+
155
+ def _show_quick_actions(self) -> None:
156
+ """Show quick action options."""
157
+ actions = [
158
+ "[cyan]filter[/cyan] - Filter results",
159
+ "[cyan]refine[/cyan] - Refine search",
160
+ "[cyan]stats[/cyan] - Show statistics",
161
+ ]
162
+ console.print(f"\n[dim]Quick actions: {' | '.join(actions)}[/dim]")
163
+
164
+ async def _filter_results(self) -> None:
165
+ """Interactive result filtering."""
166
+ if not self.last_results:
167
+ print_warning("No results to filter. Perform a search first.")
168
+ return
169
+
170
+ console.print(f"\n[bold]Filtering {len(self.last_results)} results[/bold]")
171
+
172
+ # Show available filter options
173
+ available_languages = {r.language for r in self.last_results}
174
+ available_files = {r.file_path.name for r in self.last_results}
175
+ available_functions = {
176
+ r.function_name for r in self.last_results if r.function_name
177
+ }
178
+ {r.class_name for r in self.last_results if r.class_name}
179
+
180
+ # Language filter
181
+ if len(available_languages) > 1:
182
+ lang_choice = Prompt.ask(
183
+ f"Filter by language? ({', '.join(sorted(available_languages))})",
184
+ default="",
185
+ show_default=False,
186
+ )
187
+ if lang_choice and lang_choice in available_languages:
188
+ self.last_results = [
189
+ r for r in self.last_results if r.language == lang_choice
190
+ ]
191
+ console.print(
192
+ f"[green]Filtered to {len(self.last_results)} results with language: {lang_choice}[/green]"
193
+ )
194
+
195
+ # File filter
196
+ if len(available_files) > 1 and len(self.last_results) > 1:
197
+ file_pattern = Prompt.ask(
198
+ "Filter by file name pattern (partial match)",
199
+ default="",
200
+ show_default=False,
201
+ )
202
+ if file_pattern:
203
+ self.last_results = [
204
+ r
205
+ for r in self.last_results
206
+ if file_pattern.lower() in r.file_path.name.lower()
207
+ ]
208
+ console.print(
209
+ f"[green]Filtered to {len(self.last_results)} results matching: {file_pattern}[/green]"
210
+ )
211
+
212
+ # Function filter
213
+ if available_functions and len(self.last_results) > 1:
214
+ func_pattern = Prompt.ask(
215
+ "Filter by function name pattern (partial match)",
216
+ default="",
217
+ show_default=False,
218
+ )
219
+ if func_pattern:
220
+ self.last_results = [
221
+ r
222
+ for r in self.last_results
223
+ if r.function_name
224
+ and func_pattern.lower() in r.function_name.lower()
225
+ ]
226
+ console.print(
227
+ f"[green]Filtered to {len(self.last_results)} results with function matching: {func_pattern}[/green]"
228
+ )
229
+
230
+ # Similarity threshold filter
231
+ min_similarity = Prompt.ask(
232
+ "Minimum similarity threshold (0.0-1.0)", default="", show_default=False
233
+ )
234
+ if min_similarity:
235
+ try:
236
+ threshold = float(min_similarity)
237
+ if 0.0 <= threshold <= 1.0:
238
+ self.last_results = [
239
+ r for r in self.last_results if r.similarity_score >= threshold
240
+ ]
241
+ console.print(
242
+ f"[green]Filtered to {len(self.last_results)} results with similarity >= {threshold}[/green]"
243
+ )
244
+ except ValueError:
245
+ print_warning("Invalid similarity threshold")
246
+
247
+ # Show filtered results
248
+ if self.last_results:
249
+ print_search_results(
250
+ self.last_results[:10], "Filtered Results", show_content=False
251
+ )
252
+ else:
253
+ print_warning("No results match the filters")
254
+
255
+ async def _refine_search(self) -> None:
256
+ """Refine the last search with additional terms."""
257
+ if not self.search_history:
258
+ print_warning("No previous search to refine")
259
+ return
260
+
261
+ last_query = self.search_history[-1]
262
+ console.print(f"[dim]Last search: {last_query}[/dim]")
263
+
264
+ additional_terms = Prompt.ask("Add terms to refine search", default="")
265
+ if additional_terms:
266
+ refined_query = f"{last_query} {additional_terms}"
267
+ await self._perform_search(refined_query)
268
+
269
+ def _show_history(self) -> None:
270
+ """Show search history."""
271
+ if not self.search_history:
272
+ print_info("No search history")
273
+ return
274
+
275
+ table = Table(title="Search History", show_header=True)
276
+ table.add_column("#", style="cyan", width=3)
277
+ table.add_column("Query", style="white")
278
+
279
+ for i, query in enumerate(self.search_history[-10:], 1):
280
+ table.add_row(str(i), query)
281
+
282
+ console.print(table)
283
+
284
+ def _show_stats(self) -> None:
285
+ """Show statistics for current results."""
286
+ if not self.last_results:
287
+ print_info("No results to analyze")
288
+ return
289
+
290
+ # Calculate statistics
291
+ languages = {}
292
+ files = {}
293
+ avg_similarity = sum(r.similarity_score for r in self.last_results) / len(
294
+ self.last_results
295
+ )
296
+
297
+ for result in self.last_results:
298
+ languages[result.language] = languages.get(result.language, 0) + 1
299
+ files[result.file_path.name] = files.get(result.file_path.name, 0) + 1
300
+
301
+ # Create statistics table
302
+ table = Table(title="Result Statistics", show_header=False)
303
+ table.add_column("Metric", style="cyan")
304
+ table.add_column("Value", style="white")
305
+
306
+ table.add_row("Total Results", str(len(self.last_results)))
307
+ table.add_row("Average Similarity", f"{avg_similarity:.1%}")
308
+ table.add_row(
309
+ "Languages",
310
+ ", ".join(f"{lang}({count})" for lang, count in languages.items()),
311
+ )
312
+ table.add_row("Unique Files", str(len(files)))
313
+
314
+ console.print(table)
315
+
316
+ def _suggest_alternatives(self, query: str) -> None:
317
+ """Suggest alternative search terms."""
318
+ suggestions = []
319
+
320
+ # Simple suggestions based on common patterns
321
+ words = query.lower().split()
322
+ for word in words:
323
+ if word in ["auth", "authentication"]:
324
+ suggestions.extend(["login", "user", "session", "token"])
325
+ elif word in ["db", "database"]:
326
+ suggestions.extend(["query", "model", "connection", "storage"])
327
+ elif word in ["api"]:
328
+ suggestions.extend(["endpoint", "request", "response", "handler"])
329
+ elif word in ["test", "testing"]:
330
+ suggestions.extend(["mock", "assert", "spec", "unit"])
331
+
332
+ if suggestions:
333
+ unique_suggestions = list(set(suggestions))[:5]
334
+ console.print(
335
+ f"[dim]Try these terms: {', '.join(unique_suggestions)}[/dim]"
336
+ )
337
+
338
+
339
+ async def start_interactive_search(project_root: Path) -> None:
340
+ """Start an interactive search session."""
341
+ session = InteractiveSearchSession(project_root)
342
+ await session.start()