#![allow(dead_code, clippy::if_same_then_else)]
mod api;
mod bootstrap;
mod cli;
mod commands;
mod compact;
mod config;
mod context;
mod cost;
mod db;
mod permissions;
mod plugin;
mod query;
mod repl;
mod session;
#[cfg(test)]
mod test_support;
mod theme;
mod tools;
mod tui;
mod utils;
use anyhow::Result;
use clap::Parser;
use std::sync::Arc;
#[tokio::main]
async fn main() -> Result<()> {
let args = cli::Cli::parse();
let filter = if args.debug {
"claux=debug"
} else if args.verbose {
"claux=info"
} else {
"claux=warn"
};
tracing_subscriber::fmt()
.with_env_filter(filter)
.with_writer(std::io::stderr)
.init();
let mut config = config::Config::load(args.trust_project)?;
if let Some(ref mode) = args.permission_mode {
config.permission_mode = serde_json::from_value(serde_json::Value::String(mode.clone()))
.map_err(|_| {
anyhow::anyhow!(
"Invalid permission mode {mode:?}; expected default, accept-edits, bypass, or plan"
)
})?;
}
let mut plugin_registry = plugin::PluginRegistry::new();
for plugin_config in &config.plugins {
plugin_registry.add(Box::new(plugin::CommandPlugin::new(
&plugin_config.name,
&plugin_config.command,
&plugin_config.args,
plugin_config.trigger.clone(),
)));
}
if !plugin_registry.is_empty() {
tracing::info!(
"Loaded {} plugin(s): {} context, {} tool-start, {} tool-complete, {} session-start, {} turn-end, {} permission-request",
plugin_registry.len(),
plugin_registry.get_by_trigger(&config::HookTrigger::OnContextBuild),
plugin_registry.get_by_trigger(&config::HookTrigger::OnToolStart),
plugin_registry.get_by_trigger(&config::HookTrigger::OnToolComplete),
plugin_registry.get_by_trigger(&config::HookTrigger::OnSessionStart),
plugin_registry.get_by_trigger(&config::HookTrigger::OnTurnEnd),
plugin_registry.get_by_trigger(&config::HookTrigger::OnPermissionRequest),
);
}
let plugin_registry = Arc::new(plugin_registry);
let model = args.model.as_deref().unwrap_or(&config.model).to_string();
tracing::debug!(
"Config loaded: openai_base_url={:?} openai_api_key_cmd={:?} model={}",
config.openai_base_url,
config.openai_api_key_cmd,
config.model
);
let provider = build_provider(&config, &model)?;
let provider_name = provider.name().to_string();
tracing::info!("Provider: {} ({})", provider_name, model);
let config_for_factory = config.clone();
let model_for_factory = model.clone();
let agent_factory: tools::agent::ProviderFactory = Box::new(move || {
build_provider(&config_for_factory, &model_for_factory)
.expect("failed to build agent provider")
});
let mcp_tools = bootstrap::connect_mcp_tools(&config).await;
if let Some(ref prompt) = args.prompt {
let mut tool_registry = tools::ToolRegistry::new_with_agent_factory(
agent_factory,
model.clone(),
config.permission_mode,
);
tool_registry.add_tools(mcp_tools);
let permission_checker = permissions::PermissionChecker::new(config.permission_mode);
let mut engine = query::Engine::new(provider, tool_registry, permission_checker, &model);
engine.set_plugins(plugin_registry.clone());
engine.set_auto_compact_threshold(config.auto_compact_threshold);
engine.set_max_tokens(config.max_tokens);
engine.set_model_pricing(config.model_pricing.get(&model).copied());
let system_prompt = context::build_system_prompt_for_model(
&model,
Some(&plugin_registry),
&config::HookTrigger::OnContextBuild,
config.is_anthropic(),
)
.await?;
engine.set_system_prompt(system_prompt);
let response = engine
.submit(prompt, tokio_util::sync::CancellationToken::new())
.await?;
print!("{response}");
return Ok(());
}
let mut tool_registry = tools::ToolRegistry::new_with_agent_factory(
agent_factory,
model.clone(),
config.permission_mode,
);
tool_registry.add_tools(mcp_tools);
let permission_checker = permissions::PermissionChecker::new(config.permission_mode);
let mut engine = query::Engine::new(provider, tool_registry, permission_checker, &model);
engine.set_plugins(plugin_registry.clone());
engine.set_auto_compact_threshold(config.auto_compact_threshold);
engine.set_max_tokens(config.max_tokens);
engine.set_model_pricing(config.model_pricing.get(&model).copied());
plugin::PluginRegistry::execute_side_effects(
&plugin_registry,
&config::HookTrigger::OnSessionStart,
None,
)
.await?;
let mut resumed_id: Option<String> = None;
if let Some(ref session_id) = args.resume {
match session::find_session(session_id)? {
Some((sid, path)) => {
let (meta, messages) = session::load_session(&path)?;
engine.set_messages(messages);
eprintln!(
"Resumed session {} ({}, {} messages)",
meta.id,
meta.model,
engine.message_count()
);
resumed_id = Some(sid);
}
None => {
eprintln!("Session not found: {session_id}. Starting new session.");
}
}
}
if args.tui {
tui::run(engine, &config, &plugin_registry).await
} else {
repl::run(engine, &config, &plugin_registry, resumed_id).await
}
}
fn build_provider(config: &config::Config, model: &str) -> Result<Box<dyn api::Provider>> {
if let Some(ref base_url) = config.openai_base_url {
let api_key = config.resolve_openai_key().unwrap_or_default();
if api_key.is_empty() && base_url.contains("api.openai.com") {
anyhow::bail!(
"No OpenAI API key found. Set OPENAI_API_KEY or configure \
openai_api_key_cmd in ~/.config/claux/config.toml. ChatGPT login \
credentials are not API credentials."
);
}
let name = config.openai_provider_name.as_deref().unwrap_or("openai");
return match config.openai_protocol {
config::OpenAIProtocol::ChatCompletions => Ok(Box::new(
api::OpenAICompatProvider::new(base_url, &api_key, model, name),
)),
config::OpenAIProtocol::Responses => Ok(Box::new(api::OpenAIResponsesProvider::new(
base_url,
&api_key,
model,
name,
config.openai_reasoning_effort.as_deref(),
))),
};
}
let auth = config
.resolve_auth()
.ok_or_else(|| anyhow::anyhow!(
"No authentication found. Set ANTHROPIC_API_KEY, configure ~/.config/claux/config.toml, or run `claude login`."
))?;
Ok(Box::new(api::AnthropicProvider::new(auth, model)))
}