Skip to content
Closed
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
6 changes: 0 additions & 6 deletions src/codegen/cli/api/client.py
Original file line number Diff line number Diff line change
Expand Up @@ -87,9 +87,3 @@ def _make_request(
except requests.RequestException as e:
msg = f"Network error: {e!s}"
raise ServerError(msg)

def identify(self) -> Identity:
"""Get user identity information."""
# TODO: Implement actual API call to identity endpoint
# For now, return a mock identity with active status
return Identity(auth_context=AuthContext(status="active"))
4 changes: 4 additions & 0 deletions src/codegen/cli/api/endpoints.py
Original file line number Diff line number Diff line change
Expand Up @@ -11,3 +11,7 @@
PR_LOOKUP_ENDPOINT = f"https://{MODAL_PREFIX}--cli-pr-lookup.modal.run"
CODEGEN_SYSTEM_PROMPT_URL = "https://gist.githubusercontent.com/jayhack/15681a2ceaccd726f19e6fdb3a44738b/raw/17c08054e3931b3b7fdf424458269c9e607541e8/codegen-system-prompt.txt"
IMPROVE_ENDPOINT = f"https://{MODAL_PREFIX}--cli-improve.modal.run"

# API ENDPOINT
# API_ENDPOINT = "https://codegen-sh-develop-jay--rest-api.modal.run/"
API_ENDPOINT = "https://codegen-sh-staging--rest-api.modal.run/"
2 changes: 1 addition & 1 deletion src/codegen/cli/api/modal.py
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ def get_modal_workspace():
case Environment.STAGING:
return "codegen-sh-staging"
case Environment.DEVELOP:
return "codegen-sh-develop"
return "codegen-sh-develop-jay"
case _:
msg = f"Invalid environment: {global_env.ENV}"
raise ValueError(msg)
Expand Down
11 changes: 1 addition & 10 deletions src/codegen/cli/auth/token_manager.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,9 +2,7 @@
import os
from pathlib import Path

from codegen.cli.api.client import RestAPI
from codegen.cli.auth.constants import AUTH_FILE, CONFIG_DIR
from codegen.cli.errors import AuthError


class TokenManager:
Expand All @@ -22,14 +20,7 @@ def _ensure_config_dir(self):
Path(self.config_dir).mkdir(parents=True, exist_ok=True)

def authenticate_token(self, token: str) -> None:
"""Authenticate the token with the api."""
identity = RestAPI(token).identify()
if not identity:
msg = "No identity found for session"
raise AuthError(msg)
if identity.auth_context.status != "active":
msg = "Current session is not active. API Token may be invalid or may have expired."
raise AuthError(msg)
"""Store the token locally."""
self.save_token(token)

def save_token(self, token: str) -> None:
Expand Down
6 changes: 5 additions & 1 deletion src/codegen/cli/cli.py
Original file line number Diff line number Diff line change
Expand Up @@ -8,11 +8,13 @@

# Import the actual command functions
from codegen.cli.commands.init.main import init
from codegen.cli.commands.integrations.main import integrations_app
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
from codegen.cli.commands.update.main import update

install(show_locals=True)
Expand All @@ -35,10 +37,12 @@ def version_callback(value: bool):
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)

# Config is a group, so add it as a typer
# Add Typer apps as sub-applications
main.add_typer(config_command, name="config")
main.add_typer(integrations_app, name="integrations")


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

import webbrowser

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.utils.url import generate_webapp_url

console = Console()

# Create the integrations app
integrations_app = typer.Typer(help="Manage Codegen integrations")


@integrations_app.command("list")
def list_integrations():
"""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:
console.print("[red]Error:[/red] Not authenticated. Please run 'codegen login' first.")
raise typer.Exit(1)

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

response_data = response.json()

# Extract integrations from the response structure
integrations_data = response_data.get("integrations", [])
organization_name = response_data.get("organization_name", "Unknown")
total_active = response_data.get("total_active_integrations", 0)

if not integrations_data:
console.print("[yellow]No integrations found.[/yellow]")
return

# Create a table to display integrations
table = Table(
title=f"Integrations for {organization_name}",
border_style="blue",
show_header=True,
title_justify="center",
)
table.add_column("Integration", style="cyan", no_wrap=True)
table.add_column("Status", style="white", justify="center")
table.add_column("Type", style="magenta")
table.add_column("Details", style="dim")

# Add integrations to table
for integration in integrations_data:
integration_type = integration.get("integration_type", "Unknown")
active = integration.get("active", False)
token_id = integration.get("token_id")
installation_id = integration.get("installation_id")
metadata = integration.get("metadata", {})

# Status with emoji
status = "✅ Active" if active else "❌ Inactive"

# Determine integration category
if integration_type.endswith("_user"):
category = "User Token"
elif integration_type.endswith("_app"):
category = "App Install"
elif integration_type in ["github", "slack_app", "linear_app"]:
category = "App Install"
else:
category = "Token-based"

# Build details string
details = []
if token_id:
details.append(f"Token ID: {token_id}")
if installation_id:
details.append(f"Install ID: {installation_id}")
if metadata and isinstance(metadata, dict):
for key, value in metadata.items():
if key == "webhook_secret":
details.append(f"{key}: ***secret***")
else:
details.append(f"{key}: {value}")

details_str = ", ".join(details) if details else "No details"
if len(details_str) > 50:
details_str = details_str[:47] + "..."

table.add_row(integration_type.replace("_", " ").title(), status, category, details_str)

console.print(table)
console.print(f"\n[green]Total: {len(integrations_data)} integrations ({total_active} active)[/green]")

except requests.RequestException as e:
console.print(f"[red]Error fetching integrations:[/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)


@integrations_app.command("add")
def add_integration():
"""Open the Codegen integrations page in your browser to add new integrations."""
console.print("🌐 Opening Codegen integrations page...", style="bold blue")

# Generate the web URL using the environment-aware utility
web_url = generate_webapp_url("integrations")

try:
webbrowser.open(web_url)
console.print(f"✅ Opened [link]{web_url}[/link] in your browser", style="green")
console.print("💡 You can add new integrations from the web interface", style="dim")
except Exception as e:
console.print(f"❌ Failed to open browser: {e}", style="red")
console.print(f"🔗 Please manually visit: {web_url}", style="yellow")


# Default callback for the integrations app
@integrations_app.callback(invoke_without_command=True)
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()
1 change: 0 additions & 1 deletion src/codegen/cli/commands/login/main.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,3 @@

import rich
import typer

Expand Down
1 change: 1 addition & 0 deletions src/codegen/cli/commands/mcp/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
"""MCP command module."""
47 changes: 45 additions & 2 deletions src/codegen/cli/commands/mcp/main.py
Original file line number Diff line number Diff line change
@@ -1,12 +1,49 @@
"""MCP server command for the Codegen CLI."""

from typing import Any

import requests
import typer
from rich.console import Console

from codegen.cli.api.endpoints import API_ENDPOINT
from codegen.cli.auth.token_manager import get_current_token

console = Console()


def fetch_tools_for_mcp() -> list[dict[str, Any]]:
"""Fetch available tools from the API for MCP server generation."""
try:
token = get_current_token()
if not token:
console.print("[red]Error:[/red] Not authenticated. Please run 'codegen login' first.")
raise typer.Exit(1)

console.print("🔧 Fetching available tools from API...", style="dim")
headers = {"Authorization": f"Bearer {token}"}
url = f"{API_ENDPOINT.rstrip('/')}/v1/organizations/11/tools"
response = requests.get(url, headers=headers)
response.raise_for_status()

response_data = response.json()

# Extract tools from the response structure
if isinstance(response_data, dict) and "tools" in response_data:
tools = response_data["tools"]
console.print(f"✅ Found {len(tools)} tools", style="green")
return tools

return response_data if isinstance(response_data, list) else []

except requests.RequestException as e:
console.print(f"[red]Error fetching tools:[/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)


def mcp(
host: str = typer.Option("localhost", help="Host to bind the MCP server to"),
port: int | None = typer.Option(None, help="Port to bind the MCP server to (default: stdio transport)"),
Expand All @@ -24,14 +61,20 @@ def mcp(

# Validate transport
if transport not in ["stdio", "http"]:
console.print(f"❌ Invalid transport: {transport}. Must be 'stdio' or 'http'", style="bold red")
console.print(
f"❌ Invalid transport: {transport}. Must be 'stdio' or 'http'",
style="bold red",
)
raise typer.Exit(1)

# Fetch tools from API before starting server
tools = fetch_tools_for_mcp()

# Import here to avoid circular imports and ensure dependencies are available
from codegen.cli.mcp.server import run_server

try:
run_server(transport=transport, host=host, port=port)
run_server(transport=transport, host=host, port=port, available_tools=tools)
except KeyboardInterrupt:
console.print("\n👋 MCP server stopped", style="yellow")
except Exception as e:
Expand Down
1 change: 1 addition & 0 deletions src/codegen/cli/commands/tools/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
"""Tools command module."""
90 changes: 90 additions & 0 deletions src/codegen/cli/commands/tools/main.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,90 @@
"""Tools 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

console = Console()


def tools():
"""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:
console.print("[red]Error:[/red] Not authenticated. Please run 'codegen login' first.")
raise typer.Exit(1)

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

response_data = response.json()

# Extract tools from the response structure
if isinstance(response_data, dict) and "tools" in response_data:
tools_data = response_data["tools"]
total_count = response_data.get("total_count", len(tools_data))
else:
tools_data = response_data
total_count = len(tools_data) if isinstance(tools_data, list) else 1

if not tools_data:
console.print("[yellow]No tools found.[/yellow]")
return

# Handle case where response might be a list of strings vs list of objects
if isinstance(tools_data, list) and len(tools_data) > 0:
# Check if first item is a string or object
if isinstance(tools_data[0], str):
# Simple list of tool names
console.print(f"[green]Found {len(tools_data)} tools:[/green]")
for tool_name in tools_data:
console.print(f" • {tool_name}")
return

# Create a table to display tools (for structured data)
table = Table(
title="Available Tools",
border_style="blue",
show_header=True,
title_justify="center",
)
table.add_column("Tool Name", style="cyan", no_wrap=True)
table.add_column("Description", style="white")
table.add_column("Category", style="magenta")

# Add tools to table
for tool in tools_data:
if isinstance(tool, dict):
tool_name = tool.get("name", "Unknown")
description = tool.get("description", "No description available")
category = tool.get("category", "General")

# Truncate long descriptions
if len(description) > 80:
description = description[:77] + "..."

table.add_row(tool_name, description, category)
else:
# Fallback for non-dict items
table.add_row(str(tool), "Unknown", "General")

console.print(table)
console.print(f"\n[green]Found {total_count} tools available.[/green]")

except requests.RequestException as e:
console.print(f"[red]Error fetching tools:[/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)
Loading