mirror of
https://github.com/SuperClaude-Org/SuperClaude_Framework.git
synced 2025-12-29 16:16:08 +00:00
Add API key management during SuperClaude MCP setup
Features:
- Secure API key collection via getpass (hidden input)
- Cross-platform environment variable setup
- Automatic .claude.json configuration with ${ENV_VAR} syntax
- Seamless integration with existing MCP server selection flow
- Skip options for manual configuration later
Implementation:
- Added prompt_api_key() function to setup/utils/ui.py
- Created setup/utils/environment.py for cross-platform env management
- Enhanced MCP server selection in setup/cli/commands/install.py
- Updated MCP component to handle API key configuration
- Preserves user customizations while adding environment variables
Security:
- Hidden input prevents API keys from being displayed
- No logging of sensitive data
- OS-native environment variable storage
- Basic validation with user confirmation
🤖 Generated with [Claude Code](https://claude.ai/code)
Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
@@ -15,8 +15,9 @@ from ...services.config import ConfigService
|
||||
from ...core.validator import Validator
|
||||
from ...utils.ui import (
|
||||
display_header, display_info, display_success, display_error,
|
||||
display_warning, Menu, confirm, ProgressBar, Colors, format_size
|
||||
display_warning, Menu, confirm, ProgressBar, Colors, format_size, prompt_api_key
|
||||
)
|
||||
from ...utils.environment import setup_environment_variables
|
||||
from ...utils.logger import get_logger
|
||||
from ... import DEFAULT_INSTALL_DIR, PROJECT_ROOT, DATA_DIR
|
||||
from . import OperationBase
|
||||
@@ -127,8 +128,47 @@ def get_components_to_install(args: argparse.Namespace, registry: ComponentRegis
|
||||
return interactive_component_selection(registry, config_manager)
|
||||
|
||||
|
||||
def collect_api_keys_for_servers(selected_servers: List[str], mcp_instance) -> Dict[str, str]:
|
||||
"""
|
||||
Collect API keys for servers that require them
|
||||
|
||||
Args:
|
||||
selected_servers: List of selected server keys
|
||||
mcp_instance: MCP component instance
|
||||
|
||||
Returns:
|
||||
Dictionary of environment variable names to API key values
|
||||
"""
|
||||
# Filter servers needing keys
|
||||
servers_needing_keys = [
|
||||
(server_key, mcp_instance.mcp_servers[server_key])
|
||||
for server_key in selected_servers
|
||||
if server_key in mcp_instance.mcp_servers and
|
||||
mcp_instance.mcp_servers[server_key].get("requires_api_key", False)
|
||||
]
|
||||
|
||||
if not servers_needing_keys:
|
||||
return {}
|
||||
|
||||
# Display API key configuration header
|
||||
print(f"\n{Colors.CYAN}{Colors.BRIGHT}═══ API Key Configuration ═══{Colors.RESET}")
|
||||
print(f"{Colors.YELLOW}The following servers require API keys for full functionality:{Colors.RESET}\n")
|
||||
|
||||
collected_keys = {}
|
||||
for server_key, server_info in servers_needing_keys:
|
||||
api_key_env = server_info.get("api_key_env")
|
||||
service_name = server_info["name"]
|
||||
|
||||
if api_key_env:
|
||||
key = prompt_api_key(service_name, api_key_env)
|
||||
if key:
|
||||
collected_keys[api_key_env] = key
|
||||
|
||||
return collected_keys
|
||||
|
||||
|
||||
def select_mcp_servers(registry: ComponentRegistry) -> List[str]:
|
||||
"""Stage 1: MCP Server Selection"""
|
||||
"""Stage 1: MCP Server Selection with API Key Collection"""
|
||||
logger = get_logger()
|
||||
|
||||
try:
|
||||
@@ -173,6 +213,16 @@ def select_mcp_servers(registry: ComponentRegistry) -> List[str]:
|
||||
|
||||
if selected_servers:
|
||||
logger.info(f"Selected MCP servers: {', '.join(selected_servers)}")
|
||||
|
||||
# NEW: Collect API keys for selected servers
|
||||
collected_keys = collect_api_keys_for_servers(selected_servers, mcp_instance)
|
||||
|
||||
# Set up environment variables
|
||||
if collected_keys:
|
||||
setup_environment_variables(collected_keys)
|
||||
|
||||
# Store keys for MCP component to use during installation
|
||||
mcp_instance.collected_api_keys = collected_keys
|
||||
else:
|
||||
logger.info("No MCP servers selected")
|
||||
|
||||
|
||||
@@ -75,6 +75,9 @@ class MCPComponent(Component):
|
||||
|
||||
# This will be set during installation - initialize as empty list
|
||||
self.selected_servers: List[str] = []
|
||||
|
||||
# Store collected API keys for configuration
|
||||
self.collected_api_keys: Dict[str, str] = {}
|
||||
|
||||
def _lock_file(self, file_handle, exclusive: bool = False):
|
||||
"""Cross-platform file locking"""
|
||||
@@ -225,9 +228,23 @@ class MCPComponent(Component):
|
||||
else:
|
||||
self.logger.debug(f"Preserved user customization for '{server_name}.{key}'")
|
||||
|
||||
# NEW: Apply environment variable references for API keys
|
||||
if "env" in existing_server and self.collected_api_keys:
|
||||
for env_key, env_value in existing_server["env"].items():
|
||||
if env_key in self.collected_api_keys and env_value == "":
|
||||
# Update to use environment variable reference
|
||||
existing_server["env"][env_key] = f"${{{env_key}}}"
|
||||
self.logger.info(f"Configured {env_key} to use environment variable")
|
||||
|
||||
self.logger.info(f"Updated existing MCP server '{server_name}' (preserved user customizations)")
|
||||
else:
|
||||
# New server - add complete configuration
|
||||
# Apply environment variable references if we have collected keys
|
||||
if "env" in server_def and self.collected_api_keys:
|
||||
for env_key in server_def["env"]:
|
||||
if env_key in self.collected_api_keys and server_def["env"][env_key] == "":
|
||||
server_def["env"][env_key] = f"${{{env_key}}}"
|
||||
|
||||
existing_config[server_name] = server_def
|
||||
self.logger.info(f"Added new MCP server '{server_name}' from {server_key}")
|
||||
|
||||
@@ -264,6 +281,10 @@ class MCPComponent(Component):
|
||||
|
||||
self.set_selected_servers(selected_servers)
|
||||
|
||||
# NEW: Log collected API keys information
|
||||
if hasattr(self, 'collected_api_keys') and self.collected_api_keys:
|
||||
self.logger.info(f"Using {len(self.collected_api_keys)} collected API keys for configuration")
|
||||
|
||||
# Validate prerequisites
|
||||
success, errors = self.validate_prerequisites()
|
||||
if not success:
|
||||
|
||||
216
setup/utils/environment.py
Normal file
216
setup/utils/environment.py
Normal file
@@ -0,0 +1,216 @@
|
||||
"""
|
||||
Environment variable management for SuperClaude
|
||||
Cross-platform utilities for setting up persistent environment variables
|
||||
"""
|
||||
|
||||
import os
|
||||
import sys
|
||||
import subprocess
|
||||
from pathlib import Path
|
||||
from typing import Dict, Optional
|
||||
from .ui import display_info, display_success, display_warning, Colors
|
||||
from .logger import get_logger
|
||||
|
||||
|
||||
def detect_shell_config() -> Optional[Path]:
|
||||
"""
|
||||
Detect user's shell configuration file
|
||||
|
||||
Returns:
|
||||
Path to the shell configuration file, or None if not found
|
||||
"""
|
||||
home = Path.home()
|
||||
|
||||
# Check in order of preference
|
||||
configs = [
|
||||
home / ".zshrc", # Zsh (Mac default)
|
||||
home / ".bashrc", # Bash
|
||||
home / ".profile", # Generic shell profile
|
||||
home / ".bash_profile" # Mac Bash profile
|
||||
]
|
||||
|
||||
for config in configs:
|
||||
if config.exists():
|
||||
return config
|
||||
|
||||
# Default to .bashrc if none exist (will be created)
|
||||
return home / ".bashrc"
|
||||
|
||||
|
||||
def setup_environment_variables(api_keys: Dict[str, str]) -> bool:
|
||||
"""
|
||||
Set up environment variables across platforms
|
||||
|
||||
Args:
|
||||
api_keys: Dictionary of environment variable names to values
|
||||
|
||||
Returns:
|
||||
True if all variables were set successfully, False otherwise
|
||||
"""
|
||||
logger = get_logger()
|
||||
success = True
|
||||
|
||||
if not api_keys:
|
||||
return True
|
||||
|
||||
print(f"\n{Colors.BLUE}[INFO] Setting up environment variables...{Colors.RESET}")
|
||||
|
||||
for env_var, value in api_keys.items():
|
||||
try:
|
||||
# Set for current session
|
||||
os.environ[env_var] = value
|
||||
|
||||
if os.name == 'nt': # Windows
|
||||
# Use setx for persistent user variable
|
||||
result = subprocess.run(
|
||||
['setx', env_var, value],
|
||||
capture_output=True,
|
||||
text=True
|
||||
)
|
||||
if result.returncode != 0:
|
||||
display_warning(f"Could not set {env_var} persistently: {result.stderr.strip()}")
|
||||
success = False
|
||||
else:
|
||||
logger.info(f"Windows environment variable {env_var} set persistently")
|
||||
else: # Unix-like systems
|
||||
shell_config = detect_shell_config()
|
||||
|
||||
# Check if the export already exists
|
||||
export_line = f'export {env_var}="{value}"'
|
||||
|
||||
try:
|
||||
with open(shell_config, 'r') as f:
|
||||
content = f.read()
|
||||
|
||||
# Check if this environment variable is already set
|
||||
if f'export {env_var}=' in content:
|
||||
# Variable exists - don't duplicate
|
||||
logger.info(f"Environment variable {env_var} already exists in {shell_config.name}")
|
||||
else:
|
||||
# Append export to shell config
|
||||
with open(shell_config, 'a') as f:
|
||||
f.write(f'\n# SuperClaude API Key\n{export_line}\n')
|
||||
|
||||
display_info(f"Added {env_var} to {shell_config.name}")
|
||||
logger.info(f"Added {env_var} to {shell_config}")
|
||||
|
||||
except Exception as e:
|
||||
display_warning(f"Could not update {shell_config.name}: {e}")
|
||||
success = False
|
||||
|
||||
logger.info(f"Environment variable {env_var} configured for current session")
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"Failed to set {env_var}: {e}")
|
||||
display_warning(f"Failed to set {env_var}: {e}")
|
||||
success = False
|
||||
|
||||
if success:
|
||||
display_success("Environment variables configured successfully")
|
||||
if os.name != 'nt':
|
||||
display_info("Restart your terminal or run 'source ~/.bashrc' to apply changes")
|
||||
else:
|
||||
display_info("New environment variables will be available in new terminal sessions")
|
||||
else:
|
||||
display_warning("Some environment variables could not be set persistently")
|
||||
display_info("You can set them manually or check the logs for details")
|
||||
|
||||
return success
|
||||
|
||||
|
||||
def validate_environment_setup(env_vars: Dict[str, str]) -> bool:
|
||||
"""
|
||||
Validate that environment variables are properly set
|
||||
|
||||
Args:
|
||||
env_vars: Dictionary of environment variable names to expected values
|
||||
|
||||
Returns:
|
||||
True if all variables are set correctly, False otherwise
|
||||
"""
|
||||
logger = get_logger()
|
||||
all_valid = True
|
||||
|
||||
for env_var, expected_value in env_vars.items():
|
||||
current_value = os.environ.get(env_var)
|
||||
|
||||
if current_value is None:
|
||||
logger.warning(f"Environment variable {env_var} is not set")
|
||||
all_valid = False
|
||||
elif current_value != expected_value:
|
||||
logger.warning(f"Environment variable {env_var} has unexpected value")
|
||||
all_valid = False
|
||||
else:
|
||||
logger.info(f"Environment variable {env_var} is set correctly")
|
||||
|
||||
return all_valid
|
||||
|
||||
|
||||
def get_shell_name() -> str:
|
||||
"""
|
||||
Get the name of the current shell
|
||||
|
||||
Returns:
|
||||
Name of the shell (e.g., 'bash', 'zsh', 'fish')
|
||||
"""
|
||||
shell_path = os.environ.get('SHELL', '')
|
||||
if shell_path:
|
||||
return Path(shell_path).name
|
||||
return 'unknown'
|
||||
|
||||
|
||||
def create_env_file(api_keys: Dict[str, str], env_file_path: Optional[Path] = None) -> bool:
|
||||
"""
|
||||
Create a .env file with the API keys (alternative to shell config)
|
||||
|
||||
Args:
|
||||
api_keys: Dictionary of environment variable names to values
|
||||
env_file_path: Path to the .env file (defaults to home directory)
|
||||
|
||||
Returns:
|
||||
True if .env file was created successfully, False otherwise
|
||||
"""
|
||||
if env_file_path is None:
|
||||
env_file_path = Path.home() / ".env"
|
||||
|
||||
logger = get_logger()
|
||||
|
||||
try:
|
||||
# Read existing .env file if it exists
|
||||
existing_content = ""
|
||||
if env_file_path.exists():
|
||||
with open(env_file_path, 'r') as f:
|
||||
existing_content = f.read()
|
||||
|
||||
# Prepare new content
|
||||
new_lines = []
|
||||
for env_var, value in api_keys.items():
|
||||
line = f'{env_var}="{value}"'
|
||||
|
||||
# Check if this variable already exists
|
||||
if f'{env_var}=' in existing_content:
|
||||
logger.info(f"Variable {env_var} already exists in .env file")
|
||||
else:
|
||||
new_lines.append(line)
|
||||
|
||||
# Append new lines if any
|
||||
if new_lines:
|
||||
with open(env_file_path, 'a') as f:
|
||||
if existing_content and not existing_content.endswith('\n'):
|
||||
f.write('\n')
|
||||
f.write('# SuperClaude API Keys\n')
|
||||
for line in new_lines:
|
||||
f.write(line + '\n')
|
||||
|
||||
# Set file permissions (readable only by owner)
|
||||
env_file_path.chmod(0o600)
|
||||
|
||||
display_success(f"Created .env file at {env_file_path}")
|
||||
logger.info(f"Created .env file with {len(new_lines)} new variables")
|
||||
|
||||
return True
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"Failed to create .env file: {e}")
|
||||
display_warning(f"Could not create .env file: {e}")
|
||||
return False
|
||||
@@ -6,6 +6,7 @@ Cross-platform console UI with colors and progress indication
|
||||
import sys
|
||||
import time
|
||||
import shutil
|
||||
import getpass
|
||||
from typing import List, Optional, Any, Dict, Union
|
||||
from enum import Enum
|
||||
|
||||
@@ -339,6 +340,43 @@ def display_table(headers: List[str], rows: List[List[str]], title: str = '') ->
|
||||
print()
|
||||
|
||||
|
||||
def prompt_api_key(service_name: str, env_var_name: str) -> Optional[str]:
|
||||
"""
|
||||
Prompt for API key with security and UX best practices
|
||||
|
||||
Args:
|
||||
service_name: Human-readable service name (e.g., "Magic", "Morphllm")
|
||||
env_var_name: Environment variable name (e.g., "TWENTYFIRST_API_KEY")
|
||||
|
||||
Returns:
|
||||
API key string if provided, None if skipped
|
||||
"""
|
||||
print(f"{Colors.BLUE}[API KEY] {service_name} requires: {Colors.BRIGHT}{env_var_name}{Colors.RESET}")
|
||||
print(f"{Colors.WHITE}Visit the service documentation to obtain your API key{Colors.RESET}")
|
||||
print(f"{Colors.YELLOW}Press Enter to skip (you can set this manually later){Colors.RESET}")
|
||||
|
||||
try:
|
||||
# Use getpass for hidden input
|
||||
api_key = getpass.getpass(f"Enter {env_var_name}: ").strip()
|
||||
|
||||
if not api_key:
|
||||
print(f"{Colors.YELLOW}[SKIPPED] {env_var_name} - set manually later{Colors.RESET}")
|
||||
return None
|
||||
|
||||
# Basic validation (non-empty, reasonable length)
|
||||
if len(api_key) < 10:
|
||||
print(f"{Colors.RED}[WARNING] API key seems too short. Continue anyway? (y/N){Colors.RESET}")
|
||||
if not confirm("", default=False):
|
||||
return None
|
||||
|
||||
print(f"{Colors.GREEN}[✓] {env_var_name} configured{Colors.RESET}")
|
||||
return api_key
|
||||
|
||||
except KeyboardInterrupt:
|
||||
print(f"\n{Colors.YELLOW}[SKIPPED] {env_var_name}{Colors.RESET}")
|
||||
return None
|
||||
|
||||
|
||||
def wait_for_key(message: str = "Press Enter to continue...") -> None:
|
||||
"""Wait for user to press a key"""
|
||||
try:
|
||||
|
||||
Reference in New Issue
Block a user