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::daemon::readiness::poll_until;
use leviath_cli::dispatch::{Commands, RiskyExecutors, apply_region_flags, dispatch};
#[cfg(feature = "mimalloc-allocator")]
#[global_allocator]
static GLOBAL: mimalloc::MiMalloc = mimalloc::MiMalloc;
#[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,
}
fn main() -> anyhow::Result<()> {
#[cfg(feature = "mimalloc-allocator")]
leviath_alloc::use_purge_at_free_unless_overridden();
tokio::runtime::Builder::new_multi_thread()
.enable_all()
.max_blocking_threads(2048)
.build()?
.block_on(async_main())
}
async fn async_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 allowed = leviath_cli::config::Config::load()
.map(|c| c.security.allowed_workdirs)
.unwrap_or_default();
let interactive = std::io::IsTerminal::is_terminal(&io::stdin()) && !args.yolo;
let ok = leviath_cli::workdir_guard::check(
std::path::Path::new(&workdir),
dirs::home_dir().as_deref(),
&allowed,
interactive,
&mut CrosstermSetup {
viewport: Viewport::Fullscreen,
mouse_capture: false,
enabled: false,
},
&mut CrosstermEventSource::open(),
)
.await;
if !ok {
println!("cancelled");
return Ok(());
}
}
let spawn_args = leviath_cli::daemon::client::resolve_spawn_args(
leviath_cli::daemon::client::LaunchRequest {
path,
task: args.task.as_deref(),
stdin_is_terminal: &|| std::io::IsTerminal::is_terminal(&io::stdin()),
model: args.model,
workdir: &workdir,
yolo: args.yolo,
allow: args.allow,
max_depth: args.max_depth,
regions: args.regions,
no_seed_commands: args.no_seed_commands,
output_request: commands::run::output_request(
args.output_format,
args.output_instructions,
args.output_schema,
)?,
},
)?;
ensure_daemon_running().await?;
leviath_cli::daemon::client::send_spawn_batch(
&control_client()?,
spawn_args,
args.count,
args.json,
)
.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"))?;
let running = is_daemon_running(&id);
let steps = leviath_cli::daemon::lifecycle::start_steps(
running,
running && daemon_build_is_stale(read_build_marker().as_deref()),
);
if !steps.spawn {
return Ok(());
}
if steps.shutdown_first {
eprintln!("leviath daemon is on an older build; restarting to load {CURRENT_BUILD}…");
let _ = control_client()?
.request(&leviath_runtime::control_socket::ControlRequest::Shutdown)
.await;
poll_until(&mut || !is_daemon_running(&id)).await;
}
let exe = std::env::current_exe()?;
let mut cmd = leviath_sys::child_command(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()?;
if poll_until(&mut || is_daemon_running(&id)).await {
return Ok(());
}
anyhow::bail!(
"the leviath daemon did not start within {:?}",
leviath_cli::daemon::readiness::READY_TIMEOUT
);
}
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"))?;
use leviath_cli::daemon::lifecycle::{StopFallback, stop_fallback, stop_outcome};
if !is_daemon_running(&id) {
println!("{}", stop_outcome(false, false).unwrap_or_default());
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 stop_fallback(leviath_runtime::control_socket::ControlToken::read_pid(
&dir,
)) {
StopFallback::Signal(pid) => {
eprintln!("control channel did not answer ({e}); signalling pid {pid}");
let _ = leviath_sys::kill_process_group(pid);
}
StopFallback::Propagate => return Err(e),
}
}
match stop_outcome(true, poll_until(&mut || !is_daemon_running(&id)).await) {
Ok(line) => {
println!("{line}");
Ok(())
}
Err(e) => anyhow::bail!(e),
}
}
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
};
let supervision = resolve_service_unit()
.ok()
.map(|unit| commands::daemon_service::format_supervision(unit.path.exists(), &unit.path));
for line in leviath_cli::daemon::lifecycle::status_lines(running, count, supervision) {
println!("{line}");
}
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 = leviath_sys::child_command(&cmd.0).args(&cmd.1).output()?;
if out.status.success() {
return Ok(());
}
Err(commands::daemon_service::supervisor_failure(
cmd,
&out.stderr,
))
}
fn real_daemon_install() -> anyhow::Result<()> {
let unit = resolve_service_unit()?;
let lines = commands::daemon_service::install_with(
&unit,
&mut run_supervisor,
&mut remove_legacy_services,
)?;
for line in lines {
println!("{line}");
}
Ok(())
}
fn real_daemon_uninstall() -> anyhow::Result<()> {
let unit = resolve_service_unit()?;
let lines = commands::daemon_service::uninstall_with(
&unit,
&mut run_supervisor,
&mut remove_legacy_services,
)?;
for line in lines {
println!("{line}");
}
Ok(())
}
#[cfg(target_os = "macos")]
fn remove_legacy_services() -> Vec<std::path::PathBuf> {
commands::daemon_service::remove_legacy_with(
dirs::home_dir(),
leviath_sys::current_uid(),
&mut |bootout| {
let _ = run_supervisor(bootout);
},
&mut |path| std::fs::remove_file(path).is_ok(),
)
}
#[cfg(not(target_os = "macos"))]
fn remove_legacy_services() -> Vec<std::path::PathBuf> {
Vec::new()
}
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,
mouse_capture: true,
enabled: false,
};
let mut events = CrosstermEventSource::open();
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,
mouse_capture: false,
enabled: false,
},
&mut CrosstermEventSource::open(),
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,
mouse_capture: false,
enabled: false,
};
let mut events = CrosstermEventSource::open();
commands::setup::execute_core(&mut wizard, &env, &mut setup, &mut events).await
}
struct CrosstermSetup {
viewport: Viewport,
mouse_capture: bool,
enabled: bool,
}
fn restore_terminal() {
io::stdout().execute(DisableMouseCapture).ok();
disable_raw_mode().ok();
io::stdout().execute(LeaveAlternateScreen).ok();
}
static TERMINAL_HELD: std::sync::atomic::AtomicBool = std::sync::atomic::AtomicBool::new(false);
fn install_terminal_restore_panic_hook() {
static INSTALL: std::sync::Once = std::sync::Once::new();
INSTALL.call_once(|| {
let previous = std::panic::take_hook();
std::panic::set_hook(Box::new(move |info| {
if TERMINAL_HELD.load(std::sync::atomic::Ordering::SeqCst) {
restore_terminal();
}
previous(info);
}));
});
}
impl TerminalSetup for CrosstermSetup {
type B = ratatui::backend::CrosstermBackend<io::Stdout>;
fn enable(&mut self) -> anyhow::Result<()> {
install_terminal_restore_panic_hook();
enable_raw_mode().map_err(anyhow::Error::from)?;
io::stdout()
.execute(EnterAlternateScreen)
.map_err(anyhow::Error::from)?;
if self.mouse_capture {
io::stdout()
.execute(EnableMouseCapture)
.map_err(anyhow::Error::from)?;
}
self.enabled = true;
TERMINAL_HELD.store(true, std::sync::atomic::Ordering::SeqCst);
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) {
if !self.enabled {
return;
}
self.enabled = false;
TERMINAL_HELD.store(false, std::sync::atomic::Ordering::SeqCst);
restore_terminal();
}
fn print_done(&self) {
println!("Dashboard closed.");
}
}
impl Drop for CrosstermSetup {
fn drop(&mut self) {
self.disable();
}
}