Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
45 commits
Select commit Hold shift + click to select a range
5b20ab8
fix: connect wizard command to FirstRunWizard implementation
Dec 24, 2025
668db9e
feat: show API provider menu with key detection indicators
Dec 24, 2025
a2186c5
feat(wizard): streamline wizard to only handle API key configuration\…
Dec 24, 2025
def8a8f
style: format first_run_wizard.py with black for lint compliance
Dec 24, 2025
1b96bd0
Improve cortex wizard UX: auto-detect providers, lazy key validation,…
Dec 24, 2025
2b5fefc
Fix linting issues: remove duplicate imports, organize imports, fix w…
Dec 24, 2025
5a56769
Format code with Black to fix CI formatting checks
Dec 24, 2025
c0c3ce0
Fix tests: update python commands to python3, catch Exception in prov…
Dec 24, 2025
9afe27d
Fix remaining CI test failures: update env loading priority, add miss…
Dec 24, 2025
f8dff36
Fix API key fallback logic and install test failures
Dec 24, 2025
8d13202
Fix cortex wizard OpenAI API key detection regression
Dec 24, 2025
2a59781
Fix test_install_no_api_key by adding CommandInterpreter mock
Dec 24, 2025
1aafef3
fix(cli): allow install without API key
Dec 24, 2025
c4d1a4b
Remove unnecessary _get_api_key mock from test_install_no_api_key
Dec 24, 2025
3d12434
Fix ruff linting error: remove whitespace from blank line 521
Dec 24, 2025
8b4b8f5
Fix wizard inconsistent behavior across execution contexts
Dec 24, 2025
d437c34
Fix ruff linting error: remove whitespace from blank line 336
Dec 24, 2025
4ca3a42
Always show provider selection menu and add skip option
Dec 24, 2025
d1b3df9
feat: seamless onboarding—auto-create .env from .env.example, improve…
Dec 25, 2025
d5f863e
feat(wizard): always show all providers and prompt for blank API keys
Dec 25, 2025
43aa134
fix: rename api_key_test.py to api_key_validator.py to prevent pytest…
Dec 25, 2025
9b54c87
Fix EOF newline for api_key_validator
Dec 25, 2025
4f68679
Fix __all__ and resolve ruff lint errors
Dec 25, 2025
6ad2c30
Fix W292: ensure newline at EOF in test_first_run_wizard
Dec 25, 2025
8c6ecfa
fix: rename api_key_test to api_key_validator to fix pytest collection
Dec 25, 2025
591a931
fix: rename api_key_test to api_key_validator to fix pytest issues
Dec 25, 2025
3f6b837
feat: add environment variable manager with encryption and templates
Dec 22, 2025
f8a00a9
fix: resolve ruff lint errors and PEP8 issues
Dec 22, 2025
050dc82
fix: resolve ruff lint errors and PEP8 issues
Dec 22, 2025
b79c04e
fix: connect wizard command to FirstRunWizard implementation
Dec 24, 2025
9098738
feat: show API provider menu with key detection indicators
Dec 24, 2025
ebabeb1
fix: remove duplicate env subparser and fix env_demo.py
Dec 25, 2025
82595c8
fix: remove duplicate CLI subparser definitions
Dec 25, 2025
14cf97a
fix: clean up formatting and improve readability in multiple files
Dec 29, 2025
0729505
fix: honor shell-exported API keys when .env is empty (#126)
Dec 30, 2025
60a4ccb
Merge branch 'main' into fix/cortex-wizard-not-running
jaysurse Dec 30, 2025
716d408
fix: remove duplicate line causing syntax error in cli.py
Dec 30, 2025
6a8b0b5
Fix CLI bugs: remove invalid offline param, duplicates, apply black f…
Dec 30, 2025
5c2b554
Merge upstream/main into fix/cortex-wizard-not-running
Jan 5, 2026
1c09517
fix: resolve linting issues and fix flaky test
Jan 5, 2026
a7985d2
Resolve merge conflicts
Jan 10, 2026
e50568a
Merge branch 'main' into fix/cortex-wizard-not-running
jaysurse Jan 10, 2026
c5ce259
feat: integrate wizard with API key detection
Jan 10, 2026
d60a188
fix: rename duplicate test function to avoid linting error
Jan 10, 2026
c6d5377
trigger: re-run CI checks
Jan 10, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 8 additions & 0 deletions .env.example
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
<<<<<<< HEAD
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Remove this comments.

# Cortex Linux Environment Configuration
# Copy this file to .env and configure your settings

Expand Down Expand Up @@ -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)
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

remove this one as well.

31 changes: 31 additions & 0 deletions cortex/api_key_detector.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

detect_api_key() is not truly provider-scoped (and docstring disagrees with SUPPORTED_PROVIDERS).

Right now detect_api_key(provider) returns a key only if the globally-detected provider matches provider, which can hide other available keys (e.g., both keys present but only Anthropic is “detected”). Also the docstring doesn’t mention "ollama" even though it’s now “supported”.

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 APIKeyDetector rather than reusing the global detect() path.)

135 changes: 76 additions & 59 deletions cortex/cli.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

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

Expand All @@ -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):
Expand Down Expand Up @@ -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)
Expand All @@ -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
Copy link

Copilot AI Jan 5, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The noqa comment states "assigned in loop", but the variable 'provider' is initialized to None and assigned in the loop at line 622. However, if the loop completes without finding any valid commands, 'provider' remains None and is never used afterward. The noqa comment appears to be suppressing a false positive from the linter. Consider removing this variable initialization entirely since it's not used outside the loop - only try_provider is needed.

Copilot uses AI. Check for mistakes.
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
Copy link

Copilot AI Jan 5, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Variable provider is not used.

Copilot uses AI. Check for mistakes.
api_key = try_api_key
Copy link

Copilot AI Jan 5, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Variable api_key is not used.

Copilot uses AI. Check for mistakes.
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(
Expand Down Expand Up @@ -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
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🛠️ Refactor suggestion | 🟠 Major

Remove redundant import sys.

sys is already imported at the module level (line 4). This local import is unnecessary.

🔎 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
In @cortex/cli.py around lines 972-973, Remove the redundant local "import sys"
statement in cortex/cli.py (the duplicate import of the sys module found inside
the function/block near the CLI logic); sys is already imported at module top,
so delete that local import to avoid duplication and rely on the module-level
sys import instead.

env_mgr = get_env_manager()

# Handle subcommand routing
Expand Down Expand Up @@ -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:
Expand Down Expand Up @@ -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")
Copy link

Copilot AI Jan 5, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The removed conditional check 'if "cryptography" in str(e).lower()' was previously used to provide helpful context only when the error was specifically about the cryptography library. Now the "Install with: pip install cryptography" message is shown for ALL ImportError exceptions, even if they're unrelated to cryptography. This could mislead users. Consider restoring the conditional check to only show the installation hint when it's actually relevant.

Suggested change
cx_print("Install with: pip install cryptography", "info")
if "cryptography" in str(e).lower():
cx_print("Install with: pip install cryptography", "info")

Copilot uses AI. Check for mistakes.
return 1

def _env_get(self, env_mgr: EnvironmentManager, args: argparse.Namespace) -> int:
Expand Down Expand Up @@ -1240,8 +1258,7 @@ def _env_import(self, env_mgr: EnvironmentManager, args: argparse.Namespace) ->
else:
cx_print("No variables imported", "info")

# Return success (0) even with partial errors - some vars imported successfully
return 0
return 0 if not errors else 1
Copy link

Copilot AI Jan 5, 2026

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.

Copilot uses AI. Check for mistakes.

except FileNotFoundError:
self._print_error(f"File not found: {input_file}")
Expand Down
17 changes: 15 additions & 2 deletions cortex/env_loader.py
Original file line number Diff line number Diff line change
Expand Up @@ -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:
Copy link

Copilot AI Jan 5, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

'except' clause does nothing but pass and there is no explanatory comment.

Suggested change
except ImportError:
except ImportError:
# The cortex package may not be installed; skip its optional .env location.

Copilot uses AI. Check for mistakes.
pass

# 4. User's home directory .cortex folder
home_cortex_env = Path.home() / ".cortex" / ".env"
locations.append(home_cortex_env)

# 3. System-wide config (Linux only)
# 5. System-wide config (Linux only)
if os.name == "posix":
system_env = Path("/etc/cortex/.env")
locations.append(system_env)
Expand Down
Loading
Loading