mod agent;
mod config;
mod memory;
mod messages;
mod plugin;
mod provider;
mod providers;
mod session;
mod tool;
mod tools;
mod ui;
use std::collections::HashMap;
use std::path::Path;
use clap::Parser;
use agent::Agent;
use config::Config;
use memory::store::MemoryStore;
#[derive(Parser)]
#[command(
name = "cortex",
version = "0.2.0",
about = "Self-learning AI agent with persistent memory, tools, and a beautiful terminal UI",
after_help = "Examples:\n cortex # interactive with memory\n cortex --one-shot \"Remember I use Vim\"\n cortex --config ./config.yaml\n cortex --no-memory"
)]
struct Cli {
#[arg(long, value_name = "QUESTION")]
one_shot: Option<String>,
#[arg(long)]
config: Option<String>,
#[arg(short, long)]
verbose: bool,
#[arg(long)]
no_memory: bool,
}
#[tokio::main]
async fn main() -> anyhow::Result<()> {
let args = Cli::parse();
let config_path = if let Some(ref p) = args.config {
Some(p.clone())
} else {
let candidates = [
Path::new("config.yaml").to_path_buf(),
dirs::home_dir().map(|h| h.join(".cortex/config.yaml")).unwrap_or_default(),
];
candidates.iter().find(|p| p.exists()).map(|p| p.to_string_lossy().to_string())
};
let mut cfg = match config_path {
Some(ref path) => Config::from_yaml(path).unwrap_or_default(),
None => Config::default(),
};
if args.verbose { cfg.verbose = true; }
if args.no_memory { cfg.memory_enabled = false; }
let session_state = session::load_session_state();
if !session_state.last_provider.is_empty() && cfg.providers.contains_key(&session_state.last_provider) {
cfg.active_provider = session_state.last_provider.clone();
cfg.active_model = session_state.last_model.clone();
if cfg.verbose {
eprintln!("[Session: resuming {} / {}]", cfg.active_provider, cfg.active_model);
}
}
let (provider_name, api_key, base_url) = cfg.get_active_provider_config();
let model = if cfg.active_model.is_empty() { &cfg.model } else { &cfg.active_model };
if api_key.is_empty() {
eprintln!("Error: No API key configured for provider '{}'.", provider_name);
eprintln!("Set it in config.yaml or via the corresponding environment variable.");
std::process::exit(1);
}
if cfg.verbose {
let (ok, msg) = session::health_check(&base_url, &api_key).await;
if ok {
eprintln!("[Health: {} - {}]", provider_name, msg);
} else {
eprintln!("[Health: {} - {}]", provider_name, msg);
}
}
let provider = providers::openai_compat::create_provider("openai", model, &api_key, Some(&base_url))
.map_err(|e| anyhow::anyhow!("Failed to create provider: {}", e))?;
let memory_store = if cfg.memory_enabled {
let db_dir = cfg.memory_dir_resolved();
let db_name = cfg.memory_db_resolved();
match MemoryStore::new(&db_dir, &db_name) {
Ok(store) => {
if cfg.verbose { eprintln!("[Memory store: {}]", store.db_path); }
Some(store)
}
Err(e) => { eprintln!("Warning: Failed to initialize memory store: {}", e); None }
}
} else { None };
let memory_arc = memory_store.as_ref().map(|_| {
std::sync::Arc::new(std::sync::Mutex::new(
MemoryStore::new(&cfg.memory_dir_resolved(), &cfg.memory_db_resolved()).unwrap()
))
});
let tools = tools::default_tools(memory_arc);
if cfg.verbose {
let tool_names: Vec<&str> = tools.iter().map(|t| t.name.as_str()).collect();
eprintln!("Config: active_provider={}, model={}, memory_enabled={}", cfg.active_provider, cfg.active_model, cfg.memory_enabled);
eprintln!("Tools ({}): {:?}", tool_names.len(), tool_names);
}
let provider_map: HashMap<String, serde_json::Value> = cfg.providers.iter().map(|(k, v)| {
(k.clone(), serde_json::json!({ "api_key": v.api_key, "base_url": v.base_url }))
}).collect();
let provider_names = cfg.get_provider_names();
let mut agent = Agent::new(
provider,
tools,
memory_store,
&cfg.system_prompt,
cfg.max_iterations,
cfg.max_tokens,
cfg.temperature,
cfg.verbose,
provider_map,
provider_names,
cfg.active_provider.clone(),
cfg.active_model.clone(),
);
if let Some(question) = args.one_shot {
let response = agent.run(&question, true).await;
println!("{}", response);
} else {
agent.chat().await;
}
Ok(())
}