mcp-vector-search 0.12.6__py3-none-any.whl → 1.1.22__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (92) hide show
  1. mcp_vector_search/__init__.py +3 -3
  2. mcp_vector_search/analysis/__init__.py +111 -0
  3. mcp_vector_search/analysis/baseline/__init__.py +68 -0
  4. mcp_vector_search/analysis/baseline/comparator.py +462 -0
  5. mcp_vector_search/analysis/baseline/manager.py +621 -0
  6. mcp_vector_search/analysis/collectors/__init__.py +74 -0
  7. mcp_vector_search/analysis/collectors/base.py +164 -0
  8. mcp_vector_search/analysis/collectors/cohesion.py +463 -0
  9. mcp_vector_search/analysis/collectors/complexity.py +743 -0
  10. mcp_vector_search/analysis/collectors/coupling.py +1162 -0
  11. mcp_vector_search/analysis/collectors/halstead.py +514 -0
  12. mcp_vector_search/analysis/collectors/smells.py +325 -0
  13. mcp_vector_search/analysis/debt.py +516 -0
  14. mcp_vector_search/analysis/interpretation.py +685 -0
  15. mcp_vector_search/analysis/metrics.py +414 -0
  16. mcp_vector_search/analysis/reporters/__init__.py +7 -0
  17. mcp_vector_search/analysis/reporters/console.py +646 -0
  18. mcp_vector_search/analysis/reporters/markdown.py +480 -0
  19. mcp_vector_search/analysis/reporters/sarif.py +377 -0
  20. mcp_vector_search/analysis/storage/__init__.py +93 -0
  21. mcp_vector_search/analysis/storage/metrics_store.py +762 -0
  22. mcp_vector_search/analysis/storage/schema.py +245 -0
  23. mcp_vector_search/analysis/storage/trend_tracker.py +560 -0
  24. mcp_vector_search/analysis/trends.py +308 -0
  25. mcp_vector_search/analysis/visualizer/__init__.py +90 -0
  26. mcp_vector_search/analysis/visualizer/d3_data.py +534 -0
  27. mcp_vector_search/analysis/visualizer/exporter.py +484 -0
  28. mcp_vector_search/analysis/visualizer/html_report.py +2895 -0
  29. mcp_vector_search/analysis/visualizer/schemas.py +525 -0
  30. mcp_vector_search/cli/commands/analyze.py +1062 -0
  31. mcp_vector_search/cli/commands/chat.py +1455 -0
  32. mcp_vector_search/cli/commands/index.py +621 -5
  33. mcp_vector_search/cli/commands/index_background.py +467 -0
  34. mcp_vector_search/cli/commands/init.py +13 -0
  35. mcp_vector_search/cli/commands/install.py +597 -335
  36. mcp_vector_search/cli/commands/install_old.py +8 -4
  37. mcp_vector_search/cli/commands/mcp.py +78 -6
  38. mcp_vector_search/cli/commands/reset.py +68 -26
  39. mcp_vector_search/cli/commands/search.py +224 -8
  40. mcp_vector_search/cli/commands/setup.py +1184 -0
  41. mcp_vector_search/cli/commands/status.py +339 -5
  42. mcp_vector_search/cli/commands/uninstall.py +276 -357
  43. mcp_vector_search/cli/commands/visualize/__init__.py +39 -0
  44. mcp_vector_search/cli/commands/visualize/cli.py +292 -0
  45. mcp_vector_search/cli/commands/visualize/exporters/__init__.py +12 -0
  46. mcp_vector_search/cli/commands/visualize/exporters/html_exporter.py +33 -0
  47. mcp_vector_search/cli/commands/visualize/exporters/json_exporter.py +33 -0
  48. mcp_vector_search/cli/commands/visualize/graph_builder.py +647 -0
  49. mcp_vector_search/cli/commands/visualize/layout_engine.py +469 -0
  50. mcp_vector_search/cli/commands/visualize/server.py +600 -0
  51. mcp_vector_search/cli/commands/visualize/state_manager.py +428 -0
  52. mcp_vector_search/cli/commands/visualize/templates/__init__.py +16 -0
  53. mcp_vector_search/cli/commands/visualize/templates/base.py +234 -0
  54. mcp_vector_search/cli/commands/visualize/templates/scripts.py +4542 -0
  55. mcp_vector_search/cli/commands/visualize/templates/styles.py +2522 -0
  56. mcp_vector_search/cli/didyoumean.py +27 -2
  57. mcp_vector_search/cli/main.py +127 -160
  58. mcp_vector_search/cli/output.py +158 -13
  59. mcp_vector_search/config/__init__.py +4 -0
  60. mcp_vector_search/config/default_thresholds.yaml +52 -0
  61. mcp_vector_search/config/settings.py +12 -0
  62. mcp_vector_search/config/thresholds.py +273 -0
  63. mcp_vector_search/core/__init__.py +16 -0
  64. mcp_vector_search/core/auto_indexer.py +3 -3
  65. mcp_vector_search/core/boilerplate.py +186 -0
  66. mcp_vector_search/core/config_utils.py +394 -0
  67. mcp_vector_search/core/database.py +406 -94
  68. mcp_vector_search/core/embeddings.py +24 -0
  69. mcp_vector_search/core/exceptions.py +11 -0
  70. mcp_vector_search/core/git.py +380 -0
  71. mcp_vector_search/core/git_hooks.py +4 -4
  72. mcp_vector_search/core/indexer.py +632 -54
  73. mcp_vector_search/core/llm_client.py +756 -0
  74. mcp_vector_search/core/models.py +91 -1
  75. mcp_vector_search/core/project.py +17 -0
  76. mcp_vector_search/core/relationships.py +473 -0
  77. mcp_vector_search/core/scheduler.py +11 -11
  78. mcp_vector_search/core/search.py +179 -29
  79. mcp_vector_search/mcp/server.py +819 -9
  80. mcp_vector_search/parsers/python.py +285 -5
  81. mcp_vector_search/utils/__init__.py +2 -0
  82. mcp_vector_search/utils/gitignore.py +0 -3
  83. mcp_vector_search/utils/gitignore_updater.py +212 -0
  84. mcp_vector_search/utils/monorepo.py +66 -4
  85. mcp_vector_search/utils/timing.py +10 -6
  86. {mcp_vector_search-0.12.6.dist-info → mcp_vector_search-1.1.22.dist-info}/METADATA +184 -53
  87. mcp_vector_search-1.1.22.dist-info/RECORD +120 -0
  88. {mcp_vector_search-0.12.6.dist-info → mcp_vector_search-1.1.22.dist-info}/WHEEL +1 -1
  89. {mcp_vector_search-0.12.6.dist-info → mcp_vector_search-1.1.22.dist-info}/entry_points.txt +1 -0
  90. mcp_vector_search/cli/commands/visualize.py +0 -1467
  91. mcp_vector_search-0.12.6.dist-info/RECORD +0 -68
  92. {mcp_vector_search-0.12.6.dist-info → mcp_vector_search-1.1.22.dist-info}/licenses/LICENSE +0 -0
@@ -0,0 +1,1184 @@
1
+ """Smart zero-config setup command for MCP Vector Search CLI.
2
+
3
+ This module provides a zero-configuration setup command that intelligently detects
4
+ project characteristics and configures everything automatically:
5
+
6
+ 1. Detects project root and characteristics
7
+ 2. Scans for file types in use (with timeout)
8
+ 3. Detects installed MCP platforms
9
+ 4. Initializes with optimal defaults
10
+ 5. Indexes codebase
11
+ 6. Configures all detected MCP platforms
12
+ 7. Sets up file watching
13
+
14
+ Examples:
15
+ # Zero-config setup (recommended)
16
+ $ mcp-vector-search setup
17
+
18
+ # Force re-setup
19
+ $ mcp-vector-search setup --force
20
+
21
+ # Verbose output for debugging
22
+ $ mcp-vector-search setup --verbose
23
+ """
24
+
25
+ import asyncio
26
+ import os
27
+ import shutil
28
+ import subprocess
29
+ import time
30
+ from pathlib import Path
31
+
32
+ import typer
33
+ from loguru import logger
34
+
35
+ # Import Platform enum to filter excluded platforms
36
+ from py_mcp_installer import Platform
37
+ from rich.console import Console
38
+ from rich.panel import Panel
39
+
40
+ from ...config.defaults import (
41
+ DEFAULT_EMBEDDING_MODELS,
42
+ DEFAULT_FILE_EXTENSIONS,
43
+ get_language_from_extension,
44
+ )
45
+ from ...core.exceptions import ProjectInitializationError
46
+ from ...core.project import ProjectManager
47
+ from ..didyoumean import create_enhanced_typer
48
+ from ..output import (
49
+ print_error,
50
+ print_info,
51
+ print_next_steps,
52
+ print_success,
53
+ print_warning,
54
+ )
55
+
56
+ # Import functions from refactored install module
57
+ from .install import _install_to_platform, detect_all_platforms
58
+
59
+ # Platforms to exclude from auto-setup (user can still manually install)
60
+ EXCLUDED_PLATFORMS_FROM_SETUP = {Platform.CLAUDE_DESKTOP}
61
+
62
+ # Create console for rich output
63
+ console = Console()
64
+
65
+ # Create setup app
66
+ setup_app = create_enhanced_typer(
67
+ help="""🚀 Smart zero-config setup for mcp-vector-search
68
+
69
+ [bold cyan]What it does:[/bold cyan]
70
+ ✅ Auto-detects your project's languages and file types
71
+ ✅ Initializes semantic search with optimal settings
72
+ ✅ Indexes your entire codebase
73
+ ✅ Configures ALL installed MCP platforms
74
+ ✅ Sets up automatic file watching
75
+ ✅ No configuration needed - just run it!
76
+
77
+ [bold cyan]Perfect for:[/bold cyan]
78
+ • Getting started quickly in any project
79
+ • Team onboarding (commit .mcp.json to repo)
80
+ • Setting up multiple MCP platforms at once
81
+ • Letting AI tools handle the configuration
82
+
83
+ [dim]💡 This is the recommended way to set up mcp-vector-search[/dim]
84
+ """,
85
+ invoke_without_command=True,
86
+ no_args_is_help=False,
87
+ )
88
+
89
+
90
+ # ==============================================================================
91
+ # Helper Functions
92
+ # ==============================================================================
93
+
94
+
95
+ def check_claude_cli_available() -> bool:
96
+ """Check if Claude CLI is available.
97
+
98
+ Returns:
99
+ True if claude CLI is installed and accessible
100
+ """
101
+ return shutil.which("claude") is not None
102
+
103
+
104
+ def check_uv_available() -> bool:
105
+ """Check if uv is available.
106
+
107
+ Returns:
108
+ True if uv is installed and accessible
109
+ """
110
+ return shutil.which("uv") is not None
111
+
112
+
113
+ def register_with_claude_cli(
114
+ project_root: Path,
115
+ server_name: str = "mcp-vector-search",
116
+ enable_watch: bool = True,
117
+ verbose: bool = False,
118
+ ) -> bool:
119
+ """Register MCP server with Claude CLI using native 'claude mcp add' command.
120
+
121
+ Args:
122
+ project_root: Project root directory
123
+ server_name: Name for the MCP server entry (default: "mcp-vector-search")
124
+ enable_watch: Enable file watching
125
+ verbose: Show verbose output
126
+
127
+ Returns:
128
+ True if registration was successful, False otherwise
129
+ """
130
+ try:
131
+ # Check if mcp-vector-search command is available first
132
+ # This ensures we work with pipx/homebrew installations, not just uv
133
+ if not shutil.which("mcp-vector-search"):
134
+ if verbose:
135
+ print_warning(
136
+ " ⚠️ mcp-vector-search command not in PATH, will use manual JSON configuration"
137
+ )
138
+ return False
139
+
140
+ # First, try to remove existing server (safe to ignore if doesn't exist)
141
+ # This ensures clean registration when server already exists
142
+ remove_cmd = ["claude", "mcp", "remove", server_name]
143
+
144
+ if verbose:
145
+ print_info(" Checking for existing MCP server registration...")
146
+
147
+ subprocess.run(
148
+ remove_cmd,
149
+ capture_output=True,
150
+ text=True,
151
+ timeout=10,
152
+ )
153
+ # Ignore result - it's OK if server doesn't exist
154
+
155
+ # Build the add command using mcp-vector-search CLI
156
+ # This works for all installation methods: pipx, homebrew, and uv
157
+ # Claude Code sets CWD to the project directory, so no path needed
158
+ # claude mcp add --transport stdio mcp-vector-search \
159
+ # --env MCP_ENABLE_FILE_WATCHING=true \
160
+ # -- mcp-vector-search mcp
161
+ cmd = [
162
+ "claude",
163
+ "mcp",
164
+ "add",
165
+ "--transport",
166
+ "stdio",
167
+ server_name,
168
+ "--env",
169
+ f"MCP_ENABLE_FILE_WATCHING={'true' if enable_watch else 'false'}",
170
+ "--",
171
+ "mcp-vector-search",
172
+ "mcp",
173
+ ]
174
+
175
+ if verbose:
176
+ print_info(f" Running: {' '.join(cmd)}")
177
+
178
+ # Run the add command
179
+ result = subprocess.run(
180
+ cmd,
181
+ capture_output=True,
182
+ text=True,
183
+ timeout=30,
184
+ )
185
+
186
+ if result.returncode == 0:
187
+ print_success(" ✅ Registered with Claude CLI")
188
+ if verbose:
189
+ print_info(" Command: claude mcp add mcp")
190
+ return True
191
+ else:
192
+ if verbose:
193
+ print_warning(f" ⚠️ Claude CLI registration failed: {result.stderr}")
194
+ return False
195
+
196
+ except subprocess.TimeoutExpired:
197
+ logger.warning("Claude CLI registration timed out")
198
+ if verbose:
199
+ print_warning(" ⚠️ Claude CLI command timed out")
200
+ return False
201
+ except Exception as e:
202
+ logger.warning(f"Claude CLI registration failed: {e}")
203
+ if verbose:
204
+ print_warning(f" ⚠️ Claude CLI error: {e}")
205
+ return False
206
+
207
+
208
+ def scan_project_file_extensions(
209
+ project_root: Path,
210
+ timeout: float = 2.0,
211
+ ) -> list[str] | None:
212
+ """Scan project for unique file extensions with timeout.
213
+
214
+ This function quickly scans the project to find which file extensions are
215
+ actually in use, allowing for more targeted indexing. If the scan takes too
216
+ long (e.g., very large codebase), it times out and returns None to use defaults.
217
+
218
+ Args:
219
+ project_root: Project root directory to scan
220
+ timeout: Maximum time in seconds to spend scanning (default: 2.0)
221
+
222
+ Returns:
223
+ Sorted list of file extensions found (e.g., ['.py', '.js', '.md'])
224
+ or None if scan timed out or failed
225
+ """
226
+ extensions: set[str] = set()
227
+ start_time = time.time()
228
+ file_count = 0
229
+
230
+ try:
231
+ # Create project manager to get gitignore patterns
232
+ project_manager = ProjectManager(project_root)
233
+
234
+ for path in project_root.rglob("*"):
235
+ # Check timeout
236
+ if time.time() - start_time > timeout:
237
+ logger.debug(
238
+ f"File extension scan timed out after {timeout}s "
239
+ f"({file_count} files scanned)"
240
+ )
241
+ return None
242
+
243
+ # Skip directories
244
+ if not path.is_file():
245
+ continue
246
+
247
+ # Skip ignored paths
248
+ if project_manager._should_ignore_path(path, is_directory=False):
249
+ continue
250
+
251
+ # Get extension
252
+ ext = path.suffix
253
+ if ext:
254
+ # Only include extensions we know about (in language mappings)
255
+ language = get_language_from_extension(ext)
256
+ if language != "text" or ext in [".txt", ".md", ".rst"]:
257
+ extensions.add(ext)
258
+
259
+ file_count += 1
260
+
261
+ elapsed = time.time() - start_time
262
+ logger.debug(
263
+ f"File extension scan completed in {elapsed:.2f}s "
264
+ f"({file_count} files, {len(extensions)} extensions found)"
265
+ )
266
+
267
+ return sorted(extensions) if extensions else None
268
+
269
+ except Exception as e:
270
+ logger.debug(f"File extension scan failed: {e}")
271
+ return None
272
+
273
+
274
+ def select_optimal_embedding_model(languages: list[str]) -> str:
275
+ """Select the best embedding model based on detected languages.
276
+
277
+ Args:
278
+ languages: List of detected language names
279
+
280
+ Returns:
281
+ Name of optimal embedding model
282
+ """
283
+ # For code-heavy projects, use code-optimized model
284
+ if languages:
285
+ code_languages = {"python", "javascript", "typescript", "java", "go", "rust"}
286
+ detected_set = {lang.lower() for lang in languages}
287
+
288
+ if detected_set & code_languages:
289
+ return DEFAULT_EMBEDDING_MODELS["code"]
290
+
291
+ # Default to general-purpose model
292
+ return DEFAULT_EMBEDDING_MODELS["code"]
293
+
294
+
295
+ def _obfuscate_api_key(api_key: str) -> str:
296
+ """Obfuscate API key for display.
297
+
298
+ Shows first 6 characters + "..." + last 4 characters.
299
+ For short keys (<10 chars), shows "****...1234".
300
+
301
+ Args:
302
+ api_key: API key to obfuscate
303
+
304
+ Returns:
305
+ Obfuscated string like "sk-or-...abc1234" or "****...1234"
306
+ """
307
+ if not api_key:
308
+ return "****"
309
+
310
+ if len(api_key) < 10:
311
+ # Short key - show masked prefix
312
+ return f"****...{api_key[-4:]}"
313
+
314
+ # Full key - show first 6 + last 4
315
+ return f"{api_key[:6]}...{api_key[-4:]}"
316
+
317
+
318
+ def setup_llm_api_keys(project_root: Path, interactive: bool = True) -> bool:
319
+ """Check and optionally set up LLM API keys (OpenAI or OpenRouter) for chat command.
320
+
321
+ This function checks for API keys in environment and config file.
322
+ In interactive mode, prompts user to configure either provider.
323
+
324
+ Args:
325
+ project_root: Project root directory
326
+ interactive: Whether to prompt for API key input
327
+
328
+ Returns:
329
+ True if at least one API key is configured, False otherwise
330
+ """
331
+ from ...core.config_utils import (
332
+ delete_openai_api_key,
333
+ delete_openrouter_api_key,
334
+ get_config_file_path,
335
+ get_openai_api_key,
336
+ get_openrouter_api_key,
337
+ get_preferred_llm_provider,
338
+ save_openai_api_key,
339
+ save_openrouter_api_key,
340
+ save_preferred_llm_provider,
341
+ )
342
+
343
+ config_dir = project_root / ".mcp-vector-search"
344
+
345
+ # Check if API keys are already available
346
+ openai_key = get_openai_api_key(config_dir)
347
+ openrouter_key = get_openrouter_api_key(config_dir)
348
+ preferred_provider = get_preferred_llm_provider(config_dir)
349
+
350
+ openai_from_env = bool(os.environ.get("OPENAI_API_KEY"))
351
+ openrouter_from_env = bool(os.environ.get("OPENROUTER_API_KEY"))
352
+
353
+ has_any_key = bool(openai_key or openrouter_key)
354
+
355
+ # Non-interactive mode: just report status
356
+ if not interactive:
357
+ if has_any_key:
358
+ print_success(" ✅ LLM API key(s) found")
359
+ if openai_key:
360
+ source = (
361
+ "Environment variable"
362
+ if openai_from_env
363
+ else f"Config file ({get_config_file_path(config_dir)})"
364
+ )
365
+ print_info(f" OpenAI: ends with {openai_key[-4:]} ({source})")
366
+ if openrouter_key:
367
+ source = (
368
+ "Environment variable"
369
+ if openrouter_from_env
370
+ else f"Config file ({get_config_file_path(config_dir)})"
371
+ )
372
+ print_info(
373
+ f" OpenRouter: ends with {openrouter_key[-4:]} ({source})"
374
+ )
375
+ if preferred_provider:
376
+ print_info(f" Preferred provider: {preferred_provider}")
377
+ print_info(" Chat command is ready to use!")
378
+ return True
379
+ else:
380
+ print_info(" ℹ️ No LLM API keys found")
381
+ print_info("")
382
+ print_info(
383
+ " The 'chat' command uses AI to answer questions about your code."
384
+ )
385
+ print_info("")
386
+ print_info(" [bold cyan]To enable the chat command:[/bold cyan]")
387
+ print_info(" [cyan]Option A - OpenAI (recommended):[/cyan]")
388
+ print_info(
389
+ " 1. Get a key: [cyan]https://platform.openai.com/api-keys[/cyan]"
390
+ )
391
+ print_info(" 2. [yellow]export OPENAI_API_KEY='your-key'[/yellow]")
392
+ print_info("")
393
+ print_info(" [cyan]Option B - OpenRouter:[/cyan]")
394
+ print_info(" 1. Get a key: [cyan]https://openrouter.ai/keys[/cyan]")
395
+ print_info(" 2. [yellow]export OPENROUTER_API_KEY='your-key'[/yellow]")
396
+ print_info("")
397
+ print_info(" Or run: [yellow]mcp-vector-search setup[/yellow]")
398
+ print_info("")
399
+ print_info(
400
+ " [dim]💡 You can skip this for now - search still works![/dim]"
401
+ )
402
+ return False
403
+
404
+ # Interactive mode - prompt for API key setup
405
+ print_info("")
406
+ print_info(" [bold cyan]LLM API Key Setup[/bold cyan]")
407
+ print_info("")
408
+ print_info(" The 'chat' command uses AI to answer questions about your code.")
409
+ print_info(" You can use OpenAI or OpenRouter (or both).")
410
+ print_info("")
411
+
412
+ # Show current status
413
+ if openai_key or openrouter_key:
414
+ print_info(" [bold]Current Configuration:[/bold]")
415
+ if openai_key:
416
+ obfuscated = _obfuscate_api_key(openai_key)
417
+ source = "environment variable" if openai_from_env else "config file"
418
+ print_info(f" • OpenAI: {obfuscated} [dim]({source})[/dim]")
419
+ else:
420
+ print_info(" • OpenAI: [dim]not configured[/dim]")
421
+
422
+ if openrouter_key:
423
+ obfuscated = _obfuscate_api_key(openrouter_key)
424
+ source = "environment variable" if openrouter_from_env else "config file"
425
+ print_info(f" • OpenRouter: {obfuscated} [dim]({source})[/dim]")
426
+ else:
427
+ print_info(" • OpenRouter: [dim]not configured[/dim]")
428
+
429
+ if preferred_provider:
430
+ print_info(f" • Preferred: [cyan]{preferred_provider}[/cyan]")
431
+ print_info("")
432
+
433
+ print_info(" [bold cyan]Options:[/bold cyan]")
434
+ print_info(" 1. Configure OpenAI (recommended, fast & cheap)")
435
+ print_info(" 2. Configure OpenRouter")
436
+ print_info(" 3. Set preferred provider")
437
+ print_info(" 4. Skip / Keep current")
438
+ print_info("")
439
+
440
+ try:
441
+ from ..output import console
442
+
443
+ choice = console.input(" [yellow]Select option (1-4): [/yellow]").strip()
444
+
445
+ if choice == "1":
446
+ # Configure OpenAI
447
+ return _setup_single_provider(
448
+ provider="openai",
449
+ existing_key=openai_key,
450
+ is_from_env=openai_from_env,
451
+ config_dir=config_dir,
452
+ save_func=save_openai_api_key,
453
+ delete_func=delete_openai_api_key,
454
+ get_key_url="https://platform.openai.com/api-keys",
455
+ )
456
+
457
+ elif choice == "2":
458
+ # Configure OpenRouter
459
+ return _setup_single_provider(
460
+ provider="openrouter",
461
+ existing_key=openrouter_key,
462
+ is_from_env=openrouter_from_env,
463
+ config_dir=config_dir,
464
+ save_func=save_openrouter_api_key,
465
+ delete_func=delete_openrouter_api_key,
466
+ get_key_url="https://openrouter.ai/keys",
467
+ )
468
+
469
+ elif choice == "3":
470
+ # Set preferred provider
471
+ if not has_any_key:
472
+ print_warning(" ⚠️ Configure at least one API key first")
473
+ return False
474
+
475
+ print_info("")
476
+ print_info(" [bold]Select preferred provider:[/bold]")
477
+ providers = []
478
+ if openai_key:
479
+ providers.append("openai")
480
+ print_info(" 1. OpenAI")
481
+ if openrouter_key:
482
+ providers.append("openrouter")
483
+ idx = len(providers)
484
+ print_info(f" {idx}. OpenRouter")
485
+
486
+ pref_choice = console.input(
487
+ f"\n [yellow]Select (1-{len(providers)}): [/yellow]"
488
+ ).strip()
489
+
490
+ try:
491
+ idx = int(pref_choice) - 1
492
+ if 0 <= idx < len(providers):
493
+ selected_provider = providers[idx]
494
+ save_preferred_llm_provider(selected_provider, config_dir)
495
+ print_success(
496
+ f" ✅ Preferred provider set to: {selected_provider}"
497
+ )
498
+ return True
499
+ else:
500
+ print_warning(" ⚠️ Invalid selection")
501
+ return has_any_key
502
+ except ValueError:
503
+ print_warning(" ⚠️ Invalid input")
504
+ return has_any_key
505
+
506
+ elif choice == "4" or not choice:
507
+ # Skip / Keep current
508
+ if has_any_key:
509
+ print_info(" ⏭️ Keeping existing configuration")
510
+ return True
511
+ else:
512
+ print_info(" ⏭️ Skipped LLM API key setup")
513
+ return False
514
+
515
+ else:
516
+ print_warning(" ⚠️ Invalid option")
517
+ return has_any_key
518
+
519
+ except KeyboardInterrupt:
520
+ print_info("\n ⏭️ API key setup cancelled")
521
+ return has_any_key
522
+ except Exception as e:
523
+ logger.error(f"Error during API key setup: {e}")
524
+ print_error(f" ❌ Error: {e}")
525
+ return has_any_key
526
+
527
+
528
+ def _setup_single_provider(
529
+ provider: str,
530
+ existing_key: str | None,
531
+ is_from_env: bool,
532
+ config_dir: Path,
533
+ save_func,
534
+ delete_func,
535
+ get_key_url: str,
536
+ ) -> bool:
537
+ """Helper function to set up a single LLM provider.
538
+
539
+ Args:
540
+ provider: Provider name ('openai' or 'openrouter')
541
+ existing_key: Existing API key if any
542
+ is_from_env: Whether existing key is from environment
543
+ config_dir: Config directory path
544
+ save_func: Function to save API key
545
+ delete_func: Function to delete API key
546
+ get_key_url: URL to get API key
547
+
548
+ Returns:
549
+ True if provider is configured, False otherwise
550
+ """
551
+ from ..output import console
552
+
553
+ provider_display = provider.capitalize()
554
+
555
+ print_info("")
556
+ print_info(f" [bold cyan]{provider_display} API Key Setup[/bold cyan]")
557
+ print_info("")
558
+
559
+ if not existing_key:
560
+ print_info(f" Get a key: [cyan]{get_key_url}[/cyan]")
561
+ print_info("")
562
+
563
+ # Show current status
564
+ if existing_key:
565
+ obfuscated = _obfuscate_api_key(existing_key)
566
+ source = "environment variable" if is_from_env else "config file"
567
+ print_info(f" Current: {obfuscated} [dim]({source})[/dim]")
568
+ if is_from_env:
569
+ print_info(" [dim]Note: Environment variable takes precedence[/dim]")
570
+ print_info("")
571
+
572
+ print_info(" [dim]Options:[/dim]")
573
+ if existing_key:
574
+ print_info(" [dim]• Press Enter to keep existing key[/dim]")
575
+ else:
576
+ print_info(" [dim]• Press Enter to skip[/dim]")
577
+ print_info(" [dim]• Enter new key to update[/dim]")
578
+ if existing_key and not is_from_env:
579
+ print_info(" [dim]• Type 'clear' to remove from config[/dim]")
580
+ print_info("")
581
+
582
+ try:
583
+ if existing_key:
584
+ obfuscated = _obfuscate_api_key(existing_key)
585
+ prompt_text = (
586
+ f" [yellow]{provider_display} API key [{obfuscated}]: [/yellow]"
587
+ )
588
+ else:
589
+ prompt_text = (
590
+ f" [yellow]{provider_display} API key (Enter to skip): [/yellow]"
591
+ )
592
+
593
+ user_input = console.input(prompt_text).strip()
594
+
595
+ # Handle different inputs
596
+ if not user_input:
597
+ # Empty input - keep existing or skip
598
+ if existing_key:
599
+ print_info(" ⏭️ Keeping existing API key")
600
+ return True
601
+ else:
602
+ print_info(" ⏭️ Skipped")
603
+ return False
604
+
605
+ elif user_input.lower() in ("clear", "delete", "remove"):
606
+ # Clear the API key
607
+ if not existing_key:
608
+ print_warning(" ⚠️ No API key to clear")
609
+ return False
610
+
611
+ if is_from_env:
612
+ print_warning(" ⚠️ Cannot clear environment variable from config")
613
+ return True
614
+
615
+ # Delete from config file
616
+ try:
617
+ deleted = delete_func(config_dir)
618
+ if deleted:
619
+ print_success(" ✅ API key removed from config")
620
+ return False
621
+ else:
622
+ print_warning(" ⚠️ API key not found in config")
623
+ return False
624
+ except Exception as e:
625
+ print_error(f" ❌ Failed to delete API key: {e}")
626
+ return False
627
+
628
+ else:
629
+ # New API key provided
630
+ try:
631
+ save_func(user_input, config_dir)
632
+ from ...core.config_utils import get_config_file_path
633
+
634
+ config_path = get_config_file_path(config_dir)
635
+ print_success(f" ✅ API key saved to {config_path}")
636
+ print_info(f" Last 4 characters: {user_input[-4:]}")
637
+
638
+ if is_from_env:
639
+ print_warning("")
640
+ print_warning(
641
+ " ⚠️ Note: Environment variable will still take precedence"
642
+ )
643
+
644
+ return True
645
+ except Exception as e:
646
+ print_error(f" ❌ Failed to save API key: {e}")
647
+ return False
648
+
649
+ except KeyboardInterrupt:
650
+ print_info("\n ⏭️ Setup cancelled")
651
+ return bool(existing_key)
652
+ except Exception as e:
653
+ logger.error(f"Error during {provider} setup: {e}")
654
+ print_error(f" ❌ Error: {e}")
655
+ return bool(existing_key)
656
+
657
+
658
+ def setup_openrouter_api_key(project_root: Path, interactive: bool = True) -> bool:
659
+ """Check and optionally set up OpenRouter API key for chat command.
660
+
661
+ This function checks for API key in environment and config file.
662
+ In interactive mode, always prompts user with existing value as default.
663
+
664
+ Args:
665
+ project_root: Project root directory
666
+ interactive: Whether to prompt for API key input
667
+
668
+ Returns:
669
+ True if API key is configured, False otherwise
670
+ """
671
+ from ...core.config_utils import (
672
+ delete_openrouter_api_key,
673
+ get_config_file_path,
674
+ get_openrouter_api_key,
675
+ save_openrouter_api_key,
676
+ )
677
+
678
+ config_dir = project_root / ".mcp-vector-search"
679
+
680
+ # Check if API key is already available
681
+ existing_api_key = get_openrouter_api_key(config_dir)
682
+ is_from_env = bool(os.environ.get("OPENROUTER_API_KEY"))
683
+
684
+ # Show current status
685
+ if existing_api_key and not interactive:
686
+ # Non-interactive: just report status
687
+ print_success(
688
+ f" ✅ OpenRouter API key found (ends with {existing_api_key[-4:]})"
689
+ )
690
+
691
+ # Check where it came from
692
+ if is_from_env:
693
+ print_info(" Source: Environment variable")
694
+ else:
695
+ config_path = get_config_file_path(config_dir)
696
+ print_info(f" Source: Config file ({config_path})")
697
+
698
+ print_info(" Chat command is ready to use!")
699
+ return True
700
+
701
+ if not interactive:
702
+ # No key found and not interactive
703
+ print_info(" ℹ️ OpenRouter API key not found")
704
+ print_info("")
705
+ print_info(" The 'chat' command uses AI to answer questions about your code.")
706
+ print_info(" It requires an OpenRouter API key (free tier available).")
707
+ print_info("")
708
+ print_info(" [bold cyan]To enable the chat command:[/bold cyan]")
709
+ print_info(" 1. Get a free API key: [cyan]https://openrouter.ai/keys[/cyan]")
710
+ print_info(" 2. Option A - Environment variable (recommended for security):")
711
+ print_info(" [yellow]export OPENROUTER_API_KEY='your-key-here'[/yellow]")
712
+ print_info(" 3. Option B - Save to local config (convenient):")
713
+ print_info(" [yellow]mcp-vector-search setup[/yellow]")
714
+ print_info("")
715
+ print_info(" [dim]💡 You can skip this for now - search still works![/dim]")
716
+ return False
717
+
718
+ # Interactive mode - always prompt with existing value as default
719
+ print_info("")
720
+ print_info(" [bold cyan]OpenRouter API Key Setup[/bold cyan]")
721
+ print_info("")
722
+ print_info(" The 'chat' command uses AI to answer questions about your code.")
723
+ print_info(" It requires an OpenRouter API key (free tier available).")
724
+ print_info("")
725
+
726
+ if not existing_api_key:
727
+ print_info(" Get a free API key: [cyan]https://openrouter.ai/keys[/cyan]")
728
+
729
+ # Show current status
730
+ if existing_api_key:
731
+ obfuscated = _obfuscate_api_key(existing_api_key)
732
+ if is_from_env:
733
+ print_info(
734
+ f" Current: {obfuscated} [dim](from environment variable)[/dim]"
735
+ )
736
+ print_info(
737
+ " [dim]Note: Environment variable takes precedence over config file[/dim]"
738
+ )
739
+ else:
740
+ print_info(f" Current: {obfuscated} [dim](from config file)[/dim]")
741
+
742
+ print_info("")
743
+ print_info(" [dim]Options:[/dim]")
744
+ print_info(
745
+ " [dim]• Press Enter to keep existing key (no change)[/dim]"
746
+ if existing_api_key
747
+ else " [dim]• Press Enter to skip[/dim]"
748
+ )
749
+ print_info(" [dim]• Enter new key to update[/dim]")
750
+ if existing_api_key and not is_from_env:
751
+ print_info(" [dim]• Type 'clear' or 'delete' to remove from config[/dim]")
752
+ print_info("")
753
+
754
+ try:
755
+ # Prompt for API key with obfuscated default
756
+ from ..output import console
757
+
758
+ if existing_api_key:
759
+ obfuscated = _obfuscate_api_key(existing_api_key)
760
+ prompt_text = f" [yellow]OpenRouter API key [{obfuscated}]: [/yellow]"
761
+ else:
762
+ prompt_text = (
763
+ " [yellow]OpenRouter API key (press Enter to skip): [/yellow]"
764
+ )
765
+
766
+ user_input = console.input(prompt_text).strip()
767
+
768
+ # Handle different inputs
769
+ if not user_input:
770
+ # Empty input - keep existing or skip
771
+ if existing_api_key:
772
+ print_info(" ⏭️ Keeping existing API key (no change)")
773
+ return True
774
+ else:
775
+ print_info(" ⏭️ Skipped API key setup")
776
+ print_info("")
777
+ print_info(" [dim]You can set it up later by running:[/dim]")
778
+ print_info(" [cyan]mcp-vector-search setup[/cyan]")
779
+ return False
780
+
781
+ elif user_input.lower() in ("clear", "delete", "remove"):
782
+ # Clear the API key
783
+ if not existing_api_key:
784
+ print_warning(" ⚠️ No API key to clear")
785
+ return False
786
+
787
+ if is_from_env:
788
+ print_warning(" ⚠️ Cannot clear environment variable from config")
789
+ print_info(
790
+ " [dim]To remove, unset the OPENROUTER_API_KEY environment variable[/dim]"
791
+ )
792
+ return True
793
+
794
+ # Delete from config file
795
+ try:
796
+ deleted = delete_openrouter_api_key(config_dir)
797
+ if deleted:
798
+ print_success(" ✅ API key removed from config")
799
+ return False
800
+ else:
801
+ print_warning(" ⚠️ API key not found in config")
802
+ return False
803
+ except Exception as e:
804
+ print_error(f" ❌ Failed to delete API key: {e}")
805
+ return False
806
+
807
+ else:
808
+ # New API key provided
809
+ try:
810
+ save_openrouter_api_key(user_input, config_dir)
811
+ config_path = get_config_file_path(config_dir)
812
+ print_success(f" ✅ API key saved to {config_path}")
813
+ print_info(f" Last 4 characters: {user_input[-4:]}")
814
+ print_info(" Chat command is now ready to use!")
815
+
816
+ if is_from_env:
817
+ print_warning("")
818
+ print_warning(
819
+ " ⚠️ Note: Environment variable will still take precedence"
820
+ )
821
+ print_warning(
822
+ " To use the config file key, unset OPENROUTER_API_KEY"
823
+ )
824
+
825
+ return True
826
+ except Exception as e:
827
+ print_error(f" ❌ Failed to save API key: {e}")
828
+ return False
829
+
830
+ except KeyboardInterrupt:
831
+ print_info("\n ⏭️ API key setup cancelled")
832
+ return False
833
+ except Exception as e:
834
+ logger.error(f"Error during API key setup: {e}")
835
+ print_error(f" ❌ Error: {e}")
836
+ return False
837
+
838
+
839
+ # ==============================================================================
840
+ # Main Setup Command
841
+ # ==============================================================================
842
+
843
+
844
+ @setup_app.callback()
845
+ def main(
846
+ ctx: typer.Context,
847
+ force: bool = typer.Option(
848
+ False,
849
+ "--force",
850
+ "-f",
851
+ help="Force re-initialization if already set up",
852
+ rich_help_panel="⚙️ Options",
853
+ ),
854
+ verbose: bool = typer.Option(
855
+ False,
856
+ "--verbose",
857
+ "-v",
858
+ help="Show detailed progress information",
859
+ rich_help_panel="⚙️ Options",
860
+ ),
861
+ save_api_key: bool = typer.Option(
862
+ False,
863
+ "--save-api-key",
864
+ help="Interactively save OpenRouter API key to config",
865
+ rich_help_panel="🤖 Chat Options",
866
+ ),
867
+ ) -> None:
868
+ """🚀 Smart zero-config setup for mcp-vector-search.
869
+
870
+ Automatically detects your project type, languages, and installed MCP platforms,
871
+ then configures everything with sensible defaults. No user input required!
872
+
873
+ [bold cyan]Examples:[/bold cyan]
874
+
875
+ [green]Basic setup (recommended):[/green]
876
+ $ mcp-vector-search setup
877
+
878
+ [green]Force re-setup:[/green]
879
+ $ mcp-vector-search setup --force
880
+
881
+ [green]Verbose output for debugging:[/green]
882
+ $ mcp-vector-search setup --verbose
883
+
884
+ [dim]💡 Tip: This command is idempotent - safe to run multiple times[/dim]
885
+ """
886
+ # Only run main logic if no subcommand was invoked
887
+ if ctx.invoked_subcommand is not None:
888
+ return
889
+
890
+ try:
891
+ asyncio.run(_run_smart_setup(ctx, force, verbose, save_api_key))
892
+ except KeyboardInterrupt:
893
+ print_info("\nSetup interrupted by user")
894
+ raise typer.Exit(0)
895
+ except ProjectInitializationError as e:
896
+ print_error(f"Setup failed: {e}")
897
+ raise typer.Exit(1)
898
+ except Exception as e:
899
+ logger.error(f"Unexpected error during setup: {e}")
900
+ print_error(f"Setup failed: {e}")
901
+ raise typer.Exit(1)
902
+
903
+
904
+ async def _run_smart_setup(
905
+ ctx: typer.Context, force: bool, verbose: bool, save_api_key: bool
906
+ ) -> None:
907
+ """Run the smart setup workflow."""
908
+ console.print(
909
+ Panel.fit(
910
+ "[bold cyan]🚀 Smart Setup for mcp-vector-search[/bold cyan]\n"
911
+ "[dim]Zero-config installation with auto-detection[/dim]",
912
+ border_style="cyan",
913
+ )
914
+ )
915
+
916
+ # Get project root from context or auto-detect
917
+ project_root = ctx.obj.get("project_root") or Path.cwd()
918
+
919
+ # ===========================================================================
920
+ # Phase 1: Detection & Analysis
921
+ # ===========================================================================
922
+ console.print("\n[bold blue]🔍 Detecting project...[/bold blue]")
923
+
924
+ project_manager = ProjectManager(project_root)
925
+
926
+ # Check if already initialized
927
+ already_initialized = project_manager.is_initialized()
928
+ if already_initialized and not force:
929
+ print_success("✅ Project already initialized")
930
+ print_info(" Skipping initialization, configuring MCP platforms...")
931
+ else:
932
+ if verbose:
933
+ print_info(f" Project root: {project_root}")
934
+
935
+ # Detect languages (only if not already initialized, to avoid slow scan)
936
+ languages = []
937
+ if not already_initialized or force:
938
+ print_info(" Detecting languages...")
939
+ languages = project_manager.detect_languages()
940
+ if languages:
941
+ print_success(
942
+ f" ✅ Found {len(languages)} language(s): {', '.join(languages)}"
943
+ )
944
+ else:
945
+ print_info(" No specific languages detected")
946
+
947
+ # Scan for file extensions with timeout
948
+ detected_extensions = None
949
+ if not already_initialized or force:
950
+ print_info(" Scanning file types...")
951
+ detected_extensions = scan_project_file_extensions(project_root, timeout=2.0)
952
+
953
+ if detected_extensions:
954
+ file_types_str = ", ".join(detected_extensions[:10])
955
+ if len(detected_extensions) > 10:
956
+ file_types_str += f" (+ {len(detected_extensions) - 10} more)"
957
+ print_success(f" ✅ Detected {len(detected_extensions)} file type(s)")
958
+ if verbose:
959
+ print_info(f" Extensions: {file_types_str}")
960
+ else:
961
+ print_info(" ⏱️ Scan timed out, using defaults")
962
+
963
+ # Detect installed MCP platforms
964
+ print_info(" Detecting MCP platforms...")
965
+ detected_platforms_list = detect_all_platforms()
966
+
967
+ if detected_platforms_list:
968
+ # Filter out excluded platforms for display
969
+ configurable_platforms = [
970
+ p
971
+ for p in detected_platforms_list
972
+ if p.platform not in EXCLUDED_PLATFORMS_FROM_SETUP
973
+ ]
974
+ excluded_platforms = [
975
+ p
976
+ for p in detected_platforms_list
977
+ if p.platform in EXCLUDED_PLATFORMS_FROM_SETUP
978
+ ]
979
+
980
+ if configurable_platforms:
981
+ platform_names = [p.platform.value for p in configurable_platforms]
982
+ print_success(
983
+ f" ✅ Found {len(platform_names)} platform(s): {', '.join(platform_names)}"
984
+ )
985
+ if verbose:
986
+ for platform_info in configurable_platforms:
987
+ print_info(
988
+ f" {platform_info.platform.value}: {platform_info.config_path}"
989
+ )
990
+
991
+ # Note excluded platforms
992
+ if excluded_platforms:
993
+ excluded_names = [p.platform.value for p in excluded_platforms]
994
+ print_info(
995
+ f" ℹ️ Skipping: {', '.join(excluded_names)} (use 'install mcp --platform' for manual install)"
996
+ )
997
+ else:
998
+ print_info(" No MCP platforms detected (will configure Claude Code)")
999
+
1000
+ # ===========================================================================
1001
+ # Phase 2: Smart Configuration
1002
+ # ===========================================================================
1003
+ if not already_initialized or force:
1004
+ console.print("\n[bold blue]⚙️ Configuring...[/bold blue]")
1005
+
1006
+ # Choose file extensions
1007
+ file_extensions = detected_extensions or DEFAULT_FILE_EXTENSIONS
1008
+ if verbose:
1009
+ print_info(f" File extensions: {', '.join(file_extensions[:10])}...")
1010
+
1011
+ # Choose embedding model
1012
+ embedding_model = select_optimal_embedding_model(languages)
1013
+ print_success(f" ✅ Embedding model: {embedding_model}")
1014
+
1015
+ # Other settings
1016
+ similarity_threshold = 0.5
1017
+ if verbose:
1018
+ print_info(f" Similarity threshold: {similarity_threshold}")
1019
+ print_info(" Auto-indexing: enabled")
1020
+ print_info(" File watching: enabled")
1021
+
1022
+ # ===========================================================================
1023
+ # Phase 3: Initialization
1024
+ # ===========================================================================
1025
+ if not already_initialized or force:
1026
+ console.print("\n[bold blue]🚀 Initializing...[/bold blue]")
1027
+
1028
+ project_manager.initialize(
1029
+ file_extensions=file_extensions,
1030
+ embedding_model=embedding_model,
1031
+ similarity_threshold=similarity_threshold,
1032
+ force=force,
1033
+ )
1034
+
1035
+ print_success("✅ Vector database created")
1036
+ print_success("✅ Configuration saved")
1037
+
1038
+ # ===========================================================================
1039
+ # Phase 4: Indexing
1040
+ # ===========================================================================
1041
+ # Determine if indexing is needed:
1042
+ # 1. Not already initialized (new setup)
1043
+ # 2. Force flag is set
1044
+ # 3. Index database doesn't exist
1045
+ # 4. Index exists but is empty
1046
+ # 5. Files have changed (incremental indexing will handle this)
1047
+ needs_indexing = not already_initialized or force
1048
+
1049
+ if already_initialized and not force:
1050
+ # Check if index exists and has content
1051
+ index_db_path = project_root / ".mcp-vector-search" / "chroma.sqlite3"
1052
+ if not index_db_path.exists():
1053
+ print_info(" Index database not found, will create...")
1054
+ needs_indexing = True
1055
+ else:
1056
+ # Check if index is empty or files have changed
1057
+ # Run incremental indexing to catch any changes
1058
+ print_info(" Checking for file changes...")
1059
+ needs_indexing = True # Always run incremental to catch changes
1060
+
1061
+ if needs_indexing:
1062
+ console.print("\n[bold blue]🔍 Indexing codebase...[/bold blue]")
1063
+
1064
+ from .index import run_indexing
1065
+
1066
+ try:
1067
+ start_time = time.time()
1068
+ await run_indexing(
1069
+ project_root=project_root,
1070
+ force_reindex=force,
1071
+ show_progress=True,
1072
+ )
1073
+ elapsed = time.time() - start_time
1074
+ print_success(f"✅ Indexing completed in {elapsed:.1f}s")
1075
+ except Exception as e:
1076
+ print_error(f"❌ Indexing failed: {e}")
1077
+ print_info(" You can run 'mcp-vector-search index' later")
1078
+ # Continue with MCP setup even if indexing fails
1079
+
1080
+ # ===========================================================================
1081
+ # Phase 5: MCP Integration
1082
+ # ===========================================================================
1083
+ console.print("\n[bold blue]🔗 Configuring MCP integrations...[/bold blue]")
1084
+
1085
+ configured_platforms = []
1086
+ failed_platforms = []
1087
+
1088
+ # Check if Claude CLI is available for enhanced setup
1089
+ claude_cli_available = check_claude_cli_available()
1090
+ if verbose and claude_cli_available:
1091
+ print_info(" ✅ Claude CLI detected, using native integration")
1092
+
1093
+ # Use detected platforms or default to empty list
1094
+ # Filter out excluded platforms (e.g., Claude Desktop) - exclusion already noted in Phase 1
1095
+ platforms_to_configure = [
1096
+ p
1097
+ for p in (detected_platforms_list or [])
1098
+ if p.platform not in EXCLUDED_PLATFORMS_FROM_SETUP
1099
+ ]
1100
+
1101
+ # Configure all detected platforms using new library
1102
+ for platform_info in platforms_to_configure:
1103
+ try:
1104
+ success = _install_to_platform(platform_info, project_root)
1105
+
1106
+ if success:
1107
+ configured_platforms.append(platform_info.platform.value)
1108
+ else:
1109
+ failed_platforms.append(platform_info.platform.value)
1110
+
1111
+ except Exception as e:
1112
+ logger.warning(f"Failed to configure {platform_info.platform.value}: {e}")
1113
+ print_warning(f" ⚠️ {platform_info.platform.value}: {e}")
1114
+ failed_platforms.append(platform_info.platform.value)
1115
+
1116
+ # Summary of MCP configuration
1117
+ if configured_platforms:
1118
+ print_success(f"✅ Configured {len(configured_platforms)} platform(s)")
1119
+ if verbose:
1120
+ for platform in configured_platforms:
1121
+ print_info(f" • {platform}")
1122
+
1123
+ if failed_platforms and verbose:
1124
+ print_warning(f"⚠️ Failed to configure {len(failed_platforms)} platform(s)")
1125
+ for platform in failed_platforms:
1126
+ print_info(f" • {platform}")
1127
+
1128
+ # ===========================================================================
1129
+ # Phase 6: LLM API Key Setup (Optional)
1130
+ # ===========================================================================
1131
+ console.print("\n[bold blue]🤖 Chat Command Setup (Optional)...[/bold blue]")
1132
+ # Always prompt interactively during setup - user can press Enter to skip/keep
1133
+ # The save_api_key flag is now deprecated but kept for backward compatibility
1134
+ llm_configured = setup_llm_api_keys(project_root=project_root, interactive=True)
1135
+
1136
+ # ===========================================================================
1137
+ # Phase 7: Completion
1138
+ # ===========================================================================
1139
+ console.print("\n[bold green]🎉 Setup Complete![/bold green]")
1140
+
1141
+ # Show summary
1142
+ summary_items = []
1143
+ if not already_initialized or force:
1144
+ summary_items.extend(
1145
+ [
1146
+ "Vector database initialized",
1147
+ "Codebase indexed and searchable",
1148
+ ]
1149
+ )
1150
+
1151
+ summary_items.append(f"{len(configured_platforms)} MCP platform(s) configured")
1152
+ summary_items.append("File watching enabled")
1153
+ if llm_configured:
1154
+ summary_items.append("LLM API configured for chat command")
1155
+
1156
+ console.print("\n[bold]What was set up:[/bold]")
1157
+ for item in summary_items:
1158
+ console.print(f" ✅ {item}")
1159
+
1160
+ # Next steps
1161
+ next_steps = [
1162
+ "[cyan]mcp-vector-search search 'your query'[/cyan] - Search your code",
1163
+ "[cyan]mcp-vector-search status[/cyan] - Check project status",
1164
+ ]
1165
+
1166
+ if llm_configured:
1167
+ next_steps.insert(
1168
+ 1, "[cyan]mcp-vector-search chat 'question'[/cyan] - Ask AI about your code"
1169
+ )
1170
+
1171
+ if "claude-code" in configured_platforms:
1172
+ next_steps.insert(0, "Open Claude Code in this directory to use MCP tools")
1173
+
1174
+ print_next_steps(next_steps, title="Ready to Use")
1175
+
1176
+ # Tips
1177
+ if "claude-code" in configured_platforms:
1178
+ console.print(
1179
+ "\n[dim]💡 Tip: Commit .mcp.json to share configuration with your team[/dim]"
1180
+ )
1181
+
1182
+
1183
+ if __name__ == "__main__":
1184
+ setup_app()