use anyhow::Result;
use clap::{Parser, Subcommand};
use cortex_mem_config::Config;
use cortex_mem_core::llm::LLMClientImpl;
use cortex_mem_tools::MemoryOperations;
use std::path::PathBuf;
use std::sync::Arc;
mod commands;
use commands::{add, delete, get, layers, list, search, session, stats, tenant, vector};
#[derive(Parser)]
#[command(author, version, about, long_about = None)]
#[command(propagate_version = true)]
struct Cli {
#[arg(short, long, default_value = "config.toml")]
config: PathBuf,
#[arg(long, default_value = "default")]
tenant: String,
#[arg(short, long)]
verbose: bool,
#[command(subcommand)]
command: Commands,
}
#[derive(Subcommand)]
enum Commands {
Add {
#[arg(short, long)]
thread: String,
#[arg(short, long, default_value = "user")]
role: String,
content: String,
},
Search {
query: String,
#[arg(short, long)]
thread: Option<String>,
#[arg(short = 'n', long, default_value = "10")]
limit: usize,
#[arg(short = 's', long, default_value = "0.4")]
min_score: f32,
#[arg(long, default_value = "session")]
scope: String,
},
List {
#[arg(short, long)]
uri: Option<String>,
#[arg(long)]
include_abstracts: bool,
},
Get {
uri: String,
#[arg(short, long)]
abstract_only: bool,
#[arg(short, long)]
overview: bool,
},
Delete {
uri: String,
},
Session {
#[command(subcommand)]
action: SessionAction,
},
Stats,
Layers {
#[command(subcommand)]
action: LayersAction,
},
Vector {
#[command(subcommand)]
action: VectorAction,
},
Tenant {
#[command(subcommand)]
action: TenantAction,
},
}
#[derive(Subcommand)]
enum SessionAction {
List,
Create {
thread: String,
#[arg(short, long)]
title: Option<String>,
},
Close {
thread: String,
},
}
#[derive(Subcommand)]
enum LayersAction {
EnsureAll,
Status,
RegenerateOversized,
}
#[derive(Subcommand)]
enum VectorAction {
Status,
Reindex,
Prune {
#[arg(long)]
dry_run: bool,
},
}
#[derive(Subcommand)]
enum TenantAction {
List,
}
#[tokio::main]
async fn main() -> Result<()> {
let cli = Cli::parse();
if cli.verbose {
tracing_subscriber::fmt()
.with_max_level(tracing::Level::DEBUG)
.init();
}
let config = Config::load(&cli.config).map_err(|e| {
anyhow::anyhow!(
"Failed to load config from {}: {}. \
Please ensure config.toml exists with [llm], [qdrant], and [embedding] sections.",
cli.config.display(),
e
)
})?;
let config_dir = cli.config.parent().map(|p| p.to_path_buf()).unwrap_or_default();
let data_dir = if let Some(ref dir) = config.cortex.data_dir {
let dir_path = std::path::Path::new(dir);
if dir_path.is_relative() {
config_dir.join(dir_path).to_string_lossy().to_string()
} else {
dir.clone()
}
} else if let Ok(env_dir) = std::env::var("CORTEX_DATA_DIR") {
env_dir
} else {
config_dir.to_string_lossy().to_string()
};
if let Commands::Tenant { action } = cli.command {
match action {
TenantAction::List => {
tenant::list(&data_dir).await?;
}
}
return Ok(());
}
let model_name = config.llm.model_efficient.clone();
let llm_config = cortex_mem_core::llm::LLMConfig {
api_base_url: config.llm.api_base_url,
api_key: config.llm.api_key,
model_efficient: config.llm.model_efficient,
temperature: config.llm.temperature,
max_tokens: config.llm.max_tokens as usize,
};
let llm_client = Arc::new(LLMClientImpl::new(llm_config)?);
let operations = MemoryOperations::new(
&data_dir,
&cli.tenant,
llm_client,
&config.qdrant.url,
&config.qdrant.collection_name,
config.qdrant.api_key.as_deref(),
&config.embedding.api_base_url,
&config.embedding.api_key,
&config.embedding.model_name,
config.qdrant.embedding_dim,
None, config.cortex.enable_intent_analysis,
)
.await?;
if cli.verbose {
eprintln!("LLM model: {}", model_name);
eprintln!("Data directory: {}", data_dir);
eprintln!("Tenant: {}", cli.tenant);
}
let operations = Arc::new(operations);
match cli.command {
Commands::Add {
thread,
role,
content,
} => {
add::execute(operations, &thread, &role, &content).await?;
}
Commands::Search {
query,
thread,
limit,
min_score,
scope,
} => {
search::execute(
operations,
&query,
thread.as_deref(),
limit,
min_score,
&scope,
)
.await?;
}
Commands::List {
uri,
include_abstracts,
} => {
list::execute(operations, uri.as_deref(), include_abstracts).await?;
}
Commands::Get { uri, abstract_only, overview } => {
get::execute(operations, &uri, abstract_only, overview).await?;
}
Commands::Delete { uri } => {
delete::execute(operations, &uri).await?;
}
Commands::Session { action } => match action {
SessionAction::List => {
session::list(operations).await?;
}
SessionAction::Create { thread, title } => {
session::create(operations, &thread, title.as_deref()).await?;
}
SessionAction::Close { thread } => {
session::close(operations, &thread).await?;
}
},
Commands::Stats => {
stats::execute(operations).await?;
}
Commands::Layers { action } => match action {
LayersAction::EnsureAll => {
layers::ensure_all(operations).await?;
}
LayersAction::Status => {
layers::status(operations).await?;
}
LayersAction::RegenerateOversized => {
layers::regenerate_oversized(operations).await?;
}
},
Commands::Vector { action } => match action {
VectorAction::Status => {
vector::status(operations).await?;
}
VectorAction::Reindex => {
vector::reindex(operations).await?;
}
VectorAction::Prune { dry_run } => {
vector::prune(operations, dry_run).await?;
}
},
Commands::Tenant { .. } => {
}
}
Ok(())
}