Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion src/codegen/cli/auth/constants.py
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
from pathlib import Path

# Base directories
CONFIG_DIR = Path("~/.config/codegen-sh").expanduser()
CONFIG_DIR = Path("~/.codegen").expanduser()
Copy link
Contributor

Choose a reason for hiding this comment

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

[Logic bug]: CONFIG_DIR path change breaks consistency
Other modules (e.g. src/codegen/configs/constants.py) still reference the old ~/.config/codegen-sh directory, so credentials/config written here will no longer be found elsewhere.

Suggested change
CONFIG_DIR = Path("~/.codegen").expanduser()
# Re-align with the existing global constants – keep the old location or update all usages consistently
CONFIG_DIR = Path("~/.config/codegen-sh").expanduser()

CODEGEN_DIR = Path(".codegen")
PROMPTS_DIR = CODEGEN_DIR / "prompts"

Expand Down
2 changes: 0 additions & 2 deletions src/codegen/cli/auth/login.py
Original file line number Diff line number Diff line change
Expand Up @@ -40,8 +40,6 @@ def login_routine(token: str | None = None) -> str:
token_manager = TokenManager()
token_manager.authenticate_token(token)
rich.print(f"[green]✓ Stored token to:[/green] {token_manager.token_file}")
rich.print("[cyan]📊 Hey![/cyan] We collect anonymous usage data to improve your experience 🔒")
rich.print("To opt out, set [green]telemetry_enabled = false[/green] in [cyan]~/.config/codegen-sh/analytics.json[/cyan] ✨")
return token
Copy link
Contributor

Choose a reason for hiding this comment

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

[Logic bug]: Removed consent message may hide telemetry notice
Deleting the opt-out instructions lines means users are no longer told how to disable telemetry.

Suggested change
return token
rich.print("[cyan]📊 Hey![/cyan] We collect anonymous usage data to improve your experience 🔒")
rich.print("To opt out, set [green]telemetry_enabled = false[/green] in [cyan]~/.config/codegen-sh/analytics.json[/cyan] ✨")

except AuthError as e:
Copy link
Contributor

Choose a reason for hiding this comment

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

[Logic error]: Telemetry opt-out path hard-coded to old config location
If CONFIG_DIR path is changed, this message must also reflect new location.

Suggested change
except AuthError as e:
rich.print("To opt out, set [green]telemetry_enabled = false[/green] in [cyan]~/.codegen/analytics.json[/cyan] ✨")

rich.print(f"[red]Error:[/red] {e!s}")
Expand Down
12 changes: 6 additions & 6 deletions src/codegen/cli/cli.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,16 +3,16 @@

from codegen import __version__

# Import config command (still a Typer app)
from codegen.cli.commands.agents.main import agents_app

# Import the actual command functions
from codegen.cli.commands.claude.main import claude

# Import config command (still a Typer app)
from codegen.cli.commands.config.main import config_command
from codegen.cli.commands.init.main import init
from codegen.cli.commands.integrations.main import integrations_app
Copy link
Contributor

Choose a reason for hiding this comment

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

[Syntax error]: extraneous import removed in diff but still referenced
codegen/cli/cli.py still imports codegen.cli.commands.mcp.main which has been deleted, causing ImportError at runtime.

Suggested change
from codegen.cli.commands.integrations.main import integrations_app
# from codegen.cli.commands.mcp.main import mcp # removed command

from codegen.cli.commands.login.main import login
from codegen.cli.commands.logout.main import logout
from codegen.cli.commands.mcp.main import mcp
from codegen.cli.commands.profile.main import profile
from codegen.cli.commands.style_debug.main import style_debug
from codegen.cli.commands.tools.main import tools
Expand All @@ -29,27 +29,27 @@ def version_callback(value: bool):


# Create the main Typer app
main = typer.Typer(name="codegen", help="Codegen CLI - Transform your code with AI.", rich_markup_mode="rich")
main = typer.Typer(name="codegen", help="Codegen - the Operating System for Code Agents.", rich_markup_mode="rich")

# Add individual commands to the main app
main.command("claude", help="Run Claude Code with OpenTelemetry monitoring and logging.")(claude)
Copy link
Contributor

Choose a reason for hiding this comment

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

[Logic error]: Command registration for removed mcp still present
Since mcp function is no longer imported, these lines will raise NameError when CLI module is imported.

Suggested change
main.command("claude", help="Run Claude Code with OpenTelemetry monitoring and logging.")(claude)
# main.command("mcp", help="Start the Codegen MCP server.")(mcp) # command removed

main.command("init", help="Initialize or update the Codegen folder.")(init)
main.command("login", help="Store authentication token.")(login)
main.command("logout", help="Clear stored authentication token.")(logout)
main.command("mcp", help="Start the Codegen MCP server.")(mcp)
main.command("profile", help="Display information about the currently authenticated user.")(profile)
main.command("style-debug", help="Debug command to visualize CLI styling (spinners, etc).")(style_debug)
main.command("tools", help="List available tools from the Codegen API.")(tools)
main.command("update", help="Update Codegen to the latest or specified version")(update)

# Add Typer apps as sub-applications
main.add_typer(agents_app, name="agents")
main.add_typer(config_command, name="config")
main.add_typer(integrations_app, name="integrations")


@main.callback()
def main_callback(version: bool = typer.Option(False, "--version", callback=version_callback, is_eager=True, help="Show version and exit")):
"""Codegen CLI - Transform your code with AI."""
"""Codegen - the Operating System for Code Agents"""
pass


Expand Down
1 change: 1 addition & 0 deletions src/codegen/cli/commands/agents/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
"""Agents command module."""
127 changes: 127 additions & 0 deletions src/codegen/cli/commands/agents/main.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,127 @@
"""Agents command for the Codegen CLI."""

import requests
import typer
from rich.console import Console
from rich.table import Table

from codegen.cli.api.endpoints import API_ENDPOINT
from codegen.cli.auth.token_manager import get_current_token
from codegen.cli.rich.spinners import create_spinner
from codegen.cli.utils.org import resolve_org_id

console = Console()

# Create the agents app
agents_app = typer.Typer(help="Manage Codegen agents")


@agents_app.command("list")
def list_agents(org_id: int | None = typer.Option(None, help="Organization ID (defaults to CODEGEN_ORG_ID/REPOSITORY_ORG_ID or auto-detect)")):
"""List agent runs from the Codegen API."""
# Get the current token
token = get_current_token()
if not token:
console.print("[red]Error:[/red] Not authenticated. Please run 'codegen login' first.")
raise typer.Exit(1)

try:
# Resolve org id
resolved_org_id = resolve_org_id(org_id)
if resolved_org_id is None:
console.print("[red]Error:[/red] Organization ID not provided. Pass --org-id, set CODEGEN_ORG_ID, or REPOSITORY_ORG_ID.")
raise typer.Exit(1)

# Make API request to list agent runs with spinner
spinner = create_spinner("Fetching agent runs...")
spinner.start()

try:
headers = {"Authorization": f"Bearer {token}"}
url = f"{API_ENDPOINT.rstrip('/')}/v1/organizations/{resolved_org_id}/agent/runs"
response = requests.get(url, headers=headers)
response.raise_for_status()
response_data = response.json()
finally:
spinner.stop()

# Extract agent runs from the response structure
agent_runs = response_data.get("items", [])
total = response_data.get("total", 0)
page = response_data.get("page", 1)
page_size = response_data.get("page_size", 10)

if not agent_runs:
console.print("[yellow]No agent runs found.[/yellow]")
return

# Create a table to display agent runs
table = Table(
title=f"Agent Runs (Page {page}, Total: {total})",
border_style="blue",
show_header=True,
title_justify="center",
)
table.add_column("ID", style="cyan", no_wrap=True)
table.add_column("Status", style="white", justify="center")
table.add_column("Source", style="magenta")
table.add_column("Created", style="dim")
table.add_column("Result", style="green")

# Add agent runs to table
for agent_run in agent_runs:
run_id = str(agent_run.get("id", "Unknown"))
status = agent_run.get("status", "Unknown")
source_type = agent_run.get("source_type", "Unknown")
created_at = agent_run.get("created_at", "Unknown")
result = agent_run.get("result", "")

# Status with emoji
status_display = status
if status == "COMPLETE":
status_display = "✅ Complete"
elif status == "RUNNING":
status_display = "🏃 Running"
elif status == "FAILED":
status_display = "❌ Failed"
elif status == "STOPPED":
status_display = "⏹️ Stopped"
elif status == "PENDING":
status_display = "⏳ Pending"

# Format created date (just show date and time, not full timestamp)
if created_at and created_at != "Unknown":
try:
# Parse and format the timestamp to be more readable
from datetime import datetime

dt = datetime.fromisoformat(created_at.replace("Z", "+00:00"))
created_display = dt.strftime("%m/%d %H:%M")
except (ValueError, TypeError):
created_display = created_at[:16] if len(created_at) > 16 else created_at
else:
created_display = created_at

# Truncate result if too long
result_display = result[:50] + "..." if result and len(result) > 50 else result or "No result"

table.add_row(run_id, status_display, source_type, created_display, result_display)

console.print(table)
console.print(f"\n[green]Showing {len(agent_runs)} of {total} agent runs[/green]")

except requests.RequestException as e:
console.print(f"[red]Error fetching agent runs:[/red] {e}", style="bold red")
raise typer.Exit(1)
except Exception as e:
console.print(f"[red]Unexpected error:[/red] {e}", style="bold red")
raise typer.Exit(1)


# Default callback for the agents app
@agents_app.callback(invoke_without_command=True)
def agents_callback(ctx: typer.Context):
"""Manage Codegen agents."""
if ctx.invoked_subcommand is None:
# If no subcommand is provided, run list by default
list_agents(org_id=None)
27 changes: 15 additions & 12 deletions src/codegen/cli/commands/integrations/main.py
Original file line number Diff line number Diff line change
Expand Up @@ -9,8 +9,9 @@

from codegen.cli.api.endpoints import API_ENDPOINT
from codegen.cli.auth.token_manager import get_current_token
from codegen.cli.utils.url import generate_webapp_url
from codegen.cli.rich.spinners import create_spinner
from codegen.cli.utils.org import resolve_org_id
from codegen.cli.utils.url import generate_webapp_url

console = Console()

Expand All @@ -21,8 +22,6 @@
@integrations_app.command("list")
def list_integrations(org_id: int | None = typer.Option(None, help="Organization ID (defaults to CODEGEN_ORG_ID/REPOSITORY_ORG_ID or auto-detect)")):
"""List organization integrations from the Codegen API."""
console.print("🔌 Fetching organization integrations...", style="bold blue")

# Get the current token
token = get_current_token()
if not token:
Expand All @@ -36,13 +35,18 @@ def list_integrations(org_id: int | None = typer.Option(None, help="Organization
console.print("[red]Error:[/red] Organization ID not provided. Pass --org-id, set CODEGEN_ORG_ID, or REPOSITORY_ORG_ID.")
raise typer.Exit(1)

# Make API request to list integrations
headers = {"Authorization": f"Bearer {token}"}
url = f"{API_ENDPOINT.rstrip('/')}/v1/organizations/{resolved_org_id}/integrations"
response = requests.get(url, headers=headers)
response.raise_for_status()
# Make API request to list integrations with spinner
spinner = create_spinner("Fetching organization integrations...")
spinner.start()

response_data = response.json()
try:
headers = {"Authorization": f"Bearer {token}"}
url = f"{API_ENDPOINT.rstrip('/')}/v1/organizations/{resolved_org_id}/integrations"
response = requests.get(url, headers=headers)
response.raise_for_status()
response_data = response.json()
finally:
spinner.stop()

# Extract integrations from the response structure
integrations_data = response_data.get("integrations", [])
Expand Down Expand Up @@ -138,6 +142,5 @@ def add_integration():
def integrations_callback(ctx: typer.Context):
"""Manage Codegen integrations."""
if ctx.invoked_subcommand is None:
# If no subcommand is provided, show help
print(ctx.get_help())
raise typer.Exit()
# If no subcommand is provided, run list by default
list_integrations(org_id=None)
3 changes: 1 addition & 2 deletions src/codegen/cli/commands/login/main.py
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,6 @@ def login(token: str | None = typer.Option(None, help="API token for authenticat
"""Store authentication token."""
# Check if already authenticated
if get_current_token():
rich.print("[yellow]Warning:[/yellow] Already authenticated. Use 'codegen logout' to clear the token.")
raise typer.Exit(1)
rich.print("[yellow]Info:[/yellow] You already have a token stored. Proceeding with re-authentication...")

login_routine(token)
1 change: 0 additions & 1 deletion src/codegen/cli/commands/mcp/__init__.py

This file was deleted.

90 changes: 0 additions & 90 deletions src/codegen/cli/commands/mcp/main.py

This file was deleted.

22 changes: 13 additions & 9 deletions src/codegen/cli/commands/tools/main.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,15 +7,14 @@

from codegen.cli.api.endpoints import API_ENDPOINT
from codegen.cli.auth.token_manager import get_current_token
from codegen.cli.rich.spinners import create_spinner
from codegen.cli.utils.org import resolve_org_id

console = Console()


def tools(org_id: int | None = typer.Option(None, help="Organization ID (defaults to CODEGEN_ORG_ID/REPOSITORY_ORG_ID or auto-detect)")):
"""List available tools from the Codegen API."""
console.print("🔧 Fetching available tools...", style="bold blue")

# Get the current token
token = get_current_token()
if not token:
Expand All @@ -29,13 +28,18 @@ def tools(org_id: int | None = typer.Option(None, help="Organization ID (default
console.print("[red]Error:[/red] Organization ID not provided. Pass --org-id, set CODEGEN_ORG_ID, or REPOSITORY_ORG_ID.")
raise typer.Exit(1)

# Make API request to list tools
headers = {"Authorization": f"Bearer {token}"}
url = f"{API_ENDPOINT.rstrip('/')}/v1/organizations/{resolved_org_id}/tools"
response = requests.get(url, headers=headers)
response.raise_for_status()

response_data = response.json()
# Make API request to list tools with spinner
spinner = create_spinner("Fetching available tools...")
spinner.start()

try:
headers = {"Authorization": f"Bearer {token}"}
url = f"{API_ENDPOINT.rstrip('/')}/v1/organizations/{resolved_org_id}/tools"
response = requests.get(url, headers=headers)
response.raise_for_status()
response_data = response.json()
finally:
spinner.stop()

# Extract tools from the response structure
if isinstance(response_data, dict) and "tools" in response_data:
Expand Down
Loading