use clap::Parser;
use cli_agents::{CliName, RunOptions, StreamEvent, run};
use std::io::Write;
use std::sync::Arc;
#[derive(Parser)]
#[command(name = "cli-agents", arg_required_else_help = true)]
struct Args {
#[arg(long, value_parser = parse_cli_name)]
cli: Option<CliName>,
#[arg(long)]
model: Option<String>,
#[arg(long)]
system: Option<String>,
#[arg(long)]
append_system_prompt: Option<String>,
#[arg(long)]
cwd: Option<String>,
#[arg(long)]
skip_permissions: bool,
#[arg(long)]
json: bool,
#[arg(long, short)]
verbose: bool,
#[arg(long)]
discover: bool,
task: Vec<String>,
}
fn parse_cli_name(s: &str) -> Result<CliName, String> {
match s {
"claude" => Ok(CliName::Claude),
"codex" => Ok(CliName::Codex),
"gemini" => Ok(CliName::Gemini),
other => Err(format!(
"unknown CLI '{other}': use claude, codex, or gemini"
)),
}
}
#[tokio::main]
async fn main() {
let args = Args::parse();
if args.discover {
let results = cli_agents::discovery::discover_all().await;
if results.is_empty() {
eprintln!("No CLI agents found on this system.");
eprintln!("Install one of: claude, codex, gemini");
std::process::exit(1);
}
for (name, path) in &results {
println!("{name}: {path}");
}
std::process::exit(0);
}
let task = args.task.join(" ");
if task.is_empty() {
eprintln!("Error: no task provided. Run with --help for usage.");
std::process::exit(1);
}
let system_prompt = match (&args.system, &args.append_system_prompt) {
(Some(base), Some(extra)) => Some(format!("{base}\n\n{extra}")),
(Some(base), None) => Some(base.clone()),
(None, Some(extra)) => Some(extra.clone()),
(None, None) => None,
};
let opts = RunOptions {
cli: args.cli,
task,
system_prompt,
cwd: args.cwd,
model: args.model,
skip_permissions: args.skip_permissions,
providers: Some(cli_agents::ProviderOptions {
claude: Some(cli_agents::ClaudeOptions {
include_partial_messages: Some(true),
append_system_prompt: args.append_system_prompt.clone(),
..Default::default()
}),
..Default::default()
}),
..Default::default()
};
let cli_label = args
.cli
.map(|c| c.to_string())
.unwrap_or_else(|| "auto".into());
eprintln!(
"Running with cli={cli_label}, skip_permissions={}",
args.skip_permissions
);
let json_mode = args.json;
let verbose = args.verbose;
let on_event: Arc<dyn Fn(StreamEvent) + Send + Sync> = if json_mode {
Arc::new(move |event: StreamEvent| {
if let Ok(json) = serde_json::to_string(&event) {
let mut stdout = std::io::stdout().lock();
let _ = writeln!(stdout, "{json}");
}
})
} else {
let had_deltas = Arc::new(std::sync::atomic::AtomicBool::new(false));
Arc::new(move |event: StreamEvent| {
match event {
StreamEvent::TextDelta { text } => {
had_deltas.store(true, std::sync::atomic::Ordering::Relaxed);
print!("{text}");
let _ = std::io::stdout().flush();
}
StreamEvent::ThinkingDelta { text } if verbose => {
eprint!("\x1b[2m{text}\x1b[0m"); let _ = std::io::stderr().flush();
}
StreamEvent::ToolStart { tool_name, .. } if verbose => {
eprintln!("\x1b[36m▶ {tool_name}\x1b[0m");
}
StreamEvent::ToolEnd { success, error, .. } if verbose => {
if success {
eprintln!("\x1b[32m✓\x1b[0m");
} else {
eprintln!("\x1b[31m✗ {}\x1b[0m", error.unwrap_or_default());
}
}
StreamEvent::TurnEnd if verbose => {
eprintln!("\x1b[2m--- turn end ---\x1b[0m");
}
StreamEvent::Error { message, .. } => {
eprintln!("\x1b[31mError: {message}\x1b[0m");
}
StreamEvent::Done { result } => {
if !had_deltas.load(std::sync::atomic::Ordering::Relaxed) {
if let Some(text) = &result.text {
if !text.is_empty() {
print!("{text}");
}
}
}
println!();
if verbose {
if let Some(stats) = &result.stats {
eprint!("\x1b[2m");
if let Some(t) = stats.input_tokens {
eprint!("in={t} ");
}
if let Some(t) = stats.output_tokens {
eprint!("out={t} ");
}
if let Some(ms) = stats.duration_ms {
eprint!("time={ms}ms ");
}
if let Some(n) = stats.tool_calls {
eprint!("tools={n} ");
}
eprintln!("\x1b[0m");
}
if let Some(cost) = result.cost_usd {
eprintln!("\x1b[2mcost=${cost:.4}\x1b[0m");
}
}
}
_ => {}
}
})
};
let handle = run(opts, Some(on_event));
if std::io::IsTerminal::is_terminal(&std::io::stdin()) {
let cancel = handle.cancel.clone();
tokio::spawn(async move {
tokio::signal::ctrl_c().await.ok();
eprintln!("\nCancelling...");
cancel.cancel();
});
}
match handle.result.await {
Ok(Ok(result)) => {
let code = result
.exit_code
.unwrap_or(if result.success { 0 } else { 1 });
std::process::exit(code);
}
Ok(Err(e)) => {
eprintln!("Error: {e}");
std::process::exit(1);
}
Err(e) => {
eprintln!("Panic: {e}");
std::process::exit(1);
}
}
}