Paracle CLI follows an API-first design pattern with graceful fallback to direct core access.
The CLI acts as a thin client that communicates with the Paracle API server. When the API is unavailable, it falls back to direct core access for offline functionality.
┌─────────────────────────────────────────────────────────────────┐
│ User │
└────────────────────────────┬────────────────────────────────────┘
│
▼
┌─────────────────────────────────────────────────────────────────┐
│ Paracle CLI │
│ ┌─────────────────────────────────────────────────────────────┐│
│ │ Command Layer ││
│ │ paracle agents list | paracle workflow run | paracle sync ││
│ └──────────────────────────┬──────────────────────────────────┘│
│ │ │
│ ┌──────────────────────────▼──────────────────────────────────┐│
│ │ API Client Layer ││
│ │ use_api_or_fallback() function ││
│ └──────────────────────────┬──────────────────────────────────┘│
└─────────────────────────────┼───────────────────────────────────┘
│
┌─────────────────┴─────────────────┐
│ │
▼ ▼
┌───────────────────────┐ ┌───────────────────────┐
│ API Available │ │ API Unavailable │
│ │ │ │
│ ┌─────────────────┐ │ │ ┌─────────────────┐ │
│ │ HTTP Request │ │ │ │ Direct Core │ │
│ │ to API Server │ │ │ │ Function Call │ │
│ └────────┬────────┘ │ │ └────────┬────────┘ │
│ │ │ │ │ │
│ ▼ │ │ ▼ │
│ ┌─────────────────┐ │ │ ┌─────────────────┐ │
│ │ Paracle API │ │ │ │ paracle_core │ │
│ │ (FastAPI) │ │ │ │ paracle_domain │ │
│ └─────────────────┘ │ │ │ paracle_store │ │
└───────────────────────┘ │ └─────────────────┘ │
└───────────────────────┘
All functionality is exposed through REST APIs first:
# CLI command calls API endpoint
def agents_list():
client = get_client()
response = client.agents_list() # HTTP GET /agents
display_agents(response)When API is unavailable, fall back to direct access:
from paracle_cli.api_client import use_api_or_fallback
def list_agents():
return use_api_or_fallback(
api_func=lambda client: client.agents_list(),
fallback_func=direct_list_agents,
)Users get the same experience regardless of API availability:
# Works the same whether API is running or not
paracle agents list
paracle status
paracle syncThe APIClient class provides typed methods for all API endpoints:
from paracle_cli.api_client import APIClient, get_client
# Get client instance
client = get_client() # Defaults to http://localhost:8000
# Custom URL
client = APIClient(base_url="http://api.example.com:8080")
# Check availability
if client.is_available():
response = client.agents_list()class APIClient:
# Health
def health(self) -> dict[str, Any]
def is_available(self) -> bool
# Parac/Governance
def parac_status(self) -> dict[str, Any]
def parac_sync(self, update_git, update_metrics) -> dict[str, Any]
def parac_validate(self) -> dict[str, Any]
def parac_session_start(self) -> dict[str, Any]
def parac_session_end(self, progress, completed, in_progress) -> dict[str, Any]
# Agents
def agents_list(self) -> dict[str, Any]
def agents_get(self, agent_id: str) -> dict[str, Any]
def agents_get_spec(self, agent_id: str) -> dict[str, Any]
# Workflows
def workflow_list(self, limit, offset, status) -> dict[str, Any]
def workflow_get(self, workflow_id: str) -> dict[str, Any]
def workflow_execute(self, workflow_id, inputs, async_execution) -> dict[str, Any]
def workflow_execution_status(self, execution_id: str) -> dict[str, Any]
# IDE Integration
def ide_list(self) -> dict[str, Any]
def ide_init(self, ides, force, copy) -> dict[str, Any]
def ide_sync(self, copy: bool) -> dict[str, Any]
# Approvals (Human-in-the-Loop)
def approvals_list_pending(self) -> dict[str, Any]
def approvals_approve(self, approval_id, approver, reason) -> dict[str, Any]
def approvals_reject(self, approval_id, approver, reason) -> dict[str, Any]
# Kanban Board
def boards_list(self, include_archived) -> dict[str, Any]
def boards_create(self, name, description, columns) -> dict[str, Any]
def tasks_list(self, board_id, status, assigned_to) -> dict[str, Any]
def tasks_move(self, task_id, status, reason) -> dict[str, Any]
# Observability
def metrics_list(self) -> dict[str, Any]
def metrics_export(self, format) -> dict[str, Any]
def traces_list(self, limit) -> dict[str, Any]
def alerts_list(self, severity, active_only) -> dict[str, Any]The main utility for implementing API-first with fallback:
from paracle_cli.api_client import use_api_or_fallback
def get_project_status():
"""Get status via API or direct access."""
return use_api_or_fallback(
api_func=lambda client: client.parac_status(),
fallback_func=get_status_direct,
)
def get_status_direct():
"""Direct access fallback."""
from paracle_core.parac import read_current_state
return read_current_state()- Check API availability - Quick health check
- Try API call - If available, use API
- Handle errors - Catch connection errors, timeouts
- Fall back - Use direct core access
- User notification - Optionally inform user of fallback
def use_api_or_fallback(api_func, fallback_func, *args, **kwargs):
client = get_client()
if client.is_available():
try:
return api_func(client, *args, **kwargs)
except APIError as e:
if e.status_code == 404:
pass # Let fallback handle
else:
console.print(f"[yellow]API error:[/yellow] {e.detail}")
console.print("[dim]Falling back to direct access...[/dim]")
except Exception as e:
console.print(f"[yellow]API unavailable:[/yellow] {e}")
console.print("[dim]Falling back to direct access...[/dim]")
return fallback_func(*args, **kwargs)import click
from rich.console import Console
from paracle_cli.api_client import use_api_or_fallback, get_client
console = Console()
@click.command()
def status():
"""Show project status."""
result = use_api_or_fallback(
api_func=lambda client: client.parac_status(),
fallback_func=get_status_fallback,
)
display_status(result)
def get_status_fallback():
"""Direct access when API unavailable."""
from paracle_core.parac.sync import read_current_state
from pathlib import Path
parac_dir = Path.cwd() / ".parac"
if not parac_dir.exists():
return {"error": "No .parac/ folder found"}
return read_current_state(parac_dir)
def display_status(result):
"""Format and display status."""
if "error" in result:
console.print(f"[red]{result['error']}[/red]")
return
console.print(f"[bold]Phase:[/bold] {result.get('phase', 'Unknown')}")
console.print(f"[bold]Progress:[/bold] {result.get('progress', 0)}%")@click.command()
@click.option("--format", type=click.Choice(["table", "json"]), default="table")
def list_agents(format):
"""List all available agents."""
result = use_api_or_fallback(
api_func=lambda client: client.agents_list(),
fallback_func=list_agents_fallback,
)
if format == "json":
console.print_json(data=result)
else:
display_agents_table(result)
def list_agents_fallback():
"""Scan .parac/agents/specs/ directly."""
from paracle_core.agents import AgentRegistry
registry = AgentRegistry()
return {"agents": registry.list_all()}The API client supports token-based authentication:
client = get_client()
client.set_token("your-api-token")
# All subsequent requests include the token
response = client.agents_list()Headers are automatically set:
def _get_headers(self) -> dict[str, str]:
headers = {"Content-Type": "application/json"}
if self._token:
headers["Authorization"] = f"Bearer {self._token}"
return headersCustom exception for API errors:
class APIError(Exception):
def __init__(self, status_code: int, detail: str):
self.status_code = status_code
self.detail = detaildef _handle_response(self, response: httpx.Response) -> dict[str, Any]:
if response.status_code >= 400:
try:
detail = response.json().get("detail", response.text)
except (ValueError, KeyError):
detail = response.text
raise APIError(response.status_code, detail)
return response.json()try:
result = client.workflow_execute(workflow_id, inputs)
except APIError as e:
if e.status_code == 404:
console.print(f"[red]Workflow not found: {workflow_id}[/red]")
elif e.status_code == 400:
console.print(f"[red]Invalid request: {e.detail}[/red]")
else:
console.print(f"[red]API error: {e.detail}[/red]")The CLI includes a serve command to start the API:
# Start with defaults (localhost:8000)
paracle serve
# Custom host and port
paracle serve --host 0.0.0.0 --port 9000
# With auto-reload for development
paracle serve --reload
# Production mode
paracle serve --workers 4@click.command()
@click.option("--host", default="127.0.0.1")
@click.option("--port", default=8000)
@click.option("--reload", is_flag=True)
@click.option("--workers", default=1)
def serve(host, port, reload, workers):
"""Start the Paracle API server."""
import uvicorn
uvicorn.run(
"paracle_api.main:app",
host=host,
port=port,
reload=reload,
workers=workers,
)- CLI handles user interaction and formatting
- API handles business logic and data access
- Core provides domain models and utilities
Same API serves:
- CLI commands
- IDE integrations
- MCP protocol
- Web dashboard
- CI/CD pipelines
Run CLI commands against remote Paracle instances:
export PARACLE_API_URL=https://paracle.company.com
paracle agents listAPI-first makes testing easier:
# Test API directly
def test_agents_list(client):
response = client.get("/agents")
assert response.status_code == 200
# Test CLI with mocked API
def test_cli_agents_list(mocker):
mocker.patch("paracle_cli.api_client.get_client")
result = runner.invoke(cli, ["agents", "list"])
assert result.exit_code == 0Fallback ensures CLI works without API:
# Works even if API server is down
paracle status
paracle agents list
paracle sync# Good - graceful degradation
result = use_api_or_fallback(api_func, fallback_func)
# Avoid - no fallback
result = client.api_call() # Fails if API down# Good - minimal fallback logic
def fallback():
return read_file_directly()
# Avoid - complex fallback
def fallback():
# Don't replicate full API logic here
pass# Good - specific error handling
try:
result = client.workflow_execute(id)
except APIError as e:
if e.status_code == 404:
console.print("[red]Workflow not found[/red]")
raise
# Avoid - generic error handling
try:
result = client.workflow_execute(id)
except Exception:
console.print("[red]Error[/red]")from rich.console import Console
from rich.table import Table
console = Console()
# Good - rich formatting
table = Table(title="Agents")
table.add_column("Name")
table.add_column("Status")
console.print(table)
# Avoid - plain print
print("Agents:")
for agent in agents:
print(f" {agent['name']}")- Architecture Overview - System design
- Synchronization Guide - Async patterns
- CLI Reference - Command reference