-
-
Notifications
You must be signed in to change notification settings - Fork 44
fix(wizard): allow setup to continue when API key is already present in ENV #356
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: main
Are you sure you want to change the base?
Changes from all commits
5b20ab8
668db9e
a2186c5
def8a8f
1b96bd0
2b5fefc
5a56769
c0c3ce0
9afe27d
f8dff36
8d13202
2a59781
1aafef3
c4d1a4b
3d12434
8b4b8f5
d437c34
4ca3a42
d1b3df9
d5f863e
43aa134
9b54c87
4f68679
6ad2c30
8c6ecfa
591a931
3f6b837
f8a00a9
050dc82
b79c04e
9098738
ebabeb1
82595c8
14cf97a
0729505
60a4ccb
716d408
6a8b0b5
5c2b554
1c09517
a7985d2
e50568a
c5ce259
d60a188
c6d5377
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -1,3 +1,4 @@ | ||
| <<<<<<< HEAD | ||
| # Cortex Linux Environment Configuration | ||
| # Copy this file to .env and configure your settings | ||
|
|
||
|
|
@@ -59,3 +60,10 @@ OLLAMA_MODEL=llama3.2 | |
| # - ~/.cortex/.env | ||
| # - /etc/cortex/.env (Linux only) | ||
| # | ||
| ======= | ||
| # Example .env file for Cortex | ||
| # Copy this to .env and fill in your API keys | ||
|
|
||
| OPENAI_API_KEY=sk-your-openai-key-here | ||
| ANTHROPIC_API_KEY=sk-ant-your-anthropic-key-here | ||
| >>>>>>> 34c66df (feat: seamless onboarding—auto-create .env from .env.example, improved wizard flow) | ||
|
Collaborator
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. remove this one as well. |
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -692,3 +692,34 @@ def setup_api_key() -> tuple[bool, str | None, str | None]: | |
| return (True, key, provider) | ||
|
|
||
| return (False, None, None) | ||
|
|
||
|
|
||
| # Convenience functions for backward compatibility | ||
| def detect_api_key(provider: str) -> str | None: | ||
| """ | ||
| Detect API key for a specific provider. | ||
|
|
||
| Args: | ||
| provider: The provider name ('anthropic', 'openai') | ||
|
|
||
| Returns: | ||
| The API key or None if not found | ||
| """ | ||
| found, key, detected_provider, source = auto_detect_api_key() | ||
| if found and detected_provider == provider: | ||
| return key | ||
| return None | ||
|
|
||
|
|
||
| def get_detected_provider() -> str | None: | ||
| """ | ||
| Get the detected provider name. | ||
|
|
||
| Returns: | ||
| The provider name or None if not detected | ||
| """ | ||
| found, key, provider, source = auto_detect_api_key() | ||
| return provider if found else None | ||
|
|
||
|
|
||
| SUPPORTED_PROVIDERS = ["anthropic", "openai", "ollama"] | ||
|
Comment on lines
+697
to
+725
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
Right now Proposed direction (keep backward-compat but make semantics predictable) def detect_api_key(provider: str) -> str | None:
@@
- found, key, detected_provider, source = auto_detect_api_key()
- if found and detected_provider == provider:
- return key
- return None
+ # Provider-scoped check: look at the specific env var first, then fall back to full detection.
+ provider = provider.lower().strip()
+ if provider == "anthropic":
+ key = os.environ.get("ANTHROPIC_API_KEY")
+ return key if key else None
+ if provider == "openai":
+ key = os.environ.get("OPENAI_API_KEY")
+ return key if key else None
+ if provider == "ollama":
+ # Presence-based; callers can treat "ollama-local" as configured.
+ return "ollama-local" if os.environ.get("CORTEX_PROVIDER", "").lower() == "ollama" else None
+
+ found, key, detected_provider, _source = auto_detect_api_key()
+ return key if found and detected_provider == provider else None(If you want this to search non-env locations per provider too, the clean solution is to add a real provider-scoped search inside |
||
| Original file line number | Diff line number | Diff line change | ||||||
|---|---|---|---|---|---|---|---|---|
|
|
@@ -19,6 +19,7 @@ | |||||||
| format_package_list, | ||||||||
| ) | ||||||||
| from cortex.env_manager import EnvironmentManager, get_env_manager | ||||||||
| from cortex.first_run_wizard import FirstRunWizard | ||||||||
| from cortex.installation_history import InstallationHistory, InstallationStatus, InstallationType | ||||||||
| from cortex.llm.interpreter import CommandInterpreter | ||||||||
| from cortex.network_config import NetworkConfig | ||||||||
|
|
@@ -125,26 +126,35 @@ def _debug(self, message: str): | |||||||
| if self.verbose: | ||||||||
| console.print(f"[dim][DEBUG] {message}[/dim]") | ||||||||
|
|
||||||||
| def _get_api_key(self) -> str | None: | ||||||||
| # 1. Check explicit provider override first (fake/ollama need no key) | ||||||||
| explicit_provider = os.environ.get("CORTEX_PROVIDER", "").lower() | ||||||||
| if explicit_provider == "fake": | ||||||||
| self._debug("Using Fake provider for testing") | ||||||||
| return "fake-key" | ||||||||
| if explicit_provider == "ollama": | ||||||||
| self._debug("Using Ollama (no API key required)") | ||||||||
| def _get_api_key_for_provider(self, provider: str) -> str | None: | ||||||||
| """Get API key for a specific provider.""" | ||||||||
| if provider == "ollama": | ||||||||
| return "ollama-local" | ||||||||
| if provider == "fake": | ||||||||
| return "fake-key" | ||||||||
| if provider == "claude": | ||||||||
| key = os.environ.get("ANTHROPIC_API_KEY") | ||||||||
| if key and key.strip().startswith("sk-ant-"): | ||||||||
| return key.strip() | ||||||||
| elif provider == "openai": | ||||||||
| key = os.environ.get("OPENAI_API_KEY") | ||||||||
| if key and key.strip().startswith("sk-"): | ||||||||
| return key.strip() | ||||||||
| return None | ||||||||
|
|
||||||||
| # 2. Try auto-detection + prompt to save (setup_api_key handles both) | ||||||||
| success, key, detected_provider = setup_api_key() | ||||||||
| if success: | ||||||||
| self._debug(f"Using {detected_provider} API key") | ||||||||
| # Store detected provider so _get_provider can use it | ||||||||
| self._detected_provider = detected_provider | ||||||||
| def _get_api_key(self) -> str | None: | ||||||||
| """Get API key for the current provider.""" | ||||||||
| provider = self._get_provider() | ||||||||
| key = self._get_api_key_for_provider(provider) | ||||||||
| if key: | ||||||||
| return key | ||||||||
|
|
||||||||
| # Still no key | ||||||||
| self._print_error("No API key found or provided") | ||||||||
| # If provider is ollama or no key is set, always fallback to ollama-local | ||||||||
| if provider == "ollama" or not ( | ||||||||
| os.environ.get("OPENAI_API_KEY") or os.environ.get("ANTHROPIC_API_KEY") | ||||||||
| ): | ||||||||
| return "ollama-local" | ||||||||
| # Otherwise, prompt user for setup | ||||||||
| self._print_error("No valid API key found.") | ||||||||
| cx_print("Run [bold]cortex wizard[/bold] to configure your API key.", "info") | ||||||||
| cx_print("Or use [bold]CORTEX_PROVIDER=ollama[/bold] for offline mode.", "info") | ||||||||
| return None | ||||||||
|
Comment on lines
+129
to
160
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Align key validation with wizard rules and avoid misclassifying Anthropic keys as OpenAI. Proposed fix def _get_api_key_for_provider(self, provider: str) -> str | None:
"""Get API key for a specific provider."""
@@
elif provider == "openai":
key = os.environ.get("OPENAI_API_KEY")
- if key and key.strip().startswith("sk-"):
+ if key and key.strip().startswith("sk-") and not key.strip().startswith("sk-ant-"):
return key.strip()
return None |
||||||||
|
|
@@ -168,7 +178,7 @@ def _get_provider(self) -> str: | |||||||
| elif os.environ.get("OPENAI_API_KEY"): | ||||||||
| return "openai" | ||||||||
|
|
||||||||
| # Fallback to Ollama for offline mode | ||||||||
| # No API keys available - default to Ollama for offline mode | ||||||||
| return "ollama" | ||||||||
|
|
||||||||
| def _print_status(self, emoji: str, message: str): | ||||||||
|
|
@@ -638,6 +648,7 @@ def install( | |||||||
| execute: bool = False, | ||||||||
| dry_run: bool = False, | ||||||||
| parallel: bool = False, | ||||||||
| forced_provider: str | None = None, | ||||||||
| ): | ||||||||
| # Validate input first | ||||||||
| is_valid, error = validate_install_request(software) | ||||||||
|
|
@@ -658,41 +669,55 @@ def install( | |||||||
| "pip3 install jupyter numpy pandas" | ||||||||
| ) | ||||||||
|
|
||||||||
| api_key = self._get_api_key() | ||||||||
| if not api_key: | ||||||||
| return 1 | ||||||||
|
|
||||||||
| provider = self._get_provider() | ||||||||
| self._debug(f"Using provider: {provider}") | ||||||||
| self._debug(f"API key: {api_key[:10]}...{api_key[-4:]}") | ||||||||
|
|
||||||||
| # Initialize installation history | ||||||||
| # Try providers in order | ||||||||
| initial_provider = forced_provider or self._get_provider() | ||||||||
| providers_to_try = [initial_provider] | ||||||||
| if initial_provider in ["claude", "openai"]: | ||||||||
| other_provider = "openai" if initial_provider == "claude" else "claude" | ||||||||
| if self._get_api_key_for_provider(other_provider): | ||||||||
| providers_to_try.append(other_provider) | ||||||||
|
|
||||||||
| commands = None | ||||||||
| provider = None # noqa: F841 - assigned in loop | ||||||||
|
||||||||
| api_key = None | ||||||||
| history = InstallationHistory() | ||||||||
| install_id = None | ||||||||
| start_time = datetime.now() | ||||||||
| for try_provider in providers_to_try: | ||||||||
| try: | ||||||||
| try_api_key = self._get_api_key_for_provider(try_provider) or "dummy-key" | ||||||||
| self._debug(f"Trying provider: {try_provider}") | ||||||||
| interpreter = CommandInterpreter(api_key=try_api_key, provider=try_provider) | ||||||||
|
|
||||||||
| try: | ||||||||
| self._print_status("🧠", "Understanding request...") | ||||||||
| self._print_status("🧠", "Understanding request...") | ||||||||
|
|
||||||||
| interpreter = CommandInterpreter(api_key=api_key, provider=provider) | ||||||||
| self._print_status("📦", "Planning installation...") | ||||||||
|
|
||||||||
| self._print_status("📦", "Planning installation...") | ||||||||
| for _ in range(10): | ||||||||
| self._animate_spinner("Analyzing system requirements...") | ||||||||
| self._clear_line() | ||||||||
|
|
||||||||
| for _ in range(10): | ||||||||
| self._animate_spinner("Analyzing system requirements...") | ||||||||
| self._clear_line() | ||||||||
| commands = interpreter.parse(f"install {software}") | ||||||||
|
|
||||||||
| commands = interpreter.parse(f"install {software}") | ||||||||
| if commands: | ||||||||
| provider = try_provider | ||||||||
|
||||||||
| api_key = try_api_key | ||||||||
|
||||||||
| break | ||||||||
| else: | ||||||||
| self._debug(f"No commands generated with {try_provider}") | ||||||||
| except (RuntimeError, Exception) as e: | ||||||||
| self._debug(f"API call failed with {try_provider}: {e}") | ||||||||
| continue | ||||||||
|
|
||||||||
| if not commands: | ||||||||
| self._print_error( | ||||||||
| "No commands generated. Please try again with a different request." | ||||||||
| ) | ||||||||
| return 1 | ||||||||
| if not commands: | ||||||||
| self._print_error( | ||||||||
| "No commands generated with any available provider. Please try again with a different request." | ||||||||
| ) | ||||||||
| return 1 | ||||||||
|
|
||||||||
| try: | ||||||||
| install_id = None | ||||||||
| # Extract packages from commands for tracking | ||||||||
| packages = history._extract_packages_from_commands(commands) | ||||||||
|
|
||||||||
| # Record installation start | ||||||||
| if execute or dry_run: | ||||||||
| install_id = history.record_installation( | ||||||||
|
|
@@ -1017,14 +1042,15 @@ def wizard(self): | |||||||
| """Interactive setup wizard for API key configuration""" | ||||||||
| show_banner() | ||||||||
| console.print() | ||||||||
| cx_print("Welcome to Cortex Setup Wizard!", "success") | ||||||||
| console.print() | ||||||||
| # (Simplified for brevity - keeps existing logic) | ||||||||
| cx_print("Please export your API key in your shell profile.", "info") | ||||||||
| return 0 | ||||||||
| # Run the actual first-run wizard | ||||||||
| wizard = FirstRunWizard(interactive=True) | ||||||||
| success = wizard.run() | ||||||||
| return 0 if success else 1 | ||||||||
|
|
||||||||
| def env(self, args: argparse.Namespace) -> int: | ||||||||
| """Handle environment variable management commands.""" | ||||||||
| import sys | ||||||||
|
|
||||||||
|
Comment on lines
+1052
to
+1053
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🛠️ Refactor suggestion | 🟠 Major Remove redundant
🔎 Proposed fix def env(self, args: argparse.Namespace) -> int:
"""Handle environment variable management commands."""
- import sys
-
env_mgr = get_env_manager()🤖 Prompt for AI Agents |
||||||||
| env_mgr = get_env_manager() | ||||||||
|
|
||||||||
| # Handle subcommand routing | ||||||||
|
|
@@ -1067,15 +1093,8 @@ def env(self, args: argparse.Namespace) -> int: | |||||||
| else: | ||||||||
| self._print_error(f"Unknown env subcommand: {action}") | ||||||||
| return 1 | ||||||||
| except (ValueError, OSError) as e: | ||||||||
| self._print_error(f"Environment operation failed: {e}") | ||||||||
| return 1 | ||||||||
| except Exception as e: | ||||||||
| self._print_error(f"Unexpected error: {e}") | ||||||||
| if self.verbose: | ||||||||
| import traceback | ||||||||
|
|
||||||||
| traceback.print_exc() | ||||||||
| self._print_error(f"Environment operation failed: {e}") | ||||||||
| return 1 | ||||||||
|
|
||||||||
| def _env_set(self, env_mgr: EnvironmentManager, args: argparse.Namespace) -> int: | ||||||||
|
|
@@ -1108,8 +1127,7 @@ def _env_set(self, env_mgr: EnvironmentManager, args: argparse.Namespace) -> int | |||||||
| return 1 | ||||||||
| except ImportError as e: | ||||||||
| self._print_error(str(e)) | ||||||||
| if "cryptography" in str(e).lower(): | ||||||||
| cx_print("Install with: pip install cryptography", "info") | ||||||||
| cx_print("Install with: pip install cryptography", "info") | ||||||||
|
||||||||
| cx_print("Install with: pip install cryptography", "info") | |
| if "cryptography" in str(e).lower(): | |
| cx_print("Install with: pip install cryptography", "info") |
Copilot
AI
Jan 5, 2026
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The change modifies the return value from always returning 0 (success) to returning 1 (failure) when errors are present. However, the original comment stated "Return success (0) even with partial errors - some vars imported successfully". This change in behavior means that if ANY variables fail to import (even if many succeed), the entire operation is now considered a failure. This could be a breaking change for scripts that rely on partial success. Consider whether this behavioral change is intentional and document it in the PR description if so.
| Original file line number | Diff line number | Diff line change | ||||||
|---|---|---|---|---|---|---|---|---|
|
|
@@ -33,11 +33,24 @@ def get_env_file_locations() -> list[Path]: | |||||||
| cwd_env = Path.cwd() / ".env" | ||||||||
| locations.append(cwd_env) | ||||||||
|
|
||||||||
| # 2. User's home directory .cortex folder | ||||||||
| # 2. Parent directory (for project root .env) | ||||||||
| parent_env = Path.cwd().parent / ".env" | ||||||||
| locations.append(parent_env) | ||||||||
|
|
||||||||
| # 3. Cortex package directory .env | ||||||||
| try: | ||||||||
| import cortex | ||||||||
|
|
||||||||
| cortex_dir = Path(cortex.__file__).parent / ".env" | ||||||||
| locations.append(cortex_dir) | ||||||||
| except ImportError: | ||||||||
|
||||||||
| except ImportError: | |
| except ImportError: | |
| # The cortex package may not be installed; skip its optional .env location. |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Remove this comments.