mod app;
mod commands;
mod config;
mod init;
mod input;
mod login;
mod permissions;
mod prompt;
mod render;
mod repl;
mod sessions;
mod signals;
mod status;
mod theme;
mod tui;
use clap::{Parser, Subcommand};
#[derive(Parser)]
#[command(
name = "abstract",
about = "A high-performance AI coding agent",
version,
after_help = "Examples:\n abstract Start interactive REPL\n abstract \"fix the tests\" Single-shot mode\n abstract --resume Resume last session\n abstract --model opus --max Use Opus with max thinking"
)]
pub struct Cli {
#[arg(short = 'p', long = "prompt", value_name = "PROMPT")]
pub prompt: Option<String>,
#[arg(long, value_name = "SESSION_ID", num_args = 0..=1, default_missing_value = "last")]
pub resume: Option<String>,
#[arg(short, long)]
pub model: Option<String>,
#[arg(short = 'P', long)]
pub provider: Option<String>,
#[arg(long, conflicts_with = "max")]
pub fast: bool,
#[arg(long, conflicts_with = "fast")]
pub max: bool,
#[arg(long, value_delimiter = ',', value_name = "MODELS")]
pub fallback: Vec<String>,
#[arg(long)]
pub no_permissions: bool,
#[arg(long)]
pub json: bool,
#[arg(short, long)]
pub verbose: bool,
#[arg(short = 'C', long)]
pub directory: Option<String>,
#[arg(long, alias = "benchmark")]
pub headless: bool,
#[arg(long)]
pub embedding_api: bool,
#[arg(long, value_name = "FORMAT")]
pub output_format: Option<String>,
#[arg(long)]
pub proxy: bool,
#[arg(long, value_name = "URL")]
pub proxy_url: Option<String>,
#[arg(long, value_name = "LEVEL")]
pub compress: Option<String>,
#[command(subcommand)]
pub command: Option<Commands>,
}
#[derive(Subcommand)]
pub enum Commands {
Sessions {
#[command(subcommand)]
action: SessionAction,
},
Config {
#[command(subcommand)]
action: ConfigAction,
},
Memory {
#[command(subcommand)]
action: MemoryAction,
},
Mcp {
#[command(subcommand)]
action: McpAction,
},
Init,
Login {
provider: Option<String>,
},
Logout,
}
#[derive(Subcommand)]
pub enum SessionAction {
#[command(alias = "ls")]
List,
Show { id: String },
Rm { id: String },
}
#[derive(Subcommand)]
pub enum ConfigAction {
Show,
Set { key: String, value: String },
}
#[derive(Subcommand)]
pub enum MemoryAction {
Show,
Clear,
}
#[derive(Subcommand)]
pub enum McpAction {
Add {
name: String,
#[arg(trailing_var_arg = true, allow_hyphen_values = true)]
command: Vec<String>,
},
List,
Remove { name: String },
}
#[tokio::main]
async fn main() -> anyhow::Result<()> {
let cli = Cli::parse();
if cli.verbose {
tracing_subscriber::fmt()
.with_env_filter("abstract=debug,cersei=debug")
.init();
} else {
tracing_subscriber::fmt()
.with_env_filter("abstract=warn,cersei=warn")
.init();
}
login::export_saved_keys_to_env();
let mut config = config::load();
apply_cli_overrides(&cli, &mut config);
match &cli.command {
Some(Commands::Init) => init::run()?,
Some(Commands::Login { provider }) => {
login::run_login(provider.as_deref()).await?;
return Ok(());
}
Some(Commands::Logout) => {
login::run_logout()?;
return Ok(());
}
Some(Commands::Sessions { action }) => match action {
SessionAction::List => sessions::list(&config)?,
SessionAction::Show { id } => sessions::show(&config, id)?,
SessionAction::Rm { id } => sessions::delete(&config, id)?,
},
Some(Commands::Config { action }) => match action {
ConfigAction::Show => {
println!("{}", toml::to_string_pretty(&config)?);
}
ConfigAction::Set { key, value } => {
config_set(&mut config, key, value)?;
config::save_to(&config, &config::project_config_path())?;
println!("Set {} = {}", key, value);
}
},
Some(Commands::Memory { action }) => match action {
MemoryAction::Show => sessions::show_memory(&config)?,
MemoryAction::Clear => sessions::clear_memory(&config)?,
},
Some(Commands::Mcp { action }) => match action {
McpAction::Add { name, command } => {
if command.is_empty() {
anyhow::bail!("MCP server command is required");
}
config.mcp_servers.push(config::McpServerEntry {
name: name.clone(),
command: command[0].clone(),
args: command[1..].to_vec(),
env: Default::default(),
});
config::save_to(&config, &config::project_config_path())?;
println!("Added MCP server: {}", name);
}
McpAction::List => {
if config.mcp_servers.is_empty() {
println!("No MCP servers configured.");
} else {
for s in &config.mcp_servers {
println!(" {} — {} {}", s.name, s.command, s.args.join(" "));
}
}
}
McpAction::Remove { name } => {
let before = config.mcp_servers.len();
config.mcp_servers.retain(|s| s.name != *name);
if config.mcp_servers.len() < before {
config::save_to(&config, &config::project_config_path())?;
println!("Removed MCP server: {}", name);
} else {
println!("MCP server '{}' not found.", name);
}
}
},
None => {
app::run(cli, config).await?;
}
}
Ok(())
}
fn apply_cli_overrides(cli: &Cli, config: &mut config::AppConfig) {
if let Some(m) = &cli.model {
config.model = resolve_model_alias(m);
}
if let Some(p) = &cli.provider {
config.provider = p.clone();
}
if cli.fast {
config.effort = "low".into();
}
if cli.max {
config.effort = "max".into();
}
if cli.no_permissions {
config.permissions_mode = "allow_all".into();
}
if let Some(dir) = &cli.directory {
config.working_dir = std::path::PathBuf::from(dir);
}
if !cli.fallback.is_empty() {
config.fallback_models = cli.fallback.clone();
}
if cli.proxy {
config.proxy.enabled = true;
config.proxy.force = true;
}
if cli.headless {
config.benchmark_mode = true;
config.permissions_mode = "allow_all".into();
config.max_turns = 80;
}
if cli.embedding_api {
config.embedding_api = true;
}
if let Some(fmt) = &cli.output_format {
config.output_format = fmt.clone();
}
if let Some(url) = &cli.proxy_url {
config.proxy.enabled = true;
config.proxy.url = url.clone();
}
if let Some(lvl) = &cli.compress {
config.compression_level = lvl.clone();
}
}
fn resolve_model_alias(alias: &str) -> String {
match alias {
"opus" => "anthropic/claude-opus-4-6".into(),
"sonnet" => "anthropic/claude-sonnet-4-6".into(),
"haiku" => "anthropic/claude-haiku-4-5".into(),
"gpt4o" | "4o" => "openai/gpt-4o".into(),
"gemini" => "google/gemini-3.1-pro-preview".into(),
"llama" => "groq/llama-3.1-70b-versatile".into(),
"deepseek" => "deepseek/deepseek-chat".into(),
"grok" => "xai/grok-2".into(),
"mistral" => "mistral/mistral-large-latest".into(),
other => other.into(),
}
}
fn config_set(config: &mut config::AppConfig, key: &str, value: &str) -> anyhow::Result<()> {
match key {
"model" => config.model = value.into(),
"provider" => config.provider = value.into(),
"max_turns" => config.max_turns = value.parse()?,
"max_tokens" => config.max_tokens = value.parse()?,
"effort" => config.effort = value.into(),
"output_style" => config.output_style = value.into(),
"theme" => config.theme = value.into(),
"auto_compact" => config.auto_compact = value.parse()?,
"graph_memory" => config.graph_memory = value.parse()?,
"permissions_mode" => config.permissions_mode = value.into(),
_ => anyhow::bail!("Unknown config key: {}", key),
}
Ok(())
}