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.
- mcp_vector_search/__init__.py +3 -3
- mcp_vector_search/analysis/__init__.py +111 -0
- mcp_vector_search/analysis/baseline/__init__.py +68 -0
- mcp_vector_search/analysis/baseline/comparator.py +462 -0
- mcp_vector_search/analysis/baseline/manager.py +621 -0
- mcp_vector_search/analysis/collectors/__init__.py +74 -0
- mcp_vector_search/analysis/collectors/base.py +164 -0
- mcp_vector_search/analysis/collectors/cohesion.py +463 -0
- mcp_vector_search/analysis/collectors/complexity.py +743 -0
- mcp_vector_search/analysis/collectors/coupling.py +1162 -0
- mcp_vector_search/analysis/collectors/halstead.py +514 -0
- mcp_vector_search/analysis/collectors/smells.py +325 -0
- mcp_vector_search/analysis/debt.py +516 -0
- mcp_vector_search/analysis/interpretation.py +685 -0
- mcp_vector_search/analysis/metrics.py +414 -0
- mcp_vector_search/analysis/reporters/__init__.py +7 -0
- mcp_vector_search/analysis/reporters/console.py +646 -0
- mcp_vector_search/analysis/reporters/markdown.py +480 -0
- mcp_vector_search/analysis/reporters/sarif.py +377 -0
- mcp_vector_search/analysis/storage/__init__.py +93 -0
- mcp_vector_search/analysis/storage/metrics_store.py +762 -0
- mcp_vector_search/analysis/storage/schema.py +245 -0
- mcp_vector_search/analysis/storage/trend_tracker.py +560 -0
- mcp_vector_search/analysis/trends.py +308 -0
- mcp_vector_search/analysis/visualizer/__init__.py +90 -0
- mcp_vector_search/analysis/visualizer/d3_data.py +534 -0
- mcp_vector_search/analysis/visualizer/exporter.py +484 -0
- mcp_vector_search/analysis/visualizer/html_report.py +2895 -0
- mcp_vector_search/analysis/visualizer/schemas.py +525 -0
- mcp_vector_search/cli/commands/analyze.py +1062 -0
- mcp_vector_search/cli/commands/chat.py +1455 -0
- mcp_vector_search/cli/commands/index.py +621 -5
- mcp_vector_search/cli/commands/index_background.py +467 -0
- mcp_vector_search/cli/commands/init.py +13 -0
- mcp_vector_search/cli/commands/install.py +597 -335
- mcp_vector_search/cli/commands/install_old.py +8 -4
- mcp_vector_search/cli/commands/mcp.py +78 -6
- mcp_vector_search/cli/commands/reset.py +68 -26
- mcp_vector_search/cli/commands/search.py +224 -8
- mcp_vector_search/cli/commands/setup.py +1184 -0
- mcp_vector_search/cli/commands/status.py +339 -5
- mcp_vector_search/cli/commands/uninstall.py +276 -357
- mcp_vector_search/cli/commands/visualize/__init__.py +39 -0
- mcp_vector_search/cli/commands/visualize/cli.py +292 -0
- mcp_vector_search/cli/commands/visualize/exporters/__init__.py +12 -0
- mcp_vector_search/cli/commands/visualize/exporters/html_exporter.py +33 -0
- mcp_vector_search/cli/commands/visualize/exporters/json_exporter.py +33 -0
- mcp_vector_search/cli/commands/visualize/graph_builder.py +647 -0
- mcp_vector_search/cli/commands/visualize/layout_engine.py +469 -0
- mcp_vector_search/cli/commands/visualize/server.py +600 -0
- mcp_vector_search/cli/commands/visualize/state_manager.py +428 -0
- mcp_vector_search/cli/commands/visualize/templates/__init__.py +16 -0
- mcp_vector_search/cli/commands/visualize/templates/base.py +234 -0
- mcp_vector_search/cli/commands/visualize/templates/scripts.py +4542 -0
- mcp_vector_search/cli/commands/visualize/templates/styles.py +2522 -0
- mcp_vector_search/cli/didyoumean.py +27 -2
- mcp_vector_search/cli/main.py +127 -160
- mcp_vector_search/cli/output.py +158 -13
- mcp_vector_search/config/__init__.py +4 -0
- mcp_vector_search/config/default_thresholds.yaml +52 -0
- mcp_vector_search/config/settings.py +12 -0
- mcp_vector_search/config/thresholds.py +273 -0
- mcp_vector_search/core/__init__.py +16 -0
- mcp_vector_search/core/auto_indexer.py +3 -3
- mcp_vector_search/core/boilerplate.py +186 -0
- mcp_vector_search/core/config_utils.py +394 -0
- mcp_vector_search/core/database.py +406 -94
- mcp_vector_search/core/embeddings.py +24 -0
- mcp_vector_search/core/exceptions.py +11 -0
- mcp_vector_search/core/git.py +380 -0
- mcp_vector_search/core/git_hooks.py +4 -4
- mcp_vector_search/core/indexer.py +632 -54
- mcp_vector_search/core/llm_client.py +756 -0
- mcp_vector_search/core/models.py +91 -1
- mcp_vector_search/core/project.py +17 -0
- mcp_vector_search/core/relationships.py +473 -0
- mcp_vector_search/core/scheduler.py +11 -11
- mcp_vector_search/core/search.py +179 -29
- mcp_vector_search/mcp/server.py +819 -9
- mcp_vector_search/parsers/python.py +285 -5
- mcp_vector_search/utils/__init__.py +2 -0
- mcp_vector_search/utils/gitignore.py +0 -3
- mcp_vector_search/utils/gitignore_updater.py +212 -0
- mcp_vector_search/utils/monorepo.py +66 -4
- mcp_vector_search/utils/timing.py +10 -6
- {mcp_vector_search-0.12.6.dist-info → mcp_vector_search-1.1.22.dist-info}/METADATA +184 -53
- mcp_vector_search-1.1.22.dist-info/RECORD +120 -0
- {mcp_vector_search-0.12.6.dist-info → mcp_vector_search-1.1.22.dist-info}/WHEEL +1 -1
- {mcp_vector_search-0.12.6.dist-info → mcp_vector_search-1.1.22.dist-info}/entry_points.txt +1 -0
- mcp_vector_search/cli/commands/visualize.py +0 -1467
- mcp_vector_search-0.12.6.dist-info/RECORD +0 -68
- {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()
|