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