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;
const FIRST_RUN_CONFIG_TEMPLATE: &str = r##"# Cortex Agent — configuration
# ${ENV_VAR} references are resolved from the environment at runtime.
# --- Active provider & model ---
active_provider: openai # Change this to your preferred provider
active_model: gpt-4o # Set the model you want to use
# --- Providers (OpenAI-compatible API) ---
# Each entry needs an api_key and base_url. Keys can be inline or ${ENV_VAR}.
providers:
openai:
api_key: ${OPENAI_API_KEY}
base_url: https://api.openai.com/v1
openrouter:
api_key: ${OPENROUTER_API_KEY}
base_url: https://openrouter.ai/api/v1
anthropic:
api_key: ${ANTHROPIC_API_KEY}
base_url: https://api.anthropic.com/v1
deepseek:
api_key: ${DEEPSEEK_API_KEY}
base_url: https://api.deepseek.com/v1
groq:
api_key: ${GROQ_API_KEY}
base_url: https://api.groq.com/openai/v1
together:
api_key: ${TOGETHER_API_KEY}
base_url: https://api.together.xyz/v1
mistral:
api_key: ${MISTRAL_API_KEY}
base_url: https://api.mistral.ai/v1
# --- Agent Behaviour ---
system_prompt: >
You are Cortex, an autonomous agent. Answer from your own knowledge first.
Only use tools when genuinely needed.
CONCISENESS:
- Be BRIEF. Short answers, no fluff.
- Don't introduce yourself unless asked.
- Let the user drive the conversation.
max_tokens: 4096
max_iterations: 10
temperature: 0.7
# --- Memory ---
memory_enabled: true
memory_dir: ~/.cortex/memory
memory_db: cortex.db
# --- Diagnostics ---
verbose: false
"##;
fn first_run_setup() {
let home = std::env::var("HOME").unwrap_or_else(|_| ".".into());
let config_dir = Path::new(&home).join(".cortex");
let config_path = config_dir.join("config.yaml");
if let Err(e) = std::fs::create_dir_all(&config_dir) {
eprintln!("Error: Could not create config directory ~/.cortex: {}", e);
std::process::exit(1);
}
if let Err(e) = std::fs::write(&config_path, FIRST_RUN_CONFIG_TEMPLATE) {
eprintln!("Error: Could not write config file: {}", e);
std::process::exit(1);
}
eprintln!(
"\
╭─────────────────────────────────────────────────────────────╮
│ Welcome to Cortex! │
│ │
│ A template config has been created at: │
│ ~/.cortex/config.yaml │
│ │
│ Next steps: │
│ 1. Set your API keys via environment variables or edit │
│ the config file directly. │
│ 2. Run 'cortex' again to start chatting. │
│ │
│ Supported providers (set the corresponding env var): │
│ openai → OPENAI_API_KEY │
│ openrouter → OPENROUTER_API_KEY │
│ anthropic → ANTHROPIC_API_KEY │
│ deepseek → DEEPSEEK_API_KEY │
│ groq → GROQ_API_KEY │
│ together → TOGETHER_API_KEY │
│ mistral → MISTRAL_API_KEY │
│ │
│ Example: │
│ export OPENAI_API_KEY=\"sk-...\" │
│ cortex │
╰─────────────────────────────────────────────────────────────╯"
);
}
#[derive(Parser)]
#[command(
name = "cortex",
version = "0.2.1",
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())
};
if config_path.is_none() {
first_run_setup();
return Ok(());
}
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 '{}'.\n\
Set it in config.yaml (~/.cortex/config.yaml) or set the corresponding\n\
environment variable (e.g. OPENAI_API_KEY for the 'openai' provider).\n\
\n\
Available providers: {}",
provider_name,
cfg.get_provider_names().join(", ")
);
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(())
}