use std::fs::File;
use std::io;
use clap::Parser;
use crossterm::ExecutableCommand;
use crossterm::event::{DisableMouseCapture, EnableMouseCapture};
use crossterm::terminal::{
EnterAlternateScreen, LeaveAlternateScreen, disable_raw_mode, enable_raw_mode,
};
use ratatui::{Terminal, TerminalOptions, Viewport};
use tracing::info;
use leviath_cli::commands;
use leviath_cli::commands::dashboard::{CrosstermEventSource, DashboardArgs, TerminalSetup};
use leviath_cli::dispatch::{Commands, RiskyExecutors, apply_region_flags, dispatch};
#[derive(Parser)]
#[command(name = "lev")]
#[command(about = "Leviath agent framework CLI", long_about = None)]
#[command(version)]
struct Cli {
#[arg(short, long, global = true)]
verbose: bool,
#[command(subcommand)]
command: Commands,
}
#[tokio::main]
async fn main() -> anyhow::Result<()> {
let (argv, region_flags) =
commands::run::extract_region_flags(std::env::args().collect::<Vec<_>>());
let mut cli = Cli::parse_from(argv);
apply_region_flags(&mut cli.command, region_flags);
leviath_cli::logging::init(cli.verbose);
info!("Leviath CLI v{}", env!("CARGO_PKG_VERSION"));
dispatch(cli.command, &RealExecutors).await
}
struct RealExecutors;
impl RiskyExecutors for RealExecutors {
async fn run(&self, args: commands::run::RunArgs) -> anyhow::Result<()> {
real_run(args).await
}
async fn ps(&self, args: commands::ps::PsArgs) -> anyhow::Result<()> {
commands::ps::send_list(&control_client()?, &args).await
}
async fn msg(&self, args: commands::ctl::MsgArgs) -> anyhow::Result<()> {
commands::ctl::send_message(&control_client()?, &args).await
}
async fn cancel(&self, args: commands::ctl::CancelArgs) -> anyhow::Result<()> {
commands::ctl::cancel_run(&control_client()?, &args).await
}
async fn pause(&self, args: commands::ctl::PauseArgs) -> anyhow::Result<()> {
commands::ctl::pause_run(&control_client()?, &args).await
}
async fn resume(&self, args: commands::ctl::ResumeArgs) -> anyhow::Result<()> {
commands::ctl::resume_run(&control_client()?, &args).await
}
async fn respond(&self, args: commands::ctl::RespondArgs) -> anyhow::Result<()> {
commands::ctl::respond(&control_client()?, &args).await
}
async fn doctor(&self, args: commands::doctor::DoctorArgs) -> anyhow::Result<()> {
real_doctor(args).await
}
async fn setup(&self, args: commands::setup::SetupArgs) -> anyhow::Result<()> {
real_setup(args).await
}
async fn dashboard(&self, args: DashboardArgs) -> anyhow::Result<()> {
real_dashboard(args).await
}
async fn serve(&self, args: commands::serve::ServeArgs) -> anyhow::Result<()> {
ensure_daemon_running().await?;
commands::serve::execute(args, control_client()?).await
}
async fn agent_client(
&self,
args: commands::agent_client::AgentClientArgs,
) -> anyhow::Result<()> {
ensure_daemon_running().await?;
let default_cwd = std::env::current_dir()
.map(|p| p.to_string_lossy().into_owned())
.unwrap_or_default();
commands::agent_client::serve_over(
tokio::io::BufReader::new(tokio::io::stdin()),
tokio::io::stdout(),
control_client()?,
args,
leviath_cli::runstate::runs_dir(),
default_cwd,
)
.await
}
async fn daemon(&self, args: commands::daemon::DaemonArgs) -> anyhow::Result<()> {
use commands::daemon::DaemonAction;
match args.action {
None => real_daemon(args).await,
Some(DaemonAction::Start) => real_daemon_start().await,
Some(DaemonAction::Stop) => real_daemon_stop().await,
Some(DaemonAction::Status) => real_daemon_status().await,
Some(DaemonAction::Restart) => real_daemon_restart().await,
Some(DaemonAction::Install) => real_daemon_install(),
Some(DaemonAction::Uninstall) => real_daemon_uninstall(),
}
}
async fn auth(&self, args: commands::auth::AuthArgs) -> anyhow::Result<()> {
commands::auth::execute(args).await
}
async fn mcp(&self, args: commands::mcp::McpArgs) -> anyhow::Result<()> {
let config = leviath_cli::config::Config::load()?;
let env = commands::mcp::McpEnv {
config_path: leviath_cli::config::Config::config_path(),
store_path: leviath_mcp::AuthStore::default_path().ok_or_else(|| {
anyhow::anyhow!("could not resolve a home directory for the MCP auth store")
})?,
opener: std::sync::Arc::new(leviath_sys::open_url),
now: std::time::SystemTime::now()
.duration_since(std::time::UNIX_EPOCH)
.map(|d| d.as_secs())
.unwrap_or(0),
tools_dir: leviath_core::tools_dir(),
credential_store: leviath_cli::credentials::store_for(config.security.credential_store)
.map_err(|e| anyhow::anyhow!("{e}"))?,
allow_env_vars: config.security.allow_env_vars,
};
commands::mcp::execute_with(args, &env).await
}
}
async fn real_run(args: commands::run::RunArgs) -> anyhow::Result<()> {
let path = args.path.as_deref().unwrap_or(".");
let workdir = commands::run::effective_workdir(args.workdir, std::env::current_dir()?)?;
let spawn_args = leviath_cli::daemon::client::resolve_spawn_args(
path,
args.task.as_deref(),
&|| std::io::IsTerminal::is_terminal(&io::stdin()),
args.model,
&workdir,
args.yolo,
args.allow,
args.max_depth,
args.regions,
args.no_seed_commands,
)?;
ensure_daemon_running().await?;
leviath_cli::daemon::client::send_spawn(&control_client()?, spawn_args).await
}
async fn real_doctor(args: commands::doctor::DoctorArgs) -> anyhow::Result<()> {
use commands::doctor::DaemonTarget;
if args.no_daemon {
return commands::doctor::execute(args, DaemonTarget::Skip).await;
}
let started = ensure_daemon_running()
.await
.and_then(|()| control_client());
match started {
Ok(client) => commands::doctor::execute(args, DaemonTarget::Client(&client)).await,
Err(e) => commands::doctor::execute(args, DaemonTarget::Unavailable(e.to_string())).await,
}
}
async fn ensure_daemon_running() -> anyhow::Result<()> {
use leviath_cli::daemon::setup::{
CURRENT_BUILD, control_address, daemon_build_is_stale, read_build_marker,
};
use leviath_runtime::control_socket::is_daemon_running;
let id = control_address()
.ok_or_else(|| anyhow::anyhow!("cannot resolve a home directory for the control socket"))?;
if is_daemon_running(&id) {
if !daemon_build_is_stale(read_build_marker().as_deref()) {
return Ok(()); }
eprintln!("leviath daemon is on an older build; restarting to load {CURRENT_BUILD}…");
let _ = control_client()?
.request(&leviath_runtime::control_socket::ControlRequest::Shutdown)
.await;
for _ in 0..100 {
if !is_daemon_running(&id) {
break;
}
tokio::time::sleep(std::time::Duration::from_millis(50)).await;
}
}
let exe = std::env::current_exe()?;
let mut cmd = std::process::Command::new(exe);
cmd.arg("daemon")
.stdin(std::process::Stdio::null())
.stdout(std::process::Stdio::null())
.stderr(std::process::Stdio::null());
leviath_sys::process::configure_detached(&mut cmd);
cmd.spawn()?;
for _ in 0..100 {
if is_daemon_running(&id) {
return Ok(());
}
tokio::time::sleep(std::time::Duration::from_millis(50)).await;
}
anyhow::bail!("the leviath daemon did not start within 5s");
}
async fn real_daemon_start() -> anyhow::Result<()> {
ensure_daemon_running().await?;
println!("leviath daemon is running");
Ok(())
}
async fn real_daemon_stop() -> anyhow::Result<()> {
use leviath_runtime::control_socket::is_daemon_running;
let id = leviath_cli::daemon::setup::control_address()
.ok_or_else(|| anyhow::anyhow!("cannot resolve a home directory for the control socket"))?;
if !is_daemon_running(&id) {
println!("daemon not running");
return Ok(());
}
if let Err(e) = commands::daemon::send_shutdown(&control_client()?).await {
let dir = leviath_cli::daemon::setup::control_dir()
.ok_or_else(|| anyhow::anyhow!("cannot resolve a home directory for the daemon pid"))?;
match leviath_runtime::control_socket::ControlToken::read_pid(&dir) {
Some(pid) => {
eprintln!("control channel did not answer ({e}); signalling pid {pid}");
let _ = leviath_sys::kill_process_group(pid);
}
None => return Err(e),
}
}
for _ in 0..100 {
if !is_daemon_running(&id) {
println!("daemon stopped");
return Ok(());
}
tokio::time::sleep(std::time::Duration::from_millis(50)).await;
}
anyhow::bail!("the leviath daemon did not shut down within 5s");
}
async fn real_daemon_status() -> anyhow::Result<()> {
use leviath_runtime::control_socket::{ControlResponse, is_daemon_running};
let id = leviath_cli::daemon::setup::control_address()
.ok_or_else(|| anyhow::anyhow!("cannot resolve a home directory for the control socket"))?;
let running = is_daemon_running(&id);
let count = if running {
match control_client()?.list().await {
Ok(ControlResponse::List { runs, .. }) => runs.len(),
_ => 0,
}
} else {
0
};
println!("{}", commands::daemon::format_status(running, count));
if let Ok(unit) = resolve_service_unit() {
println!(
"{}",
commands::daemon_service::format_supervision(unit.path.exists(), &unit.path)
);
}
Ok(())
}
async fn real_daemon_restart() -> anyhow::Result<()> {
real_daemon_stop().await?;
real_daemon_start().await
}
fn resolve_service_unit() -> anyhow::Result<commands::daemon_service::ServiceUnit> {
let user_home = dirs::home_dir()
.ok_or_else(|| anyhow::anyhow!("cannot resolve a home directory for the service file"))?;
let leviath_home = leviath_cli::config::leviath_home_dir()
.ok_or_else(|| anyhow::anyhow!("cannot resolve a leviath home directory"))?
.join(".leviath");
let exe = std::env::current_exe()?;
commands::daemon_service::service_unit(
&exe,
&leviath_home,
&commands::daemon_service::config_home(&user_home)?,
leviath_sys::current_uid(),
)
}
fn run_supervisor(cmd: &(String, Vec<String>)) -> anyhow::Result<()> {
let out = std::process::Command::new(&cmd.0).args(&cmd.1).output()?;
if out.status.success() {
return Ok(());
}
anyhow::bail!(
"`{} {}` failed: {}",
cmd.0,
cmd.1.join(" "),
String::from_utf8_lossy(&out.stderr).trim()
)
}
fn real_daemon_install() -> anyhow::Result<()> {
let unit = resolve_service_unit()?;
let path = commands::daemon_service::install(&unit)?;
println!("wrote {}", path.display());
let _ = run_supervisor(&unit.deactivate);
remove_legacy_services();
run_supervisor(&unit.activate)?;
println!("the leviath daemon is now supervised and will restart automatically");
Ok(())
}
fn real_daemon_uninstall() -> anyhow::Result<()> {
let unit = resolve_service_unit()?;
let _ = run_supervisor(&unit.deactivate);
remove_legacy_services();
if commands::daemon_service::uninstall(&unit)? {
println!("removed {}", unit.path.display());
} else {
println!("no leviath service was installed");
}
Ok(())
}
#[cfg(target_os = "macos")]
fn remove_legacy_services() {
let Some(user_home) = dirs::home_dir() else {
return;
};
let Ok(config_home) = commands::daemon_service::config_home(&user_home) else {
return;
};
for (path, bootout) in
commands::daemon_service::legacy_cleanup(&config_home, leviath_sys::current_uid())
{
let _ = run_supervisor(&bootout);
if std::fs::remove_file(&path).is_ok() {
println!("removed legacy service file {}", path.display());
}
}
}
#[cfg(not(target_os = "macos"))]
fn remove_legacy_services() {}
async fn real_daemon(args: commands::daemon::DaemonArgs) -> anyhow::Result<()> {
use leviath_cli::daemon::setup::{control_address, setup_daemon_host};
use leviath_runtime::control_socket::{
bind_control_listener, control_id_from_str, handle_connection,
};
let config = leviath_cli::config::Config::load()
.map_err(|e| anyhow::anyhow!("daemon refusing to start on a broken config: {e}"))?;
let runs_dir = leviath_cli::runstate::runs_dir();
let id = match args.socket {
Some(ref s) => control_id_from_str(s),
None => control_address().ok_or_else(|| {
anyhow::anyhow!("cannot resolve a home directory for the control socket")
})?,
};
let mut listener = bind_control_listener(&id)?;
let control_dir = leviath_cli::daemon::setup::control_dir()
.ok_or_else(|| anyhow::anyhow!("cannot resolve a home directory for the control token"))?;
let token = leviath_runtime::control_socket::ControlToken::create(&control_dir)?;
let _ = leviath_runtime::control_socket::ControlToken::write_pid(&control_dir);
leviath_cli::daemon::setup::write_build_marker();
let mut host = setup_daemon_host(config, runs_dir, tokio::runtime::Handle::current()).await;
let (op_tx, op_rx) = tokio::sync::mpsc::unbounded_channel();
let events = host.event_sender();
tokio::spawn(async move {
while let Ok(accepted) = listener.accept().await {
let Some(stream) = accepted else { continue };
let op_tx = op_tx.clone();
let events = events.clone();
let token = token.clone();
tokio::spawn(async move {
let _ = handle_connection(stream, op_tx, events, Some(token)).await;
});
}
});
let shutdown = host.world_mut().shutdown_handle();
tokio::spawn(async move {
let _ = tokio::signal::ctrl_c().await;
shutdown.notify_one();
});
info!("leviath daemon listening");
println!("leviath daemon listening");
host.serve(op_rx).await;
Ok(())
}
fn control_client() -> anyhow::Result<leviath_runtime::control_socket::ControlClient> {
let id = leviath_cli::daemon::setup::control_address()
.ok_or_else(|| anyhow::anyhow!("cannot resolve a home directory for the control socket"))?;
let dir = leviath_cli::daemon::setup::control_dir()
.ok_or_else(|| anyhow::anyhow!("cannot resolve a home directory for the control token"))?;
Ok(leviath_runtime::control_socket::ControlClient::for_home(
id, &dir,
))
}
async fn real_dashboard(_args: DashboardArgs) -> anyhow::Result<()> {
ensure_daemon_running().await?;
let control = control_client()?;
let mut setup = CrosstermSetup {
viewport: Viewport::Fullscreen,
};
let mut events = CrosstermEventSource::new();
commands::dashboard::execute_with(control, &mut setup, &mut events, real_yank).await
}
fn real_yank(text: &str) -> bool {
commands::dashboard::yank_to_clipboard_via(text, |t| {
let mut out = io::stdout();
leviath_sys::osc52_write_via(t, open_controlling_tty, &mut out)
})
}
#[cfg(unix)]
fn open_controlling_tty() -> io::Result<File> {
std::fs::OpenOptions::new().write(true).open("/dev/tty")
}
#[cfg(not(unix))]
fn open_controlling_tty() -> io::Result<File> {
Err(io::Error::other("no controlling terminal on this platform"))
}
async fn real_setup(args: commands::setup::SetupArgs) -> anyhow::Result<()> {
use commands::setup::{SetupEnv, import, verification_loop};
use leviath_cli::commands::setup::verify::{LiveVerifier, SkipVerifier};
let home = leviath_cli::config::leviath_home_dir().unwrap_or_default();
let env = SetupEnv {
config_path: leviath_cli::config::Config::config_path(),
agents_dir: commands::setup::real_agents_dir(Some(&home)),
roots: import::Roots::new(
home,
dirs::config_dir().unwrap_or_default(),
std::env::current_dir().unwrap_or_default(),
),
env_lookup: Box::new(|name| std::env::var(name).ok()),
opener: std::sync::Arc::new(leviath_sys::open_url),
};
if args.non_interactive {
return commands::setup::run_non_interactive(&args, &env);
}
if !std::io::IsTerminal::is_terminal(&io::stdout()) {
return commands::setup::execute_with(
&args,
&env,
&mut CrosstermSetup {
viewport: Viewport::Fullscreen,
},
&mut CrosstermEventSource::new(),
false,
)
.await;
}
let mut wizard = commands::setup::build_wizard(&env);
if let Some((requests, replies)) = wizard.take_verify_ends() {
if args.no_verify {
tokio::spawn(verification_loop(SkipVerifier, requests, replies));
} else {
tokio::spawn(verification_loop(LiveVerifier, requests, replies));
}
}
let mut setup = CrosstermSetup {
viewport: Viewport::Fullscreen,
};
let mut events = CrosstermEventSource::new();
commands::setup::execute_core(&mut wizard, &env, &mut setup, &mut events).await
}
struct CrosstermSetup {
viewport: Viewport,
}
impl TerminalSetup for CrosstermSetup {
type B = ratatui::backend::CrosstermBackend<io::Stdout>;
fn enable(&mut self) -> anyhow::Result<()> {
enable_raw_mode().map_err(anyhow::Error::from)?;
io::stdout()
.execute(EnterAlternateScreen)
.map_err(anyhow::Error::from)?;
io::stdout()
.execute(EnableMouseCapture)
.map_err(anyhow::Error::from)?;
Ok(())
}
fn create_terminal(&mut self) -> anyhow::Result<Terminal<Self::B>> {
let backend = ratatui::backend::CrosstermBackend::new(io::stdout());
Terminal::with_options(
backend,
TerminalOptions {
viewport: self.viewport.clone(),
},
)
.map_err(anyhow::Error::from)
}
fn disable(&mut self) {
io::stdout().execute(DisableMouseCapture).ok();
disable_raw_mode().ok();
io::stdout().execute(LeaveAlternateScreen).ok();
}
fn print_done(&self) {
println!("Dashboard closed.");
}
}