523 lines
15 KiB
Rust
523 lines
15 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, profile, stats)
|
|
#[command(subcommand)]
|
|
Bank(BankCommands),
|
|
|
|
/// Manage memories (recall, reflect, retain, delete)
|
|
#[command(subcommand)]
|
|
Memory(MemoryCommands),
|
|
|
|
/// Manage documents (list, get, delete)
|
|
#[command(subcommand)]
|
|
Document(DocumentCommands),
|
|
|
|
/// Manage entities (list, get, regenerate)
|
|
#[command(subcommand)]
|
|
Entity(EntityCommands),
|
|
|
|
/// Manage async operations (list, cancel)
|
|
#[command(subcommand)]
|
|
Operation(OperationCommands),
|
|
|
|
/// Interactive TUI explorer (k9s-style) for navigating banks, memories, entities, and performing recall/reflect
|
|
#[command(alias = "tui")]
|
|
Explore,
|
|
|
|
/// Configure the CLI (API URL, etc.)
|
|
#[command(after_help = "Configuration priority:\n 1. Environment variable (HINDSIGHT_API_URL) - 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>,
|
|
},
|
|
}
|
|
|
|
#[derive(Subcommand)]
|
|
enum BankCommands {
|
|
/// List all banks
|
|
List,
|
|
|
|
/// Get bank disposition and background
|
|
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 or merge bank background
|
|
Background {
|
|
/// Bank ID
|
|
bank_id: String,
|
|
|
|
/// Background content
|
|
content: String,
|
|
|
|
/// Skip automatic disposition inference
|
|
#[arg(long)]
|
|
no_update_disposition: bool,
|
|
},
|
|
|
|
/// Delete a bank and all its data
|
|
Delete {
|
|
/// Bank ID
|
|
bank_id: String,
|
|
|
|
/// Skip confirmation prompt
|
|
#[arg(short = 'y', long)]
|
|
yes: bool,
|
|
},
|
|
}
|
|
|
|
#[derive(Subcommand)]
|
|
enum MemoryCommands {
|
|
/// 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>,
|
|
},
|
|
|
|
/// 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>,
|
|
|
|
/// 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,
|
|
},
|
|
|
|
/// Cancel a pending async operation
|
|
Cancel {
|
|
/// Bank ID
|
|
bank_id: String,
|
|
|
|
/// Operation ID
|
|
operation_id: String,
|
|
},
|
|
}
|
|
|
|
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 } = cli.command {
|
|
return handle_configure(api_url, 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();
|
|
|
|
// 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::Configure { .. } => unreachable!(), // Handled above
|
|
Commands::Explore => commands::explore::run(&client),
|
|
Commands::Bank(bank_cmd) => match bank_cmd {
|
|
BankCommands::List => commands::bank::list(&client, 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::Background { bank_id, content, no_update_disposition } => {
|
|
commands::bank::update_background(&client, &bank_id, &content, no_update_disposition, verbose, output_format)
|
|
}
|
|
BankCommands::Delete { bank_id, yes } => {
|
|
commands::bank::delete(&client, &bank_id, yes, verbose, output_format)
|
|
}
|
|
},
|
|
|
|
Commands::Memory(memory_cmd) => match memory_cmd {
|
|
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 } => {
|
|
commands::memory::reflect(&client, &bank_id, query, budget, context, 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)
|
|
}
|
|
},
|
|
|
|
Commands::Document(doc_cmd) => match doc_cmd {
|
|
DocumentCommands::List { bank_id, query, limit, offset } => {
|
|
commands::document::list(&client, &bank_id, query, 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)
|
|
}
|
|
},
|
|
|
|
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)
|
|
}
|
|
},
|
|
|
|
Commands::Operation(op_cmd) => match op_cmd {
|
|
OperationCommands::List { bank_id } => {
|
|
commands::operation::list(&client, &bank_id, verbose, output_format)
|
|
}
|
|
OperationCommands::Cancel { bank_id, operation_id } => {
|
|
commands::operation::cancel(&client, &bank_id, &operation_id, 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>, 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);
|
|
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(());
|
|
}
|
|
|
|
// Save to config file
|
|
let config_path = Config::save_api_url(&new_api_url)?;
|
|
|
|
if output_format == OutputFormat::Pretty {
|
|
ui::print_success(&format!("Configuration saved to {}", config_path.display()));
|
|
println!();
|
|
println!(" API URL: {}", new_api_url);
|
|
println!();
|
|
println!("Note: Environment variable HINDSIGHT_API_URL will override this setting.");
|
|
} else {
|
|
let result = serde_json::json!({
|
|
"api_url": new_api_url,
|
|
"config_path": config_path.display().to_string(),
|
|
});
|
|
output::print_output(&result, output_format)?;
|
|
}
|
|
|
|
Ok(())
|
|
}
|