mod api; mod commands; mod config; mod errors; mod output; mod ui; mod utils; use anyhow::Result; use api::ApiClient; use clap::{Parser, Subcommand, ValueEnum}; use config::Config; use output::OutputFormat; use std::path::PathBuf; #[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 = "hindsight")] #[command(about = "Hindsight CLI - Semantic memory system", long_about = None)] #[command(version)] #[command(before_help = get_before_help())] #[command(after_help = get_after_help())] 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, } fn get_after_help() -> String { let config = config::Config::load().ok(); let (api_url, source) = match &config { Some(c) => (c.api_url.as_str(), c.source.to_string()), None => ("http://localhost:8888", "default".to_string()), }; format!( "Current API URL: {} (from {})\n\nRun 'hindsight configure' to change the API URL.", api_url, source ) } fn get_before_help() -> &'static str { ui::get_logo() } #[derive(Subcommand)] enum Commands { /// Manage banks (list, create, update, profile, stats, mission, graph, delete) #[command(subcommand)] Bank(BankCommands), /// Manage memories (list, get, recall, reflect, retain, clear) #[command(subcommand)] Memory(MemoryCommands), /// Manage documents (list, get, delete) #[command(subcommand)] Document(DocumentCommands), /// Manage entities (list, get, regenerate) #[command(subcommand)] Entity(EntityCommands), /// Manage tags (list) #[command(subcommand)] Tag(TagCommands), /// Manage chunks (get) #[command(subcommand)] Chunk(ChunkCommands), /// Manage async operations (list, get, cancel) #[command(subcommand)] Operation(OperationCommands), /// Manage mental models (user-curated summaries) #[command(subcommand)] MentalModel(MentalModelCommands), /// Manage directives (behavioral rules) #[command(subcommand)] Directive(DirectiveCommands), /// Check API health status Health, /// Get Prometheus metrics Metrics, /// Get API version information Version, /// Interactive TUI explorer (k9s-style) for navigating banks, memories, entities, and performing recall/reflect #[command(alias = "tui")] Explore, /// Launch the web-based control plane UI Ui, /// Configure the CLI (API URL, API key, etc.) #[command(after_help = "Configuration priority:\n 1. Environment variables (HINDSIGHT_API_URL, HINDSIGHT_API_KEY) - highest priority\n 2. Config file (~/.hindsight/config)\n 3. Default (http://localhost:8888)")] Configure { /// API URL to connect to (interactive prompt if not provided) #[arg(long)] api_url: Option, /// API key for authentication (sent as Bearer token) #[arg(long)] api_key: Option, }, } #[derive(Subcommand)] enum BankCommands { /// List all banks List, /// Create a new bank Create { /// Bank ID bank_id: String, /// Bank name #[arg(short = 'n', long)] name: Option, /// Mission statement #[arg(short = 'm', long)] mission: Option, /// Skepticism trait (1-5) #[arg(long, value_parser = clap::value_parser!(i64).range(1..=5))] skepticism: Option, /// Literalism trait (1-5) #[arg(long, value_parser = clap::value_parser!(i64).range(1..=5))] literalism: Option, /// Empathy trait (1-5) #[arg(long, value_parser = clap::value_parser!(i64).range(1..=5))] empathy: Option, }, /// Update bank properties (partial update) Update { /// Bank ID bank_id: String, /// Bank name #[arg(short = 'n', long)] name: Option, /// Mission statement #[arg(short = 'm', long)] mission: Option, /// Skepticism trait (1-5) #[arg(long, value_parser = clap::value_parser!(i64).range(1..=5))] skepticism: Option, /// Literalism trait (1-5) #[arg(long, value_parser = clap::value_parser!(i64).range(1..=5))] literalism: Option, /// Empathy trait (1-5) #[arg(long, value_parser = clap::value_parser!(i64).range(1..=5))] empathy: Option, }, /// Get bank disposition and profile Disposition { /// Bank ID bank_id: String, }, /// Get memory statistics for a bank Stats { /// Bank ID bank_id: String, }, /// Set bank name Name { /// Bank ID bank_id: String, /// Bank name name: String, }, /// Set bank mission Mission { /// Bank ID bank_id: String, /// Mission statement mission: String, }, /// Set or merge bank background (deprecated: use mission instead) #[command(hide = true)] Background { /// Bank ID bank_id: String, /// Background content content: String, /// Skip automatic disposition inference #[arg(long)] no_update_disposition: bool, }, /// Get memory graph data Graph { /// Bank ID bank_id: String, /// Filter by fact type (world, experience, opinion) #[arg(short = 't', long)] fact_type: Option, /// Maximum nodes to return #[arg(short = 'l', long, default_value = "1000")] limit: i64, }, /// Delete a bank and all its data Delete { /// Bank ID bank_id: String, /// Skip confirmation prompt #[arg(short = 'y', long)] yes: bool, }, /// Trigger consolidation to create/update observations Consolidate { /// Bank ID bank_id: String, /// Wait for consolidation to complete (poll for status) #[arg(long)] wait: bool, /// Poll interval in seconds (only used with --wait) #[arg(long, default_value = "10")] poll_interval: u64, }, /// Clear all observations for a bank ClearObservations { /// Bank ID bank_id: String, /// Skip confirmation prompt #[arg(short = 'y', long)] yes: bool, }, /// Get bank configuration (hierarchical overrides) Config { /// Bank ID bank_id: String, /// Show only bank-specific overrides (not full resolved config) #[arg(long)] overrides_only: bool, }, /// Update bank configuration (set hierarchical overrides) SetConfig { /// Bank ID bank_id: String, /// LLM provider override #[arg(long)] llm_provider: Option, /// LLM model override #[arg(long)] llm_model: Option, /// LLM API key override #[arg(long)] llm_api_key: Option, /// LLM base URL override #[arg(long)] llm_base_url: Option, /// Retain mission: what to focus on during fact extraction #[arg(long)] retain_mission: Option, /// Retain extraction mode (concise, verbose, custom) #[arg(long)] retain_extraction_mode: Option, /// Observations mission: what to synthesize into durable observations #[arg(long)] observations_mission: Option, /// Reflect mission: first-person identity for reflect operations #[arg(long)] reflect_mission: Option, /// Disposition skepticism trait (1-5) #[arg(long, value_parser = clap::value_parser!(i64).range(1..=5))] disposition_skepticism: Option, /// Disposition literalism trait (1-5) #[arg(long, value_parser = clap::value_parser!(i64).range(1..=5))] disposition_literalism: Option, /// Disposition empathy trait (1-5) #[arg(long, value_parser = clap::value_parser!(i64).range(1..=5))] disposition_empathy: Option, }, /// Reset bank configuration to defaults (remove all overrides) ResetConfig { /// Bank ID bank_id: String, /// Skip confirmation prompt #[arg(short = 'y', long)] yes: bool, }, } #[derive(Subcommand)] enum MemoryCommands { /// List memory units with pagination List { /// Bank ID bank_id: String, /// Filter by fact type (world, experience, opinion) #[arg(short = 't', long)] fact_type: Option, /// Full-text search query #[arg(short = 'q', long)] query: Option, /// Maximum number of results #[arg(short = 'l', long, default_value = "100")] limit: i64, /// Offset for pagination #[arg(short = 's', long, default_value = "0")] offset: i64, }, /// Get a specific memory unit by ID Get { /// Bank ID bank_id: String, /// Memory unit ID memory_id: String, }, /// Recall memories using semantic search Recall { /// Bank ID bank_id: String, /// Search query query: String, /// Fact types to search (world, experience, opinion) #[arg(short = 't', long, value_delimiter = ',', default_values = &["world", "experience", "opinion"])] fact_type: Vec, /// Thinking budget (low, mid, high) #[arg(short = 'b', long, default_value = "mid")] budget: String, /// Maximum tokens for results #[arg(long, default_value = "4096")] max_tokens: i64, /// Show trace information #[arg(long)] trace: bool, /// Include chunks in results #[arg(long)] include_chunks: bool, /// Maximum tokens for chunks (only used with --include-chunks) #[arg(long, default_value = "8192")] chunk_max_tokens: i64, /// Filter by tags (comma-separated, e.g. user:alice,team) #[arg(long, value_delimiter = ',')] tags: Vec, /// Tag matching mode: any, all, any_strict, all_strict (default: any) #[arg(long)] tags_match: Option, }, /// Generate answers using bank identity (reflect/reasoning) Reflect { /// Bank ID bank_id: String, /// Query to reflect on query: String, /// Thinking budget (low, mid, high) #[arg(short = 'b', long, default_value = "mid")] budget: String, /// Additional context #[arg(short = 'c', long)] context: Option, /// Maximum tokens for the response (server default: 4096) #[arg(short = 'm', long)] max_tokens: Option, /// Path to JSON schema file for structured output #[arg(short = 's', long)] schema: Option, /// Filter by tags (comma-separated, e.g. user:alice,team) #[arg(long, value_delimiter = ',')] tags: Vec, /// Tag matching mode: any, all, any_strict, all_strict (default: any) #[arg(long)] tags_match: Option, /// Include source facts (based_on) in the response #[arg(long)] include_facts: bool, }, /// Store (retain) a single memory Retain { /// Bank ID bank_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 (retain) RetainFiles { /// Bank ID bank_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 { /// Bank ID bank_id: String, /// Memory unit ID unit_id: String, }, /// Clear all memories for a bank Clear { /// Bank ID bank_id: String, /// Fact type to clear (world, agent, opinion). If not specified, clears all types. #[arg(short = 't', long, value_parser = ["world", "agent", "opinion"])] fact_type: Option, /// Skip confirmation prompt #[arg(short = 'y', long)] yes: bool, }, } #[derive(Subcommand)] enum DocumentCommands { /// List documents for a bank List { /// Bank ID bank_id: String, /// Search query to filter documents #[arg(short = 'q', long)] query: Option, /// Filter by date (yesterday, today, YYYY-MM-DD, or all) #[arg(short = 'd', long)] date: 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 { /// Bank ID bank_id: String, /// Document ID document_id: String, }, /// Delete a document and all its memory units Delete { /// Bank ID bank_id: String, /// Document ID document_id: String, }, } #[derive(Subcommand)] enum EntityCommands { /// List entities for a bank List { /// Bank ID bank_id: String, /// Maximum number of results #[arg(short = 'l', long, default_value = "100")] limit: i64, }, /// Get detailed information about an entity Get { /// Bank ID bank_id: String, /// Entity ID entity_id: String, }, /// Regenerate observations for an entity Regenerate { /// Bank ID bank_id: String, /// Entity ID entity_id: String, }, } #[derive(Subcommand)] enum OperationCommands { /// List async operations for a bank List { /// Bank ID bank_id: String, }, /// Get the status of a specific operation Get { /// Bank ID bank_id: String, /// Operation ID operation_id: String, }, /// Cancel a pending async operation Cancel { /// Bank ID bank_id: String, /// Operation ID operation_id: String, }, } #[derive(Subcommand)] enum TagCommands { /// List tags in a bank List { /// Bank ID bank_id: String, /// Wildcard search query (e.g., 'user:*') #[arg(short = 'q', long)] query: Option, /// Maximum number of results #[arg(short = 'l', long, default_value = "100")] limit: i64, /// Offset for pagination #[arg(short = 's', long, default_value = "0")] offset: i64, }, } #[derive(Subcommand)] enum ChunkCommands { /// Get a specific chunk by ID Get { /// Chunk ID chunk_id: String, }, } #[derive(Subcommand)] enum MentalModelCommands { /// List mental models for a bank List { /// Bank ID bank_id: String, }, /// Get a specific mental model Get { /// Bank ID bank_id: String, /// Mental model ID mental_model_id: String, }, /// Create a new mental model Create { /// Bank ID bank_id: String, /// Mental model name name: String, /// Source query to generate the mental model from source_query: String, /// Optional custom ID for the mental model (alphanumeric lowercase with hyphens) #[arg(long)] id: Option, }, /// Update a mental model Update { /// Bank ID bank_id: String, /// Mental model ID mental_model_id: String, /// New name #[arg(long)] name: Option, }, /// Delete a mental model Delete { /// Bank ID bank_id: String, /// Mental model ID mental_model_id: String, /// Skip confirmation prompt #[arg(short = 'y', long)] yes: bool, }, /// Refresh a mental model (re-run the source query) Refresh { /// Bank ID bank_id: String, /// Mental model ID mental_model_id: String, }, /// Get the change history of a mental model History { /// Bank ID bank_id: String, /// Mental model ID mental_model_id: String, }, } #[derive(Subcommand)] enum DirectiveCommands { /// List directives for a bank List { /// Bank ID bank_id: String, }, /// Get a specific directive Get { /// Bank ID bank_id: String, /// Directive ID directive_id: String, }, /// Create a new directive Create { /// Bank ID bank_id: String, /// Directive name name: String, /// Directive content (the text to inject into prompts) content: String, }, /// Update a directive Update { /// Bank ID bank_id: String, /// Directive ID directive_id: String, /// New name #[arg(long)] name: Option, /// New content #[arg(long)] content: Option, /// Enable or disable the directive #[arg(long)] is_active: Option, }, /// Delete a directive Delete { /// Bank ID bank_id: String, /// Directive ID directive_id: String, /// Skip confirmation prompt #[arg(short = 'y', long)] yes: bool, }, } fn main() { if let Err(_) = run() { std::process::exit(1); } } fn run() -> Result<()> { let cli = Cli::parse(); let output_format: OutputFormat = cli.output.into(); let verbose = cli.verbose; // Handle configure command before loading full config (it doesn't need API client) if let Commands::Configure { api_url, api_key } = cli.command { return handle_configure(api_url, api_key, output_format); } // Handle ui command - needs config but not API client if let Commands::Ui = cli.command { return handle_ui(output_format); } // 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(); let api_key = config.api_key.clone(); // Create API client let client = ApiClient::new(api_url.clone(), api_key).unwrap_or_else(|e| { errors::handle_api_error(e, &api_url); }); // Execute command and handle errors let result: Result<()> = match cli.command { Commands::Configure { .. } => unreachable!(), // Handled above Commands::Ui => unreachable!(), // Handled above Commands::Explore => commands::explore::run(&client), // Health, Metrics, and Version Commands::Health => commands::health::health(&client, verbose, output_format), Commands::Metrics => commands::health::metrics(&client, verbose, output_format), Commands::Version => commands::health::version(&client, verbose, output_format), // Bank commands Commands::Bank(bank_cmd) => match bank_cmd { BankCommands::List => commands::bank::list(&client, verbose, output_format), BankCommands::Create { bank_id, name, mission, skepticism, literalism, empathy } => { commands::bank::create(&client, &bank_id, name, mission, skepticism, literalism, empathy, verbose, output_format) } BankCommands::Update { bank_id, name, mission, skepticism, literalism, empathy } => { commands::bank::update(&client, &bank_id, name, mission, skepticism, literalism, empathy, verbose, output_format) } BankCommands::Disposition { bank_id } => commands::bank::disposition(&client, &bank_id, verbose, output_format), BankCommands::Stats { bank_id } => commands::bank::stats(&client, &bank_id, verbose, output_format), BankCommands::Name { bank_id, name } => commands::bank::update_name(&client, &bank_id, &name, verbose, output_format), BankCommands::Mission { bank_id, mission } => { commands::bank::mission(&client, &bank_id, &mission, verbose, output_format) } BankCommands::Background { bank_id, content, no_update_disposition } => { commands::bank::update_background(&client, &bank_id, &content, no_update_disposition, verbose, output_format) } BankCommands::Graph { bank_id, fact_type, limit } => { commands::bank::graph(&client, &bank_id, fact_type, limit, verbose, output_format) } BankCommands::Delete { bank_id, yes } => { commands::bank::delete(&client, &bank_id, yes, verbose, output_format) } BankCommands::Consolidate { bank_id, wait, poll_interval } => { commands::bank::consolidate(&client, &bank_id, wait, poll_interval, verbose, output_format) } BankCommands::ClearObservations { bank_id, yes } => { commands::bank::clear_observations(&client, &bank_id, yes, verbose, output_format) } BankCommands::Config { bank_id, overrides_only } => { commands::bank::config(&client, &bank_id, overrides_only, verbose, output_format) } BankCommands::SetConfig { bank_id, llm_provider, llm_model, llm_api_key, llm_base_url, retain_mission, retain_extraction_mode, observations_mission, reflect_mission, disposition_skepticism, disposition_literalism, disposition_empathy } => { commands::bank::set_config(&client, &bank_id, llm_provider, llm_model, llm_api_key, llm_base_url, retain_mission, retain_extraction_mode, observations_mission, reflect_mission, disposition_skepticism, disposition_literalism, disposition_empathy, verbose, output_format) } BankCommands::ResetConfig { bank_id, yes } => { commands::bank::reset_config(&client, &bank_id, yes, verbose, output_format) } }, // Memory commands Commands::Memory(memory_cmd) => match memory_cmd { MemoryCommands::List { bank_id, fact_type, query, limit, offset } => { commands::memory::list(&client, &bank_id, fact_type, query, limit, offset, verbose, output_format) } MemoryCommands::Get { bank_id, memory_id } => { commands::memory::get(&client, &bank_id, &memory_id, verbose, output_format) } MemoryCommands::Recall { bank_id, query, fact_type, budget, max_tokens, trace, include_chunks, chunk_max_tokens, tags, tags_match } => { commands::memory::recall(&client, &bank_id, query, fact_type, budget, max_tokens, trace, include_chunks, chunk_max_tokens, tags, tags_match, verbose, output_format) } MemoryCommands::Reflect { bank_id, query, budget, context, max_tokens, schema, tags, tags_match, include_facts } => { commands::memory::reflect(&client, &bank_id, query, budget, context, max_tokens, schema, tags, tags_match, include_facts, verbose, output_format) } MemoryCommands::Retain { bank_id, content, doc_id, context, r#async } => { commands::memory::retain(&client, &bank_id, content, doc_id, context, r#async, verbose, output_format) } MemoryCommands::RetainFiles { bank_id, path, recursive, context, r#async } => { commands::memory::retain_files(&client, &bank_id, path, recursive, context, r#async, verbose, output_format) } MemoryCommands::Delete { bank_id, unit_id } => { commands::memory::delete(&client, &bank_id, &unit_id, verbose, output_format) } MemoryCommands::Clear { bank_id, fact_type, yes } => { commands::memory::clear(&client, &bank_id, fact_type, yes, verbose, output_format) } }, // Document commands Commands::Document(doc_cmd) => match doc_cmd { DocumentCommands::List { bank_id, query, date, limit, offset } => { commands::document::list(&client, &bank_id, query, date, limit, offset, verbose, output_format) } DocumentCommands::Get { bank_id, document_id } => { commands::document::get(&client, &bank_id, &document_id, verbose, output_format) } DocumentCommands::Delete { bank_id, document_id } => { commands::document::delete(&client, &bank_id, &document_id, verbose, output_format) } }, // Entity commands Commands::Entity(entity_cmd) => match entity_cmd { EntityCommands::List { bank_id, limit } => { commands::entity::list(&client, &bank_id, limit, verbose, output_format) } EntityCommands::Get { bank_id, entity_id } => { commands::entity::get(&client, &bank_id, &entity_id, verbose, output_format) } EntityCommands::Regenerate { bank_id, entity_id } => { commands::entity::regenerate(&client, &bank_id, &entity_id, verbose, output_format) } }, // Tag commands Commands::Tag(tag_cmd) => match tag_cmd { TagCommands::List { bank_id, query, limit, offset } => { commands::tag::list(&client, &bank_id, query, limit, offset, verbose, output_format) } }, // Chunk commands Commands::Chunk(chunk_cmd) => match chunk_cmd { ChunkCommands::Get { chunk_id } => { commands::chunk::get(&client, &chunk_id, verbose, output_format) } }, // Operation commands Commands::Operation(op_cmd) => match op_cmd { OperationCommands::List { bank_id } => { commands::operation::list(&client, &bank_id, verbose, output_format) } OperationCommands::Get { bank_id, operation_id } => { commands::operation::get(&client, &bank_id, &operation_id, verbose, output_format) } OperationCommands::Cancel { bank_id, operation_id } => { commands::operation::cancel(&client, &bank_id, &operation_id, verbose, output_format) } }, // Mental model commands Commands::MentalModel(mm_cmd) => match mm_cmd { MentalModelCommands::List { bank_id } => { commands::mental_model::list(&client, &bank_id, verbose, output_format) } MentalModelCommands::Get { bank_id, mental_model_id } => { commands::mental_model::get(&client, &bank_id, &mental_model_id, verbose, output_format) } MentalModelCommands::Create { bank_id, name, source_query, id } => { commands::mental_model::create(&client, &bank_id, &name, &source_query, id.as_deref(), verbose, output_format) } MentalModelCommands::Update { bank_id, mental_model_id, name } => { commands::mental_model::update(&client, &bank_id, &mental_model_id, name, verbose, output_format) } MentalModelCommands::Delete { bank_id, mental_model_id, yes } => { commands::mental_model::delete(&client, &bank_id, &mental_model_id, yes, verbose, output_format) } MentalModelCommands::Refresh { bank_id, mental_model_id } => { commands::mental_model::refresh(&client, &bank_id, &mental_model_id, verbose, output_format) } MentalModelCommands::History { bank_id, mental_model_id } => { commands::mental_model::history(&client, &bank_id, &mental_model_id, verbose, output_format) } }, // Directive commands Commands::Directive(dir_cmd) => match dir_cmd { DirectiveCommands::List { bank_id } => { commands::directive::list(&client, &bank_id, verbose, output_format) } DirectiveCommands::Get { bank_id, directive_id } => { commands::directive::get(&client, &bank_id, &directive_id, verbose, output_format) } DirectiveCommands::Create { bank_id, name, content } => { commands::directive::create(&client, &bank_id, &name, &content, verbose, output_format) } DirectiveCommands::Update { bank_id, directive_id, name, content, is_active } => { commands::directive::update(&client, &bank_id, &directive_id, name, content, is_active, verbose, output_format) } DirectiveCommands::Delete { bank_id, directive_id, yes } => { commands::directive::delete(&client, &bank_id, &directive_id, yes, verbose, output_format) } }, }; // Handle API errors with nice messages if let Err(e) = result { errors::handle_api_error(e, &api_url); } Ok(()) } fn handle_configure(api_url: Option, api_key: Option, output_format: OutputFormat) -> Result<()> { // Load current config to show current state let current_config = Config::load().ok(); if output_format == OutputFormat::Pretty { ui::print_info("Hindsight CLI Configuration"); println!(); // Show current configuration if let Some(ref config) = current_config { println!(" Current API URL: {}", config.api_url); if let Some(ref key) = config.api_key { // Mask the API key for display let masked = if key.len() > 8 { format!("{}...{}", &key[..4], &key[key.len()-4..]) } else { "****".to_string() }; println!(" Current API Key: {}", masked); } println!(" Source: {}", config.source); println!(); } } // Get the new API URL (from argument or prompt) let new_api_url = match api_url { Some(url) => url, None => { // Interactive prompt let current = current_config.as_ref().map(|c| c.api_url.as_str()); config::prompt_api_url(current)? } }; // Validate the URL if !new_api_url.starts_with("http://") && !new_api_url.starts_with("https://") { ui::print_error(&format!( "Invalid API URL: {}. Must start with http:// or https://", new_api_url )); return Ok(()); } // Use provided api_key, or keep existing one if not provided let new_api_key = api_key.or_else(|| current_config.as_ref().and_then(|c| c.api_key.clone())); // Save to config file let config_path = Config::save_config(&new_api_url, new_api_key.as_deref())?; if output_format == OutputFormat::Pretty { ui::print_success(&format!("Configuration saved to {}", config_path.display())); println!(); println!(" API URL: {}", new_api_url); if let Some(ref key) = new_api_key { let masked = if key.len() > 8 { format!("{}...{}", &key[..4], &key[key.len()-4..]) } else { "****".to_string() }; println!(" API Key: {}", masked); } println!(); println!("Note: Environment variables HINDSIGHT_API_URL and HINDSIGHT_API_KEY will override these settings."); } else { let result = serde_json::json!({ "api_url": new_api_url, "api_key_set": new_api_key.is_some(), "config_path": config_path.display().to_string(), }); output::print_output(&result, output_format)?; } Ok(()) } fn handle_ui(output_format: OutputFormat) -> Result<()> { use std::process::Command; // Load configuration to get the API URL let config = Config::load().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(); if output_format == OutputFormat::Pretty { ui::print_info("Launching Hindsight Control Plane UI..."); println!(); println!(" API URL: {}", api_url); println!(); } // Run npx @vectorize-io/hindsight-control-plane --api-url {api_url} let status = Command::new("npx") .arg("@vectorize-io/hindsight-control-plane") .arg("--api-url") .arg(api_url) .status(); match status { Ok(exit_status) => { if !exit_status.success() { if let Some(code) = exit_status.code() { std::process::exit(code); } else { std::process::exit(1); } } } Err(e) => { ui::print_error(&format!("Failed to launch control plane UI: {}", e)); ui::print_info("Make sure you have Node.js and npm installed."); ui::print_info("You can also install the control plane globally: npm install -g @vectorize-io/hindsight-control-plane"); std::process::exit(1); } } Ok(()) }