fleet-memory/hindsight-cli/src/main.rs
Nicolò Boschi 7097716204
feat: improve mental models ux on control plane (#297)
* feat: improve mental models ux on control plane

* feat: improve mental models ux on control plane

* gen

* feat(cli): add --id flag to mental model create command

* fix(cli): revert unused variable underscore prefix that breaks compilation

The underscore prefix on stdout/stderr variables was added to suppress
warnings, but these variables are actually used in assert messages,
causing compilation errors. Reverting to original names.
2026-02-04 15:49:03 +01:00

1033 lines
30 KiB
Rust

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<Format> 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<String>,
/// API key for authentication (sent as Bearer token)
#[arg(long)]
api_key: Option<String>,
},
}
#[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<String>,
/// Mission statement
#[arg(short = 'm', long)]
mission: Option<String>,
/// Skepticism trait (1-5)
#[arg(long, value_parser = clap::value_parser!(i64).range(1..=5))]
skepticism: Option<i64>,
/// Literalism trait (1-5)
#[arg(long, value_parser = clap::value_parser!(i64).range(1..=5))]
literalism: Option<i64>,
/// Empathy trait (1-5)
#[arg(long, value_parser = clap::value_parser!(i64).range(1..=5))]
empathy: Option<i64>,
},
/// Update bank properties (partial update)
Update {
/// Bank ID
bank_id: String,
/// Bank name
#[arg(short = 'n', long)]
name: Option<String>,
/// Mission statement
#[arg(short = 'm', long)]
mission: Option<String>,
/// Skepticism trait (1-5)
#[arg(long, value_parser = clap::value_parser!(i64).range(1..=5))]
skepticism: Option<i64>,
/// Literalism trait (1-5)
#[arg(long, value_parser = clap::value_parser!(i64).range(1..=5))]
literalism: Option<i64>,
/// Empathy trait (1-5)
#[arg(long, value_parser = clap::value_parser!(i64).range(1..=5))]
empathy: Option<i64>,
},
/// 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<String>,
/// 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,
},
}
#[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<String>,
/// Full-text search query
#[arg(short = 'q', long)]
query: Option<String>,
/// 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<String>,
/// 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,
},
/// 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<String>,
/// Maximum tokens for the response (server default: 4096)
#[arg(short = 'm', long)]
max_tokens: Option<i64>,
/// Path to JSON schema file for structured output
#[arg(short = 's', long)]
schema: Option<PathBuf>,
},
/// 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<String>,
/// Context for the memory
#[arg(short = 'c', long)]
context: Option<String>,
/// 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<String>,
/// 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<String>,
/// 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<String>,
/// Filter by date (yesterday, today, YYYY-MM-DD, or all)
#[arg(short = 'd', long)]
date: Option<String>,
/// 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<String>,
/// 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<String>,
},
/// Update a mental model
Update {
/// Bank ID
bank_id: String,
/// Mental model ID
mental_model_id: String,
/// New name
#[arg(long)]
name: Option<String>,
},
/// 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,
},
}
#[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<String>,
/// New content
#[arg(long)]
content: Option<String>,
},
/// 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)
}
},
// 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 } => {
commands::memory::recall(&client, &bank_id, query, fact_type, budget, max_tokens, trace, include_chunks, chunk_max_tokens, verbose, output_format)
}
MemoryCommands::Reflect { bank_id, query, budget, context, max_tokens, schema } => {
commands::memory::reflect(&client, &bank_id, query, budget, context, max_tokens, schema, 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)
}
},
// 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 } => {
commands::directive::update(&client, &bank_id, &directive_id, name, content, 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<String>, api_key: Option<String>, 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(())
}