diff --git a/.github/workflows/ty.yml b/.github/workflows/ty.yml index bce42839d..bc6adefe3 100644 --- a/.github/workflows/ty.yml +++ b/.github/workflows/ty.yml @@ -42,4 +42,18 @@ jobs: if: ${{ env.python_files != '' }} run: | echo "Running ty on changed files: ${{ env.python_files }}" - uv run ty check --output-format concise --python-version 3.12 --exclude "**/tests/**" --exclude "**/codegen_tests/**" ${{ env.python_files }} + # Filter out test files for type checking + src_files=() + for file in ${{ env.python_files }}; do + if [[ ! $file == *"/tests/"* && ! $file == *"/codegen_tests/"* ]]; then + src_files+=("${file}") + fi + done + + # Only run type checking if there are source files to check + if [ ${#src_files[@]} -gt 0 ]; then + echo "Running ty on source files: ${src_files[*]}" + uv run ty check --output-format concise --python-version 3.12 ${src_files[@]} + else + echo "No source files to check, skipping ty" + fi diff --git a/QUICK_START_LOGGING.md b/QUICK_START_LOGGING.md index 077cc9cc9..3048df0f6 100644 --- a/QUICK_START_LOGGING.md +++ b/QUICK_START_LOGGING.md @@ -3,8 +3,8 @@ ## โšก TL;DR - 3 Step Process 1. **Import the logger**: `from codegen.shared.logging.get_logger import get_logger` -2. **Add `extra={}` to your log calls**: `logger.info("message", extra={"key": "value"})` -3. **Enable telemetry**: `codegen config telemetry enable` +1. **Add `extra={}` to your log calls**: `logger.info("message", extra={"key": "value"})` +1. **Enable telemetry**: `codegen config telemetry enable` **That's it!** Your logs automatically go to Grafana Cloud when telemetry is enabled. @@ -22,11 +22,14 @@ from codegen.shared.logging.get_logger import get_logger logger = get_logger(__name__) # Find any existing console.print() or error handling and add: -logger.info("Operation completed", extra={ - "operation": "command_name", - "org_id": org_id, # if available - "success": True -}) +logger.info( + "Operation completed", + extra={ + "operation": "command_name", + "org_id": org_id, # if available + "success": True, + }, +) ``` ### 2. Test the Integration Right Now @@ -38,7 +41,7 @@ codegen config telemetry enable # 2. Run the demo python example_enhanced_agent_command.py -# 3. Run any CLI command +# 3. Run any CLI command codegen agents # or any other command # 4. Check status @@ -48,69 +51,56 @@ codegen config telemetry status ## ๐Ÿ“ Copy-Paste Patterns ### Pattern 1: Operation Start/End + ```python logger = get_logger(__name__) # At start of function -logger.info("Operation started", extra={ - "operation": "command.subcommand", - "user_input": relevant_input -}) - -# At end of function -logger.info("Operation completed", extra={ - "operation": "command.subcommand", - "success": True -}) +logger.info("Operation started", extra={"operation": "command.subcommand", "user_input": relevant_input}) + +# At end of function +logger.info("Operation completed", extra={"operation": "command.subcommand", "success": True}) ``` ### Pattern 2: Error Handling + ```python try: # your existing code pass except SomeSpecificError as e: - logger.error("Specific error occurred", extra={ - "operation": "command.subcommand", - "error_type": "specific_error", - "error_details": str(e) - }, exc_info=True) + logger.error("Specific error occurred", extra={"operation": "command.subcommand", "error_type": "specific_error", "error_details": str(e)}, exc_info=True) # your existing error handling ``` ### Pattern 3: API Calls + ```python # Before API call -logger.info("Making API request", extra={ - "operation": "api.request", - "endpoint": "agent/run", - "org_id": org_id -}) +logger.info("Making API request", extra={"operation": "api.request", "endpoint": "agent/run", "org_id": org_id}) # After successful API call -logger.info("API request successful", extra={ - "operation": "api.request", - "endpoint": "agent/run", - "response_id": response.get("id"), - "status_code": response.status_code -}) +logger.info("API request successful", extra={"operation": "api.request", "endpoint": "agent/run", "response_id": response.get("id"), "status_code": response.status_code}) ``` ## ๐ŸŽฏ What to Log (Priority Order) ### ๐Ÿ”ฅ High Priority (Add These First) + - **Operation start/end**: When commands begin/complete - **API calls**: Requests to your backend -- **Authentication events**: Login/logout/token issues +- **Authentication events**: Login/logout/token issues - **Errors**: Any exception or failure - **User actions**: Commands run, options selected ### โญ Medium Priority + - **Performance**: Duration of operations - **State changes**: Status updates, configuration changes - **External tools**: Claude CLI detection, git operations ### ๐Ÿ’ก Low Priority (Nice to Have) + - **Debug info**: Internal state, validation steps - **User behavior**: Which features are used most @@ -126,30 +116,30 @@ logger = get_logger(__name__) def create(prompt: str, org_id: int | None = None, ...): """Create a new agent run with the given prompt.""" - + # ADD: Log start logger.info("Agent creation started", extra={ "operation": "agent.create", "org_id": org_id, "prompt_length": len(prompt) }) - + # Your existing code... try: response = requests.post(url, headers=headers, json=payload) agent_run_data = response.json() - - # ADD: Log success + + # ADD: Log success logger.info("Agent created successfully", extra={ "operation": "agent.create", "agent_run_id": agent_run_data.get("id"), "status": agent_run_data.get("status") }) - + except requests.RequestException as e: # ADD: Log error logger.error("Agent creation failed", extra={ - "operation": "agent.create", + "operation": "agent.create", "error_type": "api_error", "error": str(e) }) @@ -163,37 +153,34 @@ def create(prompt: str, org_id: int | None = None, ...): logger = get_logger(__name__) + def _run_claude_interactive(resolved_org_id: int, no_mcp: bool | None) -> None: session_id = generate_session_id() - + # ADD: Log session start - logger.info("Claude session started", extra={ - "operation": "claude.session_start", - "session_id": session_id[:8], # Short version for privacy - "org_id": resolved_org_id - }) - + logger.info( + "Claude session started", + extra={ + "operation": "claude.session_start", + "session_id": session_id[:8], # Short version for privacy + "org_id": resolved_org_id, + }, + ) + # Your existing code... - + try: process = subprocess.Popen([claude_path, "--session-id", session_id]) returncode = process.wait() - + # ADD: Log session end - logger.info("Claude session completed", extra={ - "operation": "claude.session_complete", - "session_id": session_id[:8], - "exit_code": returncode, - "status": "COMPLETE" if returncode == 0 else "ERROR" - }) - + logger.info( + "Claude session completed", extra={"operation": "claude.session_complete", "session_id": session_id[:8], "exit_code": returncode, "status": "COMPLETE" if returncode == 0 else "ERROR"} + ) + except Exception as e: # ADD: Log session error - logger.error("Claude session failed", extra={ - "operation": "claude.session_error", - "session_id": session_id[:8], - "error": str(e) - }) + logger.error("Claude session failed", extra={"operation": "claude.session_error", "session_id": session_id[:8], "error": str(e)}) ``` ## ๐Ÿงช Verification @@ -201,14 +188,14 @@ def _run_claude_interactive(resolved_org_id: int, no_mcp: bool | None) -> None: After making changes: 1. **Run the command**: Execute your enhanced CLI command -2. **Check telemetry status**: `codegen config telemetry status` -3. **Look for logs in Grafana Cloud**: Search for your operation names -4. **Test with telemetry disabled**: `codegen config telemetry disable` - should still work normally +1. **Check telemetry status**: `codegen config telemetry status` +1. **Look for logs in Grafana Cloud**: Search for your operation names +1. **Test with telemetry disabled**: `codegen config telemetry disable` - should still work normally ## ๐Ÿš€ Progressive Enhancement **Week 1**: Add basic operation logging to 2-3 commands -**Week 2**: Add error logging to all commands +**Week 2**: Add error logging to all commands **Week 3**: Add performance metrics and detailed context **Week 4**: Create Grafana dashboards using the collected data diff --git a/src/codegen/__init__.py b/src/codegen/__init__.py index 983bfec55..ca04e791f 100644 --- a/src/codegen/__init__.py +++ b/src/codegen/__init__.py @@ -1,12 +1,19 @@ # file generated by setuptools-scm # don't change, don't track in version control -__all__ = ["__version__", "__version_tuple__", "version", "version_tuple"] +# Import Agent class for top-level access +from codegen.agents.agent import Agent + +__all__ = [ + "Agent", + "__version__", + "__version_tuple__", + "version", + "version_tuple", +] TYPE_CHECKING = False if TYPE_CHECKING: - from typing import Tuple - VERSION_TUPLE = tuple[int | str, ...] else: VERSION_TUPLE = object diff --git a/src/codegen/cli/commands/claude/__init__.py b/src/codegen/cli/commands/claude/__init__.py index 82ffa52a8..1f949d8f6 100644 --- a/src/codegen/cli/commands/claude/__init__.py +++ b/src/codegen/cli/commands/claude/__init__.py @@ -1,2 +1 @@ """Claude Code integration commands.""" - diff --git a/src/codegen/cli/commands/claude/claude_log_utils.py b/src/codegen/cli/commands/claude/claude_log_utils.py index 5268726de..f850b91b7 100644 --- a/src/codegen/cli/commands/claude/claude_log_utils.py +++ b/src/codegen/cli/commands/claude/claude_log_utils.py @@ -4,7 +4,7 @@ import os import re from pathlib import Path -from typing import Dict, Any, Optional +from typing import Any, Optional def get_hyphenated_cwd() -> str: @@ -39,7 +39,7 @@ def get_claude_session_log_path(session_id: str) -> Path: return log_file -def parse_jsonl_line(line: str) -> Optional[Dict[str, Any]]: +def parse_jsonl_line(line: str) -> Optional[dict[str, Any]]: """Parse a single line from a JSONL file. Args: @@ -85,13 +85,13 @@ def read_existing_log_lines(log_path: Path) -> int: return 0 try: - with open(log_path, "r", encoding="utf-8") as f: + with open(log_path, encoding="utf-8") as f: return sum(1 for _ in f) except (OSError, UnicodeDecodeError): return 0 -def validate_log_entry(log_entry: Dict[str, Any]) -> bool: +def validate_log_entry(log_entry: dict[str, Any]) -> bool: """Validate a log entry before sending to API. Args: @@ -112,7 +112,7 @@ def validate_log_entry(log_entry: Dict[str, Any]) -> bool: return True -def format_log_for_api(log_entry: Dict[str, Any]) -> Dict[str, Any]: +def format_log_for_api(log_entry: dict[str, Any]) -> dict[str, Any]: """Format a log entry for sending to the API. Args: diff --git a/src/codegen/cli/commands/claude/claude_log_watcher.py b/src/codegen/cli/commands/claude/claude_log_watcher.py index 5c6416702..72a11ec80 100644 --- a/src/codegen/cli/commands/claude/claude_log_watcher.py +++ b/src/codegen/cli/commands/claude/claude_log_watcher.py @@ -1,20 +1,18 @@ """Claude Code session log watcher implementation.""" -import time import threading -from pathlib import Path -from typing import Optional, Callable, Dict, Any - -from .quiet_console import console +import time +from typing import Any, Callable, Optional -from .claude_log_utils import get_claude_session_log_path, parse_jsonl_line, read_existing_log_lines, validate_log_entry, format_log_for_api +from .claude_log_utils import format_log_for_api, get_claude_session_log_path, parse_jsonl_line, read_existing_log_lines, validate_log_entry from .claude_session_api import send_claude_session_log +from .quiet_console import console class ClaudeLogWatcher: """Watches Claude Code session log files for new entries and sends them to the API.""" - def __init__(self, session_id: str, org_id: Optional[int] = None, poll_interval: float = 1.0, on_log_entry: Optional[Callable[[Dict[str, Any]], None]] = None): + def __init__(self, session_id: str, org_id: Optional[int] = None, poll_interval: float = 1.0, on_log_entry: Optional[Callable[[dict[str, Any]], None]] = None): """Initialize the log watcher. Args: @@ -106,7 +104,7 @@ def _check_for_new_entries(self) -> None: except Exception as e: console.print(f"โš ๏ธ Error reading log file: {e}", style="yellow") - def _read_new_lines(self, start_line: int, end_line: int) -> list[Dict[str, Any]]: + def _read_new_lines(self, start_line: int, end_line: int) -> list[dict[str, Any]]: """Read new lines from the log file. Args: @@ -119,7 +117,7 @@ def _read_new_lines(self, start_line: int, end_line: int) -> list[Dict[str, Any] entries = [] try: - with open(self.log_path, "r", encoding="utf-8") as f: + with open(self.log_path, encoding="utf-8") as f: lines = f.readlines() # Read only the new lines @@ -135,7 +133,7 @@ def _read_new_lines(self, start_line: int, end_line: int) -> list[Dict[str, Any] return entries - def _process_log_entry(self, log_entry: Dict[str, Any]) -> None: + def _process_log_entry(self, log_entry: dict[str, Any]) -> None: """Process a single log entry. Args: @@ -161,7 +159,7 @@ def _process_log_entry(self, log_entry: Dict[str, Any]) -> None: # Send to API self._send_log_entry(formatted_entry) - def _send_log_entry(self, log_entry: Dict[str, Any]) -> None: + def _send_log_entry(self, log_entry: dict[str, Any]) -> None: """Send a log entry to the API. Args: @@ -181,7 +179,7 @@ def _send_log_entry(self, log_entry: Dict[str, Any]) -> None: self.total_send_failures += 1 console.print(f"โš ๏ธ Failed to send log entry: {e}", style="yellow") - def get_stats(self) -> Dict[str, Any]: + def get_stats(self) -> dict[str, Any]: """Get watcher statistics. Returns: @@ -204,9 +202,9 @@ class ClaudeLogWatcherManager: """Manages multiple log watchers for different sessions.""" def __init__(self): - self.watchers: Dict[str, ClaudeLogWatcher] = {} + self.watchers: dict[str, ClaudeLogWatcher] = {} - def start_watcher(self, session_id: str, org_id: Optional[int] = None, poll_interval: float = 1.0, on_log_entry: Optional[Callable[[Dict[str, Any]], None]] = None) -> bool: + def start_watcher(self, session_id: str, org_id: Optional[int] = None, poll_interval: float = 1.0, on_log_entry: Optional[Callable[[dict[str, Any]], None]] = None) -> bool: """Start a log watcher for a session. Args: @@ -252,7 +250,7 @@ def get_active_sessions(self) -> list[str]: """ return list(self.watchers.keys()) - def get_watcher_stats(self, session_id: str) -> Optional[Dict[str, Any]]: + def get_watcher_stats(self, session_id: str) -> Optional[dict[str, Any]]: """Get stats for a specific watcher. Args: @@ -265,7 +263,7 @@ def get_watcher_stats(self, session_id: str) -> Optional[Dict[str, Any]]: return self.watchers[session_id].get_stats() return None - def get_all_stats(self) -> Dict[str, Dict[str, Any]]: + def get_all_stats(self) -> dict[str, dict[str, Any]]: """Get stats for all active watchers. Returns: diff --git a/src/codegen/cli/commands/claude/claude_session_api.py b/src/codegen/cli/commands/claude/claude_session_api.py index 6ff5e5566..46f4edf7b 100644 --- a/src/codegen/cli/commands/claude/claude_session_api.py +++ b/src/codegen/cli/commands/claude/claude_session_api.py @@ -5,12 +5,13 @@ from typing import Optional import requests -from .quiet_console import console from codegen.cli.api.endpoints import API_ENDPOINT from codegen.cli.auth.token_manager import get_current_token from codegen.cli.utils.org import resolve_org_id +from .quiet_console import console + class ClaudeSessionAPIError(Exception): """Exception raised for Claude session API errors.""" diff --git a/src/codegen/cli/commands/claude/main.py b/src/codegen/cli/commands/claude/main.py index 41e532d42..e769c6bb3 100644 --- a/src/codegen/cli/commands/claude/main.py +++ b/src/codegen/cli/commands/claude/main.py @@ -290,7 +290,7 @@ def signal_handler(signum, frame): console.print("โœ… Claude Code finished successfully", style="green") except FileNotFoundError: - logger.error( + logger.exception( "Claude Code executable not found", extra={"operation": "claude.interactive", "org_id": resolved_org_id, "claude_session_id": session_id, "error_type": "claude_executable_not_found", **_get_session_context()}, ) diff --git a/src/codegen/cli/commands/claude/quiet_console.py b/src/codegen/cli/commands/claude/quiet_console.py index b9a040c3d..bb459e88d 100644 --- a/src/codegen/cli/commands/claude/quiet_console.py +++ b/src/codegen/cli/commands/claude/quiet_console.py @@ -8,6 +8,7 @@ import io import os + from rich.console import Console @@ -30,4 +31,3 @@ def _create_console() -> Console: # Shared console used across Claude CLI modules console = _create_console() - diff --git a/src/codegen/cli/commands/config/telemetry.py b/src/codegen/cli/commands/config/telemetry.py index 9aed6d204..4c4c84ffc 100644 --- a/src/codegen/cli/commands/config/telemetry.py +++ b/src/codegen/cli/commands/config/telemetry.py @@ -1,12 +1,9 @@ """Telemetry configuration commands.""" import json -from pathlib import Path import typer from rich.console import Console -from rich.panel import Panel -from rich.syntax import Syntax from rich.table import Table from codegen.cli.telemetry import update_telemetry_consent diff --git a/src/codegen/cli/commands/org/__init__.py b/src/codegen/cli/commands/org/__init__.py index ba2d89354..f16fd73c5 100644 --- a/src/codegen/cli/commands/org/__init__.py +++ b/src/codegen/cli/commands/org/__init__.py @@ -2,4 +2,4 @@ from .main import org -__all__ = ["org"] \ No newline at end of file +__all__ = ["org"] diff --git a/src/codegen/cli/commands/org/main.py b/src/codegen/cli/commands/org/main.py index 5b5f2a4d2..b05715819 100644 --- a/src/codegen/cli/commands/org/main.py +++ b/src/codegen/cli/commands/org/main.py @@ -73,14 +73,14 @@ def _set_default_organization(org_id: int, cached_orgs: list[dict]) -> None: # Set the environment variable os.environ["CODEGEN_ORG_ID"] = str(org_id) - + # Try to update .env file if it exists env_file_path = ".env" if os.path.exists(env_file_path): _update_env_file(env_file_path, "CODEGEN_ORG_ID", str(org_id)) console.print(f"[green]โœ“ Updated {env_file_path} with CODEGEN_ORG_ID={org_id}[/green]") else: - console.print(f"[yellow]Info:[/yellow] No .env file found. Set environment variable manually:") + console.print("[yellow]Info:[/yellow] No .env file found. Set environment variable manually:") console.print(f"[cyan]export CODEGEN_ORG_ID={org_id}[/cyan]") console.print(f"[green]โœ“ Default organization set to:[/green] {org_found['name']} ({org_id})") @@ -100,8 +100,8 @@ def _update_env_file(file_path: str, key: str, value: str) -> None: # Ensure all lines end with newline for i, line in enumerate(lines): - if not line.endswith('\n'): - lines[i] = line + '\n' + if not line.endswith("\n"): + lines[i] = line + "\n" # Update existing key or note if we need to add it for i, line in enumerate(lines): @@ -126,4 +126,4 @@ def _run_org_selector_tui() -> None: app.run() except Exception as e: console.print(f"[red]Error launching TUI:[/red] {e}") - raise typer.Exit(1) \ No newline at end of file + raise typer.Exit(1) diff --git a/src/codegen/cli/commands/org/tui.py b/src/codegen/cli/commands/org/tui.py index f1640103d..fd6fda0aa 100644 --- a/src/codegen/cli/commands/org/tui.py +++ b/src/codegen/cli/commands/org/tui.py @@ -29,37 +29,31 @@ def __init__(self): def compose(self) -> ComposeResult: """Create child widgets for the app.""" yield Header() - + if not self.organizations: - yield Container( - Static("โš ๏ธ No organizations found. Please run 'codegen login' first.", classes="warning-message"), - id="no-orgs-warning" - ) + yield Container(Static("โš ๏ธ No organizations found. Please run 'codegen login' first.", classes="warning-message"), id="no-orgs-warning") else: with Vertical(): yield Static("๐Ÿข Select Your Organization", classes="title") yield Static("Use โ†‘โ†“ to navigate, Enter to select, Q/Esc to quit", classes="help") - + table = DataTable(id="orgs-table", cursor_type="row") table.add_columns("Current", "ID", "Organization Name") - + # Get the actual current org ID (checks environment variables first) actual_current_org_id = resolve_org_id() - + for org in self.organizations: org_id = org["id"] org_name = org["name"] is_current = "โ—" if org_id == actual_current_org_id else " " - + table.add_row(is_current, str(org_id), org_name, key=str(org_id)) - + yield table - - yield Static( - "\n๐Ÿ’ก Selecting an organization will update your CODEGEN_ORG_ID environment variable.", - classes="help" - ) - + + yield Static("\n๐Ÿ’ก Selecting an organization will update your CODEGEN_ORG_ID environment variable.", classes="help") + yield Footer() def on_mount(self) -> None: @@ -89,7 +83,7 @@ def _handle_org_selection(self) -> None: try: table = self.query_one("#orgs-table", DataTable) - + if table.cursor_row is not None and table.cursor_row < len(self.organizations): # Get the selected organization directly from the cursor position selected_org = self.organizations[table.cursor_row] @@ -106,24 +100,24 @@ def _set_organization(self, org_id: int, org_name: str) -> None: """Set the selected organization as default.""" # Set environment variable os.environ["CODEGEN_ORG_ID"] = str(org_id) - + # Try to update .env file env_updated = self._update_env_file(org_id) - + if env_updated: self.notify(f"โœ“ Set default organization: {org_name} (ID: {org_id})") self.notify("โœ“ Updated .env file with CODEGEN_ORG_ID") else: self.notify(f"โœ“ Set organization: {org_name} (ID: {org_id})") - self.notify("โ„น Add 'export CODEGEN_ORG_ID={org_id}' to your shell for persistence") - + self.notify("i Add 'export CODEGEN_ORG_ID={org_id}' to your shell for persistence") + # Wait a moment for user to see the notifications, then close self.set_timer(2.0, self._close_screen) def _update_env_file(self, org_id: int) -> bool: """Update the .env file with the new organization ID.""" env_file_path = ".env" - + try: lines = [] key_found = False @@ -135,8 +129,8 @@ def _update_env_file(self, org_id: int) -> bool: # Ensure all lines end with newline for i, line in enumerate(lines): - if not line.endswith('\n'): - lines[i] = line + '\n' + if not line.endswith("\n"): + lines[i] = line + "\n" # Update existing CODEGEN_ORG_ID or note that we need to add it for i, line in enumerate(lines): @@ -152,9 +146,9 @@ def _update_env_file(self, org_id: int) -> bool: # Write back to file with open(env_file_path, "w") as f: f.writelines(lines) - + return True - + except Exception: return False @@ -174,7 +168,7 @@ def action_quit(self) -> None: class OrgSelectorApp(App): """Standalone app wrapper for the organization selector.""" - + CSS_PATH = "../../tui/codegen_theme.tcss" # Use custom Codegen theme TITLE = "Organization Selector - Codegen CLI" BINDINGS = [ @@ -191,37 +185,31 @@ def __init__(self): def compose(self) -> ComposeResult: """Create child widgets for the app.""" yield Header() - + if not self.organizations: - yield Container( - Static("โš ๏ธ No organizations found. Please run 'codegen login' first.", classes="warning-message"), - id="no-orgs-warning" - ) + yield Container(Static("โš ๏ธ No organizations found. Please run 'codegen login' first.", classes="warning-message"), id="no-orgs-warning") else: with Vertical(): yield Static("๐Ÿข Select Your Organization", classes="title") yield Static("Use โ†‘โ†“ to navigate, Enter to select, Q/Esc to quit", classes="help") - + table = DataTable(id="orgs-table", cursor_type="row") table.add_columns("Current", "ID", "Organization Name") - + # Get the actual current org ID (checks environment variables first) actual_current_org_id = resolve_org_id() - + for org in self.organizations: org_id = org["id"] org_name = org["name"] is_current = "โ—" if org_id == actual_current_org_id else " " - + table.add_row(is_current, str(org_id), org_name, key=str(org_id)) - + yield table - - yield Static( - "\n๐Ÿ’ก Selecting an organization will update your CODEGEN_ORG_ID environment variable.", - classes="help" - ) - + + yield Static("\n๐Ÿ’ก Selecting an organization will update your CODEGEN_ORG_ID environment variable.", classes="help") + yield Footer() def on_mount(self) -> None: @@ -251,7 +239,7 @@ def _handle_org_selection(self) -> None: try: table = self.query_one("#orgs-table", DataTable) - + if table.cursor_row is not None and table.cursor_row < len(self.organizations): # Get the selected organization directly from the cursor position selected_org = self.organizations[table.cursor_row] @@ -268,24 +256,24 @@ def _set_organization(self, org_id: int, org_name: str) -> None: """Set the selected organization as default.""" # Set environment variable os.environ["CODEGEN_ORG_ID"] = str(org_id) - + # Try to update .env file env_updated = self._update_env_file(org_id) - + if env_updated: self.notify(f"โœ“ Set default organization: {org_name} (ID: {org_id})") self.notify("โœ“ Updated .env file with CODEGEN_ORG_ID") else: self.notify(f"โœ“ Set organization: {org_name} (ID: {org_id})") - self.notify("โ„น Add 'export CODEGEN_ORG_ID={org_id}' to your shell for persistence") - + self.notify("i Add 'export CODEGEN_ORG_ID={org_id}' to your shell for persistence") + # Wait a moment for user to see the notifications, then exit self.set_timer(2.0, self.exit) def _update_env_file(self, org_id: int) -> bool: """Update the .env file with the new organization ID.""" env_file_path = ".env" - + try: lines = [] key_found = False @@ -297,8 +285,8 @@ def _update_env_file(self, org_id: int) -> bool: # Ensure all lines end with newline for i, line in enumerate(lines): - if not line.endswith('\n'): - lines[i] = line + '\n' + if not line.endswith("\n"): + lines[i] = line + "\n" # Update existing CODEGEN_ORG_ID or note that we need to add it for i, line in enumerate(lines): @@ -314,12 +302,12 @@ def _update_env_file(self, org_id: int) -> bool: # Write back to file with open(env_file_path, "w") as f: f.writelines(lines) - + return True - + except Exception: return False def action_quit(self) -> None: """Quit the application.""" - self.exit() \ No newline at end of file + self.exit() diff --git a/src/codegen/cli/commands/repo/__init__.py b/src/codegen/cli/commands/repo/__init__.py index af83c5eba..245a036bf 100644 --- a/src/codegen/cli/commands/repo/__init__.py +++ b/src/codegen/cli/commands/repo/__init__.py @@ -2,4 +2,4 @@ from .main import repo -__all__ = ["repo"] \ No newline at end of file +__all__ = ["repo"] diff --git a/src/codegen/cli/commands/repo/main.py b/src/codegen/cli/commands/repo/main.py index d3eb00fa7..8e5e16dc3 100644 --- a/src/codegen/cli/commands/repo/main.py +++ b/src/codegen/cli/commands/repo/main.py @@ -4,15 +4,8 @@ from rich.console import Console from rich.table import Table -from codegen.cli.utils.repo import ( - get_current_repo_id, - get_repo_env_status, - set_repo_env_variable, - update_env_file_with_repo, - clear_repo_env_variables, - ensure_repositories_cached -) from codegen.cli.auth.token_manager import get_current_token +from codegen.cli.utils.repo import clear_repo_env_variables, ensure_repositories_cached, get_current_repo_id, get_repo_env_status, set_repo_env_variable, update_env_file_with_repo console = Console() @@ -24,12 +17,11 @@ def repo( list_repos: bool = typer.Option(False, "--list-repos", "-lr", help="List available repositories"), ): """Manage repository configuration and environment variables.""" - # Handle list repositories mode if list_repos: _list_repositories() return - + # Handle list config mode if list_config: _list_repo_config() @@ -62,7 +54,7 @@ def _list_repo_config() -> None: table.add_row("Current Repository ID", str(current_repo_id), "โœ… Active") else: table.add_row("Current Repository ID", "Not configured", "โŒ Inactive") - + # Environment variables env_status = get_repo_env_status() for var_name, value in env_status.items(): @@ -93,13 +85,13 @@ def _list_repositories() -> None: table.add_column("Current", style="yellow") current_repo_id = get_current_repo_id() - + for repo in repositories: repo_id = repo.get("id", "Unknown") repo_name = repo.get("name", "Unknown") repo_desc = repo.get("description", "") is_current = "โ—" if repo_id == current_repo_id else "" - + table.add_row(str(repo_id), repo_name, repo_desc, is_current) console.print(table) @@ -116,13 +108,13 @@ def _set_default_repository(repo_id: int) -> None: # Try to update .env file env_updated = update_env_file_with_repo(repo_id) - + if env_updated: console.print(f"[green]โœ“[/green] Set default repository ID to: [cyan]{repo_id}[/cyan]") console.print("[green]โœ“[/green] Updated .env file with CODEGEN_REPO_ID") else: console.print(f"[green]โœ“[/green] Set repository ID to: [cyan]{repo_id}[/cyan]") - console.print("[yellow]โ„น[/yellow] Could not update .env file. Add 'export CODEGEN_REPO_ID={repo_id}' to your shell for persistence") + console.print("[yellow]i[/yellow] Could not update .env file. Add 'export CODEGEN_REPO_ID={repo_id}' to your shell for persistence") except Exception as e: console.print(f"[red]Error:[/red] Failed to set default repository: {e}") @@ -134,10 +126,10 @@ def _clear_repo_config() -> None: try: clear_repo_env_variables() console.print("[green]โœ“[/green] Cleared repository configuration from environment variables") - + # Note: We don't automatically clear the .env file to avoid data loss - console.print("[yellow]โ„น[/yellow] To permanently remove from .env file, manually delete the CODEGEN_REPO_ID line") - + console.print("[yellow]i[/yellow] To permanently remove from .env file, manually delete the CODEGEN_REPO_ID line") + except Exception as e: console.print(f"[red]Error:[/red] Failed to clear repository configuration: {e}") raise typer.Exit(1) @@ -147,13 +139,13 @@ def _run_repo_selector_tui() -> None: """Launch the repository selector TUI.""" try: from codegen.cli.commands.repo.tui import RepoSelectorApp - + app = RepoSelectorApp() app.run() - + except ImportError: console.print("[red]Error:[/red] Repository selector TUI not available") raise typer.Exit(1) except Exception as e: console.print(f"[red]Error:[/red] Failed to launch repository selector: {e}") - raise typer.Exit(1) \ No newline at end of file + raise typer.Exit(1) diff --git a/src/codegen/cli/commands/repo/tui.py b/src/codegen/cli/commands/repo/tui.py index a9368724c..a13ad124f 100644 --- a/src/codegen/cli/commands/repo/tui.py +++ b/src/codegen/cli/commands/repo/tui.py @@ -8,14 +8,7 @@ from textual.screen import Screen from textual.widgets import DataTable, Footer, Header, Static -from codegen.cli.auth.token_manager import get_cached_repositories, get_current_token -from codegen.cli.utils.repo import ( - get_current_repo_id, - set_repo_env_variable, - update_env_file_with_repo, - ensure_repositories_cached -) -from codegen.cli.utils.org import resolve_org_id +from codegen.cli.utils.repo import ensure_repositories_cached, get_current_repo_id class RepoSelectorTUI(Screen): @@ -35,37 +28,31 @@ def __init__(self): def compose(self) -> ComposeResult: """Create child widgets for the app.""" yield Header() - + if not self.repositories: - yield Container( - Static("โš ๏ธ No repositories found. Fetching repositories...", classes="warning-message"), - id="no-repos-warning" - ) + yield Container(Static("โš ๏ธ No repositories found. Fetching repositories...", classes="warning-message"), id="no-repos-warning") else: with Vertical(): yield Static("๐Ÿ—‚๏ธ Select Your Repository", classes="title") yield Static("Use โ†‘โ†“ to navigate, Enter to select, Q/Esc to quit", classes="help") - + table = DataTable(id="repos-table", cursor_type="row") table.add_columns("Current", "ID", "Repository Name") - + # Get the actual current repo ID (checks environment variables first) actual_current_repo_id = get_current_repo_id() - + for repo in self.repositories: repo_id = repo["id"] repo_name = repo["name"] is_current = "โ—" if repo_id == actual_current_repo_id else " " - + table.add_row(is_current, str(repo_id), repo_name, key=str(repo_id)) - + yield table - - yield Static( - "\n๐Ÿ’ก Selecting a repository will update your CODEGEN_REPO_ID environment variable.", - classes="help" - ) - + + yield Static("\n๐Ÿ’ก Selecting a repository will update your CODEGEN_REPO_ID environment variable.", classes="help") + yield Footer() def on_mount(self) -> None: @@ -94,7 +81,7 @@ def _handle_repo_selection(self) -> None: selected_repo = self.repositories[table.cursor_row] repo_id = selected_repo["id"] repo_name = selected_repo["name"] - + self._set_repository(repo_id, repo_name) except Exception as e: self.notify(f"โŒ Error selecting repository: {e}", severity="error") @@ -103,59 +90,59 @@ def _set_repository(self, repo_id: int, repo_name: str) -> None: """Set the selected repository as the current one.""" # Update environment variable os.environ["CODEGEN_REPO_ID"] = str(repo_id) - + # Try to update .env file env_updated = self._update_env_file(repo_id) - + if env_updated: self.notify(f"โœ“ Set default repository: {repo_name} (ID: {repo_id})") self.notify("โœ“ Updated .env file with CODEGEN_REPO_ID") else: self.notify(f"โœ“ Set repository: {repo_name} (ID: {repo_id})") - self.notify("โ„น Add 'export CODEGEN_REPO_ID={repo_id}' to your shell for persistence") - + self.notify("i Add 'export CODEGEN_REPO_ID={repo_id}' to your shell for persistence") + # Wait a moment for user to see the notifications, then exit self.set_timer(2.0, self._close_screen) def _update_env_file(self, repo_id: int) -> bool: """Update the .env file with the new repository ID.""" env_file_path = ".env" - + try: lines = [] key_updated = False key_to_update = "CODEGEN_REPO_ID" - + # Read existing .env file if it exists if os.path.exists(env_file_path): - with open(env_file_path, "r") as f: + with open(env_file_path) as f: lines = f.readlines() - + # Update or add the key for i, line in enumerate(lines): if line.strip().startswith(f"{key_to_update}="): lines[i] = f"{key_to_update}={repo_id}\n" key_updated = True break - + # If key wasn't found, add it if not key_updated: - if lines and not lines[-1].endswith('\n'): - lines.append('\n') + if lines and not lines[-1].endswith("\n"): + lines.append("\n") lines.append(f"{key_to_update}={repo_id}\n") - + # Write back to file with open(env_file_path, "w") as f: f.writelines(lines) - + return True - + except Exception: return False def _close_screen(self) -> None: """Close the screen.""" - if hasattr(self.app, 'pop_screen'): + if hasattr(self.app, "pop_screen"): self.app.pop_screen() else: self.app.exit() @@ -167,7 +154,7 @@ def action_quit(self) -> None: class RepoSelectorApp(App): """Standalone app wrapper for the repository selector.""" - + CSS_PATH = "../../tui/codegen_theme.tcss" # Use custom Codegen theme TITLE = "Repository Selector - Codegen CLI" BINDINGS = [ @@ -184,37 +171,31 @@ def __init__(self): def compose(self) -> ComposeResult: """Create child widgets for the app.""" yield Header() - + if not self.repositories: - yield Container( - Static("โš ๏ธ No repositories found. Fetching repositories...", classes="warning-message"), - id="no-repos-warning" - ) + yield Container(Static("โš ๏ธ No repositories found. Fetching repositories...", classes="warning-message"), id="no-repos-warning") else: with Vertical(): yield Static("๐Ÿ—‚๏ธ Select Your Repository", classes="title") yield Static("Use โ†‘โ†“ to navigate, Enter to select, Q/Esc to quit", classes="help") - + table = DataTable(id="repos-table", cursor_type="row") table.add_columns("Current", "ID", "Repository Name") - + # Get the actual current repo ID (checks environment variables first) actual_current_repo_id = get_current_repo_id() - + for repo in self.repositories: repo_id = repo["id"] repo_name = repo["name"] is_current = "โ—" if repo_id == actual_current_repo_id else " " - + table.add_row(is_current, str(repo_id), repo_name, key=str(repo_id)) - + yield table - - yield Static( - "\n๐Ÿ’ก Selecting a repository will update your CODEGEN_REPO_ID environment variable.", - classes="help" - ) - + + yield Static("\n๐Ÿ’ก Selecting a repository will update your CODEGEN_REPO_ID environment variable.", classes="help") + yield Footer() def on_mount(self) -> None: @@ -243,7 +224,7 @@ def _handle_repo_selection(self) -> None: selected_repo = self.repositories[table.cursor_row] repo_id = selected_repo["id"] repo_name = selected_repo["name"] - + self._set_repository(repo_id, repo_name) except Exception as e: self.notify(f"โŒ Error selecting repository: {e}", severity="error") @@ -252,52 +233,52 @@ def _set_repository(self, repo_id: int, repo_name: str) -> None: """Set the selected repository as the current one.""" # Update environment variable os.environ["CODEGEN_REPO_ID"] = str(repo_id) - + # Try to update .env file env_updated = self._update_env_file(repo_id) - + if env_updated: self.notify(f"โœ“ Set default repository: {repo_name} (ID: {repo_id})") self.notify("โœ“ Updated .env file with CODEGEN_REPO_ID") else: self.notify(f"โœ“ Set repository: {repo_name} (ID: {repo_id})") - self.notify("โ„น Add 'export CODEGEN_REPO_ID={repo_id}' to your shell for persistence") - + self.notify("i Add 'export CODEGEN_REPO_ID={repo_id}' to your shell for persistence") + # Wait a moment for user to see the notifications, then exit self.set_timer(2.0, self.exit) def _update_env_file(self, repo_id: int) -> bool: """Update the .env file with the new repository ID.""" env_file_path = ".env" - + try: lines = [] key_updated = False key_to_update = "CODEGEN_REPO_ID" - + # Read existing .env file if it exists if os.path.exists(env_file_path): - with open(env_file_path, "r") as f: + with open(env_file_path) as f: lines = f.readlines() - + # Update or add the key for i, line in enumerate(lines): if line.strip().startswith(f"{key_to_update}="): lines[i] = f"{key_to_update}={repo_id}\n" key_updated = True break - + # If key wasn't found, add it if not key_updated: - if lines and not lines[-1].endswith('\n'): - lines.append('\n') + if lines and not lines[-1].endswith("\n"): + lines.append("\n") lines.append(f"{key_to_update}={repo_id}\n") - + # Write back to file with open(env_file_path, "w") as f: f.writelines(lines) - + return True - + except Exception: - return False \ No newline at end of file + return False diff --git a/src/codegen/cli/mcp/api_client.py b/src/codegen/cli/mcp/api_client.py index 8894dad18..08297bf9c 100644 --- a/src/codegen/cli/mcp/api_client.py +++ b/src/codegen/cli/mcp/api_client.py @@ -33,6 +33,7 @@ def get_api_client(): # Set base URL from environment or use the CLI endpoint for consistency # Prefer explicit env override; else match API_ENDPOINT used by CLI commands from codegen.cli.api.endpoints import API_ENDPOINT + base_url = os.getenv("CODEGEN_API_BASE_URL", API_ENDPOINT.rstrip("/")) configuration.host = base_url diff --git a/src/codegen/cli/mcp/tools/executor.py b/src/codegen/cli/mcp/tools/executor.py index e9113e8e2..3896d00dd 100644 --- a/src/codegen/cli/mcp/tools/executor.py +++ b/src/codegen/cli/mcp/tools/executor.py @@ -1,8 +1,3 @@ -import json -import requests - -from codegen.cli.api.endpoints import API_ENDPOINT - import requests from codegen.cli.api.endpoints import API_ENDPOINT diff --git a/src/codegen/cli/telemetry/consent.py b/src/codegen/cli/telemetry/consent.py index f6bf6b6d9..7f1277fca 100644 --- a/src/codegen/cli/telemetry/consent.py +++ b/src/codegen/cli/telemetry/consent.py @@ -1,8 +1,5 @@ """Telemetry consent management for the CLI.""" -import uuid -from pathlib import Path - import rich import typer diff --git a/src/codegen/cli/telemetry/debug_exporter.py b/src/codegen/cli/telemetry/debug_exporter.py index ec0e46742..e8f950273 100644 --- a/src/codegen/cli/telemetry/debug_exporter.py +++ b/src/codegen/cli/telemetry/debug_exporter.py @@ -7,7 +7,7 @@ import json import os from collections.abc import Sequence -from datetime import datetime +from datetime import datetime, timezone from pathlib import Path from opentelemetry.sdk.trace import ReadableSpan @@ -33,7 +33,7 @@ def __init__(self, output_dir: Path | None = None): self.output_dir.mkdir(parents=True, exist_ok=True) # Create a session file for this CLI run - self.session_id = datetime.now().strftime("%Y%m%d_%H%M%S") + self.session_id = datetime.now(tz=timezone.utc).strftime("%Y%m%d_%H%M%S") self.session_file = self.output_dir / f"session_{self.session_id}.jsonl" # Write session header @@ -42,7 +42,7 @@ def __init__(self, output_dir: Path | None = None): json.dumps( { "type": "session_start", - "timestamp": datetime.now().isoformat(), + "timestamp": datetime.now(tz=timezone.utc).isoformat(), "pid": os.getpid(), } ) @@ -110,7 +110,7 @@ def shutdown(self) -> None: json.dumps( { "type": "session_end", - "timestamp": datetime.now().isoformat(), + "timestamp": datetime.now(tz=timezone.utc).isoformat(), } ) + "\n" diff --git a/src/codegen/cli/telemetry/exception_logger.py b/src/codegen/cli/telemetry/exception_logger.py index 72ecc2a15..1754adc4d 100644 --- a/src/codegen/cli/telemetry/exception_logger.py +++ b/src/codegen/cli/telemetry/exception_logger.py @@ -8,9 +8,9 @@ import traceback from typing import Any -from codegen.shared.logging.get_logger import get_logger -from codegen.cli.telemetry.otel_setup import get_session_uuid, get_otel_logging_handler from codegen.cli.telemetry.consent import ensure_telemetry_consent +from codegen.cli.telemetry.otel_setup import get_otel_logging_handler, get_session_uuid +from codegen.shared.logging.get_logger import get_logger # Initialize logger for exception handling logger = get_logger(__name__) diff --git a/src/codegen/cli/tui/agent_detail.py b/src/codegen/cli/tui/agent_detail.py index d1ddd931b..a94a5a0d6 100644 --- a/src/codegen/cli/tui/agent_detail.py +++ b/src/codegen/cli/tui/agent_detail.py @@ -3,18 +3,18 @@ import asyncio import json from pathlib import Path -from typing import Any, Dict +from typing import Any import requests from textual.app import ComposeResult from textual.binding import Binding -from textual.containers import Container, Vertical, Horizontal +from textual.containers import Horizontal, Vertical from textual.screen import Screen -from textual.widgets import Button, Footer, Header, Static, DataTable +from textual.widgets import Button, DataTable, Footer, Header, Static +from codegen.cli.api.endpoints import API_ENDPOINT from codegen.cli.auth.token_manager import get_current_token from codegen.cli.utils.org import resolve_org_id -from codegen.cli.api.endpoints import API_ENDPOINT from codegen.git.repo_operator.local_git_repo import LocalGitRepo @@ -29,39 +29,39 @@ class AgentDetailTUI(Screen): Binding("w", "open_web", "Open Web", show=True), ] - def __init__(self, agent_run: Dict[str, Any], org_id: int | None = None): + def __init__(self, agent_run: dict[str, Any], org_id: int | None = None): super().__init__() self.agent_run = agent_run self.org_id = org_id or resolve_org_id() - self.agent_data: Dict[str, Any] | None = None + self.agent_data: dict[str, Any] | None = None self.is_loading = False def compose(self) -> ComposeResult: """Create child widgets for the agent detail screen.""" run_id = self.agent_run.get("id", "Unknown") summary = self.agent_run.get("summary", "No summary available") - + yield Header() - + with Vertical(): yield Static(f"๐Ÿค– Agent Run Details - ID: {run_id}", classes="title", id="detail-title") yield Static("Use J for JSON, P to pull branch, W for web, Q/Esc to go back", classes="help") - + # Basic info section info_table = DataTable(id="info-table", cursor_type="none") info_table.add_columns("Property", "Value") yield info_table - + # Actions section with Horizontal(id="actions-section"): yield Button("๐Ÿ“„ View JSON", id="json-btn", variant="primary") yield Button("๐Ÿ”€ Pull Branch", id="pull-btn", variant="default") yield Button("๐ŸŒ Open Web", id="web-btn", variant="default") yield Button("โฌ…๏ธ Back", id="back-btn", variant="default") - + # Status/loading area yield Static("", id="status-text") - + yield Footer() def on_mount(self) -> None: @@ -74,7 +74,7 @@ def on_mount(self) -> None: def _populate_basic_info(self) -> None: """Populate the info table with basic agent run information.""" info_table = self.query_one("#info-table", DataTable) - + # Basic info from the agent run data run_id = self.agent_run.get("id", "Unknown") status = self.agent_run.get("status", "Unknown") @@ -106,7 +106,7 @@ async def _load_detailed_data(self) -> None: """Load detailed agent run data from the API.""" if self.is_loading: return - + self.is_loading = True status_text = self.query_one("#status-text", Static) status_text.update("๐Ÿ”„ Loading detailed agent data...") @@ -124,15 +124,15 @@ async def _load_detailed_data(self) -> None: headers = {"Authorization": f"Bearer {token}"} url = f"{API_ENDPOINT.rstrip('/')}/v1/organizations/{self.org_id}/agent/run/{run_id}" - + response = requests.get(url, headers=headers) response.raise_for_status() self.agent_data = response.json() - + # Update info table with additional details self._update_info_with_detailed_data() status_text.update("โœ… Agent data loaded successfully") - + except requests.HTTPError as e: if e.response.status_code == 404: status_text.update(f"โŒ Agent run {run_id} not found") @@ -149,9 +149,9 @@ def _update_info_with_detailed_data(self) -> None: """Update the info table with detailed data from the API.""" if not self.agent_data: return - + info_table = self.query_one("#info-table", DataTable) - + # Check for GitHub PRs github_prs = self.agent_data.get("github_pull_requests", []) if github_prs: @@ -163,9 +163,9 @@ def _update_info_with_detailed_data(self) -> None: pr_info += f"\n โ€ข ... and {len(github_prs) - 3} more" else: pr_info = "No PRs available" - + info_table.add_row("PR Branches", pr_info) - + # Add model info if available model = self.agent_data.get("model", "Unknown") info_table.add_row("Model", model) @@ -180,7 +180,7 @@ def action_view_json(self) -> None: if not self.agent_data: self.notify("โŒ Detailed data not loaded yet", severity="error") return - + # Create a JSON viewer screen json_screen = JSONViewerTUI(self.agent_data) self.app.push_screen(json_screen) @@ -190,7 +190,7 @@ def action_pull_branch(self) -> None: if not self.agent_data: self.notify("โŒ Detailed data not loaded yet", severity="error") return - + # Check if we're in a git repository try: current_repo = LocalGitRepo(Path.cwd()) @@ -224,10 +224,10 @@ async def _pull_branch_async(self, branch_name: str, repo_clone_url: str) -> Non """Asynchronously pull the PR branch.""" status_text = self.query_one("#status-text", Static) status_text.update(f"๐Ÿ”„ Pulling branch {branch_name}...") - + try: current_repo = LocalGitRepo(Path.cwd()) - + # Add remote if it doesn't exist remote_name = "codegen-pr" try: @@ -235,14 +235,14 @@ async def _pull_branch_async(self, branch_name: str, repo_clone_url: str) -> Non except Exception: # Remote might already exist pass - + # Fetch and checkout the branch current_repo.fetch_remote(remote_name) current_repo.checkout_branch(f"{remote_name}/{branch_name}", branch_name) - + status_text.update(f"โœ… Successfully checked out branch: {branch_name}") self.notify(f"โœ… Switched to branch: {branch_name}") - + except Exception as e: error_msg = f"โŒ Failed to pull branch: {e}" status_text.update(error_msg) @@ -257,6 +257,7 @@ def action_open_web(self) -> None: try: import webbrowser + webbrowser.open(web_url) self.notify(f"๐ŸŒ Opened {web_url}") except Exception as e: @@ -283,27 +284,27 @@ class JSONViewerTUI(Screen): Binding("escape,q", "back", "Back", show=True), ] - def __init__(self, data: Dict[str, Any]): + def __init__(self, data: dict[str, Any]): super().__init__() self.data = data def compose(self) -> ComposeResult: """Create child widgets for the JSON viewer.""" yield Header() - + with Vertical(): yield Static("๐Ÿ“„ Agent Run JSON Data", classes="title") yield Static("Use Q/Esc to go back", classes="help") - + # Format JSON with pretty printing try: json_text = json.dumps(self.data, indent=2, sort_keys=True) yield Static(json_text, id="json-content") except Exception as e: yield Static(f"Error formatting JSON: {e}", id="json-content") - + yield Footer() def action_back(self) -> None: """Go back to the agent detail screen.""" - self.app.pop_screen() \ No newline at end of file + self.app.pop_screen() diff --git a/src/codegen/cli/tui/app.py b/src/codegen/cli/tui/app.py index b0f6acfc9..7755645b4 100644 --- a/src/codegen/cli/tui/app.py +++ b/src/codegen/cli/tui/app.py @@ -601,14 +601,14 @@ def _pull_agent_branch(self, agent_id: str): ) print("\nโœ… Pull completed successfully!") else: - logger.error( + logger.exception( "Local pull failed via typer exit", extra={"operation": "tui.pull_branch", "agent_id": agent_id, "org_id": self.org_id, "duration_ms": duration_ms, "exit_code": e.exit_code, "success": False}, ) print(f"\nโŒ Pull failed (exit code: {e.exit_code})") except ValueError: duration_ms = (time.time() - start_time) * 1000 - logger.error( + logger.exception( "Invalid agent ID for pull", extra={"operation": "tui.pull_branch", "agent_id": agent_id, "org_id": getattr(self, "org_id", None), "duration_ms": duration_ms, "error_type": "invalid_agent_id"}, ) @@ -885,7 +885,7 @@ def _handle_dashboard_tab_keypress(self, key: str): webbrowser.open(me_url) # Debug details not needed for successful browser opens except Exception as e: - logger.error("Failed to open kanban in browser", extra={"operation": "tui.open_kanban", "error": str(e)}) + logger.exception("Failed to open kanban in browser", extra={"operation": "tui.open_kanban", "error": str(e)}) print(f"\nโŒ Failed to open browser: {e}") input("Press Enter to continue...") @@ -982,7 +982,7 @@ def _execute_inline_action(self): # Debug details not needed for successful browser opens # No pause - seamless flow back to collapsed state except Exception as e: - logger.error("Failed to open PR in browser", extra={"operation": "tui.open_pr", "agent_id": agent_id, "error": str(e)}) + logger.exception("Failed to open PR in browser", extra={"operation": "tui.open_pr", "agent_id": agent_id, "error": str(e)}) print(f"\nโŒ Failed to open PR: {e}") input("Press Enter to continue...") # Only pause on errors elif selected_option == "pull locally": @@ -995,7 +995,7 @@ def _execute_inline_action(self): # Debug details not needed for successful browser opens # No pause - let it flow back naturally to collapsed state except Exception as e: - logger.error("Failed to open trace in browser", extra={"operation": "tui.open_trace", "agent_id": agent_id, "error": str(e)}) + logger.exception("Failed to open trace in browser", extra={"operation": "tui.open_trace", "agent_id": agent_id, "error": str(e)}) print(f"\nโŒ Failed to open browser: {e}") input("Press Enter to continue...") # Only pause on errors diff --git a/src/codegen/cli/tui/codegen_theme.tcss b/src/codegen/cli/tui/codegen_theme.tcss index cfddfd602..4ed08ac09 100644 --- a/src/codegen/cli/tui/codegen_theme.tcss +++ b/src/codegen/cli/tui/codegen_theme.tcss @@ -1,9 +1,9 @@ /* Codegen Custom Theme - Indigo, Black, White, Teal */ -/* +/* Color Palette: - Indigo: #4f46e5 (primary), #6366f1 (light), #3730a3 (dark) -- Black/Charcoal: #000000, #1a1a1a +- Black/Charcoal: #000000, #1a1a1a - White: #ffffff, #f8fafc - Teal: #14b8a6 (accent), #2dd4bf (light), #0f766e (dark) - Grays: #111827, #1f2937, #374151, #4b5563, #9ca3af, #d1d5db, #e5e7eb, #f3f4f6 @@ -182,4 +182,4 @@ Container { .status-pending { color: #9ca3af; -} \ No newline at end of file +} diff --git a/src/codegen/cli/utils/inplace_print.py b/src/codegen/cli/utils/inplace_print.py index 22a9c4228..adfc8dfb9 100644 --- a/src/codegen/cli/utils/inplace_print.py +++ b/src/codegen/cli/utils/inplace_print.py @@ -1,5 +1,5 @@ import sys -from typing import Iterable +from collections.abc import Iterable def inplace_print(lines: Iterable[str], prev_lines_rendered: int) -> int: diff --git a/src/codegen/cli/utils/org.py b/src/codegen/cli/utils/org.py index 4dab35a9c..9a8f1be3f 100644 --- a/src/codegen/cli/utils/org.py +++ b/src/codegen/cli/utils/org.py @@ -10,7 +10,6 @@ get_cached_organizations, get_current_org_id, get_current_token, - get_org_name_from_cache, is_org_id_cached, ) from codegen.cli.commands.claude.quiet_console import console @@ -38,7 +37,7 @@ def _validate_org_id_with_cache(org_id: int, source: str) -> int | None: """Validate an org ID against the cache and show helpful errors.""" if is_org_id_cached(org_id): return org_id - + # If we have a cache but the org ID is not in it, show helpful error cached_orgs = get_cached_organizations() if cached_orgs: @@ -46,7 +45,7 @@ def _validate_org_id_with_cache(org_id: int, source: str) -> int | None: console.print(f"[red]Error:[/red] Organization ID {org_id} from {source} not found in your accessible organizations.") console.print(f"[yellow]Available organizations:[/yellow] {org_list}") return None - + # If no cache available, trust the org ID (will be validated by API) return org_id diff --git a/src/codegen/cli/utils/repo.py b/src/codegen/cli/utils/repo.py index 5e0149af0..f254f0de6 100644 --- a/src/codegen/cli/utils/repo.py +++ b/src/codegen/cli/utils/repo.py @@ -1,7 +1,7 @@ """Repository utilities for managing repository ID resolution and environment variables.""" import os -from typing import Dict, List, Any +from typing import Any from rich.console import Console @@ -10,12 +10,12 @@ def resolve_repo_id(explicit_repo_id: int | None = None) -> int | None: """Resolve repository ID with fallback strategy. - + Order of precedence: 1) explicit_repo_id passed by the caller 2) CODEGEN_REPO_ID environment variable 3) REPOSITORY_ID environment variable - + Returns None if not found. """ if explicit_repo_id is not None: @@ -47,7 +47,7 @@ def get_current_repo_id() -> int | None: return resolve_repo_id() -def get_repo_env_status() -> Dict[str, str]: +def get_repo_env_status() -> dict[str, str]: """Get the status of repository-related environment variables.""" return { "CODEGEN_REPO_ID": os.environ.get("CODEGEN_REPO_ID", "Not set"), @@ -57,11 +57,11 @@ def get_repo_env_status() -> Dict[str, str]: def set_repo_env_variable(repo_id: int, var_name: str = "CODEGEN_REPO_ID") -> bool: """Set repository ID in environment variable. - + Args: repo_id: Repository ID to set var_name: Environment variable name (default: CODEGEN_REPO_ID) - + Returns: True if successful, False otherwise """ @@ -87,107 +87,96 @@ def update_env_file_with_repo(repo_id: int, env_file_path: str = ".env") -> bool lines = [] key_updated = False key_to_update = "CODEGEN_REPO_ID" - + # Read existing .env file if it exists if os.path.exists(env_file_path): - with open(env_file_path, "r") as f: + with open(env_file_path) as f: lines = f.readlines() - + # Update or add the key for i, line in enumerate(lines): if line.strip().startswith(f"{key_to_update}="): lines[i] = f"{key_to_update}={repo_id}\n" key_updated = True break - + # If key wasn't found, add it if not key_updated: - if lines and not lines[-1].endswith('\n'): - lines.append('\n') + if lines and not lines[-1].endswith("\n"): + lines.append("\n") lines.append(f"{key_to_update}={repo_id}\n") - + # Write back to file with open(env_file_path, "w") as f: f.writelines(lines) - + return True - + except Exception as e: console.print(f"[red]Error updating .env file:[/red] {e}") return False -def get_repo_display_info() -> List[Dict[str, str]]: +def get_repo_display_info() -> list[dict[str, str]]: """Get repository information for display in TUI.""" repo_id = get_current_repo_id() env_status = get_repo_env_status() - + info = [] - + # Current repository ID if repo_id: - info.append({ - "label": "Current Repository ID", - "value": str(repo_id), - "status": "active" - }) + info.append({"label": "Current Repository ID", "value": str(repo_id), "status": "active"}) else: - info.append({ - "label": "Current Repository ID", - "value": "Not configured", - "status": "inactive" - }) - + info.append({"label": "Current Repository ID", "value": "Not configured", "status": "inactive"}) + # Environment variables status for var_name, value in env_status.items(): - info.append({ - "label": f"{var_name}", - "value": value, - "status": "active" if value != "Not set" else "inactive" - }) - + info.append({"label": f"{var_name}", "value": value, "status": "active" if value != "Not set" else "inactive"}) + return info -def fetch_repositories_for_org(org_id: int) -> List[Dict[str, Any]]: +def fetch_repositories_for_org(org_id: int) -> list[dict[str, Any]]: """Fetch repositories for an organization. - + Args: org_id: Organization ID to fetch repositories for - + Returns: List of repository dictionaries """ try: import requests + from codegen.cli.api.endpoints import API_ENDPOINT from codegen.cli.auth.token_manager import get_current_token - + token = get_current_token() if not token: return [] - + headers = {"Authorization": f"Bearer {token}"} - + # Try the repository endpoint (may not exist yet) url = f"{API_ENDPOINT.rstrip('/')}/v1/organizations/{org_id}/repositories" response = requests.get(url, headers=headers) - + if response.status_code == 200: data = response.json() return data.get("items", []) else: # API endpoint doesn't exist yet, return mock data for demo return get_mock_repositories() - + except Exception: # If API fails, return mock data return get_mock_repositories() -def get_mock_repositories() -> List[Dict[str, Any]]: +def get_mock_repositories() -> list[dict[str, Any]]: """Get mock repository data for demonstration. - + Returns: List of mock repository dictionaries """ @@ -203,34 +192,34 @@ def get_mock_repositories() -> List[Dict[str, Any]]: ] -def ensure_repositories_cached(org_id: int | None = None) -> List[Dict[str, Any]]: +def ensure_repositories_cached(org_id: int | None = None) -> list[dict[str, Any]]: """Ensure repositories are cached for the given organization. - + Args: org_id: Organization ID (will resolve if not provided) - + Returns: List of cached repositories """ - from codegen.cli.auth.token_manager import get_cached_repositories, cache_repositories + from codegen.cli.auth.token_manager import cache_repositories, get_cached_repositories from codegen.cli.utils.org import resolve_org_id - + # Get cached repositories first cached_repos = get_cached_repositories() if cached_repos: return cached_repos - + # If no cache, try to fetch from API if org_id is None: org_id = resolve_org_id() - + if org_id: repositories = fetch_repositories_for_org(org_id) if repositories: cache_repositories(repositories) return repositories - + # Fallback to mock data mock_repos = get_mock_repositories() cache_repositories(mock_repos) - return mock_repos \ No newline at end of file + return mock_repos diff --git a/src/codegen/git/repo_operator/local_git_repo.py b/src/codegen/git/repo_operator/local_git_repo.py index a5c4acea3..21e5dc506 100644 --- a/src/codegen/git/repo_operator/local_git_repo.py +++ b/src/codegen/git/repo_operator/local_git_repo.py @@ -82,3 +82,30 @@ def get_language(self, access_token: str | None = None) -> str: def has_remote(self) -> bool: return bool(self.git_cli.remotes) + + def add_remote(self, name: str, url: str) -> None: + """Add a new remote to the repository. + + Args: + name: The name of the remote + url: The URL of the remote + """ + self.git_cli.create_remote(name, url) + + def fetch_remote(self, remote_name: str) -> None: + """Fetch from a remote. + + Args: + remote_name: The name of the remote to fetch from + """ + remote = self.git_cli.remote(remote_name) + remote.fetch() + + def checkout_branch(self, ref: str, branch_name: str) -> None: + """Checkout a branch, creating it if it doesn't exist. + + Args: + ref: The reference to checkout from (e.g., 'origin/main') + branch_name: The name of the branch to create or checkout + """ + self.git_cli.git.checkout(ref, b=branch_name) diff --git a/src/codegen/shared/logging/get_logger.py b/src/codegen/shared/logging/get_logger.py index 677363bdf..77d03d350 100644 --- a/src/codegen/shared/logging/get_logger.py +++ b/src/codegen/shared/logging/get_logger.py @@ -63,8 +63,8 @@ def _get_telemetry_config(): try: # Use non-prompting config loader to avoid consent prompts during logging setup - from codegen.configs.models.telemetry import TelemetryConfig from codegen.configs.constants import GLOBAL_ENV_FILE + from codegen.configs.models.telemetry import TelemetryConfig _telemetry_config = TelemetryConfig(env_filepath=GLOBAL_ENV_FILE) except ImportError: diff --git a/tests/unit/codegen/test_imports.py b/tests/unit/codegen/test_imports.py new file mode 100644 index 000000000..2f60278b7 --- /dev/null +++ b/tests/unit/codegen/test_imports.py @@ -0,0 +1,16 @@ +"""Tests for top-level imports in the codegen package.""" + + +class TestTopLevelImports: + """Test that we can properly import classes from the top level.""" + + def test_can_import_agent_from_top_level(self): + """Test that we can import Agent directly from codegen.""" + from codegen import Agent + + assert Agent is not None + + # Verify it's the same class as the one in codegen.agents.agent + from codegen.agents.agent import Agent as AgentFromModule + + assert Agent is AgentFromModule