* 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
336 lines
9.5 KiB
Rust
336 lines
9.5 KiB
Rust
//! Mental model commands for managing user-curated summaries.
|
|
|
|
use anyhow::Result;
|
|
|
|
use crate::api::ApiClient;
|
|
use crate::output::{self, OutputFormat};
|
|
use crate::ui;
|
|
|
|
use hindsight_client::types;
|
|
|
|
/// List mental models for a bank
|
|
pub fn list(
|
|
client: &ApiClient,
|
|
bank_id: &str,
|
|
verbose: bool,
|
|
output_format: OutputFormat,
|
|
) -> Result<()> {
|
|
let spinner = if output_format == OutputFormat::Pretty {
|
|
Some(ui::create_spinner("Fetching mental models..."))
|
|
} else {
|
|
None
|
|
};
|
|
|
|
let response = client.list_mental_models(bank_id, verbose);
|
|
|
|
if let Some(mut sp) = spinner {
|
|
sp.finish();
|
|
}
|
|
|
|
match response {
|
|
Ok(result) => {
|
|
if output_format == OutputFormat::Pretty {
|
|
ui::print_section_header(&format!("Mental Models: {}", bank_id));
|
|
|
|
if result.items.is_empty() {
|
|
println!(" {}", ui::dim("No mental models found."));
|
|
} else {
|
|
for mental_model in &result.items {
|
|
println!(
|
|
" {} {}",
|
|
ui::gradient_start(&mental_model.id),
|
|
mental_model.name
|
|
);
|
|
|
|
// Show content preview
|
|
let preview: String = mental_model.content.chars().take(80).collect();
|
|
let ellipsis = if mental_model.content.len() > 80 { "..." } else { "" };
|
|
println!(" {}{}", ui::dim(&preview), ellipsis);
|
|
|
|
println!();
|
|
}
|
|
}
|
|
} else {
|
|
output::print_output(&result, output_format)?;
|
|
}
|
|
Ok(())
|
|
}
|
|
Err(e) => Err(e),
|
|
}
|
|
}
|
|
|
|
/// Get a specific mental model
|
|
pub fn get(
|
|
client: &ApiClient,
|
|
bank_id: &str,
|
|
mental_model_id: &str,
|
|
verbose: bool,
|
|
output_format: OutputFormat,
|
|
) -> Result<()> {
|
|
let spinner = if output_format == OutputFormat::Pretty {
|
|
Some(ui::create_spinner("Fetching mental model..."))
|
|
} else {
|
|
None
|
|
};
|
|
|
|
let response = client.get_mental_model(bank_id, mental_model_id, verbose);
|
|
|
|
if let Some(mut sp) = spinner {
|
|
sp.finish();
|
|
}
|
|
|
|
match response {
|
|
Ok(mental_model) => {
|
|
if output_format == OutputFormat::Pretty {
|
|
print_mental_model_detail(&mental_model);
|
|
} else {
|
|
output::print_output(&mental_model, output_format)?;
|
|
}
|
|
Ok(())
|
|
}
|
|
Err(e) => Err(e),
|
|
}
|
|
}
|
|
|
|
/// Create a new mental model
|
|
pub fn create(
|
|
client: &ApiClient,
|
|
bank_id: &str,
|
|
name: &str,
|
|
source_query: &str,
|
|
id: Option<&str>,
|
|
verbose: bool,
|
|
output_format: OutputFormat,
|
|
) -> Result<()> {
|
|
let spinner = if output_format == OutputFormat::Pretty {
|
|
Some(ui::create_spinner("Creating mental model..."))
|
|
} else {
|
|
None
|
|
};
|
|
|
|
let request = types::CreateMentalModelRequest {
|
|
id: id.map(|s| s.to_string()),
|
|
name: name.to_string(),
|
|
source_query: source_query.to_string(),
|
|
max_tokens: 2048,
|
|
tags: vec![],
|
|
trigger: None,
|
|
};
|
|
|
|
let response = client.create_mental_model(bank_id, &request, verbose);
|
|
|
|
if let Some(mut sp) = spinner {
|
|
sp.finish();
|
|
}
|
|
|
|
match response {
|
|
Ok(result) => {
|
|
if output_format == OutputFormat::Pretty {
|
|
ui::print_success(&format!("Mental model created, operation_id: {}", result.operation_id));
|
|
} else {
|
|
output::print_output(&result, output_format)?;
|
|
}
|
|
Ok(())
|
|
}
|
|
Err(e) => Err(e),
|
|
}
|
|
}
|
|
|
|
/// Update a mental model
|
|
pub fn update(
|
|
client: &ApiClient,
|
|
bank_id: &str,
|
|
mental_model_id: &str,
|
|
name: Option<String>,
|
|
verbose: bool,
|
|
output_format: OutputFormat,
|
|
) -> Result<()> {
|
|
if name.is_none() {
|
|
anyhow::bail!("--name must be provided");
|
|
}
|
|
|
|
let spinner = if output_format == OutputFormat::Pretty {
|
|
Some(ui::create_spinner("Updating mental model..."))
|
|
} else {
|
|
None
|
|
};
|
|
|
|
let request = types::UpdateMentalModelRequest {
|
|
name,
|
|
source_query: None,
|
|
max_tokens: None,
|
|
tags: None,
|
|
trigger: None,
|
|
};
|
|
|
|
let response = client.update_mental_model(bank_id, mental_model_id, &request, verbose);
|
|
|
|
if let Some(mut sp) = spinner {
|
|
sp.finish();
|
|
}
|
|
|
|
match response {
|
|
Ok(mental_model) => {
|
|
if output_format == OutputFormat::Pretty {
|
|
ui::print_success(&format!("Mental model '{}' updated successfully", mental_model_id));
|
|
println!();
|
|
print_mental_model_detail(&mental_model);
|
|
} else {
|
|
output::print_output(&mental_model, output_format)?;
|
|
}
|
|
Ok(())
|
|
}
|
|
Err(e) => Err(e),
|
|
}
|
|
}
|
|
|
|
/// Delete a mental model
|
|
pub fn delete(
|
|
client: &ApiClient,
|
|
bank_id: &str,
|
|
mental_model_id: &str,
|
|
yes: bool,
|
|
verbose: bool,
|
|
output_format: OutputFormat,
|
|
) -> Result<()> {
|
|
// Confirmation prompt unless -y flag is used
|
|
if !yes && output_format == OutputFormat::Pretty {
|
|
let message = format!(
|
|
"Are you sure you want to delete mental model '{}'? This cannot be undone.",
|
|
mental_model_id
|
|
);
|
|
|
|
let confirmed = ui::prompt_confirmation(&message)?;
|
|
|
|
if !confirmed {
|
|
ui::print_info("Operation cancelled");
|
|
return Ok(());
|
|
}
|
|
}
|
|
|
|
let spinner = if output_format == OutputFormat::Pretty {
|
|
Some(ui::create_spinner("Deleting mental model..."))
|
|
} else {
|
|
None
|
|
};
|
|
|
|
let response = client.delete_mental_model(bank_id, mental_model_id, verbose);
|
|
|
|
if let Some(mut sp) = spinner {
|
|
sp.finish();
|
|
}
|
|
|
|
match response {
|
|
Ok(_) => {
|
|
if output_format == OutputFormat::Pretty {
|
|
ui::print_success(&format!("Mental model '{}' deleted successfully", mental_model_id));
|
|
} else {
|
|
println!("{{\"success\": true}}");
|
|
}
|
|
Ok(())
|
|
}
|
|
Err(e) => Err(e),
|
|
}
|
|
}
|
|
|
|
/// Refresh a mental model
|
|
pub fn refresh(
|
|
client: &ApiClient,
|
|
bank_id: &str,
|
|
mental_model_id: &str,
|
|
verbose: bool,
|
|
output_format: OutputFormat,
|
|
) -> Result<()> {
|
|
let spinner = if output_format == OutputFormat::Pretty {
|
|
Some(ui::create_spinner("Submitting mental model refresh..."))
|
|
} else {
|
|
None
|
|
};
|
|
|
|
let response = client.refresh_mental_model(bank_id, mental_model_id, verbose);
|
|
|
|
if let Some(mut sp) = spinner {
|
|
sp.finish();
|
|
}
|
|
|
|
match response {
|
|
Ok(operation) => {
|
|
if output_format == OutputFormat::Pretty {
|
|
ui::print_success(&format!(
|
|
"Mental model refresh submitted. Operation ID: {}",
|
|
operation.operation_id
|
|
));
|
|
println!(" {} {}", ui::dim("Status:"), operation.status);
|
|
println!();
|
|
println!("{}", ui::dim("Use 'hindsight operations get' to check the operation status."));
|
|
} else {
|
|
output::print_output(&operation, output_format)?;
|
|
}
|
|
Ok(())
|
|
}
|
|
Err(e) => Err(e),
|
|
}
|
|
}
|
|
|
|
/// Get the change history of a mental model
|
|
pub fn history(
|
|
client: &ApiClient,
|
|
bank_id: &str,
|
|
mental_model_id: &str,
|
|
verbose: bool,
|
|
output_format: OutputFormat,
|
|
) -> Result<()> {
|
|
let spinner = if output_format == OutputFormat::Pretty {
|
|
Some(ui::create_spinner("Fetching mental model history..."))
|
|
} else {
|
|
None
|
|
};
|
|
|
|
let response = client.get_mental_model_history(bank_id, mental_model_id, verbose);
|
|
|
|
if let Some(mut sp) = spinner {
|
|
sp.finish();
|
|
}
|
|
|
|
match response {
|
|
Ok(history) => {
|
|
if output_format == OutputFormat::Pretty {
|
|
ui::print_section_header(&format!("History: {}", mental_model_id));
|
|
|
|
if let Some(entries) = history.as_array() {
|
|
if entries.is_empty() {
|
|
println!(" {}", ui::dim("No history entries found."));
|
|
} else {
|
|
for entry in entries {
|
|
let changed_at = entry.get("changed_at").and_then(|v| v.as_str()).unwrap_or("unknown");
|
|
let previous = entry.get("previous_content").and_then(|v| v.as_str()).unwrap_or("(none)");
|
|
println!(" {} {}", ui::dim("Changed at:"), changed_at);
|
|
let preview: String = previous.chars().take(80).collect();
|
|
let ellipsis = if previous.len() > 80 { "..." } else { "" };
|
|
println!(" {} {}{}", ui::dim("Previous:"), ui::dim(&preview), ellipsis);
|
|
println!();
|
|
}
|
|
}
|
|
}
|
|
} else {
|
|
output::print_output(&history, output_format)?;
|
|
}
|
|
Ok(())
|
|
}
|
|
Err(e) => Err(e),
|
|
}
|
|
}
|
|
|
|
// Helper function to print mental model details
|
|
fn print_mental_model_detail(mental_model: &types::MentalModelResponse) {
|
|
ui::print_section_header(&mental_model.name);
|
|
|
|
println!(" {} {}", ui::dim("ID:"), ui::gradient_start(&mental_model.id));
|
|
println!(" {} {}", ui::dim("Source Query:"), &mental_model.source_query);
|
|
|
|
println!();
|
|
println!("{}", ui::gradient_text("─── Content ───"));
|
|
println!();
|
|
println!("{}", &mental_model.content);
|
|
println!();
|
|
}
|