* fix: misc fixes for observations and mental models * feat: improve graph retrieval for observations - Update LinkExpansionRetriever to traverse through source_memory_ids for observation entity connections (avoiding data duplication) - Remove entity link copy from world facts to observations in consolidator - Add tests for link expansion graph retrieval - Add directives_applied field to ReflectResult - Include user's other changes (CLI, docs, client updates) * fix: CI test failures - Add mental_model_id parameter to create_mental_model function - Fix ToolCallTrace not including reason field from ToolCall - Improve test_link_expansion_observation_graph_retrieval to wait for consolidation with retry * chore: reduce link expansion log verbosity * Revert "chore: reduce link expansion log verbosity" This reverts commit 3ce759391cead1012157785fa78fef16ef9bfe3b. * feat: add semantic/temporal/entity links as fallback in graph retrieval - Add fallback query for semantic, temporal, and entity links from memory_links - Check both directions (outgoing and incoming links) - Weight fallback results at 0.5x to prioritize entity links via unit_entities - Fixes graph retrieval returning 0 when data has cross-cluster temporal connections * fix: enable observations fixture for link expansion test - Add enable_observations fixture to ensure observations are created - Increase wait time from 10 to 30 seconds for CI reliability
1017 lines
29 KiB
Rust
1017 lines
29 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,
|
|
},
|
|
|
|
/// 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>,
|
|
|
|
/// 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,
|
|
},
|
|
|
|
/// 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 } => {
|
|
commands::bank::consolidate(&client, &bank_id, 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, 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)
|
|
}
|
|
},
|
|
|
|
// 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 } => {
|
|
commands::mental_model::create(&client, &bank_id, &name, &source_query, 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(())
|
|
}
|