#!/usr/bin/env python3 """ Webber CLI - Client for the Webber API. Usage: webber-cli --help webber-cli chat [OPTIONS] webber-cli explore QUERY [OPTIONS] """ import asyncio import os import sys from pathlib import Path import typer from rich.live import Live from rich.markdown import Markdown from rich.panel import Panel from webber_cli.client import WebberClient from webber_cli.theme import get_console, get_theme app = typer.Typer( name="webber-cli", help="CLI client for the Webber API", no_args_is_help=True, add_completion=False, ) console = get_console() # Default API URL (can be overridden via env or option) # Development port is 8095, production is 8086 DEFAULT_API_URL = os.environ.get("WEBBER_API_URL", "http://localhost:8095") def version_callback(value: bool) -> None: """Display version and exit.""" if value: from cli import __version__ console.print(f"[title]webber-cli[/] version [success]{__version__}[/]") raise typer.Exit() @app.callback() def main( version: bool = typer.Option( False, "--version", "-v", callback=version_callback, is_eager=True, help="Show version and exit", ), ) -> None: """Webber CLI - Talk to the Webber API.""" pass @app.command() def chat( directory: str = typer.Option( ".", "--directory", "-d", help="Working directory for exploration", ), api_url: str = typer.Option( DEFAULT_API_URL, "--api", "-a", help="Webber API URL", ), agent: str = typer.Option( "explore", "--agent", help="Agent to use", ), stream: bool = typer.Option( True, "--stream/--no-stream", "-s", help="Stream responses in real-time", ), ) -> None: """ Start interactive chat session. Connects to the Webber API backend for agent execution. """ working_dir = str(Path(directory).resolve()) if not Path(working_dir).exists(): console.print(f"[error]Error:[/] Directory not found: {working_dir}") raise typer.Exit(1) try: asyncio.run(_chat_loop(api_url, agent, working_dir, stream)) except KeyboardInterrupt: console.print("\n[dim]Goodbye![/]") async def _chat_loop( api_url: str, agent_type: str, working_dir: str, stream: bool = True ) -> None: """Interactive chat loop.""" theme = get_theme() async with WebberClient(api_url) as client: # Check API health if not await client.health_check(): console.print(f"[error]Error:[/] Cannot connect to Webber API at {api_url}") console.print("[dim]Make sure the server is running: ./wakeup.sh[/]") return # Get agent info agent_info = await client.get_agent(agent_type) if not agent_info: console.print(f"[error]Error:[/] Unknown agent: {agent_type}") agents = await client.list_agents() console.print("[dim]Available agents:[/]") for a in agents: console.print(f" - {a.name}: {a.description}") return # Welcome message console.print() console.print(f"[title]Webber CLI[/] [dim]→ {api_url}[/]") console.print(f"[dim]Working in:[/] [path]{working_dir}[/]") console.print(f"[dim]Agent:[/] {agent_info.name} - {agent_info.description}") mode = "streaming" if stream else "batch" console.print(f"[dim]Mode:[/] {mode}") console.print() console.print("[dim]Type 'exit' to quit, 'clear' to clear screen.[/]") console.print() # Chat loop while True: try: user_input = console.input("[prompt]>[/] ").strip() if not user_input: continue if user_input.lower() in ("exit", "quit", "/exit", "/quit"): console.print("[dim]Goodbye![/]") break if user_input.lower() in ("clear", "/clear"): console.clear() continue if user_input.lower().startswith("cd "): new_dir = user_input[3:].strip() new_path = Path(new_dir).resolve() if new_path.exists() and new_path.is_dir(): working_dir = str(new_path) console.print(f"[info]Changed to:[/] [path]{working_dir}[/]") else: console.print(f"[error]Directory not found:[/] {new_dir}") continue console.print() if stream: # Stream response in real-time full_response = "" try: async for chunk in client.run_agent_stream( agent_type, user_input, working_dir ): sys.stdout.write(chunk) sys.stdout.flush() full_response += chunk console.print() # Newline after streaming except Exception as e: console.print(f"\n[error]Stream error:[/] {e}") else: # Batch mode with spinner with console.status("[info]Thinking...[/]", spinner=theme.spinner): result = await client.run_agent(agent_type, user_input, working_dir) if result.success: console.print(Markdown(result.response)) else: console.print(f"[error]Error:[/] {result.error}") console.print() except KeyboardInterrupt: console.print("\n[dim]Use 'exit' to quit.[/]") except Exception as e: console.print(f"[error]Error:[/] {e}") @app.command() def explore( query: str = typer.Argument(..., help="What to search for"), directory: str = typer.Option( ".", "--directory", "-d", help="Working directory", ), api_url: str = typer.Option( DEFAULT_API_URL, "--api", "-a", help="Webber API URL", ), stream: bool = typer.Option( True, "--stream/--no-stream", "-s", help="Stream responses in real-time", ), ) -> None: """ One-shot codebase exploration. Sends a query to the Webber API and displays the result. """ working_dir = str(Path(directory).resolve()) if not Path(working_dir).exists(): console.print(f"[error]Error:[/] Directory not found: {working_dir}") raise typer.Exit(1) asyncio.run(_explore(api_url, query, working_dir, stream)) async def _explore( api_url: str, query: str, working_dir: str, stream: bool = True ) -> None: """Execute exploration query.""" theme = get_theme() async with WebberClient(api_url) as client: # Check API health if not await client.health_check(): console.print(f"[error]Error:[/] Cannot connect to Webber API at {api_url}") console.print("[dim]Make sure the server is running: ./wakeup.sh[/]") return console.print(f"[dim]Exploring:[/] [path]{working_dir}[/]") console.print(f"[dim]Query:[/] {query}") console.print() if stream: # Stream response in real-time try: async for chunk in client.run_agent_stream("explore", query, working_dir): sys.stdout.write(chunk) sys.stdout.flush() console.print() # Newline after streaming except Exception as e: console.print(f"\n[error]Stream error:[/] {e}") else: # Batch mode with spinner with console.status("[info]Searching...[/]", spinner=theme.spinner): result = await client.run_agent("explore", query, working_dir) if result.success: console.print(Panel( Markdown(result.response), title="[success]Findings[/]", border_style=theme.colors.border_success, )) else: console.print(Panel( f"[error]{result.error}[/]", title="[error]Error[/]", border_style=theme.colors.border_error, )) @app.command() def status( api_url: str = typer.Option( DEFAULT_API_URL, "--api", "-a", help="Webber API URL", ), ) -> None: """Check API status and list available agents.""" asyncio.run(_status(api_url)) async def _status(api_url: str) -> None: """Check API status.""" async with WebberClient(api_url) as client: console.print(f"[dim]API URL:[/] {api_url}") if await client.health_check(): console.print("[success]Status:[/] Connected") agents = await client.list_agents() console.print(f"\n[dim]Available agents ({len(agents)}):[/]") for agent in agents: console.print(f" [info]{agent.name}[/]: {agent.description}") else: console.print("[error]Status:[/] Cannot connect") if __name__ == "__main__": app()