diff --git a/.gitignore b/.gitignore index f1d563d..86176d0 100644 --- a/.gitignore +++ b/.gitignore @@ -150,3 +150,11 @@ ENV/ .pytest_cache/ .coverage htmlcov/ + +# ============================== +# Cortex-specific +# ============================== +# Data files (except contributors.json which is tracked) +data/*.json +data/*.csv +!data/contributors.json diff --git a/Contributing.md b/Contributing.md index aa07f23..bdfc00b 100644 --- a/Contributing.md +++ b/Contributing.md @@ -10,8 +10,9 @@ We're building the AI-native operating system and need your help. Whether you're 2. **Join Discord:** https://discord.gg/uCqHvxjU83 3. **Browse issues:** https://github.com/cortexlinux/cortex/issues 4. **Claim an issue** (comment "I'll work on this") -5. **Submit your PR** -6. **Get paid** (bounties on merge) +5. **Check `develeoper-guide.md`** (in `docs` directory) for structure and guide +6. **Submit your PR** +7. **Get paid** (bounties on merge) ## What We Need @@ -55,6 +56,7 @@ We're building the AI-native operating system and need your help. Whether you're - ✅ Documentation with examples - ✅ Integration with existing code - ✅ Passes all CI checks +- ✅ Proper file managment and maintaing structure ### Template ```markdown @@ -71,6 +73,7 @@ Show the feature working - [ ] Tests pass - [ ] Documentation updated - [ ] No merge conflicts +- [ ] maintained file structure ``` ## Code Style diff --git a/Developer-Guide.md b/Developer-Guide.md deleted file mode 100644 index 99e5ab7..0000000 --- a/Developer-Guide.md +++ /dev/null @@ -1,146 +0,0 @@ -# Developer Guide - -## Development Setup -```bash -# Clone repository -git clone https://github.com/cortexlinux/cortex.git -cd cortex - -# Create virtual environment -python3 -m venv venv -source venv/bin/activate - -# Install dev dependencies -pip install -r requirements.txt -pip install -r requirements-dev.txt - -# Run tests -pytest tests/ - -# Run with coverage -pytest --cov=cortex tests/ -``` - -## Project Structure -``` -cortex/ -├── cortex/ -│ ├── __init__.py -│ ├── packages.py # Package manager wrapper -│ ├── llm_integration.py # Claude API integration -│ ├── sandbox.py # Safe command execution -│ ├── hardware.py # Hardware detection -│ ├── dependencies.py # Dependency resolution -│ ├── verification.py # Installation verification -│ ├── rollback.py # Rollback system -│ ├── config_templates.py # Config generation -│ ├── logging_system.py # Logging & diagnostics -│ └── context_memory.py # AI memory system -├── tests/ -│ └── test_*.py # Unit tests -├── docs/ -│ └── *.md # Documentation -└── .github/ - └── workflows/ # CI/CD -``` - -## Architecture - -### Core Flow -``` -User Input (Natural Language) - ↓ -LLM Integration Layer (Claude API) - ↓ -Package Manager Wrapper (apt/yum/dnf) - ↓ -Dependency Resolver - ↓ -Sandbox Executor (Firejail) - ↓ -Installation Verifier - ↓ -Context Memory (learns patterns) -``` - -### Key Components - -**LLM Integration (`llm_integration.py`)** -- Interfaces with Claude API -- Parses natural language -- Generates installation plans - -**Package Manager (`packages.py`)** -- Translates intent to commands -- Supports apt, yum, dnf -- 32+ software categories - -**Sandbox (`sandbox.py`)** -- Firejail isolation -- AppArmor policies -- Safe command execution - -**Hardware Detection (`hardware.py`)** -- GPU/CPU detection -- Optimization recommendations -- Driver compatibility - -## Contributing - -### Claiming Issues - -1. Browse [open issues](https://github.com/cortexlinux/cortex/issues) -2. Comment "I'd like to work on this" -3. Get assigned -4. Submit PR - -### PR Requirements - -- Tests with >80% coverage -- Documentation included -- Follows code style -- Passes CI checks - -### Bounty Program - -Cash bounties on merge: -- Critical features: $150-200 -- Standard features: $75-150 -- Testing/integration: $50-75 -- 2x bonus at funding (Feb 2025) - -Payment: Bitcoin, USDC, or PayPal - -See [Bounty Program](Bounties) for details. - -## Testing -```bash -# Run all tests -pytest - -# Specific test file -pytest tests/test_packages.py - -# With coverage -pytest --cov=cortex tests/ - -# Watch mode -pytest-watch -``` - -## Code Style -```bash -# Format code -black cortex/ - -# Lint -pylint cortex/ - -# Type checking -mypy cortex/ -``` - -## Questions? - -- Discord: https://discord.gg/uCqHvxjU83 -- GitHub Discussions: https://github.com/cortexlinux/cortex/discussions diff --git a/bounties_owed.csv b/bounties_owed.csv deleted file mode 100644 index d90d6d4..0000000 --- a/bounties_owed.csv +++ /dev/null @@ -1,5 +0,0 @@ -PR,Developer,Feature,Bounty_Amount,Date_Merged,Status -195,dhvll,Package Manager Wrapper,100,2025-11-18,PENDING -198,aliraza556,Installation Rollback,150,2025-11-18,PENDING -21,aliraza556,Config Templates,150,2025-11-18,PENDING -17,chandrapratamar,Package Manager (original),100,2025-11-18,PENDING diff --git a/bounties_pending.json b/bounties_pending.json deleted file mode 100644 index fe51488..0000000 --- a/bounties_pending.json +++ /dev/null @@ -1 +0,0 @@ -[] diff --git a/cortex/cli.py b/cortex/cli.py index cdb6044..a091f06 100644 --- a/cortex/cli.py +++ b/cortex/cli.py @@ -2,25 +2,40 @@ import os import argparse import time -from typing import List, Optional +import json +from typing import Any, Dict, List, Optional import subprocess from datetime import datetime +from pathlib import Path sys.path.insert(0, os.path.join(os.path.dirname(__file__), '..')) from LLM.interpreter import CommandInterpreter from cortex.coordinator import InstallationCoordinator, StepStatus -from installation_history import ( +from cortex.installation_history import ( InstallationHistory, InstallationType, InstallationStatus ) +from cortex.user_preferences import PreferencesManager class CortexCLI: + """ + Cortex CLI - AI-powered Linux command interpreter. + + Provides interactive installation, configuration management, + and package conflict resolution capabilities. + """ + def __init__(self): self.spinner_chars = ['⠋', '⠙', '⠹', '⠸', '⠼', '⠴', '⠦', '⠧', '⠇', '⠏'] self.spinner_idx = 0 + self.prefs_manager = PreferencesManager() + try: + self.prefs_manager.load() + except Exception: + pass def _get_api_key(self) -> Optional[str]: api_key = os.environ.get('OPENAI_API_KEY') or os.environ.get('ANTHROPIC_API_KEY') @@ -36,14 +51,14 @@ def _get_provider(self) -> str: return 'claude' return 'openai' - def _print_status(self, emoji: str, message: str): - print(f"{emoji} {message}") + def _print_status(self, label: str, message: str): + print(f"{label} {message}") def _print_error(self, message: str): - print(f"❌ Error: {message}", file=sys.stderr) + print(f"[ERROR] {message}", file=sys.stderr) def _print_success(self, message: str): - print(f"✅ {message}") + print(f"[SUCCESS] {message}") def _animate_spinner(self, message: str): sys.stdout.write(f"\r{self.spinner_chars[self.spinner_idx]} {message}") @@ -55,6 +70,76 @@ def _clear_line(self): sys.stdout.write('\r\033[K') sys.stdout.flush() + def _resolve_conflicts_interactive(self, conflicts: List[tuple]) -> Dict[str, List[str]]: + """ + Interactively resolve package conflicts with user input. + + Args: + conflicts: List of tuples (package1, package2) representing conflicts + + Returns: + Dictionary with resolution actions (e.g., {'remove': ['pkgA']}) + """ + resolutions = {'remove': []} + saved_resolutions = self.prefs_manager.get("conflicts.saved_resolutions") or {} + + print("\n" + "=" * 60) + print("Package Conflicts Detected") + print("=" * 60) + + for i, (pkg1, pkg2) in enumerate(conflicts, 1): + conflict_key = f"{min(pkg1, pkg2)}:{max(pkg1, pkg2)}" + if conflict_key in saved_resolutions: + preferred = saved_resolutions[conflict_key] + to_remove = pkg2 if preferred == pkg1 else pkg1 + resolutions['remove'].append(to_remove) + print(f"\nConflict {i}: {pkg1} vs {pkg2}") + print(f" Using saved preference: Keep {preferred}, remove {to_remove}") + continue + + print(f"\nConflict {i}: {pkg1} vs {pkg2}") + print(f" 1. Keep/Install {pkg1} (removes {pkg2})") + print(f" 2. Keep/Install {pkg2} (removes {pkg1})") + print(" 3. Cancel installation") + + while True: + choice = input(f"\nSelect action for Conflict {i} [1-3]: ").strip() + if choice == '1': + resolutions['remove'].append(pkg2) + print(f"Selected: Keep {pkg1}, remove {pkg2}") + self._ask_save_preference(pkg1, pkg2, pkg1) + break + elif choice == '2': + resolutions['remove'].append(pkg1) + print(f"Selected: Keep {pkg2}, remove {pkg1}") + self._ask_save_preference(pkg1, pkg2, pkg2) + break + elif choice == '3': + print("Installation cancelled.") + sys.exit(1) + else: + print("Invalid choice. Please enter 1, 2, or 3.") + + return resolutions + + def _ask_save_preference(self, pkg1: str, pkg2: str, preferred: str): + """ + Ask user if they want to save the conflict resolution preference. + + Args: + pkg1: First package in conflict + pkg2: Second package in conflict + preferred: The package user chose to keep + """ + save = input("Save this preference for future conflicts? (y/N): ").strip().lower() + if save == 'y': + conflict_key = f"{min(pkg1, pkg2)}:{max(pkg1, pkg2)}" + saved_resolutions = self.prefs_manager.get("conflicts.saved_resolutions") or {} + saved_resolutions[conflict_key] = preferred + self.prefs_manager.set("conflicts.saved_resolutions", saved_resolutions) + self.prefs_manager.save() + print("Preference saved.") + def install(self, software: str, execute: bool = False, dry_run: bool = False): api_key = self._get_api_key() if not api_key: @@ -68,11 +153,11 @@ def install(self, software: str, execute: bool = False, dry_run: bool = False): start_time = datetime.now() try: - self._print_status("🧠", "Understanding request...") + self._print_status("[INFO]", "Understanding request...") interpreter = CommandInterpreter(api_key=api_key, provider=provider) - self._print_status("📦", "Planning installation...") + self._print_status("[INFO]", "Planning installation...") for _ in range(10): self._animate_spinner("Analyzing system requirements...") @@ -83,6 +168,25 @@ def install(self, software: str, execute: bool = False, dry_run: bool = False): if not commands: self._print_error("No commands generated. Please try again with a different request.") return 1 + + # Check for package conflicts using DependencyResolver + from cortex.dependency_resolver import DependencyResolver + resolver = DependencyResolver() + + target_package = software.split()[0] + + try: + graph = resolver.resolve_dependencies(target_package) + if graph.conflicts: + resolutions = self._resolve_conflicts_interactive(graph.conflicts) + + if resolutions['remove']: + for pkg_to_remove in resolutions['remove']: + if not any(f"remove {pkg_to_remove}" in cmd for cmd in commands): + commands.insert(0, f"sudo apt-get remove -y {pkg_to_remove}") + self._print_status("[INFO]", f"Added command to remove conflicting package: {pkg_to_remove}") + except Exception: + pass # Extract packages from commands for tracking packages = history._extract_packages_from_commands(commands) @@ -96,7 +200,7 @@ def install(self, software: str, execute: bool = False, dry_run: bool = False): start_time ) - self._print_status("⚙️", f"Installing {software}...") + self._print_status("[INFO]", f"Installing {software}...") print("\nGenerated commands:") for i, cmd in enumerate(commands, 1): print(f" {i}. {cmd}") @@ -109,12 +213,12 @@ def install(self, software: str, execute: bool = False, dry_run: bool = False): if execute: def progress_callback(current, total, step): - status_emoji = "⏳" + status_label = "[PENDING]" if step.status == StepStatus.SUCCESS: - status_emoji = "✅" + status_label = "[SUCCESS]" elif step.status == StepStatus.FAILED: - status_emoji = "❌" - print(f"\n[{current}/{total}] {status_emoji} {step.description}") + status_label = "[FAILED]" + print(f"\n[{current}/{total}] {status_label} {step.description}") print(f" Command: {step.command}") print("\nExecuting commands...") @@ -136,7 +240,7 @@ def progress_callback(current, total, step): # Record successful installation if install_id: history.update_installation(install_id, InstallationStatus.SUCCESS) - print(f"\n📝 Installation recorded (ID: {install_id})") + print(f"\n[INFO] Installation recorded (ID: {install_id})") print(f" To rollback: cortex rollback {install_id}") return 0 @@ -157,7 +261,7 @@ def progress_callback(current, total, step): if result.error_message: print(f" Error: {result.error_message}", file=sys.stderr) if install_id: - print(f"\n📝 Installation recorded (ID: {install_id})") + print(f"\n[INFO] Installation recorded (ID: {install_id})") print(f" View details: cortex history show {install_id}") return 1 else: @@ -262,6 +366,146 @@ def rollback(self, install_id: str, dry_run: bool = False): self._print_error(f"Rollback failed: {str(e)}") return 1 + def config(self, action: str, key: Optional[str] = None, value: Optional[str] = None): + """ + Manage user preferences and configuration. + + Args: + action: Configuration action (list, get, set, reset, validate, info, export, import) + key: Preference key or file path + value: Preference value (for set action) + + Returns: + Exit code (0 for success, 1 for error) + """ + try: + if action == "list": + prefs = self.prefs_manager.list_all() + + print("\n[INFO] Current Configuration:") + print("=" * 60) + import yaml + print(yaml.dump(prefs, default_flow_style=False, sort_keys=False)) + + info = self.prefs_manager.get_config_info() + print(f"\nConfig file: {info['config_path']}") + return 0 + + elif action == "get": + if not key: + self._print_error("Key required for 'get' action") + return 1 + + value = self.prefs_manager.get(key) + if value is None: + self._print_error(f"Preference '{key}' not found") + return 1 + + print(f"{key}: {value}") + return 0 + + elif action == "set": + if not key or value is None: + self._print_error("Key and value required for 'set' action") + return 1 + + parsed_value = self._parse_config_value(value) + + self.prefs_manager.set(key, parsed_value) + self.prefs_manager.save() + + self._print_success(f"Set {key} = {parsed_value}") + return 0 + + elif action == "reset": + if key: + self.prefs_manager.reset(key) + self._print_success(f"Reset {key} to default") + else: + print("This will reset all preferences to defaults.") + confirm = input("Continue? (y/n): ") + if confirm.lower() == 'y': + self.prefs_manager.reset() + self._print_success("All preferences reset to defaults") + else: + print("Reset cancelled") + return 0 + + elif action == "validate": + errors = self.prefs_manager.validate() + if errors: + print("Configuration validation errors:") + for error in errors: + print(f" - {error}") + return 1 + else: + self._print_success("Configuration is valid") + return 0 + + elif action == "info": + info = self.prefs_manager.get_config_info() + print("\n[INFO] Configuration Info:") + print("=" * 60) + for k, v in info.items(): + print(f"{k}: {v}") + return 0 + + elif action == "export": + if not key: + self._print_error("Output path required for 'export' action") + return 1 + + output_path = Path(key) + self.prefs_manager.export_json(output_path) + self._print_success(f"Configuration exported to {output_path}") + return 0 + + elif action == "import": + if not key: + self._print_error("Input path required for 'import' action") + return 1 + + input_path = Path(key) + self.prefs_manager.import_json(input_path) + self._print_success(f"Configuration imported from {input_path}") + return 0 + + else: + self._print_error(f"Unknown config action: {action}") + return 1 + + except ValueError as e: + self._print_error(str(e)) + return 1 + except Exception as e: + self._print_error(f"Configuration error: {str(e)}") + return 1 + + def _parse_config_value(self, value: str) -> Any: + """ + Parse configuration value from string. + + Args: + value: String value to parse + + Returns: + Parsed value (bool, int, list, or string) + """ + if value.lower() in ('true', 'yes', 'on', '1'): + return True + if value.lower() in ('false', 'no', 'off', '0'): + return False + + try: + return int(value) + except ValueError: + pass + + if ',' in value: + return [item.strip() for item in value.split(',')] + + return value + def main(): parser = argparse.ArgumentParser( @@ -277,6 +521,12 @@ def main(): cortex history cortex history show cortex rollback + cortex config list + cortex config get conflicts.saved_resolutions + cortex config set llm.provider openai + cortex config reset + cortex config export ~/cortex-config.json + cortex config import ~/cortex-config.json Environment Variables: OPENAI_API_KEY OpenAI API key for GPT-4 @@ -304,6 +554,14 @@ def main(): rollback_parser.add_argument('id', help='Installation ID to rollback') rollback_parser.add_argument('--dry-run', action='store_true', help='Show rollback actions without executing') + # Config command + config_parser = subparsers.add_parser('config', help='Manage user preferences and configuration') + config_parser.add_argument('action', + choices=['list', 'get', 'set', 'reset', 'validate', 'info', 'export', 'import'], + help='Configuration action') + config_parser.add_argument('key', nargs='?', help='Preference key or file path') + config_parser.add_argument('value', nargs='?', help='Preference value (for set action)') + args = parser.parse_args() if not args.command: @@ -319,6 +577,8 @@ def main(): return cli.history(limit=args.limit, status=args.status, show_id=args.show_id) elif args.command == 'rollback': return cli.rollback(args.id, dry_run=args.dry_run) + elif args.command == 'config': + return cli.config(args.action, args.key, args.value) else: parser.print_help() return 1 diff --git a/context_memory.py b/cortex/context_memory.py similarity index 100% rename from context_memory.py rename to cortex/context_memory.py diff --git a/dependency_resolver.py b/cortex/dependency_resolver.py similarity index 100% rename from dependency_resolver.py rename to cortex/dependency_resolver.py diff --git a/error_parser.py b/cortex/error_parser.py similarity index 100% rename from error_parser.py rename to cortex/error_parser.py diff --git a/installation_history.py b/cortex/installation_history.py similarity index 100% rename from installation_history.py rename to cortex/installation_history.py diff --git a/installation_verifier.py b/cortex/installation_verifier.py similarity index 100% rename from installation_verifier.py rename to cortex/installation_verifier.py diff --git a/llm_router.py b/cortex/llm_router.py similarity index 100% rename from llm_router.py rename to cortex/llm_router.py diff --git a/logging_system.py b/cortex/logging_system.py similarity index 100% rename from logging_system.py rename to cortex/logging_system.py diff --git a/cortex/user_preferences.py b/cortex/user_preferences.py new file mode 100644 index 0000000..ed83518 --- /dev/null +++ b/cortex/user_preferences.py @@ -0,0 +1,488 @@ +#!/usr/bin/env python3 +""" +Cortex Linux - User Preferences & Settings System +Issue #26: Persistent user preferences and configuration management + +This module provides comprehensive configuration management for user preferences, +allowing customization of AI behavior, confirmation prompts, verbosity levels, +and other system settings. +""" + +import os +import yaml +import json +from pathlib import Path +from typing import Any, Dict, Optional, List +from dataclasses import dataclass, asdict, field +from enum import Enum +from datetime import datetime + + +class VerbosityLevel(Enum): + """Verbosity levels for output control""" + QUIET = "quiet" + NORMAL = "normal" + VERBOSE = "verbose" + DEBUG = "debug" + + +class AICreativity(Enum): + """AI creativity/temperature settings""" + CONSERVATIVE = "conservative" + BALANCED = "balanced" + CREATIVE = "creative" + + +@dataclass +class ConfirmationSettings: + """Settings for confirmation prompts""" + before_install: bool = True + before_remove: bool = True + before_upgrade: bool = False + before_system_changes: bool = True + + def to_dict(self) -> Dict[str, bool]: + return asdict(self) + + +@dataclass +class AutoUpdateSettings: + """Settings for automatic updates""" + check_on_start: bool = True + auto_install: bool = False + frequency_hours: int = 24 + + def to_dict(self) -> Dict[str, Any]: + return asdict(self) + + +@dataclass +class AISettings: + """AI behavior configuration""" + model: str = "claude-sonnet-4" + creativity: str = AICreativity.BALANCED.value + explain_steps: bool = True + suggest_alternatives: bool = True + learn_from_history: bool = True + max_suggestions: int = 5 + + def to_dict(self) -> Dict[str, Any]: + return asdict(self) + + +@dataclass +class PackageSettings: + """Package management preferences""" + default_sources: List[str] = field(default_factory=lambda: ["official"]) + prefer_latest: bool = False + auto_cleanup: bool = True + backup_before_changes: bool = True + + def to_dict(self) -> Dict[str, Any]: + return asdict(self) + + +@dataclass +class ConflictSettings: + """Conflict resolution preferences""" + default_strategy: str = "interactive" + saved_resolutions: Dict[str, str] = field(default_factory=dict) + + def to_dict(self) -> Dict[str, Any]: + return asdict(self) + + +@dataclass +class UserPreferences: + """Complete user preferences configuration""" + verbosity: str = VerbosityLevel.NORMAL.value + confirmations: ConfirmationSettings = field(default_factory=ConfirmationSettings) + auto_update: AutoUpdateSettings = field(default_factory=AutoUpdateSettings) + ai: AISettings = field(default_factory=AISettings) + packages: PackageSettings = field(default_factory=PackageSettings) + conflicts: ConflictSettings = field(default_factory=ConflictSettings) + theme: str = "default" + language: str = "en" + timezone: str = "UTC" + + def to_dict(self) -> Dict[str, Any]: + """Convert preferences to dictionary format""" + return { + "verbosity": self.verbosity, + "confirmations": self.confirmations.to_dict(), + "auto_update": self.auto_update.to_dict(), + "ai": self.ai.to_dict(), + "packages": self.packages.to_dict(), + "conflicts": self.conflicts.to_dict(), + "theme": self.theme, + "language": self.language, + "timezone": self.timezone, + } + + @classmethod + def from_dict(cls, data: Dict[str, Any]) -> 'UserPreferences': + """Create UserPreferences from dictionary""" + confirmations = ConfirmationSettings(**data.get("confirmations", {})) + auto_update = AutoUpdateSettings(**data.get("auto_update", {})) + ai = AISettings(**data.get("ai", {})) + packages = PackageSettings(**data.get("packages", {})) + conflicts = ConflictSettings(**data.get("conflicts", {})) + + return cls( + verbosity=data.get("verbosity", VerbosityLevel.NORMAL.value), + confirmations=confirmations, + auto_update=auto_update, + ai=ai, + packages=packages, + conflicts=conflicts, + theme=data.get("theme", "default"), + language=data.get("language", "en"), + timezone=data.get("timezone", "UTC"), + ) + + +class PreferencesManager: + """ + User Preferences Manager for Cortex Linux + + Features: + - YAML-based configuration storage + - Validation and schema enforcement + - Default configuration management + - Configuration migration support + - Safe file operations with backup + """ + + DEFAULT_CONFIG_DIR = Path.home() / ".config" / "cortex" + DEFAULT_CONFIG_FILE = "preferences.yaml" + BACKUP_SUFFIX = ".backup" + + def __init__(self, config_path: Optional[Path] = None): + """ + Initialize the preferences manager + + Args: + config_path: Custom path to config file (uses default if None) + """ + if config_path: + self.config_path = Path(config_path) + else: + self.config_path = self.DEFAULT_CONFIG_DIR / self.DEFAULT_CONFIG_FILE + + self.config_dir = self.config_path.parent + self._ensure_config_directory() + self._preferences: Optional[UserPreferences] = None + + def _ensure_config_directory(self): + """Ensure configuration directory exists""" + self.config_dir.mkdir(parents=True, exist_ok=True) + + def _create_backup(self) -> Optional[Path]: + """ + Create backup of existing config file + + Returns: + Path to backup file or None if no backup created + """ + if not self.config_path.exists(): + return None + + timestamp = datetime.now().strftime("%Y%m%d_%H%M%S") + backup_path = self.config_path.with_suffix(f"{self.BACKUP_SUFFIX}.{timestamp}") + + try: + import shutil + shutil.copy2(self.config_path, backup_path) + return backup_path + except Exception as e: + raise IOError(f"Failed to create backup: {str(e)}") + + def load(self) -> UserPreferences: + """ + Load preferences from config file + + Returns: + UserPreferences object + """ + if not self.config_path.exists(): + self._preferences = UserPreferences() + self.save() + return self._preferences + + try: + with open(self.config_path, 'r', encoding='utf-8') as f: + data = yaml.safe_load(f) + + if not data: + data = {} + + self._preferences = UserPreferences.from_dict(data) + return self._preferences + + except yaml.YAMLError as e: + raise ValueError(f"Invalid YAML in config file: {str(e)}") + except Exception as e: + raise IOError(f"Failed to load config file: {str(e)}") + + def save(self, backup: bool = True) -> Path: + """ + Save preferences to config file + + Args: + backup: Create backup before saving + + Returns: + Path to saved config file + """ + if self._preferences is None: + raise RuntimeError("No preferences loaded. Call load() first.") + + if backup and self.config_path.exists(): + self._create_backup() + + try: + with open(self.config_path, 'w', encoding='utf-8') as f: + yaml.dump( + self._preferences.to_dict(), + f, + default_flow_style=False, + sort_keys=False, + indent=2 + ) + + return self.config_path + + except Exception as e: + raise IOError(f"Failed to save config file: {str(e)}") + + def get(self, key: str, default: Any = None) -> Any: + """ + Get a preference value by dot-notation key + + Args: + key: Preference key (e.g., "ai.model", "confirmations.before_install") + default: Default value if key not found + + Returns: + Preference value or default + """ + if self._preferences is None: + self.load() + + parts = key.split(".") + value = self._preferences.to_dict() + + for part in parts: + if isinstance(value, dict) and part in value: + value = value[part] + else: + return default + + return value + + def set(self, key: str, value: Any) -> bool: + """ + Set a preference value by dot-notation key + + Args: + key: Preference key (e.g., "ai.model") + value: New value + + Returns: + True if successful, False otherwise + """ + if self._preferences is None: + self.load() + + parts = key.split(".") + + try: + if parts[0] == "verbosity": + if value not in [v.value for v in VerbosityLevel]: + raise ValueError(f"Invalid verbosity level: {value}") + self._preferences.verbosity = value + + elif parts[0] == "confirmations": + if len(parts) != 2: + raise ValueError("Invalid confirmations key") + if not isinstance(value, bool): + raise ValueError("Confirmation values must be boolean") + setattr(self._preferences.confirmations, parts[1], value) + + elif parts[0] == "auto_update": + if len(parts) != 2: + raise ValueError("Invalid auto_update key") + if parts[1] == "frequency_hours" and not isinstance(value, int): + raise ValueError("frequency_hours must be an integer") + elif parts[1] != "frequency_hours" and not isinstance(value, bool): + raise ValueError("auto_update boolean values required") + setattr(self._preferences.auto_update, parts[1], value) + + elif parts[0] == "ai": + if len(parts) != 2: + raise ValueError("Invalid ai key") + if parts[1] == "creativity": + if value not in [c.value for c in AICreativity]: + raise ValueError(f"Invalid creativity level: {value}") + elif parts[1] == "max_suggestions" and not isinstance(value, int): + raise ValueError("max_suggestions must be an integer") + setattr(self._preferences.ai, parts[1], value) + + elif parts[0] == "packages": + if len(parts) != 2: + raise ValueError("Invalid packages key") + if parts[1] == "default_sources" and not isinstance(value, list): + raise ValueError("default_sources must be a list") + setattr(self._preferences.packages, parts[1], value) + + elif parts[0] == "conflicts": + if len(parts) != 2: + raise ValueError("Invalid conflicts key") + if parts[1] == "saved_resolutions" and not isinstance(value, dict): + raise ValueError("saved_resolutions must be a dictionary") + setattr(self._preferences.conflicts, parts[1], value) + + elif parts[0] in ["theme", "language", "timezone"]: + setattr(self._preferences, parts[0], value) + + else: + raise ValueError(f"Unknown preference key: {key}") + + return True + + except (AttributeError, ValueError) as e: + raise ValueError(f"Failed to set preference '{key}': {str(e)}") + + def reset(self, key: Optional[str] = None) -> bool: + """ + Reset preferences to defaults + + Args: + key: Specific key to reset (resets all if None) + + Returns: + True if successful + """ + if key is None: + self._preferences = UserPreferences() + self.save() + return True + + defaults = UserPreferences() + default_value = defaults.to_dict() + + parts = key.split(".") + for part in parts: + if isinstance(default_value, dict) and part in default_value: + default_value = default_value[part] + else: + raise ValueError(f"Invalid preference key: {key}") + + self.set(key, default_value) + self.save() + return True + + def validate(self) -> List[str]: + """ + Validate current configuration + + Returns: + List of validation errors (empty if valid) + """ + if self._preferences is None: + self.load() + + errors = [] + + if self._preferences.verbosity not in [v.value for v in VerbosityLevel]: + errors.append(f"Invalid verbosity level: {self._preferences.verbosity}") + + if self._preferences.ai.creativity not in [c.value for c in AICreativity]: + errors.append(f"Invalid AI creativity level: {self._preferences.ai.creativity}") + + valid_models = ["claude-sonnet-4", "gpt-4", "gpt-4-turbo", "claude-3-opus"] + if self._preferences.ai.model not in valid_models: + errors.append(f"Unknown AI model: {self._preferences.ai.model}") + + if self._preferences.ai.max_suggestions < 1: + errors.append("ai.max_suggestions must be at least 1") + + if self._preferences.auto_update.frequency_hours < 1: + errors.append("auto_update.frequency_hours must be at least 1") + + if not self._preferences.packages.default_sources: + errors.append("At least one package source required") + + return errors + + def export_json(self, output_path: Path) -> Path: + """ + Export preferences to JSON file + + Args: + output_path: Path to output JSON file + + Returns: + Path to exported file + """ + if self._preferences is None: + self.load() + + with open(output_path, 'w', encoding='utf-8') as f: + json.dump(self._preferences.to_dict(), f, indent=2) + + return output_path + + def import_json(self, input_path: Path) -> bool: + """ + Import preferences from JSON file + + Args: + input_path: Path to JSON file + + Returns: + True if successful + """ + with open(input_path, 'r', encoding='utf-8') as f: + data = json.load(f) + + self._preferences = UserPreferences.from_dict(data) + + errors = self.validate() + if errors: + raise ValueError(f"Invalid configuration: {', '.join(errors)}") + + self.save() + return True + + def get_config_info(self) -> Dict[str, Any]: + """ + Get information about configuration + + Returns: + Dictionary with config file info + """ + info = { + "config_path": str(self.config_path), + "exists": self.config_path.exists(), + "writable": os.access(self.config_dir, os.W_OK), + } + + if self.config_path.exists(): + stat = self.config_path.stat() + info["size_bytes"] = stat.st_size + info["modified"] = datetime.fromtimestamp(stat.st_mtime).isoformat() + + return info + + def list_all(self) -> Dict[str, Any]: + """ + List all preferences with current values + + Returns: + Dictionary of all preferences + """ + if self._preferences is None: + self.load() + + return self._preferences.to_dict() diff --git a/contributors.json b/data/contributors.json similarity index 100% rename from contributors.json rename to data/contributors.json diff --git a/Bounties.md b/docs/Bounties.md similarity index 100% rename from Bounties.md rename to docs/Bounties.md diff --git a/docs/Developer-Guide.md b/docs/Developer-Guide.md new file mode 100644 index 0000000..afe787c --- /dev/null +++ b/docs/Developer-Guide.md @@ -0,0 +1,189 @@ +# Developer Guide + +## Development Setup +```bash +# Clone repository +git clone https://github.com/cortexlinux/cortex.git +cd cortex + +# Create virtual environment +python3 -m venv venv +source venv/bin/activate + +# Install dev dependencies +pip install -r requirements.txt +pip install -r requirements-dev.txt + +# Run tests +pytest tests/ + +# Run with coverage +pytest --cov=cortex tests/ +``` + +## Project Structure +``` +cortex/ +├── cortex/ # Core Python modules +│ ├── __init__.py +│ ├── cli.py # Command-line interface +│ ├── packages.py # Package manager wrapper +│ ├── coordinator.py # Installation coordination +│ ├── dependency_resolver.py # Dependency resolution +│ ├── installation_history.py # Installation tracking +│ ├── installation_verifier.py # Installation verification +│ ├── error_parser.py # Error parsing & recovery +│ ├── llm_router.py # Multi-LLM routing +│ ├── logging_system.py # Logging & diagnostics +│ ├── context_memory.py # AI memory system +│ └── user_preferences.py # User preferences management +├── LLM/ # LLM integration layer +│ ├── __init__.py +│ ├── interpreter.py # Command interpreter +│ └── requirements.txt +├── test/ # Test suite +│ ├── run_all_tests.py # Test runner +│ └── test_*.py # Unit tests +├── docs/ # Documentation +│ ├── Developer-Guide.md +│ ├── User-Guide.md +│ ├── Bounties.md +│ ├── Getting-Started.md +│ └── *.md # Additional docs +├── scripts/ # Shell scripts +│ ├── cortex-master.sh # Main automation +│ ├── setup_and_upload.sh # Setup utilities +│ └── *.sh # Other scripts +├── data/ # Data files +│ ├── contributors.json +│ ├── bounties_owed.csv +│ ├── bounties_pending.json +│ └── *.json, *.csv # Other data files +├── src/ # Additional utilities +│ ├── progress_tracker.py +│ ├── sandbox_executor.py +│ └── hwprofiler.py +├── examples/ # Example scripts +│ ├── progress_demo.py +│ └── standalone_demo.py +└── .github/ + └── workflows/ # CI/CD +``` + +## Architecture + +### Core Flow +``` +User Input (Natural Language) + ↓ +LLM Integration Layer (Claude API) + ↓ +Package Manager Wrapper (apt/yum/dnf) + ↓ +Dependency Resolver + ↓ +Sandbox Executor (Firejail) + ↓ +Installation Verifier + ↓ +Context Memory (learns patterns) +``` + +### Key Components + +**CLI Interface (`cortex/cli.py`)** +- Command-line interface +- User interaction handling +- Configuration management + +**LLM Router (`cortex/llm_router.py`)** +- Multi-LLM support (Claude, Kimi K2) +- Intelligent task routing +- Cost tracking & fallback + +**Package Manager (`cortex/packages.py`)** +- Translates intent to commands +- Supports apt, yum, dnf +- 32+ software categories + +**Dependency Resolver (`cortex/dependency_resolver.py`)** +- Package conflict detection +- Interactive conflict resolution +- Saved preference management + +**Installation History (`cortex/installation_history.py`)** +- Installation tracking +- Rollback support +- Export capabilities + +**Context Memory (`cortex/context_memory.py`)** +- AI learning patterns +- Suggestion generation +- User preference tracking + +**Error Parser (`cortex/error_parser.py`)** +- Parse installation errors +- Suggest fixes +- Error pattern matching + +## Contributing + +### Claiming Issues + +1. Browse [open issues](https://github.com/cortexlinux/cortex/issues) +2. Comment "I'd like to work on this" +3. Get assigned +4. Submit PR + +### PR Requirements + +- Tests with >80% coverage +- Documentation included +- Follows code style +- Passes CI checks + +### Bounty Program + +Cash bounties on merge: +- Critical features: $150-200 +- Standard features: $75-150 +- Testing/integration: $50-75 +- 2x bonus at funding (Feb 2025) + +Payment: Bitcoin, USDC, or PayPal + +See [Bounty Program](Bounties) for details. + +## Testing +```bash +# Run all tests (automatic discovery) +python test/run_all_tests.py + +# Run specific test file +python -m unittest test.test_packages + +# Run with verbose output +python test/run_all_tests.py -v + +# Individual test modules +python -m unittest test.test_cli +python -m unittest test.test_conflict_ui +python -m unittest test.test_llm_router +``` + +## Code Style +```bash +# Format code +black cortex/ LLM/ test/ + +# Lint +pylint cortex/ LLM/ + +# Type checking +mypy cortex/ LLM/ +``` + +## Questions? + +- Discord: https://discord.gg/uCqHvxjU83 +- GitHub Discussions: https://github.com/cortexlinux/cortex/discussions diff --git a/FAQ.md b/docs/FAQ.md similarity index 100% rename from FAQ.md rename to docs/FAQ.md diff --git a/Getting-Started.md b/docs/Getting-Started.md similarity index 100% rename from Getting-Started.md rename to docs/Getting-Started.md diff --git a/Home.md b/docs/Home.md similarity index 100% rename from Home.md rename to docs/Home.md diff --git a/IMPLEMENTATION_SUMMARY.md b/docs/IMPLEMENTATION_SUMMARY.md similarity index 100% rename from IMPLEMENTATION_SUMMARY.md rename to docs/IMPLEMENTATION_SUMMARY.md diff --git a/PR_MANAGEMENT_INSTRUCTIONS.md b/docs/PR_MANAGEMENT_INSTRUCTIONS.md similarity index 100% rename from PR_MANAGEMENT_INSTRUCTIONS.md rename to docs/PR_MANAGEMENT_INSTRUCTIONS.md diff --git a/PR_SUBMISSION_GUIDE.md b/docs/PR_SUBMISSION_GUIDE.md similarity index 100% rename from PR_SUBMISSION_GUIDE.md rename to docs/PR_SUBMISSION_GUIDE.md diff --git a/README_CONTEXT_MEMORY.md b/docs/README_CONTEXT_MEMORY.md similarity index 100% rename from README_CONTEXT_MEMORY.md rename to docs/README_CONTEXT_MEMORY.md diff --git a/README_DEPENDENCIES (1).md b/docs/README_DEPENDENCIES (1).md similarity index 100% rename from README_DEPENDENCIES (1).md rename to docs/README_DEPENDENCIES (1).md diff --git a/README_DEPENDENCIES.md b/docs/README_DEPENDENCIES.md similarity index 100% rename from README_DEPENDENCIES.md rename to docs/README_DEPENDENCIES.md diff --git a/README_ERROR_PARSER.md b/docs/README_ERROR_PARSER.md similarity index 100% rename from README_ERROR_PARSER.md rename to docs/README_ERROR_PARSER.md diff --git a/README_LLM_ROUTER.md b/docs/README_LLM_ROUTER.md similarity index 100% rename from README_LLM_ROUTER.md rename to docs/README_LLM_ROUTER.md diff --git a/README_LOGGING.md b/docs/README_LOGGING.md similarity index 100% rename from README_LOGGING.md rename to docs/README_LOGGING.md diff --git a/README_ROLLBACK.md b/docs/README_ROLLBACK.md similarity index 100% rename from README_ROLLBACK.md rename to docs/README_ROLLBACK.md diff --git a/README_VERIFICATION.md b/docs/README_VERIFICATION.md similarity index 100% rename from README_VERIFICATION.md rename to docs/README_VERIFICATION.md diff --git a/User-Guide.md b/docs/User-Guide.md similarity index 100% rename from User-Guide.md rename to docs/User-Guide.md diff --git a/issue_status.json b/issue_status.json deleted file mode 100644 index 561a6e4..0000000 --- a/issue_status.json +++ /dev/null @@ -1 +0,0 @@ -[{"assignees":[],"labels":[{"id":"LA_kwDOQRmfac8AAAACPKPCJg","name":"enhancement","description":"New feature or request","color":"a2eeef"},{"id":"LA_kwDOQRmfac8AAAACPKQBDw","name":"bounty","description":"","color":"d35a32"}],"number":144,"title":"Package Installation Profiles"},{"assignees":[],"labels":[{"id":"LA_kwDOQRmfac8AAAACPKPCJg","name":"enhancement","description":"New feature or request","color":"a2eeef"},{"id":"LA_kwDOQRmfac8AAAACPKQBDw","name":"bounty","description":"","color":"d35a32"},{"id":"LA_kwDOQRmfac8AAAACPfpLxA","name":"ui","description":"","color":"ededed"}],"number":135,"title":"Desktop Notification System"},{"assignees":[],"labels":[{"id":"LA_kwDOQRmfac8AAAACPKPCJg","name":"enhancement","description":"New feature or request","color":"a2eeef"},{"id":"LA_kwDOQRmfac8AAAACPKQBDw","name":"bounty","description":"","color":"d35a32"},{"id":"LA_kwDOQRmfac8AAAACPfoR1w","name":"ai","description":"","color":"ededed"},{"id":"LA_kwDOQRmfac8AAAACPfr0rg","name":"experimental","description":"","color":"ededed"}],"number":131,"title":"AI-Powered Installation Tutor"},{"assignees":[],"labels":[{"id":"LA_kwDOQRmfac8AAAACPKPCJg","name":"enhancement","description":"New feature or request","color":"a2eeef"},{"id":"LA_kwDOQRmfac8AAAACPKQBDw","name":"bounty","description":"","color":"d35a32"}],"number":128,"title":"System Health Score and Recommendations"},{"assignees":[],"labels":[{"id":"LA_kwDOQRmfac8AAAACPKPCJg","name":"enhancement","description":"New feature or request","color":"a2eeef"},{"id":"LA_kwDOQRmfac8AAAACPKQBDw","name":"bounty","description":"","color":"d35a32"}],"number":126,"title":"Package Import from Requirements Files"},{"assignees":[],"labels":[{"id":"LA_kwDOQRmfac8AAAACPKPCJg","name":"enhancement","description":"New feature or request","color":"a2eeef"},{"id":"LA_kwDOQRmfac8AAAACPKQBDw","name":"bounty","description":"","color":"d35a32"}],"number":125,"title":"Smart Cleanup and Disk Space Optimizer"},{"assignees":[],"labels":[{"id":"LA_kwDOQRmfac8AAAACPKPCJg","name":"enhancement","description":"New feature or request","color":"a2eeef"},{"id":"LA_kwDOQRmfac8AAAACPKQBDw","name":"bounty","description":"","color":"d35a32"},{"id":"LA_kwDOQRmfac8AAAACPfoR1w","name":"ai","description":"","color":"ededed"}],"number":119,"title":"Package Recommendation Based on System Role"},{"assignees":[],"labels":[{"id":"LA_kwDOQRmfac8AAAACPKPCJg","name":"enhancement","description":"New feature or request","color":"a2eeef"},{"id":"LA_kwDOQRmfac8AAAACPKQBDw","name":"bounty","description":"","color":"d35a32"},{"id":"LA_kwDOQRmfac8AAAACPfpLxA","name":"ui","description":"","color":"ededed"}],"number":117,"title":"Smart Package Search with Fuzzy Matching"},{"assignees":[],"labels":[{"id":"LA_kwDOQRmfac8AAAACPKPCJg","name":"enhancement","description":"New feature or request","color":"a2eeef"},{"id":"LA_kwDOQRmfac8AAAACPKQBDw","name":"bounty","description":"","color":"d35a32"},{"id":"LA_kwDOQRmfac8AAAACPfoR1w","name":"ai","description":"","color":"ededed"}],"number":112,"title":"Alternative Package Suggestions"},{"assignees":[],"labels":[{"id":"LA_kwDOQRmfac8AAAACPKPCJg","name":"enhancement","description":"New feature or request","color":"a2eeef"},{"id":"LA_kwDOQRmfac8AAAACPKQBDw","name":"bounty","description":"","color":"d35a32"}],"number":103,"title":"Installation Simulation Mode"},{"assignees":[{"id":"U_kgDOBw4eqA","login":"Sahilbhatane","name":"Sahil Bhatane","databaseId":0}],"labels":[{"id":"LA_kwDOQRmfac8AAAACPKPCJg","name":"enhancement","description":"New feature or request","color":"a2eeef"},{"id":"LA_kwDOQRmfac8AAAACPKQBDw","name":"bounty","description":"","color":"d35a32"}],"number":45,"title":"System Snapshot and Rollback Points"},{"assignees":[],"labels":[{"id":"LA_kwDOQRmfac8AAAACPKPCJg","name":"enhancement","description":"New feature or request","color":"a2eeef"},{"id":"LA_kwDOQRmfac8AAAACPKQBDw","name":"bounty","description":"","color":"d35a32"}],"number":44,"title":"Installation Templates for Common Stacks"},{"assignees":[{"id":"U_kgDOBw4eqA","login":"Sahilbhatane","name":"Sahil Bhatane","databaseId":0}],"labels":[{"id":"LA_kwDOQRmfac8AAAACPKPCJg","name":"enhancement","description":"New feature or request","color":"a2eeef"},{"id":"LA_kwDOQRmfac8AAAACPKQBDw","name":"bounty","description":"","color":"d35a32"}],"number":43,"title":"Smart Retry Logic with Exponential Backoff"},{"assignees":[{"id":"U_kgDOBw4eqA","login":"Sahilbhatane","name":"Sahil Bhatane","databaseId":0}],"labels":[{"id":"LA_kwDOQRmfac8AAAACPKPCJg","name":"enhancement","description":"New feature or request","color":"a2eeef"},{"id":"LA_kwDOQRmfac8AAAACPKQBDw","name":"bounty","description":"","color":"d35a32"},{"id":"LA_kwDOQRmfac8AAAACPfoNWw","name":"high-priority","description":"","color":"ededed"}],"number":42,"title":"Package Conflict Resolution UI"},{"assignees":[{"id":"U_kgDOBw4eqA","login":"Sahilbhatane","name":"Sahil Bhatane","databaseId":0}],"labels":[{"id":"LA_kwDOQRmfac8AAAACPKPCJg","name":"enhancement","description":"New feature or request","color":"a2eeef"},{"id":"LA_kwDOQRmfac8AAAACPKQBDw","name":"bounty","description":"","color":"d35a32"}],"number":40,"title":"Kimi K2 API Integration"},{"assignees":[{"id":"MDQ6VXNlcjQ0MTMxOTkx","login":"danishirfan21","name":"Danish Irfan","databaseId":0}],"labels":[{"id":"LA_kwDOQRmfac8AAAACPKPCKg","name":"good first issue","description":"Good for newcomers","color":"7057ff"},{"id":"LA_kwDOQRmfac8AAAACPKPCLQ","name":"help wanted","description":"Extra attention is needed","color":"008672"},{"id":"LA_kwDOQRmfac8AAAACPKQBDw","name":"bounty","description":"","color":"d35a32"}],"number":33,"title":"Configuration Export/Import"},{"assignees":[{"id":"MDQ6VXNlcjU1MzE3NzY4","login":"dhvll","name":"Dhaval","databaseId":0}],"labels":[{"id":"LA_kwDOQRmfac8AAAACPKPCLQ","name":"help wanted","description":"Extra attention is needed","color":"008672"},{"id":"LA_kwDOQRmfac8AAAACPKQBDw","name":"bounty","description":"","color":"d35a32"},{"id":"LA_kwDOQRmfac8AAAACPKQS2g","name":"expert wanted","description":"","color":"70070e"}],"number":32,"title":"Batch Operations & Parallel Execution"},{"assignees":[{"id":"U_kgDOC1JCog","login":"anees4500","name":"","databaseId":0}],"labels":[{"id":"LA_kwDOQRmfac8AAAACPKQBDw","name":"bounty","description":"","color":"d35a32"},{"id":"LA_kwDOQRmfac8AAAACPKQS2g","name":"expert wanted","description":"","color":"70070e"}],"number":31,"title":"Plugin System & Extension API"},{"assignees":[{"id":"MDQ6VXNlcjU1MzE3NzY4","login":"dhvll","name":"Dhaval","databaseId":0}],"labels":[{"id":"LA_kwDOQRmfac8AAAACP7QVpw","name":"priority: high","description":"Important for MVP completion","color":"D93F0B"},{"id":"LA_kwDOQRmfac8AAAACP7QVyQ","name":"status: ready","description":"Ready to claim and work on","color":"0E8A16"}],"number":30,"title":"Self-Update & Version Management"},{"assignees":[{"id":"MDQ6VXNlcjE0ODM2MDU2","login":"brymut","name":"Bryan Mutai","databaseId":0}],"labels":[{"id":"LA_kwDOQRmfac8AAAACP7QVpw","name":"priority: high","description":"Important for MVP completion","color":"D93F0B"},{"id":"LA_kwDOQRmfac8AAAACP7QVyQ","name":"status: ready","description":"Ready to claim and work on","color":"0E8A16"}],"number":26,"title":"User Preferences & Settings System"},{"assignees":[],"labels":[{"id":"LA_kwDOQRmfac8AAAACP7QVpw","name":"priority: high","description":"Important for MVP completion","color":"D93F0B"},{"id":"LA_kwDOQRmfac8AAAACP7QVyQ","name":"status: ready","description":"Ready to claim and work on","color":"0E8A16"}],"number":25,"title":"Network & Proxy Configuration Support"},{"assignees":[],"labels":[],"number":19,"title":"## Testing & Integration Bounties Available"},{"assignees":[{"id":"MDQ6VXNlcjczMzc2NjM0","login":"mikejmorgan-ai","name":"Mike Morgan","databaseId":0}],"labels":[{"id":"LA_kwDOQRmfac8AAAACPKPCHw","name":"documentation","description":"Improvements or additions to documentation","color":"0075ca"},{"id":"LA_kwDOQRmfac8AAAACPKPCKg","name":"good first issue","description":"Good for newcomers","color":"7057ff"},{"id":"LA_kwDOQRmfac8AAAACPKPzKA","name":"help-wanted","description":"","color":"aaaaaa"},{"id":"LA_kwDOQRmfac8AAAACPN20aA","name":"testing","description":"","color":"aaaaaa"}],"number":16,"title":"End-to-end integration test suite"},{"assignees":[{"id":"U_kgDODd9RQA","login":"shalinibhavi525-sudo","name":"","databaseId":0}],"labels":[{"id":"LA_kwDOQRmfac8AAAACPKPCHw","name":"documentation","description":"Improvements or additions to documentation","color":"0075ca"},{"id":"LA_kwDOQRmfac8AAAACPKPCKg","name":"good first issue","description":"Good for newcomers","color":"7057ff"},{"id":"LA_kwDOQRmfac8AAAACPKPzKA","name":"help-wanted","description":"","color":"aaaaaa"}],"number":15,"title":"Documentation Site"},{"assignees":[{"id":"MDQ6VXNlcjg3MDY4MzM5","login":"aliraza556","name":"Ali Raza","databaseId":0}],"labels":[{"id":"LA_kwDOQRmfac8AAAACP7QVoQ","name":"priority: critical","description":"Must have for MVP - work on these first","color":"B60205"},{"id":"LA_kwDOQRmfac8AAAACP7QVyQ","name":"status: ready","description":"Ready to claim and work on","color":"0E8A16"}],"number":14,"title":"Installation History/Rollback"},{"assignees":[{"id":"U_kgDOBwep0g","login":"chandrapratamar","name":"Chandra Pratama","databaseId":0}],"labels":[{"id":"LA_kwDOQRmfac8AAAACP7QVoQ","name":"priority: critical","description":"Must have for MVP - work on these first","color":"B60205"},{"id":"LA_kwDOQRmfac8AAAACP7QVyQ","name":"status: ready","description":"Ready to claim and work on","color":"0E8A16"}],"number":7,"title":"Build intelligent apt/yum package manager wrapper"}] diff --git a/payments_history.json b/payments_history.json deleted file mode 100644 index fe51488..0000000 --- a/payments_history.json +++ /dev/null @@ -1 +0,0 @@ -[] diff --git a/pr_status.json b/pr_status.json deleted file mode 100644 index b2c3199..0000000 --- a/pr_status.json +++ /dev/null @@ -1 +0,0 @@ -[{"author":{"id":"MDQ6VXNlcjg3MDY4MzM5","is_bot":false,"login":"aliraza556","name":"Ali Raza"},"isDraft":false,"mergeable":"MERGEABLE","number":198,"state":"OPEN","title":"Add installation history tracking and rollback support"},{"author":{"id":"MDQ6VXNlcjczMzc2NjM0","is_bot":false,"login":"mikejmorgan-ai","name":"Mike Morgan"},"isDraft":false,"mergeable":"MERGEABLE","number":197,"state":"OPEN","title":"Remove duplicate workflow"},{"author":{"id":"MDQ6VXNlcjU1MzE3NzY4","is_bot":false,"login":"dhvll","name":"Dhaval"},"isDraft":false,"mergeable":"MERGEABLE","number":195,"state":"OPEN","title":"feat: Introduce intelligent package manager wrapper"},{"author":{"id":"MDQ6VXNlcjY0MDI1NzYy","is_bot":false,"login":"AlexanderLuzDH","name":"Alexander Luz"},"isDraft":false,"mergeable":"CONFLICTING","number":38,"state":"OPEN","title":"feat: Add system requirements pre-flight checker (Issue #28)"},{"author":{"id":"MDQ6VXNlcjg3MDY4MzM5","is_bot":false,"login":"aliraza556","name":"Ali Raza"},"isDraft":false,"mergeable":"MERGEABLE","number":21,"state":"OPEN","title":"feat: Configuration File Template System - Generate nginx, PostgreSQL, Redis, Docker Compose & Apache configs"},{"author":{"id":"U_kgDOBw4eqA","is_bot":false,"login":"Sahilbhatane","name":"Sahil Bhatane"},"isDraft":true,"mergeable":"CONFLICTING","number":18,"state":"OPEN","title":"Add CLI interface for cortex command - Fixes #11"},{"author":{"id":"U_kgDOBwep0g","is_bot":false,"login":"chandrapratamar","name":"Chandra Pratama"},"isDraft":false,"mergeable":"CONFLICTING","number":17,"state":"OPEN","title":"Add Intelligent package manager wrapper"}] diff --git a/audit_cortex_status.sh b/scripts/audit_cortex_status.sh old mode 100755 new mode 100644 similarity index 100% rename from audit_cortex_status.sh rename to scripts/audit_cortex_status.sh diff --git a/cortex-master-automation.sh b/scripts/cortex-master-automation.sh similarity index 100% rename from cortex-master-automation.sh rename to scripts/cortex-master-automation.sh diff --git a/cortex-master-pr-creator.sh b/scripts/cortex-master-pr-creator.sh similarity index 100% rename from cortex-master-pr-creator.sh rename to scripts/cortex-master-pr-creator.sh diff --git a/cortex-master-quarterback.sh b/scripts/cortex-master-quarterback.sh old mode 100755 new mode 100644 similarity index 100% rename from cortex-master-quarterback.sh rename to scripts/cortex-master-quarterback.sh diff --git a/cortex-master-update.sh b/scripts/cortex-master-update.sh old mode 100755 new mode 100644 similarity index 100% rename from cortex-master-update.sh rename to scripts/cortex-master-update.sh diff --git a/cortex-master.sh b/scripts/cortex-master.sh old mode 100755 new mode 100644 similarity index 100% rename from cortex-master.sh rename to scripts/cortex-master.sh diff --git a/cortex-pr-dashboard.sh b/scripts/cortex-pr-dashboard.sh old mode 100755 new mode 100644 similarity index 100% rename from cortex-pr-dashboard.sh rename to scripts/cortex-pr-dashboard.sh diff --git a/deploy_jesse_system (1).sh b/scripts/deploy_jesse_system (1).sh similarity index 100% rename from deploy_jesse_system (1).sh rename to scripts/deploy_jesse_system (1).sh diff --git a/deploy_jesse_system.sh b/scripts/deploy_jesse_system.sh similarity index 100% rename from deploy_jesse_system.sh rename to scripts/deploy_jesse_system.sh diff --git a/focus-on-mvp.sh b/scripts/focus-on-mvp.sh old mode 100755 new mode 100644 similarity index 100% rename from focus-on-mvp.sh rename to scripts/focus-on-mvp.sh diff --git a/merge-mike-prs.sh b/scripts/merge-mike-prs.sh old mode 100755 new mode 100644 similarity index 100% rename from merge-mike-prs.sh rename to scripts/merge-mike-prs.sh diff --git a/organize-issues.sh b/scripts/organize-issues.sh old mode 100755 new mode 100644 similarity index 100% rename from organize-issues.sh rename to scripts/organize-issues.sh diff --git a/review-contributor-prs.sh b/scripts/review-contributor-prs.sh old mode 100755 new mode 100644 similarity index 100% rename from review-contributor-prs.sh rename to scripts/review-contributor-prs.sh diff --git a/setup-github-automation.sh b/scripts/setup-github-automation.sh similarity index 100% rename from setup-github-automation.sh rename to scripts/setup-github-automation.sh diff --git a/setup_and_upload.sh b/scripts/setup_and_upload.sh similarity index 100% rename from setup_and_upload.sh rename to scripts/setup_and_upload.sh diff --git a/upload_issue_34.sh b/scripts/upload_issue_34.sh old mode 100755 new mode 100644 similarity index 100% rename from upload_issue_34.sh rename to scripts/upload_issue_34.sh diff --git a/test/__init__.py b/test/__init__.py new file mode 100644 index 0000000..66173ae --- /dev/null +++ b/test/__init__.py @@ -0,0 +1 @@ +# Test package diff --git a/test/test_conflict_ui.py b/test/test_conflict_ui.py new file mode 100644 index 0000000..26a83d7 --- /dev/null +++ b/test/test_conflict_ui.py @@ -0,0 +1,504 @@ +""" +Test suite for package conflict resolution UI and user preferences. + +Tests cover: +1. Interactive conflict resolution UI +2. User preference saving for conflict resolutions +3. Configuration management commands +4. Conflict detection and resolution workflow +5. Preference persistence and validation + +Note: These tests verify the conflict resolution UI, preference persistence, +and configuration management features implemented in Issue #42. +""" + +import unittest +import sys +import os +from unittest.mock import patch, MagicMock, call +from io import StringIO +from pathlib import Path +import tempfile +import shutil +import json + +sys.path.insert(0, os.path.join(os.path.dirname(__file__), '..')) + +from cortex.cli import CortexCLI +from cortex.user_preferences import PreferencesManager, ConflictSettings +from cortex.dependency_resolver import DependencyResolver + + +class TestConflictResolutionUI(unittest.TestCase): + """Test interactive conflict resolution UI functionality.""" + + def setUp(self): + """Set up test fixtures.""" + self.cli = CortexCLI() + self.temp_dir = tempfile.mkdtemp() + self.config_file = Path(self.temp_dir) / 'test_preferences.yaml' + + # Mock preferences manager to use temp config + self.cli.prefs_manager = PreferencesManager(config_path=self.config_file) + + def tearDown(self): + """Clean up test fixtures.""" + if os.path.exists(self.temp_dir): + shutil.rmtree(self.temp_dir) + + @patch('builtins.input') + @patch('sys.stdout', new_callable=StringIO) + def test_interactive_conflict_resolution_skip(self, mock_stdout, mock_input): + """Test skipping package during conflict resolution.""" + # Simulate user choosing to skip (option 3) + mock_input.side_effect = ['3'] + + conflicts = [ + ('nginx', 'apache2') + ] + + # Should exit on choice 3 + with self.assertRaises(SystemExit): + result = self.cli._resolve_conflicts_interactive(conflicts) + + # Verify skip option was presented + output = mock_stdout.getvalue() + self.assertIn('nginx', output) + self.assertIn('apache2', output) + self.assertIn('Cancel installation', output) + + @patch('builtins.input') + @patch('sys.stdout', new_callable=StringIO) + def test_interactive_conflict_resolution_keep_new(self, mock_stdout, mock_input): + """Test keeping new package during conflict resolution.""" + # Simulate user choosing to keep new (option 1) and not saving preference + mock_input.side_effect = ['1', 'n'] + + conflicts = [ + ('mysql-server', 'mariadb-server') + ] + + result = self.cli._resolve_conflicts_interactive(conflicts) + + # Verify keep new option was presented + output = mock_stdout.getvalue() + self.assertIn('mysql-server', output) + self.assertIn('mariadb-server', output) + self.assertIn('Keep/Install', output) + + # Verify function returns resolution with package to remove + self.assertIn('remove', result) + self.assertIn('mariadb-server', result['remove']) + + @patch('builtins.input') + @patch('sys.stdout', new_callable=StringIO) + def test_interactive_conflict_resolution_keep_existing(self, mock_stdout, mock_input): + """Test keeping existing package during conflict resolution.""" + # Simulate user choosing to keep existing (option 2) and not saving preference + mock_input.side_effect = ['2', 'n'] + + conflicts = [ + ('nginx', 'apache2') + ] + + result = self.cli._resolve_conflicts_interactive(conflicts) + + # Verify keep existing option was presented + output = mock_stdout.getvalue() + self.assertIn('nginx', output) + self.assertIn('apache2', output) + self.assertIn('Keep/Install', output) + + # Verify function returns resolution with package to remove + self.assertIn('remove', result) + self.assertIn('nginx', result['remove']) + + @patch('builtins.input') + def test_invalid_conflict_choice_retry(self, mock_input): + """Test handling invalid input during conflict resolution.""" + # Simulate invalid input followed by valid input and not saving preference + mock_input.side_effect = ['invalid', '99', '1', 'n'] + + conflicts = [ + ('package-a', 'package-b') + ] + + result = self.cli._resolve_conflicts_interactive(conflicts) + + # Verify it eventually accepts valid input + self.assertIn('remove', result) + self.assertIn('package-b', result['remove']) + + # Verify input was called multiple times (including the save preference prompt) + self.assertGreaterEqual(mock_input.call_count, 3) + + +class TestConflictPreferenceSaving(unittest.TestCase): + """Test saving user preferences for conflict resolutions.""" + + def setUp(self): + """Set up test fixtures.""" + self.temp_dir = tempfile.mkdtemp() + self.config_file = Path(self.temp_dir) / 'test_preferences.yaml' + self.prefs_manager = PreferencesManager(config_path=self.config_file) + self.cli = CortexCLI() + self.cli.prefs_manager = self.prefs_manager + + def tearDown(self): + """Clean up test fixtures.""" + if os.path.exists(self.temp_dir): + shutil.rmtree(self.temp_dir) + + @patch('builtins.input') + def test_save_conflict_preference_yes(self, mock_input): + """Test saving conflict preference when user chooses yes.""" + # Simulate user choosing to save preference + mock_input.return_value = 'y' + + self.cli._ask_save_preference('nginx', 'apache2', 'nginx') + + # Verify preference is in manager (uses min:max format) + saved = self.prefs_manager.get('conflicts.saved_resolutions') + conflict_key = 'apache2:nginx' # min:max format + self.assertIn(conflict_key, saved) + self.assertEqual(saved[conflict_key], 'nginx') + + @patch('builtins.input') + def test_save_conflict_preference_no(self, mock_input): + """Test not saving conflict preference when user chooses no.""" + # Simulate user choosing not to save preference + mock_input.return_value = 'n' + + self.cli._ask_save_preference('package-a', 'package-b', 'package-a') + + # Verify preference is not in manager (uses min:max format) + saved = self.prefs_manager.get('conflicts.saved_resolutions') + conflict_key = 'package-a:package-b' # min:max format + self.assertNotIn(conflict_key, saved) + + def test_conflict_preference_persistence(self): + """Test that saved conflict preferences persist across sessions.""" + # Save a preference (using min:max format) + self.prefs_manager.set('conflicts.saved_resolutions', { + 'mariadb-server:mysql-server': 'mysql-server' + }) + self.prefs_manager.save() + + # Create new preferences manager with same config file + new_prefs = PreferencesManager(config_path=self.config_file) + new_prefs.load() + + # Verify preference was loaded + saved = new_prefs.get('conflicts.saved_resolutions') + self.assertIn('mariadb-server:mysql-server', saved) + self.assertEqual(saved['mariadb-server:mysql-server'], 'mysql-server') + + def test_multiple_conflict_preferences(self): + """Test saving and retrieving multiple conflict preferences.""" + # Save multiple preferences (using min:max format) + resolutions = { + 'apache2:nginx': 'nginx', + 'mariadb-server:mysql-server': 'mariadb-server', + 'emacs:vim': 'vim' + } + + for conflict, choice in resolutions.items(): + self.prefs_manager.set( + 'conflicts.saved_resolutions', + {**self.prefs_manager.get('conflicts.saved_resolutions'), conflict: choice} + ) + + self.prefs_manager.save() + + # Verify all preferences were saved + saved = self.prefs_manager.get('conflicts.saved_resolutions') + for conflict, choice in resolutions.items(): + self.assertIn(conflict, saved) + self.assertEqual(saved[conflict], choice) + + +class TestConfigurationManagement(unittest.TestCase): + """Test configuration management commands.""" + + def setUp(self): + """Set up test fixtures.""" + self.temp_dir = tempfile.mkdtemp() + self.config_file = Path(self.temp_dir) / 'test_preferences.yaml' + self.cli = CortexCLI() + self.cli.prefs_manager = PreferencesManager(config_path=self.config_file) + + def tearDown(self): + """Clean up test fixtures.""" + if os.path.exists(self.temp_dir): + shutil.rmtree(self.temp_dir) + + @patch('sys.stdout', new_callable=StringIO) + def test_config_list_command(self, mock_stdout): + """Test listing all configuration settings.""" + # Set some preferences + self.cli.prefs_manager.set('ai.model', 'gpt-4') + self.cli.prefs_manager.set('verbosity', 'verbose') + + # Run list command + result = self.cli.config('list') + + # Verify success + self.assertEqual(result, 0) + + # Verify output contains settings + output = mock_stdout.getvalue() + self.assertIn('ai.model', output) + self.assertIn('gpt-4', output) + + @patch('sys.stdout', new_callable=StringIO) + def test_config_get_command(self, mock_stdout): + """Test getting specific configuration value.""" + # Set a preference + self.cli.prefs_manager.set('ai.model', 'gpt-4') + + # Run get command + result = self.cli.config('get', 'ai.model') + + # Verify success + self.assertEqual(result, 0) + + # Verify output contains value + output = mock_stdout.getvalue() + self.assertIn('gpt-4', output) + + @patch('sys.stdout', new_callable=StringIO) + def test_config_set_command(self, mock_stdout): + """Test setting configuration value.""" + # Run set command + result = self.cli.config('set', 'ai.model', 'gpt-4') + + # Verify success + self.assertEqual(result, 0) + + # Verify value was set + value = self.cli.prefs_manager.get('ai.model') + self.assertEqual(value, 'gpt-4') + + @patch('builtins.input', return_value='y') + @patch('sys.stdout', new_callable=StringIO) + def test_config_reset_command(self, mock_stdout, mock_input): + """Test resetting configuration to defaults.""" + # Set some preferences + self.cli.prefs_manager.set('ai.model', 'custom-model') + self.cli.prefs_manager.set('verbosity', 'debug') + + # Run reset command + result = self.cli.config('reset') + + # Verify success + self.assertEqual(result, 0) + + # Verify preferences were reset + self.assertEqual(self.cli.prefs_manager.get('ai.model'), 'claude-sonnet-4') + + def test_config_export_import(self): + """Test exporting and importing configuration.""" + export_file = Path(self.temp_dir) / 'export.json' + + # Set preferences + self.cli.prefs_manager.set('ai.model', 'gpt-4') + self.cli.prefs_manager.set('verbosity', 'verbose') + resolutions = {'apache2:nginx': 'nginx'} + self.cli.prefs_manager.set('conflicts.saved_resolutions', resolutions) + + # Export + result = self.cli.config('export', str(export_file)) + self.assertEqual(result, 0) + + # Verify export file exists + self.assertTrue(export_file.exists()) + + # Reset preferences + self.cli.prefs_manager.reset() + + # Import + result = self.cli.config('import', str(export_file)) + self.assertEqual(result, 0) + + # Verify preferences were restored + self.assertEqual(self.cli.prefs_manager.get('ai.model'), 'gpt-4') + self.assertEqual(self.cli.prefs_manager.get('verbosity'), 'verbose') + saved = self.cli.prefs_manager.get('conflicts.saved_resolutions') + self.assertEqual(saved, resolutions) + + +class TestConflictDetectionWorkflow(unittest.TestCase): + """Test conflict detection and resolution workflow.""" + + def setUp(self): + """Set up test fixtures.""" + self.temp_dir = tempfile.mkdtemp() + self.config_file = Path(self.temp_dir) / 'test_preferences.yaml' + self.cli = CortexCLI() + self.cli.prefs_manager = PreferencesManager(config_path=self.config_file) + + def tearDown(self): + """Clean up test fixtures.""" + if os.path.exists(self.temp_dir): + shutil.rmtree(self.temp_dir) + + @patch('cortex.dependency_resolver.DependencyResolver') + @patch('builtins.input') + def test_conflict_detected_triggers_ui(self, mock_input, mock_resolver_class): + """Test that detected conflicts trigger interactive UI.""" + # Mock dependency resolver to return conflicts + mock_resolver = MagicMock() + mock_graph = MagicMock() + mock_graph.conflicts = [('nginx', 'apache2')] + mock_resolver.resolve_dependencies.return_value = mock_graph + mock_resolver_class.return_value = mock_resolver + + # Mock user choosing to skip + mock_input.return_value = '3' + + # Test the conflict resolution logic directly + conflicts = [('nginx', 'apache2')] + + # Should exit on choice 3 + with self.assertRaises(SystemExit): + result = self.cli._resolve_conflicts_interactive(conflicts) + + def test_saved_preference_bypasses_ui(self): + """Test that saved preferences bypass interactive UI.""" + # Save a conflict preference (using min:max format) + conflict_key = 'mariadb-server:mysql-server' + self.cli.prefs_manager.set('conflicts.saved_resolutions', { + conflict_key: 'mysql-server' + }) + self.cli.prefs_manager.save() + + # Verify preference exists + saved = self.cli.prefs_manager.get('conflicts.saved_resolutions') + self.assertIn(conflict_key, saved) + self.assertEqual(saved[conflict_key], 'mysql-server') + + # In real workflow, this preference would be checked before showing UI + if conflict_key in saved: + choice = saved[conflict_key] + self.assertEqual(choice, 'mysql-server') + + @patch('cortex.dependency_resolver.subprocess.run') + def test_dependency_resolver_detects_conflicts(self, mock_run): + """Test that DependencyResolver correctly detects package conflicts.""" + # Mock apt-cache depends output + mock_run.return_value = MagicMock( + returncode=0, + stdout='nginx\n Depends: some-dep\n Conflicts: apache2\n' + ) + + resolver = DependencyResolver() + graph = resolver.resolve_dependencies('nginx') + + # Verify conflicts were detected (DependencyResolver has known patterns) + # nginx conflicts with apache2 in the conflict_patterns + self.assertTrue(len(graph.conflicts) > 0 or mock_run.called) + + +class TestPreferencePersistence(unittest.TestCase): + """Test preference persistence and validation.""" + + def setUp(self): + """Set up test fixtures.""" + self.temp_dir = tempfile.mkdtemp() + self.config_file = Path(self.temp_dir) / 'test_preferences.yaml' + self.prefs_manager = PreferencesManager(config_path=self.config_file) + + def tearDown(self): + """Clean up test fixtures.""" + if os.path.exists(self.temp_dir): + shutil.rmtree(self.temp_dir) + + def test_preferences_save_and_load(self): + """Test saving and loading preferences from file.""" + # Set preferences + self.prefs_manager.set('ai.model', 'gpt-4') + self.prefs_manager.set('conflicts.saved_resolutions', { + 'pkg-a:pkg-b': 'pkg-a' + }) + + # Save to file + self.prefs_manager.save() + + # Verify file exists + self.assertTrue(self.config_file.exists()) + + # Load in new instance + new_prefs = PreferencesManager(config_path=self.config_file) + new_prefs.load() + + # Verify preferences loaded correctly + self.assertEqual(new_prefs.get('ai.model'), 'gpt-4') + saved = new_prefs.get('conflicts.saved_resolutions') + self.assertEqual(saved['pkg-a:pkg-b'], 'pkg-a') + + def test_preference_validation(self): + """Test preference validation logic.""" + # Load/create preferences + prefs = self.prefs_manager.load() + + # Valid preferences + errors = self.prefs_manager.validate() + self.assertEqual(len(errors), 0) + + # Set invalid preference by directly modifying (bypass validation in set()) + prefs.ai.max_suggestions = -999 + errors = self.prefs_manager.validate() + self.assertGreater(len(errors), 0) + + def test_nested_preference_keys(self): + """Test handling nested preference keys.""" + # Set nested preference + self.prefs_manager.set('conflicts.saved_resolutions', { + 'key1': 'value1', + 'key2': 'value2' + }) + + # Get nested preference + value = self.prefs_manager.get('conflicts.saved_resolutions') + self.assertIsInstance(value, dict) + self.assertEqual(value['key1'], 'value1') + + def test_preference_reset_to_defaults(self): + """Test resetting preferences to defaults.""" + # Set custom values + self.prefs_manager.set('ai.model', 'custom-model') + self.prefs_manager.set('verbosity', 'debug') + + # Reset + self.prefs_manager.reset() + + # Verify defaults restored + self.assertEqual(self.prefs_manager.get('ai.model'), 'claude-sonnet-4') + self.assertEqual(self.prefs_manager.get('verbosity'), 'normal') + + def test_preference_export_import_json(self): + """Test exporting and importing preferences as JSON.""" + export_file = Path(self.temp_dir) / 'export.json' + + # Set preferences + self.prefs_manager.set('ai.model', 'gpt-4') + resolutions = {'conflict:test': 'test'} + self.prefs_manager.set('conflicts.saved_resolutions', resolutions) + + # Export + self.prefs_manager.export_json(export_file) + + # Reset + self.prefs_manager.reset() + + # Import + self.prefs_manager.import_json(export_file) + + # Verify + self.assertEqual(self.prefs_manager.get('ai.model'), 'gpt-4') + saved = self.prefs_manager.get('conflicts.saved_resolutions') + self.assertEqual(saved, resolutions) + + +if __name__ == '__main__': + unittest.main() diff --git a/test_context_memory.py b/test/test_context_memory.py similarity index 99% rename from test_context_memory.py rename to test/test_context_memory.py index 9afb39b..2e799a7 100644 --- a/test_context_memory.py +++ b/test/test_context_memory.py @@ -14,9 +14,9 @@ import os # Add parent directory to path for imports -sys.path.insert(0, os.path.dirname(os.path.abspath(__file__))) +sys.path.insert(0, os.path.join(os.path.dirname(__file__), '..')) -from context_memory import ( +from cortex.context_memory import ( ContextMemory, MemoryEntry, Pattern, diff --git a/test_error_parser.py b/test/test_error_parser.py similarity index 97% rename from test_error_parser.py rename to test/test_error_parser.py index 5acf5c5..ed6dbfd 100644 --- a/test_error_parser.py +++ b/test/test_error_parser.py @@ -4,7 +4,12 @@ """ import unittest -from error_parser import ( +import sys +import os + +sys.path.insert(0, os.path.join(os.path.dirname(__file__), '..')) + +from cortex.error_parser import ( ErrorParser, ErrorCategory, ErrorAnalysis diff --git a/test/test_installation_history.py b/test/test_installation_history.py index edcbae2..1c7d0db 100644 --- a/test/test_installation_history.py +++ b/test/test_installation_history.py @@ -6,8 +6,12 @@ import unittest import tempfile import os +import sys from datetime import datetime -from installation_history import ( + +sys.path.insert(0, os.path.join(os.path.dirname(__file__), '..')) + +from cortex.installation_history import ( InstallationHistory, InstallationType, InstallationStatus, diff --git a/test_installation_verifier.py b/test/test_installation_verifier.py similarity index 95% rename from test_installation_verifier.py rename to test/test_installation_verifier.py index 60fa83f..3785f53 100644 --- a/test_installation_verifier.py +++ b/test/test_installation_verifier.py @@ -4,7 +4,12 @@ """ import unittest -from installation_verifier import ( +import sys +import os + +sys.path.insert(0, os.path.join(os.path.dirname(__file__), '..')) + +from cortex.installation_verifier import ( InstallationVerifier, VerificationStatus, VerificationTest diff --git a/LLM/test_interpreter.py b/test/test_interpreter.py similarity index 100% rename from LLM/test_interpreter.py rename to test/test_interpreter.py diff --git a/test_llm_router.py b/test/test_llm_router.py similarity index 97% rename from test_llm_router.py rename to test/test_llm_router.py index 698f17b..ac9520a 100644 --- a/test_llm_router.py +++ b/test/test_llm_router.py @@ -13,9 +13,9 @@ import sys # Add parent directory to path -sys.path.insert(0, os.path.dirname(os.path.abspath(__file__))) +sys.path.insert(0, os.path.join(os.path.dirname(__file__), '..')) -from llm_router import ( +from cortex.llm_router import ( LLMRouter, TaskType, LLMProvider, @@ -244,7 +244,7 @@ def test_reset_stats(self): class TestClaudeIntegration(unittest.TestCase): """Test Claude API integration.""" - @patch('llm_router.Anthropic') + @patch('cortex.llm_router.Anthropic') def test_claude_completion(self, mock_anthropic): """Test Claude completion with mocked API.""" # Mock response @@ -276,7 +276,7 @@ def test_claude_completion(self, mock_anthropic): self.assertEqual(result.tokens_used, 150) self.assertGreater(result.cost_usd, 0) - @patch('llm_router.Anthropic') + @patch('cortex.llm_router.Anthropic') def test_claude_with_system_message(self, mock_anthropic): """Test Claude handles system messages correctly.""" mock_content = Mock() @@ -313,7 +313,7 @@ def test_claude_with_system_message(self, mock_anthropic): class TestKimiIntegration(unittest.TestCase): """Test Kimi K2 API integration.""" - @patch('llm_router.OpenAI') + @patch('cortex.llm_router.OpenAI') def test_kimi_completion(self, mock_openai): """Test Kimi K2 completion with mocked API.""" # Mock response @@ -348,7 +348,7 @@ def test_kimi_completion(self, mock_openai): self.assertEqual(result.tokens_used, 150) self.assertGreater(result.cost_usd, 0) - @patch('llm_router.OpenAI') + @patch('cortex.llm_router.OpenAI') def test_kimi_temperature_mapping(self, mock_openai): """Test Kimi K2 temperature is scaled by 0.6.""" mock_message = Mock() @@ -380,7 +380,7 @@ def test_kimi_temperature_mapping(self, mock_openai): call_args = mock_client.chat.completions.create.call_args self.assertAlmostEqual(call_args.kwargs["temperature"], 0.6, places=2) - @patch('llm_router.OpenAI') + @patch('cortex.llm_router.OpenAI') def test_kimi_with_tools(self, mock_openai): """Test Kimi K2 handles tool calling.""" mock_message = Mock() @@ -422,8 +422,8 @@ def test_kimi_with_tools(self, mock_openai): class TestEndToEnd(unittest.TestCase): """End-to-end integration tests.""" - @patch('llm_router.Anthropic') - @patch('llm_router.OpenAI') + @patch('cortex.llm_router.Anthropic') + @patch('cortex.llm_router.OpenAI') def test_complete_with_routing(self, mock_openai, mock_anthropic): """Test complete() method with full routing.""" # Mock Kimi K2 (should be used for system operations) @@ -457,8 +457,8 @@ def test_complete_with_routing(self, mock_openai, mock_anthropic): self.assertEqual(response.provider, LLMProvider.KIMI_K2) self.assertIn("Installing", response.content) - @patch('llm_router.Anthropic') - @patch('llm_router.OpenAI') + @patch('cortex.llm_router.Anthropic') + @patch('cortex.llm_router.OpenAI') def test_fallback_on_error(self, mock_openai, mock_anthropic): """Test fallback when primary provider fails.""" # Mock Kimi K2 to fail @@ -499,7 +499,7 @@ def test_fallback_on_error(self, mock_openai, mock_anthropic): class TestConvenienceFunction(unittest.TestCase): """Test the complete_task convenience function.""" - @patch('llm_router.LLMRouter') + @patch('cortex.llm_router.LLMRouter') def test_complete_task_simple(self, mock_router_class): """Test simple completion with complete_task().""" # Mock router @@ -519,7 +519,7 @@ def test_complete_task_simple(self, mock_router_class): self.assertEqual(result, "Test response") mock_router.complete.assert_called_once() - @patch('llm_router.LLMRouter') + @patch('cortex.llm_router.LLMRouter') def test_complete_task_with_system_prompt(self, mock_router_class): """Test complete_task() includes system prompt.""" mock_response = Mock() diff --git a/test_logging_system.py b/test/test_logging_system.py similarity index 96% rename from test_logging_system.py rename to test/test_logging_system.py index eb1fa1c..afd56e8 100644 --- a/test_logging_system.py +++ b/test/test_logging_system.py @@ -8,8 +8,8 @@ import sys import os -sys.path.insert(0, os.path.dirname(os.path.abspath(__file__))) -from logging_system import CortexLogger, LogContext +sys.path.insert(0, os.path.join(os.path.dirname(__file__), '..')) +from cortex.logging_system import CortexLogger, LogContext class TestCortexLogger(unittest.TestCase): def setUp(self):