Files
Auto-Trader/agent/cli.py
T
2026-04-01 18:14:49 +08:00

1522 lines
55 KiB
Python

#!/usr/bin/env python3
"""Vibe-Trading CLI — natural-language finance research & backtesting.
Usage:
vibe-trading Interactive mode (default)
vibe-trading -p "Backtest AAPL MACD" Single run
vibe-trading serve --port 8899 Start API server
vibe-trading chat Interactive mode
vibe-trading list List runs
vibe-trading show <run_id> Show run details
"""
from __future__ import annotations
import argparse
import csv
import json
import os
import re
import shutil
import sys
import time
import uuid
from pathlib import Path
from typing import Any, Dict, List, Optional
import warnings
warnings.filterwarnings("ignore", message=".*Importing verbose from langchain.*")
warnings.filterwarnings("ignore", category=DeprecationWarning, module="langchain")
for _s in ("stdout", "stderr"):
_r = getattr(getattr(sys, _s, None), "reconfigure", None)
if callable(_r):
_r(encoding="utf-8", errors="replace")
from rich.console import Console
from rich.panel import Panel
from rich.prompt import Prompt
from rich.syntax import Syntax
from rich.table import Table
console = Console()
AGENT_DIR = Path(__file__).resolve().parent
RUNS_DIR = AGENT_DIR / "runs"
SWARM_DIR = AGENT_DIR / ".swarm" / "runs"
SESSIONS_DIR = AGENT_DIR / "sessions"
UPLOADS_DIR = AGENT_DIR / "uploads"
EXIT_SUCCESS = 0
EXIT_RUN_FAILED = 1
EXIT_USAGE_ERROR = 2
RICH_TAG_PATTERN = re.compile(r"\[/?[^\]]+\]")
_VERSION = "0.1.0"
# Agent color assignments for swarm display
_AGENT_STYLES = ["cyan", "magenta", "green", "yellow", "blue", "bright_red", "bright_cyan", "bright_magenta"]
_agent_color_map: dict[str, str] = {}
def serve_main(argv: list[str] | None = None) -> int:
"""Delegate server startup to api_server."""
from api_server import serve_main as api_serve_main
return api_serve_main(argv)
def _strip_rich_tags(text: str) -> str:
"""Remove Rich markup from plain-text output."""
return RICH_TAG_PATTERN.sub("", text)
def _print_json_result(result: dict) -> None:
"""Print a machine-readable run summary."""
payload = {
"status": result.get("status", "unknown"),
"run_id": result.get("run_id"),
"run_dir": result.get("run_dir"),
"reason": result.get("reason"),
}
print(json.dumps(payload, ensure_ascii=False))
def _result_exit_code(result: dict) -> int:
"""Map run results to stable exit codes."""
return EXIT_SUCCESS if result.get("status") == "success" else EXIT_RUN_FAILED
def _coerce_exit_code(value: Optional[int]) -> int:
"""Normalize command return values to an integer exit code."""
return EXIT_SUCCESS if value is None else int(value)
def _read_prompt_source(
prompt: Optional[str],
prompt_file: Optional[Path],
*,
no_rich: bool,
allow_interactive: bool = True,
) -> tuple[Optional[str], Optional[str]]:
"""Resolve prompt text from CLI args, file, stdin, or interactive input."""
if prompt is not None:
return prompt.strip(), None
if prompt_file is not None:
try:
return prompt_file.read_text(encoding="utf-8").strip(), None
except OSError as exc:
return None, f"Failed to read prompt file: {exc}"
if not sys.stdin.isatty():
return sys.stdin.read().strip(), None
if not allow_interactive:
return None, "A prompt is required."
try:
if no_rich:
return input("Enter strategy request: ").strip(), None
return Prompt.ask("Enter strategy request").strip(), None
except (EOFError, KeyboardInterrupt):
return None, "Prompt input cancelled."
# ---------------------------------------------------------------------------
# Helpers
# ---------------------------------------------------------------------------
def _read_json(path: Path) -> dict:
"""Safely read JSON."""
if not path.exists():
return {}
try:
return json.loads(path.read_text(encoding="utf-8"))
except (json.JSONDecodeError, OSError):
return {}
def _read_metrics(path: Path) -> dict:
"""Read metrics from metrics.csv, return formatted string dict."""
if not path.exists():
return {}
try:
with path.open(encoding="utf-8") as f:
rows = list(csv.DictReader(f))
if not rows:
return {}
out = {}
for k, v in rows[0].items():
if not v:
continue
try:
fv = float(v)
out[k] = f"{fv:.4f}" if abs(fv) < 100 else f"{fv:.0f}"
except ValueError:
out[k] = v
return out
except Exception:
return {}
# ---------------------------------------------------------------------------
# Agent execution core
# ---------------------------------------------------------------------------
def _format_tool_call_args(tool: str, args: Dict[str, str]) -> str:
"""Smart-format tool argument summary."""
if tool == "load_skill":
return f'("{args.get("name", "")}")'
if tool in ("write_file", "read_file", "edit_file"):
return f' {args.get("path", args.get("file_path", ""))}'
if tool in ("bash", "background_run"):
cmd = args.get("command", "")[:80]
return f' [yellow]{cmd}[/yellow]'
if tool == "subagent":
desc = args.get("description", args.get("prompt", "")[:60])
return f' [magenta]{desc}[/magenta]'
if tool == "task_create":
return f' "{args.get("subject", "")}"'
if tool == "task_update":
return f' #{args.get("task_id", "")} -> {args.get("status", "")}'
if tool == "check_background":
tid = args.get("task_id", "")
return f' {tid}' if tid else ""
if tool in ("backtest", "compact", "task_list"):
return ""
for v in args.values():
if v and v != "None":
return f" {v[:60]}"
return ""
def _format_tool_result_preview(tool: str, status: str, preview: str) -> str:
"""Smart-format tool result preview."""
if status != "ok":
return f"[red]{preview[:80]}[/red]"
if tool == "backtest":
sharpe = re.search(r'"sharpe":\s*([\d.eE+-]+)', preview)
ret = re.search(r'"total_return":\s*([\d.eE+-]+)', preview)
parts = []
if sharpe:
parts.append(f"sharpe={sharpe.group(1)}")
if ret:
parts.append(f"return={float(ret.group(1))*100:.1f}%")
return ", ".join(parts) if parts else ""
if tool in ("bash", "background_run"):
if "OK" in preview[:50]:
return "OK"
return preview[:60].replace("\n", " ")
if tool == "subagent":
m = re.search(r'"summary":\s*"(.{0,80})', preview)
return m.group(1).rstrip('"') if m else ""
if tool in ("task_create", "task_update"):
return preview[:60].replace("\n", " ")
if tool in ("read_file", "load_skill", "compact"):
return ""
return ""
def _run_agent(
prompt: str,
history: Optional[List[Dict]] = None,
run_dir_override: Optional[str] = None,
max_iter: int = 50,
*,
no_rich: bool = False,
stream_output: bool = True,
) -> dict:
"""Build AgentLoop and execute, return result dict."""
from src.tools import build_registry
from src.providers.chat import ChatLLM
from src.agent.loop import AgentLoop
def on_event(event_type: str, data: Dict[str, Any]) -> None:
if not stream_output:
return
if no_rich and event_type == "thinking_done":
print()
return
if no_rich and event_type == "tool_call":
tool = data.get("tool", "")
args = data.get("arguments", {})
args_preview = _format_tool_call_args(tool, args)
print(f" - {tool}{_strip_rich_tags(args_preview)}", end="")
return
if no_rich and event_type == "tool_result":
tool = data.get("tool", "")
status = data.get("status", "ok")
elapsed_ms = data.get("elapsed_ms", 0)
elapsed_s = elapsed_ms / 1000
preview = _format_tool_result_preview(tool, status, data.get("preview", ""))
suffix = f" {preview}" if preview else ""
mark = "OK" if status == "ok" else "FAIL"
print(f" {mark} {elapsed_s:.1f}s{_strip_rich_tags(suffix)}")
return
if no_rich and event_type == "compact":
tokens = data.get("tokens_before", "?")
print(f"\n context compressed ({tokens} tokens -> summary)\n")
return
if event_type == "text_delta":
if no_rich:
print(data.get("delta", ""), end="")
else:
console.print(data.get("delta", ""), end="", style="dim")
elif event_type == "thinking_done":
console.print()
elif event_type == "tool_call":
tool = data.get("tool", "")
args = data.get("arguments", {})
args_preview = _format_tool_call_args(tool, args)
console.print(f" [cyan]\u25b6 {tool}[/cyan]{args_preview}", end="")
elif event_type == "tool_result":
tool = data.get("tool", "")
status = data.get("status", "ok")
elapsed_ms = data.get("elapsed_ms", 0)
elapsed_s = elapsed_ms / 1000
ok = status == "ok"
mark = "[green]\u2713[/green]" if ok else "[red]\u2717[/red]"
preview = _format_tool_result_preview(tool, status, data.get("preview", ""))
suffix = f" {preview}" if preview else ""
console.print(f" {mark} [dim]{elapsed_s:.1f}s[/dim]{suffix}")
elif event_type == "compact":
tokens = data.get("tokens_before", "?")
console.print(f"\n [yellow]\u27f3 context compressed[/yellow] [dim]({tokens} tokens \u2192 summary)[/dim]\n")
agent = AgentLoop(
registry=build_registry(),
llm=ChatLLM(),
event_callback=on_event,
max_iterations=max_iter,
)
if run_dir_override:
agent.memory.run_dir = run_dir_override
return agent.run(user_message=prompt, history=history)
def _print_result(result: dict, elapsed: float, *, no_rich: bool = False) -> None:
"""Print execution result panel."""
status = result.get("status", "unknown")
ok = status == "success"
style = "green" if ok else "red"
lines = [f"Status: [bold {style}]{status.upper()}[/bold {style}] Time: {elapsed:.1f}s"]
if result.get("run_id"):
lines.append(f"ID: {result['run_id']}")
review = result.get("review")
if review and review.get("overall_score") is not None:
check = "\u2713" if review.get("passed") else "\u2717"
lines.append(f"Review: {review['overall_score']}pts {check}")
run_dir = result.get("run_dir")
if run_dir:
m = _read_metrics(Path(run_dir) / "artifacts" / "metrics.csv")
parts = [f"{k}={m[k]}" for k in ("total_return", "sharpe", "max_drawdown", "trade_count") if k in m]
if parts:
lines.append(f"Metrics: {', '.join(parts)}")
if result.get("reason"):
lines.append(f"Reason: {result['reason']}")
console.print(Panel("\n".join(lines), border_style=style, title="Result"))
content = result.get("content", "").strip()
if content:
console.print(f"\n{content}")
# ---------------------------------------------------------------------------
# Subcommands
# ---------------------------------------------------------------------------
def cmd_run(prompt: str, max_iter: int, *, json_mode: bool = False, no_rich: bool = False) -> int:
"""Single run."""
if not json_mode:
preview = prompt[:120]
suffix = "..." if len(prompt) > 120 else ""
if no_rich:
print(f"Prompt: {preview}{suffix}\n")
else:
console.print(f"[dim]Prompt:[/dim] {preview}{suffix}\n")
start = time.perf_counter()
try:
result = _run_agent(prompt, max_iter=max_iter, no_rich=no_rich, stream_output=not json_mode)
except KeyboardInterrupt:
if json_mode:
_print_json_result({"status": "cancelled", "run_id": None, "run_dir": None, "reason": "Interrupted"})
return EXIT_RUN_FAILED
if no_rich:
print("\nInterrupted")
return EXIT_RUN_FAILED
console.print("\n[yellow]Interrupted[/yellow]")
return EXIT_RUN_FAILED
if json_mode:
_print_json_result(result)
return _result_exit_code(result)
_print_result(result, time.perf_counter() - start, no_rich=no_rich)
if result.get("run_id"):
tip = f"--show {result['run_id']} | --continue {result['run_id']} \"...\" | --code {result['run_id']}"
if no_rich:
print(tip)
else:
console.print(f"[dim]{tip}[/dim]")
return _result_exit_code(result)
def _build_history_from_trace(run_dir: Path) -> List[Dict[str, str]]:
"""Build conversation history from trace.jsonl."""
from src.agent.trace import TraceWriter
entries = TraceWriter.read(run_dir)
history: List[Dict[str, str]] = []
for e in entries:
if e.get("type") == "start" and e.get("prompt"):
history.append({"role": "user", "content": e["prompt"]})
elif e.get("type") == "answer" and e.get("content"):
history.append({"role": "assistant", "content": e["content"]})
return history
def cmd_continue(
run_id: str,
prompt: str,
max_iter: int,
*,
json_mode: bool = False,
no_rich: bool = False,
) -> int:
"""Continue an existing run."""
run_dir = RUNS_DIR / run_id
if not run_dir.exists():
if no_rich:
print(f"Run {run_id} not found")
return EXIT_USAGE_ERROR
console.print(f"[red]Run {run_id} not found[/red]")
return EXIT_USAGE_ERROR
history = _build_history_from_trace(run_dir)
if not json_mode and no_rich:
print(f"Continue {run_id}: {prompt[:120]}\n")
if json_mode or no_rich:
start = time.perf_counter()
try:
result = _run_agent(
prompt,
history=history,
run_dir_override=str(run_dir),
max_iter=max_iter,
no_rich=no_rich,
stream_output=not json_mode,
)
except KeyboardInterrupt:
if json_mode:
_print_json_result(
{"status": "cancelled", "run_id": run_id, "run_dir": str(run_dir), "reason": "Interrupted"}
)
else:
print("\nInterrupted")
return EXIT_RUN_FAILED
if json_mode:
_print_json_result(result)
return _result_exit_code(result)
_print_result(result, time.perf_counter() - start, no_rich=True)
return _result_exit_code(result)
console.print(f"[dim]Continue {run_id}:[/dim] {prompt[:120]}\n")
start = time.perf_counter()
try:
result = _run_agent(prompt, history=history, run_dir_override=str(run_dir), max_iter=max_iter)
except KeyboardInterrupt:
console.print("\n[yellow]Interrupted[/yellow]")
return EXIT_RUN_FAILED
_print_result(result, time.perf_counter() - start)
return _result_exit_code(result)
# ---------------------------------------------------------------------------
# Interactive mode (Welcome + Slash commands + Swarm streaming)
# ---------------------------------------------------------------------------
def _print_welcome() -> None:
"""Print the welcome screen."""
welcome = (
f"[bold cyan]Vibe-Trading[/bold cyan] [dim]v{_VERSION}[/dim]\n"
"[dim]Vibe trading with your professional financial agent team[/dim]\n"
"\n"
"Type a natural-language request to start, or use [bold]/commands[/bold]:\n"
"\n"
" [cyan]/skills[/cyan] List available skills [cyan]/swarm[/cyan] Agent team presets\n"
" [cyan]/list[/cyan] Recent runs [cyan]/settings[/cyan] Runtime config\n"
" [cyan]/help[/cyan] All commands [cyan]/quit[/cyan] Exit"
)
console.print(Panel(welcome, border_style="cyan", padding=(1, 2)))
def _print_help() -> None:
"""Print all available slash commands."""
table = Table(title="Commands", show_lines=False, border_style="dim")
table.add_column("Command", style="cyan", no_wrap=True)
table.add_column("Description")
cmds = [
("/help", "Show this help"),
("/skills", "List all skills"),
("/list", "List recent runs"),
("/show <run_id>", "Show run details"),
("/code <run_id>", "Show generated code"),
("/trace <run_id>", "Replay run trace"),
("/continue <run_id> <prompt>", "Continue an existing run"),
("/swarm", "List swarm team presets"),
("/swarm run <preset> {vars}", "Run a swarm team"),
("/swarm list", "List swarm run history"),
("/swarm show <run_id>", "Show swarm run details"),
("/swarm cancel <run_id>", "Cancel a swarm run"),
("/sessions", "List chat sessions"),
("/settings", "Show runtime settings"),
("/clear", "Clear screen"),
("/quit", "Exit"),
]
for cmd, desc in cmds:
table.add_row(cmd, desc)
console.print(table)
def _show_settings() -> None:
"""Show current runtime settings."""
table = Table(title="Settings", show_lines=False, border_style="dim")
table.add_column("Key", style="cyan")
table.add_column("Value")
settings = {
"Provider": os.getenv("LANGCHAIN_PROVIDER", "(not set)"),
"Model": os.getenv("LANGCHAIN_MODEL_NAME", "(not set)"),
"Temperature": os.getenv("LANGCHAIN_TEMPERATURE", "0.0"),
"Timeout": os.getenv("TIMEOUT_SECONDS", "2400") + "s",
"Tushare Token": "***" if os.getenv("TUSHARE_TOKEN") else "(not set)",
}
for k, v in settings.items():
table.add_row(k, v)
console.print(table)
def _handle_slash_command(input_str: str, *, max_iter: int) -> None:
"""Parse and route a slash command."""
parts = input_str.split(maxsplit=1)
cmd = parts[0].lower()
arg = parts[1].strip() if len(parts) > 1 else ""
if cmd == "/help":
_print_help()
elif cmd == "/skills":
cmd_skills()
elif cmd == "/list":
cmd_list()
elif cmd == "/show":
if arg:
cmd_show(arg)
else:
console.print("[red]Usage: /show <run_id>[/red]")
elif cmd == "/code":
if arg:
cmd_code(arg)
else:
console.print("[red]Usage: /code <run_id>[/red]")
elif cmd == "/trace":
if arg:
cmd_trace(arg)
else:
console.print("[red]Usage: /trace <run_id>[/red]")
elif cmd == "/continue":
cont_parts = arg.split(maxsplit=1)
if len(cont_parts) >= 2:
cmd_continue(cont_parts[0], cont_parts[1], max_iter)
else:
console.print("[red]Usage: /continue <run_id> <prompt>[/red]")
elif cmd == "/swarm":
_handle_swarm_command(arg)
elif cmd == "/sessions":
cmd_sessions()
elif cmd == "/settings":
_show_settings()
elif cmd == "/clear":
console.clear()
_print_welcome()
elif cmd in ("/quit", "/exit"):
raise EOFError
else:
console.print(f"[red]Unknown command: {cmd}[/red] — type [cyan]/help[/cyan] for available commands")
def _handle_swarm_command(arg: str) -> None:
"""Route swarm sub-commands."""
if not arg:
cmd_swarm_presets()
return
parts = arg.split(maxsplit=1)
sub = parts[0].lower()
sub_arg = parts[1].strip() if len(parts) > 1 else ""
if sub == "run":
run_parts = sub_arg.split(maxsplit=1)
if not run_parts:
console.print("[red]Usage: /swarm run <preset> [vars_json][/red]")
return
preset = run_parts[0]
vars_json = run_parts[1] if len(run_parts) > 1 else None
cmd_swarm_run_live(preset, vars_json)
elif sub == "list":
cmd_swarm_list()
elif sub == "show":
if sub_arg:
cmd_swarm_show(sub_arg)
else:
console.print("[red]Usage: /swarm show <run_id>[/red]")
elif sub == "cancel":
if sub_arg:
cmd_swarm_cancel(sub_arg)
else:
console.print("[red]Usage: /swarm cancel <run_id>[/red]")
else:
console.print(f"[red]Unknown swarm command: {sub}[/red]")
def cmd_interactive(max_iter: int) -> None:
"""Interactive mode with welcome screen, slash commands, and agent conversation."""
_print_welcome()
history: List[Dict[str, str]] = []
while True:
try:
user_input = Prompt.ask("\n[bold]>[/bold]").strip()
except (KeyboardInterrupt, EOFError):
break
if not user_input:
continue
if user_input.lower() in ("q", "quit", "exit"):
break
# Slash commands
if user_input.startswith("/"):
try:
_handle_slash_command(user_input, max_iter=max_iter)
except EOFError:
break
continue
# Natural language -> agent
start = time.perf_counter()
try:
result = _run_agent(user_input, history=history[-6:], max_iter=max_iter)
except KeyboardInterrupt:
console.print("\n[yellow]Interrupted[/yellow]")
continue
_print_result(result, time.perf_counter() - start)
history.append({"role": "user", "content": user_input})
if result.get("content"):
history.append({"role": "assistant", "content": result["content"]})
console.print("[dim]Goodbye[/dim]")
# ---------------------------------------------------------------------------
# Swarm live streaming (Rich Live panel)
# ---------------------------------------------------------------------------
def _get_agent_style(agent_id: str) -> str:
"""Assign a consistent color to each agent."""
if agent_id not in _agent_color_map:
idx = len(_agent_color_map) % len(_AGENT_STYLES)
_agent_color_map[agent_id] = _AGENT_STYLES[idx]
return _agent_color_map[agent_id]
class _SwarmDashboard:
"""Track swarm state and render a Rich Live panel."""
def __init__(self, preset: str, run_id: str) -> None:
self.preset = preset
self.run_id = run_id
self.start_time = time.monotonic()
self.current_layer = 0
self.total_layers = 0
self.agents: Dict[str, Dict[str, Any]] = {}
self.agent_order: List[str] = []
self.completed_summaries: List[tuple[str, str]] = []
self.finished = False
self.final_status = ""
def _ensure_agent(self, agent_id: str) -> str:
"""Register an agent by its ID if not already tracked. Return its key."""
if agent_id in self.agents:
return agent_id
self.agents[agent_id] = {
"name": agent_id, "status": "waiting",
"tool": "\u2014", "elapsed": 0.0, "iters": 0,
"started_at": 0.0, "layer": self.current_layer,
"last_text": "",
}
self.agent_order.append(agent_id)
return agent_id
def handle_event(self, event) -> None:
"""Process a swarm event and update internal state."""
agent_id = event.agent_id or ""
etype = event.type
data = event.data
if etype == "layer_started":
self.current_layer = data.get("layer", 0)
self.total_layers = max(self.total_layers, self.current_layer + 1)
return
if etype == "run_completed":
self.finished = True
self.final_status = data.get("status", "unknown")
return
if not agent_id:
return
key = self._ensure_agent(agent_id)
agent = self.agents[key]
if etype == "task_started":
agent["status"] = "running"
agent["started_at"] = time.monotonic()
elif etype == "tool_call":
agent["tool"] = data.get("tool", "?")
agent["iters"] += 1
elif etype == "tool_result":
agent["elapsed"] = (time.monotonic() - agent["started_at"]) if agent["started_at"] else 0
tool_name = agent["tool"]
status_char = "\u2713" if data.get("status", "ok") == "ok" else "\u2717"
agent["tool"] = f"{tool_name} {status_char}"
elif etype == "task_completed":
agent["status"] = "done"
agent["elapsed"] = (time.monotonic() - agent["started_at"]) if agent["started_at"] else 0
agent["iters"] = data.get("iterations", agent["iters"])
summary = data.get("summary", "")
if summary:
self.completed_summaries.append((agent["name"], summary))
elif etype == "task_failed":
agent["status"] = "failed"
agent["elapsed"] = (time.monotonic() - agent["started_at"]) if agent["started_at"] else 0
error = data.get("error", "")[:80]
self.completed_summaries.append((agent["name"], f"[red]FAILED: {error}[/red]"))
elif etype == "task_retry":
attempt = data.get("attempt", "?")
agent["status"] = "retry"
agent["tool"] = f"retry {attempt}"
elif etype == "worker_text":
content = data.get("content", "").strip()
if content:
# Keep last non-empty line for display
last_line = content.split("\n")[-1].strip()
if last_line:
agent["last_text"] = last_line[:60]
def build_table(self) -> Table:
"""Build the Rich Table for the live panel."""
elapsed_total = time.monotonic() - self.start_time
mins, secs = divmod(int(elapsed_total), 60)
if self.finished:
color = "green" if self.final_status == "completed" else "red"
title_status = f"[{color}]{self.final_status.upper()}[/{color}]"
else:
title_status = f"[cyan]RUNNING[/cyan]"
title = f"{self.preset} {title_status} {mins}:{secs:02d}"
table = Table(
title=title,
border_style="cyan" if not self.finished else ("green" if self.final_status == "completed" else "red"),
show_lines=False,
pad_edge=True,
expand=True,
)
table.add_column("Agent", style="bold", width=20, no_wrap=True)
table.add_column("Status", width=12, justify="center")
table.add_column("Tool", width=14, no_wrap=True)
table.add_column("Time", width=7, justify="right")
table.add_column("Iters", width=5, justify="right")
table.add_column("Output", no_wrap=True, style="dim")
for agent_key in self.agent_order:
agent = self.agents[agent_key]
name = agent["name"]
style = _get_agent_style(name)
styled_name = f"[{style}]{name}[/{style}]"
status = agent["status"]
if status == "running":
status_str = "[\u25b6 running]"
elapsed = time.monotonic() - agent["started_at"] if agent["started_at"] else 0
elif status == "done":
status_str = "[green][\u2713 done ][/green]"
elapsed = agent["elapsed"]
elif status == "failed":
status_str = "[red][\u2717 failed][/red]"
elapsed = agent["elapsed"]
elif status == "retry":
status_str = "[yellow][\u21bb retry ][/yellow]"
elapsed = time.monotonic() - agent["started_at"] if agent["started_at"] else 0
else:
status_str = "[dim][\u25cb waiting][/dim]"
elapsed = 0
time_str = f"{elapsed:.1f}s" if elapsed > 0 else "\u2014"
iter_str = str(agent["iters"]) if agent["iters"] > 0 else "\u2014"
last_text = agent.get("last_text", "")
table.add_row(styled_name, status_str, agent["tool"], time_str, iter_str, last_text)
# Progress bar row
done_count = sum(1 for a in self.agents.values() if a["status"] in ("done", "failed"))
total_count = len(self.agents) or 1
pct = int(done_count / total_count * 100)
bar_width = 40
filled = int(bar_width * pct / 100)
bar = "\u2501" * filled + "[dim]" + "\u2501" * (bar_width - filled) + "[/dim]"
if self.finished:
bar_color = "green" if self.final_status == "completed" else "red"
progress_label = f"[{bar_color}]{self.final_status.upper()}[/{bar_color}]"
else:
progress_label = f"Layer {self.current_layer}"
table.add_section()
table.add_row(
progress_label,
f"{bar}",
f"[bold]{pct}%[/bold]",
f"{mins}:{secs:02d}",
"",
"",
)
return table
def cmd_swarm_run_live(preset: str, vars_json: Optional[str] = None) -> None:
"""Run a swarm preset with Rich Live dashboard."""
from rich.live import Live
from src.swarm.runtime import SwarmRuntime
from src.swarm.store import SwarmStore
from src.swarm.models import RunStatus
user_vars: Dict[str, str] = {}
if vars_json:
try:
user_vars = json.loads(vars_json)
except json.JSONDecodeError as exc:
console.print(f"[red]Invalid JSON: {exc}[/red]")
return
store = SwarmStore(base_dir=SWARM_DIR)
runtime = SwarmRuntime(store=store)
_agent_color_map.clear()
console.print(f"\n[dim]Starting swarm:[/dim] [cyan]{preset}[/cyan]")
if user_vars:
console.print(f"[dim]Variables:[/dim] {json.dumps(user_vars, ensure_ascii=False)}")
dashboard = _SwarmDashboard(preset, "")
try:
run = runtime.start_run(preset, user_vars, live_callback=dashboard.handle_event)
except FileNotFoundError as exc:
console.print(f"[red]{exc}[/red]")
return
except ValueError as exc:
console.print(f"[red]DAG validation failed: {exc}[/red]")
return
dashboard.run_id = run.id
with Live(dashboard.build_table(), console=console, refresh_per_second=4, transient=False) as live:
try:
while True:
time.sleep(0.25)
live.update(dashboard.build_table())
current = store.load_run(run.id)
if current is None:
console.print("[red]Run record lost[/red]")
return
if current.status in (RunStatus.completed, RunStatus.failed, RunStatus.cancelled):
dashboard.finished = True
dashboard.final_status = current.status.value
live.update(dashboard.build_table())
break
except KeyboardInterrupt:
console.print("\n[yellow]Cancelling...[/yellow]")
runtime.cancel_run(run.id)
time.sleep(1)
current = store.load_run(run.id)
if current is None:
return
# Print completed agent summaries
for agent_name, summary in dashboard.completed_summaries:
style = _get_agent_style(agent_name)
console.print(f"\n[{style}]\u2500\u2500 {agent_name} \u2500\u2500[/{style}]")
# Truncate to first meaningful chunk
lines = summary.strip().split("\n")
preview = "\n".join(lines[:8])
if len(lines) > 8:
preview += "\n[dim]...[/dim]"
console.print(preview)
# Final report
status_color = {
RunStatus.completed: "green",
RunStatus.failed: "red",
RunStatus.cancelled: "yellow",
}.get(current.status, "dim")
elapsed_total = time.monotonic() - dashboard.start_time
mins, secs = divmod(int(elapsed_total), 60)
tokens_in = current.total_input_tokens
tokens_out = current.total_output_tokens
token_str = ""
if tokens_in or tokens_out:
token_str = f"\nTokens: ~{tokens_in + tokens_out:,} (in: {tokens_in:,} out: {tokens_out:,})"
if current.final_report:
console.print(f"\n[bold]\u2500\u2500 Final Report \u2500\u2500[/bold]")
console.print(current.final_report[:2000])
console.print(f"\n[{status_color}]{current.status.value.upper()}[/{status_color}] Time: {mins}m {secs}s{token_str}")
# ---------------------------------------------------------------------------
# Legacy subcommands (used by flags and slash commands)
# ---------------------------------------------------------------------------
def cmd_chat(max_iter: int) -> None:
"""Interactive mode (delegates to cmd_interactive)."""
cmd_interactive(max_iter)
def cmd_list(limit: int = 20) -> None:
"""List run history."""
if not RUNS_DIR.exists():
console.print("[dim]No runs yet[/dim]")
return
dirs = sorted([d for d in RUNS_DIR.iterdir() if d.is_dir()], key=lambda d: d.name, reverse=True)[:limit]
if not dirs:
console.print("[dim]No runs yet[/dim]")
return
table = Table(show_lines=False)
table.add_column("Run ID", style="cyan", no_wrap=True)
table.add_column("Status", width=8)
table.add_column("Return", width=10)
table.add_column("Sharpe", width=8)
table.add_column("Prompt", max_width=40)
for d in dirs:
st = _read_json(d / "state.json").get("status", "?")
m = _read_metrics(d / "artifacts" / "metrics.csv")
c = "green" if st == "success" else "red" if st == "failed" else "dim"
table.add_row(d.name, f"[{c}]{st}[/{c}]", m.get("total_return", ""), m.get("sharpe", ""), (_read_json(d / "req.json").get("prompt") or "")[:40])
console.print(table)
def cmd_show(run_id: str) -> None:
"""Show run details."""
run_dir = RUNS_DIR / run_id
if not run_dir.exists():
console.print(f"[red]{run_id} not found[/red]")
return
state = _read_json(run_dir / "state.json")
req = _read_json(run_dir / "req.json")
metrics = _read_metrics(run_dir / "artifacts" / "metrics.csv")
st = state.get("status", "unknown")
c = "green" if st == "success" else "red"
lines = [f"[bold]Status:[/bold] [{c}]{st.upper()}[/{c}]", f"[bold]Prompt:[/bold] {req.get('prompt', '?')}"]
if metrics:
lines.append("\n[bold]Metrics:[/bold]")
lines.extend(f" {k}: {v}" for k, v in metrics.items())
from src.agent.trace import TraceWriter
entries = TraceWriter.read(run_dir)
answers = [e["content"] for e in entries if e.get("type") == "answer" and e.get("content")]
if answers:
summary = answers[-1][:200]
lines.append(f"\n[bold]Answer:[/bold] {summary}{'...' if len(answers[-1]) > 200 else ''}")
if state.get("reason"):
lines.append(f"\n[bold]Reason:[/bold] {state['reason']}")
console.print(Panel("\n".join(lines), border_style=c, title=run_id))
console.print(f"[dim]{run_dir}[/dim]")
def cmd_code(run_id: str) -> None:
"""Show generated code."""
code_dir = RUNS_DIR / run_id / "code"
if not code_dir.exists():
console.print(f"[red]{run_id}/code not found[/red]")
return
for name in ("signal_engine.py",):
path = code_dir / name
if path.exists():
code = path.read_text(encoding="utf-8")
console.print(Syntax(code, "python", theme="monokai", line_numbers=True), width=120)
console.print()
def cmd_skills() -> None:
"""List available skills."""
from src.agent.skills import SkillsLoader
loader = SkillsLoader()
table = Table(title="Skills", show_lines=False)
table.add_column("Name", style="cyan")
table.add_column("Description")
for s in loader.skills:
table.add_row(s.name, s.description)
console.print(table)
def cmd_trace(run_id: str) -> None:
"""Replay trace.jsonl to show full execution."""
from datetime import datetime
from src.agent.trace import TraceWriter
run_dir = RUNS_DIR / run_id
if not run_dir.exists():
console.print(f"[red]{run_id} not found[/red]")
return
entries = TraceWriter.read(run_dir)
if not entries:
console.print(f"[red]{run_id}/trace.jsonl is empty or missing[/red]")
return
console.print(Panel(f"[bold]Trace replay: {run_id}[/bold] ({len(entries)} entries)", border_style="cyan"))
for entry in entries:
etype = entry.get("type", "?")
ts = entry.get("ts", 0)
ts_str = datetime.fromtimestamp(ts).strftime("%H:%M:%S") if ts else ""
it = entry.get("iter", "")
iter_tag = f"[dim]#{it}[/dim] " if it else ""
if etype == "start":
console.print(f"\n[bold cyan]{ts_str}[/bold cyan] {iter_tag}[bold]START[/bold] {entry.get('prompt', '')[:120]}")
elif etype == "thinking":
content = entry.get("content", "")
console.print(f"[dim]{ts_str}[/dim] {iter_tag}[dim italic]{content[:200]}[/dim italic]")
elif etype == "tool_call":
tool = entry.get("tool", "")
args = entry.get("args", {})
args_str = ", ".join(f"{k}={v[:40]}" for k, v in args.items()) if args else ""
console.print(f"[dim]{ts_str}[/dim] {iter_tag}[cyan]\u25b6 {tool}[/cyan]({args_str})")
elif etype == "tool_result":
tool = entry.get("tool", "")
status = entry.get("status", "ok")
elapsed = entry.get("elapsed_ms", 0)
ok = status == "ok"
mark = "\u2713" if ok else "\u2717"
color = "green" if ok else "red"
preview = entry.get("preview", "")[:80]
console.print(f"[dim]{ts_str}[/dim] {iter_tag}[{color}]{mark} {tool}[/{color}] [dim]{elapsed}ms[/dim] {preview}")
elif etype == "tool_skipped":
console.print(f"[dim]{ts_str}[/dim] {iter_tag}[yellow]\u2298 {entry.get('tool', '')} (skipped)[/yellow]")
elif etype == "answer":
content = entry.get("content", "")
console.print(f"\n[dim]{ts_str}[/dim] {iter_tag}[bold green]ANSWER[/bold green]\n{content[:500]}")
elif etype == "end":
status = entry.get("status", "?")
iters = entry.get("iterations", "?")
color = "green" if status == "success" else "red"
console.print(f"\n[bold {color}]{ts_str} END[/bold {color}] status={status} iterations={iters}")
console.print()
# ---------------------------------------------------------------------------
# Swarm subcommands
# ---------------------------------------------------------------------------
def cmd_swarm_presets() -> None:
"""List available swarm presets."""
from src.swarm.presets import list_presets
presets = list_presets()
if not presets:
console.print("[dim]No presets available[/dim]")
return
table = Table(title="Swarm Presets", show_lines=False)
table.add_column("Name", style="cyan", no_wrap=True)
table.add_column("Title")
table.add_column("Agents", width=8, justify="right")
table.add_column("Variables")
table.add_column("Description", max_width=40)
for p in presets:
raw_vars = p.get("variables", [])
var_names = [
v["name"] if isinstance(v, dict) else str(v) for v in raw_vars
]
vars_str = ", ".join(var_names)
table.add_row(
p["name"],
p.get("title", ""),
str(p.get("agent_count", 0)),
vars_str,
p.get("description", "")[:40],
)
console.print(table)
def cmd_swarm_run(preset: str, vars_json: Optional[str] = None) -> None:
"""Run swarm preset (legacy polling mode, use cmd_swarm_run_live for streaming)."""
cmd_swarm_run_live(preset, vars_json)
def cmd_swarm_list() -> None:
"""List swarm run history."""
from src.swarm.store import SwarmStore
store = SwarmStore(base_dir=SWARM_DIR)
runs = store.list_runs()
if not runs:
console.print("[dim]No swarm runs yet[/dim]")
return
table = Table(title="Swarm Runs", show_lines=False)
table.add_column("Run ID", style="cyan", no_wrap=True)
table.add_column("Preset")
table.add_column("Status", width=12)
table.add_column("Tasks", width=6, justify="right")
table.add_column("Created", width=20)
for run in runs:
sc = {
"completed": "green",
"failed": "red",
"cancelled": "yellow",
"running": "blue",
}.get(run.status.value, "dim")
table.add_row(
run.id,
run.preset_name,
f"[{sc}]{run.status.value}[/{sc}]",
str(len(run.tasks)),
run.created_at[:19],
)
console.print(table)
def cmd_swarm_show(run_id: str) -> None:
"""Show swarm run details."""
from src.swarm.store import SwarmStore
from src.swarm.models import TaskStatus
store = SwarmStore(base_dir=SWARM_DIR)
run = store.load_run(run_id)
if run is None:
console.print(f"[red]Swarm run {run_id} not found[/red]")
return
status_color = {
"completed": "green",
"failed": "red",
"cancelled": "yellow",
"running": "blue",
}.get(run.status.value, "dim")
lines = [
f"[bold]Status:[/bold] [{status_color}]{run.status.value.upper()}[/{status_color}]",
f"[bold]Preset:[/bold] {run.preset_name}",
f"[bold]Created:[/bold] {run.created_at}",
]
if run.completed_at:
lines.append(f"[bold]Completed:[/bold] {run.completed_at}")
if run.user_vars:
lines.append(f"[bold]Variables:[/bold] {json.dumps(run.user_vars, ensure_ascii=False)}")
tokens_in = run.total_input_tokens
tokens_out = run.total_output_tokens
if tokens_in or tokens_out:
lines.append(f"[bold]Tokens:[/bold] ~{tokens_in + tokens_out:,} (in: {tokens_in:,} out: {tokens_out:,})")
lines.append(f"\n[bold]Tasks ({len(run.tasks)}):[/bold]")
for task in run.tasks:
tc = "green" if task.status == TaskStatus.completed else "red" if task.status == TaskStatus.failed else "dim"
dep_str = f" (deps: {', '.join(task.depends_on)})" if task.depends_on else ""
task_line = f" [{tc}]{task.id}[/{tc}] -> {task.agent_id}{dep_str} [{task.status.value}]"
lines.append(task_line)
if task.summary:
lines.append(f" {task.summary[:100]}")
if task.error:
lines.append(f" [red]{task.error[:100]}[/red]")
if run.final_report:
lines.append(f"\n[bold]Final Report:[/bold]\n{run.final_report[:800]}")
console.print(Panel("\n".join(lines), border_style=status_color, title=run_id))
def cmd_swarm_cancel(run_id: str) -> None:
"""Cancel a swarm run."""
from src.swarm.runtime import SwarmRuntime
from src.swarm.store import SwarmStore
store = SwarmStore(base_dir=SWARM_DIR)
runtime = SwarmRuntime(store=store)
if runtime.cancel_run(run_id):
console.print(f"[yellow]Cancel signal sent: {run_id}[/yellow]")
else:
console.print(f"[red]Run {run_id} not found or already finished[/red]")
# ---------------------------------------------------------------------------
# Session subcommands
# ---------------------------------------------------------------------------
def cmd_sessions() -> None:
"""List chat sessions."""
from src.session.store import SessionStore
store = SessionStore(base_dir=SESSIONS_DIR)
sessions = store.list_sessions()
if not sessions:
console.print("[dim]No sessions yet[/dim]")
return
table = Table(title="Sessions", show_lines=False)
table.add_column("Session ID", style="cyan", no_wrap=True)
table.add_column("Title", max_width=30)
table.add_column("Status", width=10)
table.add_column("Messages", width=8, justify="right")
table.add_column("Updated", width=20)
for s in sessions:
messages = store.get_messages(s.session_id)
sc = "green" if s.status.value == "active" else "dim"
table.add_row(
s.session_id,
s.title or "[dim]untitled[/dim]",
f"[{sc}]{s.status.value}[/{sc}]",
str(len(messages)),
s.updated_at[:19],
)
console.print(table)
def cmd_session_chat(session_id: str, max_iter: int) -> None:
"""Continue a session chat."""
from src.session.store import SessionStore
store = SessionStore(base_dir=SESSIONS_DIR)
session = store.get_session(session_id)
if session is None:
console.print(f"[red]Session {session_id} not found[/red]")
return
messages = store.get_messages(session_id)
history: List[Dict[str, str]] = []
for msg in messages:
if msg.role in ("user", "assistant") and msg.content.strip():
history.append({"role": msg.role, "content": msg.content})
console.print(Panel(
f"[bold cyan]Session: {session.title or session_id}[/bold cyan]\n"
f"[dim]History: {len(messages)} messages | Type q to exit[/dim]",
border_style="cyan",
))
while True:
try:
prompt = Prompt.ask("\n[bold]>[/bold]").strip()
except (KeyboardInterrupt, EOFError):
break
if not prompt or prompt.lower() in ("q", "quit", "exit"):
break
start = time.perf_counter()
try:
result = _run_agent(prompt, history=history[-6:], max_iter=max_iter)
except KeyboardInterrupt:
console.print("\n[yellow]Interrupted[/yellow]")
continue
_print_result(result, time.perf_counter() - start)
history.append({"role": "user", "content": prompt})
if result.get("content"):
history.append({"role": "assistant", "content": result["content"]})
console.print("[dim]Goodbye[/dim]")
# ---------------------------------------------------------------------------
# Upload subcommand
# ---------------------------------------------------------------------------
def cmd_upload(file_path: str) -> None:
"""Upload a file to the server."""
src = Path(file_path)
if not src.exists():
console.print(f"[red]File not found: {file_path}[/red]")
return
if not src.is_file():
console.print(f"[red]Not a file: {file_path}[/red]")
return
UPLOADS_DIR.mkdir(parents=True, exist_ok=True)
ext = src.suffix
dest_name = f"{uuid.uuid4().hex[:12]}{ext}"
dest = UPLOADS_DIR / dest_name
shutil.copy2(str(src), str(dest))
console.print(f"[green]Uploaded:[/green] {dest}")
# ---------------------------------------------------------------------------
# CLI entrypoint
# ---------------------------------------------------------------------------
def _build_parser() -> argparse.ArgumentParser:
"""Build the CLI parser with subcommands and compatibility flags."""
parser = argparse.ArgumentParser(description="Vibe-Trading CLI")
parser.add_argument("-p", "--prompt", type=str, help="Prompt text")
parser.add_argument("-f", "--prompt-file", type=Path, help="Read prompt text from a file")
parser.add_argument("--json", action="store_true", help="Print machine-readable JSON output")
parser.add_argument("--no-rich", action="store_true", help="Disable Rich formatting")
parser.add_argument("--chat", action="store_true", help="Interactive chat mode")
parser.add_argument("--continue", dest="cont", nargs=2, metavar=("RUN_ID", "PROMPT"), help="Continue a run")
parser.add_argument("--list", action="store_true", help="List runs")
parser.add_argument("--show", metavar="RUN_ID", help="Show run details")
parser.add_argument("--code", metavar="RUN_ID", help="Show generated code")
parser.add_argument("--trace", metavar="RUN_ID", help="Replay a run trace")
parser.add_argument("--skills", action="store_true", help="List skills")
parser.add_argument("--max-iter", type=int, default=50, help="Maximum agent iterations")
parser.add_argument("--swarm-presets", action="store_true", help="List swarm presets")
parser.add_argument("--swarm-run", nargs="+", metavar=("PRESET", "VARS"), help="Run a swarm preset")
parser.add_argument("--swarm-list", action="store_true", help="List swarm runs")
parser.add_argument("--swarm-show", metavar="RUN_ID", help="Show a swarm run")
parser.add_argument("--swarm-cancel", metavar="RUN_ID", help="Cancel a swarm run")
parser.add_argument("--sessions", action="store_true", help="List sessions")
parser.add_argument("--session-chat", metavar="SESSION_ID", help="Continue a session chat")
parser.add_argument("--upload", metavar="FILE_PATH", help="Upload a file")
subparsers = parser.add_subparsers(dest="command")
run_parser = subparsers.add_parser("run", help="Run a prompt")
run_parser.add_argument("-p", "--prompt", dest="run_prompt", type=str, help="Prompt text")
run_parser.add_argument("-f", "--prompt-file", dest="run_prompt_file", type=Path, help="Read prompt text from a file")
run_parser.add_argument("--json", dest="run_json", action="store_true", help="Print machine-readable JSON output")
run_parser.add_argument("--no-rich", dest="run_no_rich", action="store_true", help="Disable Rich formatting")
run_parser.add_argument("--max-iter", dest="run_max_iter", type=int, default=50, help="Maximum agent iterations")
serve_parser = subparsers.add_parser("serve", help="Start the API server")
serve_parser.add_argument("--host", default="0.0.0.0", help="Bind address")
serve_parser.add_argument("--port", type=int, default=8000, help="Listen port")
serve_parser.add_argument("--dev", action="store_true", help="Start the Vite dev server")
list_parser = subparsers.add_parser("list", help="List runs")
list_parser.add_argument("--limit", dest="list_limit", type=int, default=20, help="Maximum number of runs")
show_parser = subparsers.add_parser("show", help="Show run details")
show_parser.add_argument("run_id", help="Run identifier")
chat_parser = subparsers.add_parser("chat", help="Interactive chat mode")
chat_parser.add_argument("--max-iter", dest="chat_max_iter", type=int, default=50, help="Maximum agent iterations")
subparsers.add_parser("init", help="Interactive setup: create ~/.vibe-trading/.env")
return parser
def _handle_prompt_command(
prompt: Optional[str],
prompt_file: Optional[Path],
*,
max_iter: int,
json_mode: bool,
no_rich: bool,
) -> int:
"""Resolve a prompt and execute it."""
resolved_prompt, error_message = _read_prompt_source(prompt, prompt_file, no_rich=no_rich)
if error_message:
if json_mode:
_print_json_result({"status": "failed", "run_id": None, "run_dir": None, "reason": error_message})
else:
message = error_message if no_rich else f"[red]{error_message}[/red]"
print(error_message) if no_rich else console.print(message)
return EXIT_USAGE_ERROR
if not resolved_prompt:
if json_mode:
_print_json_result({"status": "failed", "run_id": None, "run_dir": None, "reason": "Prompt cannot be empty"})
else:
print("Prompt cannot be empty") if no_rich else console.print("[red]Prompt cannot be empty[/red]")
return EXIT_USAGE_ERROR
return cmd_run(resolved_prompt, max_iter, json_mode=json_mode, no_rich=no_rich)
_INIT_ENV_DIR = Path.home() / ".vibe-trading"
_INIT_ENV_PATH = _INIT_ENV_DIR / ".env"
def cmd_init() -> int:
"""Interactive setup: create ~/.vibe-trading/.env."""
console.print("[bold cyan]Vibe-Trading Init[/bold cyan]\n")
if _INIT_ENV_PATH.exists():
console.print(f"[yellow]Config already exists:[/yellow] {_INIT_ENV_PATH}")
overwrite = input("Overwrite? [y/N] ").strip().lower()
if overwrite != "y":
console.print("[dim]Aborted.[/dim]")
return 0
console.print("[dim]Press Enter to accept defaults shown in brackets.[/dim]\n")
provider = input("LLM provider [openrouter]: ").strip() or "openrouter"
api_key = input("API key [sk-or-v1-...]: ").strip() or "sk-or-v1-your-key-here"
default_base = {
"openrouter": "https://openrouter.ai/api/v1",
"deepseek": "https://api.deepseek.com/v1",
"openai": "https://api.openai.com/v1",
}
base_url = input(f"API base URL [{default_base.get(provider, '')}]: ").strip() or default_base.get(provider, "")
default_model = {
"openrouter": "deepseek/deepseek-v3.2",
"deepseek": "deepseek-chat",
"openai": "gpt-4o",
}
model = input(f"Model name [{default_model.get(provider, '')}]: ").strip() or default_model.get(provider, "")
tushare = input("Tushare token (A-shares, optional) []: ").strip()
env_content = (
f"LANGCHAIN_PROVIDER={provider}\n"
f"OPENAI_API_KEY={api_key}\n"
f"OPENAI_BASE_URL={base_url}\n"
f"LANGCHAIN_MODEL_NAME={model}\n"
f"LANGCHAIN_TEMPERATURE=0.0\n"
f"TIMEOUT_SECONDS=2400\n"
f"MAX_RETRIES=5\n"
)
if tushare:
env_content += f"TUSHARE_TOKEN={tushare}\n"
_INIT_ENV_DIR.mkdir(parents=True, exist_ok=True)
_INIT_ENV_PATH.write_text(env_content, encoding="utf-8")
console.print(f"\n[green]Config saved to {_INIT_ENV_PATH}[/green]")
console.print("[dim]Run [bold]vibe-trading[/bold] to start.[/dim]")
return 0
def main(argv: list[str] | None = None) -> int:
"""CLI entrypoint returning a process exit code."""
raw_argv = list(sys.argv[1:] if argv is None else argv)
parser = _build_parser()
try:
args = parser.parse_args(raw_argv)
except SystemExit as exc:
return int(exc.code) if isinstance(exc.code, int) else EXIT_USAGE_ERROR
if args.command == "init":
return cmd_init()
if args.command == "serve":
return serve_main(raw_argv[1:])
if args.command == "run":
return _handle_prompt_command(
args.run_prompt,
args.run_prompt_file,
max_iter=args.run_max_iter,
json_mode=args.run_json,
no_rich=args.run_no_rich,
)
if args.command == "list":
return _coerce_exit_code(cmd_list(args.list_limit))
if args.command == "show":
return _coerce_exit_code(cmd_show(args.run_id))
if args.command == "chat":
return _coerce_exit_code(cmd_interactive(args.chat_max_iter))
if args.list:
return _coerce_exit_code(cmd_list())
if args.show:
return _coerce_exit_code(cmd_show(args.show))
if args.code:
return _coerce_exit_code(cmd_code(args.code))
if args.trace:
return _coerce_exit_code(cmd_trace(args.trace))
if args.skills:
return _coerce_exit_code(cmd_skills())
if args.swarm_presets:
return _coerce_exit_code(cmd_swarm_presets())
if args.swarm_run:
preset_name = args.swarm_run[0]
vars_json = args.swarm_run[1] if len(args.swarm_run) > 1 else None
return _coerce_exit_code(cmd_swarm_run_live(preset_name, vars_json))
if args.swarm_list:
return _coerce_exit_code(cmd_swarm_list())
if args.swarm_show:
return _coerce_exit_code(cmd_swarm_show(args.swarm_show))
if args.swarm_cancel:
return _coerce_exit_code(cmd_swarm_cancel(args.swarm_cancel))
if args.sessions:
return _coerce_exit_code(cmd_sessions())
if args.session_chat:
return _coerce_exit_code(cmd_session_chat(args.session_chat, args.max_iter))
if args.upload:
return _coerce_exit_code(cmd_upload(args.upload))
if args.chat:
return _coerce_exit_code(cmd_interactive(args.max_iter))
if args.cont:
return cmd_continue(args.cont[0], args.cont[1], args.max_iter, json_mode=args.json, no_rich=args.no_rich)
# No flags, no subcommand → check if prompt provided, otherwise interactive mode
if args.prompt or args.prompt_file or not sys.stdin.isatty():
return _handle_prompt_command(
args.prompt,
args.prompt_file,
max_iter=args.max_iter,
json_mode=args.json,
no_rich=args.no_rich,
)
# Default: interactive mode
return _coerce_exit_code(cmd_interactive(args.max_iter))
if __name__ == "__main__":
raise SystemExit(main())