Skip to content

Commit a56cd04

Browse files
authored
feat: 4-tab code parity across all documentation examples (#613)
* 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
1 parent 438ce98 commit a56cd04

38 files changed

Lines changed: 2306 additions & 104 deletions

hindsight-cli/src/api.rs

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -601,6 +601,13 @@ impl ApiClient {
601601
})
602602
}
603603

604+
pub fn get_mental_model_history(&self, bank_id: &str, mental_model_id: &str, _verbose: bool) -> Result<serde_json::Value> {
605+
self.runtime.block_on(async {
606+
let response = self.client.get_mental_model_history(bank_id, mental_model_id, None).await?;
607+
Ok(response.into_inner())
608+
})
609+
}
610+
604611
// --- Directive Methods ---
605612

606613
pub fn list_directives(&self, bank_id: &str, _verbose: bool) -> Result<types::DirectiveListResponse> {

hindsight-cli/src/commands/bank.rs

Lines changed: 29 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -717,6 +717,13 @@ pub fn set_config(
717717
llm_model: Option<String>,
718718
llm_api_key: Option<String>,
719719
llm_base_url: Option<String>,
720+
retain_mission: Option<String>,
721+
retain_extraction_mode: Option<String>,
722+
observations_mission: Option<String>,
723+
reflect_mission: Option<String>,
724+
disposition_skepticism: Option<i64>,
725+
disposition_literalism: Option<i64>,
726+
disposition_empathy: Option<i64>,
720727
verbose: bool,
721728
output_format: OutputFormat,
722729
) -> Result<()> {
@@ -736,9 +743,30 @@ pub fn set_config(
736743
if let Some(base_url) = llm_base_url {
737744
updates.insert("llm_base_url".to_string(), serde_json::Value::String(base_url));
738745
}
746+
if let Some(mission) = retain_mission {
747+
updates.insert("retain_mission".to_string(), serde_json::Value::String(mission));
748+
}
749+
if let Some(mode) = retain_extraction_mode {
750+
updates.insert("retain_extraction_mode".to_string(), serde_json::Value::String(mode));
751+
}
752+
if let Some(mission) = observations_mission {
753+
updates.insert("observations_mission".to_string(), serde_json::Value::String(mission));
754+
}
755+
if let Some(mission) = reflect_mission {
756+
updates.insert("reflect_mission".to_string(), serde_json::Value::String(mission));
757+
}
758+
if let Some(skepticism) = disposition_skepticism {
759+
updates.insert("disposition_skepticism".to_string(), serde_json::Value::Number(skepticism.into()));
760+
}
761+
if let Some(literalism) = disposition_literalism {
762+
updates.insert("disposition_literalism".to_string(), serde_json::Value::Number(literalism.into()));
763+
}
764+
if let Some(empathy) = disposition_empathy {
765+
updates.insert("disposition_empathy".to_string(), serde_json::Value::Number(empathy.into()));
766+
}
739767

740768
if updates.is_empty() {
741-
return Err(anyhow!("No config updates provided. Use --llm-provider, --llm-model, --llm-api-key, or --llm-base-url".to_string()));
769+
return Err(anyhow!("No config updates provided. Use --llm-provider, --llm-model, --retain-mission, --observations-mission, or other flags".to_string()));
742770
}
743771

744772
let spinner = if output_format == OutputFormat::Pretty {

hindsight-cli/src/commands/directive.rs

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -149,11 +149,12 @@ pub fn update(
149149
directive_id: &str,
150150
name: Option<String>,
151151
content: Option<String>,
152+
is_active: Option<bool>,
152153
verbose: bool,
153154
output_format: OutputFormat,
154155
) -> Result<()> {
155-
if name.is_none() && content.is_none() {
156-
anyhow::bail!("At least one of --name or --content must be provided");
156+
if name.is_none() && content.is_none() && is_active.is_none() {
157+
anyhow::bail!("At least one of --name, --content, or --is-active must be provided");
157158
}
158159

159160
let spinner = if output_format == OutputFormat::Pretty {
@@ -165,7 +166,7 @@ pub fn update(
165166
let request = types::UpdateDirectiveRequest {
166167
name,
167168
content,
168-
is_active: None,
169+
is_active,
169170
priority: None,
170171
tags: None,
171172
};

hindsight-cli/src/commands/memory.rs

Lines changed: 30 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,7 @@ use crate::output::{self, OutputFormat};
99
use crate::ui;
1010

1111
// Import types from generated client
12-
use hindsight_client::types::{Budget, ChunkIncludeOptions, IncludeOptions, TagsMatch};
12+
use hindsight_client::types::{Budget, ChunkIncludeOptions, FactsIncludeOptions, IncludeOptions, ReflectIncludeOptions, TagsMatch};
1313
use serde::Deserialize;
1414
use serde_json;
1515

@@ -43,6 +43,16 @@ fn parse_budget(budget: &str) -> Budget {
4343
}
4444
}
4545

46+
// Helper function to parse tags_match string to TagsMatch enum
47+
fn parse_tags_match(tags_match: &Option<String>) -> TagsMatch {
48+
match tags_match.as_deref().unwrap_or("any").to_lowercase().as_str() {
49+
"all" => TagsMatch::All,
50+
"any_strict" => TagsMatch::AnyStrict,
51+
"all_strict" => TagsMatch::AllStrict,
52+
_ => TagsMatch::Any,
53+
}
54+
}
55+
4656
/// List memory units with pagination and optional filters
4757
pub fn list(
4858
client: &ApiClient,
@@ -250,6 +260,8 @@ pub fn recall(
250260
trace: bool,
251261
include_chunks: bool,
252262
chunk_max_tokens: i64,
263+
tags: Vec<String>,
264+
tags_match: Option<String>,
253265
verbose: bool,
254266
output_format: OutputFormat,
255267
) -> Result<()> {
@@ -280,8 +292,8 @@ pub fn recall(
280292
trace,
281293
query_timestamp: None,
282294
include,
283-
tags: None,
284-
tags_match: TagsMatch::Any,
295+
tags: if tags.is_empty() { None } else { Some(tags) },
296+
tags_match: parse_tags_match(&tags_match),
285297
tag_groups: None,
286298
};
287299

@@ -312,6 +324,9 @@ pub fn reflect(
312324
context: Option<String>,
313325
max_tokens: Option<i64>,
314326
schema_path: Option<PathBuf>,
327+
tags: Vec<String>,
328+
tags_match: Option<String>,
329+
include_facts: bool,
315330
verbose: bool,
316331
output_format: OutputFormat,
317332
) -> Result<()> {
@@ -332,15 +347,24 @@ pub fn reflect(
332347
None
333348
};
334349

350+
let include = if include_facts {
351+
Some(ReflectIncludeOptions {
352+
facts: Some(FactsIncludeOptions(serde_json::Map::new())),
353+
tool_calls: None,
354+
})
355+
} else {
356+
None
357+
};
358+
335359
let request = ReflectRequest {
336360
query,
337361
budget: Some(parse_budget(&budget)),
338362
context,
339363
max_tokens: max_tokens.unwrap_or(4096),
340-
include: None,
364+
include,
341365
response_schema,
342-
tags: None,
343-
tags_match: TagsMatch::Any,
366+
tags: if tags.is_empty() { None } else { Some(tags) },
367+
tags_match: parse_tags_match(&tags_match),
344368
tag_groups: None,
345369
};
346370

hindsight-cli/src/commands/mental_model.rs

Lines changed: 49 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -272,6 +272,55 @@ pub fn refresh(
272272
}
273273
}
274274

275+
/// Get the change history of a mental model
276+
pub fn history(
277+
client: &ApiClient,
278+
bank_id: &str,
279+
mental_model_id: &str,
280+
verbose: bool,
281+
output_format: OutputFormat,
282+
) -> Result<()> {
283+
let spinner = if output_format == OutputFormat::Pretty {
284+
Some(ui::create_spinner("Fetching mental model history..."))
285+
} else {
286+
None
287+
};
288+
289+
let response = client.get_mental_model_history(bank_id, mental_model_id, verbose);
290+
291+
if let Some(mut sp) = spinner {
292+
sp.finish();
293+
}
294+
295+
match response {
296+
Ok(history) => {
297+
if output_format == OutputFormat::Pretty {
298+
ui::print_section_header(&format!("History: {}", mental_model_id));
299+
300+
if let Some(entries) = history.as_array() {
301+
if entries.is_empty() {
302+
println!(" {}", ui::dim("No history entries found."));
303+
} else {
304+
for entry in entries {
305+
let changed_at = entry.get("changed_at").and_then(|v| v.as_str()).unwrap_or("unknown");
306+
let previous = entry.get("previous_content").and_then(|v| v.as_str()).unwrap_or("(none)");
307+
println!(" {} {}", ui::dim("Changed at:"), changed_at);
308+
let preview: String = previous.chars().take(80).collect();
309+
let ellipsis = if previous.len() > 80 { "..." } else { "" };
310+
println!(" {} {}{}", ui::dim("Previous:"), ui::dim(&preview), ellipsis);
311+
println!();
312+
}
313+
}
314+
}
315+
} else {
316+
output::print_output(&history, output_format)?;
317+
}
318+
Ok(())
319+
}
320+
Err(e) => Err(e),
321+
}
322+
}
323+
275324
// Helper function to print mental model details
276325
fn print_mental_model_detail(mental_model: &types::MentalModelResponse) {
277326
ui::print_section_header(&mental_model.name);

hindsight-cli/src/main.rs

Lines changed: 72 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -310,6 +310,34 @@ enum BankCommands {
310310
/// LLM base URL override
311311
#[arg(long)]
312312
llm_base_url: Option<String>,
313+
314+
/// Retain mission: what to focus on during fact extraction
315+
#[arg(long)]
316+
retain_mission: Option<String>,
317+
318+
/// Retain extraction mode (concise, verbose, custom)
319+
#[arg(long)]
320+
retain_extraction_mode: Option<String>,
321+
322+
/// Observations mission: what to synthesize into durable observations
323+
#[arg(long)]
324+
observations_mission: Option<String>,
325+
326+
/// Reflect mission: first-person identity for reflect operations
327+
#[arg(long)]
328+
reflect_mission: Option<String>,
329+
330+
/// Disposition skepticism trait (1-5)
331+
#[arg(long, value_parser = clap::value_parser!(i64).range(1..=5))]
332+
disposition_skepticism: Option<i64>,
333+
334+
/// Disposition literalism trait (1-5)
335+
#[arg(long, value_parser = clap::value_parser!(i64).range(1..=5))]
336+
disposition_literalism: Option<i64>,
337+
338+
/// Disposition empathy trait (1-5)
339+
#[arg(long, value_parser = clap::value_parser!(i64).range(1..=5))]
340+
disposition_empathy: Option<i64>,
313341
},
314342

315343
/// Reset bank configuration to defaults (remove all overrides)
@@ -387,6 +415,14 @@ enum MemoryCommands {
387415
/// Maximum tokens for chunks (only used with --include-chunks)
388416
#[arg(long, default_value = "8192")]
389417
chunk_max_tokens: i64,
418+
419+
/// Filter by tags (comma-separated, e.g. user:alice,team)
420+
#[arg(long, value_delimiter = ',')]
421+
tags: Vec<String>,
422+
423+
/// Tag matching mode: any, all, any_strict, all_strict (default: any)
424+
#[arg(long)]
425+
tags_match: Option<String>,
390426
},
391427

392428
/// Generate answers using bank identity (reflect/reasoning)
@@ -412,6 +448,18 @@ enum MemoryCommands {
412448
/// Path to JSON schema file for structured output
413449
#[arg(short = 's', long)]
414450
schema: Option<PathBuf>,
451+
452+
/// Filter by tags (comma-separated, e.g. user:alice,team)
453+
#[arg(long, value_delimiter = ',')]
454+
tags: Vec<String>,
455+
456+
/// Tag matching mode: any, all, any_strict, all_strict (default: any)
457+
#[arg(long)]
458+
tags_match: Option<String>,
459+
460+
/// Include source facts (based_on) in the response
461+
#[arg(long)]
462+
include_facts: bool,
415463
},
416464

417465
/// Store (retain) a single memory
@@ -678,6 +726,15 @@ enum MentalModelCommands {
678726
/// Mental model ID
679727
mental_model_id: String,
680728
},
729+
730+
/// Get the change history of a mental model
731+
History {
732+
/// Bank ID
733+
bank_id: String,
734+
735+
/// Mental model ID
736+
mental_model_id: String,
737+
},
681738
}
682739

683740
#[derive(Subcommand)]
@@ -724,6 +781,10 @@ enum DirectiveCommands {
724781
/// New content
725782
#[arg(long)]
726783
content: Option<String>,
784+
785+
/// Enable or disable the directive
786+
#[arg(long)]
787+
is_active: Option<bool>,
727788
},
728789

729790
/// Delete a directive
@@ -821,8 +882,8 @@ fn run() -> Result<()> {
821882
BankCommands::Config { bank_id, overrides_only } => {
822883
commands::bank::config(&client, &bank_id, overrides_only, verbose, output_format)
823884
}
824-
BankCommands::SetConfig { bank_id, llm_provider, llm_model, llm_api_key, llm_base_url } => {
825-
commands::bank::set_config(&client, &bank_id, llm_provider, llm_model, llm_api_key, llm_base_url, verbose, output_format)
885+
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 } => {
886+
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)
826887
}
827888
BankCommands::ResetConfig { bank_id, yes } => {
828889
commands::bank::reset_config(&client, &bank_id, yes, verbose, output_format)
@@ -837,11 +898,11 @@ fn run() -> Result<()> {
837898
MemoryCommands::Get { bank_id, memory_id } => {
838899
commands::memory::get(&client, &bank_id, &memory_id, verbose, output_format)
839900
}
840-
MemoryCommands::Recall { bank_id, query, fact_type, budget, max_tokens, trace, include_chunks, chunk_max_tokens } => {
841-
commands::memory::recall(&client, &bank_id, query, fact_type, budget, max_tokens, trace, include_chunks, chunk_max_tokens, verbose, output_format)
901+
MemoryCommands::Recall { bank_id, query, fact_type, budget, max_tokens, trace, include_chunks, chunk_max_tokens, tags, tags_match } => {
902+
commands::memory::recall(&client, &bank_id, query, fact_type, budget, max_tokens, trace, include_chunks, chunk_max_tokens, tags, tags_match, verbose, output_format)
842903
}
843-
MemoryCommands::Reflect { bank_id, query, budget, context, max_tokens, schema } => {
844-
commands::memory::reflect(&client, &bank_id, query, budget, context, max_tokens, schema, verbose, output_format)
904+
MemoryCommands::Reflect { bank_id, query, budget, context, max_tokens, schema, tags, tags_match, include_facts } => {
905+
commands::memory::reflect(&client, &bank_id, query, budget, context, max_tokens, schema, tags, tags_match, include_facts, verbose, output_format)
845906
}
846907
MemoryCommands::Retain { bank_id, content, doc_id, context, r#async } => {
847908
commands::memory::retain(&client, &bank_id, content, doc_id, context, r#async, verbose, output_format)
@@ -930,6 +991,9 @@ fn run() -> Result<()> {
930991
MentalModelCommands::Refresh { bank_id, mental_model_id } => {
931992
commands::mental_model::refresh(&client, &bank_id, &mental_model_id, verbose, output_format)
932993
}
994+
MentalModelCommands::History { bank_id, mental_model_id } => {
995+
commands::mental_model::history(&client, &bank_id, &mental_model_id, verbose, output_format)
996+
}
933997
},
934998

935999
// Directive commands
@@ -943,8 +1007,8 @@ fn run() -> Result<()> {
9431007
DirectiveCommands::Create { bank_id, name, content } => {
9441008
commands::directive::create(&client, &bank_id, &name, &content, verbose, output_format)
9451009
}
946-
DirectiveCommands::Update { bank_id, directive_id, name, content } => {
947-
commands::directive::update(&client, &bank_id, &directive_id, name, content, verbose, output_format)
1010+
DirectiveCommands::Update { bank_id, directive_id, name, content, is_active } => {
1011+
commands::directive::update(&client, &bank_id, &directive_id, name, content, is_active, verbose, output_format)
9481012
}
9491013
DirectiveCommands::Delete { bank_id, directive_id, yes } => {
9501014
commands::directive::delete(&client, &bank_id, &directive_id, yes, verbose, output_format)

hindsight-clients/python/hindsight_client/hindsight_client.py

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -792,6 +792,7 @@ def create_mental_model(
792792
tags: list[str] | None = None,
793793
max_tokens: int | None = None,
794794
trigger: dict[str, Any] | None = None,
795+
id: str | None = None,
795796
):
796797
"""
797798
Create a mental model (runs reflect in background).
@@ -803,6 +804,7 @@ def create_mental_model(
803804
tags: Optional tags for filtering during retrieval
804805
max_tokens: Optional maximum tokens for the mental model content
805806
trigger: Optional trigger settings (e.g., {"refresh_after_consolidation": True})
807+
id: Optional custom ID for the mental model (alphanumeric lowercase with hyphens)
806808
807809
Returns:
808810
CreateMentalModelResponse with operation_id
@@ -814,6 +816,7 @@ def create_mental_model(
814816
trigger_obj = mental_model_trigger.MentalModelTrigger(**trigger)
815817

816818
request_obj = create_mental_model_request.CreateMentalModelRequest(
819+
id=id,
817820
name=name,
818821
source_query=source_query,
819822
tags=tags,

0 commit comments

Comments
 (0)