Files
webber/webber-cli/webber_cli/main.py
T
jpmschweitzerandClaude Opus 4.5 3b58fa4f8b
Build and Push API / release (push) Successful in 3s
Build and Push API / build (push) Successful in 2m27s
refactor: reorganize into monorepo with separate subprojects
Structure webber into three independent subprojects:
- webber-api/: FastAPI backend server with all agent code
- webber-cli/: Standalone CLI client (renamed from cli/ to webber_cli/)
- webber-sandbox/: Test project for functional testing

Key changes:
- Each subproject has its own .venv (Python 3.12+)
- Added sandbox.sh for managing test project templates
- Created sandbox-templates/ with calculator-cli and empty starter
- Updated CI/CD for prefixed tags (api/v*, cli/v*)
- Added comprehensive AGENTS.md with operational instructions
- Added gitignore filtering to glob and grep tools
- Created pyproject.toml for each subproject

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-01-10 10:37:47 +01:00

262 lines
7.6 KiB
Python

#!/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
from pathlib import Path
import typer
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",
),
) -> 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))
except KeyboardInterrupt:
console.print("\n[dim]Goodbye![/]")
async def _chat_loop(api_url: str, agent_type: str, working_dir: str) -> 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}")
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
# Call the API
with console.status("[info]Thinking...[/]", spinner=theme.spinner):
result = await client.run_agent(agent_type, user_input, working_dir)
console.print()
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",
),
) -> 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))
async def _explore(api_url: str, query: str, working_dir: str) -> 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()
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()