* feat: independent versioning for integrations
- Add per-integration changelog pages at /changelog/integrations/<name>
- Move main changelog to changelog/index.md (URL unchanged)
- Add --integration flag to generate-changelog for LLM-based per-integration changelog generation
- Add scripts/release-integration.sh <name> <version> for cutting integration releases
- Add .github/workflows/release-integration.yml to publish on integrations/** tags
- Remove integrations from main release.sh and release.yml cycle
* fix: add agno and hermes integration docs to version-0.4 for production build
* chore: apply ruff formatting to generate_changelog.py
* feat: add 4-tab code parity across all documentation examples
Every code snippet Tabs block now has Python, Node.js, CLI, and Go variants.
Raw HTTP/curl tabs replaced with proper SDK calls.
New example files:
- Go: retain.go, recall.go, reflect.go, memory-banks.go, directives.go,
mental-models.go, documents.go, main-methods.go
- Shell: memory-banks.sh, directives.sh, mental-models.sh
- Node.js: mental-models.mjs
Extended example files with missing sections:
- recall.mjs/sh: world/experience/observation types, token-budget, all tag modes
- reflect.sh: reflect-with-params, reflect-disposition, reflect-sources, reflect-with-tags
- reflect.mjs: reflect-with-tags, fixed reflect-sources API usage
- retain.mjs/sh: retain-conversation, retain-batch, retain-files-batch
SDK/CLI additions:
- TypeScript: getMentalModelHistory method
- CLI recall: --tags, --tags-match flags
- CLI reflect: --tags, --tags-match, --include-facts flags
- CLI directive update: --is-active flag
- CLI bank set-config: --retain-mission, --retain-extraction-mode,
--observations-mission, --reflect-mission, --disposition-* flags
Build validation:
- scripts/check-code-parity.mjs validates 4-tab parity across all MDX files
- Integrated into npm run build — fails if any Tabs block is missing a variant
* fix: fix doc examples for Go, Node.js, CLI + add mental model with-id examples
- Fix Go Budget constants: BUDGET_HIGH/LOW/MID → HIGH/LOW/MID
- Fix Go documents.go: ListDocuments returns []map[string]interface{}, use map access
- Fix Go retain.go: use correct relative path for sample.pdf
- Fix Node.js createMentalModel: use positional args (name, sourceQuery) not object
- Add CLI 'history' subcommand for mental models (api.rs, main.rs, mental_model.rs)
- Rebuild TypeScript/Python clients to support id param in createMentalModel
- Add create-mental-model-with-id examples across all 4 languages and docs
* fix: move id param to end of create_mental_model signature for backwards compat
1148 lines
34 KiB
Rust
1148 lines
34 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,
|
|
},
|
|
|
|
/// 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<String>,
|
|
|
|
/// LLM model override
|
|
#[arg(long)]
|
|
llm_model: Option<String>,
|
|
|
|
/// LLM API key override
|
|
#[arg(long)]
|
|
llm_api_key: Option<String>,
|
|
|
|
/// LLM base URL override
|
|
#[arg(long)]
|
|
llm_base_url: Option<String>,
|
|
|
|
/// Retain mission: what to focus on during fact extraction
|
|
#[arg(long)]
|
|
retain_mission: Option<String>,
|
|
|
|
/// Retain extraction mode (concise, verbose, custom)
|
|
#[arg(long)]
|
|
retain_extraction_mode: Option<String>,
|
|
|
|
/// Observations mission: what to synthesize into durable observations
|
|
#[arg(long)]
|
|
observations_mission: Option<String>,
|
|
|
|
/// Reflect mission: first-person identity for reflect operations
|
|
#[arg(long)]
|
|
reflect_mission: Option<String>,
|
|
|
|
/// Disposition skepticism trait (1-5)
|
|
#[arg(long, value_parser = clap::value_parser!(i64).range(1..=5))]
|
|
disposition_skepticism: Option<i64>,
|
|
|
|
/// Disposition literalism trait (1-5)
|
|
#[arg(long, value_parser = clap::value_parser!(i64).range(1..=5))]
|
|
disposition_literalism: Option<i64>,
|
|
|
|
/// Disposition empathy trait (1-5)
|
|
#[arg(long, value_parser = clap::value_parser!(i64).range(1..=5))]
|
|
disposition_empathy: Option<i64>,
|
|
},
|
|
|
|
/// 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<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,
|
|
|
|
/// Filter by tags (comma-separated, e.g. user:alice,team)
|
|
#[arg(long, value_delimiter = ',')]
|
|
tags: Vec<String>,
|
|
|
|
/// Tag matching mode: any, all, any_strict, all_strict (default: any)
|
|
#[arg(long)]
|
|
tags_match: Option<String>,
|
|
},
|
|
|
|
/// 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>,
|
|
|
|
/// Filter by tags (comma-separated, e.g. user:alice,team)
|
|
#[arg(long, value_delimiter = ',')]
|
|
tags: Vec<String>,
|
|
|
|
/// Tag matching mode: any, all, any_strict, all_strict (default: any)
|
|
#[arg(long)]
|
|
tags_match: Option<String>,
|
|
|
|
/// 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<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,
|
|
},
|
|
|
|
/// 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<String>,
|
|
|
|
/// New content
|
|
#[arg(long)]
|
|
content: Option<String>,
|
|
|
|
/// Enable or disable the directive
|
|
#[arg(long)]
|
|
is_active: Option<bool>,
|
|
},
|
|
|
|
/// 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<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(())
|
|
}
|