mod api; mod config; mod errors; mod output; mod ui; use anyhow::{Context, Result}; use api::{ApiClient, BatchMemoryRequest, MemoryItem, SearchRequest, ThinkRequest}; use clap::{Parser, Subcommand, ValueEnum}; use config::Config; use output::OutputFormat; use std::fs; use std::path::PathBuf; use walkdir::WalkDir; #[derive(Debug, Clone, Copy, ValueEnum)] enum Format { Pretty, Json, Yaml, } impl From for OutputFormat { fn from(f: Format) -> Self { match f { Format::Pretty => OutputFormat::Pretty, Format::Json => OutputFormat::Json, Format::Yaml => OutputFormat::Yaml, } } } #[derive(Parser)] #[command(name = "memora")] #[command(about = "Memora CLI - Semantic memory system", long_about = None)] #[command(version)] struct Cli { /// Output format (pretty, json, yaml) #[arg(short = 'o', long, global = true, default_value = "pretty")] output: Format, /// Show verbose output including full requests and responses #[arg(short = 'v', long, global = true)] verbose: bool, #[command(subcommand)] command: Commands, } #[derive(Subcommand)] enum Commands { /// Manage agents (list, profile, stats) #[command(subcommand)] Agent(AgentCommands), /// Manage memories (search, think, put, delete) #[command(subcommand)] Memory(MemoryCommands), /// Manage documents (list, get, delete) #[command(subcommand)] Document(DocumentCommands), /// Manage async operations (list, cancel) #[command(subcommand)] Operation(OperationCommands), } #[derive(Subcommand)] enum AgentCommands { /// List all agents List, /// Get agent profile (personality + background) Profile { /// Agent ID agent_id: String, }, /// Get memory statistics for an agent Stats { /// Agent ID agent_id: String, }, /// Update agent personality traits SetPersonality { /// Agent ID agent_id: String, /// Openness to experience (0.0-1.0) #[arg(long)] openness: f32, /// Conscientiousness (0.0-1.0) #[arg(long)] conscientiousness: f32, /// Extraversion (0.0-1.0) #[arg(long)] extraversion: f32, /// Agreeableness (0.0-1.0) #[arg(long)] agreeableness: f32, /// Neuroticism (0.0-1.0) #[arg(long)] neuroticism: f32, /// Bias strength (0.0-1.0) #[arg(long)] bias_strength: f32, }, /// Set or merge agent background SetBackground { /// Agent ID agent_id: String, /// Background content content: String, /// Skip automatic personality inference #[arg(long)] no_update_personality: bool, }, } #[derive(Subcommand)] enum MemoryCommands { /// Search for memories using semantic search Search { /// Agent ID agent_id: String, /// Search query query: String, /// Fact types to search (world, agent, opinion) #[arg(short = 't', long, value_delimiter = ',', default_values = &["world", "agent", "opinion"])] fact_type: Vec, /// Thinking budget #[arg(short = 'b', long, default_value = "100")] budget: i32, /// Maximum tokens for results #[arg(long, default_value = "4096")] max_tokens: i32, /// Show trace information #[arg(long)] trace: bool, }, /// Generate answers using agent identity Think { /// Agent ID agent_id: String, /// Query to think about query: String, /// Thinking budget #[arg(short = 'b', long, default_value = "50")] budget: i32, /// Additional context #[arg(short = 'c', long)] context: Option, }, /// Store a single memory Put { /// Agent ID agent_id: String, /// Memory content content: String, /// Document ID (auto-generated if not provided) #[arg(short = 'd', long)] doc_id: Option, /// Context for the memory #[arg(short = 'c', long)] context: Option, /// Queue for background processing #[arg(long)] r#async: bool, }, /// Bulk import memories from files PutFiles { /// Agent ID agent_id: String, /// Path to file or directory path: PathBuf, /// Search directories recursively #[arg(short = 'r', long, default_value = "true")] recursive: bool, /// Context for all memories #[arg(short = 'c', long)] context: Option, /// Queue for background processing #[arg(long)] r#async: bool, }, /// Delete a memory unit Delete { /// Agent ID agent_id: String, /// Memory unit ID unit_id: String, }, } #[derive(Subcommand)] enum DocumentCommands { /// List documents for an agent List { /// Agent ID agent_id: String, /// Search query to filter documents #[arg(short = 'q', long)] query: Option, /// Maximum number of results #[arg(short = 'l', long, default_value = "100")] limit: i32, /// Offset for pagination #[arg(short = 's', long, default_value = "0")] offset: i32, }, /// Get a specific document by ID Get { /// Agent ID agent_id: String, /// Document ID document_id: String, }, /// Delete a document and all its memory units Delete { /// Agent ID agent_id: String, /// Document ID document_id: String, }, } #[derive(Subcommand)] enum OperationCommands { /// List async operations for an agent List { /// Agent ID agent_id: String, }, /// Cancel a pending async operation Cancel { /// Agent ID agent_id: String, /// Operation ID operation_id: String, }, } fn main() { if let Err(e) = run() { std::process::exit(1); } } fn run() -> Result<()> { let cli = Cli::parse(); let output_format: OutputFormat = cli.output.into(); let verbose = cli.verbose; // Load configuration let config = Config::from_env().unwrap_or_else(|e| { ui::print_error(&format!("Configuration error: {}", e)); errors::print_config_help(); std::process::exit(1); }); let api_url = config.api_url().to_string(); // Create API client let client = ApiClient::new(api_url.clone()).unwrap_or_else(|e| { errors::handle_api_error(e, &api_url); }); // Execute command and handle errors let result: Result<()> = match cli.command { Commands::Agent(agent_cmd) => match agent_cmd { AgentCommands::List => { let spinner = if output_format == OutputFormat::Pretty { Some(ui::create_spinner("Fetching agents...")) } else { None }; let response = client.list_agents(verbose); if let Some(sp) = spinner { sp.finish_and_clear(); } match response { Ok(agents_list) => { if output_format == OutputFormat::Pretty { if agents_list.agents.is_empty() { ui::print_warning("No agents found"); } else { ui::print_info(&format!("Found {} agent(s)", agents_list.agents.len())); for agent in &agents_list.agents { println!(" - {}", agent); } } } else { output::print_output(&agents_list, output_format)?; } Ok(()) } Err(e) => Err(e) } } AgentCommands::Profile { agent_id } => { let spinner = if output_format == OutputFormat::Pretty { Some(ui::create_spinner("Fetching profile...")) } else { None }; let response = client.get_profile(&agent_id, verbose); if let Some(sp) = spinner { sp.finish_and_clear(); } match response { Ok(profile) => { if output_format == OutputFormat::Pretty { ui::print_info(&format!("Profile for agent '{}'", agent_id)); if let Some(personality) = &profile.personality { println!("\n Personality Traits:"); println!(" Openness: {:.2}", personality.openness); println!(" Conscientiousness: {:.2}", personality.conscientiousness); println!(" Extraversion: {:.2}", personality.extraversion); println!(" Agreeableness: {:.2}", personality.agreeableness); println!(" Neuroticism: {:.2}", personality.neuroticism); println!(" Bias Strength: {:.2}", personality.bias_strength); } if !profile.background.is_empty() { println!("\n Background:\n{}", profile.background); } } else { output::print_output(&profile, output_format)?; } Ok(()) } Err(e) => Err(e) } } AgentCommands::Stats { agent_id } => { let spinner = if output_format == OutputFormat::Pretty { Some(ui::create_spinner("Fetching statistics...")) } else { None }; let response = client.get_stats(&agent_id, verbose); if let Some(sp) = spinner { sp.finish_and_clear(); } match response { Ok(stats) => { if output_format == OutputFormat::Pretty { ui::print_info(&format!("Statistics for agent '{}'", agent_id)); println!(); println!(" 📊 Overview"); println!(" Total Memory Units: {}", stats.total_nodes); println!(" Total Links: {}", stats.total_links); println!(" Total Documents: {}", stats.total_documents); println!(); println!(" 🧠 Memory Units by Type"); let mut fact_types: Vec<_> = stats.nodes_by_fact_type.iter().collect(); fact_types.sort_by_key(|(k, _)| *k); for (fact_type, count) in fact_types { let icon = match fact_type.as_str() { "world" => "🌍", "agent" => "🤖", "opinion" => "💭", _ => "•" }; println!(" {} {:<10} {}", icon, fact_type, count); } println!(); println!(" 🔗 Links by Type"); let mut link_types: Vec<_> = stats.links_by_link_type.iter().collect(); link_types.sort_by_key(|(k, _)| *k); for (link_type, count) in link_types { let icon = match link_type.as_str() { "temporal" => "⏰", "semantic" => "🔤", "entity" => "🏷️", _ => "•" }; println!(" {} {:<10} {}", icon, link_type, count); } println!(); println!(" 🔗 Links by Fact Type"); let mut fact_type_links: Vec<_> = stats.links_by_fact_type.iter().collect(); fact_type_links.sort_by_key(|(k, _)| *k); for (fact_type, count) in fact_type_links { let icon = match fact_type.as_str() { "world" => "🌍", "agent" => "🤖", "opinion" => "💭", _ => "•" }; println!(" {} {:<10} {}", icon, fact_type, count); } println!(); if !stats.links_breakdown.is_empty() { println!(" 📈 Detailed Link Breakdown"); let mut fact_types: Vec<_> = stats.links_breakdown.iter().collect(); fact_types.sort_by_key(|(k, _)| *k); for (fact_type, link_types) in fact_types { let icon = match fact_type.as_str() { "world" => "🌍", "agent" => "🤖", "opinion" => "💭", _ => "•" }; println!(" {} {}", icon, fact_type); let mut sorted_links: Vec<_> = link_types.iter().collect(); sorted_links.sort_by_key(|(k, _)| *k); for (link_type, count) in sorted_links { println!(" - {:<10} {}", link_type, count); } } println!(); } if stats.pending_operations > 0 || stats.failed_operations > 0 { println!(" ⚙️ Operations"); if stats.pending_operations > 0 { println!(" ⏳ Pending: {}", stats.pending_operations); } if stats.failed_operations > 0 { println!(" ❌ Failed: {}", stats.failed_operations); } } } else { output::print_output(&stats, output_format)?; } Ok(()) } Err(e) => Err(e) } } AgentCommands::SetPersonality { agent_id, openness, conscientiousness, extraversion, agreeableness, neuroticism, bias_strength, } => { let spinner = if output_format == OutputFormat::Pretty { Some(ui::create_spinner("Updating personality...")) } else { None }; let response = client.update_personality( &agent_id, openness, conscientiousness, extraversion, agreeableness, neuroticism, bias_strength, verbose, ); if let Some(sp) = spinner { sp.finish_and_clear(); } match response { Ok(profile) => { if output_format == OutputFormat::Pretty { ui::print_success("Personality updated successfully"); let p = &profile.personality; { println!(" Openness: {:.2}", p.openness); println!(" Conscientiousness: {:.2}", p.conscientiousness); println!(" Extraversion: {:.2}", p.extraversion); println!(" Agreeableness: {:.2}", p.agreeableness); println!(" Neuroticism: {:.2}", p.neuroticism); println!(" Bias Strength: {:.2}", p.bias_strength); } } else { output::print_output(&profile, output_format)?; } Ok(()) } Err(e) => Err(e) } } AgentCommands::SetBackground { agent_id, content, no_update_personality, } => { let current_profile = if !no_update_personality { client.get_profile(&agent_id, verbose).ok() } else { None }; let spinner = if output_format == OutputFormat::Pretty { Some(ui::create_spinner("Merging background...")) } else { None }; let response = client.add_background( &agent_id, &content, !no_update_personality, verbose, ); if let Some(sp) = spinner { sp.finish_and_clear(); } match response { Ok(profile) => { if output_format == OutputFormat::Pretty { ui::print_success("Background updated successfully"); println!("\n{}", profile.background); if !no_update_personality { if let (Some(old_p), Some(new_p)) = (current_profile.as_ref().map(|p| p.personality), &profile.personality) { println!("\nPersonality changes:"); println!(" Openness: {:.2} → {:.2}", old_p.openness, new_p.openness); println!(" Conscientiousness: {:.2} → {:.2}", old_p.conscientiousness, new_p.conscientiousness); println!(" Extraversion: {:.2} → {:.2}", old_p.extraversion, new_p.extraversion); println!(" Agreeableness: {:.2} → {:.2}", old_p.agreeableness, new_p.agreeableness); println!(" Neuroticism: {:.2} → {:.2}", old_p.neuroticism, new_p.neuroticism); } } } else { output::print_output(&profile, output_format)?; } Ok(()) } Err(e) => Err(e) } } }, Commands::Memory(memory_cmd) => match memory_cmd { MemoryCommands::Search { agent_id, query, fact_type, budget, max_tokens, trace, } => { let spinner = if output_format == OutputFormat::Pretty { Some(ui::create_spinner("Searching memories...")) } else { None }; let request = SearchRequest { query, fact_type, thinking_budget: budget, max_tokens, trace, }; let response = client.search(&agent_id, request, verbose); if let Some(sp) = spinner { sp.finish_and_clear(); } match response { Ok(result) => { output::print_output(&result, output_format)?; Ok(()) } Err(e) => Err(e) } } MemoryCommands::Think { agent_id, query, budget, context, } => { let spinner = if output_format == OutputFormat::Pretty { Some(ui::create_spinner("Thinking...")) } else { None }; let request = ThinkRequest { query, thinking_budget: budget, context, }; let response = client.think(&agent_id, request, verbose); if let Some(sp) = spinner { sp.finish_and_clear(); } match response { Ok(result) => { output::print_output(&result, output_format)?; Ok(()) } Err(e) => Err(e) } } MemoryCommands::Put { agent_id, content, doc_id, context, r#async, } => { let doc_id = doc_id.unwrap_or_else(config::generate_doc_id); let spinner = if output_format == OutputFormat::Pretty { Some(ui::create_spinner("Storing memory...")) } else { None }; let item = MemoryItem { content: content.clone(), context, }; let request = BatchMemoryRequest { items: vec![item], document_id: Some(doc_id.clone()), }; let response = client.put_memories(&agent_id, request, r#async, verbose); if let Some(sp) = spinner { sp.finish_and_clear(); } match response { Ok(result) => { if output_format == OutputFormat::Pretty { ui::print_success(&format!( "Memory stored successfully (document: {})", doc_id )); if let Some(op_id) = result.job_id { println!(" Operation ID: {}", op_id); println!(" Status: queued for background processing"); } else { println!(" Stored count: {}", result.stored_count.unwrap_or(0)); } } else { output::print_output(&result, output_format)?; } Ok(()) } Err(e) => Err(e) } } MemoryCommands::PutFiles { agent_id, path, recursive, context, r#async, } => { if !path.exists() { anyhow::bail!("Path does not exist: {}", path.display()); } let mut files = Vec::new(); if path.is_file() { files.push(path); } else if path.is_dir() { if recursive { for entry in WalkDir::new(&path) .into_iter() .filter_map(|e| e.ok()) .filter(|e| e.file_type().is_file()) { let path = entry.path(); if let Some(ext) = path.extension() { if ext == "txt" || ext == "md" { files.push(path.to_path_buf()); } } } } else { for entry in fs::read_dir(&path)? { let entry = entry?; let path = entry.path(); if path.is_file() { if let Some(ext) = path.extension() { if ext == "txt" || ext == "md" { files.push(path); } } } } } } if files.is_empty() { ui::print_warning("No .txt or .md files found"); return Ok(()); } ui::print_info(&format!("Found {} files to import", files.len())); let pb = ui::create_progress_bar(files.len() as u64, "Processing files"); let mut items = Vec::new(); let mut document_id = None; for file_path in &files { let content = fs::read_to_string(file_path) .with_context(|| format!("Failed to read file: {}", file_path.display()))?; let doc_id = file_path .file_stem() .and_then(|s| s.to_str()) .map(|s| s.to_string()) .unwrap_or_else(config::generate_doc_id); if document_id.is_none() { document_id = Some(doc_id); } items.push(MemoryItem { content, context: context.clone(), }); pb.inc(1); } pb.finish_with_message("Files processed"); let spinner = if output_format == OutputFormat::Pretty { Some(ui::create_spinner("Uploading memories...")) } else { None }; let request = BatchMemoryRequest { items, document_id, }; let response = client.put_memories(&agent_id, request, r#async, verbose); if let Some(sp) = spinner { sp.finish_and_clear(); } match response { Ok(result) => { if output_format == OutputFormat::Pretty { ui::print_success("Files imported successfully"); if let Some(op_id) = result.job_id { println!(" Operation ID: {}", op_id); println!(" Status: queued for background processing"); } else { println!(" Total units created: {}", result.stored_count.unwrap_or(0)); } } else { output::print_output(&result, output_format)?; } Ok(()) } Err(e) => Err(e) } } MemoryCommands::Delete { agent_id, unit_id } => { let spinner = if output_format == OutputFormat::Pretty { Some(ui::create_spinner("Deleting memory unit...")) } else { None }; let response = client.delete_memory(&agent_id, &unit_id, verbose); if let Some(sp) = spinner { sp.finish_and_clear(); } match response { Ok(result) => { if output_format == OutputFormat::Pretty { if result.success { ui::print_success(&result.message); } else { ui::print_error(&result.message); } } else { output::print_output(&result, output_format)?; } Ok(()) } Err(e) => Err(e) } } }, Commands::Document(doc_cmd) => match doc_cmd { DocumentCommands::List { agent_id, query, limit, offset, } => { let spinner = if output_format == OutputFormat::Pretty { Some(ui::create_spinner("Fetching documents...")) } else { None }; let response = client.list_documents(&agent_id, query.as_deref(), Some(limit), Some(offset), verbose); if let Some(sp) = spinner { sp.finish_and_clear(); } match response { Ok(docs_response) => { if output_format == OutputFormat::Pretty { ui::print_info(&format!("Documents for agent '{}' (total: {})", agent_id, docs_response.total)); for doc in &docs_response.items { println!("\n Document ID: {}", doc.id); println!(" Created: {}", doc.created_at); println!(" Updated: {}", doc.updated_at); println!(" Text Length: {}", doc.text_length); println!(" Memory Units: {}", doc.memory_unit_count); } } else { output::print_output(&docs_response, output_format)?; } Ok(()) } Err(e) => Err(e) } } DocumentCommands::Get { agent_id, document_id } => { let spinner = if output_format == OutputFormat::Pretty { Some(ui::create_spinner("Fetching document...")) } else { None }; let response = client.get_document(&agent_id, &document_id, verbose); if let Some(sp) = spinner { sp.finish_and_clear(); } match response { Ok(doc) => { if output_format == OutputFormat::Pretty { ui::print_info(&format!("Document: {}", doc.id)); println!(" Agent ID: {}", doc.agent_id); println!(" Created: {}", doc.created_at); println!(" Updated: {}", doc.updated_at); println!(" Memory Units: {}", doc.memory_unit_count); println!("\n Text:\n{}", doc.original_text); } else { output::print_output(&doc, output_format)?; } Ok(()) } Err(e) => Err(e) } } DocumentCommands::Delete { agent_id, document_id } => { let spinner = if output_format == OutputFormat::Pretty { Some(ui::create_spinner("Deleting document...")) } else { None }; let response = client.delete_document(&agent_id, &document_id, verbose); if let Some(sp) = spinner { sp.finish_and_clear(); } match response { Ok(result) => { if output_format == OutputFormat::Pretty { if result.success { ui::print_success(&result.message); } else { ui::print_error(&result.message); } } else { output::print_output(&result, output_format)?; } Ok(()) } Err(e) => Err(e) } } }, Commands::Operation(op_cmd) => match op_cmd { OperationCommands::List { agent_id } => { let spinner = if output_format == OutputFormat::Pretty { Some(ui::create_spinner("Fetching operations...")) } else { None }; let response = client.list_operations(&agent_id, verbose); if let Some(sp) = spinner { sp.finish_and_clear(); } match response { Ok(ops_response) => { if output_format == OutputFormat::Pretty { if ops_response.operations.is_empty() { ui::print_info("No operations found"); } else { ui::print_info(&format!("Found {} operation(s)", ops_response.operations.len())); for op in &ops_response.operations { println!("\n Operation ID: {}", op.id); println!(" Type: {}", op.task_type); println!(" Status: {}", op.status); println!(" Items: {}", op.items_count); if let Some(doc_id) = &op.document_id { println!(" Document ID: {}", doc_id); } } } } else { output::print_output(&ops_response, output_format)?; } Ok(()) } Err(e) => Err(e) } } OperationCommands::Cancel { agent_id, operation_id } => { let spinner = if output_format == OutputFormat::Pretty { Some(ui::create_spinner("Cancelling operation...")) } else { None }; let response = client.cancel_operation(&agent_id, &operation_id, verbose); if let Some(sp) = spinner { sp.finish_and_clear(); } match response { Ok(result) => { if output_format == OutputFormat::Pretty { if result.success { ui::print_success(&result.message); } else { ui::print_error(&result.message); } } else { output::print_output(&result, output_format)?; } Ok(()) } Err(e) => Err(e) } } }, }; // Handle API errors with nice messages if let Err(e) = result { errors::handle_api_error(e, &api_url); } Ok(()) }